contrib/subtree: fix split with squashed subtrees

98ba49ccc2 (subtree: fix split processing with multiple subtrees present, 2023-12-01) increases the performance of git subtree split --prefix=subA by ignoring subtree merges which are outside of `subA/`. It also introduces a regression. Subtree merges that should be retained are incorrectly ignored if they: 1. are nested under `subA/`; and 2. are merged with `--squash`. For example, a subtree merged like: git subtree merge --squash --prefix=subA/subB "$rev" # ^^^^^^^^ ^^^^ is erroneously ignored during a split of `subA`. This causes missing tree files and different commit hashes starting in git v2.44.0-rc0. The method: should_ignore_subtree_split_commit REV should test only a single commit REV, but the combination of git log -1 --grep=... actually searches all *parent* commits until a `--grep` match is discovered. Rewrite this method to test only one REV at a time. Extract commit information with a single `git` call as opposed to three. The `test` conditions for rejecting a commit remain unchanged. Unit tests now cover nested subtrees. Signed-off-by: Colin Stagner <ask+git@howdoi.land> Acked-by: Phillip Wood <phillip.wood@dunelm.org.uk> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Colin Stagner committed Sep 9, 2025 at 22:11 UTC 83f9dad7d6fb5988b68f80b25bd87c68693195dd
2 files changed +99 -8
contrib/subtree/git-subtree.sh
+28 -8
@@ -785,20 +785,40 @@ ensure_valid_ref_format () {
785 die "fatal: '$1' does not look like a ref"
786 }
787
788 -# Usage: check if a commit from another subtree should be
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
792 - local rev="$1"
793 - if test -n "$(git log -1 --grep="git-subtree-dir:" $rev)"
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
795 - if test -z "$(git log -1 --grep="git-subtree-mainline:" $rev)" &&
796 - test -z "$(git log -1 --grep="git-subtree-dir: $arg_prefix$" $rev)"
797 - then
798 - return 0
799 - fi
818 + return 0
819 fi
820 return 1
821 + )
822 }
823
824 # Usage: process_split_commit REV PARENTS
contrib/subtree/t/t7900-subtree.sh
+71
@@ -9,6 +9,9 @@ This test verifies the basic operation of the add, merge, split, pull,
9 and push subcommands of git subtree.
10 '
11
12 +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
13 +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
14 +
15 TEST_DIRECTORY=$(pwd)/../../../t
16 . "$TEST_DIRECTORY"/test-lib.sh
17 . "$TEST_DIRECTORY"/lib-gpg.sh
@@ -68,6 +71,33 @@ test_create_pre2_32_repo () {
71 git -C "$1-clone" replace HEAD^2 $new_commit
72 }
73
74 +# test_create_subtree_add REPO ORPHAN PREFIX FILENAME ...
75 +#
76 +# Create a simple subtree on a new branch named ORPHAN in REPO.
77 +# The subtree is then merged into the current branch of REPO,
78 +# under PREFIX. The generated subtree has has one commit
79 +# with subject and tag FILENAME with a single file "FILENAME.t"
80 +#
81 +# When this method returns:
82 +# - the current branch of REPO will have file PREFIX/FILENAME.t
83 +# - REPO will have a branch named ORPHAN with subtree history
84 +#
85 +# additional arguments are forwarded to "subtree add"
86 +test_create_subtree_add () {
87 + (
88 + cd "$1" &&
89 + orphan="$2" &&
90 + prefix="$3" &&
91 + filename="$4" &&
92 + shift 4 &&
93 + last="$(git branch --show-current)" &&
94 + git switch --orphan "$orphan" &&
95 + test_commit "$filename" &&
96 + git checkout "$last" &&
97 + git subtree add --prefix="$prefix" "$@" "$orphan"
98 + )
99 +}
100 +
101 test_expect_success 'shows short help text for -h' '
102 test_expect_code 129 git subtree -h >out 2>err &&
103 test_must_be_empty err &&
@@ -426,6 +456,47 @@ test_expect_success 'split with multiple subtrees' '
456 --squash --rejoin -d -m "Sub B Split 1" 2>&1 | grep -w "\[1\]")" = ""
457 '
458
459 +# When subtree split-ing a directory that has other subtree
460 +# *merges* underneath it, the split must include those subtrees.
461 +# This test creates a nested subtree, `subA/subB`, and tests
462 +# that the tree is correct after a subtree split of `subA/`.
463 +# The test covers:
464 +# - An initial `subtree add`; and
465 +# - A follow-up `subtree merge`
466 +# both with and without `--squashed`.
467 +for is_squashed in '' 'y'
468 +do
469 + test_expect_success "split keeps nested ${is_squashed:+--squash }subtrees that are part of the split" '
470 + subtree_test_create_repo "$test_count" &&
471 + (
472 + cd "$test_count" &&
473 + mkdir subA &&
474 + test_commit subA/file1 &&
475 + test_create_subtree_add \
476 + . mksubtree subA/subB file2 ${is_squashed:+--squash} &&
477 + test_path_is_file subA/file1.t &&
478 + test_path_is_file subA/subB/file2.t &&
479 + git subtree split --prefix=subA --branch=bsplit &&
480 + git checkout bsplit &&
481 + test_path_is_file file1.t &&
482 + test_path_is_file subB/file2.t &&
483 + git checkout mksubtree &&
484 + git branch -D bsplit &&
485 + test_commit file3 &&
486 + git checkout main &&
487 + git subtree merge \
488 + ${is_squashed:+--squash} \
489 + --prefix=subA/subB mksubtree &&
490 + test_path_is_file subA/subB/file3.t &&
491 + git subtree split --prefix=subA --branch=bsplit &&
492 + git checkout bsplit &&
493 + test_path_is_file file1.t &&
494 + test_path_is_file subB/file2.t &&
495 + test_path_is_file subB/file3.t
496 + )
497 + '
498 +done
499 +
500 test_expect_success 'split sub dir/ with --rejoin from scratch' '
501 subtree_test_create_repo "$test_count" &&
502 test_create_commit "$test_count" main1 &&