]> 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 commit`
[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    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    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 }
158
159 commit() {
160         hook pre-commit
161         for VCSH_REPO_NAME in $(list); do
162                 echo "$VCSH_REPO_NAME: "
163                 export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
164                 use
165                 git commit --untracked-files=no --quiet
166                 echo
167         done
168         hook post-commit
169 }
170
171 delete() {
172         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
173         use
174         info "This operation WILL DESTROY DATA!"
175         files=$(git ls-files)
176         echo "These files will be deleted:
177
178 $files
179
180 AGAIN, THIS WILL DELETE YOUR DATA!
181 To continue, type 'Yes, do as I say'"
182         read answer
183         [ "x$answer" = 'xYes, do as I say' ] || exit 16
184         for file in $files; do
185                 rm -f $file || info "could not delete '$file', continuing with deletion"
186         done
187         rm -rf "$GIT_DIR" || error "could not delete '$GIT_DIR'"
188 }
189
190 enter() {
191         hook pre-enter
192         use
193         $SHELL
194         hook post-enter
195 }
196
197 git_dir_exists() {
198         [ -d "$GIT_DIR" ] || fatal "no repository found for '$VCSH_REPO_NAME'" 12
199 }
200
201 hook() {
202         for hook in $VCSH_HOOK_D/$1* $VCSH_HOOK_D/$VCSH_REPO_NAME.$1*; do
203                 [ -x "$hook" ] || continue
204                 verbose "executing '$hook'"
205                 "$hook"
206         done
207 }
208
209 init() {
210         [ ! -e "$GIT_DIR" ] || fatal "'$GIT_DIR' exists" 10
211         export GIT_WORK_TREE="$VCSH_BASE"
212         mkdir -p "$GIT_WORK_TREE" || fatal "could not create '$GIT_WORK_TREE'" 50
213         cd "$GIT_WORK_TREE" || fatal "could not enter '$GIT_WORK_TREE'" 11
214         git init
215         upgrade
216 }
217
218 list() {
219         for repo in "$VCSH_REPO_D"/*.git; do
220                 [ -d "$repo" ] && [ -r "$repo" ] && echo $(basename "$repo" .git)
221         done
222 }
223
224 get_files() {
225         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
226         git ls-files
227 }
228
229 list_tracked() {
230         for VCSH_REPO_NAME in $(list); do
231                 get_files
232         done | sort -u
233 }
234
235 list_tracked_by() {
236         use
237         git ls-files | sort -u
238 }
239
240 pull() {
241         hook pre-pull
242         for VCSH_REPO_NAME in $(list); do
243                 echo -n "$VCSH_REPO_NAME: "
244                 export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
245                 use
246                 git pull
247         done
248         hook post-pull
249 }
250
251 push() {
252         hook pre-push
253         for VCSH_REPO_NAME in $(list); do
254                 echo -n "$VCSH_REPO_NAME: "
255                 export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
256                 use
257                 git push
258         done
259         hook post-push
260 }
261
262 rename() {
263         git_dir_exists
264         [ -d "$GIT_DIR_NEW" ] && fatal "'$GIT_DIR_NEW' exists" 54
265         mv -f "$GIT_DIR" "$GIT_DIR_NEW" || fatal "Could not mv '$GIT_DIR' '$GIT_DIR_NEW'" 52
266
267 }
268
269 run() {
270         hook pre-run
271         use
272         "$@"
273         hook post-run
274 }
275
276 upgrade() {
277         hook pre-upgrade
278         use
279         git config core.worktree     "$GIT_WORK_TREE"
280         git config core.excludesfile ".gitignore.d/$VCSH_REPO_NAME"
281         git config vcsh.vcsh         'true'
282         [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ] && git add -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME"
283         hook post-upgrade
284 }
285
286 use() {
287         git_dir_exists
288         export GIT_WORK_TREE="$(git config --get core.worktree)"
289         export VCSH_DIRECTORY="$VCSH_REPO_NAME"
290 }
291
292 which() {
293         for VCSH_REPO_NAME in $(list); do
294                 for VCSH_FILE in $(get_files); do
295                         echo $VCSH_FILE | grep -q "$VCSH_COMMAND_PARAMETER" && echo "$VCSH_REPO_NAME: $VCSH_FILE"
296                 done
297         done | sort -u
298 }
299
300 write_gitignore() {
301         use
302         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
303         gitignores=$(for file in $(git ls-files); do
304                 while true; do
305                         echo $file; new="${file%/*}"
306                         [ "$file" = "$new" ] && break
307                         file="$new"
308                 done;
309         done | sort -u)
310         tempfile=$(mktemp) || fatal "could not create tempfile" 51
311         echo '*' > "$tempfile" || fatal "could not write to '$tempfile'" 57
312         for gitignore in $gitignores; do
313                 echo "$gitignore" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57
314                 if [ x$VCSH_GITIGNORE = x'recursive' ] && [ -d "$gitignore" ]; then
315                         { echo "$gitignore/*" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57; }
316                 fi
317         done
318         if diff -N "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" > /dev/null; then
319                 rm -f "$tempfile" || error "could not delete '$tempfile'"
320                 exit
321         fi
322         if [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ]; then
323                 info "'$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' differs from new data, moving it to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'"
324                 mv -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak" ||
325                         fatal "could not move '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'" 53
326         fi
327         mv -f "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ||
328                 fatal "could not move '$tempfile' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME'" 53
329 }
330
331 if [ ! "x$VCSH_GITIGNORE" = 'xexact' ] && [ ! "x$VCSH_GITIGNORE" = 'xrecursive' ]; then
332         fatal "'\$VCSH_GITIGNORE' must be either 'exact' or 'recursive'" 1
333 fi
334
335 if [ "$1" = 'clone' ]; then
336         [ -z "$2" ] && fatal "$1: please specify a remote" 1
337         export VCSH_COMMAND="$1"
338         GIT_REMOTE="$2"
339         [ -n "$3" ] && VCSH_REPO_NAME="$3" || VCSH_REPO_NAME=$(basename "$GIT_REMOTE" .git)
340         export VCSH_REPO_NAME
341         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
342 elif [ "$1" = 'version' ]; then
343         echo "$SELF $VERSION"
344         exit
345 elif [ "$1" = 'which' ]; then
346         [ -z "$2" ] && fatal "$1: please specify a filename" 1
347         [ -n "$3" ] && fatal "$1: too many parameters" 1
348         export VCSH_COMMAND="$1"
349         export VCSH_COMMAND_PARAMETER="$2"
350 elif [ "$1" = 'delete' ]           ||
351      [ "$1" = 'enter' ]            ||
352      [ "$1" = 'init' ]             ||
353      [ "$1" = 'list-tracked-by' ]  ||
354      [ "$1" = 'rename' ]           ||
355      [ "$1" = 'run' ]              ||
356      [ "$1" = 'upgrade' ]          ||
357      [ "$1" = 'write-gitignore' ]; then
358         [ -z $2 ]                      && fatal "$1: please specify repository to work on" 1
359         [ "$1" = 'rename' -a -z "$3" ] && fatal "$1: please specify a target name" 1
360         [ "$1" = 'run' -a -z "$3" ]    && fatal "$1: please specify a command" 1
361         export VCSH_COMMAND="$1"
362         export VCSH_REPO_NAME="$2"
363         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
364         [ "$VCSH_COMMAND" = 'rename' ]         && export GIT_DIR_NEW="$VCSH_REPO_D/$3.git"
365         [ "$VCSH_COMMAND" = 'run' ] && shift 2
366         [ "$VCSH_COMMAND" = 'write-gitignore' ]
367 elif [ "$1" = 'commit' ] ||
368      [ "$1" = 'list' ] ||
369      [ "$1" = 'list-tracked' ] ||
370      [ "$1" = 'pull' ] ||
371      [ "$1" = 'push' ]; then
372         export VCSH_COMMAND="$1"
373 elif [ -n "$2" ]; then
374         export VCSH_COMMAND='run'
375         export VCSH_REPO_NAME="$1"
376         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
377         [ -d $GIT_DIR ] || { help; exit 1; }
378         shift 1
379         set -- "git" "$@"
380 elif [ -n "$1" ]; then
381         export VCSH_COMMAND='enter'
382         export VCSH_REPO_NAME="$1"
383         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
384         [ -d $GIT_DIR ] || { help; exit 1; }
385 else
386         # $1 is empty, or 'help'
387         help && exit
388 fi
389
390 # Did we receive a directory instead of a name?
391 # Mangle the input to fit normal operation.
392 if echo $VCSH_REPO_NAME | grep -q '/'; then
393         export GIT_DIR=$VCSH_REPO_NAME
394         export VCSH_REPO_NAME=$(basename "$VCSH_REPO_NAME" .git)
395 fi
396
397
398 for check_directory in "$VCSH_REPO_D" "$VCSH_BASE/.gitignore.d"
399 do
400         if [ ! -d "$check_directory" ]; then
401                 if [ -e "$check_directory" ]; then
402                         fatal "'$check_directory' exists but is not a directory" 13
403                 else
404                         info "attempting to create '$check_directory'"
405                         mkdir -p "$check_directory" || fatal "could not create '$check_directory'" 50
406                 fi
407         fi
408 done
409
410 verbose "$VCSH_COMMAND begin"
411 export VCSH_COMMAND=$(echo $VCSH_COMMAND | sed 's/-/_/g')
412 hook pre-command
413 $VCSH_COMMAND "$@"
414 hook post-command
415 verbose "$VCSH_COMMAND end, exiting"