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

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