Skip to content

Commit 5234a3b

Browse files
committed
t3454: cover merge-replay scenarios with the historian helper
Add a dedicated test script for `git history reword` (and `git replay` via the same code path) across 2-parent merges, using the `test-tool historian` fixture builder so each scenario reads as a small declarative recipe rather than a sequence of plumbing commands. The script exercises the cases that motivated the merge-replay work: * a clean merge where each side touches unrelated files; * a non-trivial merge where the same line was changed on both sides and the user resolved by hand (textual manual resolution must be preserved through the replay); * a non-trivial merge where the user also touched a line outside any conflict region (a "semantic" edit must also be preserved through the replay); * an octopus merge in the rewrite path, which is rejected; * a function rename across the merge with a brand-new caller introduced by the rewritten parents. The pre-existing caller that the user manually renamed in the original merge must keep its rename, and the brand-new caller must _not_ be rewritten (calvin/hobbes naming chosen for legibility). This second part is the documented limitation: the replay propagates the textual diffs the user actually made, it does not extrapolate symbol-level intent. Symbol-aware refactoring is out of scope, just as it is for plain rebase. The fixture builder lets each scenario sit in roughly a dozen lines of historian directives plus the assertions, which keeps the test file readable when more scenarios are added later. Assisted-by: Claude Opus 4.7 Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
1 parent 714f976 commit 5234a3b

1 file changed

Lines changed: 237 additions & 0 deletions

File tree

