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

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