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