#!/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
