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
54 printf "%s\n" "$*" >&2
60 if [ -z "$quiet" ]; then
61 printf "%s\n" "$*" >&2
67 if [ -z "$quiet" ]; then
68 printf "%s\r" "$*" >&2
77 die
"assertion failed: " "$@"
84 while [ $# -gt 0 ]; do
90 --annotate) annotate
="$1"; shift ;;
91 --no-annotate) annotate
= ;;
92 -b) branch
="$1"; shift ;;
93 -P) prefix
="$1"; shift ;;
94 -m) message
="$1"; shift ;;
95 --no-prefix) prefix
= ;;
96 --onto) onto
="$1"; shift ;;
99 --no-rejoin) rejoin
= ;;
100 --ignore-joins) ignore_joins
=1 ;;
101 --no-ignore-joins) ignore_joins
= ;;
102 --squash) squash
=1 ;;
103 --no-squash) squash
= ;;
105 *) die
"Unexpected option: $opt" ;;
112 add|merge|pull
) default
= ;;
113 split|push
) default
="--default HEAD" ;;
114 *) die
"Unknown command '$command'" ;;
117 if [ -z "$prefix" ]; then
118 die
"You must provide the --prefix option."
122 add
) [ -e "$prefix" ] &&
123 die
"prefix '$prefix' already exists." ;;
124 *) [ -e "$prefix" ] ||
125 die
"'$prefix' does not exist; use 'git subtree add'" ;;
128 dir
="$(dirname "$prefix/.
")"
130 if [ "$command" != "pull" -a "$command" != "add" -a "$command" != "push" ]; then
131 revs
=$
(git rev-parse
$default --revs-only "$@") ||
exit $?
132 dirs="$(git rev-parse --no-revs --no-flags "$@
")" ||
exit $?
133 if [ -n "$dirs" ]; then
134 die
"Error: Use --prefix instead of bare filenames."
138 debug
"command: {$command}"
139 debug
"quiet: {$quiet}"
140 debug
"revs: {$revs}"
147 cachedir
="$GIT_DIR/subtree-cache/$$"
148 rm -rf "$cachedir" || die
"Can't delete old cachedir: $cachedir"
149 mkdir
-p "$cachedir" || die
"Can't create new cachedir: $cachedir"
150 mkdir
-p "$cachedir/notree" || die
"Can't create new cachedir: $cachedir/notree"
151 debug
"Using cachedir: $cachedir" >&2
157 if [ -r "$cachedir/$oldrev" ]; then
158 read newrev
<"$cachedir/$oldrev"
167 if [ ! -r "$cachedir/$oldrev" ]; then
175 missed
=$
(cache_miss $
*)
176 for miss
in $missed; do
177 if [ ! -r "$cachedir/notree/$miss" ]; then
178 debug
" incorrect order: $miss"
185 echo "1" > "$cachedir/notree/$1"
192 if [ "$oldrev" != "latest_old" \
193 -a "$oldrev" != "latest_new" \
194 -a -e "$cachedir/$oldrev" ]; then
195 die
"cache for $oldrev already exists!"
197 echo "$newrev" >"$cachedir/$oldrev"
202 if git rev-parse
"$1" >/dev
/null
2>&1; then
209 rev_is_descendant_of_branch
()
213 branch_hash
=$
(git rev-parse
$branch)
214 match
=$
(git rev-list
-1 $branch_hash ^
$newrev)
216 if [ -z "$match" ]; then
223 # if a commit doesn't have a parent, this might not work. But we only want
224 # to remove the parent from the rev-list, and since it doesn't exist, it won't
225 # be there anyway, so do nothing in that case.
226 try_remove_previous
()
228 if rev_exists
"$1^"; then
235 debug
"Looking for latest squash ($dir)..."
240 git log
--grep="^git-subtree-dir: $dir/*\$" \
241 --pretty=format
:'START %H%n%s%n%n%b%nEND%n' HEAD |
242 while read a b junk
; do
244 debug
"{{$sq/$main/$sub}}"
247 git-subtree-mainline
:) main
="$b" ;;
248 git-subtree-split
:) sub
="$b" ;;
250 if [ -n "$sub" ]; then
251 if [ -n "$main" ]; then
253 # Pretend its sub was a squash.
256 debug
"Squash found: $sq $sub"
268 find_existing_splits
()
270 debug
"Looking for prior splits..."
275 git log
--grep="^git-subtree-dir: $dir/*\$" \
276 --pretty=format
:'START %H%n%s%n%n%b%nEND%n' $revs |
277 while read a b junk
; do
280 git-subtree-mainline
:) main
="$b" ;;
281 git-subtree-split
:) sub
="$b" ;;
283 debug
" Main is: '$main'"
284 if [ -z "$main" -a -n "$sub" ]; then
285 # squash commits refer to a subtree
286 debug
" Squash: $sq from $sub"
287 cache_set
"$sq" "$sub"
289 if [ -n "$main" -a -n "$sub" ]; then
290 debug
" Prior: $main -> $sub"
293 try_remove_previous
"$main"
294 try_remove_previous
"$sub"
305 # We're going to set some environment vars here, so
306 # do it in a subshell to get rid of them safely later
307 debug copy_commit
"{$1}" "{$2}" "{$3}"
308 git log
-1 --pretty=format
:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%B' "$1" |
311 read GIT_AUTHOR_EMAIL
313 read GIT_COMMITTER_NAME
314 read GIT_COMMITTER_EMAIL
315 read GIT_COMMITTER_DATE
316 export GIT_AUTHOR_NAME \
320 GIT_COMMITTER_EMAIL \
322 (printf "%s" "$annotate"; cat ) |
323 git commit-tree
"$2" $3 # reads the rest of stdin
324 ) || die
"Can't copy commit $1"
332 if [ -n "$message" ]; then
333 commit_message
="$message"
335 commit_message
="Add '$dir/' from commit '$latest_new'"
340 git-subtree-dir: $dir
341 git-subtree-mainline: $latest_old
342 git-subtree-split: $latest_new
348 if [ -n "$message" ]; then
351 echo "Merge commit '$1' as '$2'"
360 if [ -n "$message" ]; then
361 commit_message
="$message"
363 commit_message
="Split '$dir/' into commit '$latest_new'"
368 git-subtree-dir: $dir
369 git-subtree-mainline: $latest_old
370 git-subtree-split: $latest_new
379 newsub_short
=$
(git rev-parse
--short "$newsub")
381 if [ -n "$oldsub" ]; then
382 oldsub_short
=$
(git rev-parse
--short "$oldsub")
383 echo "Squashed '$dir/' changes from $oldsub_short..$newsub_short"
385 git log
--pretty=tformat
:'%h %s' "$oldsub..$newsub"
386 git log
--pretty=tformat
:'REVERT: %h %s' "$newsub..$oldsub"
388 echo "Squashed '$dir/' content from commit $newsub_short"
392 echo "git-subtree-dir: $dir"
393 echo "git-subtree-split: $newsub"
399 git log
-1 --pretty=format
:'%T' "$commit" -- ||
exit $?
406 git ls-tree
"$commit" -- "$dir" |
407 while read mode
type tree name
; do
408 assert
[ "$name" = "$dir" ]
409 assert
[ "$type" = "tree" -o "$type" = "commit" ]
410 [ "$type" = "commit" ] && continue # ignore submodules
420 if [ $# -ne 1 ]; then
421 return 0 # weird parents, consider it changed
423 ptree
=$
(toptree_for_commit
$1)
424 if [ "$ptree" != "$tree" ]; then
427 return 1 # not changed
437 tree
=$
(toptree_for_commit
$newsub) ||
exit $?
438 if [ -n "$old" ]; then
439 squash_msg
"$dir" "$oldsub" "$newsub" |
440 git commit-tree
"$tree" -p "$old" ||
exit $?
442 squash_msg
"$dir" "" "$newsub" |
443 git commit-tree
"$tree" ||
exit $?
452 assert
[ -n "$tree" ]
458 for parent
in $newparents; do
459 ptree
=$
(toptree_for_commit
$parent) ||
exit $?
460 [ -z "$ptree" ] && continue
461 if [ "$ptree" = "$tree" ]; then
462 # an identical parent could be used in place of this rev.
465 nonidentical
="$parent"
468 # sometimes both old parents map to the same newparent;
469 # eliminate duplicates
471 for gp
in $gotparents; do
472 if [ "$gp" = "$parent" ]; then
477 if [ -n "$is_new" ]; then
478 gotparents
="$gotparents $parent"
483 if [ -n "$identical" ]; then
486 copy_commit
$rev $tree "$p" ||
exit $?
492 if ! git diff-index HEAD
--exit-code --quiet 2>&1; then
493 die
"Working tree has modifications. Cannot add."
495 if ! git diff-index
--cached HEAD
--exit-code --quiet 2>&1; then
496 die
"Index has modifications. Cannot add."
500 ensure_valid_ref_format
()
502 git check-ref-format
"refs/heads/$1" ||
503 die
"'$1' does not look like a ref"
508 if [ -e "$dir" ]; then
509 die
"'$dir' already exists. Cannot add."
514 if [ $# -eq 1 ]; then
515 git rev-parse
-q --verify "$1^{commit}" >/dev
/null ||
516 die
"'$1' does not refer to a commit"
518 "cmd_add_commit" "$@"
519 elif [ $# -eq 2 ]; then
520 # Technically we could accept a refspec here but we're
521 # just going to turn around and add FETCH_HEAD under the
522 # specified directory. Allowing a refspec might be
523 # misleading because we won't do anything with any other
524 # branches fetched via the refspec.
525 ensure_valid_ref_format
"$2"
527 "cmd_add_repository" "$@"
529 say
"error: parameters were '$@'"
530 die
"Provide either a commit or a repository and commit."
536 echo "git fetch" "$@"
539 git fetch
"$@" ||
exit $?
547 revs
=$
(git rev-parse
$default --revs-only "$@") ||
exit $?
551 debug
"Adding $dir as '$rev'..."
552 git read-tree
--prefix="$dir" $rev ||
exit $?
553 git checkout
-- "$dir" ||
exit $?
554 tree
=$
(git write-tree
) ||
exit $?
556 headrev
=$
(git rev-parse HEAD
) ||
exit $?
557 if [ -n "$headrev" -a "$headrev" != "$rev" ]; then
563 if [ -n "$squash" ]; then
564 rev=$
(new_squash_commit
"" "" "$rev") ||
exit $?
565 commit
=$
(add_squashed_msg
"$rev" "$dir" |
566 git commit-tree
$tree $headp -p "$rev") ||
exit $?
568 revp
=$
(peel_committish
"$rev") &&
569 commit
=$
(add_msg
"$dir" "$headrev" "$rev" |
570 git commit-tree
$tree $headp -p "$revp") ||
exit $?
572 git
reset "$commit" ||
exit $?
574 say
"Added dir '$dir'"
579 debug
"Splitting $dir..."
580 cache_setup ||
exit $?
582 if [ -n "$onto" ]; then
583 debug
"Reading history for --onto=$onto..."
586 # the 'onto' history is already just the subdir, so
587 # any parent we find there can be used verbatim
593 if [ -n "$ignore_joins" ]; then
596 unrevs
="$(find_existing_splits "$dir" "$revs")"
599 # We can't restrict rev-list to only $dir here, because some of our
600 # parents have the $dir contents the root, and those won't match.
601 # (and rev-list --follow doesn't seem to solve this)
602 grl
='git rev-list --topo-order --reverse --parents $revs $unrevs'
603 revmax
=$
(eval "$grl" |
wc -l)
607 while read rev parents
; do
608 revcount
=$
(($revcount + 1))
609 progress
"$revcount/$revmax ($createcount)"
610 debug
"Processing commit: $rev"
611 exists
=$
(cache_get
$rev)
612 if [ -n "$exists" ]; then
613 debug
" prior: $exists"
616 createcount
=$
(($createcount + 1))
617 debug
" parents: $parents"
618 newparents
=$
(cache_get
$parents)
619 debug
" newparents: $newparents"
621 tree
=$
(subtree_for_commit
$rev "$dir")
622 debug
" tree is: $tree"
624 check_parents
$parents
626 # ugly. is there no better way to tell if this is a subtree
627 # vs. a mainline commit? Does it matter?
628 if [ -z $tree ]; then
630 if [ -n "$newparents" ]; then
636 newrev
=$
(copy_or_skip
"$rev" "$tree" "$newparents") ||
exit $?
637 debug
" newrev is: $newrev"
638 cache_set
$rev $newrev
639 cache_set latest_new
$newrev
640 cache_set latest_old
$rev
642 latest_new
=$
(cache_get latest_new
)
643 if [ -z "$latest_new" ]; then
644 die
"No new revisions were found"
647 if [ -n "$rejoin" ]; then
648 debug
"Merging split branch into HEAD..."
649 latest_old
=$
(cache_get latest_old
)
651 -m "$(rejoin_msg $dir $latest_old $latest_new)" \
652 $latest_new >&2 ||
exit $?
654 if [ -n "$branch" ]; then
655 if rev_exists
"refs/heads/$branch"; then
656 if ! rev_is_descendant_of_branch
$latest_new $branch; then
657 die
"Branch '$branch' is not an ancestor of commit '$latest_new'."
663 git update-ref
-m 'subtree split' "refs/heads/$branch" $latest_new ||
exit $?
664 say
"$action branch '$branch'"
672 revs
=$
(git rev-parse
$default --revs-only "$@") ||
exit $?
676 if [ $# -ne 1 ]; then
677 die
"You must provide exactly one revision. Got: '$revs'"
681 if [ -n "$squash" ]; then
682 first_split
="$(find_latest_squash "$dir")"
683 if [ -z "$first_split" ]; then
684 die
"Can't squash-merge: '$dir' was never added."
689 if [ "$sub" = "$rev" ]; then
690 say
"Subtree is already at commit $rev."
693 new
=$
(new_squash_commit
"$old" "$sub" "$rev") ||
exit $?
694 debug
"New squash commit: $new"
698 version
=$
(git version
)
699 if [ "$version" \
< "git version 1.7" ]; then
700 if [ -n "$message" ]; then
701 git merge
-s subtree
--message="$message" $rev
703 git merge
-s subtree
$rev
706 if [ -n "$message" ]; then
707 git merge
-Xsubtree="$prefix" --message="$message" $rev
709 git merge
-Xsubtree="$prefix" $rev
716 if [ $# -ne 2 ]; then
717 die
"You must provide <repository> <ref>"
720 ensure_valid_ref_format
"$2"
721 git fetch
"$@" ||
exit $?
729 if [ $# -ne 2 ]; then
730 die
"You must provide <repository> <ref>"
732 ensure_valid_ref_format
"$2"
733 if [ -e "$dir" ]; then
736 echo "git push using: " $repository $refspec
737 localrev
=$
(git subtree
split --prefix="$prefix") || die
738 git push
$repository $localrev:refs
/heads
/$refspec
740 die
"'$dir' must already exist. Try 'git subtree add'."