Merge branch 'tb/pack-with-duplicates' into seen

* tb/pack-with-duplicates:
  pack-bitmap: handle duplicate pack entries during MIDX reuse
  test-tool bitmap: reject packs with duplicate objects
  midx: verify duplicate pack entries by OID and offset
  packfile: recover delta cycles through duplicate entries
  t5308: test reverse indexes with duplicate objects
seen
Junio C Hamano 2026-07-24 19:10:07 -07:00
commit 45fa23ab76
9 changed files with 595 additions and 32 deletions

View File

@ -1188,12 +1188,12 @@ static size_t write_reused_pack_verbatim(struct bitmapped_pack *reuse_packfile,
size_t pos = 0;
size_t end;

if (reuse_packfile->bitmap_pos) {
if (reuse_packfile->bitmap_pos ||
reuse_packfile->bitmap_nr != reuse_packfile->p->num_objects) {
/*
* We can't reuse whole chunks verbatim out of
* non-preferred packs since we can't guarantee that
* all duplicate objects were resolved in favor of
* that pack.
* We can't reuse whole chunks verbatim from non-preferred
* packs or packs with entries missing from the bitmap because
* bitmap and pack positions may differ.
*
* Even if we have a whole eword_t worth of bits that
* could be reused, there may be objects between the
@ -1202,7 +1202,7 @@ static size_t write_reused_pack_verbatim(struct bitmapped_pack *reuse_packfile,
* pack, causing us to send duplicate or unwanted
* objects.
*
* Handle non-preferred packs from within
* Handle these packs from within
* write_reused_pack(), which inspects and reuses
* individual bits.
*/
@ -1269,20 +1269,18 @@ static void write_reused_pack(struct bitmapped_pack *reuse_packfile,
if (pos + offset >= reuse_packfile->bitmap_pos + reuse_packfile->bitmap_nr)
goto done;

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

if (!m)
BUG("non-zero bitmap position without MIDX");
BUG("cannot translate bitmap position without MIDX");

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

59
midx.c
View File

@ -906,6 +906,48 @@ static int compare_pair_pos_vs_id(const void *_a, const void *_b)
return b->pack_int_id - a->pack_int_id;
}

/*
* Return whether the pack index contains an entry with both "oid" and
* "offset". A pack index may contain duplicate OIDs, so an arbitrary
* OID lookup is not enough to validate a particular offset.
*
* Do not use offset_to_pack_pos() here: it may consult an optional '.rev'
* file, which is verified separately, or build an in-memory reverse index
* that remains attached to the pack. Verify the MIDX directly against its
* source pack index instead.
*/
static int pack_index_has_oid_at_offset(struct packed_git *p,
const struct object_id *oid,
off_t offset)
{
struct object_id candidate;
uint32_t pos, i;

if (!bsearch_pack(oid, p, &pos))
return 0;

if (nth_packed_object_offset(p, pos) == offset)
return 1;

for (i = pos; i > 0; i--) {
if (nth_packed_object_id(&candidate, p, i - 1) ||
!oideq(&candidate, oid))
break;
if (nth_packed_object_offset(p, i - 1) == offset)
return 1;
}

for (i = pos + 1; i < p->num_objects; i++) {
if (nth_packed_object_id(&candidate, p, i) ||
!oideq(&candidate, oid))
break;
if (nth_packed_object_offset(p, i) == offset)
return 1;
}

return 0;
}

/*
* Limit calls to display_progress() for performance reasons.
* The interval here was arbitrarily chosen.
@ -1015,7 +1057,6 @@ int verify_midx_file(struct odb_source_packed *source, unsigned flags)
for (i = 0; i < m->num_objects + m->num_objects_in_base; i++) {
struct object_id oid;
struct pack_entry e;
off_t m_offset, p_offset;

if (i > 0 && pairs[i-1].pack_int_id != pairs[i].pack_int_id &&
nth_midxed_pack(m, pairs[i-1].pack_int_id)) {
@ -1040,12 +1081,16 @@ int verify_midx_file(struct odb_source_packed *source, unsigned flags)
break;
}

m_offset = e.offset;
p_offset = find_pack_entry_one(&oid, e.p);

if (m_offset != p_offset)
midx_report(_("incorrect object offset for oid[%d] = %s: %"PRIx64" != %"PRIx64),
pairs[i].pos, oid_to_hex(&oid), m_offset, p_offset);
/*
* Check that the exact offset recorded in the MIDX
* belongs to this OID. A pack index may contain
* duplicate OIDs, in which case an arbitrary OID lookup
* can return a different, equally valid copy than the
* one selected by the MIDX writer.
*/
if (!pack_index_has_oid_at_offset(e.p, &oid, e.offset))
midx_report(_("incorrect object offset for oid[%d] = %s: %"PRIx64),
pairs[i].pos, oid_to_hex(&oid), e.offset);

midx_display_sparse_progress(progress, i + 1);
}

View File

@ -2388,7 +2388,8 @@ static void reuse_partial_packfile_from_bitmap_1(struct bitmap_index *bitmap_git
struct pack_window *w_curs = NULL;
size_t pos = pack->bitmap_pos / BITS_IN_EWORD;

if (allow_ref_delta && !pack->bitmap_pos) {
if (allow_ref_delta && !pack->bitmap_pos &&
pack->bitmap_nr == pack->p->num_objects) {
/*
* If we're processing the first (in the case of a MIDX, the
* preferred pack) or the only (in the case of single-pack
@ -2406,6 +2407,9 @@ static void reuse_partial_packfile_from_bitmap_1(struct bitmap_index *bitmap_git
* When REF_DELTAs are allowed, we can therefore reuse whole
* words at a time without inspecting object headers. Otherwise,
* inspect each object below to avoid reusing a REF_DELTA entry.
*
* The range must also contain every physical pack entry so that
* bitmap and pack positions correspond.
*/
while (pos < result->word_alloc &&
pos < pack->bitmap_nr / BITS_IN_EWORD &&
@ -2536,14 +2540,26 @@ void reuse_partial_packfile_from_bitmap(struct bitmap_index *bitmap_git,
} else {
struct packed_git *pack;
uint32_t pack_int_id;
uint32_t bitmap_nr;

if (bitmap_is_midx(bitmap_git)) {
struct bitmapped_pack bitmapped_pack;
struct multi_pack_index *m = bitmap_git->midx;
uint32_t preferred_pack_pos;

while (m->base_midx)
m = m->base_midx;

if (!m->chunk_bitmapped_packs) {
/*
* Without BTMP, determining the preferred
* pack's range requires scanning the pseudo-pack.
* Skip reuse and leave the bitmap result for
* normal packing.
*/
return;
}

if (midx_preferred_pack(m, &preferred_pack_pos) < 0) {
warning(_("unable to compute preferred pack, disabling pack-reuse"));
return;
@ -2551,6 +2567,12 @@ void reuse_partial_packfile_from_bitmap(struct bitmap_index *bitmap_git,

pack = nth_midxed_pack(m, preferred_pack_pos);
pack_int_id = preferred_pack_pos;

if (nth_bitmapped_pack(m, &bitmapped_pack,
pack_int_id) < 0)
return;
pack = bitmapped_pack.p;
bitmap_nr = bitmapped_pack.bitmap_nr;
} else {
pack = bitmap_git->pack;
/*
@ -2563,13 +2585,14 @@ void reuse_partial_packfile_from_bitmap(struct bitmap_index *bitmap_git,
* that we do not expect to read this field.
*/
pack_int_id = -1;
bitmap_nr = pack->num_objects;
}

if (is_pack_valid(pack)) {
ALLOC_GROW(packs, packs_nr + 1, packs_alloc);
packs[packs_nr].p = pack;
packs[packs_nr].pack_int_id = pack_int_id;
packs[packs_nr].bitmap_nr = pack->num_objects;
packs[packs_nr].bitmap_nr = bitmap_nr;
packs[packs_nr].bitmap_pos = 0;
packs[packs_nr].from_midx = bitmap_git->midx;
packs_nr++;

View File

@ -10,6 +10,7 @@
#include "dir.h"
#include "packfile.h"
#include "delta.h"
#include "ewah/ewok.h"
#include "hash-lookup.h"
#include "commit.h"
#include "object.h"
@ -1077,6 +1078,160 @@ static int get_delta_base_oid(struct packed_git *p,
return -1;
}

/*
* Search duplicate representations for a chain ending in a full object.
* Representations of the same OID are interchangeable as delta bases.
*
* Each path entry is the search frame for one OID. It walks that OID's
* contiguous .idx entries and retains the chosen entry's .pack offset
* for replay.
*/
struct delta_path_entry {
off_t selected_offset; /* zero until a candidate is selected */
uint32_t next; /* index position of the next candidate */
uint32_t remaining; /* total number of candidates remaining */
};

struct delta_path {
struct delta_path_entry *entries;
size_t nr, alloc;
};

static int push_oid_group(struct packed_git *p,
const struct object_id *oid,
struct bitmap *visited, struct delta_path *path)
{
struct object_id candidate;
struct delta_path_entry *entry;
uint32_t first_index_pos, group_index_pos, index_pos;

/*
* Determine the range of index positions referring to duplicate
* copies of the given object.
*
* Any position within that range is OK, since we will determine
* the exact range below.
*/
if (!bsearch_pack(oid, p, &group_index_pos))
return 0;
if (bitmap_get(visited, group_index_pos))
return 0;

first_index_pos = group_index_pos;
while (first_index_pos > 0) {
if (nth_packed_object_id(&candidate, p, first_index_pos - 1) < 0)
return -1;
if (!oideq(&candidate, oid))
break;
first_index_pos--;
}

for (index_pos = first_index_pos; index_pos < p->num_objects; index_pos++) {
if (nth_packed_object_id(&candidate, p, index_pos) < 0)
return -1;
if (!oideq(&candidate, oid))
break;
}

bitmap_set(visited, group_index_pos);

ALLOC_GROW(path->entries, path->nr + 1, path->alloc);
entry = &path->entries[path->nr++];
entry->selected_offset = 0;
entry->next = first_index_pos;
entry->remaining = index_pos - first_index_pos;

return 1;
}

static enum object_type find_delta_path(struct packed_git *p,
struct pack_window **w_curs,
off_t offset,
struct delta_path *path)
{
struct object_id oid;
uint32_t pack_pos;
struct bitmap *visited;
enum object_type result = OBJ_BAD;

if (offset_to_pack_pos(p, offset, &pack_pos) < 0)
return OBJ_BAD;
if (nth_packed_object_id(&oid, p, pack_pos_to_index(p, pack_pos)) < 0)
return OBJ_BAD;

visited = bitmap_new();
if (push_oid_group(p, &oid, visited, path) != 1)
goto done;

/*
* Search depth-first for a chain ending in a full object. Each frame
* tries every representation of one OID; a delta pushes its base OID,
* while exhausting a frame backtracks to its parent.
*/
while (path->nr) {
struct delta_path_entry *entry = &path->entries[path->nr - 1];
enum object_type candidate_type;
off_t curpos;
size_t size;

/*
* This OID has no path to a full object. Let its parent try
* another representation; exhausting the root fails the search.
*/
if (!entry->remaining) {
path->nr--;
continue;
}

entry->selected_offset =
nth_packed_object_offset(p, entry->next++);
entry->remaining--;
curpos = entry->selected_offset;
candidate_type = unpack_object_header(p, w_curs, &curpos, &size);

/*
* A full object terminates the chain, and its type is
* inherited by every delta above it. A delta continues
* at its base; any other type rejects only this
* representation.
*/
switch (candidate_type) {
case OBJ_COMMIT:
case OBJ_TREE:
case OBJ_BLOB:
case OBJ_TAG:
result = candidate_type;
goto done;
case OBJ_OFS_DELTA:
case OBJ_REF_DELTA:
break;
default:
/*
* A bad or unknown type rejects only this copy;
* another representation of the same OID may
* still work.
*/
continue;
}

/*
* Descend to this delta's base. A malformed reference
* or a missing or already-visited base rejects this
* copy. A newly pushed base is examined next; an index
* error aborts the search.
*/
if (get_delta_base_oid(p, w_curs, curpos, &oid, candidate_type,
entry->selected_offset))
continue;
if (push_oid_group(p, &oid, visited, path) < 0)
goto done;
}

done:
bitmap_free(visited);
return result;
}

static int retry_bad_packed_offset(struct repository *r,
struct packed_git *p,
off_t obj_offset)
@ -1105,11 +1260,27 @@ static enum object_type packed_to_object_type(struct repository *r,
{
off_t small_poi_stack[POI_STACK_PREALLOC];
off_t *poi_stack = small_poi_stack;
off_t root_offset = obj_offset;
int poi_stack_nr = 0, poi_stack_alloc = POI_STACK_PREALLOC;

while (type == OBJ_OFS_DELTA || type == OBJ_REF_DELTA) {
off_t base_offset;
size_t size;

if (poi_stack_nr > 0 && poi_stack_nr % 2 == 0 &&
obj_offset == poi_stack[poi_stack_nr / 2]) {
struct delta_path path = { 0 };
/*
* Normal lookup returned to the same pack
* entry. Restart from the requested object
* using alternate representations.
*/
type = find_delta_path(p, w_curs, root_offset, &path);
free(path.entries);
if (type == OBJ_BAD)
goto unwind;
break;
}
/* Push the object we're going to leave behind */
if (poi_stack_nr >= poi_stack_alloc && poi_stack == small_poi_stack) {
poi_stack_alloc = alloc_nr(poi_stack_nr);
@ -1525,9 +1696,11 @@ void *unpack_entry(struct repository *r, struct packed_git *p, off_t obj_offset,
{
struct pack_window *w_curs = NULL;
off_t curpos = obj_offset;
off_t root_offset = obj_offset;
void *data = NULL;
size_t size;
enum object_type type;
struct delta_path path = { 0 };
struct unpack_entry_stack_ent small_delta_stack[UNPACK_ENTRY_STACK_PREALLOC];
struct unpack_entry_stack_ent *delta_stack = small_delta_stack;
int delta_stack_nr = 0, delta_stack_alloc = UNPACK_ENTRY_STACK_PREALLOC;
@ -1553,6 +1726,22 @@ void *unpack_entry(struct repository *r, struct packed_git *p, off_t obj_offset,
break;
}

if (!path.nr &&
delta_stack_nr > 0 && delta_stack_nr % 2 == 0 &&
obj_offset == delta_stack[delta_stack_nr / 2].obj_offset) {
/*
* Normal lookup returned to the same pack
* entry. Find an acyclic path if one exists,
* discard this walk, and replay that path.
*/
if (find_delta_path(p, &w_curs, root_offset,
&path) == OBJ_BAD)
break;
delta_stack_nr = 0;
curpos = obj_offset = path.entries[0].selected_offset;
continue;
}

if (do_check_packed_object_crc && p->index_version > 1) {
uint32_t pack_pos, index_pos;
off_t len;
@ -1591,6 +1780,15 @@ void *unpack_entry(struct repository *r, struct packed_git *p, off_t obj_offset,
break;
}

/* Use the base chosen by recovery, not the one from normal lookup. */
if (path.nr) {
size_t path_pos = (size_t)delta_stack_nr + 1;

if (path_pos >= path.nr)
BUG("alternate delta path ends in a delta");
base_offset = path.entries[path_pos].selected_offset;
}

/* push object, proceed to base */
if (delta_stack_nr >= delta_stack_alloc
&& delta_stack == small_delta_stack) {
@ -1731,6 +1929,7 @@ void *unpack_entry(struct repository *r, struct packed_git *p, off_t obj_offset,

out:
unuse_pack(&w_curs);
free(path.entries);

if (delta_stack != small_delta_stack)
free(delta_stack);

View File

@ -50,6 +50,9 @@ static int add_packed_object(const struct object_id *oid,

oi.typep = &type;

if (packlist_find(packed, oid))
die("pack contains duplicate object %s", oid_to_hex(oid));

entry = packlist_alloc(packed, oid);
entry->idx.offset = nth_packed_object_offset(pack, pos);
if (packed_object_info(NULL, pack, entry->idx.offset, &oi) < 0)

View File

@ -11,12 +11,15 @@
* Display the path(s), one per line, of the packfile(s) containing
* the given object.
*
* With '--show-offset', display the offset selected by
* find_pack_entry_one() instead of the packfile path.
*
* If '--check-count <n>' is passed, then error out if the number of
* packfiles containing the object is not <n>.
*/

static const char *const find_pack_usage[] = {
"test-tool find-pack [--check-count <n>] <object>",
"test-tool find-pack [--check-count <n>] [--show-offset] <object>",
NULL
};

@ -24,11 +27,13 @@ int cmd__find_pack(int argc, const char **argv)
{
struct object_id oid;
struct packed_git *p;
int count = -1, actual_count = 0;
int count = -1, actual_count = 0, show_offset = 0;
const char *prefix = setup_git_directory(the_repository);

struct option options[] = {
OPT_INTEGER('c', "check-count", &count, "expected number of packs"),
OPT_BOOL(0, "show-offset", &show_offset,
"show matching pack offsets"),
OPT_END(),
};

@ -40,8 +45,13 @@ int cmd__find_pack(int argc, const char **argv)
die("cannot parse %s as an object name", argv[0]);

repo_for_each_pack(the_repository, p) {
if (find_pack_entry_one(&oid, p)) {
printf("%s\n", p->pack_name);
off_t offset = find_pack_entry_one(&oid, p);

if (offset) {
if (show_offset)
printf("%"PRIuMAX"\n", (uintmax_t)offset);
else
printf("%s\n", p->pack_name);
actual_count++;
}
}

View File

@ -27,6 +27,11 @@ HI_SHA1=$EMPTY_BLOB
# duplicate runs).
MISSING_SHA1=$(test_oid missing_oid)

# Three distinct objects for tests where physical pack order matters.
A=$(test_oid packlib_7_0)
B=$LO_SHA1
C=$HI_SHA1

# git will never intentionally create packfiles with
# duplicate objects, so we have to construct them by hand.
#
@ -54,6 +59,13 @@ test_expect_success 'index-pack will allow duplicate objects by default' '
git index-pack --stdin <dups.pack
'

test_expect_success 'bitmap writer rejects duplicate objects' '
pack=$(ls .git/objects/pack/pack-*.pack) &&
test_must_fail test-tool bitmap write "$(basename "$pack")" \
</dev/null 2>err &&
test_grep "fatal: pack contains duplicate object" err
'

test_expect_success 'create batch-check test vectors' '
cat >input <<-EOF &&
$LO_SHA1
@ -72,6 +84,54 @@ test_expect_success 'lookup in duplicated pack' '
test_cmp expect actual
'

test_expect_success 'verify MIDX containing duplicated pack objects' '
git multi-pack-index write &&
test-tool read-midx --show-objects .git/objects >midx-objects &&
midx_offset=$(
awk -v oid="$LO_SHA1" "\$1 == oid { print \$2 }" <midx-objects
) &&
lookup_offset=$(
test-tool find-pack --check-count=1 --show-offset "$LO_SHA1"
) &&
test "$midx_offset" -ne "$lookup_offset" &&
git multi-pack-index verify &&
git fsck --full
'

test_expect_success 'duplicate entries remain in pack reverse index' '
clear_packs &&
{
pack_header 4 &&
pack_obj $A &&
pack_obj $B &&
pack_obj $A &&
pack_obj $C
} >physical-order.pack &&
pack_trailer physical-order.pack &&

test_must_fail git index-pack --rev-index --stdin --strict \
<physical-order.pack 2>err &&
test_grep "appears twice in the pack" err &&

git index-pack --rev-index --stdin <physical-order.pack &&
git show-index <"$(ls .git/objects/pack/pack-*.idx)" >offsets.raw &&

sort -n offsets.raw | grep -A1 "$B" | cut -d" " -f1 >adjacent &&
echo $(($(tail -n1 adjacent) - $(head -n1 adjacent))) >expect &&
echo "$B" >in &&

GIT_TEST_REV_INDEX_DIE_IN_MEMORY=1 \
git cat-file --batch-check="%(objectsize:disk)" \
<in >actual.disk &&
GIT_TEST_REV_INDEX_DIE_ON_DISK=1 \
git -c pack.readReverseIndex=false \
cat-file --batch-check="%(objectsize:disk)" \
<in >actual.mem &&

test_cmp expect actual.disk &&
test_cmp expect actual.mem
'

test_expect_success 'index-pack can reject packs with duplicates' '
clear_packs &&
create_pack dups.pack 2 &&

View File

@ -9,6 +9,83 @@ test_description='test index-pack handling of delta cycles in packfiles'
A=$(test_oid packlib_7_0)
B=$(test_oid packlib_7_76)

# Copy the entries from a complete pack without its header or trailer.
pack_entries () {
entry_size=$(wc -c <"$1") &&
dd if="$1" bs=1 skip=12 \
count=$((entry_size - 12 - $(test_oid rawsz))) 2>/dev/null
}

# B as an OFS_DELTA against A at the given one-byte distance.
pack_obj_b_ofs_a () {
pack_obj "$B" "$A" >b-ref.tmp &&
printf "\145" &&
printf "\\$(printf "%03o" "$1")" &&
dd if=b-ref.tmp bs=1 skip=$((1 + $(test_oid rawsz))) 2>/dev/null
}

# Return the base of the first one-byte-header REF_DELTA for the given OID.
first_ref_base () {
idx=$(echo .git/objects/pack/*.idx) &&
offset=$(git show-index <"$idx" |
awk -v oid="$1" '$2 == oid { print $1; exit }') &&
dd if="${idx%.idx}.pack" bs=1 skip=$((offset + 1)) \
count=$(test_oid rawsz) 2>/dev/null |
test-tool hexdump |
tr -d " \n"
}

# The order of equal-OID entries in the .idx is unspecified. Retain a pack
# which selects $1 as a delta against $2. Unless $3 is "-", also require the
# first copy searched during recovery to be a REF_DELTA against $3.
install_cycle () {
cycle_oid=$1 &&
cycle_base=$2 &&
first_base=$3 &&
shift 3 &&
for pack
do
clear_packs &&
git index-pack --fix-thin --stdin <"$pack" &&
selected_base=$(echo "$cycle_oid" |
git cat-file --batch-check="%(deltabase)") ||
return 1
if test "$selected_base" = "$cycle_base" &&
{ test "$first_base" = "-" ||
test "$(first_ref_base "$cycle_oid")" = "$first_base"; }
then
return 0
fi
done
return 1
}

make_cycle_pack () {
cycle_pack=$1 &&
shift &&
test-tool -C alt-source pack-deltas --num-objects=6 >refs.tmp <<-EOF &&
REF_DELTA $T $X
REF_DELTA $X $1
REF_DELTA $Y $Z
REF_DELTA $Z $X
REF_DELTA $X $2
REF_DELTA $X $3
EOF
{
pack_header 8 &&
pack_entries refs.tmp &&
cat a-full &&
pack_obj_b_ofs_a "$a_full_size"
} >"$cycle_pack" &&
pack_trailer "$cycle_pack"
}

check_blob () {
test "$(git cat-file -t "$1")" = blob &&
git cat-file blob "$1" >actual &&
test_cmp_bin "$2" actual
}

# double-check our hand-constucted packs
test_expect_success 'index-pack works with a single delta (A->B)' '
clear_packs &&
@ -67,18 +144,77 @@ test_expect_success 'failover to an object in another pack' '
'

test_expect_success 'failover to a duplicate object in the same pack' '
clear_packs &&
{
pack_header 3 &&
pack_obj $A &&
pack_obj $B $A &&
pack_obj $A $B
} >recoverable-1.pack &&
pack_trailer recoverable-1.pack &&
{
pack_header 3 &&
pack_obj $A $B &&
pack_obj $B $A &&
pack_obj $A
} >recoverable.pack &&
pack_trailer recoverable.pack &&
} >recoverable-2.pack &&
pack_trailer recoverable-2.pack &&

# This cycle does not fail since the existence of a full copy
# of A in the pack allows us to resolve the cycle.
git index-pack --fix-thin --stdin <recoverable.pack
# The selected copy of A is part of the cycle, but the full copy
# lets both type and content lookups resolve it.
install_cycle "$A" "$B" - recoverable-1.pack recoverable-2.pack &&
printf "\7\0" >expect &&
check_blob "$A" expect
'

test_expect_success 'failover from a mixed REF/OFS cycle' '
pack_obj "$A" "$B" >a-ref &&
pack_obj "$B" >b-full &&
a_ref_size=$(wc -c <a-ref) &&
b_full_size=$(wc -c <b-full) &&

{
pack_header 3 &&
cat a-ref &&
cat b-full &&
pack_obj_b_ofs_a "$((a_ref_size + b_full_size))"
} >mixed-1.pack &&
pack_trailer mixed-1.pack &&
{
pack_header 3 &&
cat a-ref &&
pack_obj_b_ofs_a "$a_ref_size" &&
cat b-full
} >mixed-2.pack &&
pack_trailer mixed-2.pack &&

# The REF_DELTA for A selects the OFS_DELTA copy of B; the
# full B is its escape.
install_cycle "$B" "$A" - mixed-1.pack mixed-2.pack &&
printf "\7\0" >expect &&
check_blob "$A" expect
'

test_expect_success 'failover after a tail into a three-object delta cycle' '
git init alt-source &&
printf "\7\76" |
git -C alt-source hash-object -w --stdin >/dev/null &&
X=$(printf x | git -C alt-source hash-object -w --stdin) &&
Y=$(printf y | git -C alt-source hash-object -w --stdin) &&
Z=$(printf z | git -C alt-source hash-object -w --stdin) &&
printf "tail T\n" >tail &&
T=$(git -C alt-source hash-object -w --stdin <tail) &&

pack_obj "$A" >a-full &&
a_full_size=$(wc -c <a-full) &&
make_cycle_pack alternate-1.pack "$B" "$Y" "$Y" &&
make_cycle_pack alternate-2.pack "$Y" "$B" "$Y" &&
make_cycle_pack alternate-3.pack "$Y" "$Y" "$B" &&

# Lookup of T follows T->X->Y->Z->X. Recovery must exhaust that
# branch, then use X->B->A, whose final edge is an OFS_DELTA.
install_cycle "$X" "$Y" "$Y" \
alternate-1.pack alternate-2.pack alternate-3.pack &&
check_blob "$T" tail
'

test_expect_success 'index-pack works with thin pack A->B->C with B on disk' '

View File

@ -4,6 +4,7 @@ test_description='pack-objects multi-pack reuse'

. ./test-lib.sh
. "$TEST_DIRECTORY"/lib-bitmap.sh
. "$TEST_DIRECTORY"/lib-pack.sh

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

# B as an OFS_DELTA against A at the given one-byte distance.
pack_obj_b_ofs_a () {
pack_obj "$B" "$A" >b-ref.tmp &&
printf "\145" &&
printf "\\$(printf "%03o" "$1")" &&
dd if=b-ref.tmp bs=1 skip=$((1 + $(test_oid rawsz))) 2>/dev/null
}

# test_pack_objects_reused_all <pack-reused> <packs-reused>
test_pack_objects_reused_all () {
: >trace2.txt &&
@ -304,4 +313,84 @@ test_expect_success 'duplicate objects with verbatim reuse' '
)
'

test_expect_success 'reuse with intra-pack duplicate objects' '
git init intra-pack-duplicate-objects &&
(
cd intra-pack-duplicate-objects &&

# Make enough objects to exercise whole-word reuse.
test_commit_bulk 20 &&
test_commit --printf A a "\7\0" &&
test_commit --printf B b "\7\76" &&

objects_nr=$(git rev-list --count --objects --all) &&
git rev-list --objects --all |
cut -d" " -f1 >objects &&
A=$(test_oid packlib_7_0) &&
B=$(test_oid packlib_7_76) &&
grep -v -e "^$A$" -e "^$B$" objects >rest &&
pack_obj "$A" >a-full &&
pack_obj "$B" >b-full &&
while read oid
do
pack_obj "$oid" || exit 1
done <rest >rest.entries &&
{
# Arrange the pack as A, B, A, C..., so that physical
# positions diverge from MIDX pseudo-pack order.
pack_header $((objects_nr + 1)) &&
cat a-full b-full a-full rest.entries
} >duplicate.pack &&
pack_trailer duplicate.pack &&
clear_packs &&
git index-pack --stdin <duplicate.pack &&

git multi-pack-index write --bitmap &&
git config pack.allowPackReuse single &&
test_pack_objects_reused_all "$objects_nr" 1 &&
rm -f got.idx &&
test_env GIT_TEST_MIDX_READ_BTMP=false \
test_pack_objects_reused_all 0 0
)
'

test_expect_success 'omit delta whose duplicate base is not selected' '
(
cd intra-pack-duplicate-objects &&

A=$(test_oid packlib_7_0) &&
B=$(test_oid packlib_7_76) &&
objects_nr=$(wc -l <objects) &&

a_size=$(wc -c <a-full) &&
test "$((2 * a_size))" -lt 128 &&

# The .idx order of duplicate OIDs is unspecified. Try a delta
# against each copy and keep the pack whose base was omitted.
for distance in "$((2 * a_size))" "$a_size"
do
base_offset=$((12 + 2 * a_size - distance)) &&
pack_obj_b_ofs_a "$distance" >b-delta &&
{
pack_header "$((objects_nr + 1))" &&
cat a-full a-full b-delta rest.entries
} >candidate.pack &&
pack_trailer candidate.pack &&
clear_packs &&
git index-pack --stdin <candidate.pack &&
git multi-pack-index write --bitmap || return 1

selected_offset=$(
test-tool read-midx --show-objects "$objdir" |
awk -v oid="$A" "\$1 == oid { print \$2 }"
) || return 1
test -n "$selected_offset" || return 1
test "$selected_offset" = "$base_offset" || break
done &&
test "$selected_offset" != "$base_offset" &&

test_pack_objects_reused_all "$((objects_nr - 1))" 1
)
'

test_done