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

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