#!/bin/sh

test_description='git repack --geometric works correctly'

. ./test-lib.sh

GIT_TEST_MULTI_PACK_INDEX=0

objdir=.git/objects
packdir=$objdir/pack
midx=$objdir/pack/multi-pack-index

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

test_expect_success '--geometric with no packs' '
	git init geometric &&
	test_when_finished "rm -fr geometric" &&
	(
		cd geometric &&

		git repack --write-midx --geometric 2 >out &&
		test_grep "Nothing new to pack" out
	)
'

test_expect_success '--geometric with one pack' '
	git init geometric &&
	test_when_finished "rm -fr geometric" &&
	(
		cd geometric &&

		test_commit "base" &&
		git repack -d &&

		git repack --geometric 2 >out &&

		test_grep "Nothing new to pack" out
	)
'

test_expect_success '--geometric with an intact progression' '
	git init geometric &&
	test_when_finished "rm -fr geometric" &&
	(
		cd geometric &&

		# These packs already form a geometric progression.
		test_commit_bulk --start=1 1 && # 3 objects
		test_commit_bulk --start=2 2 && # 6 objects
		test_commit_bulk --start=4 4 && # 12 objects

		find $objdir/pack -name "*.pack" | sort >expect &&
		git repack --geometric 2 -d &&
		find $objdir/pack -name "*.pack" | sort >actual &&

		test_cmp expect actual
	)
'

test_expect_success '--geometric with loose objects' '
	git init geometric &&
	test_when_finished "rm -fr geometric" &&
	(
		cd geometric &&

		# These packs already form a geometric progression.
		test_commit_bulk --start=1 1 && # 3 objects
		test_commit_bulk --start=2 2 && # 6 objects
		# The loose objects are packed together, breaking the
		# progression.
		test_commit loose && # 3 objects

		find $objdir/pack -name "*.pack" | sort >before &&
		git repack --geometric 2 -d &&
		find $objdir/pack -name "*.pack" | sort >after &&

		comm -13 before after >new &&
		comm -23 before after >removed &&

		test_line_count = 1 new &&
		test_must_be_empty removed &&

		git repack --geometric 2 -d &&
		find $objdir/pack -name "*.pack" | sort >after &&

		# The progression (3, 3, 6) is combined into one new pack.
		test_line_count = 1 after
	)
'

test_expect_success '--geometric with small-pack rollup' '
	git init geometric &&
	test_when_finished "rm -fr geometric" &&
	(
		cd geometric &&

		test_commit_bulk --start=1 1 && # 3 objects
		test_commit_bulk --start=2 1 && # 3 objects
		find $objdir/pack -name "*.pack" | sort >small &&
		test_commit_bulk --start=3 4 && # 12 objects
		test_commit_bulk --start=7 8 && # 24 objects
		find $objdir/pack -name "*.pack" | sort >before &&

		git repack --geometric 2 -d &&

		# Three packs in total; two of the existing large ones, and one
		# new one.
		find $objdir/pack -name "*.pack" | sort >after &&
		test_line_count = 3 after &&
		comm -3 small before | tr -d "\t" >large &&
		grep -qFf large after
	)
'

test_expect_success '--geometric with small- and large-pack rollup' '
	git init geometric &&
	test_when_finished "rm -fr geometric" &&
	(
		cd geometric &&

		# size(small1) + size(small2) > size(medium) / 2
		test_commit_bulk --start=1 1 && # 3 objects
		test_commit_bulk --start=2 1 && # 3 objects
		test_commit_bulk --start=2 3 && # 7 objects
		test_commit_bulk --start=6 9 && # 27 objects &&

		find $objdir/pack -name "*.pack" | sort >before &&

		git repack --geometric 2 -d &&

		find $objdir/pack -name "*.pack" | sort >after &&
		comm -12 before after >untouched &&

		# Two packs in total; the largest pack from before running "git
		# repack", and one new one.
		test_line_count = 1 untouched &&
		test_line_count = 2 after
	)
'

