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

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