Raw
1 #!/bin/sh
2
3 test_description='basic git replay tests'
4
5 GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
6 export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
7
8 . ./test-lib.sh
9
10 GIT_AUTHOR_NAME=author@name
11 GIT_AUTHOR_EMAIL=bogus@email@address
12 export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL
13
14 test_expect_success 'setup' '
15 test_commit A &&
16 test_commit B &&
17
18 git switch -c topic1 &&
19 test_commit C &&
20 git switch -c topic2 &&
21 test_commit D &&
22 test_commit E &&
23 git switch topic1 &&
24 test_commit F &&
25 git switch -c topic3 &&
26 test_commit G &&
27 test_commit H &&
28 git switch -c empty &&
29 git commit --allow-empty -m empty &&
30 git switch -c topic4 main &&
31 test_commit I &&
32 test_commit J &&
33
34 git switch -c next main &&
35 test_commit K &&
36 git merge -m "Merge topic1" topic1 &&
37 git merge -m "Merge topic2" topic2 &&
38 git merge -m "Merge topic3" topic3 &&
39 >evil &&
40 git add evil &&
41 git commit --amend &&
42 git merge -m "Merge topic4" topic4 &&
43
44 git switch main &&
45 test_commit L &&
46 test_commit M &&
47
48 git switch --detach topic4 &&
49 test_commit N &&
50 test_commit O &&
51 git switch -c topic-with-merge topic4 &&
52 test_merge P O --no-ff &&
53 git switch main &&
54
55 git switch -c conflict B &&
56 test_commit C.conflict C.t conflict
57 '
58
59 test_expect_success 'setup bare' '
60 git clone --bare . bare
61 '
62
63 test_expect_success 'argument to --advance must be a reference' '
64 echo "fatal: argument to --advance must be a reference" >expect &&
65 oid=$(git rev-parse main) &&
66 test_must_fail git replay --advance=$oid topic1..topic2 2>actual &&
67 test_cmp expect actual
68 '
69
70 test_expect_success '--onto with invalid commit-ish' '
71 printf "fatal: ${SQ}refs/not-valid${SQ} is not " >expect &&
72 printf "a valid commit-ish for --onto\n" >>expect &&
73 test_must_fail git replay --onto=refs/not-valid topic1..topic2 2>actual &&
74 test_cmp expect actual
75 '
76
77 test_expect_success 'exactly one of --onto, --advance, or --revert is required' '
78 echo "error: exactly one of --onto, --advance, or --revert is required" >expect &&
79 test_might_fail git replay -h >>expect &&
80 test_must_fail git replay topic1..topic2 2>actual &&
81 test_cmp expect actual
82 '
83
84 test_expect_success 'replay down to root onto another branch' '
85 git replay --ref-action=print --onto main topic2 >result &&
86
87 test_line_count = 1 result &&
88
89 git log --format=%s $(cut -f 3 -d " " result) >actual &&
90 test_write_lines E D C M L B A >expect &&
91 test_cmp expect actual
92 '
93
94 test_expect_success '--advance and --contained cannot be used together' '
95 test_must_fail git replay --advance=main --contained \
96 topic1..topic2 2>actual &&
97 test_grep "cannot be used together" actual
98 '
99
100 test_expect_success 'cannot advance target ... ordering would be ill-defined' '
101 echo "fatal: ${SQ}--advance${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined" >expect &&
102 test_must_fail git replay --advance=main main topic1 topic2 2>actual &&
103 test_cmp expect actual
104 '
105
106 test_expect_success 'replaying merge commits is not supported yet' '
107 echo "fatal: replaying merge commits is not supported yet!" >expect &&
108 test_must_fail git replay --advance=main main..topic-with-merge 2>actual &&
109 test_cmp expect actual
110 '
111
112 test_expect_success 'using replay to rebase two branches, one on top of other' '
113 git replay --ref-action=print --onto main topic1..topic2 >result &&
114
115 test_line_count = 1 result &&
116
117 git log --format=%s $(cut -f 3 -d " " result) >actual &&
118 test_write_lines E D M L B A >expect &&
119 test_cmp expect actual &&
120
121 printf "update refs/heads/topic2 " >expect &&
122 printf "%s " $(cut -f 3 -d " " result) >>expect &&
123 git rev-parse topic2 >>expect &&
124
125 test_cmp expect result
126 '
127
128 test_expect_success 'using replay on bare repo to rebase two branches, one on top of other' '
129 git -C bare replay --ref-action=print --onto main topic1..topic2 >result-bare &&
130 test_cmp expect result-bare
131 '
132
133 test_expect_success 'using replay to rebase with a conflict' '
134 test_expect_code 1 git replay --onto topic1 B..conflict
135 '
136
137 test_expect_success 'using replay on bare repo to rebase with a conflict' '
138 test_expect_code 1 git -C bare replay --onto topic1 B..conflict
139 '
140
141 test_expect_success 'using replay to perform basic cherry-pick' '
142 # The differences between this test and previous ones are:
143 # --advance vs --onto
144 # 2nd field of result is refs/heads/main vs. refs/heads/topic2
145 # 4th field of result is hash for main instead of hash for topic2
146
147 git replay --ref-action=print --advance main topic1..topic2 >result &&
148
149 test_line_count = 1 result &&
150
151 git log --format=%s $(cut -f 3 -d " " result) >actual &&
152 test_write_lines E D M L B A >expect &&
153 test_cmp expect actual &&
154
155 printf "update refs/heads/main " >expect &&
156 printf "%s " $(cut -f 3 -d " " result) >>expect &&
157 git rev-parse main >>expect &&
158
159 test_cmp expect result
160 '
161
162 test_expect_success 'using replay on bare repo to perform basic cherry-pick' '
163 git -C bare replay --ref-action=print --advance main topic1..topic2 >result-bare &&
164 test_cmp expect result-bare
165 '
166
167 test_expect_success 'commits that become empty are dropped' '
168 # Save original branches
169 git for-each-ref --format="update %(refname) %(objectname)" \
170 refs/heads/ >original-branches &&
171 test_when_finished "git update-ref --stdin <original-branches &&
172 rm original-branches" &&
173 # Cherry-pick tip of topic1 ("F"), from the middle of A..empty, to main
174 git replay --advance main topic1^! &&
175
176 # Replay all of A..empty onto main (which includes topic1 & thus F
177 # in the middle)
178 git replay --onto main --branches --ancestry-path=empty ^A \
179 >result &&
180 git log --format="%s%d" L..empty >actual &&
181 test_write_lines >expect \
182 "empty (empty)" "H (topic3)" G "C (topic1)" "F (main)" "M (tag: M)" &&
183 test_cmp expect actual
184 '
185
186 test_expect_success 'replay on bare repo fails with both --advance and --onto' '
187 test_must_fail git -C bare replay --advance main --onto main topic1..topic2 >result-bare
188 '
189
190 test_expect_success 'replay fails when both --advance and --onto are omitted' '
191 test_must_fail git replay topic1..topic2 >result
192 '
193
194 test_expect_success 'using replay to also rebase a contained branch' '
195 git replay --ref-action=print --contained --onto main main..topic3 >result &&
196
197 test_line_count = 2 result &&
198 cut -f 3 -d " " result >new-branch-tips &&
199
200 git log --format=%s $(head -n 1 new-branch-tips) >actual &&
201 test_write_lines F C M L B A >expect &&
202 test_cmp expect actual &&
203
204 git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
205 test_write_lines H G F C M L B A >expect &&
206 test_cmp expect actual &&
207
208 printf "update refs/heads/topic1 " >expect &&
209 printf "%s " $(head -n 1 new-branch-tips) >>expect &&
210 git rev-parse topic1 >>expect &&
211 printf "update refs/heads/topic3 " >>expect &&
212 printf "%s " $(tail -n 1 new-branch-tips) >>expect &&
213 git rev-parse topic3 >>expect &&
214
215 test_cmp expect result
216 '
217
218 test_expect_success 'using replay on bare repo to also rebase a contained branch' '
219 git -C bare replay --ref-action=print --contained --onto main main..topic3 >result-bare &&
220 test_cmp expect result-bare
221 '
222
223 test_expect_success 'using replay to rebase multiple divergent branches' '
224 git replay --ref-action=print --onto main ^topic1 topic2 topic4 >result &&
225
226 test_line_count = 2 result &&
227 cut -f 3 -d " " result >new-branch-tips &&
228
229 git log --format=%s $(head -n 1 new-branch-tips) >actual &&
230 test_write_lines E D M L B A >expect &&
231 test_cmp expect actual &&
232
233 git log --format=%s $(tail -n 1 new-branch-tips) >actual &&
234 test_write_lines J I M L B A >expect &&
235 test_cmp expect actual &&
236
237 printf "update refs/heads/topic2 " >expect &&
238 printf "%s " $(head -n 1 new-branch-tips) >>expect &&
239 git rev-parse topic2 >>expect &&
240 printf "update refs/heads/topic4 " >>expect &&
241 printf "%s " $(tail -n 1 new-branch-tips) >>expect &&
242 git rev-parse topic4 >>expect &&
243
244 test_cmp expect result
245 '
246
247 test_expect_success 'using replay on bare repo to rebase multiple divergent branches, including contained ones' '
248 git -C bare replay --ref-action=print --contained --onto main ^main topic2 topic3 topic4 >result &&
249
250 test_line_count = 4 result &&
251 cut -f 3 -d " " result >new-branch-tips &&
252
253 >expect &&
254 for i in 2 1 3 4
255 do
256 printf "update refs/heads/topic$i " >>expect &&
257 printf "%s " $(grep topic$i result | cut -f 3 -d " ") >>expect &&
258 git -C bare rev-parse topic$i >>expect || return 1
259 done &&
260
261 test_cmp expect result &&
262
263 test_write_lines F C M L B A >expect1 &&
264 test_write_lines E D C M L B A >expect2 &&
265 test_write_lines H G F C M L B A >expect3 &&
266 test_write_lines J I M L B A >expect4 &&
267
268 for i in 1 2 3 4
269 do
270 git -C bare log --format=%s $(grep topic$i result | cut -f 3 -d " ") >actual &&
271 test_cmp expect$i actual || return 1
272 done
273 '
274
275 test_expect_success 'using replay to update detached HEAD' '
276 current_head=$(git branch --show-current) &&
277 test_when_finished git switch "$current_head" &&
278 git switch --detach &&
279 test_commit something &&
280 git replay --ref-action=print --onto HEAD~2 --ref-action=print HEAD~..HEAD >updates &&
281 test_grep "update HEAD " updates
282 '
283
284 test_expect_success 'merge.directoryRenames=false' '
285 # create a test case that stress-tests the rename caching
286 git switch -c rename-onto &&
287
288 mkdir -p to-rename &&
289 test_commit to-rename/move &&
290
291 mkdir -p renamed-directory &&
292 git mv to-rename/move* renamed-directory/ &&
293 test_tick &&
294 git commit -m renamed-directory &&
295
296 git switch -c rename-from HEAD^ &&
297 test_commit to-rename/add-a-file &&
298 echo modified >to-rename/add-a-file.t &&
299 test_tick &&
300 git commit -m modified to-rename/add-a-file.t &&
301
302 git -c merge.directoryRenames=false replay \
303 --onto rename-onto rename-onto..rename-from
304 '
305
306 test_expect_success 'default atomic behavior updates refs directly' '
307 # Use a separate branch to avoid contaminating topic2 for later tests
308 git branch test-atomic topic2 &&
309 test_when_finished "git branch -D test-atomic" &&
310
311 # Test default atomic behavior (no output, refs updated)
312 git replay --onto main topic1..test-atomic >output &&
313 test_must_be_empty output &&
314
315 # Verify ref was updated
316 git log --format=%s test-atomic >actual &&
317 test_write_lines E D M L B A >expect &&
318 test_cmp expect actual &&
319
320 # Verify reflog message includes SHA of onto commit
321 git reflog test-atomic -1 --format=%gs >reflog-msg &&
322 ONTO_SHA=$(git rev-parse main) &&
323 echo "replay --onto $ONTO_SHA" >expect-reflog &&
324 test_cmp expect-reflog reflog-msg
325 '
326
327 test_expect_success 'atomic behavior in bare repository' '
328 # Store original state for cleanup
329 START=$(git -C bare rev-parse topic2) &&
330 test_when_finished "git -C bare update-ref refs/heads/topic2 $START" &&
331
332 # Test atomic updates work in bare repo
333 git -C bare replay --onto main topic1..topic2 >output &&
334 test_must_be_empty output &&
335
336 # Verify ref was updated in bare repo
337 git -C bare log --format=%s topic2 >actual &&
338 test_write_lines E D M L B A >expect &&
339 test_cmp expect actual
340 '
341
342 test_expect_success 'reflog message for --advance mode' '
343 # Store original state
344 START=$(git rev-parse main) &&
345 test_when_finished "git update-ref refs/heads/main $START" &&
346
347 # Test --advance mode reflog message
348 git replay --advance main topic1..topic2 >output &&
349 test_must_be_empty output &&
350
351 # Verify reflog message includes --advance and branch name
352 git reflog main -1 --format=%gs >reflog-msg &&
353 echo "replay --advance main" >expect-reflog &&
354 test_cmp expect-reflog reflog-msg
355 '
356
357 test_expect_success 'replay.refAction=print config option' '
358 # Store original state
359 START=$(git rev-parse topic2) &&
360 test_when_finished "git branch -f topic2 $START" &&
361
362 # Test with config set to print
363 test_config replay.refAction print &&
364 git replay --onto main topic1..topic2 >output &&
365 test_line_count = 1 output &&
366 test_grep "^update refs/heads/topic2 " output
367 '
368
369 test_expect_success 'replay.refAction=update config option' '
370 # Store original state
371 START=$(git rev-parse topic2) &&
372 test_when_finished "git branch -f topic2 $START" &&
373
374 # Test with config set to update
375 test_config replay.refAction update &&
376 git replay --onto main topic1..topic2 >output &&
377 test_must_be_empty output &&
378
379 # Verify ref was updated
380 git log --format=%s topic2 >actual &&
381 test_write_lines E D M L B A >expect &&
382 test_cmp expect actual
383 '
384
385 test_expect_success 'command-line --ref-action overrides config' '
386 # Store original state
387 START=$(git rev-parse topic2) &&
388 test_when_finished "git branch -f topic2 $START" &&
389
390 # Set config to update but use --ref-action=print
391 test_config replay.refAction update &&
392 git replay --ref-action=print --onto main topic1..topic2 >output &&
393 test_line_count = 1 output &&
394 test_grep "^update refs/heads/topic2 " output
395 '
396
397 test_expect_success 'invalid replay.refAction value' '
398 test_config replay.refAction invalid &&
399 test_must_fail git replay --onto main topic1..topic2 2>error &&
400 test_grep "invalid.*replay.refAction.*value" error
401 '
402
403 test_expect_success 'argument to --revert must be a reference' '
404 echo "fatal: argument to --revert must be a reference" >expect &&
405 oid=$(git rev-parse main) &&
406 test_must_fail git replay --revert=$oid topic1..topic2 2>actual &&
407 test_cmp expect actual
408 '
409
410 test_expect_success 'cannot revert with multiple sources' '
411 echo "fatal: ${SQ}--revert${SQ} cannot be used with multiple revision ranges because the ordering would be ill-defined" >expect &&
412 test_must_fail git replay --revert main main topic1 topic2 2>actual &&
413 test_cmp expect actual
414 '
415
416 test_expect_success 'using replay --revert to revert commits' '
417 # Reuse existing topic4 branch (has commits I and J on top of main)
418 START=$(git rev-parse topic4) &&
419 test_when_finished "git branch -f topic4 $START" &&
420
421 # Revert commits I and J
422 git replay --revert topic4 topic4~2..topic4 &&
423
424 # Verify the revert commits were created (newest-first ordering
425 # means J is reverted first, then I on top)
426 git log --format=%s -4 topic4 >actual &&
427 cat >expect <<-\EOF &&
428 Revert "I"
429 Revert "J"
430 J
431 I
432 EOF
433 test_cmp expect actual &&
434
435 # Verify commit message format includes hash (tip is Revert "I")
436 test_commit_message topic4 <<-EOF &&
437 Revert "I"
438
439 This reverts commit $(git rev-parse I).
440 EOF
441
442 # Verify reflog message
443 git reflog topic4 -1 --format=%gs >reflog-msg &&
444 echo "replay --revert topic4" >expect-reflog &&
445 test_cmp expect-reflog reflog-msg
446 '
447
448 test_expect_success 'using replay --revert in bare repo' '
449 # Reuse existing topic4 in bare repo
450 START=$(git -C bare rev-parse topic4) &&
451 test_when_finished "git -C bare update-ref refs/heads/topic4 $START" &&
452
453 # Revert commit J in bare repo
454 git -C bare replay --revert topic4 topic4~1..topic4 &&
455
456 # Verify revert was created
457 git -C bare log -1 --format=%s topic4 >actual &&
458 echo "Revert \"J\"" >expect &&
459 test_cmp expect actual
460 '
461
462 test_expect_success 'revert of revert uses Reapply' '
463 # Use topic4 and first revert J, then revert the revert
464 START=$(git rev-parse topic4) &&
465 test_when_finished "git branch -f topic4 $START" &&
466
467 # First revert J
468 git replay --revert topic4 topic4~1..topic4 &&
469 REVERT_J=$(git rev-parse topic4) &&
470
471 # Now revert the revert - should become Reapply
472 git replay --revert topic4 topic4~1..topic4 &&
473
474 # Verify Reapply prefix and message format
475 test_commit_message topic4 <<-EOF
476 Reapply "J"
477
478 This reverts commit $REVERT_J.
479 EOF
480 '
481
482 test_expect_success 'git replay --revert with conflict' '
483 # conflict branch has C.conflict which conflicts with topic1s C
484 test_expect_code 1 git replay --revert conflict B..topic1
485 '
486
487 test_expect_success 'git replay --revert incompatible with --contained' '
488 test_must_fail git replay --revert topic4 --contained topic4~1..topic4 2>error &&
489 test_grep "cannot be used together" error
490 '
491
492 test_expect_success 'git replay --revert incompatible with --onto' '
493 test_must_fail git replay --revert topic4 --onto main topic4~1..topic4 2>error &&
494 test_grep "cannot be used together" error
495 '
496
497 test_expect_success 'git replay --revert incompatible with --advance' '
498 test_must_fail git replay --revert topic4 --advance main topic4~1..topic4 2>error &&
499 test_grep "cannot be used together" error
500 '
501
502 test_expect_success 'using --onto with --ref' '
503 git branch test-ref-onto topic2 &&
504 test_when_finished "git branch -D test-ref-onto" &&
505
506 git replay --ref-action=print --onto=main --ref=refs/heads/test-ref-onto topic1..topic2 >result &&
507
508 test_line_count = 1 result &&
509 test_grep "^update refs/heads/test-ref-onto " result &&
510
511 git log --format=%s $(cut -f 3 -d " " result) >actual &&
512 test_write_lines E D M L B A >expect &&
513 test_cmp expect actual
514 '
515
516 test_expect_success 'using --advance with --ref' '
517 git branch test-ref-advance main &&
518 git branch test-ref-target main &&
519 test_when_finished "git branch -D test-ref-advance test-ref-target" &&
520
521 git replay --ref-action=print --advance=test-ref-advance --ref=refs/heads/test-ref-target topic1..topic2 >result &&
522
523 test_line_count = 1 result &&
524 test_grep "^update refs/heads/test-ref-target " result
525 '
526
527 test_expect_success 'using --revert with --ref' '
528 git branch test-ref-revert topic4 &&
529 git branch test-ref-revert-target topic4 &&
530 test_when_finished "git branch -D test-ref-revert test-ref-revert-target" &&
531
532 git replay --ref-action=print --revert=test-ref-revert --ref=refs/heads/test-ref-revert-target topic4~1..topic4 >result &&
533
534 test_line_count = 1 result &&
535 test_grep "^update refs/heads/test-ref-revert-target " result
536 '
537
538 test_expect_success '--ref is incompatible with --contained' '
539 test_must_fail git replay --onto=main --ref=refs/heads/main --contained topic1..topic2 2>err &&
540 test_grep "cannot be used together" err
541 '
542
543 test_expect_success '--ref with nonexistent fully-qualified ref' '
544 test_when_finished "git update-ref -d refs/heads/new-branch" &&
545
546 git replay --onto=main --ref=refs/heads/new-branch topic1..topic2 &&
547
548 git log --format=%s -2 new-branch >actual &&
549 test_write_lines E D >expect &&
550 test_cmp expect actual
551 '
552
553 test_expect_success '--ref must be a valid refname' '
554 test_must_fail git replay --onto=main --ref="refs/heads/bad..ref" topic1..topic2 2>err &&
555 test_grep "is not a valid refname" err
556 '
557
558 test_expect_success '--ref requires fully qualified ref' '
559 test_must_fail git replay --onto=main --ref=main topic1..topic2 2>err &&
560 test_grep "is not a valid refname" err
561 '
562
563 test_expect_success '--onto with --ref rejects multiple revision ranges' '
564 test_must_fail git replay --onto=main --ref=refs/heads/topic2 ^topic1 topic2 topic4 2>err &&
565 test_grep "cannot be used with multiple revision ranges" err
566 '
567
568 test_done