#!/bin/sh

test_description='pack-objects --stdin'
GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME

. ./test-lib.sh

packed_objects () {
	git show-index <"$1" >tmp-object-list &&
	cut -d' ' -f2 tmp-object-list | sort &&
	rm tmp-object-list
 }

test_expect_success 'setup for --stdin-packs tests' '
	git init stdin-packs &&
	git -C stdin-packs config set maintenance.auto false &&
	(
		cd stdin-packs &&

		test_commit A &&
		test_commit B &&
		test_commit C &&

		for id in A B C
		do
			git pack-objects .git/objects/pack/pack-$id \
				--incremental --revs <<-EOF || exit 1
			refs/tags/$id
			EOF
		done &&

		ls -la .git/objects/pack
	)
'

test_expect_success '--stdin-packs with excluded packs' '
	(
		cd stdin-packs &&

		PACK_A="$(basename .git/objects/pack/pack-A-*.pack)" &&
		PACK_B="$(basename .git/objects/pack/pack-B-*.pack)" &&
		PACK_C="$(basename .git/objects/pack/pack-C-*.pack)" &&

		git pack-objects test --stdin-packs <<-EOF &&
		$PACK_A
		^$PACK_B
		$PACK_C
		EOF

		(
			git show-index <$(ls .git/objects/pack/pack-A-*.idx) &&
			git show-index <$(ls .git/objects/pack/pack-C-*.idx)
		) >expect.raw &&
		git show-index <$(ls test-*.idx) >actual.raw &&

		cut -d" " -f2 <expect.raw | sort >expect &&
		cut -d" " -f2 <actual.raw | sort >actual &&
		test_cmp expect actual
	)
'

test_expect_success '--stdin-packs is incompatible with --filter' '
	(
		cd stdin-packs &&
		test_must_fail git pack-objects --stdin-packs --stdout \
			--filter=blob:none </dev/null 2>err &&
		test_grep "options .--stdin-packs. and .--filter. cannot be used together" err
	)
'

test_expect_success '--stdin-packs is incompatible with --revs' '
	(
		cd stdin-packs &&
		test_must_fail git pack-objects --stdin-packs --revs out \
			</dev/null 2>err &&
		test_grep "cannot use internal rev list with --stdin-packs" err
	)
'

test_expect_success '--stdin-packs with loose objects' '
	(
		cd stdin-packs &&

		PACK_A="$(basename .git/objects/pack/pack-A-*.pack)" &&
		PACK_B="$(basename .git/objects/pack/pack-B-*.pack)" &&
		PACK_C="$(basename .git/objects/pack/pack-C-*.pack)" &&

		test_commit D && # loose

		git pack-objects test2 --stdin-packs --unpacked <<-EOF &&
		$PACK_A
		^$PACK_B
		$PACK_C
		EOF

		(
			git show-index <$(ls .git/objects/pack/pack-A-*.idx) &&
			git show-index <$(ls .git/objects/pack/pack-C-*.idx) &&
			git rev-list --objects --no-object-names \
				refs/tags/C..refs/tags/D

		) >expect.raw &&
		ls -la . &&
		git show-index <$(ls test2-*.idx) >actual.raw &&

		cut -d" " -f2 <expect.raw | sort >expect &&
		cut -d" " -f2 <actual.raw | sort >actual &&
		test_cmp expect actual
	)
'

test_expect_success '--stdin-packs with broken links' '
	(
		cd stdin-packs &&

		# make an unreachable object with a bogus parent
		git cat-file -p HEAD >commit &&
		sed "s/$(git rev-parse HEAD^)/$(test_oid zero)/" <commit |
		git hash-object -w -t commit --stdin >in &&

		git pack-objects .git/objects/pack/pack-D <in &&

		PACK_A="$(basename .git/objects/pack/pack-A-*.pack)" &&
		PACK_B="$(basename .git/objects/pack/pack-B-*.pack)" &&
		PACK_C="$(basename .git/objects/pack/pack-C-*.pack)" &&
		PACK_D="$(basename .git/objects/pack/pack-D-*.pack)" &&

		git pack-objects test3 --stdin-packs --unpacked <<-EOF &&
		$PACK_A
		^$PACK_B
		$PACK_C
		$PACK_D
		EOF

		(
			git show-index <$(ls .git/objects/pack/pack-A-*.idx) &&
			git show-index <$(ls .git/objects/pack/pack-C-*.idx) &&
			git show-index <$(ls .git/objects/pack/pack-D-*.idx) &&
			git rev-list --objects --no-object-names \
				refs/tags/C..refs/tags/D
		) >expect.raw &&
		git show-index <$(ls test3-*.idx) >actual.raw &&

		cut -d" " -f2 <expect.raw | sort >expect &&
		cut -d" " -f2 <actual.raw | sort >actual &&
		test_cmp expect actual
	)
