]> 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 'retire'
[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    upgrade <repo>       Upgrade repository to currently recommended settings
110    version              Print version information
111    which <substring>    Find substring in name of any tracked file
112    write-gitignore \\
113    <repo>               Write .gitignore.d/<repo> via git ls-files
114
115    <repo> <git command> Shortcut to run git commands directly
116    <repo>               Shortcut to enter repository" >&2
117 }
118
119 debug() {
120         [ -n "$VCSH_DEBUG" ] && echo "$SELF: debug: $@"
121 }
122
123 verbose() {
124         if [ -n "$VCSH_DEBUG" ] || [ -n "$VCSH_VERBOSE" ]; then echo "$SELF: verbose: $@"; fi
125 }
126
127 error() {
128         echo "$SELF: error: $1" >&2
129 }
130
131 info() {
132         echo "$SELF: info: $1"
133 }
134
135 clone() {
136         hook pre-clone
137         init
138         git remote add origin "$GIT_REMOTE"
139         git config branch.master.remote origin
140         git config branch.master.merge  refs/heads/master
141         if [ $(git ls-remote origin master 2> /dev/null | wc -l ) -lt 1 ]; then
142                 info "remote is empty, not merging anything"
143                 exit
144         fi
145         git fetch
146         for object in $(git ls-tree -r origin/master | awk '{print $4}'); do
147                 [ -e "$object" ] &&
148                         error "'$object' exists." &&
149                         VCSH_CONFLICT=1;
150         done
151         [ "$VCSH_CONFLICT" = '1' ] &&
152                 fatal "will stop after fetching and not try to merge!
153   Once this situation has been resolved, run 'vcsh run $VCSH_REPO_NAME git pull' to finish cloning.\n" 17
154         git merge origin/master
155         hook post-clone
156         retire
157         hook post-clone-retired
158 }
159
160 delete() {
161         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
162         use
163         info "This operation WILL DESTROY DATA!"
164         files=$(git ls-files)
165         echo "These files will be deleted:
166
167 $files
168
169 AGAIN, THIS WILL DELETE YOUR DATA!
170 To continue, type 'Yes, do as I say'"
171         read answer
172         [ "x$answer" = 'xYes, do as I say' ] || exit 16
173         for file in $files; do
174                 rm -f $file || info "could not delete '$file', continuing with deletion"
175         done
176         rm -rf "$GIT_DIR" || error "could not delete '$GIT_DIR'"
177 }
178
179 enter() {
180         hook pre-enter
181         use
182         $SHELL
183         hook post-enter
184 }
185
186 git_dir_exists() {
187         [ -d "$GIT_DIR" ] || fatal "no repository found for '$VCSH_REPO_NAME'" 12
188 }
189
190 hook() {
191         for hook in $VCSH_HOOK_D/$1* $VCSH_HOOK_D/$VCSH_REPO_NAME.$1*; do
192                 [ -x "$hook" ] || continue
193                 verbose "executing '$hook'"
194                 "$hook"
195         done
196 }
197
198 init() {
199         [ ! -e "$GIT_DIR" ] || fatal "'$GIT_DIR' exists" 10
200         export GIT_WORK_TREE="$VCSH_BASE"
201         mkdir -p "$GIT_WORK_TREE" || fatal "could not create '$GIT_WORK_TREE'" 50
202         cd "$GIT_WORK_TREE" || fatal "could not enter '$GIT_WORK_TREE'" 11
203         git init
204         upgrade
205 }
206
207 list() {
208         for repo in "$VCSH_REPO_D"/*.git; do
209                 [ -d "$repo" ] && [ -r "$repo" ] && echo $(basename "$repo" .git)
210         done
211 }
212
213 get_files() {
214         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
215         git ls-files
216 }
217
218 list_tracked() {
219         for VCSH_REPO_NAME in $(list); do
220                 get_files
221         done | sort -u
222 }
223
224 list_tracked_by() {
225         use
226         git ls-files | sort -u
227 }
228
229 pull() {
230         hook pre-pull
231         for VCSH_REPO_NAME in $(list); do
232                 echo -n "$VCSH_REPO_NAME: "
233                 export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
234                 use
235                 git pull
236         done
237         hook post-pull
238 }
239
240 push() {
241         hook pre-push
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 push
247         done
248         hook post-push
249 }
250
251 retire() {
252         unset GIT_WORK_TREE
253         unset VCSH_DIRECTORY
254 }
255
256 rename() {
257         git_dir_exists
258         [ -d "$GIT_DIR_NEW" ] && fatal "'$GIT_DIR_NEW' exists" 54
259         mv -f "$GIT_DIR" "$GIT_DIR_NEW" || fatal "Could not mv '$GIT_DIR' '$GIT_DIR_NEW'" 52
260
261 }
262
263 run() {
264         hook pre-run
265         use
266         "$@"
267         hook post-run
268 }
269
270 upgrade() {
271         hook pre-upgrade
272         use
273         git config core.worktree     "$GIT_WORK_TREE"
274         [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && git config core.excludesfile ".gitignore.d/$VCSH_REPO_NAME"
275         git config vcsh.vcsh         'true'
276         [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ] && git add -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME"
277         hook post-upgrade
278 }
279
280 use() {
281         git_dir_exists
282         export GIT_WORK_TREE="$(git config --get core.worktree)"
283         export VCSH_DIRECTORY="$VCSH_REPO_NAME"
284 }
285
286 which() {
287         for VCSH_REPO_NAME in $(list); do
288                 for VCSH_FILE in $(get_files); do
289                         echo $VCSH_FILE | grep -q "$VCSH_COMMAND_PARAMETER" && echo "$VCSH_REPO_NAME: $VCSH_FILE"
290                 done
291         done | sort -u
292 }
293
294 write_gitignore() {
295         # Don't do anything if the user does not want to write gitignore
296         if [ "x$VCSH_GITIGNORE" = 'xnone' ]; then
297                 info "Not writing gitignore as '\$VCSH_GITIGNORE' is set to 'none'"
298                 exit
299         fi
300
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" = 'xrecursive' ] && [ -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' ] && [ ! "x$VCSH_GITIGNORE" = 'xnone' ]; then
332         fatal "'\$VCSH_GITIGNORE' must equal 'exact', 'recursive', or 'none'" 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" = 'list' ] ||
368      [ "$1" = 'list-tracked' ] ||
369      [ "$1" = 'pull' ] ||
370      [ "$1" = 'push' ]; then
371         export VCSH_COMMAND="$1"
372 elif [ -n "$2" ]; then
373         export VCSH_COMMAND='run'
374         export VCSH_REPO_NAME="$1"
375         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
376         [ -d $GIT_DIR ] || { help; exit 1; }
377         shift 1
378         set -- "git" "$@"
379 elif [ -n "$1" ]; then
380         export VCSH_COMMAND='enter'
381         export VCSH_REPO_NAME="$1"
382         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
383         [ -d $GIT_DIR ] || { help; exit 1; }
384 else
385         # $1 is empty, or 'help'
386         help && exit
387 fi
388
389 # Did we receive a directory instead of a name?
390 # Mangle the input to fit normal operation.
391 if echo $VCSH_REPO_NAME | grep -q '/'; then
392         export GIT_DIR=$VCSH_REPO_NAME
393         export VCSH_REPO_NAME=$(basename "$VCSH_REPO_NAME" .git)
394 fi
395
396 check_dir() {
397         check_directory="$1"
398         if [ ! -d "$check_directory" ]; then
399                 if [ -e "$check_directory" ]; then
400                         fatal "'$check_directory' exists but is not a directory" 13
401                 else
402                         info "attempting to create '$check_directory'"
403                         mkdir -p "$check_directory" || fatal "could not create '$check_directory'" 50
404                 fi
405         fi
406 }
407
408 check_dir "$VCSH_REPO_D"
409 [ ! "x$VCSH_GITIGNORE" = 'xnone' ] && check_dir "$VCSH_BASE/.gitignore.d"
410
411 verbose "$VCSH_COMMAND begin"
412 export VCSH_COMMAND=$(echo $VCSH_COMMAND | sed 's/-/_/g')
413 hook pre-command
414 $VCSH_COMMAND "$@"
415 hook post-command
416 verbose "$VCSH_COMMAND end, exiting"