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

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