]> git.madduck.net Git - code/vcsh.git/blob - vcsh

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Revert "t/100-init.t: Fix failure due to locale"
[code/vcsh.git] / vcsh
1 #!/bin/sh
2
3 # This program is licensed under the GNU GPL version 2 or later.
4 # (c) Richard "RichiH" Hartmann <richih@debian.org>, 2011-2015
5 # For details, see LICENSE. To submit patches, you have to agree to
6 # license your code under the GNU GPL version 2 or later.
7
8 # While the following is not legally binding, the author would like to
9 # explain the choice of GPLv2+ over GPLv3+.
10 # The author prefers GPLv3+ over GPLv2+ but feels it's better to maintain
11 # full compatibility's with Git. In case Git ever changes its licensing terms,
12 # which is admittedly extremely unlikely to the point of being impossible,
13 # this software will most likely follow suit.
14
15 # This should always be the first line of code to facilitate debugging
16 [ -n "$VCSH_DEBUG" ] && set -vx
17
18
19 # If '.git-HEAD' is appended to the version, you are seeing an unreleased
20 # version of vcsh; the master branch is supposed to be clean at all times
21 # so you can most likely just use it nonetheless
22 VERSION='1.20141026'
23 SELF=$(basename $0)
24
25 fatal() {
26         echo "$SELF: fatal: $1" >&2
27         [ -z $2 ] && exit 1
28         exit $2
29 }
30
31 # We need to run getops as soon as possible so we catch -d and other
32 # options that will modify our behaviour.
33 # Commands are handled at the end of this script.
34 while getopts "c:dv" flag; do
35         if [ x"$1" = x'-d' ] || [ x"$1" = x'--debug' ]; then
36                 set -vx
37                 VCSH_DEBUG=1
38                 echo "debug mode on"
39                 echo "$SELF $VERSION"
40         elif [ x"$1" = x'-v' ]; then
41                 VCSH_VERBOSE=1
42                 echo "verbose mode on"
43                 echo "$SELF $VERSION"
44         elif [ x"$1" = x'-c' ]; then
45                 VCSH_OPTION_CONFIG=$OPTARG
46         fi
47         shift 1
48 done
49
50 source_all() {
51         # Source file even if it's in $PWD and does not have any slashes in it
52         case $1 in
53                 */*) . "$1";;
54                 *)   . "$PWD/$1";;
55         esac;
56 }
57
58
59 # Read configuration and set defaults if anything's not set
60 [ -n "$VCSH_DEBUG" ]                  && set -vx
61 : ${XDG_CONFIG_HOME:="$HOME/.config"}
62
63 # Read configuration files if there are any
64 [ -r "/etc/vcsh/config" ]             && . "/etc/vcsh/config"
65 [ -r "$XDG_CONFIG_HOME/vcsh/config" ] && . "$XDG_CONFIG_HOME/vcsh/config"
66 if [ -n "$VCSH_OPTION_CONFIG" ]; then
67         # Source $VCSH_OPTION_CONFIG if it can be read and is in $PWD of $PATH
68         if [ -r "$VCSH_OPTION_CONFIG" ]; then
69                 source_all "$VCSH_OPTION_CONFIG"
70         else
71                 fatal "Can not read configuration file '$VCSH_OPTION_CONFIG'" 1
72         fi
73 fi
74 [ -n "$VCSH_DEBUG" ]                  && set -vx
75
76 # Read defaults
77 : ${VCSH_REPO_D:="$XDG_CONFIG_HOME/vcsh/repo.d"}
78 : ${VCSH_HOOK_D:="$XDG_CONFIG_HOME/vcsh/hooks-enabled"}
79 : ${VCSH_OVERLAY_D:="$XDG_CONFIG_HOME/vcsh/overlays-enabled"}
80 : ${VCSH_BASE:="$HOME"}
81 : ${VCSH_GITIGNORE:=exact}
82 : ${VCSH_GITATTRIBUTES:=none}
83 : ${VCSH_WORKTREE:=absolute}
84
85 if [ ! "x$VCSH_GITIGNORE" = 'xexact' ] && [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && [ ! "x$VCSH_GITIGNORE" = 'xrecursive' ]; then
86         fatal "'\$VCSH_GITIGNORE' must equal 'exact', 'none', or 'recursive'" 1
87 fi
88
89 if [ ! "x$VCSH_WORKTREE" = 'xabsolute' ] && [ ! "x$VCSH_WORKTREE" = 'xrelative' ]; then
90         fatal "'\$VCSH_WORKTREE' must equal 'absolute', or 'relative'" 1
91 fi
92
93
94 help() {
95         echo "usage: $SELF <options> <command>
96
97    options:
98    -c <file>            Source file
99    -d                   Enable debug mode
100    -v                   Enable verbose mode
101
102    commands:
103    clone [-b <branch>] \\
104          <remote> \\
105          [<repo>]       Clone from an existing repository
106    commit               Commit in all repositories
107    delete <repo>        Delete an existing repository
108    enter <repo>         Enter repository; spawn new instance of \$SHELL
109    foreach [<-g>]
110      <git command>      Execute a command for every repository
111    help                 Display this help text
112    init <repo>          Initialize a new repository
113    list                 List all repositories
114    list-tracked \\
115         [<repo>]        List all files tracked all or one repositories
116    list-untracked \\
117         [<-a>] [<-r>]
118         [<repo>]        List all files not tracked by all or one repositories
119    pull                 Pull from all vcsh remotes
120    push                 Push to vcsh remotes
121    rename <repo> \\
122           <newname>     Rename repository
123    run <repo> \\
124        <command>        Use this repository
125    status \\
126      [--terse] [<repo>] Show statuses of all/one vcsh repositories
127    upgrade <repo>       Upgrade repository to currently recommended settings
128    version              Print version information
129    which <substring>    Find substring in name of any tracked file
130    write-gitignore \\
131    <repo>               Write .gitignore.d/<repo> via git ls-files
132
133    <repo> <git command> Shortcut to run git commands directly
134    <repo>               Shortcut to enter repository" >&2
135 }
136
137 debug() {
138         [ -n "$VCSH_DEBUG" ] && echo "$SELF: debug: $@"
139 }
140
141 verbose() {
142         if [ -n "$VCSH_DEBUG" ] || [ -n "$VCSH_VERBOSE" ]; then echo "$SELF: verbose: $@"; fi
143 }
144
145 error() {
146         echo "$SELF: error: $1" >&2
147 }
148
149 info() {
150         echo "$SELF: info: $1"
151 }
152
153 clone() {
154         hook pre-clone
155         init
156         git remote add origin "$GIT_REMOTE"
157         git checkout -b "$VCSH_BRANCH" || return $?
158         git config branch."$VCSH_BRANCH".remote origin
159         git config branch."$VCSH_BRANCH".merge  refs/heads/"$VCSH_BRANCH"
160         if [ $(git ls-remote origin "$VCSH_BRANCH" 2> /dev/null | wc -l ) -lt 1 ]; then
161                 info "remote is empty, not merging anything.
162   You should add files to your new repository."
163                 exit
164         fi
165         GIT_VERSION_MAJOR=$(git --version | sed -n 's/.* \([0-9]\)\..*/\1/p' )
166         if [ 1 -lt "$GIT_VERSION_MAJOR" ];then
167                 git fetch origin "$VCSH_BRANCH"
168         else
169                 git fetch origin
170         fi
171         hook pre-merge
172         git ls-tree -r --name-only origin/"$VCSH_BRANCH" | (while read object; do
173                 [ -e "$object" ] &&
174                         error "'$object' exists." &&
175                         VCSH_CONFLICT=1
176         done
177         [ x"$VCSH_CONFLICT" = x'1' ]) &&
178                 fatal "will stop after fetching and not try to merge!
179   Once this situation has been resolved, run 'vcsh $VCSH_REPO_NAME pull' to finish cloning." 17
180         git -c merge.ff=true merge origin/"$VCSH_BRANCH"
181         hook post-merge
182         hook post-clone
183         retire
184         hook post-clone-retired
185 }
186
187 commit() {
188         hook pre-commit
189         for VCSH_REPO_NAME in $(list); do
190                 echo "$VCSH_REPO_NAME: "
191                 GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
192                 use
193                 git commit --untracked-files=no --quiet
194                 VCSH_COMMAND_RETURN_CODE=$?
195                 echo
196         done
197         hook post-commit
198 }
199
200 delete() {
201         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
202         use
203         info "This operation WILL DESTROY DATA!"
204         files=$(git ls-files)
205         echo "These files will be deleted:
206
207 $files
208
209 AGAIN, THIS WILL DELETE YOUR DATA!
210 To continue, type 'Yes, do as I say'"
211         read answer
212         [ "x$answer" = 'xYes, do as I say' ] || exit 16
213         for file in $files; do
214                 rm -f $file || info "could not delete '$file', continuing with deletion"
215         done
216         rm -rf "$GIT_DIR" || error "could not delete '$GIT_DIR'"
217 }
218
219 enter() {
220         hook pre-enter
221         use
222         $SHELL
223         hook post-enter
224 }
225
226 foreach() {
227         hook pre-foreach
228
229         # We default to prefixing `git` to all commands passed to foreach, but
230         # allow running in general context with -g
231         command_prefix=git
232         while getopts "g" flag; do
233                 if [ x"$1" = x'-g' ]; then
234                         unset command_prefix
235                 fi
236                 shift 1
237         done
238         for VCSH_REPO_NAME in $(list); do
239                 echo "$VCSH_REPO_NAME:"
240                 GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
241                 use
242                 $command_prefix "$@"
243         done
244         hook post-foreach
245 }
246
247 git_dir_exists() {
248         [ -d "$GIT_DIR" ] || fatal "no repository found for '$VCSH_REPO_NAME'" 12
249 }
250
251 hook() {
252         for hook in "$VCSH_HOOK_D/$1"* "$VCSH_HOOK_D/$VCSH_REPO_NAME.$1"*; do
253                 [ -x "$hook" ] || continue
254                 verbose "executing '$hook'"
255                 "$hook"
256         done
257 }
258
259 init() {
260         hook pre-init
261         [ ! -e "$GIT_DIR" ] || fatal "'$GIT_DIR' exists" 10
262         mkdir -p "$VCSH_BASE" || fatal "could not create '$VCSH_BASE'" 50
263         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
264         git init --shared=0600
265         upgrade
266         hook post-init
267 }
268
269 list() {
270         for repo in "$VCSH_REPO_D"/*.git; do
271                 [ -d "$repo" ] && [ -r "$repo" ] && echo "$(basename "$repo" .git)"
272         done
273 }
274
275 get_files() {
276         GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
277         git ls-files
278 }
279
280 list_tracked() {
281         VCSH_REPO_NAME=$2; export VCSH_REPO_NAME
282         if [ -n "$VCSH_REPO_NAME" ]; then
283                 get_files | list_tracked_helper
284         else
285                 for VCSH_REPO_NAME in $(list); do
286                         get_files
287                 done | list_tracked_helper
288         fi
289 }
290
291 list_tracked_helper() {
292         sed "s,^,$(printf '%s\n' "$VCSH_BASE/" | sed 's/[,\&]/\\&/g')," | sort -u
293 }
294
295 list_tracked_by() {
296         list_tracked '' $2
297 }
298
299 list_untracked() {
300         command -v 'comm' >/dev/null 2>&1 || fatal "Could not find 'comm'"
301
302         temp_file_others=$(mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX") || fatal 'Could not create temp file'
303         temp_file_untracked=$(mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX") || fatal 'Could not create temp file'
304         temp_file_untracked_copy=$(mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX") || fatal 'Could not create temp file'
305
306         # Hack in support for `vcsh list-untracked -r`...
307         exclude_standard_opt='--exclude-standard'
308         directory_opt="--directory"
309         shift 1
310         while getopts "ar" flag; do
311                 if [ x"$1" = x'-a' ]; then
312                         unset exclude_standard_opt
313                 elif [ x"$1" = x'-r' ]; then
314                         unset directory_opt
315                 fi
316                 shift 1
317         done
318         # ...and parse for a potential parameter afterwards. As we shifted things out of $* in during getops, we need to look at $1
319         VCSH_REPO_NAME=$1; export VCSH_REPO_NAME
320
321         if [ -n "$VCSH_REPO_NAME" ]; then
322                 list_untracked_helper $VCSH_REPO_NAME
323         else
324                 for VCSH_REPO_NAME in $(list); do
325                         list_untracked_helper $VCSH_REPO_NAME
326                 done
327         fi
328         cat $temp_file_untracked
329
330         unset directory_opt directory_component
331         rm -f $temp_file_others $temp_file_untracked $temp_file_untracked_copy || fatal 'Could not delete temp files'
332 }
333
334 list_untracked_helper() {
335         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
336         git ls-files --others $exclude_standard_opt "$directory_opt" | (
337                 while read line; do
338                         echo "$line"
339                         directory_component=${line%%/*}
340                         [ -d "$directory_component" ] && printf '%s/\n' "$directory_component"
341                 done
342                 ) | sort -u > $temp_file_others
343         if [ -z "$ran_once" ]; then
344                 ran_once=1
345                 cp $temp_file_others $temp_file_untracked || fatal 'Could not copy temp file'
346         fi
347         cp $temp_file_untracked $temp_file_untracked_copy || fatal 'Could not copy temp file'
348         comm -12 $temp_file_others $temp_file_untracked_copy > $temp_file_untracked
349 }
350
351 pull() {
352         hook pre-pull
353         for VCSH_REPO_NAME in $(list); do
354                 printf '%s: ' "$VCSH_REPO_NAME"
355                 GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
356                 use
357                 git pull
358                 VCSH_COMMAND_RETURN_CODE=$?
359                 echo
360         done
361         hook post-pull
362 }
363
364 push() {
365         hook pre-push
366         for VCSH_REPO_NAME in $(list); do
367                 printf '%s: ' "$VCSH_REPO_NAME"
368                 GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
369                 use
370                 git push
371                 VCSH_COMMAND_RETURN_CODE=$?
372                 echo
373         done
374         hook post-push
375 }
376
377 retire() {
378         unset VCSH_DIRECTORY
379 }
380
381 rename() {
382         git_dir_exists
383         [ -d "$GIT_DIR_NEW" ] && fatal "'$GIT_DIR_NEW' exists" 54
384         mv -f "$GIT_DIR" "$GIT_DIR_NEW" || fatal "Could not mv '$GIT_DIR' '$GIT_DIR_NEW'" 52
385
386         # Now that the repository has been renamed, we need to fix up its configuration
387         # Overwrite old name..
388         GIT_DIR=$GIT_DIR_NEW
389         VCSH_REPO_NAME=$VCSH_REPO_NAME_NEW
390         # ..and clobber all old configuration
391         upgrade
392 }
393
394 run() {
395         hook pre-run
396         use
397         "$@"
398         VCSH_COMMAND_RETURN_CODE=$?
399         hook post-run
400 }
401
402 status() {
403         if [ -t 1 ]; then
404                 COLORING="-c color.status=always"
405         fi
406         if [ -n "$VCSH_REPO_NAME" ]; then
407                 status_helper $VCSH_REPO_NAME
408         else
409                 for VCSH_REPO_NAME in $(list); do
410                         STATUS=$(status_helper $VCSH_REPO_NAME "$COLORING")
411                         [ -n "$STATUS" -o -z "$VCSH_STATUS_TERSE" ] && echo "$VCSH_REPO_NAME:"
412                         [ -n "$STATUS" ]            && echo "$STATUS"
413                         [ -z "$VCSH_STATUS_TERSE" ] && echo
414                 done
415         fi
416 }
417
418 status_helper() {
419         GIT_DIR=$VCSH_REPO_D/$1.git; export GIT_DIR
420         VCSH_GIT_OPTIONS=$2
421         use
422         remote_tracking_branch=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2> /dev/null) && {
423                 commits_behind=$(git log ..${remote_tracking_branch} --oneline | wc -l)
424                 commits_ahead=$(git log ${remote_tracking_branch}.. --oneline | wc -l)
425                 [ ${commits_behind} -ne 0 ] && echo "Behind $remote_tracking_branch by $commits_behind commits"
426                 [ ${commits_ahead} -ne 0 ] && echo "Ahead of $remote_tracking_branch by $commits_ahead commits"
427         }
428         git ${VCSH_GIT_OPTIONS} status --short --untracked-files='no'
429         VCSH_COMMAND_RETURN_CODE=$?
430 }
431
432 upgrade() {
433         hook pre-upgrade
434         # fake-bare repositories are not bare, actually. Set this to false
435         # because otherwise Git complains "fatal: core.bare and core.worktree
436         # do not make sense"
437         git config core.bare false
438         # core.worktree may be absolute or relative to $GIT_DIR, depending on
439         # user preference
440         if [ ! "x$VCSH_WORKTREE" = 'xabsolute' ]; then
441                 git config core.worktree "$(cd "$GIT_DIR" && GIT_WORK_TREE=$VCSH_BASE git rev-parse --show-cdup)"
442         elif [ ! "x$VCSH_WORKTREE" = 'xrelative' ]; then
443                 git config core.worktree "$VCSH_BASE"
444         fi
445         [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && git config core.excludesfile ".gitignore.d/$VCSH_REPO_NAME"
446         [ ! "x$VCSH_GITATTRIBUTES" = 'xnone' ] && git config core.attributesfile ".gitattributes.d/$VCSH_REPO_NAME"
447         git config vcsh.vcsh 'true'
448         use
449         [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ] && git add -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME"
450         [ -e "$VCSH_BASE/.gitattributes.d/$VCSH_REPO_NAME" ] && git add -f "$VCSH_BASE/.gitattributes.d/$VCSH_REPO_NAME"
451         hook post-upgrade
452 }
453
454 use() {
455         git_dir_exists
456         VCSH_DIRECTORY=$VCSH_REPO_NAME; export VCSH_DIRECTORY
457 }
458
459 which() {
460         output=$(for VCSH_REPO_NAME in $(list); do
461                 get_files | grep -- "$VCSH_COMMAND_PARAMETER" | sed "s/^/$VCSH_REPO_NAME: /"
462         done | sort -u)
463         if [ -z "$output" ]; then
464                 fatal "'$VCSH_COMMAND_PARAMETER' does not exist" 1
465         else
466                 echo "$output"
467         fi
468 }
469
470 write_gitignore() {
471         # Don't do anything if the user does not want to write gitignore
472         if [ "x$VCSH_GITIGNORE" = 'xnone' ]; then
473                 info "Not writing gitignore as '\$VCSH_GITIGNORE' is set to 'none'"
474                 exit
475         fi
476
477         use
478         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
479         OLDIFS=$IFS
480         IFS=$(printf '\n\t')
481         gitignores=$(for file in $(git ls-files); do
482                 while true; do
483                         echo "$file"; new=${file%/*}
484                         [ x"$file" = x"$new" ] && break
485                         file=$new
486                 done;
487         done | sort -u)
488
489         # Contrary to GNU mktemp, mktemp on BSD/OSX requires a template for temp files
490         # Using a template makes GNU mktemp default to $PWD and not #TMPDIR for tempfile location
491         # To make every OS happy, set full path explicitly
492         tempfile=$(mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX") || fatal "could not create tempfile: '${tempfile}'" 51
493
494         echo '*' > "$tempfile" || fatal "could not write to '$tempfile'" 57
495         for gitignore in $gitignores; do
496                 echo "$gitignore" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57
497                 if [ "x$VCSH_GITIGNORE" = 'xrecursive' ] && [ -d "$gitignore" ]; then
498                         { echo "$gitignore/*" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57; }
499                 fi
500         done
501         IFS=$OLDIFS
502         if diff -N "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" > /dev/null; then
503                 rm -f "$tempfile" || error "could not delete '$tempfile'"
504                 exit
505         fi
506         if [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ]; then
507                 info "'$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' differs from new data, moving it to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'"
508                 mv -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak" ||
509                         fatal "could not move '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'" 53
510         fi
511         mv -f "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ||
512                 fatal "could not move '$tempfile' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME'" 53
513 }
514
515 debug $(git version)
516
517 if [ ! "x$VCSH_GITIGNORE" = 'xexact' ] && [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && [ ! "x$VCSH_GITIGNORE" = 'xrecursive' ]; then
518         fatal "'\$VCSH_GITIGNORE' must equal 'exact', 'none', or 'recursive'" 1
519 fi
520
521 VCSH_COMMAND=$1; export VCSH_COMMAND
522
523 case $VCSH_COMMAND in
524         clon|clo|cl) VCSH_COMMAND=clone;;
525         commi|comm|com|co) VCSH_COMMAND=commit;;
526         delet|dele|del|de) VCSH_COMMAND=delete;;
527         ente|ent|en) VCSH_COMMAND=enter;;
528         hel|he) VCSH_COMMAND=help;;
529         ini|in) VCSH_COMMAND=init;;
530         pul) VCSH_COMMAND=pull;;
531         pus) VCSH_COMMAND=push;;
532         renam|rena|ren|re) VCSH_COMMAND=rename;;
533         ru) VCSH_COMMAND=run;;
534         statu|stat|sta|st) VCSH_COMMAND=status;;
535         upgrad|upgra|upgr|up) VCSH_COMMAND=upgrade;;
536         versio|versi|vers|ver|ve) VCSH_COMMAND=version;;
537         which|whi|wh) VCSH_COMMAND=which;;
538         write|writ|wri|wr) VCSH_COMMAND=write-gitignore;;
539 esac    
540
541 if [ x"$VCSH_COMMAND" = x'clone' ]; then
542         VCSH_BRANCH=
543         if [ "$2" = -b ]; then
544                 VCSH_BRANCH=$3
545                 shift
546                 shift
547         fi
548         [ -z "$2" ] && fatal "$VCSH_COMMAND: please specify a remote" 1
549         GIT_REMOTE="$2"
550         [ -n "$VCSH_BRANCH" ] || if [ "$3" = -b ]; then
551                 VCSH_BRANCH=$4
552                 shift
553                 shift
554         fi
555         if [ -n "$3" ]; then
556                 VCSH_REPO_NAME=$3
557                 [ -z "$VCSH_BRANCH" ] && [ "$4" = -b ] && VCSH_BRANCH=$5
558         else
559                 VCSH_REPO_NAME=$(basename "${GIT_REMOTE#*:}" .git)
560         fi
561         [ -z "$VCSH_REPO_NAME" ] && fatal "$VCSH_COMMAND: could not determine repository name" 1
562         export VCSH_REPO_NAME
563         [ -n "$VCSH_BRANCH" ] || VCSH_BRANCH=master
564         GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
565 elif [ "$VCSH_COMMAND" = 'version' ]; then
566         echo "$SELF $VERSION"
567         git version
568         exit
569 elif [ x"$VCSH_COMMAND" = x'which' ]; then
570         [ -z "$2" ] && fatal "$VCSH_COMMAND: please specify a filename" 1
571         [ -n "$3" ] && fatal "$VCSH_COMMAND: too many parameters" 1
572         VCSH_COMMAND_PARAMETER=$2; export VCSH_COMMAND_PARAMETER
573 elif [ x"$VCSH_COMMAND" = x'delete' ]           ||
574      [ x"$VCSH_COMMAND" = x'enter' ]            ||
575      [ x"$VCSH_COMMAND" = x'init' ]             ||
576      [ x"$VCSH_COMMAND" = x'list-tracked-by' ]  ||
577      [ x"$VCSH_COMMAND" = x'rename' ]           ||
578      [ x"$VCSH_COMMAND" = x'run' ]              ||
579      [ x"$VCSH_COMMAND" = x'upgrade' ]          ||
580      [ x"$VCSH_COMMAND" = x'write-gitignore' ]; then
581         [ -z "$2" ]                                     && fatal "$VCSH_COMMAND: please specify repository to work on" 1
582         [ x"$VCSH_COMMAND" = x'rename' ] && [ -z "$3" ] && fatal "$VCSH_COMMAND: please specify a target name" 1
583         [ x"$VCSH_COMMAND" = x'run'    ] && [ -z "$3" ] && fatal "$VCSH_COMMAND: please specify a command" 1
584         VCSH_REPO_NAME=$2; export VCSH_REPO_NAME
585         GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
586         [ x"$VCSH_COMMAND" = x'rename' ] && { VCSH_REPO_NAME_NEW=$3; export VCSH_REPO_NAME_NEW;
587                                               GIT_DIR_NEW=$VCSH_REPO_D/$VCSH_REPO_NAME_NEW.git; export GIT_DIR_NEW; }
588         [ x"$VCSH_COMMAND" = x'run' ]    && shift 2
589 elif [ x"$VCSH_COMMAND" = x'foreach' ]; then
590         [ -z "$2" ] && fatal "$VCSH_COMMAND: please specify a command" 1
591         shift 1
592 elif [ x"$VCSH_COMMAND" = x'commit' ] ||
593      [ x"$VCSH_COMMAND" = x'list'   ] ||
594      [ x"$VCSH_COMMAND" = x'list-tracked' ] ||
595      [ x"$VCSH_COMMAND" = x'list-untracked' ] ||
596      [ x"$VCSH_COMMAND" = x'pull'   ] ||
597      [ x"$VCSH_COMMAND" = x'push'   ]; then
598         :
599 elif [ x"$VCSH_COMMAND" = x'status' ]; then
600         if [ x"$2" = x'--terse' ]; then
601                 VCSH_STATUS_TERSE=1; export VCSH_STATUS_TERSE
602                 shift
603         fi
604         VCSH_REPO_NAME=$2; export VCSH_REPO_NAME
605 elif [ -n "$2" ]; then
606         VCSH_COMMAND='run'; export VCSH_COMMAND
607         VCSH_REPO_NAME=$1; export VCSH_REPO_NAME
608         GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
609         [ -d "$GIT_DIR" ] || { help; exit 1; }
610         shift 1
611         set -- "git" "$@"
612 elif [ -n "$VCSH_COMMAND" ]; then
613         VCSH_COMMAND='enter'; export VCSH_COMMAND
614         VCSH_REPO_NAME=$1; export VCSH_REPO_NAME
615         GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
616         [ -d "$GIT_DIR" ] || { help; exit 1; }
617 else
618         # $1 is empty, or 'help'
619         help && exit
620 fi
621
622 # Did we receive a directory instead of a name?
623 # Mangle the input to fit normal operation.
624 if echo "$VCSH_REPO_NAME" | grep -q '/'; then
625         GIT_DIR=$VCSH_REPO_NAME; export GIT_DIR
626         VCSH_REPO_NAME=$(basename "$VCSH_REPO_NAME" .git); export VCSH_REPO_NAME
627 fi
628
629 check_dir() {
630         check_directory="$1"
631         if [ ! -d "$check_directory" ]; then
632                 if [ -e "$check_directory" ]; then
633                         fatal "'$check_directory' exists but is not a directory" 13
634                 else
635                         verbose "attempting to create '$check_directory'"
636                         mkdir -p "$check_directory" || fatal "could not create '$check_directory'" 50
637                 fi
638         fi
639 }
640
641 check_dir "$VCSH_REPO_D"
642 [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && check_dir "$VCSH_BASE/.gitignore.d"
643 [ ! "x$VCSH_GITATTRIBUTES" = 'xnone' ] && check_dir "$VCSH_BASE/.gitattributes.d"
644
645 verbose "$VCSH_COMMAND begin"
646 VCSH_COMMAND=$(echo "$VCSH_COMMAND" | sed 's/-/_/g'); export VCSH_COMMAND
647
648 # Source repo-specific configuration file
649 [ -r "$XDG_CONFIG_HOME/vcsh/config.d/$VCSH_REPO_NAME" ] && . "$XDG_CONFIG_HOME/vcsh/config.d/$VCSH_REPO_NAME"
650
651 # source overlay functions
652 for overlay in "$VCSH_OVERLAY_D/$VCSH_COMMAND"* "$VCSH_OVERLAY_D/$VCSH_REPO_NAME.$VCSH_COMMAND"*; do
653         [ -r "$overlay" ] || continue
654         info "sourcing '$overlay'"
655         . "$overlay"
656 done
657
658 hook pre-command
659 $VCSH_COMMAND "$@"
660 hook post-command
661 verbose "$VCSH_COMMAND end, exiting"
662 exit $VCSH_COMMAND_RETURN_CODE