t/t3454-history-merges.sh

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
#!/bin/sh
2+
3+
test_description='git history reword across merge commits
4+
5+
Exercises the merge-replay path in `git history reword` using the
6+
`test-tool historian` test fixture builder so each scenario is
7+
described in a small declarative input rather than a sprawling
8+
sequence of plumbing commands. The interesting cases are:
9+
10+
* a clean merge with each side touching unrelated files;
11+
* a non-trivial merge whose conflicting line was resolved by hand
12+
(textually) and whose resolution must be preserved through the
13+
replay;
14+
* a non-trivial merge with a manual *semantic* edit (an additional
15+
change outside the conflict region) that must also be preserved.
16+
'
17+
18+
GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
19+
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
20+
21+
. ./test-lib.sh
22+
23+
# Replace the commit's message via a fake editor and run reword.
24+
reword_to () {
25+
new_msg="$1"
26+
target="$2"
27+
write_script fake-editor.sh <<-EOF &&
28+
echo "$new_msg" >"\$1"
29+
EOF
30+
test_set_editor "$(pwd)/fake-editor.sh" &&
31+
git history reword "$target" &&
32+
rm fake-editor.sh
33+
}
34+
35+
build_clean_merge () {
36+
test-tool historian <<-\EOF
37+
# Setup:
38+
# A (a) --- C (a, h) ----+--- M (a, g, h)
39+
# \ /
40+
# +-- B (a, g) ------+
41+
#
42+
# Topic touches `g` only; main touches `h` only. The auto-merge
43+
# at M is clean.
44+
blob a "shared content"
45+
blob g guarded
46+
blob h host
47+
commit A main "A" a=a
48+
commit B topic "B (introduces g)" from=A a=a g=g
49+
commit C main "C (introduces h)" a=a h=h
50+
commit M main "Merge topic" merge=B a=a g=g h=h
51+
EOF
52+
}
53+
54+
test_expect_success 'clean merge: both sides touch unrelated files' '
55+
test_when_finished "rm -rf repo" &&
56+
git init repo &&
57+
(
58+
cd repo &&
59+
build_clean_merge &&
60+
61+
reword_to "AA" A &&
62+
63+
# The merge is still a 2-parent merge with the same subject
64+
# and tree (clean replay leaves content unchanged).
65+
test_cmp_rev HEAD^{tree} M^{tree} &&
66+
67+
echo "Merge topic" >expect-subject &&
68+
git log -1 --format=%s HEAD >subject &&
69+
test_cmp expect-subject subject &&
70+
71+
git rev-list --merges HEAD~..HEAD >merges &&
72+
test_line_count = 1 merges
73+
)
74+
'
75+
76+
build_textual_resolution () {
77+
test-tool historian <<-\EOF
78+
# Both sides change the same line of `a`; the user resolved with
79+
# their own combined text, recorded directly as the merge tree.
80+
blob a_v1 line1 line2 line3
81+
blob a_main line1 line2-main line3
82+
blob a_topic line1 line2-topic line3
83+
blob a_resolution line1 line2-merged-by-hand line3
84+
commit A main "A" a=a_v1
85+
commit B topic "B (line2 on topic)" from=A a=a_topic
86+
commit C main "C (line2 on main)" a=a_main
87+
commit M main "Merge topic" merge=B a=a_resolution
88+
EOF
89+
}
90+
91+
test_expect_success 'non-trivial merge: textual manual resolution is preserved' '
92+
test_when_finished "rm -rf repo" &&
93+
git init repo &&
94+
(
95+
cd repo &&
96+
build_textual_resolution &&
97+
98+
reword_to "AA" A &&
99+
100+
git show HEAD:a >after &&
101+
test_write_lines line1 line2-merged-by-hand line3 >expect &&
102+
test_cmp expect after
103+
)
104+
'
105+
106+
build_semantic_edit () {
107+
test-tool historian <<-\EOF
108+
# Topic and main conflict on line2 of `a`. The user's resolution
109+
# at M not only picks combined text on line2 but ALSO touches
110+
# line5 (a "semantic" edit outside any conflict region) -- this
111+
# kind of edit is invisible to a naive pick-one-side strategy and
112+
# must be preserved by replay.
113+
blob a_v1 line1 line2 line3 line4 line5
114+
blob a_main line1 line2-main line3 line4 line5
115+
blob a_topic line1 line2-topic line3 line4 line5
116+
blob a_resolution line1 line2-merged line3 line4 line5-touched
117+
commit A main "A" a=a_v1
118+
commit B topic "B (line2 on topic)" from=A a=a_topic
119+
commit C main "C (line2 on main)" a=a_main
120+
commit M main "Merge topic" merge=B a=a_resolution
121+
EOF
122+
}
123+
124+
test_expect_success 'non-trivial merge: semantic edit outside conflict region is preserved' '
125+
test_when_finished "rm -rf repo" &&
126+
git init repo &&
127+
(
128+
cd repo &&
129+
build_semantic_edit &&
130+
131+
reword_to "AA" A &&
132+
133+
git show HEAD:a >after &&
134+
test_write_lines line1 line2-merged line3 line4 line5-touched \
135+
>expect &&
136+
test_cmp expect after
137+
)
138+
'
139+
140+
build_octopus () {
141+
test-tool historian <<-\EOF
142+
blob a "x"
143+
commit A main "A" a=a
144+
commit B b1 "B" from=A a=a
145+
commit C b2 "C" from=A a=a
146+
commit D b3 "D" from=A a=a
147+
commit O main "octopus" merge=B merge=C merge=D a=a
148+
EOF
149+
}
150+
151+
test_expect_success 'octopus merge in the rewrite path is rejected' '
152+
test_when_finished "rm -rf repo" &&
153+
git init repo &&
154+
(
155+
cd repo &&
156+
build_octopus &&
157+
158+
test_must_fail git -c core.editor=true history reword \
159+
--dry-run A 2>err &&
160+
test_grep "octopus" err
161+
)
162+
'
163+
164+
build_function_rename () {
165+
test-tool historian <<-\EOF
166+
# Topic renames harry() -> hermione() (defs.h plus caller1). main
167+
# adds caller2 calling harry(); the original merge M manually
168+
# renames caller2 to hermione(). The "newer" base on a side branch
169+
# contains caller2 AND a brand-new caller3 calling harry();
170+
# replaying onto `newer` therefore introduces caller3 into the
171+
# merged tree.
172+
blob defs_harry "void harry(void);"
173+
blob defs_hermione "void hermione(void);"
174+
blob harry_call "harry();"
175+
blob hermione_call "hermione();"
176+
commit A main "A" defs.h=defs_harry caller1=harry_call
177+
commit B topic "B (rename)" from=A defs.h=defs_hermione caller1=hermione_call
178+
commit C main "C (caller2 calls harry)" defs.h=defs_harry caller1=harry_call caller2=harry_call
179+
commit M main "Merge topic" merge=B defs.h=defs_hermione caller1=hermione_call caller2=hermione_call
180+
commit NEW newer "newer base with caller3" from=A defs.h=defs_harry caller1=harry_call caller2=harry_call caller3=harry_call
181+
EOF
182+
}
183+
184+
# This case checks two things at once. First, the manual semantic
185+
# edit in M (renaming caller2) must be preserved when we replay onto
186+
# a different base; that is the case `git history` and `git replay`
187+
# need to handle correctly, even though nothing in the conflict
188+
# markers tells us about it. Second, a file that only enters the
189+
# tree via the rewritten parents (caller3, present on the `newer`
190+
# base) is _not_ renamed by the replay. The replay propagates the
191+
# textual diffs the user actually made in M; it does _not_ infer
192+
# the user's symbol-level intent ("rename every caller of harry").
193+
# This is a known and intentional limitation. Symbol-aware
194+
# refactoring is out of scope here, just as it is for plain rebase.
195+
test_expect_success 'preserves manual rename of pre-existing caller; does not extrapolate to new files' '
196+
test_when_finished "rm -rf repo" &&
197+
git init repo &&
198+
(
199+
cd repo &&
200+
build_function_rename &&
201+
202+
# Replay (C, B, M) onto the newer base. A `main..M` style
203+
# range across two unrelated branches is awkward; spin up a
204+
# temp branch as the spine and use --advance.
205+
git branch tmp main &&
206+
git replay --ref-action=print --onto NEW A..tmp >result &&
207+
new_tip=$(cut -f 3 -d " " result) &&
208+
209+
# defs.h and caller1 came from B (clean cherry-pick of the
210+
# rename commit) and must reflect the rename.
211+
echo "void hermione(void);" >expect &&
212+
git show $new_tip:defs.h >actual &&
213+
test_cmp expect actual &&
214+
215+
echo "hermione();" >expect &&
216+
git show $new_tip:caller1 >actual &&
217+
test_cmp expect actual &&
218+
219+
# caller2 existed in the original M; its manual rename to
220+
# hermione() is the semantic edit the replay must preserve.
221+
echo "hermione();" >expect &&
222+
git show $new_tip:caller2 >actual &&
223+
test_cmp expect actual &&
224+
225+
# caller3 only exists on the newer base, so it was brought
226+
# in by N (the auto-merge of the rewritten parents). The
227+
# replay has no way to know the user intended to rename
228+
# every caller; caller3 keeps harry(). The resulting tree
229+
# is therefore _not_ symbol-correct and needs a follow-up
230+
# edit. This is the documented limitation.
231+
echo "harry();" >expect &&
232+
git show $new_tip:caller3 >actual &&
233+
test_cmp expect actual
234+
)
235+
'
236+
237+
test_done

0 commit comments

Comments
 (0)