1
+#!/bin/sh
2
+
3
+test_description='tests for git-history squash subcommand'
4
+
5
+. ./test-lib.sh
6
+
7
+stage_file () {
8
+ printf "%s\n" "$1" >file &&
9
+ git add file
10
+}
11
+
12
+commit_with_message () {
13
+ printf "%b" "$1" >msg &&
14
+ git commit --allow-empty -qF msg
15
+}
16
+
17
+check_commit_count () {
18
+ git rev-list --count "$1" >actual &&
19
+ echo "$2" >expect &&
20
+ test_cmp expect actual
21
+}
22
+
23
+check_log_subjects () {
24
+ git log --format="%s" "$1" >actual &&
25
+ cat >expect &&
26
+ test_cmp expect actual
27
+}
28
+
29
+check_log_messages () {
30
+ git log --format="%B" "$1" >actual &&
31
+ cat >expect &&
32
+ test_cmp expect actual
33
+}
34
+
35
+test_expect_success 'setup linear history touching two files' '
36
+ test_commit base file a &&
37
+ git tag start &&
38
+ test_commit --no-tag one other x &&
39
+ test_commit --no-tag two file c &&
40
+ test_commit three file d
41
+'
42
+
43
+test_expect_success 'errors on missing range argument' '
44
+ test_must_fail git history squash 2>err &&
45
+ test_grep "expects a revision range" err
46
+'
47
+
48
+test_expect_success 'errors on an empty range' '
49
+ test_must_fail git history squash HEAD..HEAD 2>err &&
50
+ test_grep "the revision range is empty" err
51
+'
52
+
53
+test_expect_success 'errors on a single revision that is not a range' '
54
+ test_must_fail git history squash HEAD 2>err &&
55
+ test_grep "not a .*range" err &&
56
+ test_must_fail git history squash HEAD~1 2>err &&
57
+ test_grep "not a .*range" err
58
+'
59
+
60
+test_expect_success 'errors on a range holding a single commit' '
61
+ git reset --hard three &&
62
+ head_before=$(git rev-parse HEAD) &&
63
+
64
+ test_must_fail git history squash "HEAD^!" 2>err &&
65
+ test_grep "single commit; nothing to squash" err &&
66
+ test_cmp_rev "$head_before" HEAD
67
+'
68
+
69
+test_expect_success 'accepts multiple revision arguments with an exclusion' '
70
+ git reset --hard three &&
71
+ git branch -f keep HEAD~2 &&
72
+ tip_tree=$(git rev-parse HEAD^{tree}) &&
73
+
74
+ git history squash start..HEAD ^keep &&
75
+
76
+ git reflog -1 --format=%gs >actual &&
77
+ echo "squash: updating start..HEAD ^keep" >expect &&
78
+ test_cmp expect actual &&
79
+
80
+ check_log_subjects start..HEAD <<-\EOF &&
81
+ two
82
+ one
83
+ EOF
84
+ test_cmp_rev keep HEAD~1 &&
85
+ test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
86
+
87
+ git branch -D keep
88
+'
89
+
90
+test_expect_success 'squashes a branch the current branch is not on' '
91
+ git reset --hard three &&
92
+ main=$(git symbolic-ref --short HEAD) &&
93
+ head_before=$(git rev-parse HEAD) &&
94
+ git checkout -b off-history start &&
95
+ test_commit --no-tag off-one off a &&
96
+ test_commit --no-tag off-two off b &&
97
+ git checkout "$main" &&
98
+
99
+ git history squash start..off-history &&
100
+
101
+ check_commit_count start..off-history 1 &&
102
+ test_cmp_rev "$head_before" HEAD &&
103
+
104
+ git branch -D off-history
105
+'
106
+
107
+test_expect_success 'squashes a range into a single commit without changing the tree' '
108
+ git reset --hard three &&
109
+ head_before=$(git rev-parse HEAD) &&
110
+ tip_tree=$(git rev-parse HEAD^{tree}) &&
111
+
112
+ git history squash --dry-run start.. >out &&
113
+ predicted=$(awk "/^update refs\/heads\// {print \$3}" out) &&
114
+ test_cmp_rev "$head_before" HEAD &&
115
+
116
+ git history squash start.. &&
117
+
118
+ test "$predicted" = "$(git rev-parse HEAD)" &&
119
+ check_commit_count start..HEAD 1 &&
120
+ test_cmp_rev start HEAD^ &&
121
+ test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
122
+ check_log_subjects -1 <<-\EOF &&
123
+ one
124
+ EOF
125
+ git reflog >reflog &&
126
+ test_grep "squash: updating" reflog
127
+'
128
+
129
+test_expect_success 'sanitizes rev-list walk options, before and after --' '
130
+ git reset --hard three &&
131
+ tip_tree=$(git rev-parse HEAD^{tree}) &&
132
+
133
+ git history squash --date-order start.. 2>err &&
134
+ test_grep "ignoring rev-list options" err &&
135
+ test_cmp_rev start HEAD^ &&
136
+ test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
137
+
138
+ git reset --hard three &&
139
+ git history squash -- --reverse start.. 2>err &&
140
+ test_grep "ignoring rev-list options" err &&
141
+ test_cmp_rev start HEAD^ &&
142
+ test "$tip_tree" = "$(git rev-parse HEAD^{tree})"
143
+'
144
+
145
+test_expect_success 'squashes an interior range and replays descendants verbatim' '
146
+ git reset --hard three &&
147
+ final_tree=$(git rev-parse HEAD^{tree}) &&
148
+
149
+ git history squash start..@~1 &&
150
+
151
+ check_log_subjects start..HEAD <<-\EOF &&
152
+ three
153
+ one
154
+ EOF
155
+
156
+ test_cmp_rev start HEAD~2 &&
157
+ test "$final_tree" = "$(git rev-parse HEAD^{tree})"
158
+'
159
+
160
+test_expect_success 'squashes when the base is the root commit' '
161
+ git reset --hard three &&
162
+ root=$(git rev-list --max-parents=0 HEAD) &&
163
+ tip_tree=$(git rev-parse HEAD^{tree}) &&
164
+
165
+ git history squash "$root.." &&
166
+
167
+ check_commit_count "$root..HEAD" 1 &&
168
+ test_cmp_rev "$root" HEAD^ &&
169
+ test "$tip_tree" = "$(git rev-parse HEAD^{tree})"
170
+'
171
+
172
+
173
+test_expect_success 'folds fixups whose target is in the range' '
174
+ git reset --hard start &&
175
+ test_commit --no-tag target file b &&
176
+ git commit --allow-empty -m "fixup! target" &&
177
+ git commit --allow-empty -m "fixup! target" &&
178
+ test_commit --no-tag later file c &&
179
+
180
+ git history squash start.. &&
181
+
182
+ check_commit_count start..HEAD 1 &&
183
+ check_log_subjects -1 <<-\EOF
184
+ target
185
+ EOF
186
+'
187
+
188
+test_expect_success 'refuses a below-range fixup! after an in-range commit' '
189
+ git reset --hard start &&
190
+ test_commit --no-tag inside file b &&
191
+ test_commit --no-tag "fixup! outside" file c &&
192
+ head_before=$(git rev-parse HEAD) &&
193
+
194
+ test_must_fail git history squash start.. 2>err &&
195
+ test_grep "target is not in the range" err &&
196
+ test_cmp_rev "$head_before" HEAD
197
+'
198
+
199
+test_expect_success 'combines a run of fixups for one commit below the range' '
200
+ git reset --hard start &&
201
+ stage_file b && git commit -m "fixup! base" &&
202
+ stage_file c && git commit -m "fixup! base" &&
203
+
204
+ git history squash start.. &&
205
+
206
+ check_commit_count start..HEAD 1 &&
207
+ check_log_subjects -1 <<-\EOF
208
+ fixup! base
209
+ EOF
210
+'
211
+
212
+test_expect_success 'combining below-range fixups keeps the last amend! message' '
213
+ git reset --hard start &&
214
+ stage_file b && git commit -m "fixup! base" &&
215
+ stage_file c &&
216
+ commit_with_message "amend! base\n\namended body\n" &&
217
+
218
+ git history squash start.. &&
219
+
220
+ check_commit_count start..HEAD 1 &&
221
+ check_log_messages -1 <<-\EOF
222
+ amend! base
223
+
224
+ amended body
225
+
226
+ EOF
227
+'
228
+
229
+test_expect_success 'refuses fixups for two different commits below the range' '
230
+ git reset --hard start &&
231
+ stage_file b && git commit -m "fixup! aaa" &&
232
+ stage_file c && git commit -m "fixup! bbb" &&
233
+ head_before=$(git rev-parse HEAD) &&
234
+
235
+ test_must_fail git history squash start.. 2>err &&
236
+ test_grep "target is not in the range" err &&
237
+ test_cmp_rev "$head_before" HEAD
238
+'
239
+
240
+test_expect_success 'the last amend! for the oldest commit replaces its message' '
241
+ git reset --hard start &&
242
+ test_commit --no-tag marker-oldest file b &&
243
+ git commit --allow-empty -m "squash! marker-oldest" &&
244
+ commit_with_message "amend! marker-oldest\n\nearlier message\n" &&
245
+ commit_with_message \
246
+ "amend! marker-oldest\n\namended subject\n\namended body\n" &&
247
+ test_commit --no-tag marker-later file c &&
248
+ commit_with_message "amend! marker-later\n\nwrong message\n" &&
249
+
250
+ git history squash start.. &&
251
+
252
+ check_commit_count start..HEAD 1 &&
253
+ check_log_messages -1 <<-\EOF
254
+ amended subject
255
+
256
+ amended body
257
+
258
+ EOF
259
+'
260
+
261
+test_expect_success 'preserves authorship of the oldest commit' '
262
+ git reset --hard start &&
263
+ GIT_AUTHOR_NAME=Squasher GIT_AUTHOR_EMAIL=squash@example.com \
264
+ test_commit --no-tag oldest file b &&
265
+ test_commit newest file c &&
266
+
267
+ git history squash start.. &&
268
+
269
+ git log -1 --format="%an <%ae>" >actual &&
270
+ echo "Squasher <squash@example.com>" >expect &&
271
+ test_cmp expect actual
272
+'
273
+
274
+test_expect_success '--update-refs=head only moves HEAD' '
275
+ git reset --hard three &&
276
+ git branch -f other HEAD &&
277
+ other_before=$(git rev-parse other) &&
278
+
279
+ git history squash --update-refs=head start.. &&
280
+
281
+ check_commit_count start..HEAD 1 &&
282
+ test_cmp_rev "$other_before" other
283
+'
284
+
285
+test_expect_success 'refuses to fold a range a ref points into' '
286
+ git reset --hard three &&
287
+ git branch -f mid HEAD~1 &&
288
+ head_before=$(git rev-parse HEAD) &&
289
+
290
+ test_must_fail git history squash start.. 2>err &&
291
+ test_grep "error: .* points into the squashed range" err &&
292
+ test_grep "hint: .*--update-refs=head" err &&
293
+ test_cmp_rev "$head_before" HEAD &&
294
+
295
+ git branch -D mid
296
+'
297
+
298
+test_expect_success 'advice.historyUpdateRefs silences the hint' '
299
+ git reset --hard three &&
300
+ git branch -f mid HEAD~1 &&
301
+ head_before=$(git rev-parse HEAD) &&
302
+
303
+ test_must_fail git -c advice.historyUpdateRefs=false \
304
+ history squash start.. 2>err &&
305
+ test_grep "points into the squashed range" err &&
306
+ test_grep ! "hint:" err &&
307
+ test_cmp_rev "$head_before" HEAD &&
308
+
309
+ git branch -D mid
310
+'
311
+
312
+test_expect_success '--update-refs=head folds past a ref pointing into the range' '
313
+ git reset --hard three &&
314
+ git branch -f mid HEAD~1 &&
315
+ mid_before=$(git rev-parse mid) &&
316
+
317
+ git history squash --update-refs=head start.. &&
318
+
319
+ check_commit_count start..HEAD 1 &&
320
+ test_cmp_rev "$mid_before" mid &&
321
+
322
+ git branch -D mid
323
+'
324
+
325
+test_expect_success 'refuses to fold a range a tag points into' '
326
+ git reset --hard three &&
327
+ git tag -f mark HEAD~1 &&
328
+ head_before=$(git rev-parse HEAD) &&
329
+
330
+ test_must_fail git history squash start.. 2>err &&
331
+ test_grep "refs/tags/mark" err &&
332
+ test_grep "points into the squashed range" err &&
333
+ test_cmp_rev "$head_before" HEAD &&
334
+
335
+ git tag -d mark
336
+'
337
+
338
+test_expect_success 'squashes a range whose internal merge has a single base' '
339
+ git reset --hard start &&
340
+ main=$(git symbolic-ref --short HEAD) &&
341
+ test_commit --no-tag before-side file b &&
342
+ git checkout -b inner-side &&
343
+ test_commit --no-tag on-inner-side inner x &&
344
+ git checkout "$main" &&
345
+ test_commit --no-tag after-side file c &&
346
+ git merge --no-ff -m merge inner-side &&
347
+ git branch -D inner-side &&
348
+ test_commit --no-tag after-merge file d &&
349
+ tip_tree=$(git rev-parse HEAD^{tree}) &&
350
+
351
+ git history squash start.. &&
352
+
353
+ check_commit_count start..HEAD 1 &&
354
+ check_log_subjects -1 <<-\EOF &&
355
+ before-side
356
+ EOF
357
+ test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
358
+ test_path_is_file inner
359
+'
360
+
361
+test_expect_success 'folds a merge of a branch that forked at the base' '
362
+ git reset --hard start &&
363
+ main=$(git symbolic-ref --short HEAD) &&
364
+ git checkout -b base-fork-side &&
365
+ test_commit --no-tag base-fork-side side x &&
366
+ git checkout "$main" &&
367
+ test_commit --no-tag base-fork-main file b &&
368
+ git merge --no-ff -m "merge base-fork-side" base-fork-side &&
369
+ git branch -D base-fork-side &&
370
+ test_commit --no-tag base-fork-tail file c &&
371
+ tip_tree=$(git rev-parse HEAD^{tree}) &&
372
+
373
+ git history squash start.. &&
374
+
375
+ check_commit_count start..HEAD 1 &&
376
+ test_cmp_rev start HEAD^ &&
377
+ test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
378
+ test_path_is_file side
379
+'
380
+
381
+test_expect_success 'refuses a merge whose other parent is outside the range' '
382
+ git reset --hard start &&
383
+ main=$(git symbolic-ref --short HEAD) &&
384
+ git checkout -b outside-parent &&
385
+ test_commit --no-tag outside-parent outside x &&
386
+ git checkout "$main" &&
387
+ test_commit --no-tag outside-main file b &&
388
+ base=$(git rev-parse HEAD) &&
389
+ test_commit --no-tag outside-mid file c &&
390
+ git merge --no-ff -m "merge outside-parent" outside-parent &&
391
+ git branch -D outside-parent &&
392
+ merged=$(git rev-parse HEAD) &&
393
+
394
+ test_must_fail git history squash "$base.." 2>err &&
395
+ test_grep "more than one base" err &&
396
+ test_cmp_rev "$merged" HEAD
397
+'
398
+
399
+test_expect_success 'folds a range whose tip is a merge commit' '
400
+ git reset --hard start &&
401
+ main=$(git symbolic-ref --short HEAD) &&
402
+ test_commit --no-tag tipmerge-base file b &&
403
+ git checkout -b tipmerge-side &&
404
+ test_commit --no-tag tipmerge-side side x &&
405
+ git checkout "$main" &&
406
+ test_commit --no-tag tipmerge-main file c &&
407
+ git merge --no-ff -m "merge tipmerge-side" tipmerge-side &&
408
+ git branch -D tipmerge-side &&
409
+ tip_tree=$(git rev-parse HEAD^{tree}) &&
410
+
411
+ git history squash start.. &&
412
+
413
+ check_commit_count start..HEAD 1 &&
414
+ test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
415
+ test_path_is_file side
416
+'
417
+
418
+test_expect_success 'folds a range whose base is a merge commit' '
419
+ git reset --hard start &&
420
+ main=$(git symbolic-ref --short HEAD) &&
421
+ git checkout -b basemerge-side &&
422
+ test_commit --no-tag basemerge-side side x &&
423
+ git checkout "$main" &&
424
+ test_commit --no-tag basemerge-main file b &&
425
+ git merge --no-ff -m "merge basemerge-side" basemerge-side &&
426
+ git branch -D basemerge-side &&
427
+ base=$(git rev-parse HEAD) &&
428
+ test_commit --no-tag basemerge-one file c &&
429
+ test_commit --no-tag basemerge-two file d &&
430
+ tip_tree=$(git rev-parse HEAD^{tree}) &&
431
+
432
+ git history squash "$base.." &&
433
+
434
+ check_commit_count "$base..HEAD" 1 &&
435
+ test_cmp_rev "$base" HEAD^ &&
436
+ test "$tip_tree" = "$(git rev-parse HEAD^{tree})"
437
+'
438
+
439
+test_expect_success 'folds a range with two interior merges' '
440
+ git reset --hard start &&
441
+ main=$(git symbolic-ref --short HEAD) &&
442
+ test_commit --no-tag two-merge-a file a1 &&
443
+ git checkout -b two-merge-s1 &&
444
+ test_commit --no-tag two-merge-s1 s1 x &&
445
+ git checkout "$main" &&
446
+ git merge --no-ff -m "merge s1" two-merge-s1 &&
447
+ test_commit --no-tag two-merge-b file b1 &&
448
+ git checkout -b two-merge-s2 &&
449
+ test_commit --no-tag two-merge-s2 s2 y &&
450
+ git checkout "$main" &&
451
+ git merge --no-ff -m "merge s2" two-merge-s2 &&
452
+ git branch -D two-merge-s1 two-merge-s2 &&
453
+ tip_tree=$(git rev-parse HEAD^{tree}) &&
454
+
455
+ git history squash start.. &&
456
+
457
+ check_commit_count start..HEAD 1 &&
458
+ test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
459
+ test_path_is_file s1 &&
460
+ test_path_is_file s2
461
+'
462
+
463
+test_expect_success 'folds a range with a nested merge' '
464
+ git reset --hard start &&
465
+ main=$(git symbolic-ref --short HEAD) &&
466
+ git checkout -b nested-outer &&
467
+ test_commit --no-tag nested-outer outer x &&
468
+ git checkout -b nested-inner &&
469
+ test_commit --no-tag nested-inner inner y &&
470
+ git checkout nested-outer &&
471
+ git merge --no-ff -m "merge inner" nested-inner &&
472
+ git checkout "$main" &&
473
+ test_commit --no-tag nested-main file b1 &&
474
+ git merge --no-ff -m "merge outer" nested-outer &&
475
+ git branch -D nested-outer nested-inner &&
476
+ tip_tree=$(git rev-parse HEAD^{tree}) &&
477
+
478
+ git history squash start.. &&
479
+
480
+ check_commit_count start..HEAD 1 &&
481
+ test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
482
+ test_path_is_file outer &&
483
+ test_path_is_file inner
484
+'
485
+
486
+test_expect_success 'folds a range with an octopus merge' '
487
+ git reset --hard start &&
488
+ main=$(git symbolic-ref --short HEAD) &&
489
+ test_commit --no-tag octo-base file a1 &&
490
+ git checkout -b octo-1 &&
491
+ test_commit --no-tag octo-1 o1 x &&
492
+ git checkout "$main" &&
493
+ git checkout -b octo-2 &&
494
+ test_commit --no-tag octo-2 o2 y &&
495
+ git checkout "$main" &&
496
+ git merge --no-ff -m octopus octo-1 octo-2 &&
497
+ git branch -D octo-1 octo-2 &&
498
+ tip_tree=$(git rev-parse HEAD^{tree}) &&
499
+
500
+ git history squash start.. &&
501
+
502
+ check_commit_count start..HEAD 1 &&
503
+ test "$tip_tree" = "$(git rev-parse HEAD^{tree})" &&
504
+ test_path_is_file o1 &&
505
+ test_path_is_file o2
506
+'
507
+
508
+test_expect_success 'refuses an octopus merge with an arm forked before the base' '
509
+ git reset --hard start &&
510
+ main=$(git symbolic-ref --short HEAD) &&
511
+ git checkout -b octo-pre &&
512
+ test_commit octo-pre-side pside x &&
513
+ git checkout "$main" &&
514
+ test_commit octo-pre-main file b1 &&
515
+ octo_base=$(git rev-parse HEAD) &&
516
+ git checkout -b octo-within &&
517
+ test_commit --no-tag octo-within wside y &&
518
+ git checkout "$main" &&
519
+ git merge --no-ff -m octopus octo-pre octo-within &&
520
+ merged=$(git rev-parse HEAD) &&
521
+ git branch -D octo-pre octo-within &&
522
+
523
+ test_must_fail git history squash "$octo_base.." 2>err &&
524
+ test_grep "more than one base" err &&
525
+ test_cmp_rev "$merged" HEAD
526
+'
527
+
528
+test_expect_success 'refuses when a descendant above the range is a merge' '
529
+ git reset --hard start &&
530
+ main=$(git symbolic-ref --short HEAD) &&
531
+ test_commit --no-tag desc-one file b &&
532
+ test_commit --no-tag desc-two file c &&
533
+ git tag desc-tip &&
534
+ git checkout -b desc-above &&
535
+ test_commit --no-tag desc-above above x &&
536
+ git checkout "$main" &&
537
+ test_commit --no-tag desc-main file d &&
538
+ git merge --no-ff -m "merge desc-above" desc-above &&
539
+ git branch -D desc-above &&
540
+ head_before=$(git rev-parse HEAD) &&
541
+
542
+ test_must_fail git history squash start..desc-tip 2>err &&
543
+ test_grep "merge commits is not supported" err &&
544
+ test_cmp_rev "$head_before" HEAD
545
+'
546
+
547
+test_expect_success 'refuses to fold a range a ref points into at a merge' '
548
+ git reset --hard start &&
549
+ main=$(git symbolic-ref --short HEAD) &&
550
+ test_commit --no-tag refmerge-base file b &&
551
+ git checkout -b refmerge-side &&
552
+ test_commit --no-tag refmerge-side side x &&
553
+ git checkout "$main" &&
554
+ test_commit --no-tag refmerge-main file c &&
555
+ git merge --no-ff -m "interior merge" refmerge-side &&
556
+ git branch -D refmerge-side &&
557
+ git branch at-merge HEAD &&
558
+ test_commit --no-tag refmerge-tail file d &&
559
+ head_before=$(git rev-parse HEAD) &&
560
+
561
+ test_must_fail git history squash start.. 2>err &&
562
+ test_grep "at-merge" err &&
563
+ test_grep "points into the squashed range" err &&
564
+ test_cmp_rev "$head_before" HEAD &&
565
+
566
+ git branch -D at-merge
567
+'
568
+
569
+test_done