3 # git-subtree.sh: split/join git repositories in subdirectories of this one
5 # Copyright (C) 2009 Avery Pennarun <apenwarr@gmail.com>
11 git subtree add --prefix=<prefix> <commit>
12 git subtree add --prefix=<prefix> <repository> <ref>
13 git subtree merge --prefix=<prefix> <commit>
14 git subtree pull --prefix=<prefix> <repository> <ref>
15 git subtree push --prefix=<prefix> <repository> <ref>
16 git subtree split --prefix=<prefix> <commit...>
21 P,prefix= the name of the subdir to split out
22 m,message= use the given message as the commit message for the merge commit
24 annotate= add a prefix to commit message of new commits
25 b,branch= create a new branch from the split subtree
26 ignore-joins ignore prior --rejoin commits
27 onto= try connecting new tree to an existing one
28 rejoin merge the new branch back into HEAD
29 options for 'add', 'merge', 'pull' and 'push'
30 squash merge subtree changes as a single commit
32 eval "$(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@
" || echo exit $?)"
34 PATH
=$PATH:$
(git
--exec-path)
53 if [ -n "$debug" ]; then
60 if [ -z "$quiet" ]; then
70 die
"assertion failed: " "$@"
77 while [ $# -gt 0 ]; do
83 --annotate) annotate
="$1"; shift ;;
84 --no-annotate) annotate
= ;;
85 -b) branch
="$1"; shift ;;
86 -P) prefix
="$1"; shift ;;
87 -m) message
="$1"; shift ;;
88 --no-prefix) prefix
= ;;
89 --onto) onto
="$1"; shift ;;
92 --no-rejoin) rejoin
= ;;
93 --ignore-joins) ignore_joins
=1 ;;
94 --no-ignore-joins) ignore_joins
= ;;
96 --no-squash) squash
= ;;
98 *) die
"Unexpected option: $opt" ;;
105 add|merge|pull
) default
= ;;
106 split|push
) default
="--default HEAD" ;;
107 *) die
"Unknown command '$command'" ;;
110 if [ -z "$prefix" ]; then
111 die
"You must provide the --prefix option."
115 add
) [ -e "$prefix" ] &&
116 die
"prefix '$prefix' already exists." ;;
117 *) [ -e "$prefix" ] ||
118 die
"'$prefix' does not exist; use 'git subtree add'" ;;
121 dir
="$(dirname "$prefix/.
")"
123 if [ "$command" != "pull" -a "$command" != "add" -a "$command" != "push" ]; then
124 revs
=$
(git rev-parse
$default --revs-only "$@") ||
exit $?
125 dirs="$(git rev-parse --no-revs --no-flags "$@
")" ||
exit $?
126 if [ -n "$dirs" ]; then
127 die
"Error: Use --prefix instead of bare filenames."
131 debug
"command: {$command}"
132 debug
"quiet: {$quiet}"
133 debug
"revs: {$revs}"
140 cachedir
="$GIT_DIR/subtree-cache/$$"
141 rm -rf "$cachedir" || die
"Can't delete old cachedir: $cachedir"
142 mkdir
-p "$cachedir" || die
"Can't create new cachedir: $cachedir"
143 mkdir
-p "$cachedir/notree" || die
"Can't create new cachedir: $cachedir/notree"
144 debug
"Using cachedir: $cachedir" >&2
150 if [ -r "$cachedir/$oldrev" ]; then
151 read newrev
<"$cachedir/$oldrev"
160 if [ ! -r "$cachedir/$oldrev" ]; then
168 missed
=$
(cache_miss $
*)
169 for miss
in $missed; do
170 if [ ! -r "$cachedir/notree/$miss" ]; then
171 debug
" incorrect order: $miss"
178 echo "1" > "$cachedir/notree/$1"
185 if [ "$oldrev" != "latest_old" \
186 -a "$oldrev" != "latest_new" \
187 -a -e "$cachedir/$oldrev" ]; then
188 die
"cache for $oldrev already exists!"
190 echo "$newrev" >"$cachedir/$oldrev"
195 if git rev-parse
"$1" >/dev
/null
2>&1; then
202 rev_is_descendant_of_branch
()
206 branch_hash
=$
(git rev-parse
$branch)
207 match
=$
(git rev-list
-1 $branch_hash ^
$newrev)
209 if [ -z "$match" ]; then
216 # if a commit doesn't have a parent, this might not work. But we only want
217 # to remove the parent from the rev-list, and since it doesn't exist, it won't
218 # be there anyway, so do nothing in that case.
219 try_remove_previous
()
221 if rev_exists
"$1^"; then
228 debug
"Looking for latest squash ($dir)..."
233 git log
--grep="^git-subtree-dir: $dir/*\$" \
234 --pretty=format
:'START %H%n%s%n%n%b%nEND%n' HEAD |
235 while read a b junk
; do
237 debug
"{{$sq/$main/$sub}}"
240 git-subtree-mainline
:) main
="$b" ;;
241 git-subtree-split
:) sub
="$b" ;;
243 if [ -n "$sub" ]; then
244 if [ -n "$main" ]; then
246 # Pretend its sub was a squash.
249 debug
"Squash found: $sq $sub"
261 find_existing_splits
()
263 debug
"Looking for prior splits..."
268 git log
--grep="^git-subtree-dir: $dir/*\$" \
269 --pretty=format
:'START %H%n%s%n%n%b%nEND%n' $revs |
270 while read a b junk
; do
273 git-subtree-mainline
:) main
="$b" ;;
274 git-subtree-split
:) sub
="$b" ;;
276 debug
" Main is: '$main'"
277 if [ -z "$main" -a -n "$sub" ]; then
278 # squash commits refer to a subtree
279 debug
" Squash: $sq from $sub"
280 cache_set
"$sq" "$sub"
282 if [ -n "$main" -a -n "$sub" ]; then
283 debug
" Prior: $main -> $sub"
286 try_remove_previous
"$main"
287 try_remove_previous
"$sub"
298 # We're going to set some environment vars here, so
299 # do it in a subshell to get rid of them safely later
300 debug copy_commit
"{$1}" "{$2}" "{$3}"
301 git log
-1 --pretty=format
:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%B' "$1" |
304 read GIT_AUTHOR_EMAIL
306 read GIT_COMMITTER_NAME
307 read GIT_COMMITTER_EMAIL
308 read GIT_COMMITTER_DATE
309 export GIT_AUTHOR_NAME \
313 GIT_COMMITTER_EMAIL \
315 (printf "%s" "$annotate"; cat ) |
316 git commit-tree
"$2" $3 # reads the rest of stdin
317 ) || die
"Can't copy commit $1"
325 if [ -n "$message" ]; then
326 commit_message
="$message"
328 commit_message
="Add '$dir/' from commit '$latest_new'"
333 git-subtree-dir: $dir
334 git-subtree-mainline: $latest_old
335 git-subtree-split: $latest_new
341 if [ -n "$message" ]; then
344 echo "Merge commit '$1' as '$2'"
353 if [ -n "$message" ]; then
354 commit_message
="$message"
356 commit_message
="Split '$dir/' into commit '$latest_new'"
361 git-subtree-dir: $dir
362 git-subtree-mainline: $latest_old
363 git-subtree-split: $latest_new
372 newsub_short
=$
(git rev-parse
--short "$newsub")
374 if [ -n "$oldsub" ]; then
375 oldsub_short
=$
(git rev-parse
--short "$oldsub")
376 echo "Squashed '$dir/' changes from $oldsub_short..$newsub_short"
378 git log
--pretty=tformat
:'%h %s' "$oldsub..$newsub"
379 git log
--pretty=tformat
:'REVERT: %h %s' "$newsub..$oldsub"
381 echo "Squashed '$dir/' content from commit $newsub_short"
385 echo "git-subtree-dir: $dir"
386 echo "git-subtree-split: $newsub"
392 git log
-1 --pretty=format
:'%T' "$commit" -- ||
exit $?
399 git ls-tree
"$commit" -- "$dir" |
400 while read mode
type tree name
; do
401 assert
[ "$name" = "$dir" ]
402 assert
[ "$type" = "tree" -o "$type" = "commit" ]
403 [ "$type" = "commit" ] && continue # ignore submodules
413 if [ $# -ne 1 ]; then
414 return 0 # weird parents, consider it changed
416 ptree
=$
(toptree_for_commit
$1)
417 if [ "$ptree" != "$tree" ]; then
420 return 1 # not changed
430 tree
=$
(toptree_for_commit
$newsub) ||
exit $?
431 if [ -n "$old" ]; then
432 squash_msg
"$dir" "$oldsub" "$newsub" |
433 git commit-tree
"$tree" -p "$old" ||
exit $?
435 squash_msg
"$dir" "" "$newsub" |
436 git commit-tree
"$tree" ||
exit $?
445 assert
[ -n "$tree" ]
451 for parent
in $newparents; do
452 ptree
=$
(toptree_for_commit
$parent) ||
exit $?
453 [ -z "$ptree" ] && continue
454 if [ "$ptree" = "$tree" ]; then
455 # an identical parent could be used in place of this rev.
458 nonidentical
="$parent"
461 # sometimes both old parents map to the same newparent;
462 # eliminate duplicates
464 for gp
in $gotparents; do
465 if [ "$gp" = "$parent" ]; then
470 if [ -n "$is_new" ]; then
471 gotparents
="$gotparents $parent"
476 if [ -n "$identical" ]; then
479 copy_commit
$rev $tree "$p" ||
exit $?
485 if ! git diff-index HEAD
--exit-code --quiet 2>&1; then
486 die
"Working tree has modifications. Cannot add."
488 if ! git diff-index
--cached HEAD
--exit-code --quiet 2>&1; then
489 die
"Index has modifications. Cannot add."
493 ensure_valid_ref_format
()
495 git check-ref-format
"refs/heads/$1" ||
496 die
"'$1' does not look like a ref"
501 if [ -e "$dir" ]; then
502 die
"'$dir' already exists. Cannot add."
507 if [ $# -eq 1 ]; then
508 git rev-parse
-q --verify "$1^{commit}" >/dev
/null ||
509 die
"'$1' does not refer to a commit"
511 "cmd_add_commit" "$@"
512 elif [ $# -eq 2 ]; then
513 # Technically we could accept a refspec here but we're
514 # just going to turn around and add FETCH_HEAD under the
515 # specified directory. Allowing a refspec might be
516 # misleading because we won't do anything with any other
517 # branches fetched via the refspec.
518 ensure_valid_ref_format
"$2"
520 "cmd_add_repository" "$@"
522 say
"error: parameters were '$@'"
523 die
"Provide either a commit or a repository and commit."
529 echo "git fetch" "$@"
532 git fetch
"$@" ||
exit $?
540 revs
=$
(git rev-parse
$default --revs-only "$@") ||
exit $?
544 debug
"Adding $dir as '$rev'..."
545 git read-tree
--prefix="$dir" $rev ||
exit $?
546 git checkout
-- "$dir" ||
exit $?
547 tree
=$
(git write-tree
) ||
exit $?
549 headrev
=$
(git rev-parse HEAD
) ||
exit $?
550 if [ -n "$headrev" -a "$headrev" != "$rev" ]; then
556 if [ -n "$squash" ]; then
557 rev=$
(new_squash_commit
"" "" "$rev") ||
exit $?
558 commit
=$
(add_squashed_msg
"$rev" "$dir" |
559 git commit-tree
$tree $headp -p "$rev") ||
exit $?
561 revp
=$
(peel_committish
"$rev") &&
562 commit
=$
(add_msg
"$dir" "$headrev" "$rev" |
563 git commit-tree
$tree $headp -p "$revp") ||
exit $?
565 git
reset "$commit" ||
exit $?
567 say
"Added dir '$dir'"
572 debug
"Splitting $dir..."
573 cache_setup ||
exit $?
575 if [ -n "$onto" ]; then
576 debug
"Reading history for --onto=$onto..."
579 # the 'onto' history is already just the subdir, so
580 # any parent we find there can be used verbatim
586 if [ -n "$ignore_joins" ]; then
589 unrevs
="$(find_existing_splits "$dir" "$revs")"
592 # We can't restrict rev-list to only $dir here, because some of our
593 # parents have the $dir contents the root, and those won't match.
594 # (and rev-list --follow doesn't seem to solve this)
595 grl
='git rev-list --topo-order --reverse --parents $revs $unrevs'
596 revmax
=$
(eval "$grl" |
wc -l)
600 while read rev parents
; do
601 revcount
=$
(($revcount + 1))
602 say
-n "$revcount/$revmax ($createcount)
"
603 debug
"Processing commit: $rev"
604 exists
=$
(cache_get
$rev)
605 if [ -n "$exists" ]; then
606 debug
" prior: $exists"
609 createcount
=$
(($createcount + 1))
610 debug
" parents: $parents"
611 newparents
=$
(cache_get
$parents)
612 debug
" newparents: $newparents"
614 tree
=$
(subtree_for_commit
$rev "$dir")
615 debug
" tree is: $tree"
617 check_parents
$parents
619 # ugly. is there no better way to tell if this is a subtree
620 # vs. a mainline commit? Does it matter?
621 if [ -z $tree ]; then
623 if [ -n "$newparents" ]; then
629 newrev
=$
(copy_or_skip
"$rev" "$tree" "$newparents") ||
exit $?
630 debug
" newrev is: $newrev"
631 cache_set
$rev $newrev
632 cache_set latest_new
$newrev
633 cache_set latest_old
$rev
635 latest_new
=$
(cache_get latest_new
)
636 if [ -z "$latest_new" ]; then
637 die
"No new revisions were found"
640 if [ -n "$rejoin" ]; then
641 debug
"Merging split branch into HEAD..."
642 latest_old
=$
(cache_get latest_old
)
644 -m "$(rejoin_msg $dir $latest_old $latest_new)" \
645 $latest_new >&2 ||
exit $?
647 if [ -n "$branch" ]; then
648 if rev_exists
"refs/heads/$branch"; then
649 if ! rev_is_descendant_of_branch
$latest_new $branch; then
650 die
"Branch '$branch' is not an ancestor of commit '$latest_new'."
656 git update-ref
-m 'subtree split' "refs/heads/$branch" $latest_new ||
exit $?
657 say
"$action branch '$branch'"
665 revs
=$
(git rev-parse
$default --revs-only "$@") ||
exit $?
669 if [ $# -ne 1 ]; then
670 die
"You must provide exactly one revision. Got: '$revs'"
674 if [ -n "$squash" ]; then
675 first_split
="$(find_latest_squash "$dir")"
676 if [ -z "$first_split" ]; then
677 die
"Can't squash-merge: '$dir' was never added."
682 if [ "$sub" = "$rev" ]; then
683 say
"Subtree is already at commit $rev."
686 new
=$
(new_squash_commit
"$old" "$sub" "$rev") ||
exit $?
687 debug
"New squash commit: $new"
691 version
=$
(git version
)
692 if [ "$version" \
< "git version 1.7" ]; then
693 if [ -n "$message" ]; then
694 git merge
-s subtree
--message="$message" $rev
696 git merge
-s subtree
$rev
699 if [ -n "$message" ]; then
700 git merge
-Xsubtree="$prefix" --message="$message" $rev
702 git merge
-Xsubtree="$prefix" $rev
709 if [ $# -ne 2 ]; then
710 die
"You must provide <repository> <ref>"
713 ensure_valid_ref_format
"$2"
714 git fetch
"$@" ||
exit $?
722 if [ $# -ne 2 ]; then
723 die
"You must provide <repository> <ref>"
725 ensure_valid_ref_format
"$2"
726 if [ -e "$dir" ]; then
729 echo "git push using: " $repository $refspec
730 localrev
=$
(git subtree
split --prefix="$prefix") || die
731 git push
$repository $localrev:refs
/heads
/$refspec
733 die
"'$dir' must already exist. Try 'git subtree add'."