blob: 68a86d1e370a94b11c0e9fb29400ca0f11590cc6 [file] [edit]
#!/bin/sh
test_description='tests for git-history drop subcommand'
. ./test-lib.sh
. "$TEST_DIRECTORY/lib-log-graph.sh"
expect_graph () {
cat >expect &&
lib_test_cmp_graph --format=%s "$@"
}
expect_log () {
git log --format="%s" "$@" >actual &&
cat >expect &&
test_cmp expect actual
}
test_expect_success 'errors on missing commit argument' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit initial &&
test_must_fail git history drop 2>err &&
test_grep "command expects a single revision" err
)
'
test_expect_success 'errors on too many arguments' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit initial &&
test_must_fail git history drop HEAD HEAD 2>err &&
test_grep "command expects a single revision" err
)
'
test_expect_success 'errors on unknown revision' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit initial &&
test_must_fail git history drop does-not-exist 2>err &&
test_grep "commit cannot be found: does-not-exist" err
)
'
test_expect_success 'errors with invalid --empty= value' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit initial &&
test_commit second &&
test_must_fail git history drop --empty=bogus HEAD 2>err &&
test_grep "unrecognized.*--empty.*bogus" err
)
'
test_expect_success 'drops a commit in the middle and replays descendants' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit first &&
test_commit second &&
test_commit third &&
git symbolic-ref HEAD >expect &&
git history drop HEAD~ &&
git symbolic-ref HEAD >actual &&
test_cmp expect actual &&
expect_log <<-\EOF &&
third
first
EOF
test_must_fail git show HEAD:second.t &&
test_path_is_missing second.t &&
git reflog >reflog &&
test_grep "drop: dropping HEAD~" reflog
)
'
test_expect_success 'drops the HEAD commit' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit first &&
test_commit second &&
git history drop HEAD &&
expect_log <<-\EOF
first
EOF
)
'
test_expect_success 'drops a commit on detached HEAD' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit first &&
test_commit second &&
test_commit third &&
git checkout --detach HEAD &&
git history drop HEAD~ &&
expect_log <<-\EOF
third
first
EOF
)
'
# Note: in this case it would actually be fine to drop the root commit, as we
# do have a descendant commit, and no reference points to the root commit
# directly. So this is something that we may relax eventually.
test_expect_success 'refuses to drop the root commit' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit first &&
test_commit second &&
test_must_fail git history drop HEAD~ 2>err &&
test_grep "cannot drop root commit" err
)
'
# In contrast to the above case, we actually don't want to drop the root commit
# here as that would cause us to end up with an empty commit graph.
test_expect_success 'refuses to drop the root commit when branch becomes empty' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit first &&
test_must_fail git history drop HEAD 2>err &&
test_grep "cannot drop root commit" err
)
'
test_expect_success 'refuses to drop a merge commit' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit base &&
git branch branch &&
test_commit ours &&
git switch branch &&
test_commit theirs &&
git switch - &&
git merge theirs &&
test_must_fail git history drop HEAD 2>err &&
test_grep "cannot drop merge commit" err
)
'
test_expect_success 'refuses when descendants contain a merge commit' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit base &&
test_commit middle &&
git branch branch &&
test_commit ours &&
git switch branch &&
test_commit theirs &&
git switch - &&
git merge theirs &&
test_must_fail git history drop middle 2>err &&
test_grep "replaying merge commits is not supported yet" err
)
'
test_expect_success 'works in a bare repository' '
test_when_finished "rm -rf repo repo.git" &&
git init repo &&
test_commit -C repo first &&
test_commit -C repo second &&
test_commit -C repo third &&
git clone --bare repo repo.git &&
(
cd repo.git &&
git history drop HEAD~ &&
expect_log <<-\EOF
third
first
EOF
)
'
test_expect_success 'updates branches on other lines of descent' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit base &&
test_commit target &&
git branch theirs &&
test_commit ours &&
git switch theirs &&
test_commit theirs &&
expect_graph --branches <<-\EOF &&
* theirs
| * ours
|/
* target
* base
EOF
git history drop target &&
expect_graph --branches <<-\EOF
* ours
| * theirs
|/
* base
EOF
)
'
test_expect_success 'moves branch pointing at dropped commit to its parent' '
test_when_finished "rm -rf repo" &&
git init repo --initial-branch=main &&
(
cd repo &&
test_commit first &&
test_commit second &&
git branch points-at-second &&
test_commit third &&
git rev-parse first >expect &&
git history drop second &&
git rev-parse points-at-second >actual &&
test_cmp expect actual &&
expect_log --format="%s %D" --branches <<-\EOF
third HEAD -> main
first tag: first, points-at-second
EOF
)
'
test_expect_success '--dry-run prints ref updates without modifying repo' '
test_when_finished "rm -rf repo" &&
git init repo --initial-branch=main &&
(
cd repo &&
test_commit base &&
git branch branch &&
test_commit middle &&
test_commit ours &&
git switch branch &&
test_commit theirs &&
git refs list >refs-expect &&
git history drop --dry-run main~ >updates &&
git refs list >refs-actual &&
test_cmp refs-expect refs-actual &&
test_grep "update refs/heads/main" updates &&
git update-ref --stdin <updates &&
expect_log main <<-\EOF
ours
base
EOF
)
'
test_expect_success '--dry-run detects conflicts with modified working tree' '
test_when_finished "rm -rf repo" &&
git init repo --initial-branch=main &&
(
cd repo &&
test_commit first &&
test_commit second modify-me &&
echo modified >modify-me &&
git refs list >refs-expect &&
git diff >diff-expect &&
test_must_fail git history drop --dry-run HEAD 2>err &&
test_grep "dropping this commit would overwrite local changes" err &&
git diff >diff-actual &&
git refs list >refs-actual &&
test_cmp diff-expect diff-actual &&
test_cmp refs-expect refs-actual
)
'
test_expect_success '--update-refs=head updates only HEAD' '
test_when_finished "rm -rf repo" &&
git init repo --initial-branch=main &&
(
cd repo &&
test_commit base &&
test_commit target &&
git branch theirs &&
test_commit ours &&
git switch theirs &&
test_commit theirs &&
# When told to update HEAD only, the command refuses to
# rewrite commits that are not an ancestor of HEAD.
test_must_fail git history drop --update-refs=head main 2>err &&
test_grep "rewritten commit must be an ancestor of HEAD" err &&
expect_graph --branches <<-\EOF &&
* theirs
| * ours
|/
* target
* base
EOF
git switch main &&
git history drop --update-refs=head target &&
expect_graph --branches <<-\EOF
* ours
| * theirs
| * target
|/
* base
EOF
)
'
test_expect_success '--update-refs=head can rewrite detached HEAD' '
test_when_finished "rm -rf repo" &&
git init repo --initial-branch=main &&
(
cd repo &&
test_commit first &&
test_commit second &&
test_commit third &&
git switch --detach HEAD &&
git history drop --update-refs=head second &&
expect_log HEAD <<-\EOF &&
third
first
EOF
expect_log main <<-\EOF
third
second
first
EOF
)
'
test_expect_success 'conflict with replayed commit aborts cleanly' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit base &&
test_commit conflict-a file &&
test_commit conflict-b file &&
git refs list >refs-expect &&
test_must_fail git history drop HEAD~ 2>err &&
test_grep "failed replaying descendants" err &&
git refs list >refs-actual &&
test_cmp refs-expect refs-actual
)
'
# Build a history where a descendant of the drop target reverts the change
# introduced by the drop target. After dropping, the descendant's diff applies
# against a tree that already lacks the change, so it becomes empty.
setup_empty_descendant_repo () {
git init "$1" &&
(
cd "$1" &&
echo C1 >file &&
git add file &&
git commit -m "base" &&
git tag base &&
echo C2 >file &&
git add file &&
git commit -m "drop-me" &&
git tag drop-me &&
test_commit middle &&
echo C1 >file &&
git add file &&
git commit -m "revert-drop-me" &&
git tag revert-drop-me
)
}
test_expect_success '--empty=drop drops descendants that become empty' '
test_when_finished "rm -rf repo" &&
setup_empty_descendant_repo repo &&
(
cd repo &&
git history drop --empty=drop drop-me &&
expect_log <<-\EOF
middle
base
EOF
)
'
test_expect_success '--empty=keep keeps descendants that become empty' '
test_when_finished "rm -rf repo" &&
setup_empty_descendant_repo repo &&
(
cd repo &&
git history drop --empty=keep drop-me &&
expect_log <<-\EOF &&
revert-drop-me
middle
base
EOF
git diff HEAD~ HEAD >diff &&
test_must_be_empty diff
)
'
test_expect_success '--empty=abort errors out when a descendant becomes empty' '
test_when_finished "rm -rf repo" &&
setup_empty_descendant_repo repo &&
(
cd repo &&
test_must_fail git history drop --empty=abort drop-me 2>err &&
test_grep "became empty after replay" err
)
'
test_expect_success 'updates index and worktree when HEAD moves' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit first &&
test_commit second &&
test_commit third &&
git history drop second &&
# Worktree should no longer contain second.t.
test_path_is_missing second.t &&
test_path_is_file first.t &&
test_path_is_file third.t &&
# Index and worktree should both match the new HEAD.
git status --porcelain --untracked-files=no >status &&
test_must_be_empty status
)
'
test_expect_success 'updates worktree when dropping HEAD itself' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit first &&
test_commit second &&
git history drop HEAD &&
test_path_is_missing second.t &&
test_path_is_file first.t &&
git status --porcelain --untracked-files=no >status &&
test_must_be_empty status
)
'
test_expect_success 'preserves unrelated unstaged modifications' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit first &&
echo first-content >unrelated.txt &&
git add unrelated.txt &&
git commit -m "add unrelated" &&
test_commit second &&
test_commit third &&
echo locally-modified >unrelated.txt &&
git diff >diff-expect &&
git history drop second &&
git diff >diff-actual &&
test_cmp diff-expect diff-actual &&
test_path_is_missing second.t
)
'
test_expect_success 'preserves unrelated staged changes' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit first &&
echo first-content >unrelated.txt &&
git add unrelated.txt &&
git commit -m "add unrelated" &&
test_commit second &&
test_commit third &&
echo staged-change >unrelated.txt &&
git add unrelated.txt &&
git diff --cached >diff-expect &&
git history drop second &&
git diff --cached >diff-actual &&
test_cmp diff-expect diff-actual &&
test_path_is_missing second.t
)
'
test_expect_success 'aborts when local modifications would be overwritten' '
test_when_finished "rm -rf repo" &&
git init repo &&
(
cd repo &&
test_commit base &&
test_commit conflict &&
echo local-edit >conflict.t &&
git diff >diff-expect &&
test_must_fail git history drop HEAD 2>err &&
test_grep "would overwrite local changes" err &&
git diff >diff-actual &&
test_cmp diff-expect diff-actual
)
'
test_done