1
+#!/bin/sh
2
+
3
+test_description='tests for git-history drop subcommand'
4
+
5
+. ./test-lib.sh
6
+. "$TEST_DIRECTORY/lib-log-graph.sh"
7
+
8
+expect_graph () {
9
+ cat >expect &&
10
+ lib_test_cmp_graph --format=%s "$@"
11
+}
12
+
13
+expect_log () {
14
+ git log --format="%s" "$@" >actual &&
15
+ cat >expect &&
16
+ test_cmp expect actual
17
+}
18
+
19
+test_expect_success 'errors on missing commit argument' '
20
+ test_when_finished "rm -rf repo" &&
21
+ git init repo &&
22
+ (
23
+ cd repo &&
24
+ test_commit initial &&
25
+ test_must_fail git history drop 2>err &&
26
+ test_grep "command expects a single revision" err
27
+ )
28
+'
29
+
30
+test_expect_success 'errors on too many arguments' '
31
+ test_when_finished "rm -rf repo" &&
32
+ git init repo &&
33
+ (
34
+ cd repo &&
35
+ test_commit initial &&
36
+ test_must_fail git history drop HEAD HEAD 2>err &&
37
+ test_grep "command expects a single revision" err
38
+ )
39
+'
40
+
41
+test_expect_success 'errors on unknown revision' '
42
+ test_when_finished "rm -rf repo" &&
43
+ git init repo &&
44
+ (
45
+ cd repo &&
46
+ test_commit initial &&
47
+ test_must_fail git history drop does-not-exist 2>err &&
48
+ test_grep "commit cannot be found: does-not-exist" err
49
+ )
50
+'
51
+
52
+test_expect_success 'errors with invalid --empty= value' '
53
+ test_when_finished "rm -rf repo" &&
54
+ git init repo &&
55
+ (
56
+ cd repo &&
57
+ test_commit initial &&
58
+ test_commit second &&
59
+ test_must_fail git history drop --empty=bogus HEAD 2>err &&
60
+ test_grep "unrecognized.*--empty.*bogus" err
61
+ )
62
+'
63
+
64
+test_expect_success 'drops a commit in the middle and replays descendants' '
65
+ test_when_finished "rm -rf repo" &&
66
+ git init repo &&
67
+ (
68
+ cd repo &&
69
+ test_commit first &&
70
+ test_commit second &&
71
+ test_commit third &&
72
+
73
+ git symbolic-ref HEAD >expect &&
74
+ git history drop HEAD~ &&
75
+ git symbolic-ref HEAD >actual &&
76
+ test_cmp expect actual &&
77
+
78
+ expect_log <<-\EOF &&
79
+ third
80
+ first
81
+ EOF
82
+
83
+ test_must_fail git show HEAD:second.t &&
84
+ test_path_is_missing second.t &&
85
+
86
+ git reflog >reflog &&
87
+ test_grep "drop: dropping HEAD~" reflog
88
+ )
89
+'
90
+
91
+test_expect_success 'drops the HEAD commit' '
92
+ test_when_finished "rm -rf repo" &&
93
+ git init repo &&
94
+ (
95
+ cd repo &&
96
+ test_commit first &&
97
+ test_commit second &&
98
+
99
+ git history drop HEAD &&
100
+
101
+ expect_log <<-\EOF
102
+ first
103
+ EOF
104
+ )
105
+'
106
+
107
+test_expect_success 'drops a commit on detached HEAD' '
108
+ test_when_finished "rm -rf repo" &&
109
+ git init repo &&
110
+ (
111
+ cd repo &&
112
+ test_commit first &&
113
+ test_commit second &&
114
+ test_commit third &&
115
+ git checkout --detach HEAD &&
116
+
117
+ git history drop HEAD~ &&
118
+
119
+ expect_log <<-\EOF
120
+ third
121
+ first
122
+ EOF
123
+ )
124
+'
125
+
126
+# Note: in this case it would actually be fine to drop the root commit, as we
127
+# do have a descendant commit, and no reference points to the root commit
128
+# directly. So this is something that we may relax eventually.
129
+test_expect_success 'refuses to drop the root commit' '
130
+ test_when_finished "rm -rf repo" &&
131
+ git init repo &&
132
+ (
133
+ cd repo &&
134
+ test_commit first &&
135
+ test_commit second &&
136
+
137
+ test_must_fail git history drop HEAD~ 2>err &&
138
+ test_grep "cannot drop root commit" err
139
+ )
140
+'
141
+
142
+# In contrast to the above case, we actually don't want to drop the root commit
143
+# here as that would cause us to end up with an empty commit graph.
144
+test_expect_success 'refuses to drop the root commit when branch becomes empty' '
145
+ test_when_finished "rm -rf repo" &&
146
+ git init repo &&
147
+ (
148
+ cd repo &&
149
+ test_commit first &&
150
+
151
+ test_must_fail git history drop HEAD 2>err &&
152
+ test_grep "cannot drop root commit" err
153
+ )
154
+'
155
+
156
+test_expect_success 'refuses to drop a merge commit' '
157
+ test_when_finished "rm -rf repo" &&
158
+ git init repo &&
159
+ (
160
+ cd repo &&
161
+ test_commit base &&
162
+ git branch branch &&
163
+ test_commit ours &&
164
+ git switch branch &&
165
+ test_commit theirs &&
166
+ git switch - &&
167
+ git merge theirs &&
168
+
169
+ test_must_fail git history drop HEAD 2>err &&
170
+ test_grep "cannot drop merge commit" err
171
+ )
172
+'
173
+
174
+test_expect_success 'refuses when descendants contain a merge commit' '
175
+ test_when_finished "rm -rf repo" &&
176
+ git init repo &&
177
+ (
178
+ cd repo &&
179
+ test_commit base &&
180
+ test_commit middle &&
181
+ git branch branch &&
182
+ test_commit ours &&
183
+ git switch branch &&
184
+ test_commit theirs &&
185
+ git switch - &&
186
+ git merge theirs &&
187
+
188
+ test_must_fail git history drop middle 2>err &&
189
+ test_grep "replaying merge commits is not supported yet" err
190
+ )
191
+'
192
+
193
+test_expect_success 'works in a bare repository' '
194
+ test_when_finished "rm -rf repo repo.git" &&
195
+
196
+ git init repo &&
197
+ test_commit -C repo first &&
198
+ test_commit -C repo second &&
199
+ test_commit -C repo third &&
200
+
201
+ git clone --bare repo repo.git &&
202
+ (
203
+ cd repo.git &&
204
+
205
+ git history drop HEAD~ &&
206
+ expect_log <<-\EOF
207
+ third
208
+ first
209
+ EOF
210
+ )
211
+'
212
+
213
+test_expect_success 'updates branches on other lines of descent' '
214
+ test_when_finished "rm -rf repo" &&
215
+ git init repo &&
216
+ (
217
+ cd repo &&
218
+ test_commit base &&
219
+ test_commit target &&
220
+ git branch theirs &&
221
+ test_commit ours &&
222
+ git switch theirs &&
223
+ test_commit theirs &&
224
+
225
+ expect_graph --branches <<-\EOF &&
226
+ * theirs
227
+ | * ours
228
+ |/
229
+ * target
230
+ * base
231
+ EOF
232
+
233
+ git history drop target &&
234
+
235
+ expect_graph --branches <<-\EOF
236
+ * ours
237
+ | * theirs
238
+ |/
239
+ * base
240
+ EOF
241
+ )
242
+'
243
+
244
+test_expect_success 'moves branch pointing at dropped commit to its parent' '
245
+ test_when_finished "rm -rf repo" &&
246
+ git init repo --initial-branch=main &&
247
+ (
248
+ cd repo &&
249
+ test_commit first &&
250
+ test_commit second &&
251
+ git branch points-at-second &&
252
+ test_commit third &&
253
+
254
+ git rev-parse first >expect &&
255
+ git history drop second &&
256
+ git rev-parse points-at-second >actual &&
257
+ test_cmp expect actual &&
258
+
259
+ expect_log --format="%s %D" --branches <<-\EOF
260
+ third HEAD -> main
261
+ first tag: first, points-at-second
262
+ EOF
263
+ )
264
+'
265
+
266
+test_expect_success '--dry-run prints ref updates without modifying repo' '
267
+ test_when_finished "rm -rf repo" &&
268
+ git init repo --initial-branch=main &&
269
+ (
270
+ cd repo &&
271
+ test_commit base &&
272
+ git branch branch &&
273
+ test_commit middle &&
274
+ test_commit ours &&
275
+ git switch branch &&
276
+ test_commit theirs &&
277
+
278
+ git refs list >refs-expect &&
279
+ git history drop --dry-run main~ >updates &&
280
+ git refs list >refs-actual &&
281
+ test_cmp refs-expect refs-actual &&
282
+ test_grep "update refs/heads/main" updates &&
283
+
284
+ git update-ref --stdin <updates &&
285
+ expect_log main <<-\EOF
286
+ ours
287
+ base
288
+ EOF
289
+ )
290
+'
291
+
292
+test_expect_success '--dry-run detects conflicts with modified working tree' '
293
+ test_when_finished "rm -rf repo" &&
294
+ git init repo --initial-branch=main &&
295
+ (
296
+ cd repo &&
297
+ test_commit first &&
298
+ test_commit second modify-me &&
299
+ echo modified >modify-me &&
300
+
301
+ git refs list >refs-expect &&
302
+ git diff >diff-expect &&
303
+ test_must_fail git history drop --dry-run HEAD 2>err &&
304
+ test_grep "dropping this commit would overwrite local changes" err &&
305
+ git diff >diff-actual &&
306
+ git refs list >refs-actual &&
307
+
308
+ test_cmp diff-expect diff-actual &&
309
+ test_cmp refs-expect refs-actual
310
+ )
311
+'
312
+
313
+test_expect_success '--update-refs=head updates only HEAD' '
314
+ test_when_finished "rm -rf repo" &&
315
+ git init repo --initial-branch=main &&
316
+ (
317
+ cd repo &&
318
+ test_commit base &&
319
+ test_commit target &&
320
+ git branch theirs &&
321
+ test_commit ours &&
322
+ git switch theirs &&
323
+ test_commit theirs &&
324
+
325
+ # When told to update HEAD only, the command refuses to
326
+ # rewrite commits that are not an ancestor of HEAD.
327
+ test_must_fail git history drop --update-refs=head main 2>err &&
328
+ test_grep "rewritten commit must be an ancestor of HEAD" err &&
329
+
330
+ expect_graph --branches <<-\EOF &&
331
+ * theirs
332
+ | * ours
333
+ |/
334
+ * target
335
+ * base
336
+ EOF
337
+
338
+ git switch main &&
339
+ git history drop --update-refs=head target &&
340
+
341
+ expect_graph --branches <<-\EOF
342
+ * ours
343
+ | * theirs
344
+ | * target
345
+ |/
346
+ * base
347
+ EOF
348
+ )
349
+'
350
+
351
+test_expect_success '--update-refs=head can rewrite detached HEAD' '
352
+ test_when_finished "rm -rf repo" &&
353
+ git init repo --initial-branch=main &&
354
+ (
355
+ cd repo &&
356
+ test_commit first &&
357
+ test_commit second &&
358
+ test_commit third &&
359
+ git switch --detach HEAD &&
360
+
361
+ git history drop --update-refs=head second &&
362
+
363
+ expect_log HEAD <<-\EOF &&
364
+ third
365
+ first
366
+ EOF
367
+ expect_log main <<-\EOF
368
+ third
369
+ second
370
+ first
371
+ EOF
372
+ )
373
+'
374
+
375
+test_expect_success 'conflict with replayed commit aborts cleanly' '
376
+ test_when_finished "rm -rf repo" &&
377
+ git init repo &&
378
+ (
379
+ cd repo &&
380
+ test_commit base &&
381
+ test_commit conflict-a file &&
382
+ test_commit conflict-b file &&
383
+
384
+ git refs list >refs-expect &&
385
+ test_must_fail git history drop HEAD~ 2>err &&
386
+ test_grep "failed replaying descendants" err &&
387
+ git refs list >refs-actual &&
388
+ test_cmp refs-expect refs-actual
389
+ )
390
+'
391
+
392
+# Build a history where a descendant of the drop target reverts the change
393
+# introduced by the drop target. After dropping, the descendant's diff applies
394
+# against a tree that already lacks the change, so it becomes empty.
395
+setup_empty_descendant_repo () {
396
+ git init "$1" &&
397
+ (
398
+ cd "$1" &&
399
+ echo C1 >file &&
400
+ git add file &&
401
+ git commit -m "base" &&
402
+ git tag base &&
403
+ echo C2 >file &&
404
+ git add file &&
405
+ git commit -m "drop-me" &&
406
+ git tag drop-me &&
407
+ test_commit middle &&
408
+ echo C1 >file &&
409
+ git add file &&
410
+ git commit -m "revert-drop-me" &&
411
+ git tag revert-drop-me
412
+ )
413
+}
414
+
415
+test_expect_success '--empty=drop drops descendants that become empty' '
416
+ test_when_finished "rm -rf repo" &&
417
+ setup_empty_descendant_repo repo &&
418
+ (
419
+ cd repo &&
420
+
421
+ git history drop --empty=drop drop-me &&
422
+
423
+ expect_log <<-\EOF
424
+ middle
425
+ base
426
+ EOF
427
+ )
428
+'
429
+
430
+test_expect_success '--empty=keep keeps descendants that become empty' '
431
+ test_when_finished "rm -rf repo" &&
432
+ setup_empty_descendant_repo repo &&
433
+ (
434
+ cd repo &&
435
+
436
+ git history drop --empty=keep drop-me &&
437
+
438
+ expect_log <<-\EOF &&
439
+ revert-drop-me
440
+ middle
441
+ base
442
+ EOF
443
+ git diff HEAD~ HEAD >diff &&
444
+ test_must_be_empty diff
445
+ )
446
+'
447
+
448
+test_expect_success '--empty=abort errors out when a descendant becomes empty' '
449
+ test_when_finished "rm -rf repo" &&
450
+ setup_empty_descendant_repo repo &&
451
+ (
452
+ cd repo &&
453
+
454
+ test_must_fail git history drop --empty=abort drop-me 2>err &&
455
+ test_grep "became empty after replay" err
456
+ )
457
+'
458
+
459
+test_expect_success 'updates index and worktree when HEAD moves' '
460
+ test_when_finished "rm -rf repo" &&
461
+ git init repo &&
462
+ (
463
+ cd repo &&
464
+ test_commit first &&
465
+ test_commit second &&
466
+ test_commit third &&
467
+
468
+ git history drop second &&
469
+
470
+ # Worktree should no longer contain second.t.
471
+ test_path_is_missing second.t &&
472
+ test_path_is_file first.t &&
473
+ test_path_is_file third.t &&
474
+
475
+ # Index and worktree should both match the new HEAD.
476
+ git status --porcelain --untracked-files=no >status &&
477
+ test_must_be_empty status
478
+ )
479
+'
480
+
481
+test_expect_success 'updates worktree when dropping HEAD itself' '
482
+ test_when_finished "rm -rf repo" &&
483
+ git init repo &&
484
+ (
485
+ cd repo &&
486
+ test_commit first &&
487
+ test_commit second &&
488
+
489
+ git history drop HEAD &&
490
+
491
+ test_path_is_missing second.t &&
492
+ test_path_is_file first.t &&
493
+
494
+ git status --porcelain --untracked-files=no >status &&
495
+ test_must_be_empty status
496
+ )
497
+'
498
+
499
+test_expect_success 'preserves unrelated unstaged modifications' '
500
+ test_when_finished "rm -rf repo" &&
501
+ git init repo &&
502
+ (
503
+ cd repo &&
504
+ test_commit first &&
505
+ echo first-content >unrelated.txt &&
506
+ git add unrelated.txt &&
507
+ git commit -m "add unrelated" &&
508
+ test_commit second &&
509
+ test_commit third &&
510
+
511
+ echo locally-modified >unrelated.txt &&
512
+
513
+ git diff >diff-expect &&
514
+ git history drop second &&
515
+ git diff >diff-actual &&
516
+ test_cmp diff-expect diff-actual &&
517
+ test_path_is_missing second.t
518
+ )
519
+'
520
+
521
+test_expect_success 'preserves unrelated staged changes' '
522
+ test_when_finished "rm -rf repo" &&
523
+ git init repo &&
524
+ (
525
+ cd repo &&
526
+ test_commit first &&
527
+ echo first-content >unrelated.txt &&
528
+ git add unrelated.txt &&
529
+ git commit -m "add unrelated" &&
530
+ test_commit second &&
531
+ test_commit third &&
532
+
533
+ echo staged-change >unrelated.txt &&
534
+ git add unrelated.txt &&
535
+
536
+ git diff --cached >diff-expect &&
537
+ git history drop second &&
538
+ git diff --cached >diff-actual &&
539
+ test_cmp diff-expect diff-actual &&
540
+ test_path_is_missing second.t
541
+ )
542
+'
543
+
544
+test_expect_success 'aborts when local modifications would be overwritten' '
545
+ test_when_finished "rm -rf repo" &&
546
+ git init repo &&
547
+ (
548
+ cd repo &&
549
+ test_commit base &&
550
+ test_commit conflict &&
551
+
552
+ echo local-edit >conflict.t &&
553
+ git diff >diff-expect &&
554
+ test_must_fail git history drop HEAD 2>err &&
555
+ test_grep "would overwrite local changes" err &&
556
+ git diff >diff-actual &&
557
+ test_cmp diff-expect diff-actual
558
+ )
559
+'
560
+
561
+test_done