#!/bin/sh

test_description='git repack works correctly'

. ./test-lib.sh

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

test_expect_success '--expire-to stores pruned objects (now)' '
	git init expire-to-now &&
	(
		cd expire-to-now &&

		git branch -M main &&

		test_commit base &&

		git checkout -b cruft &&
		test_commit --no-tag cruft &&

		git rev-list --objects --no-object-names main..cruft >moved.raw &&
		sort moved.raw >moved.want &&

		git rev-list --all --objects --no-object-names >expect.raw &&
		sort expect.raw >expect &&

		git checkout main &&
		git branch -D cruft &&
		git reflog expire --all --expire=all &&

		git init --bare expired.git &&
		git repack -d \
			--cruft --cruft-expiration="now" \
			--expire-to="expired.git/objects/pack/pack" &&

		expired="$(ls expired.git/objects/pack/pack-*.idx)" &&
		test_path_is_file "${expired%.idx}.mtimes" &&

		# Since the `--cruft-expiration` is "now", the effective
		# behavior is to move _all_ unreachable objects out to
		# the location in `--expire-to`.
		git show-index <$expired >expired.raw &&
		cut -d" " -f2 expired.raw | sort >expired.objects &&
		git rev-list --all --objects --no-object-names \
			>remaining.objects &&

		# ...in other words, the combined contents of this
		# repository and expired.git should be the same as the
		# set of objects we started with.
		sort expired.objects remaining.objects >actual &&
		test_cmp expect actual &&

		# The "moved" objects (i.e., those in expired.git)
		# should be the same as the cruft objects which were
		# expired in the previous step.
		test_cmp moved.want expired.objects
	)
'

test_expect_success '--expire-to stores pruned objects (5.minutes.ago)' '
	git init expire-to-5.minutes.ago &&
	(
		cd expire-to-5.minutes.ago &&

		git branch -M main &&

		test_commit base &&

		# Create two classes of unreachable objects, one which
		# is older than 5 minutes (stale), and another which is
		# newer (recent).
		for kind in stale recent
		do
			git checkout -b $kind main &&
			test_commit --no-tag $kind || return 1
		done &&

		git rev-list --objects --no-object-names main..stale >in &&
		stale="$(git pack-objects $objdir/pack/pack <in)" &&
		mtime="$(test-tool chmtime --get =-600 $objdir/pack/pack-$stale.pack)" &&

		# expect holds the set of objects we expect to find in
		# this repository after repacking
		git rev-list --objects --no-object-names recent >expect.raw &&
		sort expect.raw >expect &&

		# moved.want holds the set of objects we expect to find
		# in expired.git
		git rev-list --objects --no-object-names main..stale >out &&
		sort out >moved.want &&

		git checkout main &&
		git branch -D stale recent &&
		git reflog expire --all --expire=all &&
		git prune-packed &&

		git init --bare expired.git &&
		git repack -d \
			--cruft --cruft-expiration=5.minutes.ago \
			--expire-to="expired.git/objects/pack/pack" &&

		# Some of the remaining objects in this repository are
		# unreachable, so use `cat-file --batch-all-objects`
		# instead of `rev-list` to get their names
		git cat-file --batch-all-objects --batch-check="%(objectname)" \
			>remaining.objects &&
		sort remaining.objects >actual &&
		test_cmp expect actual &&

		(
			cd expired.git &&

			expired="$(ls objects/pack/pack-*.mtimes)" &&
			test-tool pack-mtimes $(basename $expired) >out &&
			cut -d" " -f1 out | sort >../moved.got &&

			# Ensure that there are as many objects with the
			# expected mtime as were moved to expired.git.
			#
			# In other words, ensure that the recorded
			# mtimes of any moved objects was written
			# correctly.
			grep " $mtime$" out >matching &&
			test_line_count = $(wc -l <../moved.want) matching
		) &&
		test_cmp moved.want moved.got
	)
'

generate_random_blob() {
	test-tool genrandom "$@" >blob &&
	git hash-object -w -t blob blob &&
	rm blob
}

pack_random_blob () {
	generate_random_blob "$@" &&
	git repack -d -q >/dev/null
}

