midx: support custom --base for incremental MIDX writes

Both `compact` and `write --incremental` fix the base of the resulting
MIDX layer: `compact` always places the compacted result on top of
"from's" immediate parent in the chain, and `write --incremental` always
appends a new layer to the existing tip. In both cases the base is not
configurable.

Future callers need additional flexibility. For instance, the incremental
MIDX-based repacking code may wish to write a layer based on some
intermediate ancestor rather than the current tip, or produce a root
layer when replacing the bottommost entries in the chain.

Introduce a new `--base` option for both subcommands to specify the
checksum of the MIDX layer to use as the base. The given checksum must
refer to a valid layer in the MIDX chain that is an ancestor of the
topmost layer being written or compacted.

The special value "none" is accepted to produce a root layer with no
parent. This will be needed when the incremental repacking machinery
determines that the bottommost layers of the chain should be replaced.

If no `--base` is given, behavior is unchanged: `compact` uses "from's"
immediate parent in the chain, and `write` appends to the existing tip.

For the `write` subcommand, `--base` requires `--no-write-chain-file`. A plain
`write --incremental` appends a new layer to the live chain tip with no
mechanism to atomically replace it; overriding the base would produce a
layer that does not extend the tip, breaking chain invariants. With
`--no-write-chain-file` the chain is left unmodified and the caller is
responsible for assembling a valid chain.

For `compact`, no such restriction applies. The compaction operation
atomically replaces the compacted range in the chain file, so writing
the result on top of any valid ancestor preserves chain invariants.

Signed-off-by: Taylor Blau <me@ttaylorr.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Taylor Blau
2026-05-19 11:57:54 -04:00
committed by Junio C Hamano
parent 8d342ed4b5
commit 0cd2255e64
6 changed files with 178 additions and 9 deletions

View File

@@ -12,8 +12,10 @@ SYNOPSIS
'git multi-pack-index' [<options>] write [--preferred-pack=<pack>] 'git multi-pack-index' [<options>] write [--preferred-pack=<pack>]
[--[no-]bitmap] [--[no-]incremental] [--[no-]stdin-packs] [--[no-]bitmap] [--[no-]incremental] [--[no-]stdin-packs]
[--refs-snapshot=<path>] [--[no-]write-chain-file] [--refs-snapshot=<path>] [--[no-]write-chain-file]
[--base=<checksum>]
'git multi-pack-index' [<options>] compact [--[no-]incremental] 'git multi-pack-index' [<options>] compact [--[no-]incremental]
[--[no-]bitmap] [--[no-]write-chain-file] <from> <to> [--[no-]bitmap] [--base=<checksum>] [--[no-]write-chain-file]
<from> <to>
'git multi-pack-index' [<options>] verify 'git multi-pack-index' [<options>] verify
'git multi-pack-index' [<options>] expire 'git multi-pack-index' [<options>] expire
'git multi-pack-index' [<options>] repack [--batch-size=<size>] 'git multi-pack-index' [<options>] repack [--batch-size=<size>]
@@ -90,6 +92,13 @@ marker).
The checksum of the new layer is printed to standard The checksum of the new layer is printed to standard
output, allowing the caller to assemble and write the output, allowing the caller to assemble and write the
chain itself. Requires `--incremental`. chain itself. Requires `--incremental`.
--base=<checksum>::
Specify the checksum of an existing MIDX layer to use
as the base when writing a new incremental layer.
The special value `none` indicates that the new layer
should have no base (i.e., it becomes a root layer).
Requires `--no-write-chain-file`.
-- --
compact:: compact::
@@ -110,6 +119,12 @@ compact::
MIDX layer but do not update the multi-pack-index-chain MIDX layer but do not update the multi-pack-index-chain
file. The checksum of the new layer is printed to file. The checksum of the new layer is printed to
standard output. Requires `--incremental`. standard output. Requires `--incremental`.
--base=<checksum>::
Specify the checksum of an existing MIDX layer to use
as the base for the compacted result, instead of using
the immediate parent of `<from>`. The special value
`none` indicates that the result should have no base.
-- --
+ +
Note that the compact command requires writing a version-2 midx that Note that the compact command requires writing a version-2 midx that

View File

