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

vcsh: Move up gitignore check
[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_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         tempfile=$(mktemp) || fatal "could not create tempfile" 51
369         echo '*' > "$tempfile" || fatal "could not write to '$tempfile'" 57
370         for gitignore in $gitignores; do
371                 echo "$gitignore" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57
372                 if [ "x$VCSH_GITIGNORE" = 'xrecursive' ] && [ -d "$gitignore" ]; then
373                         { echo "$gitignore/*" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57; }
374                 fi
375         done
376         if diff -N "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" > /dev/null; then
377                 rm -f "$tempfile" || error "could not delete '$tempfile'"
378                 exit
379         fi
380         if [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ]; then
381                 info "'$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' differs from new data, moving it to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'"
382                 mv -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak" ||
383                         fatal "could not move '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'" 53
384         fi
385         mv -f "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ||
386                 fatal "could not move '$tempfile' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME'" 53
387 }
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"