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

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