git/t/t5329-pack-objects-cruft.sh
Taylor Blau 4dc16e2cb0 gc: introduce gc.recentObjectsHook
This patch introduces a new multi-valued configuration option,
`gc.recentObjectsHook` as a means to mark certain objects as recent (and
thus exempt from garbage collection), regardless of their age.

When performing a garbage collection operation on a repository with
unreachable objects, Git makes its decision on what to do with those
object(s) based on how recent the objects are or not. Generally speaking,
unreachable-but-recent objects stay in the repository, and older objects
are discarded.

However, we have no convenient way to keep certain precious, unreachable
objects around in the repository, even if they have aged out and would
be pruned. Our options today consist of:

  - Point references at the reachability tips of any objects you
    consider precious, which may be undesirable or infeasible if there
    are many such objects.

  - Track them via the reflog, which may be undesirable since the
    reflog's lifetime is limited to that of the reference it's tracking
    (and callers may want to keep those unreachable objects around for
    longer).

  - Extend the grace period, which may keep around other objects that
    the caller *does* want to discard.

  - Manually modify the mtimes of objects you want to keep. If those
    objects are already loose, this is easy enough to do (you can just
    enumerate and `touch -m` each one).

    But if they are packed, you will either end up modifying the mtimes
    of *all* objects in that pack, or be forced to write out a loose
    copy of that object, both of which may be undesirable. Even worse,
    if they are in a cruft pack, that requires modifying its `*.mtimes`
    file by hand, since there is no exposed plumbing for this.

  - Force the caller to construct the pack of objects they want
    to keep themselves, and then mark the pack as kept by adding a
    ".keep" file. This works, but is burdensome for the caller, and
    having extra packs is awkward as you roll forward your cruft pack.

This patch introduces a new option to the above list via the
`gc.recentObjectsHook` configuration, which allows the caller to
specify a program (or set of programs) whose output is treated as a set
of objects to treat as recent, regardless of their true age.

The implementation is straightforward. Git enumerates recent objects via
`add_unseen_recent_objects_to_traversal()`, which enumerates loose and
packed objects, and eventually calls add_recent_object() on any objects
for which `want_recent_object()`'s conditions are met.

This patch modifies the recency condition from simply "is the mtime of
this object more recent than the cutoff?" to "[...] or, is this object
mentioned by at least one `gc.recentObjectsHook`?".

Depending on whether or not we are generating a cruft pack, this allows
the caller to do one of two things:

  - If generating a cruft pack, the caller is able to retain additional
    objects via the cruft pack, even if they would have otherwise been
    pruned due to their age.

  - If not generating a cruft pack, the caller is likewise able to
    retain additional objects as loose.

A potential alternative here is to introduce a new mode to alter the
contents of the reachable pack instead of the cruft one. One could
imagine a new option to `pack-objects`, say `--extra-reachable-tips`
that does the same thing as above, adding the visited set of objects
along the traversal to the pack.

But this has the unfortunate side-effect of altering the reachability
closure of that pack. If parts of the unreachable object graph mentioned
by one or more of the "extra reachable tips" programs is not closed,
then the resulting pack won't be either. This makes it impossible in the
general case to write out reachability bitmaps for that pack, since
closure is a requirement there.

Instead, keep these unreachable objects in the cruft pack (or set of
unreachable, loose objects) instead, to ensure that we can continue to
have a pack containing just reachable objects, which is always safe to
write a bitmap over.

Helped-by: Jeff King <peff@peff.net>
Signed-off-by: Taylor Blau <me@ttaylorr.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
2023-06-12 14:12:20 -07:00

914 lines
24 KiB
Bash
Executable file

