#!/bin/sh

test_description='migration of ref storage backends'

GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME

. ./test-lib.sh

print_all_reflog_entries () {
	repo=$1 &&
	test-tool -C "$repo" ref-store main for-each-reflog >reflogs &&
	while read reflog
	do
		echo "REFLOG: $reflog" &&
		test-tool -C "$repo" ref-store main for-each-reflog-ent "$reflog" ||
		return 1
	done <reflogs
}

# Migrate the provided repository from one format to the other and
# verify that the references and logs are migrated over correctly.
# Usage: test_migration <repo> <format> [<skip_reflog_verify> [<options...>]]
#   <repo> is the relative path to the repo to be migrated.
#   <format> is the ref format to be migrated to.
#   <skip_reflog_verify> (default: false) whether to skip reflog verification.
#   <options...> are other options be passed directly to 'git refs migrate'.
test_migration () {
	repo=$1 &&
	format=$2 &&
	shift 2 &&
	skip_reflog_verify=false &&
	if test $# -ge 1
	then
		skip_reflog_verify=$1
		shift
	fi &&
	git -C "$repo" for-each-ref --include-root-refs \
		--format='%(refname) %(objectname) %(symref)' >expect &&
	if ! $skip_reflog_verify
	then
		print_all_reflog_entries "$repo" >expect_logs
	fi &&

	git -C "$repo" refs migrate --ref-format="$format" "$@" &&

	git -C "$repo" for-each-ref --include-root-refs \
		--format='%(refname) %(objectname) %(symref)' >actual &&
	test_cmp expect actual &&
	if ! $skip_reflog_verify
	then
		print_all_reflog_entries "$repo" >actual_logs &&
		test_cmp expect_logs actual_logs
	fi &&

	git -C "$repo" rev-parse --show-ref-format >actual &&
	echo "$format" >expect &&
	test_cmp expect actual
}

test_expect_success 'setup' '
	rm -rf .git
'

test_expect_success "superfluous arguments" '
	test_when_finished "rm -rf repo" &&
	git init repo &&
	test_must_fail git -C repo refs migrate foo 2>err &&
	cat >expect <<-EOF &&
	usage: too many arguments
	EOF
	test_cmp expect err
'

test_expect_success "missing ref storage format" '
	test_when_finished "rm -rf repo" &&
	git init repo &&
	test_must_fail git -C repo refs migrate 2>err &&
	cat >expect <<-EOF &&
	usage: missing --ref-format=<format>
	EOF
	test_cmp expect err
'

test_expect_success "unknown ref storage format" '
	test_when_finished "rm -rf repo" &&
	git init repo &&
	test_must_fail git -C repo refs migrate \
		--ref-format=unknown 2>err &&
	cat >expect <<-EOF &&
	error: unknown ref storage format ${SQ}unknown${SQ}
	EOF
	test_cmp expect err
'

