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

Merge pull request #43 from madduck/anchor-ignores
[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 compability 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 SELF=$(basename $0)
16 VERSION='1.2'
17
18 fatal() {
19         echo "$SELF: fatal: $1" >&2
20         exit $2
21 }
22
23 # We need to run getops as soon as possible so we catch -d and other
24 # options that will modify our behaviour.
25 # Commands are handled at the end of this script.
26 while getopts  "c:dv" flag; do
27         if [ "$1" = '-d' ] || [ "$1" = '--debug' ]; then
28                 set -vx
29                 VCSH_DEBUG=1
30         elif [ "$1" = '-v' ];then
31                 VCSH_VERBOSE=1
32         elif [ "$1" = '-c' ];then
33                 VCSH_OPTION_CONFIG=$OPTARG
34         fi
35         shift 1
36 done
37
38 source_all() {
39         # Source file even if it's in $PWD and does not have any slashes in it
40         case "$1" in
41                 */*) . "$1";;
42                 *)   . "$PWD/$1";;
43         esac;
44 }
45
46
47 # Read configuration and set defaults if anything's not set
48 [ -n "$VCSH_DEBUG" ]                  && set -vx
49 [ -z "$XDG_CONFIG_HOME" ]             && XDG_CONFIG_HOME="$HOME/.config"
50
51 # Read configuration files if there are any
52 [ -r "/etc/vcsh/config" ]             && . "/etc/vcsh/config"
53 [ -r "$XDG_CONFIG_HOME/vcsh/config" ] && . "$XDG_CONFIG_HOME/vcsh/config"
54 if [ -n "$VCSH_OPTION_CONFIG" ]; then
55         # Source $VCSH_OPTION_CONFIG if it can be read and is in $PWD of $PATH
56         if [ -r "$VCSH_OPTION_CONFIG" ]; then
57                 source_all "$VCSH_OPTION_CONFIG"
58         else
59                 fatal "Can not read configuration file '$VCSH_OPTION_CONFIG'" 1
60         fi
61 fi
62 [ -n "$VCSH_DEBUG" ]                  && set -vx
63
64 # Read defaults
65 [ -z "$VCSH_REPO_D" ]                 && VCSH_REPO_D="$XDG_CONFIG_HOME/vcsh/repo.d"
66 [ -z "$VCSH_HOOK_D" ]                 && VCSH_HOOK_D="$XDG_CONFIG_HOME/vcsh/hooks-enabled"
67 [ -z "$VCSH_BASE" ]                   && VCSH_BASE="$HOME"
68 [ -z "$VCSH_GITIGNORE" ]              && VCSH_GITIGNORE='exact'
69
70
71 help() {
72         echo "usage: $SELF <options> <command>
73
74    options:
75    -c <file>            Source file
76    -d                   Enable debug mode
77    -v                   Enable verbose mode
78
79    commands:
80    clone <remote> \\
81          [<repo>]       Clone from an existing repository
82    delete <repo>        Delete an existing repository
83    enter <repo>         Enter repository; spawn new instance of \$SHELL
84    help                 Display this help text
85    init <repo>          Initialize a new repository
86    list                 List all repositories
87    list-tracked         List all files tracked by vcsh
88    list-tracked-by \\
89         <repo>          List files tracked by a repository
90    rename <repo> \\
91           <newname>     Rename repository
92    run <repo> \\
93        <command>        Use this repository
94    setup <repo>         Set up repository with recommended settings
95    version              Print version information
96    which <substring>    Find substring in name of any tracked file
97    write-gitignore \\
98    <repo>               Write .gitignore.d/<repo> via git ls-files
99
100    <repo> <git command> Shortcut to run git commands directly
101    <repo>               Shortcut to enter repository" >&2
102 }
103
104 debug() {
105         [ -n "$VCSH_DEBUG" ] && echo "$SELF: debug: $@"
106 }
107
108 verbose() {
109         if [ -n "$VCSH_DEBUG" ] || [ -n "$VCSH_VERBOSE" ]; then echo "$SELF: verbose: $@"; fi
110 }
111
112 error() {
113         echo "$SELF: error: $1" >&2
114 }
115
116 info() {
117         echo "$SELF: info: $1"
118 }
119
120 clone() {
121         init
122         git remote add origin "$GIT_REMOTE"
123         git config branch.master.remote origin
124         git config branch.master.merge  refs/heads/master
125         if [ $(git ls-remote origin master 2> /dev/null | wc -l ) -lt 1 ]; then
126                 info "remote is empty, not merging anything"
127                 exit
128         fi
129         git fetch
130         for object in $(git ls-tree -r origin/master | awk '{print $4}'); do
131                 [ -e "$object" ] &&
132                         error "'$object' exists." &&
133                         VCSH_CONFLICT=1;
134         done
135         [ "$VCSH_CONFLICT" = '1' ] &&
136                 fatal "will stop after fetching and not try to merge!
137   Once this situation has been resolved, run 'vcsh run $VCSH_REPO_NAME git pull' to finish cloning.\n" 17
138         git merge origin/master
139 }
140
141 delete() {
142         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
143         use
144         info "This operation WILL DESTROY DATA!"
145         files=$(git ls-files)
146         echo "These files will be deleted:
147
148 $files
149
150 AGAIN, THIS WILL DELETE YOUR DATA!
151 To continue, type 'Yes, do as I say'"
152         read answer
153         [ "x$answer" = 'xYes, do as I say' ] || exit 16
154         for file in $files; do
155                 rm -f $file || info "could not delete '$file', continuing with deletion"
156         done
157         rm -rf "$GIT_DIR" || error "could not delete '$GIT_DIR'"
158 }
159
160 enter() {
161         hook pre-enter
162         use
163         $SHELL
164         hook post-enter
165 }
166
167 git_dir_exists() {
168         [ -d "$GIT_DIR" ] || fatal "no repository found for '$VCSH_REPO_NAME'" 12
169 }
170
171 hook() {
172         for hook in $VCSH_HOOK_D/$1* $VCSH_HOOK_D/$VCSH_REPO_NAME.$1*; do
173                 [ -x "$hook" ] || continue
174                 verbose "executing '$hook'"
175                 "$hook"
176         done
177 }
178
179 init() {
180         [ ! -e "$GIT_DIR" ] || fatal "'$GIT_DIR' exists" 10
181         export GIT_WORK_TREE="$VCSH_BASE"
182         mkdir -p "$GIT_WORK_TREE" || fatal "could not create '$GIT_WORK_TREE'" 50
183         cd "$GIT_WORK_TREE" || fatal "could not enter '$GIT_WORK_TREE'" 11
184         git init
185         setup
186 }
187
188 list() {
189         for repo in "$VCSH_REPO_D"/*.git; do
190                 [ -d "$repo" ] && [ -r "$repo" ] && echo $(basename "$repo" .git)
191         done
192 }
193
194 get_files() {
195         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
196         git ls-files
197 }
198
199 list_tracked() {
200         for VCSH_REPO_NAME in $(list); do
201                 get_files
202         done | sort -u
203 }
204
205 list_tracked_by() {
206         use
207         git ls-files | sort -u
208 }
209
210 rename() {
211         git_dir_exists
212         [ -d "$GIT_DIR_NEW" ] && fatal "'$GIT_DIR_NEW' exists" 54
213         mv -f "$GIT_DIR" "$GIT_DIR_NEW" || fatal "Could not mv '$GIT_DIR' '$GIT_DIR_NEW'" 52
214
215 }
216
217 run() {
218         hook pre-run
219         use
220         "$@"
221         hook post-run
222 }
223
224 setup() {
225         hook pre-setup
226         use
227         git config core.worktree     "$GIT_WORK_TREE"
228         git config core.excludesfile ".gitignore.d/$VCSH_REPO_NAME"
229         git config vcsh.vcsh         'true'
230         [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ] && git add -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME"
231         hook post-setup
232 }
233
234 use() {
235         git_dir_exists
236         export GIT_WORK_TREE="$(git config --get core.worktree)"
237         export VCSH_DIRECTORY="$VCSH_REPO_NAME"
238 }
239
240 which() {
241         for VCSH_REPO_NAME in $(list); do
242                 for VCSH_FILE in $(get_files); do
243                         echo $VCSH_FILE | grep -q "$VCSH_COMMAND_PARAMETER" && echo "$VCSH_REPO_NAME: $VCSH_FILE"
244                 done
245         done | sort -u
246 }
247
248 write_gitignore() {
249         use
250         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
251         gitignores=$(for file in $(git ls-files); do
252                 while true; do
253                         echo $file; new="${file%/*}"
254                         [ "$file" = "$new" ] && break
255                         file="$new"
256                 done;
257         done | sort -u)
258         tempfile=$(mktemp) || fatal "could not create tempfile" 51
259         echo '*' > "$tempfile" || fatal "could not write to '$tempfile'" 57
260         for gitignore in $gitignores; do
261                 echo "$gitignore" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57
262                 if [ x$VCSH_GITIGNORE = x'recursive' ] && [ -d "$gitignore" ]; then
263                         { echo "$gitignore/*" | sed 's@^@!/@' >> "$tempfile" || fatal "could not write to '$tempfile'" 57; }
264                 fi
265         done
266         if diff -N "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" > /dev/null; then
267                 rm -f "$tempfile" || error "could not delete '$tempfile'"
268                 exit
269         fi
270         if [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ]; then
271                 info "'$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' differs from new data, moving it to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'"
272                 mv -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak" ||
273                         fatal "could not move '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'" 53
274         fi
275         mv -f "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ||
276                 fatal "could not move '$tempfile' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME'" 53
277 }
278
279 if [ ! "x$VCSH_GITIGNORE" = 'xexact' ] && [ ! "x$VCSH_GITIGNORE" = 'xrecursive' ]; then
280         fatal "'\$VCSH_GITIGNORE' must be either 'exact' or 'recursive'" 1
281 fi
282
283 if [ "$1" = 'clone' ]; then
284         [ -z "$2" ] && fatal "$1: please specify a remote" 1
285         export VCSH_COMMAND="$1"
286         GIT_REMOTE="$2"
287         [ -n "$3" ] && VCSH_REPO_NAME="$3" || VCSH_REPO_NAME=$(basename "$GIT_REMOTE" .git)
288         export VCSH_REPO_NAME
289         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
290 elif [ "$1" = 'version' ]; then
291         echo "$SELF $VERSION"
292         exit
293 elif [ "$1" = 'which' ]; then
294         [ -z "$2" ] && fatal "$1: please specify a filename" 1
295         [ -n "$3" ] && fatal "$1: too many parameters" 1
296         export VCSH_COMMAND="$1"
297         export VCSH_COMMAND_PARAMETER="$2"
298 elif [ "$1" = 'delete' ]           ||
299      [ "$1" = 'enter' ]            ||
300      [ "$1" = 'init' ]             ||
301      [ "$1" = 'list-tracked-by' ]  ||
302      [ "$1" = 'rename' ]           ||
303      [ "$1" = 'run' ]              ||
304      [ "$1" = 'setup' ]            ||
305      [ "$1" = 'write-gitignore' ]; then
306         [ -z $2 ]                      && fatal "$1: please specify repository to work on" 1
307         [ "$1" = 'rename' -a -z "$3" ] && fatal "$1: please specify a target name" 1
308         [ "$1" = 'run' -a -z "$3" ]    && fatal "$1: please specify a command" 1
309         export VCSH_COMMAND="$1"
310         export VCSH_REPO_NAME="$2"
311         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
312         [ "$VCSH_COMMAND" = 'rename' ]         && export GIT_DIR_NEW="$VCSH_REPO_D/$3.git"
313         [ "$VCSH_COMMAND" = 'run' ] && shift 2
314         [ "$VCSH_COMMAND" = 'write-gitignore' ]
315 elif [ "$1" = 'list' ] ||
316      [ "$1" = 'list-tracked' ]; then
317         export VCSH_COMMAND="$1"
318 elif [ -n "$2" ]; then
319         export VCSH_COMMAND='run'
320         export VCSH_REPO_NAME="$1"
321         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
322         [ -d $GIT_DIR ] || { help; exit 1; }
323         shift 1
324         set -- "git" "$@"
325 elif [ -n "$1" ]; then
326         export VCSH_COMMAND='enter'
327         export VCSH_REPO_NAME="$1"
328         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
329         [ -d $GIT_DIR ] || { help; exit 1; }
330 else
331         # $1 is empty, or 'help'
332         help && exit
333 fi
334
335 # Did we receive a directory instead of a name?
336 # Mangle the input to fit normal operation.
337 if echo $VCSH_REPO_NAME | grep -q '/'; then
338         export GIT_DIR=$VCSH_REPO_NAME
339         export VCSH_REPO_NAME=$(basename "$VCSH_REPO_NAME" .git)
340 fi
341
342
343 for check_directory in "$VCSH_REPO_D" "$VCSH_BASE/.gitignore.d"
344 do
345         if [ ! -d "$check_directory" ]; then
346                 if [ -e "$check_directory" ]; then
347                         fatal "'$check_directory' exists but is not a directory" 13
348                 else
349                         info "attempting to create '$check_directory'"
350                         mkdir -p "$check_directory" || fatal "could not create '$check_directory'" 50
351                 fi
352         fi
353 done
354
355 verbose "$VCSH_COMMAND begin"
356 export VCSH_COMMAND=$(echo $VCSH_COMMAND | sed 's/-/_/g')
357 hook pre-command
358 $VCSH_COMMAND "$@"
359 hook post-command
360 verbose "$VCSH_COMMAND end, exiting"