generate_cruft_pack () {
	pack_random_blob "$@" >/dev/null &&

	ls $packdir/pack-*.pack | xargs -n 1 basename >in &&
	pack="$(git pack-objects --cruft $packdir/pack <in)" &&
	git prune-packed &&

	echo "$packdir/pack-$pack.mtimes"
}

test_expect_success '--max-cruft-size creates new packs when too large' '
	git init max-cruft-size-large &&
	(
		cd max-cruft-size-large &&
		test_commit base &&

		foo="$(pack_random_blob foo $((1*1024*1024)))" &&
		git repack --cruft -d &&
		cruft_foo="$(ls $packdir/pack-*.mtimes)" &&

		bar="$(pack_random_blob bar $((1*1024*1024)))" &&
		git repack --cruft -d --max-cruft-size=1M &&
		cruft_bar="$(ls $packdir/pack-*.mtimes | grep -v $cruft_foo)" &&

		test-tool pack-mtimes $(basename "$cruft_foo") >foo.objects &&
		test-tool pack-mtimes $(basename "$cruft_bar") >bar.objects &&

		grep "^$foo" foo.objects &&
		test_line_count = 1 foo.objects &&
		grep "^$bar" bar.objects &&
		test_line_count = 1 bar.objects
	)
'

test_expect_success '--max-cruft-size combines existing packs when not too large' '
	git init max-cruft-size-small &&
	(
		cd max-cruft-size-small &&
		test_commit base &&

		foo="$(pack_random_blob foo $((1*1024*1024)))" &&
		git repack --cruft -d &&

		bar="$(pack_random_blob bar $((1*1024*1024)))" &&
		git repack --cruft -d --max-cruft-size=10M &&

		cruft=$(ls $packdir/pack-*.mtimes) &&
		test-tool pack-mtimes $(basename "$cruft") >cruft.objects &&

		grep "^$foo" cruft.objects &&
		grep "^$bar" cruft.objects &&
		test_line_count = 2 cruft.objects
	)
'

test_expect_success '--combine-cruft-below-size combines packs' '
	repo=combine-cruft-below-size &&
	test_when_finished "rm -fr $repo" &&

	git init "$repo" &&
	(
		cd "$repo" &&

		test_commit base &&
		git repack -ad &&

		cruft_foo="$(generate_cruft_pack foo 524288)" &&    # 0.5 MiB
		cruft_bar="$(generate_cruft_pack bar 524288)" &&    # 0.5 MiB
		cruft_baz="$(generate_cruft_pack baz 1048576)" &&   # 1.0 MiB
		cruft_quux="$(generate_cruft_pack quux 1572864)" && # 1.5 MiB

		test-tool pack-mtimes "$(basename $cruft_foo)" >expect.raw &&
		test-tool pack-mtimes "$(basename $cruft_bar)" >>expect.raw &&
		sort expect.raw >expect.objects &&

		# Repacking with `--combine-cruft-below-size=1M`
		# should combine both 0.5 MiB packs together, but
		# ignore the two packs which are >= 1.0 MiB.
		ls $packdir/pack-*.mtimes | sort >cruft.before &&
		git repack -d --cruft --combine-cruft-below-size=1M &&
		ls $packdir/pack-*.mtimes | sort >cruft.after &&

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

		test_line_count = 1 cruft.new &&
		test_line_count = 2 cruft.removed &&

		# The two packs smaller than 1.0MiB should be repacked
		# together.
		printf "%s\n" $cruft_foo $cruft_bar | sort >expect.removed &&
		test_cmp expect.removed cruft.removed &&

		# ...and contain the set of objects rolled up.
		test-tool pack-mtimes "$(basename $(cat cruft.new))" >actual.raw &&
		sort actual.raw >actual.objects &&

		test_cmp expect.objects actual.objects
	)
'

test_expect_success 'setup cruft with freshened objects' '
	git init cruft-freshen &&
	(
		cd cruft-freshen &&

		test_commit base &&
		git repack -ad &&

		foo="$(generate_random_blob foo 64)" &&
		test-tool chmtime --get -10000 \
			"$objdir/$(test_oid_to_path "$foo")" >foo.mtime &&

		git repack --cruft -d &&

		cruft="$(ls $packdir/pack-*.mtimes)" &&
		test-tool pack-mtimes "$(basename $cruft)" >actual &&
		echo "$foo $(cat foo.mtime)" >expect &&
		test_cmp expect actual
	)