ref_formats="files reftable"
for from_format in $ref_formats
do
	for to_format in $ref_formats
	do
		if test "$from_format" = "$to_format"
		then
			continue
		fi

		test_expect_success "$from_format: migration to same format fails" '
			test_when_finished "rm -rf repo" &&
			git init --ref-format=$from_format repo &&
			test_must_fail git -C repo refs migrate \
				--ref-format=$from_format 2>err &&
			cat >expect <<-EOF &&
			error: repository already uses ${SQ}$from_format${SQ} format
			EOF
			test_cmp expect err
		'

		test_expect_success "$from_format -> $to_format: migration with worktree fails" '
			test_when_finished "rm -rf repo" &&
			git init --ref-format=$from_format repo &&
			git -C repo worktree add wt &&
			test_must_fail git -C repo refs migrate \
				--ref-format=$to_format 2>err &&
			cat >expect <<-EOF &&
			error: migrating repositories with worktrees is not supported yet
			EOF
			test_cmp expect err
		'

		test_expect_success "$from_format -> $to_format: unborn HEAD" '
			test_when_finished "rm -rf repo" &&
			git init --ref-format=$from_format repo &&
			test_migration repo "$to_format"
		'

		test_expect_success "$from_format -> $to_format: single ref" '
			test_when_finished "rm -rf repo" &&
			git init --ref-format=$from_format repo &&
			test_commit -C repo initial &&
			test_migration repo "$to_format"
		'

		test_expect_success "$from_format -> $to_format: bare repository" '
			test_when_finished "rm -rf repo repo.git" &&
			git init --ref-format=$from_format repo &&
			test_commit -C repo initial &&
			git clone --ref-format=$from_format --mirror repo repo.git &&
			test_migration repo.git "$to_format"
		'

		test_expect_success "$from_format -> $to_format: dangling symref" '
			test_when_finished "rm -rf repo" &&
			git init --ref-format=$from_format repo &&
			test_commit -C repo initial &&
			git -C repo symbolic-ref BROKEN_HEAD refs/heads/nonexistent &&
			test_migration repo "$to_format" &&
			echo refs/heads/nonexistent >expect &&
			git -C repo symbolic-ref BROKEN_HEAD >actual &&
			test_cmp expect actual
		'

		test_expect_success "$from_format -> $to_format: broken ref" '
			test_when_finished "rm -rf repo" &&
			git init --ref-format=$from_format repo &&
			test_commit -C repo initial &&
			test-tool -C repo ref-store main update-ref "" refs/heads/broken \
				"$(test_oid 001)" "$ZERO_OID" REF_SKIP_CREATE_REFLOG,REF_SKIP_OID_VERIFICATION &&
			test_migration repo "$to_format" true &&
			test_oid 001 >expect &&
			git -C repo rev-parse refs/heads/broken >actual &&
			test_cmp expect actual
		'

		test_expect_success "$from_format -> $to_format: pseudo-refs" '
			test_when_finished "rm -rf repo" &&
			git init --ref-format=$from_format repo &&
			test_commit -C repo initial &&
			git -C repo update-ref FOO_HEAD HEAD &&
			test_migration repo "$to_format"
		'

		test_expect_success "$from_format -> $to_format: special refs are left alone" '
			test_when_finished "rm -rf repo" &&
			git init --ref-format=$from_format repo &&
			test_commit -C repo initial &&
			git -C repo rev-parse HEAD >repo/.git/MERGE_HEAD &&
			git -C repo rev-parse MERGE_HEAD &&
			test_migration repo "$to_format" &&
			test_path_is_file repo/.git/MERGE_HEAD
		'

		test_expect_success "$from_format -> $to_format: a bunch of refs" '
			test_when_finished "rm -rf repo" &&
			git init --ref-format=$from_format repo &&

			test_commit -C repo initial &&
			cat >input <<-EOF &&
			create FOO_HEAD HEAD
			create refs/heads/branch-1 HEAD
			create refs/heads/branch-2 HEAD
			create refs/heads/branch-3 HEAD
			create refs/heads/branch-4 HEAD
			create refs/tags/tag-1 HEAD
			create refs/tags/tag-2 HEAD
			EOF
			git -C repo update-ref --stdin <input &&
			test_migration repo "$to_format"
		'

		test_expect_success "$from_format -> $to_format: dry-run migration does not modify repository" '
			test_when_finished "rm -rf repo" &&
			git init --ref-format=$from_format repo &&
			test_commit -C repo initial &&
			git -C repo refs migrate --dry-run \
				--ref-format=$to_format >output &&
			grep "Finished dry-run migration of refs" output &&
			test_path_is_dir repo/.git/ref_migration.* &&
			echo $from_format >expect &&
			git -C repo rev-parse --show-ref-format >actual &&
			test_cmp expect actual
		'

		test_expect_success "$from_format -> $to_format: reflogs of symrefs with target deleted" '
			test_when_finished "rm -rf repo" &&
			git init --ref-format=$from_format repo &&
			test_commit -C repo initial &&
			git -C repo branch branch-1 HEAD &&
			git -C repo symbolic-ref refs/heads/symref refs/heads/branch-1 &&
			cat >input <<-EOF &&
			delete refs/heads/branch-1
			EOF
			git -C repo update-ref --stdin <input &&
			test_migration repo "$to_format"
		'

		test_expect_success "$from_format -> $to_format: reflogs order is retained" '
			test_when_finished "rm -rf repo" &&
			git init --ref-format=$from_format repo &&
			test_commit --date "100005000 +0700" --no-tag -C repo initial &&
			test_commit --date "100003000 +0700" --no-tag -C repo second &&
			test_migration repo "$to_format"
		'

		test_expect_success "$from_format -> $to_format: stash is retained" '
			test_when_finished "rm -rf repo" &&
			git init --ref-format=$from_format repo &&
			(
				cd repo &&
				test_commit initial A &&
				echo foo >A &&
				git stash push &&
				echo bar >A &&
				git stash push &&
				git stash list >expect.reflog &&
				test_migration . "$to_format" &&
				git stash list >actual.reflog &&
				test_cmp expect.reflog actual.reflog
			)
		'

		test_expect_success "$from_format -> $to_format: skip reflog with --skip-reflog" '
			test_when_finished "rm -rf repo" &&
			git init --ref-format=$from_format repo &&
			test_commit -C repo initial &&
			# we see that the repository contains reflogs.
			git -C repo reflog --all >reflogs &&
			test_line_count = 2 reflogs &&
			test_migration repo "$to_format" true --no-reflog &&
			# there should be no reflogs post migration.
			git -C repo reflog --all >reflogs &&
			test_must_be_empty reflogs
		'
	done