test_expect_success '--geometric ignores kept packs' '
	git init geometric &&
	test_when_finished "rm -fr geometric" &&
	(
		cd geometric &&

		test_commit kept && # 3 objects
		test_commit pack && # 3 objects

		KEPT=$(git pack-objects --revs $objdir/pack/pack <<-EOF
		refs/tags/kept
		EOF
		) &&
		PACK=$(git pack-objects --revs $objdir/pack/pack <<-EOF
		refs/tags/pack
		^refs/tags/kept
		EOF
		) &&

		# neither pack contains more than twice the number of objects in
		# the other, so they should be combined. but, marking one as
		# .kept on disk will "freeze" it, so the pack structure should
		# remain unchanged.
		touch $objdir/pack/pack-$KEPT.keep &&

		find $objdir/pack -name "*.pack" | sort >before &&
		git repack --geometric 2 -d &&
		find $objdir/pack -name "*.pack" | sort >after &&

		# both packs should still exist
		test_path_is_file $objdir/pack/pack-$KEPT.pack &&
		test_path_is_file $objdir/pack/pack-$PACK.pack &&

		# and no new packs should be created
		test_cmp before after &&

		# Passing --pack-kept-objects causes packs with a .keep file to
		# be repacked, too.
		git repack --geometric 2 -d --pack-kept-objects &&

		# After repacking, two packs remain: one new one (containing the
		# objects in both the .keep and non-kept pack), and the .keep
		# pack (since `--pack-kept-objects -d` does not actually delete
		# the kept pack).
		find $objdir/pack -name "*.pack" >after &&
		test_line_count = 2 after
	)
'

test_expect_success '--geometric ignores --keep-pack packs' '
	git init geometric &&
	test_when_finished "rm -fr geometric" &&
	(
		cd geometric &&

		# Create two equal-sized packs
		test_commit kept && # 3 objects
		git repack -d &&
		test_commit pack && # 3 objects
		git repack -d &&

		find $objdir/pack -type f -name "*.pack" | sort >packs.before &&
		git repack --geometric 2 -dm \
			--keep-pack="$(basename "$(head -n 1 packs.before)")" >out &&
		find $objdir/pack -type f -name "*.pack" | sort >packs.after &&

		# Packs should not have changed (only one non-kept pack, no
		# loose objects), but $midx should now exist.
		grep "Nothing new to pack" out &&
		test_path_is_file $midx &&

		test_cmp packs.before packs.after &&

		git fsck
	)
'

test_expect_success '--geometric chooses largest MIDX preferred pack' '
	git init geometric &&
	test_when_finished "rm -fr geometric" &&
	(
		cd geometric &&

		# These packs already form a geometric progression.
		test_commit_bulk --start=1 1 && # 3 objects
		test_commit_bulk --start=2 2 && # 6 objects
		ls $objdir/pack/pack-*.idx >before &&
		test_commit_bulk --start=4 4 && # 12 objects
		ls $objdir/pack/pack-*.idx >after &&

		git repack --geometric 2 -dbm &&

		comm -3 before after | xargs -n 1 basename >expect &&
		test-tool read-midx --preferred-pack $objdir >actual &&

		test_cmp expect actual
	)
'

test_expect_success '--geometric with pack.packSizeLimit' '
	git init pack-rewrite &&
	test_when_finished "rm -fr pack-rewrite" &&
	(
		cd pack-rewrite &&

		test-tool genrandom foo 1048576 >foo &&
		test-tool genrandom bar 1048576 >bar &&

		git add foo bar &&
		test_tick &&
		git commit -m base &&

		git rev-parse HEAD:foo HEAD:bar >p1.objects &&
		git rev-parse HEAD HEAD^{tree} >p2.objects &&

		# These two packs each contain two objects, so the following
		# `--geometric` repack will try to combine them.
		p1="$(git pack-objects $packdir/pack <p1.objects)" &&
		p2="$(git pack-objects $packdir/pack <p2.objects)" &&

		# Remove any loose objects in packs, since we do not want extra
		# copies around (which would mask over potential object
		# corruption issues).
		git prune-packed &&

		# Both p1 and p2 will be rolled up, but pack-objects will write
		# three packs:
		#
		#   - one containing object "foo",
		#   - another containing object "bar",
		#   - a final pack containing the commit and tree objects
		#     (identical to p2 above)
		git repack --geometric 2 -d --max-pack-size=1048576 &&

		# Ensure `repack` can detect that the third pack it wrote
		# (containing just the tree and commit objects) was identical to
		# one that was below the geometric split, so that we can save it
		# from deletion.
		#
		# If `repack` fails to do that, we will incorrectly delete p2,
		# causing object corruption.
		git fsck
	)
'

test_expect_success '--geometric --write-midx retains up-to-date MIDX without bitmap index' '
	test_when_finished "rm -fr repo" &&
	git init repo &&
	(
		cd repo &&
		test_commit initial &&

		test_path_is_missing .git/objects/pack/multi-pack-index &&
		git repack --geometric=2 --write-midx --no-write-bitmap-index &&
		test_path_is_file .git/objects/pack/multi-pack-index &&
		test-tool chmtime =0 .git/objects/pack/multi-pack-index &&

		ls -l .git/objects/pack/ >expect &&
		git repack --geometric=2 --write-midx --no-write-bitmap-index &&
		ls -l .git/objects/pack/ >actual &&
		test_cmp expect actual
	)
