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