Skip to content

Commit fd2739b

Browse files
ttaylorr-oaigitster
authored andcommitted
pack-bitmap: handle duplicate pack entries during MIDX reuse
A MIDX pseudo-pack assigns one bit position to each selected OID. Its preferred-pack fast paths handle duplicate OIDs across packs in the MIDX, where the MIDX chooses one copy, but assume that each individual pack contains one entry per OID. Consider a preferred pack ordered [A, B, A, C]. The MIDX retains one copy of A, leaving three pseudo-pack positions for four physical entries. This creates two problems: - In MIDX-backed single-pack reuse, bitmap_nr came from pack->num_objects and therefore counted both copies of A. Read the selected count from the root MIDX layer BTMP chunk instead. If that layer has no BTMP chunk, skip pack reuse and let the ordinary packing path handle the bitmap-selected objects. MIDXs without BTMP lose preferred-pack reuse until rewritten, rather than requiring a scan of the entire pseudo-pack to retain an optional optimization. - Correcting the range length still does not align the two orderings. Once one copy of A is omitted, MIDX position N need not name physical pack entry N. That mismatch matters during both selection and output. Whole-word selection bypasses the per-object check that the exact physical base of a delta is present. Verbatim reuse copies the first N pack entries for the first N bits. The per-object writer likewise used each bit as a physical pack position. Use the direct mapping only when the range begins at zero and its selected count equals the physical entry count. Since selected entries remain in pack-offset order, equal counts mean that none was omitted and the two positions coincide. For every other MIDX range, let whole-word selection fall through to the existing per-bit path, which resolves the selected MIDX offset to a physical pack position before checking the delta base. Disable verbatim prefix reuse, and perform the same translation in the per-object writer. Leave classic single-pack bitmap handling unchanged. The production writer creates those bitmaps only for the pack it just wrote, which has one entry per OID. The test helper can target an existing pack, but rejects duplicate OIDs. Cover the A, B, A, C case in a MIDX-backed preferred pack. Check reuse with BTMP, then hide BTMP and check that the optional reuse path is skipped. Also make B a delta against the omitted copy of A and require normal packing to handle it. Signed-off-by: Taylor Blau <ttaylorr@openai.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent 89810d5 commit fd2739b

3 files changed

Lines changed: 125 additions & 15 deletions

File tree

