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

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