'

test_expect_success 'pack-objects --stdin with duplicate packfile' '
	test_when_finished "rm -fr repo" &&

	git init repo &&
	(
		cd repo &&
		test_commit "commit" &&
		git repack -ad &&

		{
			basename .git/objects/pack/pack-*.pack &&
			basename .git/objects/pack/pack-*.pack
		} >packfiles &&

		git pack-objects --stdin-packs generated-pack <packfiles &&
		packed_objects .git/objects/pack/pack-*.idx >expect &&
		packed_objects generated-pack-*.idx >actual &&
		test_cmp expect actual
	)
'

test_expect_success 'pack-objects --stdin with same packfile excluded and included' '
	test_when_finished "rm -fr repo" &&

	git init repo &&
	(
		cd repo &&
		test_commit "commit" &&
		git repack -ad &&

		{
			basename .git/objects/pack/pack-*.pack &&
			printf "^%s\n" "$(basename .git/objects/pack/pack-*.pack)"
		} >packfiles &&

		git pack-objects --stdin-packs generated-pack <packfiles &&
		packed_objects generated-pack-*.idx >packed-objects &&
		test_must_be_empty packed-objects
	)
'

test_expect_success 'pack-objects --stdin with packfiles from alternate object database' '
	test_when_finished "rm -fr shared member" &&

	# Set up a shared repository with a single packfile.
	git init shared &&
	test_commit -C shared "shared-objects" &&
	git -C shared repack -ad &&
	basename shared/.git/objects/pack/pack-*.pack >packfile &&

	# Set up a repository that is connected to the shared repository. This
	# repository has no objects on its own, but we still expect to be able
	# to pack objects from its alternate.
	git clone --shared shared member &&
	git -C member pack-objects --stdin-packs generated-pack <packfile &&
	test_cmp shared/.git/objects/pack/pack-*.pack member/generated-pack-*.pack
'

test_expect_success 'pack-objects --stdin with packfiles from main and alternate object database' '
	test_when_finished "rm -fr shared member" &&

	# Set up a shared repository with a single packfile.
	git init shared &&
	test_commit -C shared "shared-commit" &&
	git -C shared repack -ad &&

	# Set up a repository that is connected to the shared repository. This
	# repository has a second packfile so that we can verify that it is
	# possible to write packs that include packfiles from different object
	# databases.
	git clone --shared shared member &&
	test_commit -C member "local-commit" &&
	git -C member repack -dl &&

	{
		basename shared/.git/objects/pack/pack-*.pack &&
		basename member/.git/objects/pack/pack-*.pack
	} >packfiles &&

	{
		packed_objects shared/.git/objects/pack/pack-*.idx &&
		packed_objects member/.git/objects/pack/pack-*.idx
	} | sort >expected-objects &&

	git -C member pack-objects --stdin-packs generated-pack <packfiles &&
	packed_objects member/generated-pack-*.idx >actual-objects &&
	test_cmp expected-objects actual-objects
'

objdir=.git/objects
packdir=$objdir/pack

objects_in_packs () {
	for p in "$@"
	do
		git show-index <"$packdir/pack-$p.idx" || return 1
	done >objects.raw &&

	cut -d' ' -f2 objects.raw | sort &&
	rm -f objects.raw
}