builtin/pack-objects.c

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,12 +1182,12 @@ static size_t write_reused_pack_verbatim(struct bitmapped_pack *reuse_packfile,
11821182
size_t pos = 0;
11831183
size_t end;
11841184

1185-
if (reuse_packfile->bitmap_pos) {
1185+
if (reuse_packfile->bitmap_pos ||
1186+
reuse_packfile->bitmap_nr != reuse_packfile->p->num_objects) {
11861187
/*
1187-
* We can't reuse whole chunks verbatim out of
1188-
* non-preferred packs since we can't guarantee that
1189-
* all duplicate objects were resolved in favor of
1190-
* that pack.
1188+
* We can't reuse whole chunks verbatim from non-preferred
1189+
* packs or packs with entries missing from the bitmap because
1190+
* bitmap and pack positions may differ.
11911191
*
11921192
* Even if we have a whole eword_t worth of bits that
11931193
* could be reused, there may be objects between the
@@ -1196,7 +1196,7 @@ static size_t write_reused_pack_verbatim(struct bitmapped_pack *reuse_packfile,
11961196
* pack, causing us to send duplicate or unwanted
11971197
* objects.
11981198
*
1199-
* Handle non-preferred packs from within
1199+
* Handle these packs from within
12001200
* write_reused_pack(), which inspects and reuses
12011201
* individual bits.
12021202
*/
@@ -1263,20 +1263,18 @@ static void write_reused_pack(struct bitmapped_pack *reuse_packfile,
12631263
if (pos + offset >= reuse_packfile->bitmap_pos + reuse_packfile->bitmap_nr)
12641264
goto done;
12651265

1266-
if (reuse_packfile->bitmap_pos) {
1266+
if (reuse_packfile->bitmap_pos ||
1267+
reuse_packfile->bitmap_nr != reuse_packfile->p->num_objects) {
12671268
/*
1268-
* When doing multi-pack reuse on a
1269-
* non-preferred pack, translate bit positions
1270-
* from the MIDX pseudo-pack order back to their
1271-
* pack-relative positions before attempting
1272-
* reuse.
1269+
* Translate MIDX bitmap positions which do not
1270+
* correspond directly to physical pack positions.
12731271
*/
12741272
struct multi_pack_index *m = reuse_packfile->from_midx;
12751273
uint32_t midx_pos;
12761274
off_t pack_ofs;
12771275

12781276
if (!m)
1279-
BUG("non-zero bitmap position without MIDX");
1277+
BUG("cannot translate bitmap position without MIDX");
12801278

12811279
midx_pos = pack_pos_to_midx(m, pos + offset);
12821280
pack_ofs = nth_midxed_offset(m, midx_pos);

pack-bitmap.c

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2383,7 +2383,8 @@ static void reuse_partial_packfile_from_bitmap_1(struct bitmap_index *bitmap_git
23832383
struct pack_window *w_curs = NULL;
23842384
size_t pos = pack->bitmap_pos / BITS_IN_EWORD;
23852385

2386-
if (!pack->bitmap_pos) {
2386+
if (!pack->bitmap_pos &&
2387+
pack->bitmap_nr == pack->p->num_objects) {
23872388
/*
23882389
* If we're processing the first (in the case of a MIDX, the
23892390
* preferred pack) or the only (in the case of single-pack
@@ -2399,6 +2400,9 @@ static void reuse_partial_packfile_from_bitmap_1(struct bitmap_index *bitmap_git
23992400
* all ties are broken in favor of that pack (i.e. the one
24002401
* we're currently processing). So any duplicate bases will be
24012402
* resolved in favor of the pack we're processing.
2403+
*
2404+
* The range must also contain every physical pack entry so that
2405+
* bitmap and pack positions correspond.
24022406
*/
24032407
while (pos < result->word_alloc &&
24042408
pos < pack->bitmap_nr / BITS_IN_EWORD &&
@@ -2527,21 +2531,39 @@ void reuse_partial_packfile_from_bitmap(struct bitmap_index *bitmap_git,
25272531
} else {
25282532
struct packed_git *pack;
25292533
uint32_t pack_int_id;
2534+
uint32_t bitmap_nr;
25302535

25312536
if (bitmap_is_midx(bitmap_git)) {
2537+
struct bitmapped_pack bitmapped_pack;
25322538
struct multi_pack_index *m = bitmap_git->midx;
25332539
uint32_t preferred_pack_pos;
25342540

25352541
while (m->base_midx)
25362542
m = m->base_midx;
25372543

2544+
if (!m->chunk_bitmapped_packs) {
2545+
/*
2546+
* Without BTMP, determining the preferred
2547+
* pack's range requires scanning the pseudo-pack.
2548+
* Skip reuse and leave the bitmap result for
2549+
* normal packing.
2550+
*/
2551+
return;
2552+
}
2553+
25382554
if (midx_preferred_pack(m, &preferred_pack_pos) < 0) {
25392555
warning(_("unable to compute preferred pack, disabling pack-reuse"));
25402556
return;
25412557
}
25422558

25432559
pack = nth_midxed_pack(m, preferred_pack_pos);
25442560
pack_int_id = preferred_pack_pos;
2561+
2562+
if (nth_bitmapped_pack(m, &bitmapped_pack,
2563+
pack_int_id) < 0)
2564+
return;
2565+
pack = bitmapped_pack.p;
2566+
bitmap_nr = bitmapped_pack.bitmap_nr;
25452567
} else {
25462568
pack = bitmap_git->pack;
25472569
/*
@@ -2554,13 +2576,14 @@ void reuse_partial_packfile_from_bitmap(struct bitmap_index *bitmap_git,
25542576
* that we do not expect to read this field.
25552577
*/
25562578
pack_int_id = -1;
2579+
bitmap_nr = pack->num_objects;
25572580
}
25582581

25592582
if (is_pack_valid(pack)) {
25602583
ALLOC_GROW(packs, packs_nr + 1, packs_alloc);
25612584
packs[packs_nr].p = pack;
25622585
packs[packs_nr].pack_int_id = pack_int_id;
2563-
packs[packs_nr].bitmap_nr = pack->num_objects;
2586+
packs[packs_nr].bitmap_nr = bitmap_nr;
25642587
packs[packs_nr].bitmap_pos = 0;
25652588
packs[packs_nr].from_midx = bitmap_git->midx;
25662589
packs_nr++;

t/t5332-multi-pack-reuse.sh

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ test_description='pack-objects multi-pack reuse'
44

55
. ./test-lib.sh
66
. "$TEST_DIRECTORY"/lib-bitmap.sh
7+
. "$TEST_DIRECTORY"/lib-pack.sh
78

89
GIT_TEST_MULTI_PACK_INDEX=0
910
GIT_TEST_MULTI_PACK_INDEX_WRITE_INCREMENTAL=0
@@ -32,6 +33,14 @@ pack_position () {
3233
grep "$1" objects | cut -d" " -f1
3334
}
3435

36+
# B as an OFS_DELTA against A at the given one-byte distance.
37+
pack_obj_b_ofs_a () {
38+
pack_obj "$B" "$A" >b-ref.tmp &&
39+
printf "\145" &&
40+
printf "\\$(printf "%03o" "$1")" &&
41+
dd if=b-ref.tmp bs=1 skip=$((1 + $(test_oid rawsz))) 2>/dev/null
42+
}
43+
3544
# test_pack_objects_reused_all <pack-reused> <packs-reused>
3645
test_pack_objects_reused_all () {
3746
: >trace2.txt &&
@@ -288,4 +297,84 @@ test_expect_success 'duplicate objects with verbatim reuse' '
288297
)
289298
'
290299

300+
test_expect_success 'reuse with intra-pack duplicate objects' '
301+
git init intra-pack-duplicate-objects &&
302+
(
303+
cd intra-pack-duplicate-objects &&
304+
305+
# Make enough objects to exercise whole-word reuse.
306+
test_commit_bulk 20 &&
307+
test_commit --printf A a "\7\0" &&
308+
test_commit --printf B b "\7\76" &&
309+
310+
objects_nr=$(git rev-list --count --objects --all) &&
311+
git rev-list --objects --all |
312+
cut -d" " -f1 >objects &&
313+
A=$(test_oid packlib_7_0) &&
314+
B=$(test_oid packlib_7_76) &&
315+
grep -v -e "^$A$" -e "^$B$" objects >rest &&
316+
pack_obj "$A" >a-full &&
317+
pack_obj "$B" >b-full &&
318+
while read oid
319+
do
320+
pack_obj "$oid" || exit 1
321+
done <rest >rest.entries &&
322+
{
323+
# Arrange the pack as A, B, A, C..., so that physical
324+
# positions diverge from MIDX pseudo-pack order.
325+
pack_header $((objects_nr + 1)) &&
326+
cat a-full b-full a-full rest.entries
327+
} >duplicate.pack &&
328+
pack_trailer duplicate.pack &&
329+
clear_packs &&
330+
git index-pack --stdin <duplicate.pack &&
331+
332+
git multi-pack-index write --bitmap &&
333+
git config pack.allowPackReuse single &&
334+
test_pack_objects_reused_all "$objects_nr" 1 &&
335+
rm -f got.idx &&
336+
test_env GIT_TEST_MIDX_READ_BTMP=false \
337+
test_pack_objects_reused_all 0 0
338+
)
339+
'
340+
341+
test_expect_success 'omit delta whose duplicate base is not selected' '
342+
(
343+
cd intra-pack-duplicate-objects &&
344+
345+
A=$(test_oid packlib_7_0) &&
346+
B=$(test_oid packlib_7_76) &&
347+
objects_nr=$(wc -l <objects) &&
348+
349+
a_size=$(wc -c <a-full) &&
350+
test "$((2 * a_size))" -lt 128 &&
351+
352+
# The .idx order of duplicate OIDs is unspecified. Try a delta
353+
# against each copy and keep the pack whose base was omitted.
354+
for distance in "$((2 * a_size))" "$a_size"
355+
do
356+
base_offset=$((12 + 2 * a_size - distance)) &&
357+
pack_obj_b_ofs_a "$distance" >b-delta &&
358+
{
359+
pack_header "$((objects_nr + 1))" &&
360+
cat a-full a-full b-delta rest.entries
361+
} >candidate.pack &&
362+
pack_trailer candidate.pack &&
363+
clear_packs &&
364+
git index-pack --stdin <candidate.pack &&
365+
git multi-pack-index write --bitmap || return 1
366+
367+
selected_offset=$(
368+
test-tool read-midx --show-objects "$objdir" |
369+
awk -v oid="$A" "\$1 == oid { print \$2 }"
370+
) || return 1
371+
test -n "$selected_offset" || return 1
372+
test "$selected_offset" = "$base_offset" || break
373+
done &&
374+
test "$selected_offset" != "$base_offset" &&
375+
376+
test_pack_objects_reused_all "$((objects_nr - 1))" 1
377+
)
378+
'
379+
291380
test_done

0 commit comments

Comments
 (0)