'

test_expect_success '--geometric --write-midx retains up-to-date MIDX with bitmap index' '
	test_when_finished "rm -fr repo" &&
	git init repo &&
	test_commit -C repo initial &&

	test_path_is_missing repo/.git/objects/pack/multi-pack-index &&
	git -C repo repack --geometric=2 --write-midx --write-bitmap-index &&
	test_path_is_file repo/.git/objects/pack/multi-pack-index &&
	test-tool chmtime =0 repo/.git/objects/pack/multi-pack-index &&

	ls -l repo/.git/objects/pack/ >expect &&
	git -C repo repack --geometric=2 --write-midx --write-bitmap-index &&
	ls -l repo/.git/objects/pack/ >actual &&
	test_cmp expect actual
'

test_expect_success '--geometric --write-midx with packfiles in main and alternate ODB' '
	test_when_finished "rm -fr shared member" &&

	# Create a shared repository that will serve as the alternate object
	# database for the member linked to it. It has got some objects on its
	# own that are packed into a single packfile.
	git init shared &&
	test_commit -C shared common-object &&
	git -C shared repack -ad &&

	# We create member so that its alternates file points to the shared
	# repository. We then create a commit in it so that git-repack(1) has
	# something to repack.
	# of the shared object database.
	git clone --shared shared member &&
	test_commit -C member unique-object &&
	git -C member repack --geometric=2 --write-midx 2>err &&
	test_must_be_empty err &&

	# We should see that a new packfile was generated.
	find shared/.git/objects/pack -type f -name "*.pack" >packs &&
	test_line_count = 1 packs &&

	# We should also see a multi-pack-index. This multi-pack-index should
	# never refer to any packfiles in the alternate object database.
	test_path_is_file member/.git/objects/pack/multi-pack-index &&
	test-tool read-midx member/.git/objects >packs.midx &&
	grep "^pack-.*\.idx$" packs.midx | sort >actual &&
	basename member/.git/objects/pack/pack-*.idx >expect &&
	test_cmp expect actual
'

test_expect_success '--geometric --with-midx with no local objects' '
	test_when_finished "rm -fr shared member" &&

	# Create a repository with a single packfile that acts as alternate
	# object database.
	git init shared &&
	test_commit -C shared "shared-objects" &&
	git -C shared repack -ad &&

	# Create a second repository linked to the first one and perform a
	# geometric repack on it.
	git clone --shared shared member &&
	git -C member repack --geometric 2 --write-midx 2>err &&
	test_must_be_empty err &&

	# Assert that we wrote neither a new packfile nor a multi-pack-index.
	# We should not have a packfile because the single packfile in the
	# alternate object database does not invalidate the geometric sequence.
	# And we should not have a multi-pack-index because these only index
	# local packfiles, and there are none.
	test_dir_is_empty member/$packdir
'

test_expect_success '--geometric with same pack in main and alternate ODB' '
	test_when_finished "rm -fr shared member" &&

	# Create a repository with a single packfile that acts as alternate
	# object database.
	git init shared &&
	test_commit -C shared "shared-objects" &&
	git -C shared repack -ad &&

	# We create the member repository as an exact copy so that it has the
	# same packfile.
	cp -r shared member &&
	test-tool path-utils real_path shared/.git/objects >member/.git/objects/info/alternates &&
	find shared/.git/objects -type f >expected-files &&

	# Verify that we can repack objects as expected without observing any
	# error. Having the same packfile in both ODBs used to cause an error
	# in git-pack-objects(1).
	git -C member repack --geometric 2 2>err &&
	test_must_be_empty err &&
	# Nothing should have changed.
	find shared/.git/objects -type f >actual-files &&
	test_cmp expected-files actual-files
'