#!/bin/sh
test_description='cruft pack related pack-objects tests'
. ./test-lib.sh
objdir=.git/objects
packdir=$objdir/pack
basic_cruft_pack_tests () {
expire="$1"
test_expect_success "unreachable loose objects are packed (expire $expire)" '
git init repo &&
test_when_finished "rm -fr repo" &&
(
cd repo &&
test_commit base &&
git repack -Ad &&
test_commit loose &&
test-tool chmtime +2000 "$objdir/$(test_oid_to_path \
$(git rev-parse loose:loose.t))" &&
test-tool chmtime +1000 "$objdir/$(test_oid_to_path \
$(git rev-parse loose^{tree}))" &&
(
git rev-list --objects --no-object-names base..loose |
while read oid
do
path="$objdir/$(test_oid_to_path "$oid")" &&
printf "%s %d\n" "$oid" "$(test-tool chmtime --get "$path")" ||
echo "object list generation failed for $oid"
done |
sort -k1
) >expect &&
keep="$(basename "$(ls $packdir/pack-*.pack)")" &&
cruft="$(echo $keep | git pack-objects --cruft \
--cruft-expiration="$expire" $packdir/pack)" &&
test-tool pack-mtimes "pack-$cruft.mtimes" >actual &&
test_cmp expect actual
)
'
test_expect_success "unreachable packed objects are packed (expire $expire)" '
git init repo &&
test_when_finished "rm -fr repo" &&
(
cd repo &&
test_commit packed &&
git repack -Ad &&
test_commit other &&
git rev-list --objects --no-object-names packed.. >objects &&
keep="$(basename "$(ls $packdir/pack-*.pack)")" &&
other="$(git pack-objects --delta-base-offset \
$packdir/pack <objects)" &&
git prune-packed &&
test-tool chmtime --get -100 "$packdir/pack-$other.pack" >expect &&
cruft="$(git pack-objects --cruft --cruft-expiration="$expire" $packdir/pack <<-EOF
$keep
-pack-$other.pack
EOF
)" &&
test-tool pack-mtimes "pack-$cruft.mtimes" >actual.raw &&
cut -d" " -f2 <actual.raw | sort -u >actual &&
test_cmp expect actual
)
'
test_expect_success "unreachable cruft objects are repacked (expire $expire)" '
git init repo &&
test_when_finished "rm -fr repo" &&
(
cd repo &&
test_commit packed &&
git repack -Ad &&
test_commit other &&
git rev-list --objects --no-object-names packed.. >objects &&
keep="$(basename "$(ls $packdir/pack-*.pack)")" &&
cruft_a="$(echo $keep | git pack-objects --cruft --cruft-expiration="$expire" $packdir/pack)" &&
git prune-packed &&
cruft_b="$(git pack-objects --cruft --cruft-expiration="$expire" $packdir/pack <<-EOF
$keep
-pack-$cruft_a.pack
EOF
)" &&
test-tool pack-mtimes "pack-$cruft_a.mtimes" >expect.raw &&
test-tool pack-mtimes "pack-$cruft_b.mtimes" >actual.raw &&
sort <expect.raw >expect &&
sort <actual.raw >actual &&
test_cmp expect actual
)
'
test_expect_success "multiple cruft packs (expire $expire)" '
git init repo &&
test_when_finished "rm -fr repo" &&
(
cd repo &&
test_commit reachable &&
git repack -Ad &&
keep="$(basename "$(ls $packdir/pack-*.pack)")" &&
test_commit cruft &&
loose="$objdir/$(test_oid_to_path $(git rev-parse cruft))" &&
# generate three copies of the cruft object in different
# cruft packs, each with a unique mtime:
# - one expired (1000 seconds ago)
# - two non-expired (one 1000 seconds in the future,
# one 1500 seconds in the future)
test-tool chmtime =-1000 "$loose" &&
git pack-objects --cruft $packdir/pack-A <<-EOF &&
$keep
EOF
test-tool chmtime =+1000 "$loose" &&
git pack-objects --cruft $packdir/pack-B <<-EOF &&
$keep
-$(basename $(ls $packdir/pack-A-*.pack))
EOF
test-tool chmtime =+1500 "$loose" &&
git pack-objects --cruft $packdir/pack-C <<-EOF &&
$keep
-$(basename $(ls $packdir/pack-A-*.pack))
-$(basename $(ls $packdir/pack-B-*.pack))
EOF
# ensure the resulting cruft pack takes the most recent
# mtime among all copies
cruft="$(git pack-objects --cruft \
--cruft-expiration="$expire" \
$packdir/pack <<-EOF
$keep
-$(basename $(ls $packdir/pack-A-*.pack))
-$(basename $(ls $packdir/pack-B-*.pack))
-$(basename $(ls $packdir/pack-C-*.pack))
EOF
)" &&
test-tool pack-mtimes "$(basename $(ls $packdir/pack-C-*.mtimes))" >expect.raw &&
test-tool pack-mtimes "pack-$cruft.mtimes" >actual.raw &&
sort expect.raw >expect &&
sort actual.raw >actual &&
test_cmp expect actual
)
'
test_expect_success "cruft packs tolerate missing trees (expire $expire)" '
git init repo &&
test_when_finished "rm -fr repo" &&
(
cd repo &&
test_commit reachable &&
test_commit cruft &&
tree="$(git rev-parse cruft^{tree})" &&
git reset --hard reachable &&
git tag -d cruft &&
git reflog expire --all --expire=all &&
# remove the unreachable tree, but leave the commit
# which has it as its root tree intact
rm -fr "$objdir/$(test_oid_to_path "$tree")" &&
git repack -Ad &&
basename $(ls $packdir/pack-*.pack) >in &&
git pack-objects --cruft --cruft-expiration="$expire" \
$packdir/pack <in
)
'
test_expect_success "cruft packs tolerate missing blobs (expire $expire)" '
git init repo &&
test_when_finished "rm -fr repo" &&
(
cd repo &&
test_commit reachable &&
test_commit cruft &&
blob="$(git rev-parse cruft:cruft.t)" &&
git reset --hard reachable &&
git tag -d cruft &&
git reflog expire --all --expire=all &&
# remove the unreachable blob, but leave the commit (and
# the root tree of that commit) intact
rm -fr "$objdir/$(test_oid_to_path "$blob")" &&
git repack -Ad &&
basename $(ls $packdir/pack-*.pack) >in &&
git pack-objects --cruft --cruft-expiration="$expire" \
$packdir/pack <in
)
'
}
basic_cruft_pack_tests never
basic_cruft_pack_tests 2.weeks.ago
test_expect_success 'cruft tags rescue tagged objects' '
git init repo &&
test_when_finished "rm -fr repo" &&
(
cd repo &&
test_commit packed &&
git repack -Ad &&
test_commit tagged &&
git tag -a annotated -m tag &&
git rev-list --objects --no-object-names packed.. >objects &&
while read oid
do
test-tool chmtime -1000 \
"$objdir/$(test_oid_to_path $oid)" || exit 1
done <objects &&
test-tool chmtime -500 \
"$objdir/$(test_oid_to_path $(git rev-parse annotated))" &&
keep="$(basename "$(ls $packdir/pack-*.pack)")" &&
cruft="$(echo $keep | git pack-objects --cruft \
--cruft-expiration=750.seconds.ago \
$packdir/pack)" &&
test-tool pack-mtimes "pack-$cruft.mtimes" >actual.raw &&
cut -f1 -d" " <actual.raw | sort >actual &&
(
cat objects &&
git rev-parse annotated
) >expect.raw &&
sort <expect.raw >expect &&
test_cmp expect actual &&
cat actual
)
'
test_expect_success 'cruft commits rescue parents, trees' '
git init repo &&
test_when_finished "rm -fr repo" &&
(
cd repo &&
test_commit packed &&
git repack -Ad &&
test_commit old &&
test_commit new &&
git rev-list --objects --no-object-names packed..new >objects &&
while read object
do
test-tool chmtime -1000 \
"$objdir/$(test_oid_to_path $object)" || exit 1
done <objects &&
test-tool chmtime +500 "$objdir/$(test_oid_to_path \
$(git rev-parse HEAD))" &&
keep="$(basename "$(ls $packdir/pack-*.pack)")" &&
cruft="$(echo $keep | git pack-objects --cruft \
--cruft-expiration=750.seconds.ago \
$packdir/pack)" &&
test-tool pack-mtimes "pack-$cruft.mtimes" >actual.raw &&
cut -d" " -f1 <actual.raw | sort >actual &&
sort <objects >expect &&
test_cmp expect actual
)
'
test_expect_success 'cruft trees rescue sub-trees, blobs' '
git init repo &&
test_when_finished "rm -fr repo" &&
(
cd repo &&
test_commit packed &&
git repack -Ad &&
mkdir -p dir/sub &&
echo foo >foo &&
echo bar >dir/bar &&
echo baz >dir/sub/baz &&
test_tick &&
git add . &&
git commit -m "pruned" &&
test-tool chmtime -1000 "$objdir/$(test_oid_to_path $(git rev-parse HEAD))" &&
test-tool chmtime -1000 "$objdir/$(test_oid_to_path $(git rev-parse HEAD^{tree}))" &&
test-tool chmtime -1000 "$objdir/$(test_oid_to_path $(git rev-parse HEAD:foo))" &&
test-tool chmtime -500 "$objdir/$(test_oid_to_path $(git rev-parse HEAD:dir))" &&
test-tool chmtime -1000 "$objdir/$(test_oid_to_path $(git rev-parse HEAD:dir/bar))" &&
test-tool chmtime -1000 "$objdir/$(test_oid_to_path $(git rev-parse HEAD:dir/sub))" &&
test-tool chmtime -1000 "$objdir/$(test_oid_to_path $(git rev-parse HEAD:dir/sub/baz))" &&
keep="$(basename "$(ls $packdir/pack-*.pack)")" &&
cruft="$(echo $keep | git pack-objects --cruft \
--cruft-expiration=750.seconds.ago \
$packdir/pack)" &&
test-tool pack-mtimes "pack-$cruft.mtimes" >actual.raw &&
cut -f1 -d" " <actual.raw | sort >actual &&
git rev-parse HEAD:dir HEAD:dir/bar HEAD:dir/sub HEAD:dir/sub/baz >expect.raw &&
sort <expect.raw >expect &&
test_cmp expect actual
)
'
test_expect_success 'expired objects are pruned' '
git init repo &&
test_when_finished "rm -fr repo" &&
(
cd repo &&
test_commit packed &&
git repack -Ad &&
test_commit pruned &&
git rev-list --objects --no-object-names packed..pruned >objects &&
while read object
do
test-tool chmtime -1000 \
"$objdir/$(test_oid_to_path $object)" || exit 1
done <objects &&
keep="$(basename "$(ls $packdir/pack-*.pack)")" &&
cruft="$(echo $keep | git pack-objects --cruft \
--cruft-expiration=750.seconds.ago \
$packdir/pack)" &&
test-tool pack-mtimes "pack-$cruft.mtimes" >actual &&
test_must_be_empty actual
)
'
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 'loose objects mtimes upsert others' '
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 &&
# incremental repack, leaving existing objects loose (so
# they can be "freshened")
git repack &&
tip="$(git rev-parse cruft)" &&
path="$objdir/$(test_oid_to_path "$tip")" &&
test-tool chmtime --get +1000 "$path" >expect &&
git checkout main &&
git branch -D other &&
git tag -d cruft &&
git reflog expire --all --expire=all &&
git repack --cruft -d &&
mtimes="$(basename $(ls $packdir/pack-*.mtimes))" &&
test-tool pack-mtimes "$mtimes" >actual.raw &&
grep "$tip" actual.raw | cut -d" " -f2 >actual &&
test_cmp expect actual
)
'
test_expect_success 'expiring cruft objects with git gc' '
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 &&
# Write a cruft pack containing all unreachable objects.
git gc --cruft --prune="01-01-1980" &&
mtimes=$(ls .git/objects/pack/pack-*.mtimes) &&
test_path_is_file $mtimes &&
# Prune all unreachable objects from the cruft pack.
git gc --cruft --prune=now &&
git cat-file --batch-all-objects --batch-check="%(objectname)" >objects &&
comm -23 unreachable objects >removed &&
test_cmp unreachable removed &&
test_path_is_missing $mtimes
)
'
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
)
'
test_expect_success 'cruft repack ignores --max-pack-size' '
git init max-pack-size &&
(
cd max-pack-size &&
test_commit base &&
# two cruft objects which exceed the maximum pack size
test-tool genrandom foo 1048576 | git hash-object --stdin -w &&
test-tool genrandom bar 1048576 | git hash-object --stdin -w &&
git repack --cruft --max-pack-size=1M &&
find $packdir -name "*.mtimes" >cruft &&
test_line_count = 1 cruft &&
test-tool pack-mtimes "$(basename "$(cat cruft)")" >objects &&
test_line_count = 2 objects
)
'
test_expect_success 'cruft repack ignores 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 = 1 cruft &&
test-tool pack-mtimes "$(basename "$(cat cruft)")" >objects &&
test_line_count = 2 objects
)
'
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
)
'
test_expect_success 'cruft --local drops unreachable objects' '
git init alternate &&
git init repo &&
test_when_finished "rm -fr alternate repo" &&
test_commit -C alternate base &&
# Pack all objects in alterate so that the cruft repack in "repo" sees
# the object it dropped due to `--local` as packed. Otherwise this
# object would not appear packed anywhere (since it is not packed in
# alternate and likewise not part of the cruft pack in the other repo
# because of `--local`).
git -C alternate repack -ad &&
(
cd repo &&
object="$(git -C ../alternate rev-parse HEAD:base.t)" &&
git -C ../alternate cat-file -p $object >contents &&
# Write some reachable objects and two unreachable ones: one
# that the alternate has and another that is unique.
test_commit other &&
git hash-object -w -t blob contents &&
cruft="$(echo cruft | git hash-object -w -t blob --stdin)" &&
( cd ../alternate/.git/objects && pwd ) \
>.git/objects/info/alternates &&
test_path_is_file $objdir/$(test_oid_to_path $cruft) &&
test_path_is_file $objdir/$(test_oid_to_path $object) &&
git repack -d --cruft --local &&
test-tool pack-mtimes "$(basename $(ls $packdir/pack-*.mtimes))" \
>objects &&
! grep $object objects &&
grep $cruft objects
)
'
test_expect_success 'MIDX bitmaps tolerate reachable cruft objects' '
git init repo &&
test_when_finished "rm -fr repo" &&
(
cd repo &&
test_commit reachable &&
test_commit cruft &&
unreachable="$(git rev-parse cruft)" &&
git reset --hard $unreachable^ &&
git tag -d cruft &&
git reflog expire --all --expire=all &&
git repack --cruft -d &&
# resurrect the unreachable object via a new commit. the
# new commit will get selected for a bitmap, but be
# missing one of its parents from the selected packs.
git reset --hard $unreachable &&
test_commit resurrect &&
git repack --write-midx --write-bitmap-index --geometric=2 -d
)
'
test_expect_success 'cruft objects are freshend via loose' '
git init repo &&
test_when_finished "rm -fr repo" &&
(
cd repo &&
echo "cruft" >contents &&
blob="$(git hash-object -w -t blob contents)" &&
loose="$objdir/$(test_oid_to_path $blob)" &&
test_commit base &&
git repack --cruft -d &&
test_path_is_missing "$loose" &&
test-tool pack-mtimes "$(basename "$(ls $packdir/pack-*.mtimes)")" >cruft &&
grep "$blob" cruft &&
# write the same object again
git hash-object -w -t blob contents &&
test_path_is_file "$loose"
)
'
test_expect_success 'gc.recentObjectsHook' '
git init repo &&
test_when_finished "rm -fr repo" &&
(
cd repo &&
# Create a handful of objects.
#
# - one reachable commit, "base", designated for the reachable
# pack
# - one unreachable commit, "cruft.discard", which is marked
# for deletion
# - one unreachable commit, "cruft.old", which would be marked
# for deletion, but is rescued as an extra cruft tip
# - one unreachable commit, "cruft.new", which is not marked
# for deletion
test_commit base &&
git branch -M main &&
git checkout --orphan discard &&
git rm -fr . &&
test_commit --no-tag cruft.discard &&
git checkout --orphan old &&
git rm -fr . &&
test_commit --no-tag cruft.old &&
cruft_old="$(git rev-parse HEAD)" &&
git checkout --orphan new &&
git rm -fr . &&
test_commit --no-tag cruft.new &&
cruft_new="$(git rev-parse HEAD)" &&
git checkout main &&
git branch -D discard old new &&
git reflog expire --all --expire=all &&
# mark cruft.old with an mtime that is many minutes
# older than the expiration period, and mark cruft.new
# with an mtime that is in the future (and thus not
# eligible for pruning).
test-tool chmtime -2000 "$objdir/$(test_oid_to_path $cruft_old)" &&
test-tool chmtime +1000 "$objdir/$(test_oid_to_path $cruft_new)" &&
# Write the list of cruft objects we expect to
# accumulate, which is comprised of everything reachable
# from cruft.old and cruft.new, but not cruft.discard.
git rev-list --objects --no-object-names \
$cruft_old $cruft_new >cruft.raw &&
sort cruft.raw >cruft.expect &&
# Write the script to list extra tips, which are limited
# to cruft.old, in this case.
write_script extra-tips <<-EOF &&
echo $cruft_old
EOF
git config gc.recentObjectsHook ./extra-tips &&
git repack --cruft --cruft-expiration=now -d &&
mtimes="$(ls .git/objects/pack/pack-*.mtimes)" &&
git show-index <${mtimes%.mtimes}.idx >cruft &&
cut -d" " -f2 cruft | sort >cruft.actual &&
test_cmp cruft.expect cruft.actual &&
# Ensure that the "old" objects are removed after
# dropping the gc.recentObjectsHook hook.
git config --unset gc.recentObjectsHook &&
git repack --cruft --cruft-expiration=now -d &&
mtimes="$(ls .git/objects/pack/pack-*.mtimes)" &&
git show-index <${mtimes%.mtimes}.idx >cruft &&
cut -d" " -f2 cruft | sort >cruft.actual &&
git rev-list --objects --no-object-names $cruft_new >cruft.raw &&
cp cruft.expect cruft.old &&
sort cruft.raw >cruft.expect &&
test_cmp cruft.expect cruft.actual &&
# ensure objects which are no longer in the cruft pack were
# removed from the repository
for object in $(comm -13 cruft.expect cruft.old)
do
test_must_fail git cat-file -t $object || return 1
done
)
'
test_expect_success 'multi-valued gc.recentObjectsHook' '
git init repo &&
test_when_finished "rm -fr repo" &&
(
cd repo &&
test_commit base &&
git branch -M main &&
git checkout --orphan cruft.a &&
git rm -fr . &&
test_commit --no-tag cruft.a &&
cruft_a="$(git rev-parse HEAD)" &&
git checkout --orphan cruft.b &&
git rm -fr . &&
test_commit --no-tag cruft.b &&
cruft_b="$(git rev-parse HEAD)" &&
git checkout main &&
git branch -D cruft.a cruft.b &&
git reflog expire --all --expire=all &&
echo "echo $cruft_a" | write_script extra-tips.a &&
echo "echo $cruft_b" | write_script extra-tips.b &&
echo "false" | write_script extra-tips.c &&
git rev-list --objects --no-object-names $cruft_a $cruft_b \
>cruft.raw &&
sort cruft.raw >cruft.expect &&
# ensure that each extra cruft tip is saved by its
# respective hook
git config --add gc.recentObjectsHook ./extra-tips.a &&
git config --add gc.recentObjectsHook ./extra-tips.b &&
git repack --cruft --cruft-expiration=now -d &&
mtimes="$(ls .git/objects/pack/pack-*.mtimes)" &&
git show-index <${mtimes%.mtimes}.idx >cruft &&
cut -d" " -f2 cruft | sort >cruft.actual &&
test_cmp cruft.expect cruft.actual &&
# ensure that a dirty exit halts cruft pack generation
git config --add gc.recentObjectsHook ./extra-tips.c &&
test_must_fail git repack --cruft --cruft-expiration=now -d 2>err &&
grep "unable to enumerate additional recent objects" err &&
# and that the existing cruft pack is left alone
test_path_is_file "$mtimes"
)
'
test_expect_success 'additional cruft blobs via gc.recentObjectsHook' '
git init repo &&
test_when_finished "rm -fr repo" &&
(
cd repo &&
test_commit base &&
blob=$(echo "unreachable" | git hash-object -w --stdin) &&
# mark the unreachable blob we wrote above as having
# aged out of the retention period
test-tool chmtime -2000 "$objdir/$(test_oid_to_path $blob)" &&
# Write the script to list extra tips, which is just the
# extra blob as above.
write_script extra-tips <<-EOF &&
echo $blob
EOF
git config gc.recentObjectsHook ./extra-tips &&
git repack --cruft --cruft-expiration=now -d &&
mtimes="$(ls .git/objects/pack/pack-*.mtimes)" &&
git show-index <${mtimes%.mtimes}.idx >cruft &&
cut -d" " -f2 cruft >actual &&
echo $blob >expect &&
test_cmp expect actual
)
'
test_done