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

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