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

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