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#
7if [ $# -eq 0 ]; then
8 set -- -h
9fi
10OPTS_SPEC="\
11git subtree add --prefix=<prefix> <commit>
12git subtree merge --prefix=<prefix> <commit>
13git subtree pull --prefix=<prefix> <repository> <refspec...>
14git subtree push --prefix=<prefix> <repository> <refspec...>
15git subtree split --prefix=<prefix> <commit...>
16--
17h,help show the help
18q quiet
19d show debug messages
20P,prefix= the name of the subdir to split out
21m,message= use the given message as the commit message for the merge commit
22 options for 'split'
23annotate= add a prefix to commit message of new commits
24b,branch= create a new branch from the split subtree
25ignore-joins ignore prior --rejoin commits
26onto= try connecting new tree to an existing one
27rejoin merge the new branch back into HEAD
28 options for 'add', 'merge', 'pull' and 'push'
29squash merge subtree changes as a single commit
30"
31eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)
32PATH=$(git --exec-path):$PATH
33. git-sh-setup
34require_work_tree
35
36quiet=
37branch=
38debug=
39command=
40onto=
41rejoin=
42ignore_joins=
43annotate=
44squash=
45message=
46
47debug()
48{
49 if [ -n "$debug" ]; then
50 echo "$@" >&2
51 fi
52}
53
54say()
55{
56 if [ -z "$quiet" ]; then
57 echo "$@" >&2
58 fi
59}
60
61assert()
62{
63 if "$@"; then
64 :
65 else
66 die "assertion failed: " "$@"
67 fi
68}
69
70
71#echo "Options: $*"
72
73while [ $# -gt 0 ]; do
74 opt="$1"
75 shift
76 case "$opt" in
77 -q) quiet=1 ;;
78 -d) debug=1 ;;
79 --annotate) annotate="$1"; shift ;;
80 --no-annotate) annotate= ;;
81 -b) branch="$1"; shift ;;
82 -P) prefix="$1"; shift ;;
83 -m) message="$1"; shift ;;
84 --no-prefix) prefix= ;;
85 --onto) onto="$1"; shift ;;
86 --no-onto) onto= ;;
87 --rejoin) rejoin=1 ;;
88 --no-rejoin) rejoin= ;;
89 --ignore-joins) ignore_joins=1 ;;
90 --no-ignore-joins) ignore_joins= ;;
91 --squash) squash=1 ;;
92 --no-squash) squash= ;;
93 --) break ;;
94 *) die "Unexpected option: $opt" ;;
95 esac
96done
97
98command="$1"
99shift
100case "$command" in
101 add|merge|pull) default= ;;
102 split|push) default="--default HEAD" ;;
103 *) die "Unknown command '$command'" ;;
104esac
105
106if [ -z "$prefix" ]; then
107 die "You must provide the --prefix option."
108fi
109
110case "$command" in
111 add) [ -e "$prefix" ] &&
112 die "prefix '$prefix' already exists." ;;
113 *) [ -e "$prefix" ] ||
114 die "'$prefix' does not exist; use 'git subtree add'" ;;
115esac
116
117dir="$(dirname "$prefix/.")"
118
119if [ "$command" != "pull" -a "$command" != "add" -a "$command" != "push" ]; then
120 revs=$(git rev-parse $default --revs-only "$@") || exit $?
121 dirs="$(git rev-parse --no-revs --no-flags "$@")" || exit $?
122 if [ -n "$dirs" ]; then
123 die "Error: Use --prefix instead of bare filenames."
124 fi
125fi
126
127debug "command: {$command}"
128debug "quiet: {$quiet}"
129debug "revs: {$revs}"
130debug "dir: {$dir}"
131debug "opts: {$*}"
132debug
133
134cache_setup()
135{
136 cachedir="$GIT_DIR/subtree-cache/$$"
137 rm -rf "$cachedir" || die "Can't delete old cachedir: $cachedir"
138 mkdir -p "$cachedir" || die "Can't create new cachedir: $cachedir"
139 debug "Using cachedir: $cachedir" >&2
140}
141
142cache_get()
143{
144 for oldrev in $*; do
145 if [ -r "$cachedir/$oldrev" ]; then
146 read newrev <"$cachedir/$oldrev"
147 echo $newrev
148 fi
149 done
150}
151
152cache_set()
153{
154 oldrev="$1"
155 newrev="$2"
156 if [ "$oldrev" != "latest_old" \
157 -a "$oldrev" != "latest_new" \
158 -a -e "$cachedir/$oldrev" ]; then
159 die "cache for $oldrev already exists!"
160 fi
161 echo "$newrev" >"$cachedir/$oldrev"
162}
163
164rev_exists()
165{
166 if git rev-parse "$1" >/dev/null 2>&1; then
167 return 0
168 else
169 return 1
170 fi
171}
172
173rev_is_descendant_of_branch()
174{
175 newrev="$1"
176 branch="$2"
177 branch_hash=$(git rev-parse $branch)
178 match=$(git rev-list -1 $branch_hash ^$newrev)
179
180 if [ -z "$match" ]; then
181 return 0
182 else
183 return 1
184 fi
185}
186
187# if a commit doesn't have a parent, this might not work. But we only want
188# to remove the parent from the rev-list, and since it doesn't exist, it won't
189# be there anyway, so do nothing in that case.
190try_remove_previous()
191{
192 if rev_exists "$1^"; then
193 echo "^$1^"
194 fi
195}
196
197find_latest_squash()
198{
199 debug "Looking for latest squash ($dir)..."
200 dir="$1"
201 sq=
202 main=
203 sub=
204 git log --grep="^git-subtree-dir: $dir/*\$" \
205 --pretty=format:'START %H%n%s%n%n%b%nEND%n' HEAD |
206 while read a b junk; do
207 debug "$a $b $junk"
208 debug "{{$sq/$main/$sub}}"
209 case "$a" in
210 START) sq="$b" ;;
211 git-subtree-mainline:) main="$b" ;;
212 git-subtree-split:) sub="$b" ;;
213 END)
214 if [ -n "$sub" ]; then
215 if [ -n "$main" ]; then
216 # a rejoin commit?
217 # Pretend its sub was a squash.
218 sq="$sub"
219 fi
220 debug "Squash found: $sq $sub"
221 echo "$sq" "$sub"
222 break
223 fi
224 sq=
225 main=
226 sub=
227 ;;
228 esac
229 done
230}
231
232find_existing_splits()
233{
234 debug "Looking for prior splits..."
235 dir="$1"
236 revs="$2"
237 main=
238 sub=
239 git log --grep="^git-subtree-dir: $dir/*\$" \
240 --pretty=format:'START %H%n%s%n%n%b%nEND%n' $revs |
241 while read a b junk; do
242 case "$a" in
243 START) sq="$b" ;;
244 git-subtree-mainline:) main="$b" ;;
245 git-subtree-split:) sub="$b" ;;
246 END)
247 debug " Main is: '$main'"
248 if [ -z "$main" -a -n "$sub" ]; then
249 # squash commits refer to a subtree
250 debug " Squash: $sq from $sub"
251 cache_set "$sq" "$sub"
252 fi
253 if [ -n "$main" -a -n "$sub" ]; then
254 debug " Prior: $main -> $sub"
255 cache_set $main $sub
256 cache_set $sub $sub
257 try_remove_previous "$main"
258 try_remove_previous "$sub"
259 fi
260 main=
261 sub=
262 ;;
263 esac
264 done
265}
266
267copy_commit()
268{
269 # We're going to set some environment vars here, so
270 # do it in a subshell to get rid of them safely later
271 debug copy_commit "{$1}" "{$2}" "{$3}"
272 git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%n%b' "$1" |
273 (
274 read GIT_AUTHOR_NAME
275 read GIT_AUTHOR_EMAIL
276 read GIT_AUTHOR_DATE
277 read GIT_COMMITTER_NAME
278 read GIT_COMMITTER_EMAIL
279 read GIT_COMMITTER_DATE
280 export GIT_AUTHOR_NAME \
281 GIT_AUTHOR_EMAIL \
282 GIT_AUTHOR_DATE \
283 GIT_COMMITTER_NAME \
284 GIT_COMMITTER_EMAIL \
285 GIT_COMMITTER_DATE
286 (echo -n "$annotate"; cat ) |
287 git commit-tree "$2" $3 # reads the rest of stdin
288 ) || die "Can't copy commit $1"
289}
290
291add_msg()
292{
293 dir="$1"
294 latest_old="$2"
295 latest_new="$3"
296 if [ -n "$message" ]; then
297 commit_message="$message"
298 else
299 commit_message="Add '$dir/' from commit '$latest_new'"
300 fi
301 cat <<-EOF
302 $commit_message
303
304 git-subtree-dir: $dir
305 git-subtree-mainline: $latest_old
306 git-subtree-split: $latest_new
307 EOF
308}
309
310add_squashed_msg()
311{
312 if [ -n "$message" ]; then
313 echo "$message"
314 else
315 echo "Merge commit '$1' as '$2'"
316 fi
317}
318
319rejoin_msg()
320{
321 dir="$1"
322 latest_old="$2"
323 latest_new="$3"
324 if [ -n "$message" ]; then
325 commit_message="$message"
326 else
327 commit_message="Split '$dir/' into commit '$latest_new'"
328 fi
329 cat <<-EOF
330 $commit_message
331
332 git-subtree-dir: $dir
333 git-subtree-mainline: $latest_old
334 git-subtree-split: $latest_new
335 EOF
336}
337
338squash_msg()
339{
340 dir="$1"
341 oldsub="$2"
342 newsub="$3"
343 newsub_short=$(git rev-parse --short "$newsub")
344
345 if [ -n "$oldsub" ]; then
346 oldsub_short=$(git rev-parse --short "$oldsub")
347 echo "Squashed '$dir/' changes from $oldsub_short..$newsub_short"
348 echo
349 git log --pretty=tformat:'%h %s' "$oldsub..$newsub"
350 git log --pretty=tformat:'REVERT: %h %s' "$newsub..$oldsub"
351 else
352 echo "Squashed '$dir/' content from commit $newsub_short"
353 fi
354
355 echo
356 echo "git-subtree-dir: $dir"
357 echo "git-subtree-split: $newsub"
358}
359
360toptree_for_commit()
361{
362 commit="$1"
363 git log -1 --pretty=format:'%T' "$commit" -- || exit $?
364}
365
366subtree_for_commit()
367{
368 commit="$1"
369 dir="$2"
370 git ls-tree "$commit" -- "$dir" |
371 while read mode type tree name; do
372 assert [ "$name" = "$dir" ]
373 assert [ "$type" = "tree" ]
374 echo $tree
375 break
376 done
377}
378
379tree_changed()
380{
381 tree=$1
382 shift
383 if [ $# -ne 1 ]; then
384 return 0 # weird parents, consider it changed
385 else
386 ptree=$(toptree_for_commit $1)
387 if [ "$ptree" != "$tree" ]; then
388 return 0 # changed
389 else
390 return 1 # not changed
391 fi
392 fi
393}
394
395new_squash_commit()
396{
397 old="$1"
398 oldsub="$2"
399 newsub="$3"
400 tree=$(toptree_for_commit $newsub) || exit $?
401 if [ -n "$old" ]; then
402 squash_msg "$dir" "$oldsub" "$newsub" |
403 git commit-tree "$tree" -p "$old" || exit $?
404 else
405 squash_msg "$dir" "" "$newsub" |
406 git commit-tree "$tree" || exit $?
407 fi
408}
409
410copy_or_skip()
411{
412 rev="$1"
413 tree="$2"
414 newparents="$3"
415 assert [ -n "$tree" ]
416
417 identical=
418 nonidentical=
419 p=
420 gotparents=
421 for parent in $newparents; do
422 ptree=$(toptree_for_commit $parent) || exit $?
423 [ -z "$ptree" ] && continue
424 if [ "$ptree" = "$tree" ]; then
425 # an identical parent could be used in place of this rev.
426 identical="$parent"
427 else
428 nonidentical="$parent"
429 fi
430
431 # sometimes both old parents map to the same newparent;
432 # eliminate duplicates
433 is_new=1
434 for gp in $gotparents; do
435 if [ "$gp" = "$parent" ]; then
436 is_new=
437 break
438 fi
439 done
440 if [ -n "$is_new" ]; then
441 gotparents="$gotparents $parent"
442 p="$p -p $parent"
443 fi
444 done
445
446 if [ -n "$identical" ]; then
447 echo $identical
448 else
449 copy_commit $rev $tree "$p" || exit $?
450 fi
451}
452
453ensure_clean()
454{
455 if ! git diff-index HEAD --exit-code --quiet 2>&1; then
456 die "Working tree has modifications. Cannot add."
457 fi
458 if ! git diff-index --cached HEAD --exit-code --quiet 2>&1; then
459 die "Index has modifications. Cannot add."
460 fi
461}
462
463cmd_add()
464{
465 if [ -e "$dir" ]; then
466 die "'$dir' already exists. Cannot add."
467 fi
468
469 ensure_clean
470
471 if [ $# -eq 1 ]; then
472 "cmd_add_commit" "$@"
473 elif [ $# -eq 2 ]; then
474 "cmd_add_repository" "$@"
475 else
476 say "error: parameters were '$@'"
477 die "Provide either a refspec or a repository and refspec."
478 fi
479}
480
481cmd_add_repository()
482{
483 echo "git fetch" "$@"
484 repository=$1
485 refspec=$2
486 git fetch "$@" || exit $?
487 revs=FETCH_HEAD
488 set -- $revs
489 cmd_add_commit "$@"
490}
491
492cmd_add_commit()
493{
494 revs=$(git rev-parse $default --revs-only "$@") || exit $?
495 set -- $revs
496 rev="$1"
497
498 debug "Adding $dir as '$rev'..."
499 git read-tree --prefix="$dir" $rev || exit $?
500 git checkout -- "$dir" || exit $?
501 tree=$(git write-tree) || exit $?
502
503 headrev=$(git rev-parse HEAD) || exit $?
504 if [ -n "$headrev" -a "$headrev" != "$rev" ]; then
505 headp="-p $headrev"
506 else
507 headp=
508 fi
509
510 if [ -n "$squash" ]; then
511 rev=$(new_squash_commit "" "" "$rev") || exit $?
512 commit=$(add_squashed_msg "$rev" "$dir" |
513 git commit-tree $tree $headp -p "$rev") || exit $?
514 else
515 commit=$(add_msg "$dir" "$headrev" "$rev" |
516 git commit-tree $tree $headp -p "$rev") || exit $?
517 fi
518 git reset "$commit" || exit $?
519
520 say "Added dir '$dir'"
521}
522
523cmd_split()
524{
525 debug "Splitting $dir..."
526 cache_setup || exit $?
527
528 if [ -n "$onto" ]; then
529 debug "Reading history for --onto=$onto..."
530 git rev-list $onto |
531 while read rev; do
532 # the 'onto' history is already just the subdir, so
533 # any parent we find there can be used verbatim
534 debug " cache: $rev"
535 cache_set $rev $rev
536 done
537 fi
538
539 if [ -n "$ignore_joins" ]; then
540 unrevs=
541 else
542 unrevs="$(find_existing_splits "$dir" "$revs")"
543 fi
544
545 # We can't restrict rev-list to only $dir here, because some of our
546 # parents have the $dir contents the root, and those won't match.
547 # (and rev-list --follow doesn't seem to solve this)
548 grl='git rev-list --reverse --parents $revs $unrevs'
549 revmax=$(eval "$grl" | wc -l)
550 revcount=0
551 createcount=0
552 eval "$grl" |
553 while read rev parents; do
554 revcount=$(($revcount + 1))
555 say -n "$revcount/$revmax ($createcount)
"
556 debug "Processing commit: $rev"
557 exists=$(cache_get $rev)
558 if [ -n "$exists" ]; then
559 debug " prior: $exists"
560 continue
561 fi
562 createcount=$(($createcount + 1))
563 debug " parents: $parents"
564 newparents=$(cache_get $parents)
565 debug " newparents: $newparents"
566
567 tree=$(subtree_for_commit $rev "$dir")
568 debug " tree is: $tree"
569
570 # ugly. is there no better way to tell if this is a subtree
571 # vs. a mainline commit? Does it matter?
572 if [ -z $tree ]; then
573 if [ -n "$newparents" ]; then
574 cache_set $rev $rev
575 fi
576 continue
577 fi
578
579 newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $?
580 debug " newrev is: $newrev"
581 cache_set $rev $newrev
582 cache_set latest_new $newrev
583 cache_set latest_old $rev
584 done || exit $?
585 latest_new=$(cache_get latest_new)
586 if [ -z "$latest_new" ]; then
587 die "No new revisions were found"
588 fi
589
590 if [ -n "$rejoin" ]; then
591 debug "Merging split branch into HEAD..."
592 latest_old=$(cache_get latest_old)
593 git merge -s ours \
594 -m "$(rejoin_msg $dir $latest_old $latest_new)" \
595 $latest_new >&2 || exit $?
596 fi
597 if [ -n "$branch" ]; then
598 if rev_exists "refs/heads/$branch"; then
599 if ! rev_is_descendant_of_branch $latest_new $branch; then
600 die "Branch '$branch' is not an ancestor of commit '$latest_new'."
601 fi
602 action='Updated'
603 else
604 action='Created'
605 fi
606 git update-ref -m 'subtree split' "refs/heads/$branch" $latest_new || exit $?
607 say "$action branch '$branch'"
608 fi
609 echo $latest_new
610 exit 0
611}
612
613cmd_merge()
614{
615 revs=$(git rev-parse $default --revs-only "$@") || exit $?
616 ensure_clean
617
618 set -- $revs
619 if [ $# -ne 1 ]; then
620 die "You must provide exactly one revision. Got: '$revs'"
621 fi
622 rev="$1"
623
624 if [ -n "$squash" ]; then
625 first_split="$(find_latest_squash "$dir")"
626 if [ -z "$first_split" ]; then
627 die "Can't squash-merge: '$dir' was never added."
628 fi
629 set $first_split
630 old=$1
631 sub=$2
632 if [ "$sub" = "$rev" ]; then
633 say "Subtree is already at commit $rev."
634 exit 0
635 fi
636 new=$(new_squash_commit "$old" "$sub" "$rev") || exit $?
637 debug "New squash commit: $new"
638 rev="$new"
639 fi
640
641 version=$(git version)
642 if [ "$version" \< "git version 1.7" ]; then
643 if [ -n "$message" ]; then
644 git merge -s subtree --message="$message" $rev
645 else
646 git merge -s subtree $rev
647 fi
648 else
649 if [ -n "$message" ]; then
650 git merge -Xsubtree="$prefix" --message="$message" $rev
651 else
652 git merge -Xsubtree="$prefix" $rev
653 fi
654 fi
655}
656
657cmd_pull()
658{
659 ensure_clean
660 git fetch "$@" || exit $?
661 revs=FETCH_HEAD
662 set -- $revs
663 cmd_merge "$@"
664}
665
666cmd_push()
667{
668 if [ $# -ne 2 ]; then
669 die "You must provide <repository> <refspec>"
670 fi
671 if [ -e "$dir" ]; then
672 repository=$1
673 refspec=$2
674 echo "git push using: " $repository $refspec
675 git push $repository $(git subtree split --prefix=$prefix):refs/heads/$refspec
676 else
677 die "'$dir' must already exist. Try 'git subtree add'."
678 fi
679}
680
681"cmd_$command" "$@"