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