]> 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: Support whitespace in filenames for gitignore
[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         OLDIFS="$IFS"
368         IFS=$(printf '\n\t')
369         gitignores=$(for file in $(git ls-files); do
370                 while true; do
371                         echo $file; new="${file%/*}"
372                         [ "$file" = "$new" ] && break
373                         file="$new"
374                 done;
375         done | sort -u)
376
377         # Contrary to GNU mktemp, mktemp on BSD/OSX requires a template for temp files
378         # Using a template makes GNU mktemp default to $PWD and not #TMPDIR for tempfile location
379         # To make every OS happy, set full path explicitly
380         tempfile=$(mktemp "${TMPDIR:-/tmp}/tmp.XXXXXXXXXX") || fatal "could not create tempfile: '${tempfile}'" 51
381
382         echo '*' > "$tempfile" || fatal "could not write to '$tempfile'" 57
383         for gitignore in $gitignores; do
384                 echo "$gitignore" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57
385                 if [ "x$VCSH_GITIGNORE" = 'xrecursive' ] && [ -d "$gitignore" ]; then
386                         { echo "$gitignore/*" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57; }
387                 fi
388         done
389         IFS="$OLDIFS"
390         if diff -N "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" > /dev/null; then
391                 rm -f "$tempfile" || error "could not delete '$tempfile'"
392                 exit
393         fi
394         if [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ]; then
395                 info "'$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' differs from new data, moving it to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'"
396                 mv -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak" ||
397                         fatal "could not move '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'" 53
398         fi
399         mv -f "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ||
400                 fatal "could not move '$tempfile' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME'" 53
401 }
402
403 debug `git version`
404
405 if [ ! "x$VCSH_GITIGNORE" = 'xexact' ] && [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && [ ! "x$VCSH_GITIGNORE" = 'xrecursive' ]; then
406         fatal "'\$VCSH_GITIGNORE' must equal 'exact', 'none', or 'recursive'" 1
407 fi
408
409 export VCSH_COMMAND="$1"
410
411 case "$VCSH_COMMAND" in
412         clon|clo|cl) VCSH_COMMAND=clone;;
413         commi|comm|com|co) VCSH_COMMAND=commit;;
414         delet|dele|del|de) VCSH_COMMAND=delete;;
415         ente|ent|en) VCSH_COMMAND=enter;;
416         hel|he) VCSH_COMMAND=help;;
417         ini|in) VCSH_COMMAND=init;;
418         pul) VCSH_COMMAND=pull;;
419         pus) VCSH_COMMAND=push;;
420         renam|rena|ren|re) VCSH_COMMAND=rename;;
421         ru) VCSH_COMMAND=run;;
422         statu|stat|sta|st) VCSH_COMMAND=status;;
423         upgrad|upgra|upgr|up) VCSH_COMMAND=upgrade;;
424         versio|versi|vers|ver|ve) VCSH_COMMAND=version;;
425         which|whi|wh) VCSH_COMMAND=which;;
426         write|writ|wri|wr) VCSH_COMMAND=write-gitignore;;
427 esac    
428
429 if [ "$VCSH_COMMAND" = 'clone' ]; then
430         [ -z "$2" ] && fatal "$VCSH_COMMAND: please specify a remote" 1
431         GIT_REMOTE="$2"
432         [ -n "$3" ] && VCSH_REPO_NAME="$3" || VCSH_REPO_NAME=$(basename "${GIT_REMOTE#*:}" .git)
433         [ -z "$VCSH_REPO_NAME" ] && fatal "$VCSH_COMMAND: could not determine repository name" 1
434         export VCSH_REPO_NAME
435         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
436 elif [ "$VCSH_COMMAND" = 'version' ]; then
437         echo "$SELF $VERSION"
438         git version
439         exit
440 elif [ "$VCSH_COMMAND" = 'which' ]; then
441         [ -z "$2" ] && fatal "$VCSH_COMMAND: please specify a filename" 1
442         [ -n "$3" ] && fatal "$VCSH_COMMAND: too many parameters" 1
443         export VCSH_COMMAND_PARAMETER="$2"
444 elif [ "$VCSH_COMMAND" = 'delete' ]           ||
445      [ "$VCSH_COMMAND" = 'enter' ]            ||
446      [ "$VCSH_COMMAND" = 'init' ]             ||
447      [ "$VCSH_COMMAND" = 'list-tracked-by' ]  ||
448      [ "$VCSH_COMMAND" = 'rename' ]           ||
449      [ "$VCSH_COMMAND" = 'run' ]              ||
450      [ "$VCSH_COMMAND" = 'upgrade' ]          ||
451      [ "$VCSH_COMMAND" = 'write-gitignore' ]; then
452         [ -z $2 ]                                 && fatal "$VCSH_COMMAND: please specify repository to work on" 1
453         [ "$VCSH_COMMAND" = 'rename' -a -z "$3" ] && fatal "$VCSH_COMMAND: please specify a target name" 1
454         [ "$VCSH_COMMAND" = 'run'    -a -z "$3" ] && fatal "$VCSH_COMMAND: please specify a command" 1
455         export VCSH_REPO_NAME="$2"
456         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
457         [ "$VCSH_COMMAND" = 'rename' ] && { export VCSH_REPO_NAME_NEW="$3";
458                                             export GIT_DIR_NEW="$VCSH_REPO_D/$VCSH_REPO_NAME_NEW.git"; }
459         [ "$VCSH_COMMAND" = 'run' ]    && shift 2
460 elif [ "$VCSH_COMMAND" = 'commit' ] ||
461      [ "$VCSH_COMMAND" = 'list' ] ||
462      [ "$VCSH_COMMAND" = 'list-tracked' ] ||
463      [ "$VCSH_COMMAND" = 'pull' ] ||
464      [ "$VCSH_COMMAND" = 'push' ]; then
465         :
466 elif [ "$VCSH_COMMAND" = 'status' ]; then
467         export VCSH_REPO_NAME="$2"
468 elif [ -n "$2" ]; then
469         export VCSH_COMMAND='run'
470         export VCSH_REPO_NAME="$1"
471         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
472         [ -d $GIT_DIR ] || { help; exit 1; }
473         shift 1
474         set -- "git" "$@"
475 elif [ -n "$VCSH_COMMAND" ]; then
476         export VCSH_COMMAND='enter'
477         export VCSH_REPO_NAME="$1"
478         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
479         [ -d $GIT_DIR ] || { help; exit 1; }
480 else
481         # $1 is empty, or 'help'
482         help && exit
483 fi
484
485 # Did we receive a directory instead of a name?
486 # Mangle the input to fit normal operation.
487 if echo $VCSH_REPO_NAME | grep -q '/'; then
488         export GIT_DIR=$VCSH_REPO_NAME
489         export VCSH_REPO_NAME=$(basename "$VCSH_REPO_NAME" .git)
490 fi
491
492 check_dir() {
493         check_directory="$1"
494         if [ ! -d "$check_directory" ]; then
495                 if [ -e "$check_directory" ]; then
496                         fatal "'$check_directory' exists but is not a directory" 13
497                 else
498                         verbose "attempting to create '$check_directory'"
499                         mkdir -p "$check_directory" || fatal "could not create '$check_directory'" 50
500                 fi
501         fi
502 }
503
504 check_dir "$VCSH_REPO_D"
505 [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && check_dir "$VCSH_BASE/.gitignore.d"
506 check_dir "$VCSH_BASE/.gitattributes.d"
507
508 verbose "$VCSH_COMMAND begin"
509 export VCSH_COMMAND=$(echo $VCSH_COMMAND | sed 's/-/_/g')
510 hook pre-command
511 $VCSH_COMMAND "$@"
512 hook post-command
513 verbose "$VCSH_COMMAND end, exiting"
514 exit $VCSH_COMMAND_RETURN_CODE