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