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