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

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