Skip to content

Commit 79e3055

Browse files
committed
Merge branch 'cs/rebased-subtree-split'
The split command in "git subtree" (in contrib/) has been taught to deal better with rebased history. * cs/rebased-subtree-split: contrib/subtree: detect rewritten subtree commits
2 parents 9813aac + 28a7e27 commit 79e3055

File tree

2 files changed

+169
-55
lines changed

2 files changed

+169
-55
lines changed

contrib/subtree/git-subtree.sh

Lines changed: 92 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,12 @@ check_parents () {
325325
done
326326
}
327327

328+
# Usage: get_notree REV
329+
get_notree () {
330+
assert test $# = 1
331+
test -r "$cachedir/notree/$1"
332+
}
333+
328334
# Usage: set_notree REV
329335
set_notree () {
330336
assert test $# = 1
@@ -511,6 +517,71 @@ find_existing_splits () {
511517
done || exit $?
512518
}
513519

520+
# Usage: find_other_splits DIR REV UNREVS...
521+
#
522+
# Scan history in REV UNREVS for other `git subtree split --rejoin`
523+
# merge commits belonging to prefixes outside of DIR. These
524+
# "other splits" don't contribute to DIR and can be ignored.
525+
#
526+
# If any such rejoins are found,
527+
#
528+
# * emit their second-parent as an UNREV, avoiding a
529+
# potentially costly history traversal
530+
#
531+
# * mark the merge commit as "notree" to ignore it
532+
find_other_splits () {
533+
assert test $# -ge 2
534+
dir="${1%/}"
535+
rev="$2"
536+
shift 2
537+
debug "Looking for other splits with dir != $dir..."
538+
539+
git log \
540+
--grep '^git-subtree-mainline:' \
541+
--no-patch \
542+
--no-show-signature \
543+
--format='hash: %H%nparents: %P%n%(trailers:key=git-subtree-dir,key=git-subtree-mainline,key=git-subtree-split)%nEND' \
544+
"$rev" ${@:+"$@"} |
545+
while read -r key val
546+
do
547+
case "$key" in
548+
hash:)
549+
commit_hash="${val}"
550+
commit_parents=
551+
subtree_dir=
552+
subtree_mainline=
553+
subtree_split=
554+
;;
555+
parents:)
556+
commit_parents="${val}" ;;
557+
git-subtree-dir:)
558+
subtree_dir="${val%/}/" ;;
559+
git-subtree-mainline:)
560+
subtree_mainline="${val}" ;;
561+
git-subtree-split:)
562+
subtree_split="${val}" ;;
563+
END)
564+
# verify:
565+
# * all git-subtree-* trailers are present
566+
# * this subtree is outside of $dir
567+
# * the first parent is the git-subtree-mainline:
568+
# * the commit has at least two parents
569+
if test -n "${subtree_dir}" &&
570+
test -n "${subtree_split}" &&
571+
test -n "${subtree_mainline}" &&
572+
test "${subtree_dir}" = "${subtree_dir#"${dir}/"}" &&
573+
test "${commit_parents}" != "${commit_parents#"$subtree_mainline "}" &&
574+
rev_exists "${commit_hash}^2"
575+
then
576+
debug "find_other_splits excluding dir=$subtree_dir merged in ${commit_hash}"
577+
echo "^${commit_hash}^2"
578+
set_notree "${commit_hash}"
579+
fi
580+
;;
581+
esac
582+
done
583+
}
584+
514585
# Usage: copy_commit REV TREE FLAGS_STR
515586
copy_commit () {
516587
assert test $# = 3
@@ -785,42 +856,6 @@ ensure_valid_ref_format () {
785856
die "fatal: '$1' does not look like a ref"
786857
}
787858

788-
# Usage: should_ignore_subtree_split_commit REV
789-
#
790-
# Check if REV is a commit from another subtree and should be
791-
# ignored from processing for splits
792-
should_ignore_subtree_split_commit () {
793-
assert test $# = 1
794-
795-
git show \
796-
--no-patch \
797-
--no-show-signature \
798-
--format='%(trailers:key=git-subtree-dir,key=git-subtree-mainline)' \
799-
"$1" |
800-
(
801-
have_mainline=
802-
subtree_dir=
803-
804-
while read -r trailer val
805-
do
806-
case "$trailer" in
807-
git-subtree-dir:)
808-
subtree_dir="${val%/}" ;;
809-
git-subtree-mainline:)
810-
have_mainline=y ;;
811-
esac
812-
done
813-
814-
if test -n "${subtree_dir}" &&
815-
test -z "${have_mainline}" &&
816-
test "${subtree_dir}" != "$arg_prefix"
817-
then
818-
return 0
819-
fi
820-
return 1
821-
)
822-
}
823-
824859
# Usage: process_split_commit REV PARENTS
825860
process_split_commit () {
826861
assert test $# = 2
@@ -994,31 +1029,39 @@ cmd_split () {
9941029
fi
9951030

9961031
unrevs="$(find_existing_splits "$dir" "$rev" "$repository")" || exit $?
1032+
(find_other_splits >"$cachedir/prune" "$dir" "$rev" $unrevs) || exit $?
9971033

9981034
# We can't restrict rev-list to only $dir here, because some of our
9991035
# parents have the $dir contents the root, and those won't match.
10001036
# (and rev-list --follow doesn't seem to solve this)
1001-
grl='git rev-list --topo-order --reverse --parents $rev $unrevs'
1002-
revmax=$(eval "$grl" | wc -l)
1037+
revmax="$(git rev-list \
1038+
<"$cachedir/prune" \
1039+
--topo-order \
1040+
--reverse \
1041+
--parents \
1042+
--stdin \
1043+
--count \
1044+
"$rev" \
1045+
$unrevs
1046+
)"
10031047
revcount=0
10041048
createcount=0
10051049
extracount=0
1006-
eval "$grl" |
1050+
git rev-list \
1051+
<"$cachedir/prune" \
1052+
--topo-order \
1053+
--reverse \
1054+
--parents \
1055+
--stdin \
1056+
"$rev" \
1057+
$unrevs |
10071058
while read rev parents
10081059
do
1009-
if should_ignore_subtree_split_commit "$rev"
1060+
if get_notree "$rev"
10101061
then
10111062
continue
10121063
fi
1013-
parsedparents=''
1014-
for parent in $parents
1015-
do
1016-
if ! should_ignore_subtree_split_commit "$parent"
1017-
then
1018-
parsedparents="$parsedparents$parent "
1019-
fi
1020-
done
1021-
process_split_commit "$rev" "$parsedparents"
1064+
process_split_commit "$rev" "$parents"
10221065
done || exit $?
10231066

10241067
latest_new=$(cache_get latest_new) || exit $?

contrib/subtree/t/t7900-subtree.sh

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -411,8 +411,9 @@ test_expect_success 'split sub dir/ with --rejoin' '
411411
git fetch ./"sub proj" HEAD &&
412412
git subtree merge --prefix="sub dir" FETCH_HEAD &&
413413
split_hash=$(git subtree split --prefix="sub dir" --annotate="*") &&
414-
git subtree split --prefix="sub dir" --annotate="*" --rejoin &&
415-
test "$(last_commit_subject)" = "Split '\''sub dir/'\'' into commit '\''$split_hash'\''"
414+
git subtree split --prefix="sub dir" --annotate="*" -b spl --rejoin &&
415+
test "$(last_commit_subject)" = "Split '\''sub dir/'\'' into commit '\''$split_hash'\''" &&
416+
test "$(git rev-list --count spl)" -eq 5
416417
)
417418
'
418419

