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

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