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

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