]> 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 #233 from danielshahaf/bugfix/list-tracked-in-subdir-v1
[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         shift  # remove the "commit" command.
194         for VCSH_REPO_NAME in $(list); do
195                 echo "$VCSH_REPO_NAME: "
196                 GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
197                 use
198                 git commit --untracked-files=no --quiet "$@"
199                 VCSH_COMMAND_RETURN_CODE=$?
200                 echo
201         done
202         hook post-commit
203 }
204
205 delete() {
206         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
207         use
208         info "This operation WILL DESTROY DATA!"
209         files=$(git ls-files)
210         echo "These files will be deleted:
211
212 $files
213
214 AGAIN, THIS WILL DELETE YOUR DATA!
215 To continue, type 'Yes, do as I say'"
216         read answer
217         [ "x$answer" = 'xYes, do as I say' ] || exit 16
218         for file in $files; do
219                 rm -f $file || info "could not delete '$file', continuing with deletion"
220         done
221         rm -rf "$GIT_DIR" || error "could not delete '$GIT_DIR'"
222 }
223
224 enter() {
225         hook pre-enter
226         use
227         $SHELL
228         hook post-enter
229 }
230
231 foreach() {
232         hook pre-foreach
233
234         # We default to prefixing `git` to all commands passed to foreach, but
235         # allow running in general context with -g
236         command_prefix=git
237         while getopts "g" flag; do
238                 if [ x"$1" = x'-g' ]; then
239                         unset command_prefix
240                 fi
241                 shift 1
242         done
243         for VCSH_REPO_NAME in $(list); do
244                 echo "$VCSH_REPO_NAME:"
245                 GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
246                 use
247                 $command_prefix "$@"
248         done
249         hook post-foreach
250 }
251
252 git_dir_exists() {
253         [ -d "$GIT_DIR" ] || fatal "no repository found for '$VCSH_REPO_NAME'" 12
254 }
255
256 hook() {
257         for hook in "$VCSH_HOOK_D/$1"* "$VCSH_HOOK_D/$VCSH_REPO_NAME.$1"*; do
258                 [ -x "$hook" ] || continue
259                 verbose "executing '$hook'"
260                 "$hook"
261         done
262 }
263
264 init() {
265         hook pre-init
266         [ ! -e "$GIT_DIR" ] || fatal "'$GIT_DIR' exists" 10
267         mkdir -p "$VCSH_BASE" || fatal "could not create '$VCSH_BASE'" 50
268         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
269         git init --shared=false
270         upgrade
271         hook post-init
272 }
273
274 list() {
275         for repo in "$VCSH_REPO_D"/*.git; do
276                 [ -d "$repo" ] && [ -r "$repo" ] && echo "$(basename "$repo" .git)"
277         done
278 }
279
280 get_files() {
281         GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
282         git ls-files --full-name
283 }
284
285 list_tracked() {
286         VCSH_REPO_NAME=$2; export VCSH_REPO_NAME
287         if [ -n "$VCSH_REPO_NAME" ]; then
288                 get_files | list_tracked_helper
289         else
290                 for VCSH_REPO_NAME in $(list); do
291                         get_files
292                 done | list_tracked_helper
293         fi
294 }
295
296 list_tracked_helper() {
297         sed "s,^,$(printf '%s\n' "$VCSH_BASE/" | sed 's/[,\&]/\\&/g')," | sort -u
298 }
299
300 list_tracked_by() {
301         list_tracked '' $2
302 }
303
304 list_untracked() {
305         command -v 'comm' >/dev/null 2>&1 || fatal "Could not find 'comm'"
306
307         temp_file_others=$(mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX") || fatal 'Could not create temp file'
308         temp_file_untracked=$(mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX") || fatal 'Could not create temp file'
309         temp_file_untracked_copy=$(mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX") || fatal 'Could not create temp file'
310
311         # Hack in support for `vcsh list-untracked -r`...
312         exclude_standard_opt='--exclude-standard'
313         directory_opt="--directory"
314         shift 1
315         while getopts "ar" flag; do
316                 if [ x"$1" = x'-a' ]; then
317                         unset exclude_standard_opt
318                 elif [ x"$1" = x'-r' ]; then
319                         unset directory_opt
320                 fi
321                 shift 1
322         done
323         # ...and parse for a potential parameter afterwards. As we shifted things out of $* in during getops, we need to look at $1
324         VCSH_REPO_NAME=$1; export VCSH_REPO_NAME
325
326         if [ -n "$VCSH_REPO_NAME" ]; then
327                 list_untracked_helper $VCSH_REPO_NAME
328         else
329                 for VCSH_REPO_NAME in $(list); do
330                         list_untracked_helper $VCSH_REPO_NAME
331                 done
332         fi
333         cat $temp_file_untracked
334
335         unset directory_opt directory_component
336         rm -f $temp_file_others $temp_file_untracked $temp_file_untracked_copy || fatal 'Could not delete temp files'
337 }
338
339 list_untracked_helper() {
340         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
341         git ls-files --others $exclude_standard_opt "$directory_opt" | (
342                 while read line; do
343                         echo "$line"
344                         directory_component=${line%%/*}
345                         [ -d "$directory_component" ] && printf '%s/\n' "$directory_component"
346                 done
347                 ) | sort -u > $temp_file_others
348         if [ -z "$ran_once" ]; then
349                 ran_once=1
350                 cp $temp_file_others $temp_file_untracked || fatal 'Could not copy temp file'
351         fi
352         cp $temp_file_untracked $temp_file_untracked_copy || fatal 'Could not copy temp file'
353         comm -12 $temp_file_others $temp_file_untracked_copy > $temp_file_untracked
354 }
355
356 pull() {
357         hook pre-pull
358         for VCSH_REPO_NAME in $(list); do
359                 printf '%s: ' "$VCSH_REPO_NAME"
360                 GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
361                 use
362                 git pull
363                 VCSH_COMMAND_RETURN_CODE=$?
364                 echo
365         done
366         hook post-pull
367 }
368
369 push() {
370         hook pre-push
371         for VCSH_REPO_NAME in $(list); do
372                 printf '%s: ' "$VCSH_REPO_NAME"
373                 GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
374                 use
375                 git push
376                 VCSH_COMMAND_RETURN_CODE=$?
377                 echo
378         done
379         hook post-push
380 }
381
382 retire() {
383         unset VCSH_DIRECTORY
384 }
385
386 rename() {
387         git_dir_exists
388         [ -d "$GIT_DIR_NEW" ] && fatal "'$GIT_DIR_NEW' exists" 54
389         mv -f "$GIT_DIR" "$GIT_DIR_NEW" || fatal "Could not mv '$GIT_DIR' '$GIT_DIR_NEW'" 52
390
391         # Now that the repository has been renamed, we need to fix up its configuration
392         # Overwrite old name..
393         GIT_DIR=$GIT_DIR_NEW
394         VCSH_REPO_NAME=$VCSH_REPO_NAME_NEW
395         # ..and clobber all old configuration
396         upgrade
397 }
398
399 run() {
400         hook pre-run
401         use
402         "$@"
403         VCSH_COMMAND_RETURN_CODE=$?
404         hook post-run
405 }
406
407 status() {
408         if [ -t 1 ]; then
409                 COLORING="-c color.status=always"
410         fi
411         if [ -n "$VCSH_REPO_NAME" ]; then
412                 status_helper $VCSH_REPO_NAME
413         else
414                 for VCSH_REPO_NAME in $(list); do
415                         STATUS=$(status_helper $VCSH_REPO_NAME "$COLORING")
416                         [ -n "$STATUS" -o -z "$VCSH_STATUS_TERSE" ] && echo "$VCSH_REPO_NAME:"
417                         [ -n "$STATUS" ]            && echo "$STATUS"
418                         [ -z "$VCSH_STATUS_TERSE" ] && echo
419                 done
420         fi
421 }
422
423 status_helper() {
424         GIT_DIR=$VCSH_REPO_D/$1.git; export GIT_DIR
425         VCSH_GIT_OPTIONS=$2
426         use
427         remote_tracking_branch=$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2> /dev/null) && {
428                 commits_behind=$(git log ..${remote_tracking_branch} --oneline | wc -l)
429                 commits_ahead=$(git log ${remote_tracking_branch}.. --oneline | wc -l)
430                 [ ${commits_behind} -ne 0 ] && echo "Behind $remote_tracking_branch by $commits_behind commits"
431                 [ ${commits_ahead} -ne 0 ] && echo "Ahead of $remote_tracking_branch by $commits_ahead commits"
432         }
433         git ${VCSH_GIT_OPTIONS} status --short --untracked-files='no'
434         VCSH_COMMAND_RETURN_CODE=$?
435 }
436
437 upgrade() {
438         hook pre-upgrade
439         # fake-bare repositories are not bare, actually. Set this to false
440         # because otherwise Git complains "fatal: core.bare and core.worktree
441         # do not make sense"
442         git config core.bare false
443         # core.worktree may be absolute or relative to $GIT_DIR, depending on
444         # user preference
445         if [ ! "x$VCSH_WORKTREE" = 'xabsolute' ]; then
446                 git config core.worktree "$(cd "$GIT_DIR" && GIT_WORK_TREE=$VCSH_BASE git rev-parse --show-cdup)"
447         elif [ ! "x$VCSH_WORKTREE" = 'xrelative' ]; then
448                 git config core.worktree "$VCSH_BASE"
449         fi
450         [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && git config core.excludesfile ".gitignore.d/$VCSH_REPO_NAME"
451         [ ! "x$VCSH_GITATTRIBUTES" = 'xnone' ] && git config core.attributesfile ".gitattributes.d/$VCSH_REPO_NAME"
452         git config vcsh.vcsh 'true'
453         use
454         [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ] && git add -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME"
455         [ -e "$VCSH_BASE/.gitattributes.d/$VCSH_REPO_NAME" ] && git add -f "$VCSH_BASE/.gitattributes.d/$VCSH_REPO_NAME"
456         hook post-upgrade
457 }
458
459 use() {
460         git_dir_exists
461         VCSH_DIRECTORY=$VCSH_REPO_NAME; export VCSH_DIRECTORY
462 }
463
464 which() {
465         output=$(for VCSH_REPO_NAME in $(list); do
466                 get_files | grep -- "$VCSH_COMMAND_PARAMETER" | sed "s/^/$VCSH_REPO_NAME: /"
467         done | sort -u)
468         if [ -z "$output" ]; then
469                 fatal "'$VCSH_COMMAND_PARAMETER' does not exist" 1
470         else
471                 echo "$output"
472         fi
473 }
474
475 write_gitignore() {
476         # Don't do anything if the user does not want to write gitignore
477         if [ "x$VCSH_GITIGNORE" = 'xnone' ]; then
478                 info "Not writing gitignore as '\$VCSH_GITIGNORE' is set to 'none'"
479                 exit
480         fi
481
482         use
483         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
484         local GIT_VERSION="$(git --version)"
485         local GIT_VERSION_MAJOR=$(echo $GIT_VERSION | sed -n 's/.* \([0-9]\+\)\..*/\1/p')
486         local GIT_VERSION_MINOR=$(echo $GIT_VERSION | sed -n 's/.* \([0-9]\+\)\.\([0-9]\+\)\..*/\2/p')
487         OLDIFS=$IFS
488         IFS=$(printf '\n\t')
489         gitignores=$(for file in $(git ls-files); do
490                 if [ $GIT_VERSION_MAJOR -ge 2 -a $GIT_VERSION_MINOR -ge 7 ]; then
491                         echo "$file";
492                 else
493                         while true; do
494                                 echo "$file"; new=${file%/*}
495                                 [ x"$file" = x"$new" ] && break
496                                 file=$new
497                         done;
498                 fi
499         done | sort -u)
500
501         # Contrary to GNU mktemp, mktemp on BSD/OSX requires a template for temp files
502         # Using a template makes GNU mktemp default to $PWD and not #TMPDIR for tempfile location
503         # To make every OS happy, set full path explicitly
504         tempfile=$(mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX") || fatal "could not create tempfile: '${tempfile}'" 51
505
506         echo '*' > "$tempfile" || fatal "could not write to '$tempfile'" 57
507         for gitignore in $gitignores; do
508                 echo "$gitignore" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57
509                 if [ "x$VCSH_GITIGNORE" = 'xrecursive' ] && [ -d "$gitignore" ]; then
510                         { echo "$gitignore/*" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57; }
511                 fi
512         done
513         IFS=$OLDIFS
514         if diff -N "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" > /dev/null; then
515                 rm -f "$tempfile" || error "could not delete '$tempfile'"
516                 exit
517         fi
518         if [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ]; then
519                 info "'$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' differs from new data, moving it to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'"
520                 mv -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak" ||
521                         fatal "could not move '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'" 53
522         fi
523         mv -f "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ||
524                 fatal "could not move '$tempfile' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME'" 53
525 }
526
527 debug $(git version)
528
529 if [ ! "x$VCSH_GITIGNORE" = 'xexact' ] && [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && [ ! "x$VCSH_GITIGNORE" = 'xrecursive' ]; then
530         fatal "'\$VCSH_GITIGNORE' must equal 'exact', 'none', or 'recursive'" 1
531 fi
532
533 VCSH_COMMAND=$1; export VCSH_COMMAND
534
535 case $VCSH_COMMAND in
536         clon|clo|cl) VCSH_COMMAND=clone;;
537         commi|comm|com|co) VCSH_COMMAND=commit;;
538         delet|dele|del|de) VCSH_COMMAND=delete;;
539         ente|ent|en) VCSH_COMMAND=enter;;
540         hel|he) VCSH_COMMAND=help;;
541         ini|in) VCSH_COMMAND=init;;
542         pul) VCSH_COMMAND=pull;;
543         pus) VCSH_COMMAND=push;;
544         renam|rena|ren|re) VCSH_COMMAND=rename;;
545         ru) VCSH_COMMAND=run;;
546         statu|stat|sta|st) VCSH_COMMAND=status;;
547         upgrad|upgra|upgr|up) VCSH_COMMAND=upgrade;;
548         versio|versi|vers|ver|ve) VCSH_COMMAND=version;;
549         which|whi|wh) VCSH_COMMAND=which;;
550         write|writ|wri|wr) VCSH_COMMAND=write-gitignore;;
551 esac    
552
553 if [ x"$VCSH_COMMAND" = x'clone' ]; then
554         VCSH_BRANCH=
555         if [ "$2" = -b ]; then
556                 VCSH_BRANCH=$3
557                 shift
558                 shift
559         fi
560         [ -z "$2" ] && fatal "$VCSH_COMMAND: please specify a remote" 1
561         GIT_REMOTE="$2"
562         [ -n "$VCSH_BRANCH" ] || if [ "$3" = -b ]; then
563                 VCSH_BRANCH=$4
564                 shift
565                 shift
566         fi
567         if [ -n "$3" ]; then
568                 VCSH_REPO_NAME=$3
569                 [ -z "$VCSH_BRANCH" ] && [ "$4" = -b ] && VCSH_BRANCH=$5
570         else
571                 VCSH_REPO_NAME=$(basename "${GIT_REMOTE#*:}" .git)
572         fi
573         [ -z "$VCSH_REPO_NAME" ] && fatal "$VCSH_COMMAND: could not determine repository name" 1
574         export VCSH_REPO_NAME
575         [ -n "$VCSH_BRANCH" ] || VCSH_BRANCH=master
576         GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
577 elif [ "$VCSH_COMMAND" = 'version' ]; then
578         echo "$SELF $VERSION"
579         git version
580         exit
581 elif [ x"$VCSH_COMMAND" = x'which' ]; then
582         [ -z "$2" ] && fatal "$VCSH_COMMAND: please specify a filename" 1
583         [ -n "$3" ] && fatal "$VCSH_COMMAND: too many parameters" 1
584         VCSH_COMMAND_PARAMETER=$2; export VCSH_COMMAND_PARAMETER
585 elif [ x"$VCSH_COMMAND" = x'delete' ]           ||
586      [ x"$VCSH_COMMAND" = x'enter' ]            ||
587      [ x"$VCSH_COMMAND" = x'init' ]             ||
588      [ x"$VCSH_COMMAND" = x'list-tracked-by' ]  ||
589      [ x"$VCSH_COMMAND" = x'rename' ]           ||
590      [ x"$VCSH_COMMAND" = x'run' ]              ||
591      [ x"$VCSH_COMMAND" = x'upgrade' ]          ||
592      [ x"$VCSH_COMMAND" = x'write-gitignore' ]; then
593         [ -z "$2" ]                                     && fatal "$VCSH_COMMAND: please specify repository to work on" 1
594         [ x"$VCSH_COMMAND" = x'rename' ] && [ -z "$3" ] && fatal "$VCSH_COMMAND: please specify a target name" 1
595         [ x"$VCSH_COMMAND" = x'run'    ] && [ -z "$3" ] && fatal "$VCSH_COMMAND: please specify a command" 1
596         VCSH_REPO_NAME=$2; export VCSH_REPO_NAME
597         GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
598         [ x"$VCSH_COMMAND" = x'rename' ] && { VCSH_REPO_NAME_NEW=$3; export VCSH_REPO_NAME_NEW;
599                                               GIT_DIR_NEW=$VCSH_REPO_D/$VCSH_REPO_NAME_NEW.git; export GIT_DIR_NEW; }
600         [ x"$VCSH_COMMAND" = x'run' ]    && shift 2
601 elif [ x"$VCSH_COMMAND" = x'foreach' ]; then
602         [ -z "$2" ] && fatal "$VCSH_COMMAND: please specify a command" 1
603         shift 1
604 elif [ x"$VCSH_COMMAND" = x'commit' ] ||
605      [ x"$VCSH_COMMAND" = x'list'   ] ||
606      [ x"$VCSH_COMMAND" = x'list-tracked' ] ||
607      [ x"$VCSH_COMMAND" = x'list-untracked' ] ||
608      [ x"$VCSH_COMMAND" = x'pull'   ] ||
609      [ x"$VCSH_COMMAND" = x'push'   ]; then
610         :
611 elif [ x"$VCSH_COMMAND" = x'status' ]; then
612         if [ x"$2" = x'--terse' ]; then
613                 VCSH_STATUS_TERSE=1; export VCSH_STATUS_TERSE
614                 shift
615         fi
616         VCSH_REPO_NAME=$2; export VCSH_REPO_NAME
617 elif [ -n "$2" ]; then
618         VCSH_COMMAND='run'; export VCSH_COMMAND
619         VCSH_REPO_NAME=$1; export VCSH_REPO_NAME
620         GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
621         [ -d "$GIT_DIR" ] || { help; exit 1; }
622         shift 1
623         set -- "git" "$@"
624 elif [ -n "$VCSH_COMMAND" ]; then
625         VCSH_COMMAND='enter'; export VCSH_COMMAND
626         VCSH_REPO_NAME=$1; export VCSH_REPO_NAME
627         GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
628         [ -d "$GIT_DIR" ] || { help; exit 1; }
629 else
630         # $1 is empty, or 'help'
631         help && exit
632 fi
633
634 # Did we receive a directory instead of a name?
635 # Mangle the input to fit normal operation.
636 if echo "$VCSH_REPO_NAME" | grep -q '/'; then
637         GIT_DIR=$VCSH_REPO_NAME; export GIT_DIR
638         VCSH_REPO_NAME=$(basename "$VCSH_REPO_NAME" .git); export VCSH_REPO_NAME
639 fi
640
641 check_dir() {
642         check_directory="$1"
643         if [ ! -d "$check_directory" ]; then
644                 if [ -e "$check_directory" ]; then
645                         fatal "'$check_directory' exists but is not a directory" 13
646                 else
647                         verbose "attempting to create '$check_directory'"
648                         mkdir -p "$check_directory" || fatal "could not create '$check_directory'" 50
649                 fi
650         fi
651 }
652
653 check_dir "$VCSH_REPO_D"
654 [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && check_dir "$VCSH_BASE/.gitignore.d"
655 [ ! "x$VCSH_GITATTRIBUTES" = 'xnone' ] && check_dir "$VCSH_BASE/.gitattributes.d"
656
657 verbose "$VCSH_COMMAND begin"
658 VCSH_COMMAND=$(echo "$VCSH_COMMAND" | sed 's/-/_/g'); export VCSH_COMMAND
659
660 # Source repo-specific configuration file
661 [ -r "$XDG_CONFIG_HOME/vcsh/config.d/$VCSH_REPO_NAME" ] && . "$XDG_CONFIG_HOME/vcsh/config.d/$VCSH_REPO_NAME"
662
663 # source overlay functions
664 for overlay in "$VCSH_OVERLAY_D/$VCSH_COMMAND"* "$VCSH_OVERLAY_D/$VCSH_REPO_NAME.$VCSH_COMMAND"*; do
665         [ -r "$overlay" ] || continue
666         info "sourcing '$overlay'"
667         . "$overlay"
668 done
669
670 hook pre-command
671 $VCSH_COMMAND "$@"
672 hook post-command
673 verbose "$VCSH_COMMAND end, exiting"
674 exit $VCSH_COMMAND_RETURN_CODE