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

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