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

README.md: Move Introduction to beginning
[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.3'
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         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         use
263         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
264         gitignores=$(for file in $(git ls-files); do
265                 while true; do
266                         echo $file; new="${file%/*}"
267                         [ "$file" = "$new" ] && break
268                         file="$new"
269                 done;
270         done | sort -u)
271         tempfile=$(mktemp) || fatal "could not create tempfile" 51
272         echo '*' > "$tempfile" || fatal "could not write to '$tempfile'" 57
273         for gitignore in $gitignores; do
274                 echo "$gitignore" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57
275                 if [ x$VCSH_GITIGNORE = x'recursive' ] && [ -d "$gitignore" ]; then
276                         { echo "$gitignore/*" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57; }
277                 fi
278         done
279         if diff -N "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" > /dev/null; then
280                 rm -f "$tempfile" || error "could not delete '$tempfile'"
281                 exit
282         fi
283         if [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ]; then
284                 info "'$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' differs from new data, moving it to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'"
285                 mv -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak" ||
286                         fatal "could not move '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'" 53
287         fi
288         mv -f "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ||
289                 fatal "could not move '$tempfile' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME'" 53
290 }
291
292 if [ ! "x$VCSH_GITIGNORE" = 'xexact' ] && [ ! "x$VCSH_GITIGNORE" = 'xrecursive' ]; then
293         fatal "'\$VCSH_GITIGNORE' must be either 'exact' or 'recursive'" 1
294 fi
295
296 if [ "$1" = 'clone' ]; then
297         [ -z "$2" ] && fatal "$1: please specify a remote" 1
298         export VCSH_COMMAND="$1"
299         GIT_REMOTE="$2"
300         [ -n "$3" ] && VCSH_REPO_NAME="$3" || VCSH_REPO_NAME=$(basename "$GIT_REMOTE" .git)
301         export VCSH_REPO_NAME
302         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
303 elif [ "$1" = 'version' ]; then
304         echo "$SELF $VERSION"
305         exit
306 elif [ "$1" = 'which' ]; then
307         [ -z "$2" ] && fatal "$1: please specify a filename" 1
308         [ -n "$3" ] && fatal "$1: too many parameters" 1
309         export VCSH_COMMAND="$1"
310         export VCSH_COMMAND_PARAMETER="$2"
311 elif [ "$1" = 'delete' ]           ||
312      [ "$1" = 'enter' ]            ||
313      [ "$1" = 'init' ]             ||
314      [ "$1" = 'list-tracked-by' ]  ||
315      [ "$1" = 'rename' ]           ||
316      [ "$1" = 'run' ]              ||
317      [ "$1" = 'upgrade' ]          ||
318      [ "$1" = 'write-gitignore' ]; then
319         [ -z $2 ]                      && fatal "$1: please specify repository to work on" 1
320         [ "$1" = 'rename' -a -z "$3" ] && fatal "$1: please specify a target name" 1
321         [ "$1" = 'run' -a -z "$3" ]    && fatal "$1: please specify a command" 1
322         export VCSH_COMMAND="$1"
323         export VCSH_REPO_NAME="$2"
324         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
325         [ "$VCSH_COMMAND" = 'rename' ]         && export GIT_DIR_NEW="$VCSH_REPO_D/$3.git"
326         [ "$VCSH_COMMAND" = 'run' ] && shift 2
327         [ "$VCSH_COMMAND" = 'write-gitignore' ]
328 elif [ "$1" = 'list' ] ||
329      [ "$1" = 'list-tracked' ]; then
330         export VCSH_COMMAND="$1"
331 elif [ -n "$2" ]; then
332         export VCSH_COMMAND='run'
333         export VCSH_REPO_NAME="$1"
334         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
335         [ -d $GIT_DIR ] || { help; exit 1; }
336         shift 1
337         set -- "git" "$@"
338 elif [ -n "$1" ]; then
339         export VCSH_COMMAND='enter'
340         export VCSH_REPO_NAME="$1"
341         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
342         [ -d $GIT_DIR ] || { help; exit 1; }
343 else
344         # $1 is empty, or 'help'
345         help && exit
346 fi
347
348 # Did we receive a directory instead of a name?
349 # Mangle the input to fit normal operation.
350 if echo $VCSH_REPO_NAME | grep -q '/'; then
351         export GIT_DIR=$VCSH_REPO_NAME
352         export VCSH_REPO_NAME=$(basename "$VCSH_REPO_NAME" .git)
353 fi
354
355
356 for check_directory in "$VCSH_REPO_D" "$VCSH_BASE/.gitignore.d"
357 do
358         if [ ! -d "$check_directory" ]; then
359                 if [ -e "$check_directory" ]; then
360                         fatal "'$check_directory' exists but is not a directory" 13
361                 else
362                         info "attempting to create '$check_directory'"
363                         mkdir -p "$check_directory" || fatal "could not create '$check_directory'" 50
364                 fi
365         fi
366 done
367
368 verbose "$VCSH_COMMAND begin"
369 export VCSH_COMMAND=$(echo $VCSH_COMMAND | sed 's/-/_/g')
370 hook pre-command
371 $VCSH_COMMAND "$@"
372 hook post-command
373 verbose "$VCSH_COMMAND end, exiting"