'

test_expect_success 'cruft with freshened objects (loose)' '
	(
		cd cruft-freshen &&

		# regenerate the object, setting its mtime to be more recent
		foo="$(generate_random_blob foo 64)" &&
		test-tool chmtime --get -100 \
			"$objdir/$(test_oid_to_path "$foo")" >foo.mtime &&

		git repack --cruft -d &&

		cruft="$(ls $packdir/pack-*.mtimes)" &&
		test-tool pack-mtimes "$(basename $cruft)" >actual &&
		echo "$foo $(cat foo.mtime)" >expect &&
		test_cmp expect actual
	)
'

test_expect_success 'cruft with freshened objects (packed)' '
	(
		cd cruft-freshen &&

		# regenerate the object and store it in a packfile,
		# setting its mtime to be more recent
		#
		# store it alongside another cruft object so that we
		# do not create an identical copy of the existing
		# cruft pack (which contains $foo).
		foo="$(generate_random_blob foo 64)" &&
		bar="$(generate_random_blob bar 64)" &&
		foo_pack="$(printf "%s\n" $foo $bar | git pack-objects $packdir/pack)" &&
		git prune-packed &&

		test-tool chmtime --get -10 \
			"$packdir/pack-$foo_pack.pack" >foo.mtime &&

		git repack --cruft -d &&

		cruft="$(ls $packdir/pack-*.mtimes)" &&
		test-tool pack-mtimes "$(basename $cruft)" >actual &&
		echo "$foo $(cat foo.mtime)" >expect.raw &&
		echo "$bar $(cat foo.mtime)" >>expect.raw &&
		sort expect.raw >expect &&
		test_cmp expect actual
	)
'

test_expect_success 'multi-cruft with freshened objects (previously cruft)' '
	repo="max-cruft-size-threshold" &&

	test_when_finished "rm -fr $repo" &&
	git init "$repo" &&
	(
		cd "$repo" &&

		test_commit base &&
		foo="$(generate_random_blob foo $((2*1024*1024)))" &&
		bar="$(generate_random_blob bar $((2*1024*1024)))" &&
		baz="$(generate_random_blob baz $((2*1024*1024)))" &&

		test-tool chmtime --get -100000 \
			"$objdir/$(test_oid_to_path "$foo")" >foo.old &&
		test-tool chmtime --get -100000 \
			"$objdir/$(test_oid_to_path "$bar")" >bar.old &&
		test-tool chmtime --get -100000 \
			"$objdir/$(test_oid_to_path "$baz")" >baz.old &&

		git repack --cruft -d &&

		# Make an identical copy of foo stored in a pack with a more
		# recent mtime.
		foo="$(generate_random_blob foo $((2*1024*1024)))" &&
		foo_pack="$(echo "$foo" | git pack-objects $packdir/pack)" &&
		test-tool chmtime --get -100 \
			"$packdir/pack-$foo_pack.pack" >foo.new &&
		git prune-packed &&

		# Make a loose copy of bar, also with a more recent mtime.
		bar="$(generate_random_blob bar $((2*1024*1024)))" &&
		test-tool chmtime --get -100 \
			"$objdir/$(test_oid_to_path "$bar")" >bar.new &&

		# Make a new cruft object $quux to ensure we do not
		# generate an identical pack to the existing cruft
		# pack.
		quux="$(generate_random_blob quux $((1024)))" &&
		test-tool chmtime --get -100 \
			"$objdir/$(test_oid_to_path "$quux")" >quux.new &&

		git repack --cruft --max-cruft-size=3M -d &&

		for p in $packdir/pack-*.mtimes
		do
			test-tool pack-mtimes "$(basename "$p")" || return 1
		done >actual.raw &&
		sort actual.raw >actual &&

		# Among the set of all cruft packs, we should see the
		# new mtimes for object $foo and $bar, as well as the
		# single new copy of $baz.
		sort >expect <<-EOF &&
		$foo $(cat foo.new)
		$bar $(cat bar.new)
		$baz $(cat baz.old)
		$quux $(cat quux.new)
		EOF

		test_cmp expect actual
	)