test_expect_success '--geometric -l with non-intact geometric sequence across ODBs' '
	test_when_finished "rm -fr shared member" &&

	git init shared &&
	test_commit_bulk -C shared --start=1 1 &&

	git clone --shared shared member &&
	test_commit_bulk -C member --start=2 1 &&

	# Verify that our assumptions actually hold: both generated packfiles
	# should have three objects and should be non-equal.
	packed_objects shared/.git/objects/pack/pack-*.idx >shared-objects &&
	packed_objects member/.git/objects/pack/pack-*.idx >member-objects &&
	test_line_count = 3 shared-objects &&
	test_line_count = 3 member-objects &&
	! test_cmp shared-objects member-objects &&

	# Perform the geometric repack. With `-l`, we should only see the local
	# packfile and thus arrive at the conclusion that the geometric
	# sequence is intact. We thus expect no changes.
	#
	# Note that we are tweaking mtimes of the packfiles so that we can
	# verify they did not change. This is done in order to detect the case
	# where we do repack objects, but the resulting packfile is the same.
	test-tool chmtime --verbose =0 member/.git/objects/pack/* >expected-member-packs &&
	git -C member repack --geometric=2 -l -d &&
	test-tool chmtime --verbose member/.git/objects/pack/* >actual-member-packs &&
	test_cmp expected-member-packs actual-member-packs &&

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

	# On the other hand, when doing a non-local geometric repack we should
	# see both packfiles and thus repack them. We expect that the shared
	# object database was not changed.
	test-tool chmtime --verbose =0 shared/.git/objects/pack/* >expected-shared-packs &&
	git -C member repack --geometric=2 -d &&
	test-tool chmtime --verbose shared/.git/objects/pack/* >actual-shared-packs &&
	test_cmp expected-shared-packs actual-shared-packs &&

	# Furthermore, we expect that the member repository now has a single
	# packfile that contains the combined shared and non-shared objects.
	ls member/.git/objects/pack/pack-*.idx >actual &&
	test_line_count = 1 actual &&
	packed_objects member/.git/objects/pack/pack-*.idx >actual-objects &&
	test_line_count = 6 actual-objects &&
	test_cmp expected-objects actual-objects
'

test_expect_success '--geometric -l disables writing bitmaps with non-local packfiles' '
	test_when_finished "rm -fr shared member" &&

	git init shared &&
	test_commit_bulk -C shared --start=1 1 &&

	git clone --shared shared member &&
	test_commit_bulk -C member --start=2 1 &&

	# When performing a geometric repack with `-l` while connected to an
	# alternate object database that has a packfile we do not have full
	# coverage of objects. As a result, we expect that writing the bitmap
	# will be disabled.
	git -C member repack -l --geometric=2 --write-midx --write-bitmap-index 2>err &&
	cat >expect <<-EOF &&
	warning: disabling bitmap writing, as some objects are not being packed
	EOF
	test_cmp expect err &&
	test_path_is_missing member/.git/objects/pack/multi-pack-index-*.bitmap &&

	# On the other hand, when we repack without `-l`, we should see that
	# the bitmap gets created.
	git -C member repack --geometric=2 --write-midx --write-bitmap-index 2>err &&
	test_must_be_empty err &&
	test_path_is_file member/.git/objects/pack/multi-pack-index-*.bitmap
'

write_packfile () {
	NR="$1"
	PREFIX="$2"

	printf "blob\ndata <<EOB\n$PREFIX %s\nEOB\n" $(test_seq $NR) |
		git fast-import &&
	git pack-objects --pack-loose-unreachable .git/objects/pack/pack &&
	git prune-packed
}

write_promisor_packfile () {
	PACKFILE=$(write_packfile "$@") &&
	touch .git/objects/pack/pack-$PACKFILE.promisor &&
	echo "$PACKFILE"
}

test_expect_success 'geometric repack works with promisor packs' '
	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 &&

		# Packs A and B need to be merged.
		NORMAL_A=$(write_packfile 2 normal-a) &&
		NORMAL_B=$(write_packfile 2 normal-b) &&
		NORMAL_C=$(write_packfile 14 normal-c) &&

		# Packs A, B and C need to be merged.
		PROMISOR_A=$(write_promisor_packfile 1 promisor-a) &&
		PROMISOR_B=$(write_promisor_packfile 3 promisor-b) &&
		PROMISOR_C=$(write_promisor_packfile 3 promisor-c) &&
		PROMISOR_D=$(write_promisor_packfile 20 promisor-d) &&
		PROMISOR_E=$(write_promisor_packfile 40 promisor-e) &&

		git cat-file --batch-all-objects --batch-check="%(objectname)" >objects-expect &&

		ls .git/objects/pack/*.pack >packs-before &&
		test_line_count = 8 packs-before &&
		git repack --geometric=2 -d &&
		ls .git/objects/pack/*.pack >packs-after &&
		test_line_count = 5 packs-after &&
		test_grep ! "$NORMAL_A" packs-after &&
		test_grep ! "$NORMAL_B" packs-after &&
		test_grep "$NORMAL_C" packs-after &&
		test_grep ! "$PROMISOR_A" packs-after &&
		test_grep ! "$PROMISOR_B" packs-after &&
		test_grep ! "$PROMISOR_C" packs-after &&
		test_grep "$PROMISOR_D" packs-after &&
		test_grep "$PROMISOR_E" packs-after &&

		ls .git/objects/pack/*.promisor >promisors &&
		test_line_count = 3 promisors &&

		git cat-file --batch-all-objects --batch-check="%(objectname)" >objects-actual &&
		test_cmp objects-expect objects-actual
	)
'

test_done
