mirror of
https://github.com/git-for-windows/git.git
synced 2026-02-04 03:33:01 -06:00
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>
368 lines
11 KiB
Bash
Executable File
368 lines
11 KiB
Bash
Executable File
#!/bin/sh
|
|
#
|
|
# Copyright (c) 2008 Johannes E. Schindelin
|
|
#
|
|
|
|
test_description='prune'
|
|
GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
|
|
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
|
|
|
|
. ./test-lib.sh
|
|
|
|
day=$((60*60*24))
|
|
week=$(($day*7))
|
|
|
|
add_blob() {
|
|
before=$(git count-objects | sed "s/ .*//") &&
|
|
BLOB=$(echo aleph_0 | git hash-object -w --stdin) &&
|
|
BLOB_FILE=.git/objects/$(echo $BLOB | sed "s/^../&\//") &&
|
|
verbose test $((1 + $before)) = $(git count-objects | sed "s/ .*//") &&
|
|
test_path_is_file $BLOB_FILE &&
|
|
test-tool chmtime =+0 $BLOB_FILE
|
|
}
|
|
|
|
test_expect_success setup '
|
|
>file &&
|
|
git add file &&
|
|
test_tick &&
|
|
git commit -m initial &&
|
|
git gc
|
|
'
|
|
|
|
test_expect_success 'bare repo prune is quiet without $GIT_DIR/objects/pack' '
|
|
git clone -q --shared --template= --bare . bare.git &&
|
|
rmdir bare.git/objects/pack &&
|
|
git --git-dir=bare.git prune --no-progress 2>prune.err &&
|
|
test_must_be_empty prune.err &&
|
|
rm -r bare.git prune.err
|
|
'
|
|
|
|
test_expect_success 'prune stale packs' '
|
|
orig_pack=$(echo .git/objects/pack/*.pack) &&
|
|
>.git/objects/tmp_1.pack &&
|
|
>.git/objects/tmp_2.pack &&
|
|
test-tool chmtime =-86501 .git/objects/tmp_1.pack &&
|
|
git prune --expire 1.day &&
|
|
test_path_is_file $orig_pack &&
|
|
test_path_is_file .git/objects/tmp_2.pack &&
|
|
test_path_is_missing .git/objects/tmp_1.pack
|
|
'
|
|
|
|
test_expect_success 'prune --expire' '
|
|
add_blob &&
|
|
git prune --expire=1.hour.ago &&
|
|
verbose test $((1 + $before)) = $(git count-objects | sed "s/ .*//") &&
|
|
test_path_is_file $BLOB_FILE &&
|
|
test-tool chmtime =-86500 $BLOB_FILE &&
|
|
git prune --expire 1.day &&
|
|
verbose test $before = $(git count-objects | sed "s/ .*//") &&
|
|
test_path_is_missing $BLOB_FILE
|
|
'
|
|
|
|
test_expect_success 'gc: implicit prune --expire' '
|
|
add_blob &&
|
|
test-tool chmtime =-$((2*$week-30)) $BLOB_FILE &&
|
|
git gc --no-cruft &&
|
|
verbose test $((1 + $before)) = $(git count-objects | sed "s/ .*//") &&
|
|
test_path_is_file $BLOB_FILE &&
|
|
test-tool chmtime =-$((2*$week+1)) $BLOB_FILE &&
|
|
git gc --no-cruft &&
|
|
verbose test $before = $(git count-objects | sed "s/ .*//") &&
|
|
test_path_is_missing $BLOB_FILE
|
|
'
|
|
|
|
test_expect_success 'gc: refuse to start with invalid gc.pruneExpire' '
|
|
test_when_finished "rm -rf repo" &&
|
|
git init repo &&
|
|
>repo/.git/config &&
|
|
git -C repo config gc.pruneExpire invalid &&
|
|
cat >expect <<-\EOF &&
|
|
error: Invalid gc.pruneexpire: '\''invalid'\''
|
|
fatal: bad config variable '\''gc.pruneexpire'\'' in file '\''.git/config'\'' at line 2
|
|
EOF
|
|
test_must_fail git -C repo gc 2>actual &&
|
|
test_cmp expect actual
|
|
'
|
|
|
|
test_expect_success 'gc: start with ok gc.pruneExpire' '
|
|
git config gc.pruneExpire 2.days.ago &&
|
|
git gc --no-cruft
|
|
'
|
|
|
|
test_expect_success 'prune: prune nonsense parameters' '
|
|
test_must_fail git prune garbage &&
|
|
test_must_fail git prune --- &&
|
|
test_must_fail git prune --no-such-option
|
|
'
|
|
|
|
test_expect_success 'prune: prune unreachable heads' '
|
|
git config core.logAllRefUpdates false &&
|
|
>file2 &&
|
|
git add file2 &&
|
|
git commit -m temporary &&
|
|
tmp_head=$(git rev-list -1 HEAD) &&
|
|
git reset HEAD^ &&
|
|
git reflog expire --all &&
|
|
git prune &&
|
|
test_must_fail git reset $tmp_head --
|
|
'
|
|
|
|
test_expect_success 'prune: do not prune detached HEAD with no reflog' '
|
|
git checkout --detach --quiet &&
|
|
git commit --allow-empty -m "detached commit" &&
|
|
git reflog expire --all &&
|
|
git prune -n >prune_actual &&
|
|
test_must_be_empty prune_actual
|
|
'
|
|
|
|
test_expect_success 'prune: prune former HEAD after checking out branch' '
|
|
head_oid=$(git rev-parse HEAD) &&
|
|
git checkout --quiet main &&
|
|
git reflog expire --all &&
|
|
git prune -v >prune_actual &&
|
|
grep "$head_oid" prune_actual
|
|
'
|
|
|
|
test_expect_success 'prune: do not prune heads listed as an argument' '
|
|
>file2 &&
|
|
git add file2 &&
|
|
git commit -m temporary &&
|
|
tmp_head=$(git rev-list -1 HEAD) &&
|
|
git reset HEAD^ &&
|
|
git prune -- $tmp_head &&
|
|
git reset $tmp_head --
|
|
'
|
|
|
|
test_expect_success 'gc --no-prune' '
|
|
add_blob &&
|
|
test-tool chmtime =-$((5001*$day)) $BLOB_FILE &&
|
|
git config gc.pruneExpire 2.days.ago &&
|
|
git gc --no-prune --no-cruft &&
|
|
verbose test 1 = $(git count-objects | sed "s/ .*//") &&
|
|
test_path_is_file $BLOB_FILE
|
|
'
|
|
|
|
test_expect_success 'gc respects gc.pruneExpire' '
|
|
git config gc.pruneExpire 5002.days.ago &&
|
|
git gc --no-cruft &&
|
|
test_path_is_file $BLOB_FILE &&
|
|
git config gc.pruneExpire 5000.days.ago &&
|
|
git gc --no-cruft &&
|
|
test_path_is_missing $BLOB_FILE
|
|
'
|
|
|
|
test_expect_success 'gc --prune=<date>' '
|
|
add_blob &&
|
|
test-tool chmtime =-$((5001*$day)) $BLOB_FILE &&
|
|
git gc --prune=5002.days.ago --no-cruft &&
|
|
test_path_is_file $BLOB_FILE &&
|
|
git gc --prune=5000.days.ago --no-cruft &&
|
|
test_path_is_missing $BLOB_FILE
|
|
'
|
|
|
|
test_expect_success 'gc --prune=never' '
|
|
add_blob &&
|
|
git gc --prune=never --no-cruft &&
|
|
test_path_is_file $BLOB_FILE &&
|
|
git gc --prune=now --no-cruft &&
|
|
test_path_is_missing $BLOB_FILE
|
|
'
|
|
|
|
test_expect_success 'gc respects gc.pruneExpire=never' '
|
|
git config gc.pruneExpire never &&
|
|
add_blob &&
|
|
git gc --no-cruft &&
|
|
test_path_is_file $BLOB_FILE &&
|
|
git config gc.pruneExpire now &&
|
|
git gc --no-cruft &&
|
|
test_path_is_missing $BLOB_FILE
|
|
'
|
|
|
|
test_expect_success 'prune --expire=never' '
|
|
add_blob &&
|
|
git prune --expire=never &&
|
|
test_path_is_file $BLOB_FILE &&
|
|
git prune &&
|
|
test_path_is_missing $BLOB_FILE
|
|
'
|
|
|
|
test_expect_success 'gc: prune old objects after local clone' '
|
|
add_blob &&
|
|
test-tool chmtime =-$((2*$week+1)) $BLOB_FILE &&
|
|
git clone --no-hardlinks . aclone &&
|
|
(
|
|
cd aclone &&
|
|
verbose test 1 = $(git count-objects | sed "s/ .*//") &&
|
|
test_path_is_file $BLOB_FILE &&
|
|
git gc --prune --no-cruft &&
|
|
verbose test 0 = $(git count-objects | sed "s/ .*//") &&
|
|
test_path_is_missing $BLOB_FILE
|
|
)
|
|
'
|
|
|
|
test_expect_success 'garbage report in count-objects -v' '
|
|
test_when_finished "rm -f .git/objects/pack/fake*" &&
|
|
test_when_finished "rm -f .git/objects/pack/foo*" &&
|
|
>.git/objects/pack/foo &&
|
|
>.git/objects/pack/foo.bar &&
|
|
>.git/objects/pack/foo.keep &&
|
|
>.git/objects/pack/foo.pack &&
|
|
>.git/objects/pack/fake.bar &&
|
|
>.git/objects/pack/fake.keep &&
|
|
>.git/objects/pack/fake.pack &&
|
|
>.git/objects/pack/fake.idx &&
|
|
>.git/objects/pack/fake2.keep &&
|
|
>.git/objects/pack/fake3.idx &&
|
|
git count-objects -v 2>stderr &&
|
|
grep "index file .git/objects/pack/fake.idx is too small" stderr &&
|
|
grep "^warning:" stderr | sort >actual &&
|
|
cat >expected <<\EOF &&
|
|
warning: garbage found: .git/objects/pack/fake.bar
|
|
warning: garbage found: .git/objects/pack/foo
|
|
warning: garbage found: .git/objects/pack/foo.bar
|
|
warning: no corresponding .idx or .pack: .git/objects/pack/fake2.keep
|
|
warning: no corresponding .idx: .git/objects/pack/foo.keep
|
|
warning: no corresponding .idx: .git/objects/pack/foo.pack
|
|
warning: no corresponding .pack: .git/objects/pack/fake3.idx
|
|
EOF
|
|
test_cmp expected actual
|
|
'
|
|
|
|
test_expect_success 'clean pack garbage with gc' '
|
|
test_when_finished "rm -f .git/objects/pack/fake*" &&
|
|
test_when_finished "rm -f .git/objects/pack/foo*" &&
|
|
>.git/objects/pack/foo.keep &&
|
|
>.git/objects/pack/foo.pack &&
|
|
>.git/objects/pack/fake.idx &&
|
|
>.git/objects/pack/fake2.keep &&
|
|
>.git/objects/pack/fake2.idx &&
|
|
>.git/objects/pack/fake3.keep &&
|
|
git gc --no-cruft &&
|
|
git count-objects -v 2>stderr &&
|
|
grep "^warning:" stderr | sort >actual &&
|
|
cat >expected <<\EOF &&
|
|
warning: no corresponding .idx or .pack: .git/objects/pack/fake3.keep
|
|
warning: no corresponding .idx: .git/objects/pack/foo.keep
|
|
warning: no corresponding .idx: .git/objects/pack/foo.pack
|
|
EOF
|
|
test_cmp expected actual
|
|
'
|
|
|
|
test_expect_success 'prune .git/shallow' '
|
|
oid=$(echo hi|git commit-tree HEAD^{tree}) &&
|
|
echo $oid >.git/shallow &&
|
|
git prune --dry-run >out &&
|
|
grep $oid .git/shallow &&
|
|
grep $oid out &&
|
|
git prune &&
|
|
test_path_is_missing .git/shallow
|
|
'
|
|
|
|
test_expect_success 'prune .git/shallow when there are no loose objects' '
|
|
oid=$(echo hi|git commit-tree HEAD^{tree}) &&
|
|
echo $oid >.git/shallow &&
|
|
git update-ref refs/heads/shallow-tip $oid &&
|
|
git repack -ad &&
|
|
# verify assumption that all loose objects are gone
|
|
git count-objects | grep ^0 &&
|
|
git prune &&
|
|
echo $oid >expect &&
|
|
test_cmp expect .git/shallow
|
|
'
|
|
|
|
test_expect_success 'prune: handle alternate object database' '
|
|
test_create_repo A &&
|
|
git -C A commit --allow-empty -m "initial commit" &&
|
|
git clone --shared A B &&
|
|
git -C B commit --allow-empty -m "next commit" &&
|
|
git -C B prune
|
|
'
|
|
|
|
test_expect_success 'prune: handle index in multiple worktrees' '
|
|
git worktree add second-worktree &&
|
|
echo "new blob for second-worktree" >second-worktree/blob &&
|
|
git -C second-worktree add blob &&
|
|
git prune --expire=now &&
|
|
git -C second-worktree show :blob >actual &&
|
|
test_cmp second-worktree/blob actual
|
|
'
|
|
|
|
test_expect_success 'prune: handle HEAD in multiple worktrees' '
|
|
git worktree add --detach third-worktree &&
|
|
echo "new blob for third-worktree" >third-worktree/blob &&
|
|
git -C third-worktree add blob &&
|
|
git -C third-worktree commit -m "third" &&
|
|
rm .git/worktrees/third-worktree/index &&
|
|
test_must_fail git -C third-worktree show :blob &&
|
|
git prune --expire=now &&
|
|
git -C third-worktree show HEAD:blob >actual &&
|
|
test_cmp third-worktree/blob actual
|
|
'
|
|
|
|
test_expect_success 'prune: handle HEAD reflog in multiple worktrees' '
|
|
git config core.logAllRefUpdates true &&
|
|
echo "lost blob for third-worktree" >expected &&
|
|
(
|
|
cd third-worktree &&
|
|
cat ../expected >blob &&
|
|
git add blob &&
|
|
git commit -m "second commit in third" &&
|
|
git clean -f && # Remove untracked left behind by deleting index
|
|
git reset --hard HEAD^
|
|
) &&
|
|
git prune --expire=now &&
|
|
oid=`git hash-object expected` &&
|
|
git -C third-worktree show "$oid" >actual &&
|
|
test_cmp expected actual
|
|
'
|
|
|
|
test_expect_success 'prune: handle expire option correctly' '
|
|
test_must_fail git prune --expire 2>error &&
|
|
test_i18ngrep "requires a value" error &&
|
|
|
|
test_must_fail git prune --expire=nyah 2>error &&
|
|
test_i18ngrep "malformed expiration" error &&
|
|
|
|
git prune --no-expire
|
|
'
|
|
|
|
test_expect_success 'trivial prune with bitmaps enabled' '
|
|
git repack -adb &&
|
|
blob=$(echo bitmap-unreachable-blob | git hash-object -w --stdin) &&
|
|
git prune --expire=now &&
|
|
git cat-file -e HEAD &&
|
|
test_must_fail git cat-file -e $blob
|
|
'
|
|
|
|
test_expect_success 'old reachable-from-recent retained with bitmaps' '
|
|
git repack -adb &&
|
|
to_drop=$(echo bitmap-from-recent-1 | git hash-object -w --stdin) &&
|
|
test-tool chmtime -86400 .git/objects/$(test_oid_to_path $to_drop) &&
|
|
to_save=$(echo bitmap-from-recent-2 | git hash-object -w --stdin) &&
|
|
test-tool chmtime -86400 .git/objects/$(test_oid_to_path $to_save) &&
|
|
tree=$(printf "100644 blob $to_save\tfile\n" | git mktree) &&
|
|
test-tool chmtime -86400 .git/objects/$(test_oid_to_path $tree) &&
|
|
commit=$(echo foo | git commit-tree $tree) &&
|
|
git prune --expire=12.hours.ago &&
|
|
git cat-file -e $commit &&
|
|
git cat-file -e $tree &&
|
|
git cat-file -e $to_save &&
|
|
test_must_fail git cat-file -e $to_drop
|
|
'
|
|
|
|
test_expect_success 'gc.recentObjectsHook' '
|
|
add_blob &&
|
|
test-tool chmtime =-86500 $BLOB_FILE &&
|
|
|
|
write_script precious-objects <<-EOF &&
|
|
echo $BLOB
|
|
EOF
|
|
test_config gc.recentObjectsHook ./precious-objects &&
|
|
|
|
git prune --expire=now &&
|
|
|
|
git cat-file -p $BLOB
|
|
'
|
|
|
|
test_done
|