'

test_expect_success '--max-cruft-size with pruning' '
	git init max-cruft-size-prune &&
	(
		cd max-cruft-size-prune &&

		test_commit base &&
		foo="$(generate_random_blob foo $((1024*1024)))" &&
		bar="$(generate_random_blob bar $((1024*1024)))" &&
		baz="$(generate_random_blob baz $((1024*1024)))" &&

		test-tool chmtime -10000 "$objdir/$(test_oid_to_path "$foo")" &&

		git repack -d --cruft --max-cruft-size=1M &&

		# backdate the mtimes of all cruft packs to validate
		# that they were rewritten as a result of pruning
		ls $packdir/pack-*.mtimes | sort >cruft.before &&
		for cruft in $(cat cruft.before)
		do
			mtime="$(test-tool chmtime --get -10000 "$cruft")" &&
			echo $cruft $mtime >>mtimes || return 1
		done &&

		# repack (and prune) with a --max-cruft-size to ensure
		# that we appropriately split the resulting set of packs
		git repack -d --cruft --max-cruft-size=1M \
			--cruft-expiration=1000.seconds.ago &&
		ls $packdir/pack-*.mtimes | sort >cruft.after &&

		for cruft in $(cat cruft.after)
		do
			old_mtime="$(grep $cruft mtimes | cut -d" " -f2)" &&
			new_mtime="$(test-tool chmtime --get $cruft)" &&
			test $old_mtime -lt $new_mtime || return 1
		done &&

		test_line_count = 3 cruft.before &&
		test_line_count = 2 cruft.after &&
		test_must_fail git cat-file -e $foo &&
		git cat-file -e $bar &&
		git cat-file -e $baz
	)
'

test_expect_success '--max-cruft-size ignores non-local packs' '
	repo="max-cruft-size-non-local" &&
	git init $repo &&
	(
		cd $repo &&
		test_commit base &&
		generate_random_blob foo 64 &&
		git repack --cruft -d
	) &&

	git clone --reference=$repo $repo $repo-alt &&
	(
		cd $repo-alt &&

		test_commit other &&
		generate_random_blob bar 64 &&

		# ensure that we do not attempt to pick up packs from
		# the non-alternated repository, which would result in a
		# crash
		git repack --cruft --max-cruft-size=1M -d
	)
'

test_expect_success 'reachable packs are preferred over cruft ones' '
	repo="cruft-preferred-packs" &&
	git init "$repo" &&
	(
		cd "$repo" &&

		# This test needs to exercise careful control over when a MIDX
		# is and is not written. Unset the corresponding TEST variable
		# accordingly.
		sane_unset GIT_TEST_MULTI_PACK_INDEX &&

		test_commit base &&
		test_commit --no-tag cruft &&

		non_cruft="$(echo base | git pack-objects --revs $packdir/pack)" &&
		# Write a cruft pack which both (a) sorts ahead of the non-cruft
		# pack in lexical order, and (b) has an older mtime to appease
		# the MIDX preferred pack selection routine.
		cruft="$(echo pack-$non_cruft.pack | git pack-objects --cruft $packdir/pack-A)" &&
		test-tool chmtime -1000 $packdir/pack-A-$cruft.pack &&

		test_commit other &&
		git repack -d &&

		git repack --geometric 2 -d --write-midx --write-bitmap-index &&

		# After repacking, there are two packs left: one reachable one
		# (which is the result of combining both of the existing two
		# non-cruft packs), and one cruft pack.
		find .git/objects/pack -type f -name "*.pack" >packs &&
		test_line_count = 2 packs &&

		# Make sure that the pack we just wrote is marked as preferred,
		# not the cruft one.
		pack="$(test-tool read-midx --preferred-pack $objdir)" &&
		test_path_is_missing "$packdir/$(basename "$pack" ".idx").mtimes"
	)
'

