]> 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 branch 'hook-support'
[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
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                 "$hook"
121         done
122 }
123
124 init() {
125         [ ! -e "$GIT_DIR" ] || fatal "'$GIT_DIR' exists" 10
126         export GIT_WORK_TREE="$VCSH_BASE"
127         mkdir -p "$GIT_WORK_TREE" || fatal "could not create '$GIT_WORK_TREE'" 50
128         cd "$GIT_WORK_TREE" || fatal "could not enter '$GIT_WORK_TREE'" 11
129         git init
130         setup
131 }
132
133 list() {
134         for i in "$VCSH_REPO_D"/*.git; do
135                 echo $(basename "$i" .git)
136         done
137 }
138
139 get_files() {
140         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
141         git ls-files
142 }
143
144 list_tracked() {
145         for VCSH_REPO_NAME in $(list); do
146                 get_files
147         done | sort -u
148 }
149
150 list_tracked_by() {
151         use
152         git ls-files | sort -u
153 }
154
155 rename() {
156         git_dir_exists
157         [ -d "$GIT_DIR_NEW" ] && fatal "'$GIT_DIR_NEW' exists" 54
158         mv -f "$GIT_DIR" "$GIT_DIR_NEW" || fatal "Could not mv '$GIT_DIR' '$GIT_DIR_NEW'" 52
159
160 }
161
162 run() {
163         hook pre-run
164         use
165         $VCSH_EXTERNAL_COMMAND
166         hook post-run
167 }
168
169 setup() {
170         hook pre-setup
171         use
172         git config core.worktree     "$GIT_WORK_TREE"
173         git config core.excludesfile ".gitignore.d/$VCSH_REPO_NAME"
174         git config vcsh.vcsh         'true'
175         [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ] && git add -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME"
176         hook post-setup
177 }
178
179 use() {
180         git_dir_exists
181         export GIT_WORK_TREE="$(git config --get core.worktree)"
182         export VCSH_DIRECTORY="$VCSH_REPO_NAME"
183 }
184
185 write_gitignore() {
186         use
187         cd "$VCSH_BASE" || fatal "could not enter '$VCSH_BASE'" 11
188         gitignores=$(for file in $(git ls-files); do
189                 while true; do
190                         echo $file; new="${file%/*}"
191                         [ "$file" = "$new" ] && break
192                         file="$new"
193                 done;
194         done | sort -u)
195         tempfile=$(mktemp) || fatal "could not create tempfile" 51
196         echo '*' > "$tempfile" || fatal "could not write to '$tempfile'" 57
197         for gitignore in $gitignores; do
198                 echo "$gitignore" | sed 's/^/!/' >> "$tempfile" || fatal "could not write to '$tempfile'" 57
199                 if [ x$VCSH_GITIGNORE = x'recursive' ] && [ -d "$gitignore" ]; then
200                         { echo "$gitignore/*" | sed 's/^/!/' >> "$tempfile" || fatal "could not write to '$tempfile'" 57; }
201                 fi
202         done
203         if diff -N "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" > /dev/null; then
204                 rm -f "$tempfile" || error "could not delete '$tempfile'"
205                 exit
206         fi
207         if [ -e "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ]; then
208                 info "'$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' differs from new data, moving it to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'"
209                 mv -f "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak" ||
210                         fatal "could not move '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME.bak'" 53
211         fi
212         mv -f "$tempfile" "$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME" ||
213                 fatal "could not move '$tempfile' to '$VCSH_BASE/.gitignore.d/$VCSH_REPO_NAME'" 53
214 }
215
216 if [ ! x$VCSH_GITIGNORE = x'exact' ] && [ ! x$VCSH_GITIGNORE = x'recursive' ]; then
217         fatal "\$VCSH_GITIGNORE must be either 'exact' or 'recursive'" 1
218 fi
219
220 if [ "$1" = 'clone' ]; then
221         [ -z $2 ] && fatal "$1: please specify a remote" 1
222         export VCSH_COMMAND="$1"
223         GIT_REMOTE="$2"
224         [ -n "$3" ] && VCSH_REPO_NAME="$3" || VCSH_REPO_NAME=$(basename "$GIT_REMOTE" .git)
225         export VCSH_REPO_NAME
226         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
227 elif [ "$1" = 'delete' ] ||
228      [ "$1" = 'enter' ] ||
229      [ "$1" = 'init' ] ||
230      [ "$1" = 'list-tracked-by' ] ||
231      [ "$1" = 'rename' ] ||
232      [ "$1" = 'run' ] ||
233      [ "$1" = 'setup' ] ||
234      [ "$1" = 'write-gitignore' ]; then
235         [ -z $2 ]                      && fatal "$1: please specify repository to work on" 1
236         [ "$1" = 'rename' -a -z "$3" ] && fatal "$1: please specify a target name" 1
237         [ "$1" = 'run' -a -z "$3" ]    && fatal "$1: please specify a command" 1
238         export VCSH_COMMAND="$1"
239         export VCSH_REPO_NAME="$2"
240         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
241         [ "$VCSH_COMMAND" = 'rename' ]         && export GIT_DIR_NEW="$VCSH_REPO_D/$3.git"
242         [ "$VCSH_COMMAND" = 'run' ] && shift 2 && export VCSH_EXTERNAL_COMMAND="$*"
243         [ "$VCSH_COMMAND" = 'write-gitignore' ]
244 elif [ "$1" = 'list' ] ||
245      [ "$1" = 'list-tracked' ]; then
246         export VCSH_COMMAND="$1"
247 elif [ -n "$2" ]; then
248         export VCSH_COMMAND='run'
249         export VCSH_REPO_NAME="$1"
250         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
251         [ -d $GIT_DIR ] || { help; exit 1; }
252         shift 1
253         export VCSH_EXTERNAL_COMMAND="git $*"
254 elif [ -n "$1" ]; then
255         export VCSH_COMMAND='enter'
256         export VCSH_REPO_NAME="$1"
257         export GIT_DIR="$VCSH_REPO_D/$VCSH_REPO_NAME.git"
258         [ -d $GIT_DIR ] || { help; exit 1; }
259 else
260         # $1 is empty, or 'help'
261         help && exit
262 fi
263
264 # Did we receive a directory instead of a name?
265 # Mangle the input to fit normal operation.
266 if echo $VCSH_REPO_NAME | grep -q '/'; then
267         export GIT_DIR=$VCSH_REPO_NAME
268         export VCSH_REPO_NAME=$(basename $VCSH_REPO_NAME .git)
269 fi
270
271
272 for check_directory in "$VCSH_REPO_D" "$VCSH_BASE/.gitignore.d"
273 do
274         if [ ! -d "$check_directory" ]; then
275                 if [ -e "$check_directory" ]; then
276                         fatal "'$check_directory' exists but is not a directory" 13
277                 else
278                         info "attempting to create '$check_directory'"
279                         mkdir -p "$check_directory" || fatal "could not create '$check_directory'" 50
280                 fi
281         fi
282 done
283
284 verbose "$VCSH_COMMAND begin"
285 export VCSH_COMMAND=$(echo $VCSH_COMMAND | sed 's/-/_/g')
286 $VCSH_COMMAND
287 verbose "$VCSH_COMMAND end, exiting"