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

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