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