@@ -16,11 +16,13 @@
#define BUILTIN_MIDX_WRITE_USAGE \ #define BUILTIN_MIDX_WRITE_USAGE \
N_("git multi-pack-index [<options>] write [--preferred-pack=<pack>]\n" \ N_("git multi-pack-index [<options>] write [--preferred-pack=<pack>]\n" \
" [--[no-]bitmap] [--[no-]incremental] [--[no-]stdin-packs]\n" \ " [--[no-]bitmap] [--[no-]incremental] [--[no-]stdin-packs]\n" \
" [--refs-snapshot=<path>] [--[no-]write-chain-file]") " [--refs-snapshot=<path>] [--[no-]write-chain-file]\n" \
" [--base=<checksum>]")
#define BUILTIN_MIDX_COMPACT_USAGE \ #define BUILTIN_MIDX_COMPACT_USAGE \
N_("git multi-pack-index [<options>] compact [--[no-]incremental]\n" \ N_("git multi-pack-index [<options>] compact [--[no-]incremental]\n" \
" [--[no-]bitmap] [--[no-]write-chain-file] <from> <to>") " [--[no-]bitmap] [--base=<checksum>] [--[no-]write-chain-file]\n" \
" <from> <to>")
#define BUILTIN_MIDX_VERIFY_USAGE \ #define BUILTIN_MIDX_VERIFY_USAGE \
N_("git multi-pack-index [<options>] verify") N_("git multi-pack-index [<options>] verify")
@@ -63,6 +65,7 @@ static char const * const builtin_multi_pack_index_usage[] = {
static struct opts_multi_pack_index { static struct opts_multi_pack_index {
char *object_dir; char *object_dir;
const char *preferred_pack; const char *preferred_pack;
const char *incremental_base;
char *refs_snapshot; char *refs_snapshot;
unsigned long batch_size; unsigned long batch_size;
unsigned flags; unsigned flags;
@@ -151,6 +154,8 @@ static int cmd_multi_pack_index_write(int argc, const char **argv,
N_("pack for reuse when computing a multi-pack bitmap")), N_("pack for reuse when computing a multi-pack bitmap")),
OPT_BIT(0, "bitmap", &opts.flags, N_("write multi-pack bitmap"), OPT_BIT(0, "bitmap", &opts.flags, N_("write multi-pack bitmap"),
MIDX_WRITE_BITMAP | MIDX_WRITE_REV_INDEX), MIDX_WRITE_BITMAP | MIDX_WRITE_REV_INDEX),
OPT_STRING(0, "base", &opts.incremental_base, N_("checksum"),
N_("base MIDX for incremental writes")),
OPT_BIT(0, "incremental", &opts.flags, OPT_BIT(0, "incremental", &opts.flags,
N_("write a new incremental MIDX"), MIDX_WRITE_INCREMENTAL), N_("write a new incremental MIDX"), MIDX_WRITE_INCREMENTAL),
OPT_NEGBIT(0, "write-chain-file", &opts.flags, OPT_NEGBIT(0, "write-chain-file", &opts.flags,
@@ -190,6 +195,13 @@ static int cmd_multi_pack_index_write(int argc, const char **argv,
options); options);
} }
if (opts.incremental_base &&
!(opts.flags & MIDX_WRITE_NO_CHAIN)) {
error(_("cannot use --base without --no-write-chain-file"));
usage_with_options(builtin_multi_pack_index_write_usage,
options);
}
source = handle_object_dir_option(repo); source = handle_object_dir_option(repo);
FREE_AND_NULL(options); FREE_AND_NULL(options);
@@ -201,7 +213,8 @@ static int cmd_multi_pack_index_write(int argc, const char **argv,
ret = write_midx_file_only(source, &packs, ret = write_midx_file_only(source, &packs,
opts.preferred_pack, opts.preferred_pack,
opts.refs_snapshot, opts.flags); opts.refs_snapshot,
opts.incremental_base, opts.flags);
string_list_clear(&packs, 0); string_list_clear(&packs, 0);
free(opts.refs_snapshot); free(opts.refs_snapshot);
@@ -229,6 +242,8 @@ static int cmd_multi_pack_index_compact(int argc, const char **argv,
struct option *options; struct option *options;
static struct option builtin_multi_pack_index_compact_options[] = { static struct option builtin_multi_pack_index_compact_options[] = {
OPT_STRING(0, "base", &opts.incremental_base, N_("checksum"),
N_("base MIDX for incremental writes")),
OPT_BIT(0, "bitmap", &opts.flags, N_("write multi-pack bitmap"), OPT_BIT(0, "bitmap", &opts.flags, N_("write multi-pack bitmap"),
MIDX_WRITE_BITMAP | MIDX_WRITE_REV_INDEX), MIDX_WRITE_BITMAP | MIDX_WRITE_REV_INDEX),
OPT_BIT(0, "incremental", &opts.flags, OPT_BIT(0, "incremental", &opts.flags,
@@ -290,7 +305,8 @@ static int cmd_multi_pack_index_compact(int argc, const char **argv,
die(_("MIDX %s must be an ancestor of %s"), argv[0], argv[1]); die(_("MIDX %s must be an ancestor of %s"), argv[0], argv[1]);
} }
ret = write_midx_file_compact(source, from_midx, to_midx, opts.flags); ret = write_midx_file_compact(source, from_midx, to_midx,
opts.incremental_base, opts.flags);
return ret; return ret;
} }

View File

@@ -1247,6 +1247,7 @@ struct write_midx_opts {
const char *preferred_pack_name; const char *preferred_pack_name;
const char *refs_snapshot; const char *refs_snapshot;
const char *incremental_base;
unsigned flags; unsigned flags;
}; };
@@ -1330,11 +1331,32 @@ static int write_midx_internal(struct write_midx_opts *opts)
/* /*
* If compacting MIDX layer(s) in the range [from, to], then the * If compacting MIDX layer(s) in the range [from, to], then the
* compacted MIDX will share the same base MIDX as 'from'. * compacted MIDX will share the same base MIDX as 'from',
* unless a custom --base is specified (see below).
*/ */
if (ctx.compact) if (ctx.compact)
ctx.base_midx = ctx.compact_from->base_midx; ctx.base_midx = ctx.compact_from->base_midx;
if (opts->incremental_base) {
if (!strcmp(opts->incremental_base, "none")) {
ctx.base_midx = NULL;
} else {
while (ctx.base_midx) {
const char *cmp = midx_get_checksum_hex(ctx.base_midx);
if (!strcmp(opts->incremental_base, cmp))
break;
ctx.base_midx = ctx.base_midx->base_midx;
}
if (!ctx.base_midx) {
error(_("could not find base MIDX '%s'"),
opts->incremental_base);
goto cleanup;
}
}
}
ctx.nr = 0; ctx.nr = 0;
ctx.alloc = ctx.m ? ctx.m->num_packs + ctx.m->num_packs_in_base : 16; ctx.alloc = ctx.m ? ctx.m->num_packs + ctx.m->num_packs_in_base : 16;
ctx.info = NULL; ctx.info = NULL;
@@ -1827,7 +1849,8 @@ cleanup:
int write_midx_file(struct odb_source *source, int write_midx_file(struct odb_source *source,
const char *preferred_pack_name, const char *preferred_pack_name,
const char *refs_snapshot, unsigned flags) const char *refs_snapshot,
unsigned flags)
{ {
struct write_midx_opts opts = { struct write_midx_opts opts = {
.source = source, .source = source,
@@ -1842,13 +1865,16 @@ int write_midx_file(struct odb_source *source,
int write_midx_file_only(struct odb_source *source, int write_midx_file_only(struct odb_source *source,
struct string_list *packs_to_include, struct string_list *packs_to_include,
const char *preferred_pack_name, const char *preferred_pack_name,
const char *refs_snapshot, unsigned flags) const char *refs_snapshot,
const char *incremental_base,
unsigned flags)
{ {
struct write_midx_opts opts = { struct write_midx_opts opts = {
.source = source, .source = source,
.packs_to_include = packs_to_include, .packs_to_include = packs_to_include,
.preferred_pack_name = preferred_pack_name, .preferred_pack_name = preferred_pack_name,
.refs_snapshot = refs_snapshot, .refs_snapshot = refs_snapshot,
.incremental_base = incremental_base,
.flags = flags, .flags = flags,
}; };
@@ -1858,12 +1884,14 @@ int write_midx_file_only(struct odb_source *source,
int write_midx_file_compact(struct odb_source *source, int write_midx_file_compact(struct odb_source *source,
struct multi_pack_index *from, struct multi_pack_index *from,
struct multi_pack_index *to, struct multi_pack_index *to,
const char *incremental_base,
unsigned flags) unsigned flags)
{ {
struct write_midx_opts opts = { struct write_midx_opts opts = {
.source = source, .source = source,
.compact_from = from, .compact_from = from,
.compact_to = to, .compact_to = to,
.incremental_base = incremental_base,
.flags = flags | MIDX_WRITE_COMPACT, .flags = flags | MIDX_WRITE_COMPACT,
}; };

5
midx.h
View File

@@ -132,10 +132,13 @@ int write_midx_file(struct odb_source *source,
int write_midx_file_only(struct odb_source *source, int write_midx_file_only(struct odb_source *source,
struct string_list *packs_to_include, struct string_list *packs_to_include,
const char *preferred_pack_name, const char *preferred_pack_name,
const char *refs_snapshot, unsigned flags); const char *refs_snapshot,
const char *incremental_base,
unsigned flags);
int write_midx_file_compact(struct odb_source *source, int write_midx_file_compact(struct odb_source *source,
struct multi_pack_index *from, struct multi_pack_index *from,
struct multi_pack_index *to, struct multi_pack_index *to,
const char *incremental_base,
unsigned flags); unsigned flags);
void clear_midx_file(struct repository *r); void clear_midx_file(struct repository *r);
int verify_midx_file(struct odb_source *source, unsigned flags); int verify_midx_file(struct odb_source *source, unsigned flags);

View File

@@ -113,6 +113,36 @@ test_expect_success 'write non-incremental MIDX layer with --no-write-chain-file
test_grep "cannot use --no-write-chain-file without --incremental" err test_grep "cannot use --no-write-chain-file without --incremental" err
' '
test_expect_success 'write MIDX layer with --base without --no-write-chain-file' '
test_must_fail git multi-pack-index write --bitmap --incremental \
--base=none 2>err &&
test_grep "cannot use --base without --no-write-chain-file" err
'
test_expect_success 'write MIDX layer with --base=none and --no-write-chain-file' '
test_commit base-none &&
git repack -d &&
cp "$midx_chain" "$midx_chain.bak" &&
layer="$(git multi-pack-index write --bitmap --incremental \
--no-write-chain-file --base=none)" &&
test_cmp "$midx_chain.bak" "$midx_chain" &&
test_path_is_file "$midxdir/multi-pack-index-$layer.midx"
'
test_expect_success 'write MIDX layer with --base=<hash> and --no-write-chain-file' '
test_commit base-hash &&
git repack -d &&
cp "$midx_chain" "$midx_chain.bak" &&
layer="$(git multi-pack-index write --bitmap --incremental \
--no-write-chain-file --base="$(nth_line 1 "$midx_chain")")" &&
test_cmp "$midx_chain.bak" "$midx_chain" &&
test_path_is_file "$midxdir/multi-pack-index-$layer.midx"
'
for reuse in false single multi for reuse in false single multi
do do
test_expect_success "full clone (pack.allowPackReuse=$reuse)" ' test_expect_success "full clone (pack.allowPackReuse=$reuse)" '

View File

@@ -304,6 +304,7 @@ test_expect_success 'MIDX compaction with --no-write-chain-file' '
layer="$(git multi-pack-index compact --incremental \ layer="$(git multi-pack-index compact --incremental \
--no-write-chain-file \ --no-write-chain-file \
--base="$(nth_line 1 "$midx_chain")" \
"$(nth_line 2 "$midx_chain")" \ "$(nth_line 2 "$midx_chain")" \
"$(nth_line 3 "$midx_chain")")" && "$(nth_line 3 "$midx_chain")")" &&
@@ -326,4 +327,80 @@ test_expect_success 'MIDX compaction with --no-write-chain-file' '
) )
' '
test_expect_success 'MIDX compaction with --base' '
git init midx-compact-with--base &&
(
cd midx-compact-with--base &&
git config maintenance.auto false &&
write_packs A B C D &&
test_line_count = 4 "$midx_chain" &&
cp "$midx_chain" "$midx_chain.bak" &&
git multi-pack-index compact --incremental \
--base="$(nth_line 1 "$midx_chain")" \
"$(nth_line 3 "$midx_chain")" \
"$(nth_line 4 "$midx_chain")" &&
test_line_count = 2 $midx_chain &&
nth_line 1 "$midx_chain.bak" >expect &&
nth_line 1 "$midx_chain" >actual &&
test_cmp expect actual
)
'
test_expect_success 'MIDX compaction with --base=none' '
git init midx-compact-base-none &&
(
cd midx-compact-base-none &&
git config maintenance.auto false &&
write_packs A B C D &&
test_line_count = 4 $midx_chain &&
cp "$midx_chain" "$midx_chain".bak &&
# Compact the two bottommost layers (A and B) into a new
# root layer with no parent.
git multi-pack-index compact --incremental \
--base=none \
"$(nth_line 1 "$midx_chain")" \
"$(nth_line 2 "$midx_chain")" &&
test_line_count = 3 $midx_chain &&
# The upper layers (C and D) should be preserved
# unchanged.
nth_line 3 "$midx_chain.bak" >expect &&
nth_line 4 "$midx_chain.bak" >>expect &&
nth_line 2 "$midx_chain" >actual &&
nth_line 3 "$midx_chain" >>actual &&
test_cmp expect actual
)
'
test_expect_success 'MIDX compaction with bogus --base checksum' '
git init midx-compact-bogus-base &&
(
cd midx-compact-bogus-base &&
git config maintenance.auto false &&
write_packs A B C &&
test_must_fail git multi-pack-index compact --incremental \
--base=deadbeef \
"$(nth_line 2 "$midx_chain")" \
"$(nth_line 3 "$midx_chain")" 2>err &&
test_grep "could not find base MIDX" err
)
'
test_done test_done