done

test_expect_success 'multiple reftable blocks with multiple entries' '
	test_when_finished "rm -rf repo" &&
	git init --ref-format=files repo &&
	test_commit -C repo first &&
	test_seq -f "create refs/heads/ref-%d HEAD" 5000 |
	git -C repo update-ref --stdin &&
	test_commit -C repo second &&
	test_seq -f "update refs/heads/ref-%d HEAD" 3000 |
	git -C repo update-ref --stdin &&
	test_migration repo reftable true
'

test_expect_success 'migrating from files format deletes backend files' '
	test_when_finished "rm -rf repo" &&
	git init --ref-format=files repo &&
	test_commit -C repo first &&
	git -C repo pack-refs --all &&
	test_commit -C repo second &&
	git -C repo update-ref ORIG_HEAD HEAD &&
	git -C repo rev-parse HEAD >repo/.git/FETCH_HEAD &&

	test_path_is_file repo/.git/HEAD &&
	test_path_is_file repo/.git/ORIG_HEAD &&
	test_path_is_file repo/.git/refs/heads/main &&
	test_path_is_file repo/.git/packed-refs &&

	test_migration repo reftable &&

	echo "ref: refs/heads/.invalid" >expect &&
	test_cmp expect repo/.git/HEAD &&
	echo "this repository uses the reftable format" >expect &&
	test_cmp expect repo/.git/refs/heads &&
	test_path_is_file repo/.git/FETCH_HEAD &&
	test_path_is_missing repo/.git/ORIG_HEAD &&
	test_path_is_missing repo/.git/refs/heads/main &&
	test_path_is_missing repo/.git/logs &&
	test_path_is_missing repo/.git/packed-refs
'

test_expect_success 'migrating from reftable format deletes backend files' '
	test_when_finished "rm -rf repo" &&
	git init --ref-format=reftable repo &&
	test_commit -C repo first &&

	test_path_is_dir repo/.git/reftable &&
	test_migration repo files &&

	test_path_is_missing repo/.git/reftable &&
	echo "ref: refs/heads/main" >expect &&
	test_cmp expect repo/.git/HEAD &&
	test_path_is_file repo/.git/packed-refs
'

test_done