test_expect_success 'repack --cruft generates a cruft pack' '
	git init repo &&
	test_when_finished "rm -fr repo" &&
	(
		cd repo &&

		test_commit reachable &&
		git branch -M main &&
		git checkout --orphan other &&
		test_commit unreachable &&

		git checkout main &&
		git branch -D other &&
		git tag -d unreachable &&
		# objects are not cruft if they are contained in the reflogs
		git reflog expire --all --expire=all &&

		git rev-list --objects --all --no-object-names >reachable.raw &&
		git cat-file --batch-all-objects --batch-check="%(objectname)" >objects &&
		sort <reachable.raw >reachable &&
		comm -13 reachable objects >unreachable &&

		git repack --cruft -d &&

		cruft=$(basename $(ls $packdir/pack-*.mtimes) .mtimes) &&
		pack=$(basename $(ls $packdir/pack-*.pack | grep -v $cruft) .pack) &&

		git show-index <$packdir/$pack.idx >actual.raw &&
		cut -f2 -d" " actual.raw | sort >actual &&
		test_cmp reachable actual &&

		git show-index <$packdir/$cruft.idx >actual.raw &&
		cut -f2 -d" " actual.raw | sort >actual &&
		test_cmp unreachable actual
	)
'

test_expect_success 'cruft packs are not included in geometric repack' '
	git init repo &&
	test_when_finished "rm -fr repo" &&
	(
		cd repo &&

		test_commit reachable &&
		git repack -Ad &&
		git branch -M main &&

		git checkout --orphan other &&
		test_commit cruft &&
		git repack -d &&

		git checkout main &&
		git branch -D other &&
		git tag -d cruft &&
		git reflog expire --all --expire=all &&

		git repack --cruft &&

		find $packdir -type f | sort >before &&
		git repack --geometric=2 -d &&
		find $packdir -type f | sort >after &&

		test_cmp before after
	)
'

test_expect_success 'repack --geometric collects once-cruft objects' '
	git init repo &&
	test_when_finished "rm -fr repo" &&
	(
		cd repo &&

		test_commit reachable &&
		git repack -Ad &&
		git branch -M main &&

		git checkout --orphan other &&
		git rm -rf . &&
		test_commit --no-tag cruft &&
		cruft="$(git rev-parse HEAD)" &&

		git checkout main &&
		git branch -D other &&
		git reflog expire --all --expire=all &&

		# Pack the objects created in the previous step into a cruft
		# pack. Intentionally leave loose copies of those objects
		# around so we can pick them up in a subsequent --geometric
		# reapack.
		git repack --cruft &&

		# Now make those objects reachable, and ensure that they are
		# packed into the new pack created via a --geometric repack.
		git update-ref refs/heads/other $cruft &&

		# Without this object, the set of unpacked objects is exactly
		# the set of objects already in the cruft pack. Tweak that set
		# to ensure we do not overwrite the cruft pack entirely.
		test_commit reachable2 &&

		find $packdir -name "pack-*.idx" | sort >before &&
		git repack --geometric=2 -d &&
		find $packdir -name "pack-*.idx" | sort >after &&

		{
			git rev-list --objects --no-object-names $cruft &&
			git rev-list --objects --no-object-names reachable..reachable2
		} >want.raw &&
		sort want.raw >want &&

		pack=$(comm -13 before after) &&
		git show-index <$pack >objects.raw &&

		cut -d" " -f2 objects.raw | sort >got &&

		test_cmp want got
	)
'

test_expect_success 'cruft repack with no reachable objects' '
	git init repo &&
	test_when_finished "rm -fr repo" &&
	(
		cd repo &&

		test_commit base &&
		git repack -ad &&

		base="$(git rev-parse base)" &&

		git for-each-ref --format="delete %(refname)" >in &&
		git update-ref --stdin <in &&
		git reflog expire --all --expire=all &&
		rm -fr .git/index &&

		git repack --cruft -d &&

		git cat-file -t $base
	)
'

find_pack () {
	for idx in $(ls $packdir/pack-*.idx)
	do
		git show-index <$idx >out &&
		if grep -q "$1" out
		then
			echo $idx
		fi || return 1
	done
}

