Raw
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