1 #!/bin/bash
2 #
3 # git-subtree.sh: split/join git repositories in subdirectories of this one
4 #
5 # Copyright (C) 2009 Avery Pennarun <apenwarr@gmail.com>
6 #
7 if [ $# -eq 0 ]; then
8 set -- -h
9 fi
10 OPTS_SPEC="\
11 git subtree add --prefix=<prefix> <commit>
12 git subtree split [options...] --prefix=<prefix> <commit...>
13 git subtree merge --prefix=<prefix> <commit>
14 git subtree pull --prefix=<prefix> <repository> <refspec...>
15 --
16 h,help show the help
17 q quiet
18 prefix= the name of the subdir to split out
19 options for 'split'
20 onto= try connecting new tree to an existing one
21 rejoin merge the new branch back into HEAD
22 ignore-joins ignore prior --rejoin commits
23 "
24 eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)
25 . git-sh-setup
26 require_work_tree
28 quiet=
29 command=
30 onto=
31 rejoin=
32 ignore_joins=
34 debug()
35 {
36 if [ -z "$quiet" ]; then
37 echo "$@" >&2
38 fi
39 }
41 assert()
42 {
43 if "$@"; then
44 :
45 else
46 die "assertion failed: " "$@"
47 fi
48 }
51 #echo "Options: $*"
53 while [ $# -gt 0 ]; do
54 opt="$1"
55 shift
56 case "$opt" in
57 -q) quiet=1 ;;
58 --prefix) prefix="$1"; shift ;;
59 --no-prefix) prefix= ;;
60 --onto) onto="$1"; shift ;;
61 --no-onto) onto= ;;
62 --rejoin) rejoin=1 ;;
63 --no-rejoin) rejoin= ;;
64 --ignore-joins) ignore_joins=1 ;;
65 --no-ignore-joins) ignore_joins= ;;
66 --) break ;;
67 esac
68 done
70 command="$1"
71 shift
72 case "$command" in
73 add|merge|pull) default= ;;
74 split) default="--default HEAD" ;;
75 *) die "Unknown command '$command'" ;;
76 esac
78 if [ -z "$prefix" ]; then
79 die "You must provide the --prefix option."
80 fi
81 dir="$prefix"
83 if [ "$command" != "pull" ]; then
84 revs=$(git rev-parse $default --revs-only "$@") || exit $?
85 dirs="$(git rev-parse --no-revs --no-flags "$@")" || exit $?
86 if [ -n "$dirs" ]; then
87 die "Error: Use --prefix instead of bare filenames."
88 fi
89 fi
91 debug "command: {$command}"
92 debug "quiet: {$quiet}"
93 debug "revs: {$revs}"
94 debug "dir: {$dir}"
95 debug "opts: {$*}"
96 debug
98 cache_setup()
99 {
100 cachedir="$GIT_DIR/subtree-cache/$$"
101 rm -rf "$cachedir" || die "Can't delete old cachedir: $cachedir"
102 mkdir -p "$cachedir" || die "Can't create new cachedir: $cachedir"
103 debug "Using cachedir: $cachedir" >&2
104 }
106 cache_get()
107 {
108 for oldrev in $*; do
109 if [ -r "$cachedir/$oldrev" ]; then
110 read newrev <"$cachedir/$oldrev"
111 echo $newrev
112 fi
113 done
114 }
116 cache_set()
117 {
118 oldrev="$1"
119 newrev="$2"
120 if [ "$oldrev" != "latest_old" \
121 -a "$oldrev" != "latest_new" \
122 -a -e "$cachedir/$oldrev" ]; then
123 die "cache for $oldrev already exists!"
124 fi
125 echo "$newrev" >"$cachedir/$oldrev"
126 }
128 # if a commit doesn't have a parent, this might not work. But we only want
129 # to remove the parent from the rev-list, and since it doesn't exist, it won't
130 # be there anyway, so do nothing in that case.
131 try_remove_previous()
132 {
133 if git rev-parse "$1^" >/dev/null 2>&1; then
134 echo "^$1^"
135 fi
136 }
138 find_existing_splits()
139 {
140 debug "Looking for prior splits..."
141 dir="$1"
142 revs="$2"
143 git log --grep="^git-subtree-dir: $dir\$" \
144 --pretty=format:'%s%n%n%b%nEND' "$revs" |
145 while read a b junk; do
146 case "$a" in
147 git-subtree-mainline:) main="$b" ;;
148 git-subtree-split:) sub="$b" ;;
149 *)
150 if [ -n "$main" -a -n "$sub" ]; then
151 debug " Prior: $main -> $sub"
152 cache_set $main $sub
153 try_remove_previous "$main"
154 try_remove_previous "$sub"
155 main=
156 sub=
157 fi
158 ;;
159 esac
160 done
161 }
163 copy_commit()
164 {
165 # We're doing to set some environment vars here, so
166 # do it in a subshell to get rid of them safely later
167 git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%n%b' "$1" |
168 (
169 read GIT_AUTHOR_NAME
170 read GIT_AUTHOR_EMAIL
171 read GIT_AUTHOR_DATE
172 read GIT_COMMITTER_NAME
173 read GIT_COMMITTER_EMAIL
174 read GIT_COMMITTER_DATE
175 export GIT_AUTHOR_NAME \
176 GIT_AUTHOR_EMAIL \
177 GIT_AUTHOR_DATE \
178 GIT_COMMITTER_NAME \
179 GIT_COMMITTER_EMAIL \
180 GIT_COMMITTER_DATE
181 (echo -n '*'; cat ) | # FIXME
182 git commit-tree "$2" $3 # reads the rest of stdin
183 ) || die "Can't copy commit $1"
184 }
186 add_msg()
187 {
188 dir="$1"
189 latest_old="$2"
190 latest_new="$3"
191 cat <<-EOF
192 Add '$dir/' from commit '$latest_new'
194 git-subtree-dir: $dir
195 git-subtree-mainline: $latest_old
196 git-subtree-split: $latest_new
197 EOF
198 }
200 merge_msg()
201 {
202 dir="$1"
203 latest_old="$2"
204 latest_new="$3"
205 cat <<-EOF
206 Split '$dir/' into commit '$latest_new'
208 git-subtree-dir: $dir
209 git-subtree-mainline: $latest_old
210 git-subtree-split: $latest_new
211 EOF
212 }
214 toptree_for_commit()
215 {
216 commit="$1"
217 git log -1 --pretty=format:'%T' "$commit" -- || exit $?
218 }
220 subtree_for_commit()
221 {
222 commit="$1"
223 dir="$2"
224 git ls-tree "$commit" -- "$dir" |
225 while read mode type tree name; do
226 assert [ "$name" = "$dir" ]
227 echo $tree
228 break
229 done
230 }
232 tree_changed()
233 {
234 tree=$1
235 shift
236 if [ $# -ne 1 ]; then
237 return 0 # weird parents, consider it changed
238 else
239 ptree=$(toptree_for_commit $1)
240 if [ "$ptree" != "$tree" ]; then
241 return 0 # changed
242 else
243 return 1 # not changed
244 fi
245 fi
246 }
248 copy_or_skip()
249 {
250 rev="$1"
251 tree="$2"
252 newparents="$3"
253 assert [ -n "$tree" ]
255 identical=
256 p=
257 for parent in $newparents; do
258 ptree=$(toptree_for_commit $parent) || exit $?
259 if [ "$ptree" = "$tree" ]; then
260 # an identical parent could be used in place of this rev.
261 identical="$parent"
262 fi
263 if [ -n "$ptree" ]; then
264 parentmatch="$parentmatch$parent"
265 p="$p -p $parent"
266 fi
267 done
269 if [ -n "$identical" -a "$parentmatch" = "$identical" ]; then
270 echo $identical
271 else
272 copy_commit $rev $tree "$p" || exit $?
273 fi
274 }
276 ensure_clean()
277 {
278 if ! git diff-index HEAD --exit-code --quiet; then
279 die "Working tree has modifications. Cannot add."
280 fi
281 if ! git diff-index --cached HEAD --exit-code --quiet; then
282 die "Index has modifications. Cannot add."
283 fi
284 }
286 cmd_add()
287 {
288 if [ -e "$dir" ]; then
289 die "'$dir' already exists. Cannot add."
290 fi
291 ensure_clean
293 set -- $revs
294 if [ $# -ne 1 ]; then
295 die "You must provide exactly one revision. Got: '$revs'"
296 fi
297 rev="$1"
299 debug "Adding $dir as '$rev'..."
300 git read-tree --prefix="$dir" $rev || exit $?
301 git checkout "$dir" || exit $?
302 tree=$(git write-tree) || exit $?
304 headrev=$(git rev-parse HEAD) || exit $?
305 if [ -n "$headrev" -a "$headrev" != "$rev" ]; then
306 headp="-p $headrev"
307 else
308 headp=
309 fi
310 commit=$(add_msg "$dir" "$headrev" "$rev" |
311 git commit-tree $tree $headp -p "$rev") || exit $?
312 git reset "$commit" || exit $?
313 }
315 cmd_split()
316 {
317 debug "Splitting $dir..."
318 cache_setup || exit $?
320 if [ -n "$onto" ]; then
321 debug "Reading history for --onto=$onto..."
322 git rev-list $onto |
323 while read rev; do
324 # the 'onto' history is already just the subdir, so
325 # any parent we find there can be used verbatim
326 debug " cache: $rev"
327 cache_set $rev $rev
328 done
329 fi
331 if [ -n "$ignore_joins" ]; then
332 unrevs=
333 else
334 unrevs="$(find_existing_splits "$dir" "$revs")"
335 fi
337 # We can't restrict rev-list to only "$dir" here, because that leaves out
338 # critical information about commit parents.
339 debug "git rev-list --reverse --parents $revs $unrevs"
340 git rev-list --reverse --parents $revs $unrevs |
341 while read rev parents; do
342 debug
343 debug "Processing commit: $rev"
344 exists=$(cache_get $rev)
345 if [ -n "$exists" ]; then
346 debug " prior: $exists"
347 continue
348 fi
349 debug " parents: $parents"
350 newparents=$(cache_get $parents)
351 debug " newparents: $newparents"
353 tree=$(subtree_for_commit $rev "$dir")
354 debug " tree is: $tree"
355 [ -z $tree ] && continue
357 newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $?
358 debug " newrev is: $newrev"
359 cache_set $rev $newrev
360 cache_set latest_new $newrev
361 cache_set latest_old $rev
362 done || exit $?
363 latest_new=$(cache_get latest_new)
364 if [ -z "$latest_new" ]; then
365 die "No new revisions were found"
366 fi
368 if [ -n "$rejoin" ]; then
369 debug "Merging split branch into HEAD..."
370 latest_old=$(cache_get latest_old)
371 git merge -s ours \
372 -m "$(merge_msg $dir $latest_old $latest_new)" \
373 $latest_new >&2
374 fi
375 echo $latest_new
376 exit 0
377 }
379 cmd_merge()
380 {
381 ensure_clean
383 set -- $revs
384 if [ $# -ne 1 ]; then
385 die "You must provide exactly one revision. Got: '$revs'"
386 fi
387 rev="$1"
389 git merge -s subtree $rev
390 }
392 cmd_pull()
393 {
394 ensure_clean
395 set -x
396 git pull -s subtree "$@"
397 }
399 "cmd_$command" "$@"