Merge branch 'ps/packfile-stale-delta-base-cache-fix' into jch

Closing a packfile did not evict the entries belonging to it from
the delta base cache, which could lead to data corruption when a
newly allocated packfile happened to reuse the same memory address.
The cache eviction logic has been updated to remove these stale
entries upon closing a pack

* ps/packfile-stale-delta-base-cache-fix:
  packfile: fix corruption due to stale delta base cache entries
  packfile: move around `close_pack()`
jch
Junio C Hamano 2026-10-12 09:25:58 -07:00
commit b59d45a334
2 changed files with 70 additions and 10 deletions

View File

@ -355,16 +355,6 @@ static void close_pack_mtimes(struct packed_git *p)
p->mtimes_map = NULL;
}

void close_pack(struct packed_git *p)
{
close_pack_windows(p);
close_pack_fd(p);
close_pack_index(p);
close_pack_revindex(p);
close_pack_mtimes(p);
oidset_clear(&p->bad_objects);
}

void unlink_pack_path(const char *pack_name, int force_delete)
{
static const char *exts[] = {".idx", ".pack", ".rev", ".keep", ".bitmap", ".promisor", ".mtimes"};
@ -1263,6 +1253,29 @@ void clear_delta_base_cache(void)
}
}

static void delta_base_cache_evict_entry(struct packed_git *p)
{
struct list_head *lru, *tmp;

list_for_each_safe(lru, tmp, &delta_base_cache_lru) {
struct delta_base_cache_entry *entry =
list_entry(lru, struct delta_base_cache_entry, lru);
if (entry->key.p == p)
release_delta_base_cache(entry);
}
}

void close_pack(struct packed_git *p)
{
close_pack_windows(p);
close_pack_fd(p);
close_pack_index(p);
close_pack_revindex(p);
close_pack_mtimes(p);
oidset_clear(&p->bad_objects);
delta_base_cache_evict_entry(p);
}

static void add_delta_base_cache(struct packed_git *p, off_t base_offset,
void *base, size_t base_size,
size_t delta_base_cache_limit,

View File

@ -553,4 +553,51 @@ test_expect_success 'merge does not mix up superproject and submodule commit-gra
test_cmp expect actual)
'

test_expect_success 'merge with many packed submodules reports conflicts' '
test_config_global protocol.file.allow always &&

# Create 16 submodules with two divergent branches each.
submodules="A B C D E F G H I J K L M N O P" &&
for name in $submodules
do
git init source-$name &&
test_commit -C source-$name $name-main &&
git -C source-$name switch --create branch-a main &&
git -C source-$name commit --allow-empty --message $name-branch-a &&
git -C source-$name switch --create branch-b main &&
git -C source-$name commit --allow-empty --message $name-branch-b || return 1
done &&

# Create the superproject and add all submodules.
git init many-packed &&
for name in $submodules
do
git -C many-packed submodule add --branch main "file://$PWD/source-$name" $name || return 1
done &&
git -C many-packed commit --message main &&

# Create two divergent commits in the superproject that update all
# submodules to the divergent branches.
for branch in branch-a branch-b
do
git -C many-packed switch -c $branch main &&
for name in $submodules
do
git -C many-packed/$name switch $branch || return 1
done &&
git -C many-packed add $submodules &&
git -C many-packed commit --message $branch || return 1
done &&

# Clone the superproject to ensure that everything is well-packed and
# then merge the two branches, creating conflicts for every submodule.
git clone many-packed many-packed-clone &&
git -C many-packed-clone submodule update --init &&
git -C many-packed-clone switch branch-a &&
test_expect_code 1 git -C many-packed-clone -c advice.submoduleMergeConflict=false merge branch-b >out 2>err &&
grep "^CONFLICT (submodule)" out >conflicts &&
test_line_count = 16 conflicts &&
test_must_be_empty err
'

test_done