]> 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 'merge--madduck/relative_worktree'
[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.20130829'
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_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         for object in $(git ls-tree -r origin/master | awk '{print $4}'); do
156                 [ -e "$object" ] &&
157                         error "'$object' exists." &&
158                         VCSH_CONFLICT=1;
159         done
160         [ "$VCSH_CONFLICT" = '1' ] &&
161                 fatal "will stop after fetching and not try to merge!
162   Once this situation has been resolved, run 'vcsh run $VCSH_REPO_NAME git pull' to finish cloning.\n" 17
163         git merge origin/master
164         hook post-clone
165         retire
166         hook post-clone-retired
167 }
168
169 commit() {
170         hook pre-commit
171         for VCSH_REPO_NAME in $(list); do
172                 echo "$VCSH_REPO_NAME: "
173                 export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
174                 use
175                 git commit --untracked-files=no --quiet
176                 echo
177         done
178         hook post-commit
179 }
180
181 delete() {
182         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
183         use
184         info "This operation WILL DESTROY DATA!"
185         files=$(git ls-files)
186         echo "These files will be deleted:
187
188 $files
189
190 AGAIN, THIS WILL DELETE YOUR DATA!
191 To continue, type 'Yes, do as I say'"
192         read answer
193         [ "x$answer" = 'xYes, do as I say' ] || exit 16
194         for file in $files; do
195                 rm -f $file || info "could not delete '$file', continuing with deletion"
196         done
197         rm -rf "$GIT_DIR" || error "could not delete '$GIT_DIR'"
198 }
199
200 enter() {
201         hook pre-enter
202         use
203         $SHELL
204         hook post-enter
205 }
206
207 git_dir_exists() {
208         [ -d "$GIT_DIR" ] || fatal "no repository found for '$VCSH_REPO_NAME'" 12
209 }
210
211 hook() {
212         for hook in $VCSH_HOOK_D/$1* $VCSH_HOOK_D/$VCSH_REPO_NAME.$1*; do
213                 [ -x "$hook" ] || continue
214                 verbose "executing '$hook'"
215                 "$hook"
216         done
217 }
218
219 init() {
220         hook pre-init
221         [ ! -e "$GIT_DIR" ] || fatal "'$GIT_DIR' exists" 10
222         mkdir -p "$VCSH_BASE" || fatal "could not create '$VCSH_BASE'" 50
223         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
224         git init
225         upgrade
226         hook post-init
227 }
228
229 list() {
230         for repo in "$VCSH_REPO_D"/*.git; do
231                 [ -d "$repo" ] && [ -r "$repo" ] && echo $(basename "$repo" .git)
232         done
233 }
234
235 get_files() {
236         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
237         git ls-files
238 }
239
240 list_tracked() {
241         for VCSH_REPO_NAME in $(list); do
242                 get_files
243         done | sort -u
244 }
245
246 list_tracked_by() {
247         use
248         git ls-files | sort -u
249 }
250
251 pull() {
252         hook pre-pull
253         for VCSH_REPO_NAME in $(list); do
254                 echo -n "$VCSH_REPO_NAME: "
255                 export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
256                 use
257                 git pull
258                 echo
259         done
260         hook post-pull
261 }
262
263 push() {
264         hook pre-push
265         for VCSH_REPO_NAME in $(list); do
266                 echo -n "$VCSH_REPO_NAME: "
267                 export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
268                 use
269                 git push
270                 echo
271         done
272         hook post-push
273 }
274
275 retire() {
276         unset VCSH_DIRECTORY
277 }
278
279 rename() {
280         git_dir_exists
281         [ -d "$GIT_DIR_NEW" ] && fatal "'$GIT_DIR_NEW' exists" 54
282         mv -f "$GIT_DIR" "$GIT_DIR_NEW" || fatal "Could not mv '$GIT_DIR' '$GIT_DIR_NEW'" 52
283
284         # Now that the repository has been renamed, we need to fix up its configuration
285         # Overwrite old name..
286         GIT_DIR="$GIT_DIR_NEW"
287         $VCSH_REPO_NAME="$VCSH_REPO_NAME_NEW"
288         # ..and clobber all old configuration
289         upgrade
290 }
291
292 run() {
293         hook pre-run
294         use
295         "$@"
296         hook post-run
297 }
298
299 status() {
300         if [ ! "x$VCSH_REPO_NAME" = "x" ]; then
301                 export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
302                 use
303                 git status --short --untracked-files='no'
304         else
305                 for VCSH_REPO_NAME in $(list); do
306                         echo "$VCSH_REPO_NAME:"
307                         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
308                         use
309                         git status --short --untracked-files='no'
310                         echo
311                 done
312         fi
313 }
314
315 upgrade() {
316         hook pre-upgrade
317         # fake-bare repositories are not bare, actually. Set this to false
318         # because otherwise Git complains "fatal: core.bare and core.worktree
319         # do not make sense"
320         git config core.bare false
321         # core.worktree may be absolute or relative to $GIT_DIR, depending on
322         # user preference
323         if [ ! "x$VCSH_WORKTREE" = 'xabsolute' ]; then
324                 git config core.worktree $(cd $GIT_DIR && GIT_WORK_TREE="$VCSH_BASE" git rev-parse --show-cdup)
325         elif [ ! "x$VCSH_WORKTREE" = 'xrelative' ]; then
326                 git config core.worktree "$VCSH_BASE"
327         fi
328         [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && git config core.excludesfile ".gitignore.d/$VCSH_REPO_NAME"
329         git config vcsh.vcsh         'true'
330         use
331         [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ] && git add -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME"
332         hook post-upgrade
333 }
334
335 use() {
336         git_dir_exists
337         export VCSH_DIRECTORY="$VCSH_REPO_NAME"
338 }
339
340 which() {
341         for VCSH_REPO_NAME in $(list); do
342                 for VCSH_FILE in $(get_files); do
343                         echo $VCSH_FILE | grep -q "$VCSH_COMMAND_PARAMETER" && echo "$VCSH_REPO_NAME: $VCSH_FILE"
344                 done
345         done | sort -u
346 }
347
348 write_gitignore() {
349         # Don't do anything if the user does not want to write gitignore
350         if [ "x$VCSH_GITIGNORE" = 'xnone' ]; then
351                 info "Not writing gitignore as '\$VCSH_GITIGNORE' is set to 'none'"
352                 exit
353         fi
354
355         use
356         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
357         gitignores=$(for file in $(git ls-files); do
358                 while true; do
359                         echo $file; new="${file%/*}"
360                         [ "$file" = "$new" ] && break
361                         file="$new"
362                 done;
363         done | sort -u)
364         tempfile=$(mktemp) || fatal "could not create tempfile" 51
365         echo '*' > "$tempfile" || fatal "could not write to '$tempfile'" 57
366         for gitignore in $gitignores; do
367                 echo "$gitignore" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57
368                 if [ "x$VCSH_GITIGNORE" = 'xrecursive' ] && [ -d "$gitignore" ]; then
369                         { echo "$gitignore/*" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57; }
370                 fi
371         done
372         if diff -N "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" > /dev/null; then
373                 rm -f "$tempfile" || error "could not delete '$tempfile'"
374                 exit
375         fi
376         if [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ]; then
377                 info "'$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' differs from new data, moving it to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'"
378                 mv -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak" ||
379                         fatal "could not move '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'" 53
380         fi
381         mv -f "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ||
382                 fatal "could not move '$tempfile' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME'" 53
383 }
384
385 if [ ! "x$VCSH_GITIGNORE" = 'xexact' ] && [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && [ ! "x$VCSH_GITIGNORE" = 'xrecursive' ]; then
386         fatal "'\$VCSH_GITIGNORE' must equal 'exact', 'none', or 'recursive'" 1
387 fi
388
389 if [ "$1" = 'clone' ]; then
390         [ -z "$2" ] && fatal "$1: please specify a remote" 1
391         export VCSH_COMMAND="$1"
392         GIT_REMOTE="$2"
393         [ -n "$3" ] && VCSH_REPO_NAME="$3" || VCSH_REPO_NAME=$(basename "$GIT_REMOTE" .git)
394         export VCSH_REPO_NAME
395         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
396 elif [ "$1" = 'version' ]; then
397         echo "$SELF $VERSION"
398         exit
399 elif [ "$1" = 'which' ]; then
400         [ -z "$2" ] && fatal "$1: please specify a filename" 1
401         [ -n "$3" ] && fatal "$1: too many parameters" 1
402         export VCSH_COMMAND="$1"
403         export VCSH_COMMAND_PARAMETER="$2"
404 elif [ "$1" = 'delete' ]           ||
405      [ "$1" = 'enter' ]            ||
406      [ "$1" = 'init' ]             ||
407      [ "$1" = 'list-tracked-by' ]  ||
408      [ "$1" = 'rename' ]           ||
409      [ "$1" = 'run' ]              ||
410      [ "$1" = 'upgrade' ]          ||
411      [ "$1" = 'write-gitignore' ]; then
412         [ -z $2 ]                      && fatal "$1: please specify repository to work on" 1
413         [ "$1" = 'rename' -a -z "$3" ] && fatal "$1: please specify a target name" 1
414         [ "$1" = 'run'    -a -z "$3" ] && fatal "$1: please specify a command" 1
415         export VCSH_COMMAND="$1"
416         export VCSH_REPO_NAME="$2"
417         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
418         [ "$VCSH_COMMAND" = 'rename' ] && { export VCSH_REPO_NAME_NEW="$3";
419                                             export GIT_DIR_NEW="$VCSH_REPO_D/$VCSH_REPO_NAME_NEW.git"; }
420         [ "$VCSH_COMMAND" = 'run' ]    && shift 2
421 elif [ "$1" = 'commit' ] ||
422      [ "$1" = 'list' ] ||
423      [ "$1" = 'list-tracked' ] ||
424      [ "$1" = 'pull' ] ||
425      [ "$1" = 'push' ]; then
426         export VCSH_COMMAND="$1"
427 elif [ "$1" = 'status' ]; then
428         export VCSH_COMMAND="$1"
429         export VCSH_REPO_NAME="$2"
430 elif [ -n "$2" ]; then
431         export VCSH_COMMAND='run'
432         export VCSH_REPO_NAME="$1"
433         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
434         [ -d $GIT_DIR ] || { help; exit 1; }
435         shift 1
436         set -- "git" "$@"
437 elif [ -n "$1" ]; then
438         export VCSH_COMMAND='enter'
439         export VCSH_REPO_NAME="$1"
440         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
441         [ -d $GIT_DIR ] || { help; exit 1; }
442 else
443         # $1 is empty, or 'help'
444         help && exit
445 fi
446
447 # Did we receive a directory instead of a name?
448 # Mangle the input to fit normal operation.
449 if echo $VCSH_REPO_NAME | grep -q '/'; then
450         export GIT_DIR=$VCSH_REPO_NAME
451         export VCSH_REPO_NAME=$(basename "$VCSH_REPO_NAME" .git)
452 fi
453
454 check_dir() {
455         check_directory="$1"
456         if [ ! -d "$check_directory" ]; then
457                 if [ -e "$check_directory" ]; then
458                         fatal "'$check_directory' exists but is not a directory" 13
459                 else
460                         verbose "attempting to create '$check_directory'"
461                         mkdir -p "$check_directory" || fatal "could not create '$check_directory'" 50
462                 fi
463         fi
464 }
465
466 check_dir "$VCSH_REPO_D"
467 [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && check_dir "$VCSH_BASE/.gitignore.d"
468
469 verbose "$VCSH_COMMAND begin"
470 export VCSH_COMMAND=$(echo $VCSH_COMMAND | sed 's/-/_/g')
471 hook pre-command
472 $VCSH_COMMAND "$@"
473 hook post-command
474 verbose "$VCSH_COMMAND end, exiting"