test_expect_success '--stdin-packs=follow walks into unknown packs' '
	test_when_finished "rm -fr repo" &&

	git init repo &&
	(
		cd repo &&
		git config set maintenance.auto false &&

		for c in A B C D
		do
			test_commit "$c" || return 1
		done &&

		A="$(echo A | git pack-objects --revs $packdir/pack)" &&
		B="$(echo A..B | git pack-objects --revs $packdir/pack)" &&
		C="$(echo B..C | git pack-objects --revs $packdir/pack)" &&
		D="$(echo C..D | git pack-objects --revs $packdir/pack)" &&
		test_commit E &&

		git prune-packed &&

		cat >in <<-EOF &&
		pack-$B.pack
		^pack-$C.pack
		pack-$D.pack
		EOF

		# With just --stdin-packs, pack "A" is unknown to us, so
		# only objects from packs "B" and "D" are included in
		# the output pack.
		P=$(git pack-objects --stdin-packs $packdir/pack <in) &&
		objects_in_packs $B $D >expect &&
		objects_in_packs $P >actual &&
		test_cmp expect actual &&

		# But with --stdin-packs=follow, objects from both
		# included packs reach objects from the unknown pack, so
		# objects from pack "A" is included in the output pack
		# in addition to the above.
		P=$(git pack-objects --stdin-packs=follow $packdir/pack <in) &&
		objects_in_packs $A $B $D >expect &&
		objects_in_packs $P >actual &&
		test_cmp expect actual &&

		# And with --unpacked, we will pick up objects from unknown
		# packs that are reachable from loose objects. Loose object E
		# reaches objects in pack A, but there are three excluded packs
		# in between.
		#
		# The resulting pack should include objects reachable from E
		# that are not present in packs B, C, or D, along with those
		# present in pack A.
		cat >in <<-EOF &&
		^pack-$B.pack
		^pack-$C.pack
		^pack-$D.pack
		EOF

		P=$(git pack-objects --stdin-packs=follow --unpacked \
			$packdir/pack <in) &&

		{
			objects_in_packs $A &&
			git rev-list --objects --no-object-names D..E
		}>expect.raw &&
		sort expect.raw >expect &&
		objects_in_packs $P >actual &&
		test_cmp expect actual
	)
'

test_expect_success '--stdin-packs with promisors' '
	test_when_finished "rm -fr repo" &&
	git init repo &&
	(
		cd repo &&
		git config set maintenance.auto false &&
		git remote add promisor garbage &&
		git config set remote.promisor.promisor true &&

		for c in A B C D
		do
			echo "$c" >file &&
			git add file &&
			git commit --message "$c" &&
			git tag "$c" || return 1
		done &&

		A="$(echo A | git pack-objects --revs $packdir/pack)" &&
		B="$(echo A..B | git pack-objects --revs $packdir/pack --filter=blob:none)" &&
		C="$(echo B..C | git pack-objects --revs $packdir/pack)" &&
		D="$(echo C..D | git pack-objects --revs $packdir/pack)" &&
		touch $packdir/pack-$B.promisor &&

		test_must_fail git pack-objects --stdin-packs --exclude-promisor-objects pack- 2>err <<-EOF &&
			pack-$B.pack
		EOF
		test_grep "is a promisor but --exclude-promisor-objects was given" err &&

		PACK=$(git pack-objects --stdin-packs=follow --exclude-promisor-objects $packdir/pack <<-EOF
			pack-$D.pack
			EOF
		) &&
		objects_in_packs $C $D >expect &&
		objects_in_packs $PACK >actual &&
		test_cmp expect actual &&
		rm -f $packdir/pack-$PACK.*
	)
'

test_expect_success '--stdin-packs does not perform backfill fetch' '
	test_when_finished "rm -rf remote client" &&

	git init remote &&
	test_commit_bulk -C remote 10 &&
	git -C remote config set --local uploadpack.allowfilter 1 &&
	git -C remote config set --local uploadpack.allowanysha1inwant 1 &&

	git clone --filter=tree:0 "file://$(pwd)/remote" client &&
	(
		cd client &&
		ls .git/objects/pack/*.promisor | sed "s|.*/||; s/\.promisor$/.pack/" >packs &&
		test_line_count -gt 1 packs &&
		GIT_TRACE2_EVENT="$(pwd)/event.log" git pack-objects --stdin-packs pack <packs &&
		test_grep ! "\"event\":\"child_start\"" event.log
	)
'