test_expect_success 'cruft repack with --max-pack-size' '
	git init max-pack-size &&
	(
		cd max-pack-size &&
		test_commit base &&

		# two cruft objects which exceed the maximum pack size
		foo=$(generate_random_blob foo 1048576) &&
		bar=$(generate_random_blob bar 1048576) &&
		test-tool chmtime --get -1000 \
			"$objdir/$(test_oid_to_path $foo)" >foo.mtime &&
		test-tool chmtime --get -2000 \
			"$objdir/$(test_oid_to_path $bar)" >bar.mtime &&
		git repack --cruft --max-pack-size=1M &&
		find $packdir -name "*.mtimes" >cruft &&
		test_line_count = 2 cruft &&

		foo_mtimes="$(basename $(find_pack $foo) .idx).mtimes" &&
		bar_mtimes="$(basename $(find_pack $bar) .idx).mtimes" &&
		test-tool pack-mtimes $foo_mtimes >foo.actual &&
		test-tool pack-mtimes $bar_mtimes >bar.actual &&

		echo "$foo $(cat foo.mtime)" >foo.expect &&
		echo "$bar $(cat bar.mtime)" >bar.expect &&

		test_cmp foo.expect foo.actual &&
		test_cmp bar.expect bar.actual &&
		test "$foo_mtimes" != "$bar_mtimes"
	)
'

test_expect_success 'cruft repack with pack.packSizeLimit' '
	(
		cd max-pack-size &&
		# repack everything back together to remove the existing cruft
		# pack (but to keep its objects)
		git repack -adk &&
		git -c pack.packSizeLimit=1M repack --cruft &&
		# ensure the same post condition is met when --max-pack-size
		# would otherwise be inferred from the configuration
		find $packdir -name "*.mtimes" >cruft &&
		test_line_count = 2 cruft &&
		for pack in $(cat cruft)
		do
			test-tool pack-mtimes "$(basename $pack)" >objects &&
			test_line_count = 1 objects || return 1
		done
	)
'

test_expect_success 'cruft repack respects repack.cruftWindow' '
	git init repo &&
	test_when_finished "rm -fr repo" &&
	(
		cd repo &&

		test_commit base &&

		GIT_TRACE2_EVENT=$(pwd)/event.trace \
		git -c pack.window=1 -c repack.cruftWindow=2 repack \
		       --cruft --window=3 &&

		grep "pack-objects.*--window=2.*--cruft" event.trace
	)
'

test_expect_success 'cruft repack respects --window by default' '
	git init repo &&
	test_when_finished "rm -fr repo" &&
	(
		cd repo &&

		test_commit base &&

		GIT_TRACE2_EVENT=$(pwd)/event.trace \
		git -c pack.window=2 repack --cruft --window=3 &&

		grep "pack-objects.*--window=3.*--cruft" event.trace
	)
'

test_expect_success 'cruft repack respects --quiet' '
	git init repo &&
	test_when_finished "rm -fr repo" &&
	(
		cd repo &&

		test_commit base &&
		GIT_PROGRESS_DELAY=0 git repack --cruft --quiet 2>err &&
		test_must_be_empty err
	)
'

setup_cruft_exclude_tests() {
	git init "$1" &&
	(
		cd "$1" &&

		git config repack.midxMustContainCruft false &&

		test_commit one &&

		test_commit --no-tag two &&
		two="$(git rev-parse HEAD)" &&
		test_commit --no-tag three &&
		three="$(git rev-parse HEAD)" &&
		git reset --hard one &&
		git reflog expire --all --expire=all &&

		GIT_TEST_MULTI_PACK_INDEX=0 git repack --cruft -d &&

		git merge $two &&
		test_commit four
	)
}

test_expect_success 'repack --write-midx excludes cruft where possible' '
	setup_cruft_exclude_tests exclude-cruft-when-possible &&
	(
		cd exclude-cruft-when-possible &&

		GIT_TEST_MULTI_PACK_INDEX=0 \
		git repack -d --geometric=2 --write-midx --write-bitmap-index &&

		test-tool read-midx --show-objects $objdir >midx &&
		cruft="$(ls $packdir/*.mtimes)" &&
		test_grep ! "$(basename "$cruft" .mtimes).idx" midx &&

		git rev-list --all --objects --no-object-names >reachable.raw &&
		sort reachable.raw >reachable.objects &&
		awk "/\.pack$/ { print \$1 }" <midx | sort >midx.objects &&

		test_cmp reachable.objects midx.objects
	)
'

