]> 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 'fix--mktemp_on_OSX'
[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 basename() {
19         # Implemented in shell to avoid spawning another process
20         local file
21         file="${1##*/}"
22         [ -z "$2" ] || file="${file%$2}"
23         echo "$file"
24 }
25
26 SELF=$(basename $0)
27 VERSION='1.20130909'
28
29 fatal() {
30         echo "$SELF: fatal: $1" >&2
31         exit $2
32 }
33
34 # We need to run getops as soon as possible so we catch -d and other
35 # options that will modify our behaviour.
36 # Commands are handled at the end of this script.
37 while getopts "c:dv" flag; do
38         if [ "$1" = '-d' ] || [ "$1" = '--debug' ]; then
39                 set -vx
40                 VCSH_DEBUG=1
41                 echo "debug mode on"
42                 echo "$SELF $VERSION"
43         elif [ "$1" = '-v' ];then
44                 VCSH_VERBOSE=1
45                 echo "verbose mode on"
46                 echo "$SELF $VERSION"
47         elif [ "$1" = '-c' ];then
48                 VCSH_OPTION_CONFIG=$OPTARG
49         fi
50         shift 1
51 done
52
53 source_all() {
54         # Source file even if it's in $PWD and does not have any slashes in it
55         case "$1" in
56                 */*) . "$1";;
57                 *)   . "$PWD/$1";;
58         esac;
59 }
60
61
62 # Read configuration and set defaults if anything's not set
63 [ -n "$VCSH_DEBUG" ]                  && set -vx
64 [ -z "$XDG_CONFIG_HOME" ]             && XDG_CONFIG_HOME="$HOME/.config"
65
66 # Read configuration files if there are any
67 [ -r "/etc/vcsh/config" ]             && . "/etc/vcsh/config"
68 [ -r "$XDG_CONFIG_HOME/vcsh/config" ] && . "$XDG_CONFIG_HOME/vcsh/config"
69 if [ -n "$VCSH_OPTION_CONFIG" ]; then
70         # Source $VCSH_OPTION_CONFIG if it can be read and is in $PWD of $PATH
71         if [ -r "$VCSH_OPTION_CONFIG" ]; then
72                 source_all "$VCSH_OPTION_CONFIG"
73         else
74                 fatal "Can not read configuration file '$VCSH_OPTION_CONFIG'" 1
75         fi
76 fi
77 [ -n "$VCSH_DEBUG" ]                  && set -vx
78
79 # Read defaults
80 [ -z "$VCSH_REPO_D" ]                 && VCSH_REPO_D="$XDG_CONFIG_HOME/vcsh/repo.d"
81 [ -z "$VCSH_HOOK_D" ]                 && VCSH_HOOK_D="$XDG_CONFIG_HOME/vcsh/hooks-enabled"
82 [ -z "$VCSH_BASE" ]                   && VCSH_BASE="$HOME"
83 [ -z "$VCSH_GITIGNORE" ]              && VCSH_GITIGNORE='exact'
84 [ -z "$VCSH_WORKTREE" ]               && VCSH_WORKTREE='absolute'
85
86 if [ ! "x$VCSH_GITIGNORE" = 'xexact' ] && [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && [ ! "x$VCSH_GITIGNORE" = 'xrecursive' ]; then
87         fatal "'\$VCSH_GITIGNORE' must equal 'exact', 'none', or 'recursive'" 1
88 fi
89
90 if [ ! "x$VCSH_WORKTREE" = 'xabsolute' ] && [ ! "x$VCSH_WORKTREE" = 'xrelative' ]; then
91         fatal "'\$VCSH_WORKTREE' must equal 'absolute', or 'relative'" 1
92 fi
93
94
95 help() {
96         echo "usage: $SELF <options> <command>
97
98    options:
99    -c <file>            Source file
100    -d                   Enable debug mode
101    -v                   Enable verbose mode
102
103    commands:
104    clone <remote> \\
105          [<repo>]       Clone from an existing repository
106    commit               Commit in all repositories
107    delete <repo>        Delete an existing repository
108    enter <repo>         Enter repository; spawn new instance of \$SHELL
109    help                 Display this help text
110    init <repo>          Initialize a new repository
111    list                 List all repositories
112    list-tracked         List all files tracked by vcsh
113    list-tracked-by \\
114         <repo>          List files tracked by a repository
115    pull                 Pull from all vcsh remotes
116    push                 Push to vcsh remotes
117    rename <repo> \\
118           <newname>     Rename repository
119    run <repo> \\
120        <command>        Use this repository
121    status [<repo>]      Show statuses of all/one vcsh repositories
122    upgrade <repo>       Upgrade repository to currently recommended settings
123    version              Print version information
124    which <substring>    Find substring in name of any tracked file
125    write-gitignore \\
126    <repo>               Write .gitignore.d/<repo> via git ls-files
127
128    <repo> <git command> Shortcut to run git commands directly
129    <repo>               Shortcut to enter repository" >&2
130 }
131
132 debug() {
133         [ -n "$VCSH_DEBUG" ] && echo "$SELF: debug: $@"
134 }
135
136 verbose() {
137         if [ -n "$VCSH_DEBUG" ] || [ -n "$VCSH_VERBOSE" ]; then echo "$SELF: verbose: $@"; fi
138 }
139
140 error() {
141         echo "$SELF: error: $1" >&2
142 }
143
144 info() {
145         echo "$SELF: info: $1"
146 }
147
148 clone() {
149         hook pre-clone
150         init
151         git remote add origin "$GIT_REMOTE"
152         git config branch.master.remote origin
153         git config branch.master.merge  refs/heads/master
154         if [ $(git ls-remote origin master 2> /dev/null | wc -l ) -lt 1 ]; then
155                 info "remote is empty, not merging anything"
156                 exit
157         fi
158         git fetch
159         for object in $(git ls-tree -r origin/master | awk '{print $4}'); do
160                 [ -e "$object" ] &&
161                         error "'$object' exists." &&
162                         VCSH_CONFLICT=1;
163         done
164         [ "$VCSH_CONFLICT" = '1' ] &&
165                 fatal "will stop after fetching and not try to merge!
166   Once this situation has been resolved, run 'vcsh run $VCSH_REPO_NAME git pull' to finish cloning.\n" 17
167         git merge origin/master
168         hook post-clone
169         retire
170         hook post-clone-retired
171 }
172
173 commit() {
174         hook pre-commit
175         for VCSH_REPO_NAME in $(list); do
176                 echo "$VCSH_REPO_NAME: "
177                 export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
178                 use
179                 git commit --untracked-files=no --quiet
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
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                 echo -n "$VCSH_REPO_NAME: "
259                 export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
260                 use
261                 git pull
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                 echo -n "$VCSH_REPO_NAME: "
271                 export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
272                 use
273                 git push
274                 echo
275         done
276         hook post-push
277 }
278
279 retire() {
280         unset VCSH_DIRECTORY
281 }
282
283 rename() {
284         git_dir_exists
285         [ -d "$GIT_DIR_NEW" ] && fatal "'$GIT_DIR_NEW' exists" 54
286         mv -f "$GIT_DIR" "$GIT_DIR_NEW" || fatal "Could not mv '$GIT_DIR' '$GIT_DIR_NEW'" 52
287
288         # Now that the repository has been renamed, we need to fix up its configuration
289         # Overwrite old name..
290         GIT_DIR="$GIT_DIR_NEW"
291         $VCSH_REPO_NAME="$VCSH_REPO_NAME_NEW"
292         # ..and clobber all old configuration
293         upgrade
294 }
295
296 run() {
297         hook pre-run
298         use
299         "$@"
300         hook post-run
301 }
302
303 status() {
304         if [ ! "x$VCSH_REPO_NAME" = "x" ]; then
305                 export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
306                 use
307                 git status --short --untracked-files='no'
308         else
309                 for VCSH_REPO_NAME in $(list); do
310                         echo "$VCSH_REPO_NAME:"
311                         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
312                         use
313                         git status --short --untracked-files='no'
314                         echo
315                 done
316         fi
317 }
318
319 upgrade() {
320         hook pre-upgrade
321         # fake-bare repositories are not bare, actually. Set this to false
322         # because otherwise Git complains "fatal: core.bare and core.worktree
323         # do not make sense"
324         git config core.bare false
325         # core.worktree may be absolute or relative to $GIT_DIR, depending on
326         # user preference
327         if [ ! "x$VCSH_WORKTREE" = 'xabsolute' ]; then
328                 git config core.worktree $(cd $GIT_DIR && GIT_WORK_TREE="$VCSH_BASE" git rev-parse --show-cdup)
329         elif [ ! "x$VCSH_WORKTREE" = 'xrelative' ]; then
330                 git config core.worktree "$VCSH_BASE"
331         fi
332         [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && git config core.excludesfile ".gitignore.d/$VCSH_REPO_NAME"
333         git config vcsh.vcsh         'true'
334         use
335         [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ] && git add -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME"
336         hook post-upgrade
337 }
338
339 use() {
340         git_dir_exists
341         export VCSH_DIRECTORY="$VCSH_REPO_NAME"
342 }
343
344 which() {
345         for VCSH_REPO_NAME in $(list); do
346                 for VCSH_FILE in $(get_files); do
347                         echo $VCSH_FILE | grep -q "$VCSH_COMMAND_PARAMETER" && echo "$VCSH_REPO_NAME: $VCSH_FILE"
348                 done
349         done | sort -u
350 }
351
352 write_gitignore() {
353         # Don't do anything if the user does not want to write gitignore
354         if [ "x$VCSH_GITIGNORE" = 'xnone' ]; then
355                 info "Not writing gitignore as '\$VCSH_GITIGNORE' is set to 'none'"
356                 exit
357         fi
358
359         use
360         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
361         gitignores=$(for file in $(git ls-files); do
362                 while true; do
363                         echo $file; new="${file%/*}"
364                         [ "$file" = "$new" ] && break
365                         file="$new"
366                 done;
367         done | sort -u)
368
369         # Contrary to GNU mktemp, mktemp on BSD/OSX requires a template for temp files
370         # Use the template GNU mktemo defaults to
371         tempfile=$(mktemp tmp.XXXXXXXXXX) || fatal "could not create tempfile" 51
372
373         echo '*' > "$tempfile" || fatal "could not write to '$tempfile'" 57
374         for gitignore in $gitignores; do
375                 echo "$gitignore" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57
376                 if [ "x$VCSH_GITIGNORE" = 'xrecursive' ] && [ -d "$gitignore" ]; then
377                         { echo "$gitignore/*" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57; }
378                 fi
379         done
380         if diff -N "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" > /dev/null; then
381                 rm -f "$tempfile" || error "could not delete '$tempfile'"
382                 exit
383         fi
384         if [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ]; then
385                 info "'$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' differs from new data, moving it to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'"
386                 mv -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak" ||
387                         fatal "could not move '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'" 53
388         fi
389         mv -f "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ||
390                 fatal "could not move '$tempfile' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME'" 53
391 }
392
393 if [ "$1" = 'clone' ]; then
394         [ -z "$2" ] && fatal "$1: please specify a remote" 1
395         export VCSH_COMMAND="$1"
396         GIT_REMOTE="$2"
397         [ -n "$3" ] && VCSH_REPO_NAME="$3" || VCSH_REPO_NAME=$(basename "$GIT_REMOTE" .git)
398         export VCSH_REPO_NAME
399         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
400 elif [ "$1" = 'version' ]; then
401         echo "$SELF $VERSION"
402         exit
403 elif [ "$1" = 'which' ]; then
404         [ -z "$2" ] && fatal "$1: please specify a filename" 1
405         [ -n "$3" ] && fatal "$1: too many parameters" 1
406         export VCSH_COMMAND="$1"
407         export VCSH_COMMAND_PARAMETER="$2"
408 elif [ "$1" = 'delete' ]           ||
409      [ "$1" = 'enter' ]            ||
410      [ "$1" = 'init' ]             ||
411      [ "$1" = 'list-tracked-by' ]  ||
412      [ "$1" = 'rename' ]           ||
413      [ "$1" = 'run' ]              ||
414      [ "$1" = 'upgrade' ]          ||
415      [ "$1" = 'write-gitignore' ]; then
416         [ -z $2 ]                      && fatal "$1: please specify repository to work on" 1
417         [ "$1" = 'rename' -a -z "$3" ] && fatal "$1: please specify a target name" 1
418         [ "$1" = 'run'    -a -z "$3" ] && fatal "$1: please specify a command" 1
419         export VCSH_COMMAND="$1"
420         export VCSH_REPO_NAME="$2"
421         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
422         [ "$VCSH_COMMAND" = 'rename' ] && { export VCSH_REPO_NAME_NEW="$3";
423                                             export GIT_DIR_NEW="$VCSH_REPO_D/$VCSH_REPO_NAME_NEW.git"; }
424         [ "$VCSH_COMMAND" = 'run' ]    && shift 2
425 elif [ "$1" = 'commit' ] ||
426      [ "$1" = 'list' ] ||
427      [ "$1" = 'list-tracked' ] ||
428      [ "$1" = 'pull' ] ||
429      [ "$1" = 'push' ]; then
430         export VCSH_COMMAND="$1"
431 elif [ "$1" = 'status' ]; then
432         export VCSH_COMMAND="$1"
433         export VCSH_REPO_NAME="$2"
434 elif [ -n "$2" ]; then
435         export VCSH_COMMAND='run'
436         export VCSH_REPO_NAME="$1"
437         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
438         [ -d $GIT_DIR ] || { help; exit 1; }
439         shift 1
440         set -- "git" "$@"
441 elif [ -n "$1" ]; then
442         export VCSH_COMMAND='enter'
443         export VCSH_REPO_NAME="$1"
444         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
445         [ -d $GIT_DIR ] || { help; exit 1; }
446 else
447         # $1 is empty, or 'help'
448         help && exit
449 fi
450
451 # Did we receive a directory instead of a name?
452 # Mangle the input to fit normal operation.
453 if echo $VCSH_REPO_NAME | grep -q '/'; then
454         export GIT_DIR=$VCSH_REPO_NAME
455         export VCSH_REPO_NAME=$(basename "$VCSH_REPO_NAME" .git)
456 fi
457
458 check_dir() {
459         check_directory="$1"
460         if [ ! -d "$check_directory" ]; then
461                 if [ -e "$check_directory" ]; then
462                         fatal "'$check_directory' exists but is not a directory" 13
463                 else
464                         verbose "attempting to create '$check_directory'"
465                         mkdir -p "$check_directory" || fatal "could not create '$check_directory'" 50
466                 fi
467         fi
468 }
469
470 check_dir "$VCSH_REPO_D"
471 [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && check_dir "$VCSH_BASE/.gitignore.d"
472
473 verbose "$VCSH_COMMAND begin"
474 export VCSH_COMMAND=$(echo $VCSH_COMMAND | sed 's/-/_/g')
475 hook pre-command
476 $VCSH_COMMAND "$@"
477 hook post-command
478 verbose "$VCSH_COMMAND end, exiting"