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

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