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

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