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

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