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