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