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

9bef842ab6ac4490b05066f2e16de5f763983b46
[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.20130724'
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
85
86 help() {
87         echo "usage: $SELF <options> <command>
88
89    options:
90    -c <file>            Source file
91    -d                   Enable debug mode
92    -v                   Enable verbose mode
93
94    commands:
95    clone <remote> \\
96          [<repo>]       Clone from an existing repository
97    commit               Commit in all repositories
98    delete <repo>        Delete an existing repository
99    enter <repo>         Enter repository; spawn new instance of \$SHELL
100    help                 Display this help text
101    init <repo>          Initialize a new repository
102    list                 List all repositories
103    list-tracked         List all files tracked by vcsh
104    list-tracked-by \\
105         <repo>          List files tracked by a repository
106    pull                 Pull from all vcsh remotes
107    push                 Push to vcsh remotes
108    rename <repo> \\
109           <newname>     Rename repository
110    run <repo> \\
111        <command>        Use this repository
112    status [<repo>]      Show statuses of all/one vcsh repositories
113    upgrade <repo>       Upgrade repository to currently recommended settings
114    version              Print version information
115    which <substring>    Find substring in name of any tracked file
116    write-gitignore \\
117    <repo>               Write .gitignore.d/<repo> via git ls-files
118
119    <repo> <git command> Shortcut to run git commands directly
120    <repo>               Shortcut to enter repository" >&2
121 }
122
123 debug() {
124         [ -n "$VCSH_DEBUG" ] && echo "$SELF: debug: $@"
125 }
126
127 verbose() {
128         if [ -n "$VCSH_DEBUG" ] || [ -n "$VCSH_VERBOSE" ]; then echo "$SELF: verbose: $@"; fi
129 }
130
131 error() {
132         echo "$SELF: error: $1" >&2
133 }
134
135 info() {
136         echo "$SELF: info: $1"
137 }
138
139 clone() {
140         hook pre-clone
141         init
142         git remote add origin "$GIT_REMOTE"
143         git config branch.master.remote origin
144         git config branch.master.merge  refs/heads/master
145         if [ $(git ls-remote origin master 2> /dev/null | wc -l ) -lt 1 ]; then
146                 info "remote is empty, not merging anything"
147                 exit
148         fi
149         git fetch
150         for object in $(git ls-tree -r origin/master | awk '{print $4}'); do
151                 [ -e "$object" ] &&
152                         error "'$object' exists." &&
153                         VCSH_CONFLICT=1;
154         done
155         [ "$VCSH_CONFLICT" = '1' ] &&
156                 fatal "will stop after fetching and not try to merge!
157   Once this situation has been resolved, run 'vcsh run $VCSH_REPO_NAME git pull' to finish cloning.\n" 17
158         git merge origin/master
159         hook post-clone
160         retire
161         hook post-clone-retired
162 }
163
164 commit() {
165         hook pre-commit
166         for VCSH_REPO_NAME in $(list); do
167                 echo "$VCSH_REPO_NAME: "
168                 export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
169                 use
170                 git commit --untracked-files=no --quiet
171                 echo
172         done
173         hook post-commit
174 }
175
176 delete() {
177         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
178         use
179         info "This operation WILL DESTROY DATA!"
180         files=$(git ls-files)
181         echo "These files will be deleted:
182
183 $files
184
185 AGAIN, THIS WILL DELETE YOUR DATA!
186 To continue, type 'Yes, do as I say'"
187         read answer
188         [ "x$answer" = 'xYes, do as I say' ] || exit 16
189         for file in $files; do
190                 rm -f $file || info "could not delete '$file', continuing with deletion"
191         done
192         rm -rf "$GIT_DIR" || error "could not delete '$GIT_DIR'"
193 }
194
195 enter() {
196         hook pre-enter
197         use
198         $SHELL
199         hook post-enter
200 }
201
202 git_dir_exists() {
203         [ -d "$GIT_DIR" ] || fatal "no repository found for '$VCSH_REPO_NAME'" 12
204 }
205
206 hook() {
207         for hook in $VCSH_HOOK_D/$1* $VCSH_HOOK_D/$VCSH_REPO_NAME.$1*; do
208                 [ -x "$hook" ] || continue
209                 verbose "executing '$hook'"
210                 "$hook"
211         done
212 }
213
214 init() {
215         hook pre-init
216         [ ! -e "$GIT_DIR" ] || fatal "'$GIT_DIR' exists" 10
217         export GIT_WORK_TREE="$VCSH_BASE"
218         mkdir -p "$GIT_WORK_TREE" || fatal "could not create '$GIT_WORK_TREE'" 50
219         cd "$GIT_WORK_TREE" || fatal "could not enter '$GIT_WORK_TREE'" 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                 echo -n "$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                 echo -n "$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 GIT_WORK_TREE
273         unset VCSH_DIRECTORY
274 }
275
276 rename() {
277         git_dir_exists
278         [ -d "$GIT_DIR_NEW" ] && fatal "'$GIT_DIR_NEW' exists" 54
279         mv -f "$GIT_DIR" "$GIT_DIR_NEW" || fatal "Could not mv '$GIT_DIR' '$GIT_DIR_NEW'" 52
280
281         # Now that the repository has been renamed, we need to fix up its configuration
282         # Overwrite old name..
283         GIT_DIR="$GIT_DIR_NEW"
284         $VCSH_REPO_NAME="$VCSH_REPO_NAME_NEW"
285         # ..and clobber all old configuration
286         upgrade
287 }
288
289 run() {
290         hook pre-run
291         use
292         "$@"
293         hook post-run
294 }
295
296 status() {
297         if [ ! "x$VCSH_REPO_NAME" = "x" ]; then
298                 export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
299                 use
300                 git status --short --untracked-files='no'
301         else
302                 for VCSH_REPO_NAME in $(list); do
303                         echo "$VCSH_REPO_NAME:"
304                         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
305                         use
306                         git status --short --untracked-files='no'
307                         echo
308                 done
309         fi
310 }
311
312 upgrade() {
313         hook pre-upgrade
314         use
315         git config core.worktree     "$GIT_WORK_TREE"
316         [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && git config core.excludesfile ".gitignore.d/$VCSH_REPO_NAME"
317         git config vcsh.vcsh         'true'
318         [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ] && git add -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME"
319         hook post-upgrade
320 }
321
322 use() {
323         git_dir_exists
324         export GIT_WORK_TREE="$(git config --get core.worktree)"
325         export VCSH_DIRECTORY="$VCSH_REPO_NAME"
326 }
327
328 which() {
329         for VCSH_REPO_NAME in $(list); do
330                 for VCSH_FILE in $(get_files); do
331                         echo $VCSH_FILE | grep -q "$VCSH_COMMAND_PARAMETER" && echo "$VCSH_REPO_NAME: $VCSH_FILE"
332                 done
333         done | sort -u
334 }
335
336 write_gitignore() {
337         # Don't do anything if the user does not want to write gitignore
338         if [ "x$VCSH_GITIGNORE" = 'xnone' ]; then
339                 info "Not writing gitignore as '\$VCSH_GITIGNORE' is set to 'none'"
340                 exit
341         fi
342
343         use
344         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
345         gitignores=$(for file in $(git ls-files); do
346                 while true; do
347                         echo $file; new="${file%/*}"
348                         [ "$file" = "$new" ] && break
349                         file="$new"
350                 done;
351         done | sort -u)
352         tempfile=$(mktemp) || fatal "could not create tempfile" 51
353         echo '*' > "$tempfile" || fatal "could not write to '$tempfile'" 57
354         for gitignore in $gitignores; do
355                 echo "$gitignore" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57
356                 if [ "x$VCSH_GITIGNORE" = 'xrecursive' ] && [ -d "$gitignore" ]; then
357                         { echo "$gitignore/*" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57; }
358                 fi
359         done
360         if diff -N "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" > /dev/null; then
361                 rm -f "$tempfile" || error "could not delete '$tempfile'"
362                 exit
363         fi
364         if [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ]; then
365                 info "'$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' differs from new data, moving it to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'"
366                 mv -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak" ||
367                         fatal "could not move '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'" 53
368         fi
369         mv -f "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ||
370                 fatal "could not move '$tempfile' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME'" 53
371 }
372
373 if [ ! "x$VCSH_GITIGNORE" = 'xexact' ] && [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && [ ! "x$VCSH_GITIGNORE" = 'xrecursive' ]; then
374         fatal "'\$VCSH_GITIGNORE' must equal 'exact', 'none', or 'recursive'" 1
375 fi
376
377 if [ "$1" = 'clone' ]; then
378         [ -z "$2" ] && fatal "$1: please specify a remote" 1
379         export VCSH_COMMAND="$1"
380         GIT_REMOTE="$2"
381         [ -n "$3" ] && VCSH_REPO_NAME="$3" || VCSH_REPO_NAME=$(basename "$GIT_REMOTE" .git)
382         export VCSH_REPO_NAME
383         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
384 elif [ "$1" = 'version' ]; then
385         echo "$SELF $VERSION"
386         exit
387 elif [ "$1" = 'which' ]; then
388         [ -z "$2" ] && fatal "$1: please specify a filename" 1
389         [ -n "$3" ] && fatal "$1: too many parameters" 1
390         export VCSH_COMMAND="$1"
391         export VCSH_COMMAND_PARAMETER="$2"
392 elif [ "$1" = 'delete' ]           ||
393      [ "$1" = 'enter' ]            ||
394      [ "$1" = 'init' ]             ||
395      [ "$1" = 'list-tracked-by' ]  ||
396      [ "$1" = 'rename' ]           ||
397      [ "$1" = 'run' ]              ||
398      [ "$1" = 'upgrade' ]          ||
399      [ "$1" = 'write-gitignore' ]; then
400         [ -z $2 ]                      && fatal "$1: please specify repository to work on" 1
401         [ "$1" = 'rename' -a -z "$3" ] && fatal "$1: please specify a target name" 1
402         [ "$1" = 'run'    -a -z "$3" ] && fatal "$1: please specify a command" 1
403         export VCSH_COMMAND="$1"
404         export VCSH_REPO_NAME="$2"
405         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
406         [ "$VCSH_COMMAND" = 'rename' ] && { export VCSH_REPO_NAME_NEW="$3";
407                                             export GIT_DIR_NEW="$VCSH_REPO_D/$VCSH_REPO_NAME_NEW.git"; }
408         [ "$VCSH_COMMAND" = 'run' ]    && shift 2
409 elif [ "$1" = 'commit' ] ||
410      [ "$1" = 'list' ] ||
411      [ "$1" = 'list-tracked' ] ||
412      [ "$1" = 'pull' ] ||
413      [ "$1" = 'push' ]; then
414         export VCSH_COMMAND="$1"
415 elif [ "$1" = 'status' ]; then
416         export VCSH_COMMAND="$1"
417         export VCSH_REPO_NAME="$2"
418 elif [ -n "$2" ]; then
419         export VCSH_COMMAND='run'
420         export VCSH_REPO_NAME="$1"
421         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
422         [ -d $GIT_DIR ] || { help; exit 1; }
423         shift 1
424         set -- "git" "$@"
425 elif [ -n "$1" ]; then
426         export VCSH_COMMAND='enter'
427         export VCSH_REPO_NAME="$1"
428         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
429         [ -d $GIT_DIR ] || { help; exit 1; }
430 else
431         # $1 is empty, or 'help'
432         help && exit
433 fi
434
435 # Did we receive a directory instead of a name?
436 # Mangle the input to fit normal operation.
437 if echo $VCSH_REPO_NAME | grep -q '/'; then
438         export GIT_DIR=$VCSH_REPO_NAME
439         export VCSH_REPO_NAME=$(basename "$VCSH_REPO_NAME" .git)
440 fi
441
442 check_dir() {
443         check_directory="$1"
444         if [ ! -d "$check_directory" ]; then
445                 if [ -e "$check_directory" ]; then
446                         fatal "'$check_directory' exists but is not a directory" 13
447                 else
448                         verbose "attempting to create '$check_directory'"
449                         mkdir -p "$check_directory" || fatal "could not create '$check_directory'" 50
450                 fi
451         fi
452 }
453
454 check_dir "$VCSH_REPO_D"
455 [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && check_dir "$VCSH_BASE/.gitignore.d"
456
457 verbose "$VCSH_COMMAND begin"
458 export VCSH_COMMAND=$(echo $VCSH_COMMAND | sed 's/-/_/g')
459 hook pre-command
460 $VCSH_COMMAND "$@"
461 hook post-command
462 verbose "$VCSH_COMMAND end, exiting"