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

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