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

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