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

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