@@ -442,18 +443,25 @@ test_expect_success 'split with multiple subtrees' '
442443
git -C "$test_count" subtree add --prefix=subADir FETCH_HEAD &&
443444
git -C "$test_count" fetch ./subB HEAD &&
444445
git -C "$test_count" subtree add --prefix=subBDir FETCH_HEAD &&
446+
test "$(git -C "$test_count" rev-list --count main)" -eq 7 &&
445447
test_create_commit "$test_count" subADir/main-subA1 &&
446448
test_create_commit "$test_count" subBDir/main-subB1 &&
447449
git -C "$test_count" subtree split --prefix=subADir \
448-
--squash --rejoin -m "Sub A Split 1" &&
450+
--squash --rejoin -m "Sub A Split 1" -b a1 &&
451+
test "$(git -C "$test_count" rev-list --count main..a1)" -eq 1 &&
449452
git -C "$test_count" subtree split --prefix=subBDir \
450-
--squash --rejoin -m "Sub B Split 1" &&
453+
--squash --rejoin -m "Sub B Split 1" -b b1 &&
454+
test "$(git -C "$test_count" rev-list --count main..b1)" -eq 1 &&
451455
test_create_commit "$test_count" subADir/main-subA2 &&
452456
test_create_commit "$test_count" subBDir/main-subB2 &&
453457
git -C "$test_count" subtree split --prefix=subADir \
454-
--squash --rejoin -m "Sub A Split 2" &&
458+
--squash --rejoin -m "Sub A Split 2" -b a2 &&
459+
test "$(git -C "$test_count" rev-list --count main..a2)" -eq 2 &&
460+
test "$(git -C "$test_count" rev-list --count a1..a2)" -eq 1 &&
455461
test "$(git -C "$test_count" subtree split --prefix=subBDir \
456-
--squash --rejoin -d -m "Sub B Split 1" 2>&1 | grep -w "\[1\]")" = ""
462+
--squash --rejoin -d -m "Sub B Split 1" -b b2 2>&1 | grep -w "\[1\]")" = "" &&
463+
test "$(git -C "$test_count" rev-list --count main..b2)" -eq 2 &&
464+
test "$(git -C "$test_count" rev-list --count b1..b2)" -eq 1
457465
'
458466