test_expect_success 'repack --write-midx includes cruft when instructed' '
	setup_cruft_exclude_tests exclude-cruft-when-instructed &&
	(
		cd exclude-cruft-when-instructed &&

		GIT_TEST_MULTI_PACK_INDEX=0 \
		git -c repack.midxMustContainCruft=true repack \
			-d --geometric=2 --write-midx --write-bitmap-index &&

		test-tool read-midx --show-objects $objdir >midx &&
		cruft="$(ls $packdir/*.mtimes)" &&
		test_grep "$(basename "$cruft" .mtimes).idx" midx &&

		git cat-file --batch-check="%(objectname)" --batch-all-objects \
			>all.objects &&
		awk "/\.pack$/ { print \$1 }" <midx | sort >midx.objects &&

		test_cmp all.objects midx.objects
	)
'

test_expect_success 'repack --write-midx includes cruft when necessary' '
	setup_cruft_exclude_tests exclude-cruft-when-necessary &&
	(
		cd exclude-cruft-when-necessary &&

		test_path_is_file $(ls $packdir/pack-*.mtimes) &&
		( cd $packdir && ls pack-*.idx ) | sort >packs.all &&
		git multi-pack-index write --stdin-packs --bitmap <packs.all &&

		test_commit five &&
		GIT_TEST_MULTI_PACK_INDEX=0 \
		git repack -d --geometric=2 --write-midx --write-bitmap-index &&

		test-tool read-midx --show-objects $objdir >midx &&
		awk "/\.pack$/ { print \$1 }" <midx | sort >midx.objects &&
		git cat-file --batch-all-objects --batch-check="%(objectname)" \
			>expect.objects &&
		test_cmp expect.objects midx.objects &&

		grep "^pack-" midx >midx.packs &&
		test_line_count = "$(($(wc -l <packs.all) + 1))" midx.packs
	)
'

test_expect_success 'repack --write-midx includes cruft when already geometric' '
	git init repack--write-midx-geometric-noop &&
	(
		cd repack--write-midx-geometric-noop &&

		git branch -M main &&
		test_commit A &&
		test_commit B &&

		git checkout -B side &&
		test_commit --no-tag C &&
		C="$(git rev-parse HEAD)" &&

		git checkout main &&
		git branch -D side &&
		git reflog expire --all --expire=all &&

		# At this point we have two packs: one containing the
		# objects belonging to commits A and B, and another
		# (cruft) pack containing the objects belonging to
		# commit C.
		git repack --cruft -d &&

		# Create a third pack which contains a merge commit
		# making commit C reachable again.
		#
		# --no-ff is important here, as it ensures that we
		# actually write a new object and subsequently a new
		# pack to contain it.
		git merge --no-ff $C &&
		git repack -d &&

		ls $packdir/pack-*.idx | sort >packs.all &&
		cruft="$(ls $packdir/pack-*.mtimes)" &&
		cruft="${cruft%.mtimes}.idx" &&

		for idx in $(grep -v $cruft <packs.all)
		do
			git show-index <$idx >out &&
			wc -l <out || return 1
		done >sizes.raw &&

		# Make sure that there are two non-cruft packs, and
		# that one of them contains at least twice as many
		# objects as the other, ensuring that they are already
		# in a geometric progression.
		sort -n sizes.raw >sizes &&
		test_line_count = 2 sizes &&
		s1=$(head -n 1 sizes) &&
		s2=$(tail -n 1 sizes) &&
		test "$s2" -gt "$((2 * $s1))" &&

		git -c repack.midxMustContainCruft=false repack --geometric=2 \
			--write-midx --write-bitmap-index
	)
'

test_expect_success 'repack rescues once-cruft objects above geometric split' '
	git config repack.midxMustContainCruft false &&

	test_commit reachable &&
	test_commit unreachable &&

	unreachable="$(git rev-parse HEAD)" &&

	git reset --hard HEAD^ &&
	git tag -d unreachable &&
	git reflog expire --all --expire=all &&

	git repack --cruft -d &&

	echo $unreachable | git pack-objects .git/objects/pack/pack &&

	test_commit new &&

	git update-ref refs/heads/other $unreachable &&
	git repack --geometric=2 -d --write-midx --write-bitmap-index
'

test_done
