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

doc/sample_hooks/: Cleanups
[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                 echo
261         done
262         hook post-pull
263 }
264
265 push() {
266         hook pre-push
267         for VCSH_REPO_NAME in $(list); do
268                 printf "$VCSH_REPO_NAME: "
269                 export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
270                 use
271                 git push
272                 echo
273         done
274         hook post-push
275 }
276
277 retire() {
278         unset VCSH_DIRECTORY
279 }
280
281 rename() {
282         git_dir_exists
283         [ -d "$GIT_DIR_NEW" ] && fatal "'$GIT_DIR_NEW' exists" 54
284         mv -f "$GIT_DIR" "$GIT_DIR_NEW" || fatal "Could not mv '$GIT_DIR' '$GIT_DIR_NEW'" 52
285
286         # Now that the repository has been renamed, we need to fix up its configuration
287         # Overwrite old name..
288         GIT_DIR="$GIT_DIR_NEW"
289         VCSH_REPO_NAME="$VCSH_REPO_NAME_NEW"
290         # ..and clobber all old configuration
291         upgrade
292 }
293
294 run() {
295         hook pre-run
296         use
297         "$@"
298         hook post-run
299 }
300
301 status() {
302         if [ ! "x$VCSH_REPO_NAME" = "x" ]; then
303                 export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
304                 use
305                 git status --short --untracked-files='no'
306         else
307                 for VCSH_REPO_NAME in $(list); do
308                         echo "$VCSH_REPO_NAME:"
309                         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
310                         use
311                         git status --short --untracked-files='no'
312                         echo
313                 done
314         fi
315 }
316
317 upgrade() {
318         hook pre-upgrade
319         # fake-bare repositories are not bare, actually. Set this to false
320         # because otherwise Git complains "fatal: core.bare and core.worktree
321         # do not make sense"
322         git config core.bare false
323         # core.worktree may be absolute or relative to $GIT_DIR, depending on
324         # user preference
325         if [ ! "x$VCSH_WORKTREE" = 'xabsolute' ]; then
326                 git config core.worktree $(cd $GIT_DIR && GIT_WORK_TREE="$VCSH_BASE" git rev-parse --show-cdup)
327         elif [ ! "x$VCSH_WORKTREE" = 'xrelative' ]; then
328                 git config core.worktree "$VCSH_BASE"
329         fi
330         [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && git config core.excludesfile ".gitignore.d/$VCSH_REPO_NAME"
331         git config vcsh.vcsh         'true'
332         use
333         [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ] && git add -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME"
334         hook post-upgrade
335 }
336
337 use() {
338         git_dir_exists
339         export VCSH_DIRECTORY="$VCSH_REPO_NAME"
340 }
341
342 which() {
343         for VCSH_REPO_NAME in $(list); do
344                 for VCSH_FILE in $(get_files); do
345                         echo $VCSH_FILE | grep -q "$VCSH_COMMAND_PARAMETER" && echo "$VCSH_REPO_NAME: $VCSH_FILE"
346                 done
347         done | sort -u
348 }
349
350 write_gitignore() {
351         # Don't do anything if the user does not want to write gitignore
352         if [ "x$VCSH_GITIGNORE" = 'xnone' ]; then
353                 info "Not writing gitignore as '\$VCSH_GITIGNORE' is set to 'none'"
354                 exit
355         fi
356
357         use
358         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
359         gitignores=$(for file in $(git ls-files); do
360                 while true; do
361                         echo $file; new="${file%/*}"
362                         [ "$file" = "$new" ] && break
363                         file="$new"
364                 done;
365         done | sort -u)
366
367         # Contrary to GNU mktemp, mktemp on BSD/OSX requires a template for temp files
368         # Using a template makes GNU mktemp default to $PWD and not #TMPDIR for tempfile location
369         # To make every OS happy, set full path explicitly
370         tempfile=$(mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX") || fatal "could not create tempfile: '${tempfile}'" 51
371
372         echo '*' > "$tempfile" || fatal "could not write to '$tempfile'" 57
373         for gitignore in $gitignores; do
374                 echo "$gitignore" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57
375                 if [ "x$VCSH_GITIGNORE" = 'xrecursive' ] && [ -d "$gitignore" ]; then
376                         { echo "$gitignore/*" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57; }
377                 fi
378         done
379         if diff -N "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" > /dev/null; then
380                 rm -f "$tempfile" || error "could not delete '$tempfile'"
381                 exit
382         fi
383         if [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ]; then
384                 info "'$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' differs from new data, moving it to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'"
385                 mv -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak" ||
386                         fatal "could not move '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'" 53
387         fi
388         mv -f "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ||
389                 fatal "could not move '$tempfile' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME'" 53
390 }
391
392 debug `git version`
393
394 if [ ! "x$VCSH_GITIGNORE" = 'xexact' ] && [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && [ ! "x$VCSH_GITIGNORE" = 'xrecursive' ]; then
395         fatal "'\$VCSH_GITIGNORE' must equal 'exact', 'none', or 'recursive'" 1
396 fi
397
398 export VCSH_COMMAND="$1"
399
400 case "$VCSH_COMMAND" in
401         clon|clo|cl) VCSH_COMMAND=clone;;
402         commi|comm|com|co) VCSH_COMMAND=commit;;
403         delet|dele|del|de) VCSH_COMMAND=delete;;
404         ente|ent|en) VCSH_COMMAND=enter;;
405         hel|he) VCSH_COMMAND=help;;
406         ini|in) VCSH_COMMAND=init;;
407         pul) VCSH_COMMAND=pull;;
408         pus) VCSH_COMMAND=push;;
409         renam|rena|ren|re) VCSH_COMMAND=rename;;
410         ru) VCSH_COMMAND=run;;
411         statu|stat|sta|st) VCSH_COMMAND=status;;
412         upgrad|upgra|upgr|up) VCSH_COMMAND=upgrade;;
413         versio|versi|vers|ver|ve) VCSH_COMMAND=version;;
414         which|whi|wh) VCSH_COMMAND=which;;
415         write|writ|wri|wr) VCSH_COMMAND=write-gitignore;;
416 esac    
417
418 if [ "$VCSH_COMMAND" = 'clone' ]; then
419         [ -z "$2" ] && fatal "$VCSH_COMMAND: please specify a remote" 1
420         GIT_REMOTE="$2"
421         [ -n "$3" ] && VCSH_REPO_NAME="$3" || VCSH_REPO_NAME=$(basename "${GIT_REMOTE#*:}" .git)
422         [ -z "$VCSH_REPO_NAME" ] && fatal "$VCSH_COMMAND: could not determine repository name" 1
423         export VCSH_REPO_NAME
424         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
425 elif [ "$VCSH_COMMAND" = 'version' ]; then
426         echo "$SELF $VERSION"
427         git version
428         exit
429 elif [ "$VCSH_COMMAND" = 'which' ]; then
430         [ -z "$2" ] && fatal "$VCSH_COMMAND: please specify a filename" 1
431         [ -n "$3" ] && fatal "$VCSH_COMMAND: too many parameters" 1
432         export VCSH_COMMAND_PARAMETER="$2"
433 elif [ "$VCSH_COMMAND" = 'delete' ]           ||
434      [ "$VCSH_COMMAND" = 'enter' ]            ||
435      [ "$VCSH_COMMAND" = 'init' ]             ||
436      [ "$VCSH_COMMAND" = 'list-tracked-by' ]  ||
437      [ "$VCSH_COMMAND" = 'rename' ]           ||
438      [ "$VCSH_COMMAND" = 'run' ]              ||
439      [ "$VCSH_COMMAND" = 'upgrade' ]          ||
440      [ "$VCSH_COMMAND" = 'write-gitignore' ]; then
441         [ -z $2 ]                                 && fatal "$VCSH_COMMAND: please specify repository to work on" 1
442         [ "$VCSH_COMMAND" = 'rename' -a -z "$3" ] && fatal "$VCSH_COMMAND: please specify a target name" 1
443         [ "$VCSH_COMMAND" = 'run'    -a -z "$3" ] && fatal "$VCSH_COMMAND: please specify a command" 1
444         export VCSH_REPO_NAME="$2"
445         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
446         [ "$VCSH_COMMAND" = 'rename' ] && { export VCSH_REPO_NAME_NEW="$3";
447                                             export GIT_DIR_NEW="$VCSH_REPO_D/$VCSH_REPO_NAME_NEW.git"; }
448         [ "$VCSH_COMMAND" = 'run' ]    && shift 2
449 elif [ "$VCSH_COMMAND" = 'commit' ] ||
450      [ "$VCSH_COMMAND" = 'list' ] ||
451      [ "$VCSH_COMMAND" = 'list-tracked' ] ||
452      [ "$VCSH_COMMAND" = 'pull' ] ||
453      [ "$VCSH_COMMAND" = 'push' ]; then
454         :
455 elif [ "$VCSH_COMMAND" = 'status' ]; then
456         export VCSH_REPO_NAME="$2"
457 elif [ -n "$2" ]; then
458         export VCSH_COMMAND='run'
459         export VCSH_REPO_NAME="$1"
460         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
461         [ -d $GIT_DIR ] || { help; exit 1; }
462         shift 1
463         set -- "git" "$@"
464 elif [ -n "$VCSH_COMMAND" ]; then
465         export VCSH_COMMAND='enter'
466         export VCSH_REPO_NAME="$1"
467         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
468         [ -d $GIT_DIR ] || { help; exit 1; }
469 else
470         # $1 is empty, or 'help'
471         help && exit
472 fi
473
474 # Did we receive a directory instead of a name?
475 # Mangle the input to fit normal operation.
476 if echo $VCSH_REPO_NAME | grep -q '/'; then
477         export GIT_DIR=$VCSH_REPO_NAME
478         export VCSH_REPO_NAME=$(basename "$VCSH_REPO_NAME" .git)
479 fi
480
481 check_dir() {
482         check_directory="$1"
483         if [ ! -d "$check_directory" ]; then
484                 if [ -e "$check_directory" ]; then
485                         fatal "'$check_directory' exists but is not a directory" 13
486                 else
487                         verbose "attempting to create '$check_directory'"
488                         mkdir -p "$check_directory" || fatal "could not create '$check_directory'" 50
489                 fi
490         fi
491 }
492
493 check_dir "$VCSH_REPO_D"
494 [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && check_dir "$VCSH_BASE/.gitignore.d"
495
496 verbose "$VCSH_COMMAND begin"
497 export VCSH_COMMAND=$(echo $VCSH_COMMAND | sed 's/-/_/g')
498 hook pre-command
499 $VCSH_COMMAND "$@"
500 hook post-command
501 verbose "$VCSH_COMMAND end, exiting"