]> 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: Make fatal() default to `exit 1`
[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.20141009'
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_BASE:="$HOME"}
80 : ${VCSH_GITIGNORE:=exact}
81 : ${VCSH_GITATTRIBUTES:=none}
82 : ${VCSH_WORKTREE:=absolute}
83
84 if [ ! "x$VCSH_GITIGNORE" = 'xexact' ] && [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && [ ! "x$VCSH_GITIGNORE" = 'xrecursive' ]; then
85         fatal "'\$VCSH_GITIGNORE' must equal 'exact', 'none', or 'recursive'" 1
86 fi
87
88 if [ ! "x$VCSH_WORKTREE" = 'xabsolute' ] && [ ! "x$VCSH_WORKTREE" = 'xrelative' ]; then
89         fatal "'\$VCSH_WORKTREE' must equal 'absolute', or 'relative'" 1
90 fi
91
92
93 help() {
94         echo "usage: $SELF <options> <command>
95
96    options:
97    -c <file>            Source file
98    -d                   Enable debug mode
99    -v                   Enable verbose mode
100
101    commands:
102    clone <remote> \\
103          [<repo>]       Clone from an existing repository
104    commit               Commit in all repositories
105    delete <repo>        Delete an existing repository
106    enter <repo>         Enter repository; spawn new instance of \$SHELL
107    help                 Display this help text
108    init <repo>          Initialize a new repository
109    list                 List all repositories
110    list-tracked         List all files tracked by vcsh
111    list-tracked-by \\
112         <repo>          List files tracked by a repository
113    pull                 Pull from all vcsh remotes
114    push                 Push to vcsh remotes
115    rename <repo> \\
116           <newname>     Rename repository
117    run <repo> \\
118        <command>        Use this repository
119    status [<repo>]      Show statuses of all/one vcsh repositories
120    upgrade <repo>       Upgrade repository to currently recommended settings
121    version              Print version information
122    which <substring>    Find substring in name of any tracked file
123    write-gitignore \\
124    <repo>               Write .gitignore.d/<repo> via git ls-files
125
126    <repo> <git command> Shortcut to run git commands directly
127    <repo>               Shortcut to enter repository" >&2
128 }
129
130 debug() {
131         [ -n "$VCSH_DEBUG" ] && echo "$SELF: debug: $@"
132 }
133
134 verbose() {
135         if [ -n "$VCSH_DEBUG" ] || [ -n "$VCSH_VERBOSE" ]; then echo "$SELF: verbose: $@"; fi
136 }
137
138 error() {
139         echo "$SELF: error: $1" >&2
140 }
141
142 info() {
143         echo "$SELF: info: $1"
144 }
145
146 clone() {
147         hook pre-clone
148         init
149         git remote add origin "$GIT_REMOTE"
150         git config branch.master.remote origin
151         git config branch.master.merge  refs/heads/master
152         if [ $(git ls-remote origin master 2> /dev/null | wc -l ) -lt 1 ]; then
153                 info "remote is empty, not merging anything"
154                 exit
155         fi
156         git fetch
157         hook pre-merge
158         git ls-tree -r --name-only origin/master | (while read object; do
159                 [ -e "$object" ] &&
160                         error "'$object' exists." &&
161                         VCSH_CONFLICT=1
162         done
163         [ x"$VCSH_CONFLICT" = x'1' ]) &&
164                 fatal "will stop after fetching and not try to merge!
165   Once this situation has been resolved, run 'vcsh $VCSH_REPO_NAME pull' to finish cloning." 17
166         git merge origin/master
167         hook post-merge
168         hook post-clone
169         retire
170         hook post-clone-retired
171 }
172
173 commit() {
174         hook pre-commit
175         for VCSH_REPO_NAME in $(list); do
176                 echo "$VCSH_REPO_NAME: "
177                 GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
178                 use
179                 git commit --untracked-files=no --quiet
180                 VCSH_COMMAND_RETURN_CODE=$?
181                 echo
182         done
183         hook post-commit
184 }
185
186 delete() {
187         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
188         use
189         info "This operation WILL DESTROY DATA!"
190         files=$(git ls-files)
191         echo "These files will be deleted:
192
193 $files
194
195 AGAIN, THIS WILL DELETE YOUR DATA!
196 To continue, type 'Yes, do as I say'"
197         read answer
198         [ "x$answer" = 'xYes, do as I say' ] || exit 16
199         for file in $files; do
200                 rm -f $file || info "could not delete '$file', continuing with deletion"
201         done
202         rm -rf "$GIT_DIR" || error "could not delete '$GIT_DIR'"
203 }
204
205 enter() {
206         hook pre-enter
207         use
208         $SHELL
209         hook post-enter
210 }
211
212 git_dir_exists() {
213         [ -d "$GIT_DIR" ] || fatal "no repository found for '$VCSH_REPO_NAME'" 12
214 }
215
216 hook() {
217         for hook in "$VCSH_HOOK_D/$1"* "$VCSH_HOOK_D/$VCSH_REPO_NAME.$1"*; do
218                 [ -x "$hook" ] || continue
219                 verbose "executing '$hook'"
220                 "$hook"
221         done
222 }
223
224 init() {
225         hook pre-init
226         [ ! -e "$GIT_DIR" ] || fatal "'$GIT_DIR' exists" 10
227         mkdir -p "$VCSH_BASE" || fatal "could not create '$VCSH_BASE'" 50
228         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
229         git init --shared=0600
230         upgrade
231         hook post-init
232 }
233
234 list() {
235         for repo in "$VCSH_REPO_D"/*.git; do
236                 [ -d "$repo" ] && [ -r "$repo" ] && echo "$(basename "$repo" .git)"
237         done
238 }
239
240 get_files() {
241         GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
242         git ls-files
243 }
244
245 list_tracked() {
246         for VCSH_REPO_NAME in $(list); do
247                 get_files
248         done | sed "s,^,$(printf '%s\n' "$VCSH_BASE/" | \
249             sed 's/[,\&]/\\&/g')," | sort -u
250 }
251
252 list_tracked_by() {
253         use
254         git ls-files | sed "s,^,$(printf '%s\n' "$VCSH_BASE/" | \
255             sed 's/[,\&]/\\&/g')," | sort -u
256 }
257
258 pull() {
259         hook pre-pull
260         for VCSH_REPO_NAME in $(list); do
261                 printf '%s: ' "$VCSH_REPO_NAME"
262                 GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
263                 use
264                 git pull
265                 VCSH_COMMAND_RETURN_CODE=$?
266                 echo
267         done
268         hook post-pull
269 }
270
271 push() {
272         hook pre-push
273         for VCSH_REPO_NAME in $(list); do
274                 printf '%s: ' "$VCSH_REPO_NAME"
275                 GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
276                 use
277                 git push
278                 VCSH_COMMAND_RETURN_CODE=$?
279                 echo
280         done
281         hook post-push
282 }
283
284 retire() {
285         unset VCSH_DIRECTORY
286 }
287
288 rename() {
289         git_dir_exists
290         [ -d "$GIT_DIR_NEW" ] && fatal "'$GIT_DIR_NEW' exists" 54
291         mv -f "$GIT_DIR" "$GIT_DIR_NEW" || fatal "Could not mv '$GIT_DIR' '$GIT_DIR_NEW'" 52
292
293         # Now that the repository has been renamed, we need to fix up its configuration
294         # Overwrite old name..
295         GIT_DIR=$GIT_DIR_NEW
296         VCSH_REPO_NAME=$VCSH_REPO_NAME_NEW
297         # ..and clobber all old configuration
298         upgrade
299 }
300
301 run() {
302         hook pre-run
303         use
304         "$@"
305         VCSH_COMMAND_RETURN_CODE=$?
306         hook post-run
307 }
308
309 status() {
310         if [ -n "$VCSH_REPO_NAME" ]; then
311                 GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
312                 use
313                 git status --short --untracked-files='no'
314                 VCSH_COMMAND_RETURN_CODE=$?
315         else
316                 for VCSH_REPO_NAME in $(list); do
317                         echo "$VCSH_REPO_NAME:"
318                         GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
319                         use
320                         git status --short --untracked-files='no'
321                         VCSH_COMMAND_RETURN_CODE=$?
322                         echo
323                 done
324         fi
325 }
326
327 upgrade() {
328         hook pre-upgrade
329         # fake-bare repositories are not bare, actually. Set this to false
330         # because otherwise Git complains "fatal: core.bare and core.worktree
331         # do not make sense"
332         git config core.bare false
333         # core.worktree may be absolute or relative to $GIT_DIR, depending on
334         # user preference
335         if [ ! "x$VCSH_WORKTREE" = 'xabsolute' ]; then
336                 git config core.worktree "$(cd "$GIT_DIR" && GIT_WORK_TREE=$VCSH_BASE git rev-parse --show-cdup)"
337         elif [ ! "x$VCSH_WORKTREE" = 'xrelative' ]; then
338                 git config core.worktree "$VCSH_BASE"
339         fi
340         [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && git config core.excludesfile ".gitignore.d/$VCSH_REPO_NAME"
341         [ ! "x$VCSH_GITATTRIBUTES" = 'xnone' ] && git config core.attributesfile ".gitattributes.d/$VCSH_REPO_NAME"
342         git config vcsh.vcsh 'true'
343         use
344         [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ] && git add -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME"
345         [ -e "$VCSH_BASE/.gitattributes.d/$VCSH_REPO_NAME" ] && git add -f "$VCSH_BASE/.gitattributes.d/$VCSH_REPO_NAME"
346         hook post-upgrade
347 }
348
349 use() {
350         git_dir_exists
351         VCSH_DIRECTORY=$VCSH_REPO_NAME; export VCSH_DIRECTORY
352 }
353
354 which() {
355         [ -e "$VCSH_COMMAND_PARAMETER" ] || fatal "'$VCSH_COMMAND_PARAMETER' does not exist" 1
356         for VCSH_REPO_NAME in $(list); do
357                 for VCSH_FILE in $(get_files); do
358                         echo "$VCSH_FILE" | grep -q "$VCSH_COMMAND_PARAMETER" && echo "$VCSH_REPO_NAME: $VCSH_FILE"
359                 done
360         done | sort -u
361 }
362
363 write_gitignore() {
364         # Don't do anything if the user does not want to write gitignore
365         if [ "x$VCSH_GITIGNORE" = 'xnone' ]; then
366                 info "Not writing gitignore as '\$VCSH_GITIGNORE' is set to 'none'"
367                 exit
368         fi
369
370         use
371         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
372         OLDIFS=$IFS
373         IFS=$(printf '\n\t')
374         gitignores=$(for file in $(git ls-files); do
375                 while true; do
376                         echo "$file"; new=${file%/*}
377                         [ x"$file" = x"$new" ] && break
378                         file=$new
379                 done;
380         done | sort -u)
381
382         # Contrary to GNU mktemp, mktemp on BSD/OSX requires a template for temp files
383         # Using a template makes GNU mktemp default to $PWD and not #TMPDIR for tempfile location
384         # To make every OS happy, set full path explicitly
385         tempfile=$(mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX") || fatal "could not create tempfile: '${tempfile}'" 51
386
387         echo '*' > "$tempfile" || fatal "could not write to '$tempfile'" 57
388         for gitignore in $gitignores; do
389                 echo "$gitignore" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57
390                 if [ "x$VCSH_GITIGNORE" = 'xrecursive' ] && [ -d "$gitignore" ]; then
391                         { echo "$gitignore/*" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57; }
392                 fi
393         done
394         IFS=$OLDIFS
395         if diff -N "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" > /dev/null; then
396                 rm -f "$tempfile" || error "could not delete '$tempfile'"
397                 exit
398         fi
399         if [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ]; then
400                 info "'$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' differs from new data, moving it to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'"
401                 mv -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak" ||
402                         fatal "could not move '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'" 53
403         fi
404         mv -f "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ||
405                 fatal "could not move '$tempfile' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME'" 53
406 }
407
408 debug $(git version)
409
410 if [ ! "x$VCSH_GITIGNORE" = 'xexact' ] && [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && [ ! "x$VCSH_GITIGNORE" = 'xrecursive' ]; then
411         fatal "'\$VCSH_GITIGNORE' must equal 'exact', 'none', or 'recursive'" 1
412 fi
413
414 VCSH_COMMAND=$1; export VCSH_COMMAND
415
416 case $VCSH_COMMAND in
417         clon|clo|cl) VCSH_COMMAND=clone;;
418         commi|comm|com|co) VCSH_COMMAND=commit;;
419         delet|dele|del|de) VCSH_COMMAND=delete;;
420         ente|ent|en) VCSH_COMMAND=enter;;
421         hel|he) VCSH_COMMAND=help;;
422         ini|in) VCSH_COMMAND=init;;
423         pul) VCSH_COMMAND=pull;;
424         pus) VCSH_COMMAND=push;;
425         renam|rena|ren|re) VCSH_COMMAND=rename;;
426         ru) VCSH_COMMAND=run;;
427         statu|stat|sta|st) VCSH_COMMAND=status;;
428         upgrad|upgra|upgr|up) VCSH_COMMAND=upgrade;;
429         versio|versi|vers|ver|ve) VCSH_COMMAND=version;;
430         which|whi|wh) VCSH_COMMAND=which;;
431         write|writ|wri|wr) VCSH_COMMAND=write-gitignore;;
432 esac    
433
434 if [ x"$VCSH_COMMAND" = x'clone' ]; then
435         [ -z "$2" ] && fatal "$VCSH_COMMAND: please specify a remote" 1
436         GIT_REMOTE="$2"
437         [ -n "$3" ] && VCSH_REPO_NAME=$3 || VCSH_REPO_NAME=$(basename "${GIT_REMOTE#*:}" .git)
438         [ -z "$VCSH_REPO_NAME" ] && fatal "$VCSH_COMMAND: could not determine repository name" 1
439         export VCSH_REPO_NAME
440         GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
441 elif [ "$VCSH_COMMAND" = 'version' ]; then
442         echo "$SELF $VERSION"
443         git version
444         exit
445 elif [ x"$VCSH_COMMAND" = x'which' ]; then
446         [ -z "$2" ] && fatal "$VCSH_COMMAND: please specify a filename" 1
447         [ -n "$3" ] && fatal "$VCSH_COMMAND: too many parameters" 1
448         VCSH_COMMAND_PARAMETER=$2; export VCSH_COMMAND_PARAMETER
449 elif [ x"$VCSH_COMMAND" = x'delete' ]           ||
450      [ x"$VCSH_COMMAND" = x'enter' ]            ||
451      [ x"$VCSH_COMMAND" = x'init' ]             ||
452      [ x"$VCSH_COMMAND" = x'list-tracked-by' ]  ||
453      [ x"$VCSH_COMMAND" = x'rename' ]           ||
454      [ x"$VCSH_COMMAND" = x'run' ]              ||
455      [ x"$VCSH_COMMAND" = x'upgrade' ]          ||
456      [ x"$VCSH_COMMAND" = x'write-gitignore' ]; then
457         [ -z "$2" ]                                     && fatal "$VCSH_COMMAND: please specify repository to work on" 1
458         [ x"$VCSH_COMMAND" = x'rename' ] && [ -z "$3" ] && fatal "$VCSH_COMMAND: please specify a target name" 1
459         [ x"$VCSH_COMMAND" = x'run'    ] && [ -z "$3" ] && fatal "$VCSH_COMMAND: please specify a command" 1
460         VCSH_REPO_NAME=$2; export VCSH_REPO_NAME
461         GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
462         [ x"$VCSH_COMMAND" = x'rename' ] && { VCSH_REPO_NAME_NEW=$3; export VCSH_REPO_NAME_NEW;
463                                               GIT_DIR_NEW=$VCSH_REPO_D/$VCSH_REPO_NAME_NEW.git; export GIT_DIR_NEW; }
464         [ x"$VCSH_COMMAND" = x'run' ]    && shift 2
465 elif [ x"$VCSH_COMMAND" = x'commit' ] ||
466      [ x"$VCSH_COMMAND" = x'list'   ] ||
467      [ x"$VCSH_COMMAND" = x'list-tracked' ] ||
468      [ x"$VCSH_COMMAND" = x'pull'   ] ||
469      [ x"$VCSH_COMMAND" = x'push'   ]; then
470         :
471 elif [ x"$VCSH_COMMAND" = x'status' ]; then
472         VCSH_REPO_NAME=$2; export VCSH_REPO_NAME
473 elif [ -n "$2" ]; then
474         VCSH_COMMAND='run'; export VCSH_COMMAND
475         VCSH_REPO_NAME=$1; export VCSH_REPO_NAME
476         GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
477         [ -d "$GIT_DIR" ] || { help; exit 1; }
478         shift 1
479         set -- "git" "$@"
480 elif [ -n "$VCSH_COMMAND" ]; then
481         VCSH_COMMAND='enter'; export VCSH_COMMAND
482         VCSH_REPO_NAME=$1; export VCSH_REPO_NAME
483         GIT_DIR=$VCSH_REPO_D/$VCSH_REPO_NAME.git; export GIT_DIR
484         [ -d "$GIT_DIR" ] || { help; exit 1; }
485 else
486         # $1 is empty, or 'help'
487         help && exit
488 fi
489
490 # Did we receive a directory instead of a name?
491 # Mangle the input to fit normal operation.
492 if echo "$VCSH_REPO_NAME" | grep -q '/'; then
493         GIT_DIR=$VCSH_REPO_NAME; export GIT_DIR
494         VCSH_REPO_NAME=$(basename "$VCSH_REPO_NAME" .git); export VCSH_REPO_NAME
495 fi
496
497 check_dir() {
498         check_directory="$1"
499         if [ ! -d "$check_directory" ]; then
500                 if [ -e "$check_directory" ]; then
501                         fatal "'$check_directory' exists but is not a directory" 13
502                 else
503                         verbose "attempting to create '$check_directory'"
504                         mkdir -p "$check_directory" || fatal "could not create '$check_directory'" 50
505                 fi
506         fi
507 }
508
509 check_dir "$VCSH_REPO_D"
510 [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && check_dir "$VCSH_BASE/.gitignore.d"
511 [ ! "x$VCSH_GITATTRIBUTES" = 'xnone' ] && check_dir "$VCSH_BASE/.gitattributes.d"
512
513 verbose "$VCSH_COMMAND begin"
514 VCSH_COMMAND=$(echo "$VCSH_COMMAND" | sed 's/-/_/g'); export VCSH_COMMAND
515 hook pre-command
516 $VCSH_COMMAND "$@"
517 hook post-command
518 verbose "$VCSH_COMMAND end, exiting"
519 exit $VCSH_COMMAND_RETURN_CODE