459467
# When subtree split-ing a directory that has other subtree
@@ -477,6 +485,7 @@ do
477485
test_path_is_file subA/file1.t &&
478486
test_path_is_file subA/subB/file2.t &&
479487
git subtree split --prefix=subA --branch=bsplit &&
488+
test "$(git rev-list --count bsplit)" -eq 2 &&
480489
git checkout bsplit &&
481490
test_path_is_file file1.t &&
482491
test_path_is_file subB/file2.t &&
@@ -489,6 +498,7 @@ do
489498
--prefix=subA/subB mksubtree &&
490499
test_path_is_file subA/subB/file3.t &&
491500
git subtree split --prefix=subA --branch=bsplit &&
501+
test "$(git rev-list --count bsplit)" -eq 3 &&
492502
git checkout bsplit &&
493503
test_path_is_file file1.t &&
494504
test_path_is_file subB/file2.t &&
@@ -497,6 +507,67 @@ do
497507
'
498508
done
499509

510+
# Usually,
511+
#
512+
# git subtree merge -P subA --squash f00...
513+
#
514+
# makes two commits, in this order:
515+
#
516+
# 1. Squashed 'subA/' content from commit f00...
517+
# 2. Merge commit (1) as 'subA'
518+
#
519+
# Commit 1 updates the subtree but does *not* rewrite paths.
520+
# Commit 2 rewrites all trees to start with `subA/`
521+
#
522+
# Commit 1 either has no parents or depends only on other
523+
# "Squashed 'subA/' content" commits.
524+
#
525+
# For merge without --squash, subtree produces just one commit:
526+
# a merge commit with git-subtree trailers.
527+
#
528+
# In either case, if the user rebases these commits, they will
529+
# still have the git-subtree-* trailers… but will NOT have
530+
# the layout described above.
531+
#
532+
# Test that subsequent `git subtree split` are not confused by this.
533+
test_expect_success 'split with rebased subtree commit' '
534+
subtree_test_create_repo "$test_count" &&
535+
(
536+
cd "$test_count" &&
537+
test_commit file0 &&
538+
test_create_subtree_add \
539+
. mksubtree subA file1 --squash &&
540+
test_path_is_file subA/file1.t &&
541+
mkdir subB &&
542+
test_commit subB/bfile &&
543+
git commit --amend -F - <<'EOF' &&
544+
Squashed '\''subB/'\'' content from commit '\''badf00da911bbe895347b4b236f5461d55dc9877'\''
545+
546+
Simulate a cherry-picked or rebased subtree commit.
547+
548+
git-subtree-dir: subB
549+
git-subtree-split: badf00da911bbe895347b4b236f5461d55dc9877
550+
EOF
551+
test_commit subA/file2 &&
552+
test_commit subB/bfile2 &&
553+
git commit --amend -F - <<'EOF' &&
554+
Split '\''subB/'\'' into commit '\''badf00da911bbe895347b4b236f5461d55dc9877'\''
555+
556+
Simulate a cherry-picked or rebased subtree commit.
557+
558+
git-subtree-dir: subB
559+
git-subtree-mainline: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
560+
git-subtree-split: badf00da911bbe895347b4b236f5461d55dc9877
561+
EOF
562+
git subtree split --prefix=subA --branch=bsplit &&
563+
git checkout bsplit &&
564+
test_path_is_file file1.t &&
565+
test_path_is_file file2.t &&
566+
test "$(last_commit_subject)" = "subA/file2" &&
567+
test "$(git rev-list --count bsplit)" -eq 2
568+
)
569+
'
570+
500571
test_expect_success 'split sub dir/ with --rejoin from scratch' '
501572
subtree_test_create_repo "$test_count" &&
502573
test_create_commit "$test_count" main1 &&

0 commit comments

Comments
 (0)