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

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