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

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