stdin_packs__follow_with_only () {
	rm -fr stdin_packs__follow_with_only &&
	git init stdin_packs__follow_with_only &&
	(
		cd stdin_packs__follow_with_only &&

		test_commit A &&
		test_commit B &&

		git rev-parse "$@" >B.objects &&

		echo A | git pack-objects --revs $packdir/pack &&
		B="$(git pack-objects $packdir/pack <B.objects)" &&

		git cat-file --batch-check="%(objectname)" --batch-all-objects >objs &&
		for obj in $(cat objs)
		do
			rm -f $objdir/$(test_oid_to_path $obj) || return 1
		done &&

		( cd $packdir && ls pack-*.pack ) >in &&
		git pack-objects --stdin-packs=follow --stdout >/dev/null <in
	)
}

test_expect_success '--stdin-packs=follow tolerates missing blobs' '
	stdin_packs__follow_with_only HEAD HEAD^{tree}
'

test_expect_success '--stdin-packs=follow tolerates missing trees' '
	stdin_packs__follow_with_only HEAD HEAD:B.t
'

test_expect_success '--stdin-packs=follow tolerates missing commits' '
	stdin_packs__follow_with_only HEAD HEAD^{tree}
'

test_expect_success '--stdin-packs=follow with open-excluded packs' '
	test_when_finished "rm -fr repo" &&

	git init repo &&
	(
		cd repo &&
		git config set maintenance.auto false &&

		git branch -M main &&

		# Create the following commit structure:
		#
		#   A <-- B <-- D     (main)
		#         ^
		#          \
		#           C        (other)
		test_commit A &&
		test_commit B &&
		git checkout -B other &&
		test_commit C &&
		git checkout main &&
		test_commit D &&

		A="$(echo A | git pack-objects --revs $packdir/pack)" &&
		B="$(echo A..B | git pack-objects --revs $packdir/pack)" &&
		C="$(echo B..C | git pack-objects --revs $packdir/pack)" &&
		D="$(echo B..D | git pack-objects --revs $packdir/pack)" &&

		C_ONLY="$(git rev-parse other | git pack-objects $packdir/pack)" &&

		git prune-packed &&

		# Create a pack using --stdin-packs=follow where:
		#
		#  - pack D is included,
		#  - pack C_ONLY is excluded, but open,
		#  - pack B is excluded, but closed, and
		#  - packs A and C are unknown
		#
		# The resulting pack should therefore contain:
		#
		#  - objects from the included pack D,
		#  - A.t (rescued via D^{tree}), and
		#  - C^{tree} and C.t (rescued via pack C_ONLY)
		#
		# , but should omit:
		#
		#  - C (excluded via C_ONLY),
		#  - objects from pack B (trivially excluded-closed)
		#  - A and A^{tree} (ancestors of B)
		P=$(git pack-objects --stdin-packs=follow $packdir/pack <<-EOF
		pack-$D.pack
		!pack-$C_ONLY.pack
		^pack-$B.pack
		EOF
		) &&

		{
			objects_in_packs $D &&
			git rev-parse A:A.t "C^{tree}" C:C.t
		} >expect.raw &&
		sort expect.raw >expect &&

		objects_in_packs $P >actual &&
		test_cmp expect actual
	)
'

test_expect_success '--stdin-packs with !-delimited pack without follow' '
	test_when_finished "rm -fr repo" &&

	git init repo &&
	(
		test_commit A &&
		test_commit B &&
		test_commit C &&

		A="$(echo A | git pack-objects --revs $packdir/pack)" &&
		B="$(echo A..B | git pack-objects --revs $packdir/pack)" &&
		C="$(echo B..C | git pack-objects --revs $packdir/pack)" &&

		cat >in <<-EOF &&
		!pack-$A.pack
		pack-$B.pack
		pack-$C.pack
		EOF

		# Without --stdin-packs=follow, we treat the first
		# line of input as a literal packfile name, and thus
		# expect pack-objects to complain of a missing pack
		test_must_fail git pack-objects --stdin-packs --stdout \
			>/dev/null <in 2>err &&
		test_grep "could not find pack .!pack-$A.pack." err &&

		# With --stdin-packs=follow, we treat the second line
		# of input as indicating pack-$A.pack is an excluded
		# open pack, and thus expect pack-objects to succeed
		P=$(git pack-objects --stdin-packs=follow $packdir/pack <in) &&

		objects_in_packs $B $C >expect &&
		objects_in_packs $P >actual &&
		test_cmp expect actual
	)
'

test_done
