Raw
1 #!/bin/sh
2
3 test_description='tests for git-history fixup subcommand'
4
5 . ./test-lib.sh
6
7 fixup_with_message () {
8 cat >message &&
9 write_script fake-editor.sh <<-\EOF &&
10 cp message "$1"
11 EOF
12 test_set_editor "$(pwd)"/fake-editor.sh &&
13 git history fixup --reedit-message "$@" &&
14 rm fake-editor.sh message
15 }
16
17 expect_changes () {
18 git log --format="%s" --numstat "$@" >actual.raw &&
19 sed '/^$/d' <actual.raw >actual &&
20 cat >expect &&
21 test_cmp expect actual
22 }
23
24 test_expect_success 'errors on missing commit argument' '
25 test_when_finished "rm -rf repo" &&
26 git init repo &&
27 (
28 cd repo &&
29 test_commit initial &&
30 test_must_fail git history fixup 2>err &&
31 test_grep "command expects a single revision" err
32 )
33 '
34
35 test_expect_success 'errors on too many arguments' '
36 test_when_finished "rm -rf repo" &&
37 git init repo &&
38 (
39 cd repo &&
40 test_commit initial &&
41 test_must_fail git history fixup HEAD HEAD 2>err &&
42 test_grep "command expects a single revision" err
43 )
44 '
45
46 test_expect_success 'errors on unknown revision' '
47 test_when_finished "rm -rf repo" &&
48 git init repo &&
49 (
50 cd repo &&
51 test_commit initial &&
52 test_must_fail git history fixup does-not-exist 2>err &&
53 test_grep "commit cannot be found: does-not-exist" err
54 )
55 '
56
57 test_expect_success 'errors when nothing is staged' '
58 test_when_finished "rm -rf repo" &&
59 git init repo &&
60 (
61 cd repo &&
62 test_commit initial &&
63 test_must_fail git history fixup HEAD 2>err &&
64 test_grep "nothing to fixup: no staged changes" err
65 )
66 '
67
68 test_expect_success 'errors in a bare repository' '
69 test_when_finished "rm -rf repo repo.git" &&
70 git init repo &&
71 test_commit -C repo initial &&
72 git clone --bare repo repo.git &&
73 test_must_fail git -C repo.git history fixup HEAD 2>err &&
74 test_grep "cannot run fixup in a bare repository" err
75 '
76
77 test_expect_success 'errors with invalid --empty= value' '
78 test_when_finished "rm -rf repo" &&
79 git init repo &&
80 test_must_fail git -C repo history fixup --empty=bogus HEAD 2>err &&
81 test_grep "unrecognized.*--empty.*bogus" err
82 '
83
84 test_expect_success 'can fixup the tip commit' '
85 test_when_finished "rm -rf repo" &&
86 git init repo &&
87 (
88 cd repo &&
89 test_commit initial &&
90 echo content >file.txt &&
91 git add file.txt &&
92 git commit -m "add file" &&
93
94 echo fix >>file.txt &&
95 git add file.txt &&
96
97 expect_changes <<-\EOF &&
98 add file
99 1 0 file.txt
100 initial
101 1 0 initial.t
102 EOF
103
104 git symbolic-ref HEAD >branch-expect &&
105 git history fixup HEAD &&
106 git symbolic-ref HEAD >branch-actual &&
107 test_cmp branch-expect branch-actual &&
108
109 expect_changes <<-\EOF &&
110 add file
111 2 0 file.txt
112 initial
113 1 0 initial.t
114 EOF
115
116 # Verify the fix is in the tip commit tree
117 git show HEAD:file.txt >actual &&
118 printf "content\nfix\n" >expect &&
119 test_cmp expect actual &&
120
121 git reflog >reflog &&
122 test_grep "fixup: updating HEAD" reflog
123 )
124 '
125
126 test_expect_success 'can fixup a commit in the middle of history' '
127 test_when_finished "rm -rf repo" &&
128 git init repo &&
129 (
130 cd repo &&
131 test_commit first &&
132 echo content >file.txt &&
133 git add file.txt &&
134 git commit -m "add file" &&
135 test_commit third &&
136
137 echo fix >>file.txt &&
138 git add file.txt &&
139
140 expect_changes <<-\EOF &&
141 third
142 1 0 third.t
143 add file
144 1 0 file.txt
145 first
146 1 0 first.t
147 EOF
148
149 git history fixup HEAD~ &&
150
151 expect_changes <<-\EOF &&
152 third
153 1 0 third.t
154 add file
155 2 0 file.txt
156 first
157 1 0 first.t
158 EOF
159
160 # Verify the fix landed in the "add file" commit.
161 git show HEAD~:file.txt >actual &&
162 printf "content\nfix\n" >expect &&
163 test_cmp expect actual &&
164
165 # And verify that the replayed commit also has the change.
166 git show HEAD:file.txt >actual &&
167 printf "content\nfix\n" >expect &&
168 test_cmp expect actual
169 )
170 '
171
172 test_expect_success 'can fixup root commit' '
173 test_when_finished "rm -rf repo" &&
174 git init repo &&
175 (
176 cd repo &&
177 echo initial >root.txt &&
178 git add root.txt &&
179 git commit -m "root" &&
180 test_commit second &&
181
182 expect_changes <<-\EOF &&
183 second
184 1 0 second.t
185 root
186 1 0 root.txt
187 EOF
188
189 echo fix >>root.txt &&
190 git add root.txt &&
191 git history fixup HEAD~ &&
192
193 expect_changes <<-\EOF &&
194 second
195 1 0 second.t
196 root
197 2 0 root.txt
198 EOF
199
200 git show HEAD~:root.txt >actual &&
201 printf "initial\nfix\n" >expect &&
202 test_cmp expect actual
203 )
204 '
205
206 test_expect_success 'preserves commit message and authorship' '
207 test_when_finished "rm -rf repo" &&
208 git init repo &&
209 (
210 cd repo &&
211 test_commit initial &&
212 echo content >file.txt &&
213 git add file.txt &&
214 git commit --author="Original <original@example.com>" -m "original message" &&
215
216 echo fix >>file.txt &&
217 git add file.txt &&
218 git history fixup HEAD &&
219
220 # Message preserved
221 git log -1 --format="%s" >actual &&
222 echo "original message" >expect &&
223 test_cmp expect actual &&
224
225 # Authorship preserved
226 git log -1 --format="%an <%ae>" >actual &&
227 echo "Original <original@example.com>" >expect &&
228 test_cmp expect actual
229 )
230 '
231
232 test_expect_success 'updates all descendant branches by default' '
233 test_when_finished "rm -rf repo" &&
234 git init repo --initial-branch=main &&
235 (
236 cd repo &&
237 test_commit base &&
238 git branch branch &&
239 test_commit ours &&
240 git switch branch &&
241 test_commit theirs &&
242 git switch main &&
243
244 echo fix >fix.txt &&
245 git add fix.txt &&
246 git history fixup base &&
247
248 expect_changes --branches <<-\EOF &&
249 theirs
250 1 0 theirs.t
251 ours
252 1 0 ours.t
253 base
254 1 0 base.t
255 1 0 fix.txt
256 EOF
257
258 # Both branches should have the fix in the base
259 git show main~:fix.txt >actual &&
260 echo fix >expect &&
261 test_cmp expect actual &&
262 git show branch~:fix.txt >actual &&
263 test_cmp expect actual
264 )
265 '
266
267 test_expect_success 'can fixup commit on a different branch' '
268 test_when_finished "rm -rf repo" &&
269 git init repo &&
270 (
271 cd repo &&
272 test_commit base &&
273 git branch theirs &&
274 test_commit ours &&
275 git switch theirs &&
276 test_commit theirs &&
277
278 # Stage a change while on "theirs"
279 echo fix >fix.txt &&
280 git add fix.txt &&
281
282 # Ensure that "ours" does not change, as it does not contain
283 # the commit in question.
284 git rev-parse ours >ours-before &&
285 git history fixup theirs &&
286 git rev-parse ours >ours-after &&
287 test_cmp ours-before ours-after &&
288
289 git show HEAD:fix.txt >actual &&
290 echo fix >expect &&
291 test_cmp expect actual
292 )
293 '
294
295 test_expect_success '--dry-run prints ref updates without modifying repo' '
296 test_when_finished "rm -rf repo" &&
297 git init repo --initial-branch=main &&
298 (
299 cd repo &&
300 test_commit base &&
301 git branch branch &&
302 test_commit main-tip &&
303 git switch branch &&
304 test_commit branch-tip &&
305 git switch main &&
306
307 echo fix >fix.txt &&
308 git add fix.txt &&
309
310 git refs list >refs-before &&
311 git history fixup --dry-run base >updates &&
312 git refs list >refs-after &&
313 test_cmp refs-before refs-after &&
314
315 test_grep "update refs/heads/main" updates &&
316 test_grep "update refs/heads/branch" updates &&
317
318 expect_changes --branches <<-\EOF &&
319 branch-tip
320 1 0 branch-tip.t
321 main-tip
322 1 0 main-tip.t
323 base
324 1 0 base.t
325 EOF
326
327 git update-ref --stdin <updates &&
328 expect_changes --branches <<-\EOF
329 branch-tip
330 1 0 branch-tip.t
331 main-tip
332 1 0 main-tip.t
333 base
334 1 0 base.t
335 1 0 fix.txt
336 EOF
337 )
338 '
339
340 test_expect_success '--update-refs=head updates only HEAD' '
341 test_when_finished "rm -rf repo" &&
342 git init repo --initial-branch=main &&
343 (
344 cd repo &&
345 test_commit base &&
346 git branch branch &&
347 test_commit main-tip &&
348 git switch branch &&
349 test_commit branch-tip &&
350
351 echo fix >fix.txt &&
352 git add fix.txt &&
353
354 # Only HEAD (branch) should be updated
355 git history fixup --update-refs=head base &&
356
357 # The main branch should be unaffected.
358 expect_changes main <<-\EOF &&
359 main-tip
360 1 0 main-tip.t
361 base
362 1 0 base.t
363 EOF
364
365 # But the currently checked out branch should be modified.
366 expect_changes branch <<-\EOF
367 branch-tip
368 1 0 branch-tip.t
369 base
370 1 0 base.t
371 1 0 fix.txt
372 EOF
373 )
374 '
375
376 test_expect_success '--update-refs=head refuses to rewrite commits not in HEAD ancestry' '
377 test_when_finished "rm -rf repo" &&
378 git init repo --initial-branch=main &&
379 (
380 cd repo &&
381 test_commit base &&
382 git branch other &&
383 test_commit main-tip &&
384 git switch other &&
385 test_commit other-tip &&
386
387 echo fix >fix.txt &&
388 git add fix.txt &&
389
390 test_must_fail git history fixup --update-refs=head main-tip 2>err &&
391 test_grep "rewritten commit must be an ancestor of HEAD" err
392 )
393 '
394
395 test_expect_success 'aborts when fixup would produce conflicts' '
396 test_when_finished "rm -rf repo" &&
397 git init repo &&
398 (
399 cd repo &&
400
401 echo "line one" >file.txt &&
402 git add file.txt &&
403 git commit -m "first" &&
404
405 echo "line two" >file.txt &&
406 git add file.txt &&
407 git commit -m "second" &&
408
409 echo "conflicting change" >file.txt &&
410 git add file.txt &&
411
412 git refs list >refs-before &&
413 test_must_fail git history fixup HEAD~ 2>err &&
414 test_grep "fixup would produce conflicts" err &&
415 git refs list >refs-after &&
416 test_cmp refs-before refs-after
417 )
418 '
419
420 test_expect_success '--reedit-message opens editor for the commit message' '
421 test_when_finished "rm -rf repo" &&
422 git init repo &&
423 (
424 cd repo &&
425 test_commit initial &&
426 echo content >file.txt &&
427 git add file.txt &&
428 git commit -m "add file" &&
429
430 echo fix >>file.txt &&
431 git add file.txt &&
432
433 fixup_with_message HEAD <<-\EOF &&
434 add file with fix
435 EOF
436
437 expect_changes --branches <<-\EOF
438 add file with fix
439 2 0 file.txt
440 initial
441 1 0 initial.t
442 EOF
443 )
444 '
445
446 test_expect_success 'retains unstaged working tree changes after fixup' '
447 test_when_finished "rm -rf repo" &&
448 git init repo &&
449 (
450 cd repo &&
451 touch a b &&
452 git add . &&
453 git commit -m "initial commit" &&
454 echo staged >a &&
455 echo unstaged >b &&
456 git add a &&
457 git history fixup HEAD &&
458
459 # b is still modified in the worktree but not staged
460 cat >expect <<-\EOF &&
461 M b
462 EOF
463 git status --porcelain --untracked-files=no >actual &&
464 test_cmp expect actual
465 )
466 '
467
468 test_expect_success 'index is clean after fixup when target is HEAD' '
469 test_when_finished "rm -rf repo" &&
470 git init repo &&
471 (
472 cd repo &&
473
474 test_commit initial &&
475 echo fix >fix.txt &&
476 git add fix.txt &&
477 git history fixup HEAD &&
478
479 git status --porcelain --untracked-files=no >actual &&
480 test_must_be_empty actual
481 )
482 '
483
484 test_expect_success 'index is unchanged on conflict' '
485 test_when_finished "rm -rf repo" &&
486 git init repo &&
487 (
488 cd repo &&
489
490 echo base >file.txt &&
491 git add file.txt &&
492 git commit -m base &&
493 echo change >file.txt &&
494 git add file.txt &&
495 git commit -m change &&
496
497 echo conflict >file.txt &&
498 git add file.txt &&
499
500 git diff --cached >index-before &&
501 test_must_fail git history fixup HEAD~ &&
502 git diff --cached >index-after &&
503 test_cmp index-before index-after
504 )
505 '
506
507 test_expect_success '--empty=drop removes target commit and replays descendants onto its parent' '
508 test_when_finished "rm -rf repo" &&
509 git init repo --initial-branch=main &&
510 (
511 cd repo &&
512
513 test_commit first &&
514 test_commit second &&
515 test_commit third &&
516
517 git rm second.t &&
518 git history fixup --empty=drop HEAD~ &&
519
520 expect_changes <<-\EOF &&
521 third
522 1 0 third.t
523 first
524 1 0 first.t
525 EOF
526 test_must_fail git show HEAD:second.t
527 )
528 '
529
530 test_expect_success '--empty=drop errors out when dropping the root commit' '
531 test_when_finished "rm -rf repo" &&
532 git init repo &&
533 (
534 cd repo &&
535
536 test_commit first &&
537 test_commit second &&
538
539 git rm first.t &&
540 test_must_fail git history fixup --empty=drop HEAD~ 2>err &&
541 test_grep "cannot drop root commit" err
542 )
543 '
544
545 test_expect_success '--empty=drop can drop the HEAD commit' '
546 test_when_finished "rm -rf repo" &&
547 git init repo &&
548 (
549 cd repo &&
550
551 test_commit first &&
552 test_commit second &&
553
554 git rm second.t &&
555 git history fixup --empty=drop HEAD &&
556
557 expect_changes <<-\EOF
558 first
559 1 0 first.t
560 EOF
561 )
562 '
563
564 test_expect_success '--empty=drop drops empty replayed commits' '
565 test_when_finished "rm -rf repo" &&
566 git init repo &&
567 (
568 cd repo &&
569
570 touch base remove-me &&
571 git add . &&
572 git commit -m "base" &&
573 git rm remove-me &&
574 git commit -m "remove" &&
575 touch reintroduce remove-me &&
576 git add . &&
577 git commit -m "reintroduce" &&
578
579 git rm remove-me &&
580 git history fixup --empty=drop HEAD~2 &&
581
582 expect_changes <<-\EOF
583 reintroduce
584 0 0 reintroduce
585 0 0 remove-me
586 base
587 0 0 base
588 EOF
589 )
590 '
591
592 test_expect_success '--empty=keep keeps commit when fixup target becomes empty' '
593 test_when_finished "rm -rf repo" &&
594 git init repo &&
595 (
596 cd repo &&
597
598 test_commit first &&
599 test_commit second &&
600 test_commit third &&
601
602 git rm second.t &&
603 git history fixup --empty=keep HEAD~ &&
604
605 expect_changes <<-\EOF
606 third
607 1 0 third.t
608 second
609 first
610 1 0 first.t
611 EOF
612 )
613 '
614
615 test_expect_success '--empty=keep keeps commit when replayed commit becomes empty' '
616 test_when_finished "rm -rf repo" &&
617 git init repo &&
618 (
619 cd repo &&
620
621 touch base remove-me &&
622 git add . &&
623 git commit -m "base" &&
624 git rm remove-me &&
625 git commit -m "remove" &&
626 touch reintroduce remove-me &&
627 git add . &&
628 git commit -m "reintroduce" &&
629
630 git rm remove-me &&
631 git history fixup --empty=keep HEAD~2 &&
632
633 expect_changes <<-\EOF
634 reintroduce
635 0 0 reintroduce
636 0 0 remove-me
637 remove
638 base
639 0 0 base
640 EOF
641 )
642 '
643
644 test_expect_success '--empty=abort errors out when fixup target becomes empty' '
645 test_when_finished "rm -rf repo" &&
646 git init repo &&
647 (
648 cd repo &&
649
650 test_commit first &&
651 test_commit second &&
652
653 git rm first.t &&
654 test_must_fail git history fixup --empty=abort HEAD~ 2>err &&
655 test_grep "fixup makes commit.*empty" err
656 )
657 '
658
659 test_expect_success '--empty=abort errors out when a descendant becomes empty during replay' '
660 test_when_finished "rm -rf repo" &&
661 git init repo --initial-branch=main &&
662 (
663 cd repo &&
664
665 touch base remove-me &&
666 git add . &&
667 git commit -m "base" &&
668 git rm remove-me &&
669 git commit -m "remove" &&
670 touch reintroduce remove-me &&
671 git add . &&
672 git commit -m "reintroduce" &&
673
674 git rm remove-me &&
675 test_must_fail git history fixup --empty=abort HEAD~2 2>err &&
676 test_grep "became empty after replay" err
677 )
678 '
679
680 test_done