]> 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:

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