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

c2b6ff474644e75f9a2f3ab3d2435d18d9a065fa
[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-2014
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.20141009'
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 <remote> \\
104          [<repo>]       Clone from an existing repository
105    commit               Commit in all repositories
106    delete <repo>        Delete an existing repository
107    enter <repo>         Enter repository; spawn new instance of \$SHELL
108    help                 Display this help text
109    init <repo>          Initialize a new repository
110    list                 List all repositories
111    list-tracked         List all files tracked by vcsh
112    list-tracked-by \\
113         <repo>          List files tracked by a repository
114    list-untracked \\
115         [<-r>] [<repo>] List all files not tracked by all or one repositories
116    pull                 Pull from all vcsh remotes
117    push                 Push to vcsh remotes
118    rename <repo> \\
119           <newname>     Rename repository
120    run <repo> \\
121        <command>        Use this repository
122    status [<repo>]      Show statuses of all/one vcsh repositories
123    upgrade <repo>       Upgrade repository to currently recommended settings
124    version              Print version information
125    which <substring>    Find substring in name of any tracked file
126    write-gitignore \\
127    <repo>               Write .gitignore.d/<repo> via git ls-files
128
129    <repo> <git command> Shortcut to run git commands directly
130    <repo>               Shortcut to enter repository" >&2
131 }
132
133 debug() {
134         [ -n "$VCSH_DEBUG" ] && echo "$SELF: debug: $@"
135 }
136
137 verbose() {
138         if [ -n "$VCSH_DEBUG" ] || [ -n "$VCSH_VERBOSE" ]; then echo "$SELF: verbose: $@"; fi
139 }
140
141 error() {
142         echo "$SELF: error: $1" >&2
143 }
144
145 info() {
146         echo "$SELF: info: $1"
147 }
148
149 clone() {
150         hook pre-clone
151         init
152         git remote add origin "$GIT_REMOTE"
153         git config branch.master.remote origin
154         git config branch.master.merge  refs/heads/master
155         VCSH_CLONE_ERROR=$(git ls-remote origin master 2>&1)
156         if [ -n "$VCSH_CLONE_ERROR" ]; then
157                 rm -rf "$GIT_DIR"
158                 fatal "$VCSH_CLONE_ERROR" 1
159         fi
160         git fetch
161         hook pre-merge
162         git ls-tree -r --name-only origin/master | (while read object; do
163                 [ -e "$object" ] &&
164                         error "'$object' exists." &&
165                         VCSH_CONFLICT=1
166         done
167         [ x"$VCSH_CONFLICT" = x'1' ]) &&
168                 fatal "will stop after fetching and not try to merge!
169   Once this situation has been resolved, run 'vcsh $VCSH_REPO_NAME pull' to finish cloning." 17
170         git merge origin/master
171         hook post-merge
172         hook post-clone
173         retire
174         hook post-clone-retired
175 }
176
177 commit() {
178         hook pre-commit
179         for VCSH_REPO_NAME in $(list); do
180                 echo "$VCSH_REPO_NAME: "
181                 GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
182                 use
183                 git commit --untracked-files=no --quiet
184                 VCSH_COMMAND_RETURN_CODE=$?
185                 echo
186         done
187         hook post-commit
188 }
189
190 delete() {
191         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
192         use
193         info "This operation WILL DESTROY DATA!"
194         files=$(git ls-files)
195         echo "These files will be deleted:
196
197 $files
198
199 AGAIN, THIS WILL DELETE YOUR DATA!
200 To continue, type 'Yes, do as I say'"
201         read answer
202         [ "x$answer" = 'xYes, do as I say' ] || exit 16
203         for file in $files; do
204                 rm -f $file || info "could not delete '$file', continuing with deletion"
205         done
206         rm -rf "$GIT_DIR" || error "could not delete '$GIT_DIR'"
207 }
208
209 enter() {
210         hook pre-enter
211         use
212         $SHELL
213         hook post-enter
214 }
215
216 git_dir_exists() {
217         [ -d "$GIT_DIR" ] || fatal "no repository found for '$VCSH_REPO_NAME'" 12
218 }
219
220 hook() {
221         for hook in "$VCSH_HOOK_D/$1"* "$VCSH_HOOK_D/$VCSH_REPO_NAME.$1"*; do
222                 [ -x "$hook" ] || continue
223                 verbose "executing '$hook'"
224                 "$hook"
225         done
226 }
227
228 init() {
229         hook pre-init
230         [ ! -e "$GIT_DIR" ] || fatal "'$GIT_DIR' exists" 10
231         mkdir -p "$VCSH_BASE" || fatal "could not create '$VCSH_BASE'" 50
232         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
233         git init --shared=0600
234         upgrade
235         hook post-init
236 }
237
238 list() {
239         for repo in "$VCSH_REPO_D"/*.git; do
240                 [ -d "$repo" ] && [ -r "$repo" ] && echo "$(basename "$repo" .git)"
241         done
242 }
243
244 get_files() {
245         GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
246         git ls-files
247 }
248
249 list_tracked() {
250         for VCSH_REPO_NAME in $(list); do
251                 get_files
252         done | sed "s,^,$(printf '%s\n' "$VCSH_BASE/" | \
253             sed 's/[,\&]/\\&/g')," | sort -u
254 }
255
256 list_tracked_by() {
257         use
258         git ls-files | sed "s,^,$(printf '%s\n' "$VCSH_BASE/" | \
259             sed 's/[,\&]/\\&/g')," | sort -u
260 }
261
262 list_untracked() {
263         command -v 'comm' >/dev/null 2>&1 || fatal "Could not find 'comm'"
264
265         temp_file_others=$(mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX") || fatal 'Could not create temp file'
266         temp_file_untracked=$(mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX") || fatal 'Could not create temp file'
267         temp_file_untracked_copy=$(mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX") || fatal 'Could not create temp file'
268
269         # Hack in support for `vcsh list-untracked -r`...
270         directory_opt="--directory"
271         shift 1
272         while getopts "r" flag; do
273                 if [ x"$1" = x'-r' ]; then
274                         unset directory_opt
275                 fi
276                 shift 1
277         done
278         # ...and parse for a potential parameter afterwards. As we shifted things out of $* in during getops, we need to look at $1
279         VCSH_REPO_NAME=$1; export VCSH_REPO_NAME
280
281         if [ -n "$VCSH_REPO_NAME" ]; then
282                 list_untracked_helper $VCSH_REPO_NAME
283         else
284                 for VCSH_REPO_NAME in $(list); do
285                         list_untracked_helper $VCSH_REPO_NAME
286                 done
287         fi
288         cat $temp_file_untracked
289
290         unset directory_opt directory_component
291         rm -f $temp_file_others $temp_file_untracked $temp_file_untracked_copy || fatal 'Could not delete temp files'
292 }
293
294 list_untracked_helper() {
295         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
296         git ls-files --others "$directory_opt" | (
297                 while read line; do
298                         echo "$line"
299                         directory_component=${line%%/*}
300                         [ -d "$directory_component" ] && printf '%s/\n' "$directory_component"
301                 done
302                 ) | sort -u > $temp_file_others
303         if [ -z "$ran_once" ]; then
304                 ran_once=1
305                 cp $temp_file_others $temp_file_untracked || fatal 'Could not copy temp file'
306         fi
307         cp $temp_file_untracked $temp_file_untracked_copy || fatal 'Could not copy temp file'
308         comm -12 --nocheck-order $temp_file_others $temp_file_untracked_copy > $temp_file_untracked
309 }
310
311 pull() {
312         hook pre-pull
313         for VCSH_REPO_NAME in $(list); do
314                 printf '%s: ' "$VCSH_REPO_NAME"
315                 GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
316                 use
317                 git pull
318                 VCSH_COMMAND_RETURN_CODE=$?
319                 echo
320         done
321         hook post-pull
322 }
323
324 push() {
325         hook pre-push
326         for VCSH_REPO_NAME in $(list); do
327                 printf '%s: ' "$VCSH_REPO_NAME"
328                 GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
329                 use
330                 git push
331                 VCSH_COMMAND_RETURN_CODE=$?
332                 echo
333         done
334         hook post-push
335 }
336
337 retire() {
338         unset VCSH_DIRECTORY
339 }
340
341 rename() {
342         git_dir_exists
343         [ -d "$GIT_DIR_NEW" ] && fatal "'$GIT_DIR_NEW' exists" 54
344         mv -f "$GIT_DIR" "$GIT_DIR_NEW" || fatal "Could not mv '$GIT_DIR' '$GIT_DIR_NEW'" 52
345
346         # Now that the repository has been renamed, we need to fix up its configuration
347         # Overwrite old name..
348         GIT_DIR=$GIT_DIR_NEW
349         VCSH_REPO_NAME=$VCSH_REPO_NAME_NEW
350         # ..and clobber all old configuration
351         upgrade
352 }
353
354 run() {
355         hook pre-run
356         use
357         "$@"
358         VCSH_COMMAND_RETURN_CODE=$?
359         hook post-run
360 }
361
362 status() {
363         if [ -n "$VCSH_REPO_NAME" ]; then
364                 status_helper $VCSH_REPO_NAME
365         else
366                 for VCSH_REPO_NAME in $(list); do
367                         echo "$VCSH_REPO_NAME:"
368                         status_helper $VCSH_REPO_NAME
369                         echo
370                 done
371         fi
372 }
373
374 status_helper() {
375         GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
376         use
377         git status --short --untracked-files='no'
378         VCSH_COMMAND_RETURN_CODE=$?
379 }
380
381 upgrade() {
382         hook pre-upgrade
383         # fake-bare repositories are not bare, actually. Set this to false
384         # because otherwise Git complains "fatal: core.bare and core.worktree
385         # do not make sense"
386         git config core.bare false
387         # core.worktree may be absolute or relative to $GIT_DIR, depending on
388         # user preference
389         if [ ! "x$VCSH_WORKTREE" = 'xabsolute' ]; then
390                 git config core.worktree "$(cd "$GIT_DIR" && GIT_WORK_TREE=$VCSH_BASE git rev-parse --show-cdup)"
391         elif [ ! "x$VCSH_WORKTREE" = 'xrelative' ]; then
392                 git config core.worktree "$VCSH_BASE"
393         fi
394         [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && git config core.excludesfile ".gitignore.d/$VCSH_REPO_NAME"
395         [ ! "x$VCSH_GITATTRIBUTES" = 'xnone' ] && git config core.attributesfile ".gitattributes.d/$VCSH_REPO_NAME"
396         git config vcsh.vcsh 'true'
397         use
398         [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ] && git add -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME"
399         [ -e "$VCSH_BASE/.gitattributes.d/$VCSH_REPO_NAME" ] && git add -f "$VCSH_BASE/.gitattributes.d/$VCSH_REPO_NAME"
400         hook post-upgrade
401 }
402
403 use() {
404         git_dir_exists
405         VCSH_DIRECTORY=$VCSH_REPO_NAME; export VCSH_DIRECTORY
406 }
407
408 which() {
409         [ -e "$VCSH_COMMAND_PARAMETER" ] || fatal "'$VCSH_COMMAND_PARAMETER' does not exist" 1
410         for VCSH_REPO_NAME in $(list); do
411                 for VCSH_FILE in $(get_files); do
412                         echo "$VCSH_FILE" | grep -q "$VCSH_COMMAND_PARAMETER" && echo "$VCSH_REPO_NAME: $VCSH_FILE"
413                 done
414         done | sort -u
415 }
416
417 write_gitignore() {
418         # Don't do anything if the user does not want to write gitignore
419         if [ "x$VCSH_GITIGNORE" = 'xnone' ]; then
420                 info "Not writing gitignore as '\$VCSH_GITIGNORE' is set to 'none'"
421                 exit
422         fi
423
424         use
425         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
426         OLDIFS=$IFS
427         IFS=$(printf '\n\t')
428         gitignores=$(for file in $(git ls-files); do
429                 while true; do
430                         echo "$file"; new=${file%/*}
431                         [ x"$file" = x"$new" ] && break
432                         file=$new
433                 done;
434         done | sort -u)
435
436         # Contrary to GNU mktemp, mktemp on BSD/OSX requires a template for temp files
437         # Using a template makes GNU mktemp default to $PWD and not #TMPDIR for tempfile location
438         # To make every OS happy, set full path explicitly
439         tempfile=$(mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX") || fatal "could not create tempfile: '${tempfile}'" 51
440
441         echo '*' > "$tempfile" || fatal "could not write to '$tempfile'" 57
442         for gitignore in $gitignores; do
443                 echo "$gitignore" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57
444                 if [ "x$VCSH_GITIGNORE" = 'xrecursive' ] && [ -d "$gitignore" ]; then
445                         { echo "$gitignore/*" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57; }
446                 fi
447         done
448         IFS=$OLDIFS
449         if diff -N "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" > /dev/null; then
450                 rm -f "$tempfile" || error "could not delete '$tempfile'"
451                 exit
452         fi
453         if [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ]; then
454                 info "'$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' differs from new data, moving it to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'"
455                 mv -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak" ||
456                         fatal "could not move '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'" 53
457         fi
458         mv -f "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ||
459                 fatal "could not move '$tempfile' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME'" 53
460 }
461
462 debug $(git version)
463
464 if [ ! "x$VCSH_GITIGNORE" = 'xexact' ] && [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && [ ! "x$VCSH_GITIGNORE" = 'xrecursive' ]; then
465         fatal "'\$VCSH_GITIGNORE' must equal 'exact', 'none', or 'recursive'" 1
466 fi
467
468 VCSH_COMMAND=$1; export VCSH_COMMAND
469
470 case $VCSH_COMMAND in
471         clon|clo|cl) VCSH_COMMAND=clone;;
472         commi|comm|com|co) VCSH_COMMAND=commit;;
473         delet|dele|del|de) VCSH_COMMAND=delete;;
474         ente|ent|en) VCSH_COMMAND=enter;;
475         hel|he) VCSH_COMMAND=help;;
476         ini|in) VCSH_COMMAND=init;;
477         pul) VCSH_COMMAND=pull;;
478         pus) VCSH_COMMAND=push;;
479         renam|rena|ren|re) VCSH_COMMAND=rename;;
480         ru) VCSH_COMMAND=run;;
481         statu|stat|sta|st) VCSH_COMMAND=status;;
482         upgrad|upgra|upgr|up) VCSH_COMMAND=upgrade;;
483         versio|versi|vers|ver|ve) VCSH_COMMAND=version;;
484         which|whi|wh) VCSH_COMMAND=which;;
485         write|writ|wri|wr) VCSH_COMMAND=write-gitignore;;
486 esac    
487
488 if [ x"$VCSH_COMMAND" = x'clone' ]; then
489         [ -z "$2" ] && fatal "$VCSH_COMMAND: please specify a remote" 1
490         GIT_REMOTE="$2"
491         [ -n "$3" ] && VCSH_REPO_NAME=$3 || VCSH_REPO_NAME=$(basename "${GIT_REMOTE#*:}" .git)
492         [ -z "$VCSH_REPO_NAME" ] && fatal "$VCSH_COMMAND: could not determine repository name" 1
493         export VCSH_REPO_NAME
494         GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
495 elif [ "$VCSH_COMMAND" = 'version' ]; then
496         echo "$SELF $VERSION"
497         git version
498         exit
499 elif [ x"$VCSH_COMMAND" = x'which' ]; then
500         [ -z "$2" ] && fatal "$VCSH_COMMAND: please specify a filename" 1
501         [ -n "$3" ] && fatal "$VCSH_COMMAND: too many parameters" 1
502         VCSH_COMMAND_PARAMETER=$2; export VCSH_COMMAND_PARAMETER
503 elif [ x"$VCSH_COMMAND" = x'delete' ]           ||
504      [ x"$VCSH_COMMAND" = x'enter' ]            ||
505      [ x"$VCSH_COMMAND" = x'init' ]             ||
506      [ x"$VCSH_COMMAND" = x'list-tracked-by' ]  ||
507      [ x"$VCSH_COMMAND" = x'rename' ]           ||
508      [ x"$VCSH_COMMAND" = x'run' ]              ||
509      [ x"$VCSH_COMMAND" = x'upgrade' ]          ||
510      [ x"$VCSH_COMMAND" = x'write-gitignore' ]; then
511         [ -z "$2" ]                                     && fatal "$VCSH_COMMAND: please specify repository to work on" 1
512         [ x"$VCSH_COMMAND" = x'rename' ] && [ -z "$3" ] && fatal "$VCSH_COMMAND: please specify a target name" 1
513         [ x"$VCSH_COMMAND" = x'run'    ] && [ -z "$3" ] && fatal "$VCSH_COMMAND: please specify a command" 1
514         VCSH_REPO_NAME=$2; export VCSH_REPO_NAME
515         GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
516         [ x"$VCSH_COMMAND" = x'rename' ] && { VCSH_REPO_NAME_NEW=$3; export VCSH_REPO_NAME_NEW;
517                                               GIT_DIR_NEW=$VCSH_REPO_D/$VCSH_REPO_NAME_NEW.git; export GIT_DIR_NEW; }
518         [ x"$VCSH_COMMAND" = x'run' ]    && shift 2
519 elif [ x"$VCSH_COMMAND" = x'commit' ] ||
520      [ x"$VCSH_COMMAND" = x'list'   ] ||
521      [ x"$VCSH_COMMAND" = x'list-tracked' ] ||
522      [ x"$VCSH_COMMAND" = x'list-untracked' ] ||
523      [ x"$VCSH_COMMAND" = x'pull'   ] ||
524      [ x"$VCSH_COMMAND" = x'push'   ]; then
525         :
526 elif [ x"$VCSH_COMMAND" = x'status' ]; then
527         VCSH_REPO_NAME=$2; export VCSH_REPO_NAME
528 elif [ -n "$2" ]; then
529         VCSH_COMMAND='run'; export VCSH_COMMAND
530         VCSH_REPO_NAME=$1; export VCSH_REPO_NAME
531         GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
532         [ -d "$GIT_DIR" ] || { help; exit 1; }
533         shift 1
534         set -- "git" "$@"
535 elif [ -n "$VCSH_COMMAND" ]; then
536         VCSH_COMMAND='enter'; export VCSH_COMMAND
537         VCSH_REPO_NAME=$1; export VCSH_REPO_NAME
538         GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
539         [ -d "$GIT_DIR" ] || { help; exit 1; }
540 else
541         # $1 is empty, or 'help'
542         help && exit
543 fi
544
545 # Did we receive a directory instead of a name?
546 # Mangle the input to fit normal operation.
547 if echo "$VCSH_REPO_NAME" | grep -q '/'; then
548         GIT_DIR=$VCSH_REPO_NAME; export GIT_DIR
549         VCSH_REPO_NAME=$(basename "$VCSH_REPO_NAME" .git); export VCSH_REPO_NAME
550 fi
551
552 check_dir() {
553         check_directory="$1"
554         if [ ! -d "$check_directory" ]; then
555                 if [ -e "$check_directory" ]; then
556                         fatal "'$check_directory' exists but is not a directory" 13
557                 else
558                         verbose "attempting to create '$check_directory'"
559                         mkdir -p "$check_directory" || fatal "could not create '$check_directory'" 50
560                 fi
561         fi
562 }
563
564 check_dir "$VCSH_REPO_D"
565 [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && check_dir "$VCSH_BASE/.gitignore.d"
566 [ ! "x$VCSH_GITATTRIBUTES" = 'xnone' ] && check_dir "$VCSH_BASE/.gitattributes.d"
567
568 verbose "$VCSH_COMMAND begin"
569 VCSH_COMMAND=$(echo "$VCSH_COMMAND" | sed 's/-/_/g'); export VCSH_COMMAND
570
571 # source overlay functions
572 for overlay in "$VCSH_OVERLAY_D/$VCSH_COMMAND"* "$VCSH_OVERLAY_D/$VCSH_REPO_NAME.$VCSH_COMMAND"*; do
573         [ -r "$overlay" ] || continue
574         info "sourcing '$overlay'"
575         . "$overlay"
576 done
577
578 hook pre-command
579 $VCSH_COMMAND "$@"
580 hook post-command
581 verbose "$VCSH_COMMAND end, exiting"
582 exit $VCSH_COMMAND_RETURN_CODE