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

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