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

README.md: Make intro less scary
[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               Show statuses of all 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         [ ! -e "$GIT_DIR" ] || fatal "'$GIT_DIR' exists" 10
216         export GIT_WORK_TREE="$VCSH_BASE"
217         mkdir -p "$GIT_WORK_TREE" || fatal "could not create '$GIT_WORK_TREE'" 50
218         cd "$GIT_WORK_TREE" || fatal "could not enter '$GIT_WORK_TREE'" 11
219         git init
220         upgrade
221 }
222
223 list() {
224         for repo in "$VCSH_REPO_D"/*.git; do
225                 [ -d "$repo" ] && [ -r "$repo" ] && echo $(basename "$repo" .git)
226         done
227 }
228
229 get_files() {
230         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
231         git ls-files
232 }
233
234 list_tracked() {
235         for VCSH_REPO_NAME in $(list); do
236                 get_files
237         done | sort -u
238 }
239
240 list_tracked_by() {
241         use
242         git ls-files | sort -u
243 }
244
245 pull() {
246         hook pre-pull
247         for VCSH_REPO_NAME in $(list); do
248                 echo -n "$VCSH_REPO_NAME: "
249                 export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
250                 use
251                 git pull
252                 echo
253         done
254         hook post-pull
255 }
256
257 push() {
258         hook pre-push
259         for VCSH_REPO_NAME in $(list); do
260                 echo -n "$VCSH_REPO_NAME: "
261                 export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
262                 use
263                 git push
264                 echo
265         done
266         hook post-push
267 }
268
269 retire() {
270         unset GIT_WORK_TREE
271         unset VCSH_DIRECTORY
272 }
273
274 rename() {
275         git_dir_exists
276         [ -d "$GIT_DIR_NEW" ] && fatal "'$GIT_DIR_NEW' exists" 54
277         mv -f "$GIT_DIR" "$GIT_DIR_NEW" || fatal "Could not mv '$GIT_DIR' '$GIT_DIR_NEW'" 52
278
279         # Now that the repository has been renamed, we need to fix up its configuration
280         # Overwrite old name..
281         GIT_DIR="$GIT_DIR_NEW"
282         $VCSH_REPO_NAME="$VCSH_REPO_NAME_NEW"
283         # ..and clobber all old configuration
284         upgrade
285 }
286
287 run() {
288         hook pre-run
289         use
290         "$@"
291         hook post-run
292 }
293
294 status() {
295         for VCSH_REPO_NAME in $(list); do
296                 echo "$VCSH_REPO_NAME:"
297                 export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
298                 use
299                 git status --short --untracked-files='no'
300                 echo
301         done
302 }
303
304 upgrade() {
305         hook pre-upgrade
306         use
307         git config core.worktree     "$GIT_WORK_TREE"
308         [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && git config core.excludesfile ".gitignore.d/$VCSH_REPO_NAME"
309         git config vcsh.vcsh         'true'
310         [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ] && git add -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME"
311         hook post-upgrade
312 }
313
314 use() {
315         git_dir_exists
316         export GIT_WORK_TREE="$(git config --get core.worktree)"
317         export VCSH_DIRECTORY="$VCSH_REPO_NAME"
318 }
319
320 which() {
321         for VCSH_REPO_NAME in $(list); do
322                 for VCSH_FILE in $(get_files); do
323                         echo $VCSH_FILE | grep -q "$VCSH_COMMAND_PARAMETER" && echo "$VCSH_REPO_NAME: $VCSH_FILE"
324                 done
325         done | sort -u
326 }
327
328 write_gitignore() {
329         # Don't do anything if the user does not want to write gitignore
330         if [ "x$VCSH_GITIGNORE" = 'xnone' ]; then
331                 info "Not writing gitignore as '\$VCSH_GITIGNORE' is set to 'none'"
332                 exit
333         fi
334
335         use
336         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
337         gitignores=$(for file in $(git ls-files); do
338                 while true; do
339                         echo $file; new="${file%/*}"
340                         [ "$file" = "$new" ] && break
341                         file="$new"
342                 done;
343         done | sort -u)
344         tempfile=$(mktemp) || fatal "could not create tempfile" 51
345         echo '*' > "$tempfile" || fatal "could not write to '$tempfile'" 57
346         for gitignore in $gitignores; do
347                 echo "$gitignore" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57
348                 if [ "x$VCSH_GITIGNORE" = 'xrecursive' ] && [ -d "$gitignore" ]; then
349                         { echo "$gitignore/*" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57; }
350                 fi
351         done
352         if diff -N "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" > /dev/null; then
353                 rm -f "$tempfile" || error "could not delete '$tempfile'"
354                 exit
355         fi
356         if [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ]; then
357                 info "'$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' differs from new data, moving it to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'"
358                 mv -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak" ||
359                         fatal "could not move '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'" 53
360         fi
361         mv -f "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ||
362                 fatal "could not move '$tempfile' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME'" 53
363 }
364
365 if [ ! "x$VCSH_GITIGNORE" = 'xexact' ] && [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && [ ! "x$VCSH_GITIGNORE" = 'xrecursive' ]; then
366         fatal "'\$VCSH_GITIGNORE' must equal 'exact', 'none', or 'recursive'" 1
367 fi
368
369 if [ "$1" = 'clone' ]; then
370         [ -z "$2" ] && fatal "$1: please specify a remote" 1
371         export VCSH_COMMAND="$1"
372         GIT_REMOTE="$2"
373         [ -n "$3" ] && VCSH_REPO_NAME="$3" || VCSH_REPO_NAME=$(basename "$GIT_REMOTE" .git)
374         export VCSH_REPO_NAME
375         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
376 elif [ "$1" = 'version' ]; then
377         echo "$SELF $VERSION"
378         exit
379 elif [ "$1" = 'which' ]; then
380         [ -z "$2" ] && fatal "$1: please specify a filename" 1
381         [ -n "$3" ] && fatal "$1: too many parameters" 1
382         export VCSH_COMMAND="$1"
383         export VCSH_COMMAND_PARAMETER="$2"
384 elif [ "$1" = 'delete' ]           ||
385      [ "$1" = 'enter' ]            ||
386      [ "$1" = 'init' ]             ||
387      [ "$1" = 'list-tracked-by' ]  ||
388      [ "$1" = 'rename' ]           ||
389      [ "$1" = 'run' ]              ||
390      [ "$1" = 'upgrade' ]          ||
391      [ "$1" = 'write-gitignore' ]; then
392         [ -z $2 ]                      && fatal "$1: please specify repository to work on" 1
393         [ "$1" = 'rename' -a -z "$3" ] && fatal "$1: please specify a target name" 1
394         [ "$1" = 'run'    -a -z "$3" ] && fatal "$1: please specify a command" 1
395         export VCSH_COMMAND="$1"
396         export VCSH_REPO_NAME="$2"
397         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
398         [ "$VCSH_COMMAND" = 'rename' ] && { export VCSH_REPO_NAME_NEW="$3";
399                                             export GIT_DIR_NEW="$VCSH_REPO_D/$VCSH_REPO_NAME_NEW.git"; }
400         [ "$VCSH_COMMAND" = 'run' ]    && shift 2
401 elif [ "$1" = 'commit' ] ||
402      [ "$1" = 'list' ] ||
403      [ "$1" = 'list-tracked' ] ||
404      [ "$1" = 'pull' ] ||
405      [ "$1" = 'push' ] ||
406      [ "$1" = 'status' ]; then
407         export VCSH_COMMAND="$1"
408 elif [ -n "$2" ]; then
409         export VCSH_COMMAND='run'
410         export VCSH_REPO_NAME="$1"
411         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
412         [ -d $GIT_DIR ] || { help; exit 1; }
413         shift 1
414         set -- "git" "$@"
415 elif [ -n "$1" ]; then
416         export VCSH_COMMAND='enter'
417         export VCSH_REPO_NAME="$1"
418         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
419         [ -d $GIT_DIR ] || { help; exit 1; }
420 else
421         # $1 is empty, or 'help'
422         help && exit
423 fi
424
425 # Did we receive a directory instead of a name?
426 # Mangle the input to fit normal operation.
427 if echo $VCSH_REPO_NAME | grep -q '/'; then
428         export GIT_DIR=$VCSH_REPO_NAME
429         export VCSH_REPO_NAME=$(basename "$VCSH_REPO_NAME" .git)
430 fi
431
432 check_dir() {
433         check_directory="$1"
434         if [ ! -d "$check_directory" ]; then
435                 if [ -e "$check_directory" ]; then
436                         fatal "'$check_directory' exists but is not a directory" 13
437                 else
438                         info "attempting to create '$check_directory'"
439                         mkdir -p "$check_directory" || fatal "could not create '$check_directory'" 50
440                 fi
441         fi
442 }
443
444 check_dir "$VCSH_REPO_D"
445 [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && check_dir "$VCSH_BASE/.gitignore.d"
446
447 verbose "$VCSH_COMMAND begin"
448 export VCSH_COMMAND=$(echo $VCSH_COMMAND | sed 's/-/_/g')
449 hook pre-command
450 $VCSH_COMMAND "$@"
451 hook post-command
452 verbose "$VCSH_COMMAND end, exiting"