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