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

Tell people when hooks are executed
[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 write_gitignore() {
187         use
188         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
189         gitignores=$(for file in $(git ls-files); do
190                 while true; do
191                         echo $file; new="${file%/*}"
192                         [ "$file" = "$new" ] && break
193                         file="$new"
194                 done;
195         done | sort -u)
196         tempfile=$(mktemp) || fatal "could not create tempfile" 51
197         echo '*' > "$tempfile" || fatal "could not write to '$tempfile'" 57
198         for gitignore in $gitignores; do
199                 echo "$gitignore" | sed 's/^/!/' >> "$tempfile" || fatal "could not write to '$tempfile'" 57
200                 if [ x$VCSH_GITIGNORE = x'recursive' ] && [ -d "$gitignore" ]; then
201                         { echo "$gitignore/*" | sed 's/^/!/' >> "$tempfile" || fatal "could not write to '$tempfile'" 57; }
202                 fi
203         done
204         if diff -N "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" > /dev/null; then
205                 rm -f "$tempfile" || error "could not delete '$tempfile'"
206                 exit
207         fi
208         if [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ]; then
209                 info "'$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' differs from new data, moving it to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'"
210                 mv -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak" ||
211                         fatal "could not move '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'" 53
212         fi
213         mv -f "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ||
214                 fatal "could not move '$tempfile' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME'" 53
215 }
216
217 if [ ! "x$VCSH_GITIGNORE" = 'xexact' ] && [ ! "x$VCSH_GITIGNORE" = 'xrecursive' ]; then
218         fatal "'\$VCSH_GITIGNORE' must be either 'exact' or 'recursive'" 1
219 fi
220
221 if [ "$1" = 'clone' ]; then
222         [ -z $2 ] && fatal "$1: please specify a remote" 1
223         export VCSH_COMMAND="$1"
224         GIT_REMOTE="$2"
225         [ -n "$3" ] && VCSH_REPO_NAME="$3" || VCSH_REPO_NAME=$(basename "$GIT_REMOTE" .git)
226         export VCSH_REPO_NAME
227         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
228 elif [ "$1" = 'delete' ]          ||
229      [ "$1" = 'enter' ]           ||
230      [ "$1" = 'init' ]            ||
231      [ "$1" = 'list-tracked-by' ] ||
232      [ "$1" = 'rename' ]          ||
233      [ "$1" = 'run' ]             ||
234      [ "$1" = 'setup' ]           ||
235      [ "$1" = 'write-gitignore' ]; then
236         [ -z $2 ]                      && fatal "$1: please specify repository to work on" 1
237         [ "$1" = 'rename' -a -z "$3" ] && fatal "$1: please specify a target name" 1
238         [ "$1" = 'run' -a -z "$3" ]    && fatal "$1: please specify a command" 1
239         export VCSH_COMMAND="$1"
240         export VCSH_REPO_NAME="$2"
241         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
242         [ "$VCSH_COMMAND" = 'rename' ]         && export GIT_DIR_NEW="$VCSH_REPO_D/$3.git"
243         [ "$VCSH_COMMAND" = 'run' ] && shift 2 && export VCSH_EXTERNAL_COMMAND="$*"
244         [ "$VCSH_COMMAND" = 'write-gitignore' ]
245 elif [ "$1" = 'list' ] ||
246      [ "$1" = 'list-tracked' ]; then
247         export VCSH_COMMAND="$1"
248 elif [ -n "$2" ]; then
249         export VCSH_COMMAND='run'
250         export VCSH_REPO_NAME="$1"
251         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
252         [ -d $GIT_DIR ] || { help; exit 1; }
253         shift 1
254         export VCSH_EXTERNAL_COMMAND="git $*"
255 elif [ -n "$1" ]; then
256         export VCSH_COMMAND='enter'
257         export VCSH_REPO_NAME="$1"
258         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
259         [ -d $GIT_DIR ] || { help; exit 1; }
260 else
261         # $1 is empty, or 'help'
262         help && exit
263 fi
264
265 # Did we receive a directory instead of a name?
266 # Mangle the input to fit normal operation.
267 if echo $VCSH_REPO_NAME | grep -q '/'; then
268         export GIT_DIR=$VCSH_REPO_NAME
269         export VCSH_REPO_NAME=$(basename "$VCSH_REPO_NAME" .git)
270 fi
271
272
273 for check_directory in "$VCSH_REPO_D" "$VCSH_BASE/.gitignore.d"
274 do
275         if [ ! -d "$check_directory" ]; then
276                 if [ -e "$check_directory" ]; then
277                         fatal "'$check_directory' exists but is not a directory" 13
278                 else
279                         info "attempting to create '$check_directory'"
280                         mkdir -p "$check_directory" || fatal "could not create '$check_directory'" 50
281                 fi
282         fi
283 done
284
285 verbose "$VCSH_COMMAND begin"
286 export VCSH_COMMAND=$(echo $VCSH_COMMAND | sed 's/-/_/g')
287 $VCSH_COMMAND
288 verbose "$VCSH_COMMAND end, exiting"