* [PATCH v4 02/16] midx: use `strset` for retained MIDX files
From: Taylor Blau @ 2026-05-19 15:57 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1779206239.git.me@ttaylorr.com>
Both `clear_midx_files_ext()` and `clear_incremental_midx_files_ext()`
build a list of filenames to keep while pruning stale MIDX files. Today
they hand-roll an array instead of using a `strset`, thus requiring us
to pass an additional length parameter, and makes lookups linear.
Replace the bare array with a `strset` which can be passed around as a
single parameter. Though it improves lookup performance, the difference
is likely immeasurable given how small the keep_hashes array typically
is.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
midx.c | 57 +++++++++++++++++++++++++++------------------------------
1 file changed, 27 insertions(+), 30 deletions(-)
diff --git a/midx.c b/midx.c
index 81d6ab11e6e..f75e3c9fa6d 100644
--- a/midx.c
+++ b/midx.c
@@ -758,8 +758,7 @@ int midx_checksum_valid(struct multi_pack_index *m)
}
struct clear_midx_data {
- char **keep;
- uint32_t keep_nr;
+ struct strset keep;
const char *ext;
};
@@ -767,15 +766,12 @@ static void clear_midx_file_ext(const char *full_path, size_t full_path_len UNUS
const char *file_name, void *_data)
{
struct clear_midx_data *data = _data;
- uint32_t i;
if (!(starts_with(file_name, "multi-pack-index-") &&
ends_with(file_name, data->ext)))
return;
- for (i = 0; i < data->keep_nr; i++) {
- if (!strcmp(data->keep[i], file_name))
- return;
- }
+ if (strset_contains(&data->keep, file_name))
+ return;
if (unlink(full_path))
die_errno(_("failed to remove %s"), full_path);
}
@@ -783,48 +779,49 @@ static void clear_midx_file_ext(const char *full_path, size_t full_path_len UNUS
void clear_midx_files_ext(struct odb_source *source, const char *ext,
const char *keep_hash)
{
- struct clear_midx_data data;
- memset(&data, 0, sizeof(struct clear_midx_data));
+ struct clear_midx_data data = {
+ .keep = STRSET_INIT,
+ .ext = ext,
+ };
if (keep_hash) {
- ALLOC_ARRAY(data.keep, 1);
+ struct strbuf buf = STRBUF_INIT;
+ strbuf_addf(&buf, "multi-pack-index-%s.%s", keep_hash, ext);
- data.keep[0] = xstrfmt("multi-pack-index-%s.%s", keep_hash, ext);
- data.keep_nr = 1;
+ strset_add(&data.keep, buf.buf);
+
+ strbuf_release(&buf);
}
- data.ext = ext;
- for_each_file_in_pack_dir(source->path,
- clear_midx_file_ext,
- &data);
+ for_each_file_in_pack_dir(source->path, clear_midx_file_ext, &data);
- if (keep_hash)
- free(data.keep[0]);
- free(data.keep);
+ strset_clear(&data.keep);
}
void clear_incremental_midx_files_ext(struct odb_source *source, const char *ext,
char **keep_hashes,
uint32_t hashes_nr)
{
- struct clear_midx_data data;
+ struct clear_midx_data data = {
+ .keep = STRSET_INIT,
+ .ext = ext,
+ };
+ struct strbuf buf = STRBUF_INIT;
uint32_t i;
- memset(&data, 0, sizeof(struct clear_midx_data));
+ for (i = 0; i < hashes_nr; i++) {
+ strbuf_reset(&buf);
+ strbuf_addf(&buf, "multi-pack-index-%s.%s", keep_hashes[i],
+ ext);
- ALLOC_ARRAY(data.keep, hashes_nr);
- for (i = 0; i < hashes_nr; i++)
- data.keep[i] = xstrfmt("multi-pack-index-%s.%s", keep_hashes[i],
- ext);
- data.keep_nr = hashes_nr;
- data.ext = ext;
+ strset_add(&data.keep, buf.buf);
+ }
for_each_file_in_pack_subdir(source->path, "multi-pack-index.d",
clear_midx_file_ext, &data);
- for (i = 0; i < hashes_nr; i++)
- free(data.keep[i]);
- free(data.keep);
+ strbuf_release(&buf);
+ strset_clear(&data.keep);
}
void clear_midx_file(struct repository *r)
--
2.54.0.175.g8bd0ec98dc3
^ permalink raw reply related
* [PATCH v4 03/16] midx: build `keep_hashes` array in order
From: Taylor Blau @ 2026-05-19 15:57 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1779206239.git.me@ttaylorr.com>
Instead of filling the keep_hashes array using reverse indexing (e.g.,
`keep_hashes[count - i - 1]`) while traversing linked lists forward,
collect linked list nodes into a temporary `layers` array and then
iterate it backwards to fill `keep_hashes` sequentially.
This makes the filling logic easier to follow, since each segment of the
array is filled with a simple forward-marching index. Moreover, this
change prepares us for a subsequent commit that will switch to using a
`strvec`.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
midx-write.c | 66 ++++++++++++++++++++++++++++------------------------
1 file changed, 36 insertions(+), 30 deletions(-)
diff --git a/midx-write.c b/midx-write.c
index 9328f65a201..55c778a97cb 100644
--- a/midx-write.c
+++ b/midx-write.c
@@ -1731,6 +1731,9 @@ static int write_midx_internal(struct write_midx_opts *opts)
FILE *chainf = fdopen_lock_file(&lk, "w");
struct strbuf final_midx_name = STRBUF_INIT;
struct multi_pack_index *m = ctx.base_midx;
+ struct multi_pack_index **layers = NULL;
+ size_t layers_nr = 0, layers_alloc = 0;
+ size_t j = 0;
if (!chainf) {
error_errno(_("unable to open multi-pack-index chain file"));
@@ -1751,46 +1754,49 @@ static int write_midx_internal(struct write_midx_opts *opts)
strbuf_release(&final_midx_name);
if (ctx.compact) {
- struct multi_pack_index *m;
- uint32_t num_layers_before_from = 0;
- uint32_t i;
+ struct multi_pack_index *mp;
- for (m = ctx.base_midx; m; m = m->base_midx)
- num_layers_before_from++;
-
- m = ctx.base_midx;
- for (i = 0; i < num_layers_before_from; i++) {
- uint32_t j = num_layers_before_from - i - 1;
-
- keep_hashes[j] = xstrdup(midx_get_checksum_hex(m));
- m = m->base_midx;
+ for (mp = ctx.base_midx; mp; mp = mp->base_midx) {
+ ALLOC_GROW(layers, layers_nr + 1, layers_alloc);
+ layers[layers_nr++] = mp;
}
+ while (layers_nr)
+ keep_hashes[j++] =
+ xstrdup(midx_get_checksum_hex(layers[--layers_nr]));
- keep_hashes[i] = xstrdup(hash_to_hex_algop(midx_hash,
- r->hash_algo));
+ keep_hashes[j++] =
+ xstrdup(hash_to_hex_algop(midx_hash,
+ r->hash_algo));
- i = 0;
- for (m = ctx.m;
- m && midx_hashcmp(m, ctx.compact_to, r->hash_algo);
- m = m->base_midx) {
- keep_hashes[keep_hashes_nr - i - 1] =
- xstrdup(midx_get_checksum_hex(m));
- i++;
+ for (mp = ctx.m;
+ mp && midx_hashcmp(mp, ctx.compact_to,
+ r->hash_algo);
+ mp = mp->base_midx) {
+ ALLOC_GROW(layers, layers_nr + 1, layers_alloc);
+ layers[layers_nr++] = mp;
}
+ while (layers_nr)
+ keep_hashes[j++] =
+ xstrdup(midx_get_checksum_hex(layers[--layers_nr]));
} else {
- keep_hashes[ctx.num_multi_pack_indexes_before] =
+ for (; m; m = m->base_midx) {
+ ALLOC_GROW(layers, layers_nr + 1, layers_alloc);
+ layers[layers_nr++] = m;
+ }
+ while (layers_nr)
+ keep_hashes[j++] =
+ xstrdup(midx_get_checksum_hex(layers[--layers_nr]));
+
+ keep_hashes[j++] =
xstrdup(hash_to_hex_algop(midx_hash,
r->hash_algo));
-
- for (uint32_t i = 0; i < ctx.num_multi_pack_indexes_before; i++) {
- uint32_t j = ctx.num_multi_pack_indexes_before - i - 1;
-
- keep_hashes[j] = xstrdup(midx_get_checksum_hex(m));
- m = m->base_midx;
- }
}
- for (uint32_t i = 0; i < keep_hashes_nr; i++)
+ ASSERT(j == keep_hashes_nr);
+
+ free(layers);
+
+ for (uint32_t i = 0; i < j; i++)
fprintf(get_lock_file_fp(&lk), "%s\n", keep_hashes[i]);
} else {
keep_hashes[ctx.num_multi_pack_indexes_before] =
--
2.54.0.175.g8bd0ec98dc3
^ permalink raw reply related
* [PATCH v4 04/16] midx: use `strvec` for `keep_hashes`
From: Taylor Blau @ 2026-05-19 15:57 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1779206239.git.me@ttaylorr.com>
The `keep_hashes` array in `write_midx_internal()` accumulates the
checksums of MIDX files that should be retained when pruning stale
entries from the MIDX chain. For similar reasons as in a previous
commit, rewrite this using a strvec, requiring us to pass one fewer
parameter.
Unlike the aforementioned previous commit, use a `strvec` instead of a
`string_list`, which provides a more ergonomic interface to adjust the
values at a particular index. The ordering is important here, as this
value is used to determine the contents of the resulting
`multi-pack-index-chain` file when writing with "--incremental".
Since the previous commit already builds the array in forward order, the
conversion is straightforward: replace indexed assignments with
`strvec_push()`, drop the pre-counting and `CALLOC_ARRAY()`, and
simplify cleanup via `strvec_clear()`.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
midx-write.c | 84 ++++++++++++++++++----------------------------------
midx.c | 20 ++++++-------
2 files changed, 38 insertions(+), 66 deletions(-)
diff --git a/midx-write.c b/midx-write.c
index 55c778a97cb..5d9409a9741 100644
--- a/midx-write.c
+++ b/midx-write.c
@@ -29,8 +29,7 @@ extern void clear_midx_files_ext(struct odb_source *source, const char *ext,
const char *keep_hash);
extern void clear_incremental_midx_files_ext(struct odb_source *source,
const char *ext,
- const char **keep_hashes,
- uint32_t hashes_nr);
+ const struct strvec *keep_hashes);
extern int cmp_idx_or_pack_name(const char *idx_or_pack_name,
const char *idx_name);
@@ -1109,8 +1108,7 @@ static int link_midx_to_chain(struct multi_pack_index *m)
}
static void clear_midx_files(struct odb_source *source,
- const char **hashes, uint32_t hashes_nr,
- unsigned incremental)
+ const struct strvec *hashes, unsigned incremental)
{
/*
* if incremental:
@@ -1124,13 +1122,15 @@ static void clear_midx_files(struct odb_source *source,
*/
struct strbuf buf = STRBUF_INIT;
const char *exts[] = { MIDX_EXT_BITMAP, MIDX_EXT_REV, MIDX_EXT_MIDX };
- uint32_t i, j;
+ uint32_t i;
for (i = 0; i < ARRAY_SIZE(exts); i++) {
- clear_incremental_midx_files_ext(source, exts[i],
- hashes, hashes_nr);
- for (j = 0; j < hashes_nr; j++)
- clear_midx_files_ext(source, exts[i], hashes[j]);
+ clear_incremental_midx_files_ext(source, exts[i], hashes);
+ if (hashes) {
+ for (size_t j = 0; j < hashes->nr; j++)
+ clear_midx_files_ext(source, exts[i],
+ hashes->v[j]);
+ }
}
if (incremental)
@@ -1267,8 +1267,7 @@ static int write_midx_internal(struct write_midx_opts *opts)
int pack_name_concat_len = 0;
int dropped_packs = 0;
int result = -1;
- const char **keep_hashes = NULL;
- size_t keep_hashes_nr = 0;
+ struct strvec keep_hashes = STRVEC_INIT;
struct chunkfile *cf;
trace2_region_enter("midx", "write_midx_internal", r);
@@ -1708,32 +1707,12 @@ static int write_midx_internal(struct write_midx_opts *opts)
if (ctx.num_multi_pack_indexes_before == UINT32_MAX)
die(_("too many multi-pack-indexes"));
- if (ctx.compact) {
- struct multi_pack_index *m;
-
- /*
- * Keep all MIDX layers excluding those in the range [from, to].
- */
- for (m = ctx.base_midx; m; m = m->base_midx)
- keep_hashes_nr++;
- for (m = ctx.m;
- m && midx_hashcmp(m, ctx.compact_to, r->hash_algo);
- m = m->base_midx)
- keep_hashes_nr++;
-
- keep_hashes_nr++; /* include the compacted layer */
- } else {
- keep_hashes_nr = ctx.num_multi_pack_indexes_before + 1;
- }
- CALLOC_ARRAY(keep_hashes, keep_hashes_nr);
-
if (ctx.incremental) {
FILE *chainf = fdopen_lock_file(&lk, "w");
struct strbuf final_midx_name = STRBUF_INIT;
struct multi_pack_index *m = ctx.base_midx;
struct multi_pack_index **layers = NULL;
size_t layers_nr = 0, layers_alloc = 0;
- size_t j = 0;
if (!chainf) {
error_errno(_("unable to open multi-pack-index chain file"));
@@ -1761,12 +1740,12 @@ static int write_midx_internal(struct write_midx_opts *opts)
layers[layers_nr++] = mp;
}
while (layers_nr)
- keep_hashes[j++] =
- xstrdup(midx_get_checksum_hex(layers[--layers_nr]));
+ strvec_push(&keep_hashes,
+ midx_get_checksum_hex(layers[--layers_nr]));
- keep_hashes[j++] =
- xstrdup(hash_to_hex_algop(midx_hash,
- r->hash_algo));
+ strvec_push(&keep_hashes,
+ hash_to_hex_algop(midx_hash,
+ r->hash_algo));
for (mp = ctx.m;
mp && midx_hashcmp(mp, ctx.compact_to,
@@ -1776,31 +1755,29 @@ static int write_midx_internal(struct write_midx_opts *opts)
layers[layers_nr++] = mp;
}
while (layers_nr)
- keep_hashes[j++] =
- xstrdup(midx_get_checksum_hex(layers[--layers_nr]));
+ strvec_push(&keep_hashes,
+ midx_get_checksum_hex(layers[--layers_nr]));
} else {
for (; m; m = m->base_midx) {
ALLOC_GROW(layers, layers_nr + 1, layers_alloc);
layers[layers_nr++] = m;
}
while (layers_nr)
- keep_hashes[j++] =
- xstrdup(midx_get_checksum_hex(layers[--layers_nr]));
+ strvec_push(&keep_hashes,
+ midx_get_checksum_hex(layers[--layers_nr]));
- keep_hashes[j++] =
- xstrdup(hash_to_hex_algop(midx_hash,
- r->hash_algo));
+ strvec_push(&keep_hashes,
+ hash_to_hex_algop(midx_hash,
+ r->hash_algo));
}
- ASSERT(j == keep_hashes_nr);
-
free(layers);
- for (uint32_t i = 0; i < j; i++)
- fprintf(get_lock_file_fp(&lk), "%s\n", keep_hashes[i]);
+ for (size_t i = 0; i < keep_hashes.nr; i++)
+ fprintf(get_lock_file_fp(&lk), "%s\n", keep_hashes.v[i]);
} else {
- keep_hashes[ctx.num_multi_pack_indexes_before] =
- xstrdup(hash_to_hex_algop(midx_hash, r->hash_algo));
+ strvec_push(&keep_hashes,
+ hash_to_hex_algop(midx_hash, r->hash_algo));
}
if (ctx.m || ctx.base_midx)
@@ -1809,8 +1786,7 @@ static int write_midx_internal(struct write_midx_opts *opts)
if (commit_lock_file(&lk) < 0)
die_errno(_("could not write multi-pack-index"));
- clear_midx_files(opts->source, keep_hashes, keep_hashes_nr,
- ctx.incremental);
+ clear_midx_files(opts->source, &keep_hashes, ctx.incremental);
result = 0;
cleanup:
@@ -1826,11 +1802,7 @@ static int write_midx_internal(struct write_midx_opts *opts)
free(ctx.entries);
free(ctx.pack_perm);
free(ctx.pack_order);
- if (keep_hashes) {
- for (uint32_t i = 0; i < keep_hashes_nr; i++)
- free((char *)keep_hashes[i]);
- free(keep_hashes);
- }
+ strvec_clear(&keep_hashes);
strbuf_release(&midx_name);
close_midx(midx_to_free);
diff --git a/midx.c b/midx.c
index f75e3c9fa6d..bcb8c999015 100644
--- a/midx.c
+++ b/midx.c
@@ -12,6 +12,7 @@
#include "chunk-format.h"
#include "pack-bitmap.h"
#include "pack-revindex.h"
+#include "strvec.h"
#define MIDX_PACK_ERROR ((void *)(intptr_t)-1)
@@ -19,8 +20,7 @@ int midx_checksum_valid(struct multi_pack_index *m);
void clear_midx_files_ext(struct odb_source *source, const char *ext,
const char *keep_hash);
void clear_incremental_midx_files_ext(struct odb_source *source, const char *ext,
- char **keep_hashes,
- uint32_t hashes_nr);
+ const struct strvec *keep_hashes);
int cmp_idx_or_pack_name(const char *idx_or_pack_name,
const char *idx_name);
@@ -799,22 +799,22 @@ void clear_midx_files_ext(struct odb_source *source, const char *ext,
}
void clear_incremental_midx_files_ext(struct odb_source *source, const char *ext,
- char **keep_hashes,
- uint32_t hashes_nr)
+ const struct strvec *keep_hashes)
{
struct clear_midx_data data = {
.keep = STRSET_INIT,
.ext = ext,
};
struct strbuf buf = STRBUF_INIT;
- uint32_t i;
- for (i = 0; i < hashes_nr; i++) {
- strbuf_reset(&buf);
- strbuf_addf(&buf, "multi-pack-index-%s.%s", keep_hashes[i],
- ext);
+ if (keep_hashes) {
+ for (size_t i = 0; i < keep_hashes->nr; i++) {
+ strbuf_reset(&buf);
+ strbuf_addf(&buf, "multi-pack-index-%s.%s",
+ keep_hashes->v[i], ext);
- strset_add(&data.keep, buf.buf);
+ strset_add(&data.keep, buf.buf);
+ }
}
for_each_file_in_pack_subdir(source->path, "multi-pack-index.d",
--
2.54.0.175.g8bd0ec98dc3
^ permalink raw reply related
* [PATCH v4 05/16] midx: introduce `--no-write-chain-file` for incremental MIDX writes
From: Taylor Blau @ 2026-05-19 15:57 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1779206239.git.me@ttaylorr.com>
When writing an incremental MIDX layer, the MIDX machinery writes the
new layer into the multi-pack-index.d directory and then updates the
multi-pack-index-chain file to include the freshly written layer.
Future callers however may not wish to immediately update the MIDX chain
itself, preferring instead to write out new layer(s) themselves before
atomically updating the chain. Concretely, the new incremental
MIDX-based repacking strategy will want to do exactly this (that is,
assemble the new MIDX chain itself before writing a new chain file and
atomically linking it into place).
Introduce a `--no-write-chain-file` flag that:
* writes the new MIDX layer into the multi-pack-index.d directory
* prints its checksum
* does not update the multi-pack-index-chain file.
The MIDX chain file (and thus, the lock protecting it) remain untouched,
allowing callers to assemble the chain themselves. This flag requires
`--incremental`, since the notion of a separate layer only makes sense
for incremental MIDXs.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
Documentation/git-multi-pack-index.adoc | 17 ++++++++--
builtin/multi-pack-index.c | 28 +++++++++++++++--
midx-write.c | 42 ++++++++++++++++---------
midx.h | 1 +
t/t5334-incremental-multi-pack-index.sh | 17 ++++++++++
t/t5335-compact-multi-pack-index.sh | 36 +++++++++++++++++++++
6 files changed, 123 insertions(+), 18 deletions(-)
diff --git a/Documentation/git-multi-pack-index.adoc b/Documentation/git-multi-pack-index.adoc
index 3a5aa227784..c26196815e2 100644
--- a/Documentation/git-multi-pack-index.adoc
+++ b/Documentation/git-multi-pack-index.adoc
@@ -11,9 +11,9 @@ SYNOPSIS
[verse]
'git multi-pack-index' [<options>] write [--preferred-pack=<pack>]
[--[no-]bitmap] [--[no-]incremental] [--[no-]stdin-packs]
- [--refs-snapshot=<path>]
+ [--refs-snapshot=<path>] [--[no-]write-chain-file]
'git multi-pack-index' [<options>] compact [--[no-]incremental]
- [--[no-]bitmap] <from> <to>
+ [--[no-]bitmap] [--[no-]write-chain-file] <from> <to>
'git multi-pack-index' [<options>] verify
'git multi-pack-index' [<options>] expire
'git multi-pack-index' [<options>] repack [--batch-size=<size>]
@@ -83,6 +83,13 @@ marker).
and packs not present in an existing MIDX layer.
Migrates non-incremental MIDXs to incremental ones when
necessary.
+
+ --[no-]write-chain-file::
+ When used with `--incremental`, write a new MIDX layer
+ but do not update the multi-pack-index-chain file.
+ The checksum of the new layer is printed to standard
+ output, allowing the caller to assemble and write the
+ chain itself. Requires `--incremental`.
--
compact::
@@ -97,6 +104,12 @@ compact::
--[no-]bitmap::
Control whether or not a multi-pack bitmap is written.
+
+ --[no-]write-chain-file::
+ When used with `--incremental`, write a new compacted
+ MIDX layer but do not update the multi-pack-index-chain
+ file. The checksum of the new layer is printed to
+ standard output. Requires `--incremental`.
--
+
Note that the compact command requires writing a version-2 midx that
diff --git a/builtin/multi-pack-index.c b/builtin/multi-pack-index.c
index 0f72d96c02d..f861b4b8394 100644
--- a/builtin/multi-pack-index.c
+++ b/builtin/multi-pack-index.c
@@ -16,11 +16,11 @@
#define BUILTIN_MIDX_WRITE_USAGE \
N_("git multi-pack-index [<options>] write [--preferred-pack=<pack>]\n" \
" [--[no-]bitmap] [--[no-]incremental] [--[no-]stdin-packs]\n" \
- " [--refs-snapshot=<path>]")
+ " [--refs-snapshot=<path>] [--[no-]write-chain-file]")
#define BUILTIN_MIDX_COMPACT_USAGE \
N_("git multi-pack-index [<options>] compact [--[no-]incremental]\n" \
- " [--[no-]bitmap] <from> <to>")
+ " [--[no-]bitmap] [--[no-]write-chain-file] <from> <to>")
#define BUILTIN_MIDX_VERIFY_USAGE \
N_("git multi-pack-index [<options>] verify")
@@ -153,6 +153,9 @@ static int cmd_multi_pack_index_write(int argc, const char **argv,
MIDX_WRITE_BITMAP | MIDX_WRITE_REV_INDEX),
OPT_BIT(0, "incremental", &opts.flags,
N_("write a new incremental MIDX"), MIDX_WRITE_INCREMENTAL),
+ OPT_NEGBIT(0, "write-chain-file", &opts.flags,
+ N_("write the multi-pack-index chain file"),
+ MIDX_WRITE_NO_CHAIN),
OPT_BOOL(0, "stdin-packs", &opts.stdin_packs,
N_("write multi-pack index containing only given indexes")),
OPT_FILENAME(0, "refs-snapshot", &opts.refs_snapshot,
@@ -178,6 +181,15 @@ static int cmd_multi_pack_index_write(int argc, const char **argv,
if (argc)
usage_with_options(builtin_multi_pack_index_write_usage,
options);
+
+ if (opts.flags & MIDX_WRITE_NO_CHAIN &&
+ !(opts.flags & MIDX_WRITE_INCREMENTAL)) {
+ error(_("cannot use %s without %s"),
+ "--no-write-chain-file", "--incremental");
+ usage_with_options(builtin_multi_pack_index_write_usage,
+ options);
+ }
+
source = handle_object_dir_option(repo);
FREE_AND_NULL(options);
@@ -221,6 +233,9 @@ static int cmd_multi_pack_index_compact(int argc, const char **argv,
MIDX_WRITE_BITMAP | MIDX_WRITE_REV_INDEX),
OPT_BIT(0, "incremental", &opts.flags,
N_("write a new incremental MIDX"), MIDX_WRITE_INCREMENTAL),
+ OPT_NEGBIT(0, "write-chain-file", &opts.flags,
+ N_("write the multi-pack-index chain file"),
+ MIDX_WRITE_NO_CHAIN),
OPT_END(),
};
@@ -239,6 +254,15 @@ static int cmd_multi_pack_index_compact(int argc, const char **argv,
if (argc != 2)
usage_with_options(builtin_multi_pack_index_compact_usage,
options);
+
+ if (opts.flags & MIDX_WRITE_NO_CHAIN &&
+ !(opts.flags & MIDX_WRITE_INCREMENTAL)) {
+ error(_("cannot use %s without %s"),
+ "--no-write-chain-file", "--incremental");
+ usage_with_options(builtin_multi_pack_index_compact_usage,
+ options);
+ }
+
source = handle_object_dir_option(the_repository);
FREE_AND_NULL(options);
diff --git a/midx-write.c b/midx-write.c
index 5d9409a9741..38c898e5ff5 100644
--- a/midx-write.c
+++ b/midx-write.c
@@ -1257,7 +1257,7 @@ static int write_midx_internal(struct write_midx_opts *opts)
unsigned char midx_hash[GIT_MAX_RAWSZ];
uint32_t start_pack;
struct hashfile *f = NULL;
- struct lock_file lk;
+ struct lock_file lk = LOCK_INIT;
struct tempfile *incr;
struct write_midx_context ctx = {
.preferred_pack_idx = NO_PREFERRED_PACK,
@@ -1601,11 +1601,14 @@ static int write_midx_internal(struct write_midx_opts *opts)
}
if (ctx.incremental) {
- struct strbuf lock_name = STRBUF_INIT;
+ if (!(opts->flags & MIDX_WRITE_NO_CHAIN)) {
+ struct strbuf lock_name = STRBUF_INIT;
- get_midx_chain_filename(opts->source, &lock_name);
- hold_lock_file_for_update(&lk, lock_name.buf, LOCK_DIE_ON_ERROR);
- strbuf_release(&lock_name);
+ get_midx_chain_filename(opts->source, &lock_name);
+ hold_lock_file_for_update(&lk, lock_name.buf,
+ LOCK_DIE_ON_ERROR);
+ strbuf_release(&lock_name);
+ }
incr = mks_tempfile_m(midx_name.buf, 0444);
if (!incr) {
@@ -1707,16 +1710,23 @@ static int write_midx_internal(struct write_midx_opts *opts)
if (ctx.num_multi_pack_indexes_before == UINT32_MAX)
die(_("too many multi-pack-indexes"));
+ if (!is_lock_file_locked(&lk))
+ printf("%s\n", hash_to_hex_algop(midx_hash, r->hash_algo));
+ else if (opts->flags & MIDX_WRITE_NO_CHAIN)
+ BUG("lockfile held with MIDX_WRITE_NO_CHAIN set?");
+
if (ctx.incremental) {
- FILE *chainf = fdopen_lock_file(&lk, "w");
struct strbuf final_midx_name = STRBUF_INIT;
struct multi_pack_index *m = ctx.base_midx;
struct multi_pack_index **layers = NULL;
size_t layers_nr = 0, layers_alloc = 0;
- if (!chainf) {
- error_errno(_("unable to open multi-pack-index chain file"));
- goto cleanup;
+ if (is_lock_file_locked(&lk)){
+ FILE *chainf = fdopen_lock_file(&lk, "w");
+ if (!chainf) {
+ error_errno(_("unable to open multi-pack-index chain file"));
+ goto cleanup;
+ }
}
if (link_midx_to_chain(ctx.base_midx) < 0)
@@ -1773,8 +1783,10 @@ static int write_midx_internal(struct write_midx_opts *opts)
free(layers);
- for (size_t i = 0; i < keep_hashes.nr; i++)
- fprintf(get_lock_file_fp(&lk), "%s\n", keep_hashes.v[i]);
+ if (is_lock_file_locked(&lk))
+ for (size_t i = 0; i < keep_hashes.nr; i++)
+ fprintf(get_lock_file_fp(&lk), "%s\n",
+ keep_hashes.v[i]);
} else {
strvec_push(&keep_hashes,
hash_to_hex_algop(midx_hash, r->hash_algo));
@@ -1783,10 +1795,12 @@ static int write_midx_internal(struct write_midx_opts *opts)
if (ctx.m || ctx.base_midx)
odb_close(ctx.repo->objects);
- if (commit_lock_file(&lk) < 0)
- die_errno(_("could not write multi-pack-index"));
+ if (is_lock_file_locked(&lk)) {
+ if (commit_lock_file(&lk) < 0)
+ die_errno(_("could not write multi-pack-index"));
- clear_midx_files(opts->source, &keep_hashes, ctx.incremental);
+ clear_midx_files(opts->source, &keep_hashes, ctx.incremental);
+ }
result = 0;
cleanup:
diff --git a/midx.h b/midx.h
index 08f3728e520..5b193882dcf 100644
--- a/midx.h
+++ b/midx.h
@@ -83,6 +83,7 @@ struct multi_pack_index {
#define MIDX_WRITE_BITMAP_LOOKUP_TABLE (1 << 4)
#define MIDX_WRITE_INCREMENTAL (1 << 5)
#define MIDX_WRITE_COMPACT (1 << 6)
+#define MIDX_WRITE_NO_CHAIN (1 << 7)
#define MIDX_EXT_REV "rev"
#define MIDX_EXT_BITMAP "bitmap"
diff --git a/t/t5334-incremental-multi-pack-index.sh b/t/t5334-incremental-multi-pack-index.sh
index c9f5b4e87aa..66d6894761b 100755
--- a/t/t5334-incremental-multi-pack-index.sh
+++ b/t/t5334-incremental-multi-pack-index.sh
@@ -96,6 +96,23 @@ test_expect_success 'show object from second pack' '
git cat-file -p 2.2
'
+test_expect_success 'write MIDX layer with --no-write-chain-file' '
+ test_commit no-write-chain-file &&
+ git repack -d &&
+
+ cp "$midx_chain" "$midx_chain.bak" &&
+ layer="$(git multi-pack-index write --bitmap --incremental \
+ --no-write-chain-file)" &&
+
+ test_cmp "$midx_chain.bak" "$midx_chain" &&
+ test_path_is_file "$midxdir/multi-pack-index-$layer.midx"
+'
+
+test_expect_success 'write non-incremental MIDX layer with --no-write-chain-file' '
+ test_must_fail git multi-pack-index write --bitmap --no-write-chain-file 2>err &&
+ test_grep "cannot use --no-write-chain-file without --incremental" err
+'
+
for reuse in false single multi
do
test_expect_success "full clone (pack.allowPackReuse=$reuse)" '
diff --git a/t/t5335-compact-multi-pack-index.sh b/t/t5335-compact-multi-pack-index.sh
index 40f3844282f..1a65d48b62b 100755
--- a/t/t5335-compact-multi-pack-index.sh
+++ b/t/t5335-compact-multi-pack-index.sh
@@ -290,4 +290,40 @@ test_expect_success 'MIDX compaction with bitmaps (non-trivial)' '
)
'
+test_expect_success 'MIDX compaction with --no-write-chain-file' '
+ git init midx-compact-with--no-write-chain-file &&
+ (
+ cd midx-compact-with--no-write-chain-file &&
+
+ git config maintenance.auto false &&
+
+ write_packs A B C D &&
+
+ test_line_count = 4 $midx_chain &&
+ cp "$midx_chain" "$midx_chain".bak &&
+
+ layer="$(git multi-pack-index compact --incremental \
+ --no-write-chain-file \
+ "$(nth_line 2 "$midx_chain")" \
+ "$(nth_line 3 "$midx_chain")")" &&
+
+ test_cmp "$midx_chain.bak" "$midx_chain" &&
+
+ # After writing the new layer, insert it into the chain
+ # manually. This is done in order to make $layer visible
+ # to the read-midx test helper below, and matches what
+ # the MIDX command would do without --no-write-chain-file.
+ {
+ nth_line 1 "$midx_chain.bak" &&
+ echo $layer &&
+ nth_line 4 "$midx_chain.bak"
+ } >$midx_chain &&
+
+ test-tool read-midx $objdir $layer >midx.data &&
+ grep "^pack-B-.*\.idx" midx.data &&
+ grep "^pack-C-.*\.idx" midx.data
+
+ )
+'
+
test_done
--
2.54.0.175.g8bd0ec98dc3
^ permalink raw reply related
* [PATCH v4 06/16] midx: support custom `--base` for incremental MIDX writes
From: Taylor Blau @ 2026-05-19 15:57 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1779206239.git.me@ttaylorr.com>
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>
---
Documentation/git-multi-pack-index.adoc | 17 +++++-
builtin/multi-pack-index.c | 24 ++++++--
midx-write.c | 34 ++++++++++-
midx.h | 5 +-
t/t5334-incremental-multi-pack-index.sh | 30 ++++++++++
t/t5335-compact-multi-pack-index.sh | 77 +++++++++++++++++++++++++
6 files changed, 178 insertions(+), 9 deletions(-)
diff --git a/Documentation/git-multi-pack-index.adoc b/Documentation/git-multi-pack-index.adoc
index c26196815e2..c6d23aeeb9a 100644
--- a/Documentation/git-multi-pack-index.adoc
+++ b/Documentation/git-multi-pack-index.adoc
@@ -12,8 +12,10 @@ SYNOPSIS
'git multi-pack-index' [<options>] write [--preferred-pack=<pack>]
[--[no-]bitmap] [--[no-]incremental] [--[no-]stdin-packs]
[--refs-snapshot=<path>] [--[no-]write-chain-file]
+ [--base=<checksum>]
'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>] expire
'git multi-pack-index' [<options>] repack [--batch-size=<size>]
@@ -90,6 +92,13 @@ marker).
The checksum of the new layer is printed to standard
output, allowing the caller to assemble and write the
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::
@@ -110,6 +119,12 @@ compact::
MIDX layer but do not update the multi-pack-index-chain
file. The checksum of the new layer is printed to
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
diff --git a/builtin/multi-pack-index.c b/builtin/multi-pack-index.c
index f861b4b8394..00ffb36394d 100644
--- a/builtin/multi-pack-index.c
+++ b/builtin/multi-pack-index.c
@@ -16,11 +16,13 @@
#define BUILTIN_MIDX_WRITE_USAGE \
N_("git multi-pack-index [<options>] write [--preferred-pack=<pack>]\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 \
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 \
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 {
char *object_dir;
const char *preferred_pack;
+ const char *incremental_base;
char *refs_snapshot;
unsigned long batch_size;
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")),
OPT_BIT(0, "bitmap", &opts.flags, N_("write multi-pack bitmap"),
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,
N_("write a new incremental MIDX"), MIDX_WRITE_INCREMENTAL),
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);
}
+ 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);
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,
opts.preferred_pack,
- opts.refs_snapshot, opts.flags);
+ opts.refs_snapshot,
+ opts.incremental_base, opts.flags);
string_list_clear(&packs, 0);
free(opts.refs_snapshot);
@@ -229,6 +242,8 @@ static int cmd_multi_pack_index_compact(int argc, const char **argv,
struct option *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"),
MIDX_WRITE_BITMAP | MIDX_WRITE_REV_INDEX),
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]);
}
- 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;
}
diff --git a/midx-write.c b/midx-write.c
index 38c898e5ff5..561e9eedc0e 100644
--- a/midx-write.c
+++ b/midx-write.c
@@ -1247,6 +1247,7 @@ struct write_midx_opts {
const char *preferred_pack_name;
const char *refs_snapshot;
+ const char *incremental_base;
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
- * 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)
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.alloc = ctx.m ? ctx.m->num_packs + ctx.m->num_packs_in_base : 16;
ctx.info = NULL;
@@ -1827,7 +1849,8 @@ static int write_midx_internal(struct write_midx_opts *opts)
int write_midx_file(struct odb_source *source,
const char *preferred_pack_name,
- const char *refs_snapshot, unsigned flags)
+ const char *refs_snapshot,
+ unsigned flags)
{
struct write_midx_opts opts = {
.source = source,
@@ -1842,13 +1865,16 @@ int write_midx_file(struct odb_source *source,
int write_midx_file_only(struct odb_source *source,
struct string_list *packs_to_include,
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 = {
.source = source,
.packs_to_include = packs_to_include,
.preferred_pack_name = preferred_pack_name,
.refs_snapshot = refs_snapshot,
+ .incremental_base = incremental_base,
.flags = flags,
};
@@ -1858,12 +1884,14 @@ int write_midx_file_only(struct odb_source *source,
int write_midx_file_compact(struct odb_source *source,
struct multi_pack_index *from,
struct multi_pack_index *to,
+ const char *incremental_base,
unsigned flags)
{
struct write_midx_opts opts = {
.source = source,
.compact_from = from,
.compact_to = to,
+ .incremental_base = incremental_base,
.flags = flags | MIDX_WRITE_COMPACT,
};
diff --git a/midx.h b/midx.h
index 5b193882dcf..77dd66de02b 100644
--- a/midx.h
+++ b/midx.h
@@ -132,10 +132,13 @@ int write_midx_file(struct odb_source *source,
int write_midx_file_only(struct odb_source *source,
struct string_list *packs_to_include,
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,
struct multi_pack_index *from,
struct multi_pack_index *to,
+ const char *incremental_base,
unsigned flags);
void clear_midx_file(struct repository *r);
int verify_midx_file(struct odb_source *source, unsigned flags);
diff --git a/t/t5334-incremental-multi-pack-index.sh b/t/t5334-incremental-multi-pack-index.sh
index 66d6894761b..68a103d13d2 100755
--- a/t/t5334-incremental-multi-pack-index.sh
+++ b/t/t5334-incremental-multi-pack-index.sh
@@ -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_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
do
test_expect_success "full clone (pack.allowPackReuse=$reuse)" '
diff --git a/t/t5335-compact-multi-pack-index.sh b/t/t5335-compact-multi-pack-index.sh
index 1a65d48b62b..ec1dafe89fc 100755
--- a/t/t5335-compact-multi-pack-index.sh
+++ b/t/t5335-compact-multi-pack-index.sh
@@ -304,6 +304,7 @@ test_expect_success 'MIDX compaction with --no-write-chain-file' '
layer="$(git multi-pack-index compact --incremental \
--no-write-chain-file \
+ --base="$(nth_line 1 "$midx_chain")" \
"$(nth_line 2 "$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
--
2.54.0.175.g8bd0ec98dc3
^ permalink raw reply related
* [PATCH v4 07/16] repack: track the ODB source via existing_packs
From: Taylor Blau @ 2026-05-19 15:57 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1779206239.git.me@ttaylorr.com>
Store the ODB source in the `existing_packs` struct and use that in
place of the raw `repo->objects->sources` access within `cmd_repack()`.
The source used is still assigned from the first source in the list, so
there are no functional changes in this commit. The changes instead
serve two purposes (one immediate, one not):
- The incremental MIDX-based repacking machinery will need to know what
source is being used to read the existing MIDX/chain (should one
exist).
- In the future, if "git repack" is taught how to operate on other
object sources, this field will serve as the authoritative value for
that source.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
builtin/repack.c | 5 ++---
repack.c | 2 ++
repack.h | 1 +
3 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/builtin/repack.c b/builtin/repack.c
index 4c5a82c2c8d..24be147d39a 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -417,7 +417,7 @@ int cmd_repack(int argc,
* midx_has_unknown_packs() will make the decision for
* us.
*/
- if (!get_multi_pack_index(repo->objects->sources))
+ if (!get_multi_pack_index(existing.source))
midx_must_contain_cruft = 1;
}
@@ -564,8 +564,7 @@ int cmd_repack(int argc,
unsigned flags = 0;
if (git_env_bool(GIT_TEST_MULTI_PACK_INDEX_WRITE_INCREMENTAL, 0))
flags |= MIDX_WRITE_INCREMENTAL;
- write_midx_file(repo->objects->sources,
- NULL, NULL, flags);
+ write_midx_file(existing.source, NULL, NULL, flags);
}
cleanup:
diff --git a/repack.c b/repack.c
index 596841027af..2ee6b51420a 100644
--- a/repack.c
+++ b/repack.c
@@ -154,6 +154,8 @@ void existing_packs_collect(struct existing_packs *existing,
string_list_append(&existing->non_kept_packs, buf.buf);
}
+ existing->source = existing->repo->objects->sources;
+
string_list_sort(&existing->kept_packs);
string_list_sort(&existing->non_kept_packs);
string_list_sort(&existing->cruft_packs);
diff --git a/repack.h b/repack.h
index bc9f2e1a5de..c0e9f0ca647 100644
--- a/repack.h
+++ b/repack.h
@@ -56,6 +56,7 @@ struct packed_git;
struct existing_packs {
struct repository *repo;
+ struct odb_source *source;
struct string_list kept_packs;
struct string_list non_kept_packs;
struct string_list cruft_packs;
--
2.54.0.175.g8bd0ec98dc3
^ permalink raw reply related
* [PATCH v4 08/16] midx: expose `midx_layer_contains_pack()`
From: Taylor Blau @ 2026-05-19 15:58 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1779206239.git.me@ttaylorr.com>
Rename the function `midx_contains_pack_1()` to instead be called
`midx_layer_contains_pack()` and make it accessible. Unlike
`midx_contains_pack()` (which recurses through the entire chain), this
function checks only a single MIDX layer.
This will be used by a subsequent commit to determine whether a given
pack belongs to the tip MIDX layer specifically, rather than to any
layer in the chain.
No functional changes are present in this commit.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
midx.c | 6 +++---
midx.h | 2 ++
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/midx.c b/midx.c
index bcb8c999015..dc86c8e7fee 100644
--- a/midx.c
+++ b/midx.c
@@ -667,8 +667,8 @@ static int midx_pack_names_cmp(const void *a, const void *b, void *m_)
m->pack_names[*(const size_t *)b]);
}
-static int midx_contains_pack_1(struct multi_pack_index *m,
- const char *idx_or_pack_name)
+int midx_layer_contains_pack(struct multi_pack_index *m,
+ const char *idx_or_pack_name)
{
uint32_t first = 0, last = m->num_packs;
@@ -709,7 +709,7 @@ static int midx_contains_pack_1(struct multi_pack_index *m,
int midx_contains_pack(struct multi_pack_index *m, const char *idx_or_pack_name)
{
for (; m; m = m->base_midx)
- if (midx_contains_pack_1(m, idx_or_pack_name))
+ if (midx_layer_contains_pack(m, idx_or_pack_name))
return 1;
return 0;
}
diff --git a/midx.h b/midx.h
index 77dd66de02b..3ee12dd08ec 100644
--- a/midx.h
+++ b/midx.h
@@ -119,6 +119,8 @@ struct object_id *nth_midxed_object_oid(struct object_id *oid,
int fill_midx_entry(struct multi_pack_index *m, const struct object_id *oid, struct pack_entry *e);
int midx_contains_pack(struct multi_pack_index *m,
const char *idx_or_pack_name);
+int midx_layer_contains_pack(struct multi_pack_index *m,
+ const char *idx_or_pack_name);
int midx_preferred_pack(struct multi_pack_index *m, uint32_t *pack_int_id);
int prepare_multi_pack_index_one(struct odb_source *source);
--
2.54.0.175.g8bd0ec98dc3
^ permalink raw reply related
* [PATCH v4 09/16] repack-midx: factor out `repack_prepare_midx_command()`
From: Taylor Blau @ 2026-05-19 15:58 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1779206239.git.me@ttaylorr.com>
The `write_midx_included_packs()` function assembles and executes a
`git multi-pack-index write` command, constructing the argument list
inline.
Future commits will introduce additional callers that need to construct
similar `git multi-pack-index` commands (for both `write` and `compact`
subcommands), so extract the common portions of the command setup into a
reusable `repack_prepare_midx_command()` helper.
The extracted helper sets `git_cmd`, pushes `multi-pack-index` and a
subcommand, and handles `--progress`/`--no-progress` and `--bitmap`
flags. The remaining arguments that are specific to the `write`
subcommand (such as `--stdin-packs`) are left to the caller.
No functional changes are included in this patch.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
repack-midx.c | 30 +++++++++++++++++++-----------
1 file changed, 19 insertions(+), 11 deletions(-)
diff --git a/repack-midx.c b/repack-midx.c
index 0682b80c427..5634dc186d0 100644
--- a/repack-midx.c
+++ b/repack-midx.c
@@ -275,6 +275,23 @@ static void remove_redundant_bitmaps(struct string_list *include,
strbuf_release(&path);
}
+static void repack_prepare_midx_command(struct child_process *cmd,
+ struct repack_write_midx_opts *opts,
+ const char *subcommand)
+{
+ cmd->git_cmd = 1;
+
+ strvec_pushl(&cmd->args, "multi-pack-index", subcommand, NULL);
+
+ if (opts->show_progress)
+ strvec_push(&cmd->args, "--progress");
+ else
+ strvec_push(&cmd->args, "--no-progress");
+
+ if (opts->write_bitmaps)
+ strvec_push(&cmd->args, "--bitmap");
+}
+
int write_midx_included_packs(struct repack_write_midx_opts *opts)
{
struct child_process cmd = CHILD_PROCESS_INIT;
@@ -289,18 +306,9 @@ int write_midx_included_packs(struct repack_write_midx_opts *opts)
goto done;
cmd.in = -1;
- cmd.git_cmd = 1;
- strvec_push(&cmd.args, "multi-pack-index");
- strvec_pushl(&cmd.args, "write", "--stdin-packs", NULL);
-
- if (opts->show_progress)
- strvec_push(&cmd.args, "--progress");
- else
- strvec_push(&cmd.args, "--no-progress");
-
- if (opts->write_bitmaps)
- strvec_push(&cmd.args, "--bitmap");
+ repack_prepare_midx_command(&cmd, opts, "write");
+ strvec_push(&cmd.args, "--stdin-packs");
if (preferred)
strvec_pushf(&cmd.args, "--preferred-pack=%s",
--
2.54.0.175.g8bd0ec98dc3
^ permalink raw reply related
* [PATCH v4 10/16] repack-midx: extract `repack_fill_midx_stdin_packs()`
From: Taylor Blau @ 2026-05-19 15:58 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1779206239.git.me@ttaylorr.com>
The function `write_midx_included_packs()` manages the lifecycle of
writing packs to stdin when running `git multi-pack-index write` as a
child process.
Extract a standalone `repack_fill_midx_stdin_packs()` helper, which
handles `--stdin-packs` argument setup, starting the command, writing
pack names to its standard input, and finishing the command.
This simplifies `write_midx_included_packs()` and prepares for a
subsequent commit where the same helper is called with `cmd->out = -1`
to capture the MIDX's checksum from the command's standard output,
which is needed when writing MIDX layers with `--no-write-chain-file`.
No functional changes are included in this patch.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
repack-midx.c | 38 ++++++++++++++++++++++++--------------
1 file changed, 24 insertions(+), 14 deletions(-)
diff --git a/repack-midx.c b/repack-midx.c
index 5634dc186d0..3fe83715da4 100644
--- a/repack-midx.c
+++ b/repack-midx.c
@@ -292,23 +292,42 @@ static void repack_prepare_midx_command(struct child_process *cmd,
strvec_push(&cmd->args, "--bitmap");
}
+static int repack_fill_midx_stdin_packs(struct child_process *cmd,
+ struct string_list *include)
+{
+ struct string_list_item *item;
+ FILE *in;
+ int ret;
+
+ cmd->in = -1;
+
+ strvec_push(&cmd->args, "--stdin-packs");
+
+ ret = start_command(cmd);
+ if (ret)
+ return ret;
+
+ in = xfdopen(cmd->in, "w");
+ for_each_string_list_item(item, include)
+ fprintf(in, "%s\n", item->string);
+ fclose(in);
+
+ return finish_command(cmd);
+}
+
int write_midx_included_packs(struct repack_write_midx_opts *opts)
{
struct child_process cmd = CHILD_PROCESS_INIT;
struct string_list include = STRING_LIST_INIT_DUP;
struct string_list_item *item;
struct packed_git *preferred = pack_geometry_preferred_pack(opts->geometry);
- FILE *in;
int ret = 0;
midx_included_packs(&include, opts);
if (!include.nr)
goto done;
- cmd.in = -1;
-
repack_prepare_midx_command(&cmd, opts, "write");
- strvec_push(&cmd.args, "--stdin-packs");
if (preferred)
strvec_pushf(&cmd.args, "--preferred-pack=%s",
@@ -350,16 +369,7 @@ int write_midx_included_packs(struct repack_write_midx_opts *opts)
strvec_pushf(&cmd.args, "--refs-snapshot=%s",
opts->refs_snapshot);
- ret = start_command(&cmd);
- if (ret)
- goto done;
-
- in = xfdopen(cmd.in, "w");
- for_each_string_list_item(item, &include)
- fprintf(in, "%s\n", item->string);
- fclose(in);
-
- ret = finish_command(&cmd);
+ ret = repack_fill_midx_stdin_packs(&cmd, &include);
done:
if (!ret && opts->write_bitmaps)
remove_redundant_bitmaps(&include, opts->packdir);
--
2.54.0.175.g8bd0ec98dc3
^ permalink raw reply related
* [PATCH v4 11/16] repack-geometry: prepare for incremental MIDX repacking
From: Taylor Blau @ 2026-05-19 15:58 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1779206239.git.me@ttaylorr.com>
Teach `pack_geometry_init()` to optionally restrict the set of
repacking candidates to only packs in the tip MIDX layer when a
`midx_layer_threshold` is configured. If the tip layer has fewer packs
than the threshold, those packs are excluded entirely; otherwise only
packs in that layer participate in the geometric repack.
Also track whether any tip-layer packs were included in the rollup
(`midx_tip_rewritten`), which a subsequent commit will use to decide
how to update the MIDX chain after repacking.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
repack-geometry.c | 35 +++++++++++++++++++++++++++++++++++
repack.h | 4 ++++
2 files changed, 39 insertions(+)
diff --git a/repack-geometry.c b/repack-geometry.c
index 7cebd0cb45f..2408b8a3cc2 100644
--- a/repack-geometry.c
+++ b/repack-geometry.c
@@ -4,6 +4,7 @@
#include "repack.h"
#include "repository.h"
#include "hex.h"
+#include "midx.h"
#include "packfile.h"
static uint32_t pack_geometry_weight(struct packed_git *p)
@@ -31,8 +32,28 @@ void pack_geometry_init(struct pack_geometry *geometry,
{
struct packed_git *p;
struct strbuf buf = STRBUF_INIT;
+ struct multi_pack_index *m = get_multi_pack_index(existing->source);
repo_for_each_pack(existing->repo, p) {
+ if (geometry->midx_layer_threshold_set && m &&
+ p->multi_pack_index) {
+ /*
+ * When writing MIDX layers incrementally,
+ * ignore packs unless they are in the most
+ * recent MIDX layer *and* there are at least
+ * 'midx_layer_threshold' packs in that layer.
+ *
+ * Otherwise 'p' is either in an older layer, or
+ * the youngest layer does not have enough packs
+ * to consider its packs as candidates for
+ * repacking. In either of those cases we want
+ * to ignore the pack.
+ */
+ if (m->num_packs < geometry->midx_layer_threshold ||
+ !midx_layer_contains_pack(m, pack_basename(p)))
+ continue;
+ }
+
if (args->local && !p->pack_local)
/*
* When asked to only repack local packfiles we skip
@@ -173,6 +194,20 @@ void pack_geometry_split(struct pack_geometry *geometry)
geometry->promisor_split = compute_pack_geometry_split(geometry->promisor_pack,
geometry->promisor_pack_nr,
geometry->split_factor);
+ for (uint32_t i = 0; i < geometry->split; i++) {
+ struct packed_git *p = geometry->pack[i];
+ /*
+ * During incremental MIDX/bitmap repacking, any packs
+ * included in the rollup are either (a) not MIDX'd, or
+ * (b) contained in the tip layer iff it has at least
+ * the threshold number of packs.
+ *
+ * In the latter case, we can safely conclude that the
+ * tip of the MIDX chain will be rewritten.
+ */
+ if (p->multi_pack_index)
+ geometry->midx_tip_rewritten = true;
+ }
}
struct packed_git *pack_geometry_preferred_pack(struct pack_geometry *geometry)
diff --git a/repack.h b/repack.h
index c0e9f0ca647..77d24ee45fb 100644
--- a/repack.h
+++ b/repack.h
@@ -108,6 +108,10 @@ struct pack_geometry {
uint32_t promisor_pack_nr, promisor_pack_alloc;
uint32_t promisor_split;
+ uint32_t midx_layer_threshold;
+ bool midx_layer_threshold_set;
+ bool midx_tip_rewritten;
+
int split_factor;
};
--
2.54.0.175.g8bd0ec98dc3
^ permalink raw reply related
* [PATCH v4 12/16] builtin/repack.c: convert `--write-midx` to an `OPT_CALLBACK`
From: Taylor Blau @ 2026-05-19 15:58 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1779206239.git.me@ttaylorr.com>
Change the --write-midx (-m) flag from an OPT_BOOL to an OPT_CALLBACK
that accepts an optional mode argument. Introduce an enum with
REPACK_WRITE_MIDX_NONE and REPACK_WRITE_MIDX_DEFAULT to distinguish
between the two states, and update all existing boolean checks
accordingly.
For now, passing no argument (or just `-m`) selects the default mode,
preserving existing behavior. A subsequent commit will add a new mode
for writing incremental MIDXs.
Extract repack_write_midx() as a dispatcher that selects the
appropriate MIDX-writing implementation based on the mode.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
builtin/repack.c | 50 ++++++++++++++++++++++++++++++++++++------------
repack-midx.c | 14 +++++++++++++-
repack.h | 8 +++++++-
3 files changed, 58 insertions(+), 14 deletions(-)
diff --git a/builtin/repack.c b/builtin/repack.c
index 24be147d39a..5d366340c34 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -97,6 +97,24 @@ static int repack_config(const char *var, const char *value,
return git_default_config(var, value, ctx, cb);
}
+static int option_parse_write_midx(const struct option *opt, const char *arg,
+ int unset)
+{
+ enum repack_write_midx_mode *cfg = opt->value;
+
+ if (unset) {
+ *cfg = REPACK_WRITE_MIDX_NONE;
+ return 0;
+ }
+
+ if (!arg || !*arg)
+ *cfg = REPACK_WRITE_MIDX_DEFAULT;
+ else
+ return error(_("unknown value for %s: %s"), opt->long_name, arg);
+
+ return 0;
+}
+
int cmd_repack(int argc,
const char **argv,
const char *prefix,
@@ -119,7 +137,7 @@ int cmd_repack(int argc,
struct string_list keep_pack_list = STRING_LIST_INIT_NODUP;
struct pack_objects_args po_args = PACK_OBJECTS_ARGS_INIT;
struct pack_objects_args cruft_po_args = PACK_OBJECTS_ARGS_INIT;
- int write_midx = 0;
+ enum repack_write_midx_mode write_midx = REPACK_WRITE_MIDX_NONE;
const char *cruft_expiration = NULL;
const char *expire_to = NULL;
const char *filter_to = NULL;
@@ -185,8 +203,14 @@ int cmd_repack(int argc,
N_("do not repack this pack")),
OPT_INTEGER('g', "geometric", &geometry.split_factor,
N_("find a geometric progression with factor <N>")),
- OPT_BOOL('m', "write-midx", &write_midx,
- N_("write a multi-pack index of the resulting packs")),
+ OPT_CALLBACK_F(0, "write-midx", &write_midx,
+ N_("mode"),
+ N_("write a multi-pack index of the resulting packs"),
+ PARSE_OPT_OPTARG, option_parse_write_midx),
+ OPT_SET_INT_F('m', NULL, &write_midx,
+ N_("write a multi-pack index of the resulting packs"),
+ REPACK_WRITE_MIDX_DEFAULT,
+ PARSE_OPT_HIDDEN),
OPT_STRING(0, "expire-to", &expire_to, N_("dir"),
N_("pack prefix to store a pack containing pruned objects")),
OPT_STRING(0, "filter-to", &filter_to, N_("dir"),
@@ -221,14 +245,16 @@ int cmd_repack(int argc,
pack_everything |= ALL_INTO_ONE;
if (write_bitmaps < 0) {
- if (!write_midx &&
+ if (write_midx == REPACK_WRITE_MIDX_NONE &&
(!(pack_everything & ALL_INTO_ONE) || !is_bare_repository()))
write_bitmaps = 0;
}
if (po_args.pack_kept_objects < 0)
- po_args.pack_kept_objects = write_bitmaps > 0 && !write_midx;
+ po_args.pack_kept_objects = write_bitmaps > 0 &&
+ write_midx == REPACK_WRITE_MIDX_NONE;
- if (write_bitmaps && !(pack_everything & ALL_INTO_ONE) && !write_midx)
+ if (write_bitmaps && !(pack_everything & ALL_INTO_ONE) &&
+ write_midx == REPACK_WRITE_MIDX_NONE)
die(_(incremental_bitmap_conflict_error));
if (write_bitmaps && po_args.local &&
@@ -244,7 +270,7 @@ int cmd_repack(int argc,
write_bitmaps = 0;
}
- if (write_midx && write_bitmaps) {
+ if (write_midx != REPACK_WRITE_MIDX_NONE && write_bitmaps) {
struct strbuf path = STRBUF_INIT;
strbuf_addf(&path, "%s/%s_XXXXXX",
@@ -297,7 +323,7 @@ int cmd_repack(int argc,
}
if (repo_has_promisor_remote(repo))
strvec_push(&cmd.args, "--exclude-promisor-objects");
- if (!write_midx) {
+ if (write_midx == REPACK_WRITE_MIDX_NONE) {
if (write_bitmaps > 0)
strvec_push(&cmd.args, "--write-bitmap-index");
else if (write_bitmaps < 0)
@@ -519,7 +545,7 @@ int cmd_repack(int argc,
if (delete_redundant && pack_everything & ALL_INTO_ONE)
existing_packs_mark_for_deletion(&existing, &names);
- if (write_midx) {
+ if (write_midx != REPACK_WRITE_MIDX_NONE) {
struct repack_write_midx_opts opts = {
.existing = &existing,
.geometry = &geometry,
@@ -528,11 +554,11 @@ int cmd_repack(int argc,
.packdir = packdir,
.show_progress = show_progress,
.write_bitmaps = write_bitmaps > 0,
- .midx_must_contain_cruft = midx_must_contain_cruft
+ .midx_must_contain_cruft = midx_must_contain_cruft,
+ .mode = write_midx,
};
- ret = write_midx_included_packs(&opts);
-
+ ret = repack_write_midx(&opts);
if (ret)
goto cleanup;
}
diff --git a/repack-midx.c b/repack-midx.c
index 3fe83715da4..b1ca3797080 100644
--- a/repack-midx.c
+++ b/repack-midx.c
@@ -315,7 +315,7 @@ static int repack_fill_midx_stdin_packs(struct child_process *cmd,
return finish_command(cmd);
}
-int write_midx_included_packs(struct repack_write_midx_opts *opts)
+static int write_midx_included_packs(struct repack_write_midx_opts *opts)
{
struct child_process cmd = CHILD_PROCESS_INIT;
struct string_list include = STRING_LIST_INIT_DUP;
@@ -378,3 +378,15 @@ int write_midx_included_packs(struct repack_write_midx_opts *opts)
return ret;
}
+
+int repack_write_midx(struct repack_write_midx_opts *opts)
+{
+ switch (opts->mode) {
+ case REPACK_WRITE_MIDX_NONE:
+ BUG("write_midx mode is NONE?");
+ case REPACK_WRITE_MIDX_DEFAULT:
+ return write_midx_included_packs(opts);
+ default:
+ BUG("unhandled write_midx mode: %d", opts->mode);
+ }
+}
diff --git a/repack.h b/repack.h
index 77d24ee45fb..81907fcce7f 100644
--- a/repack.h
+++ b/repack.h
@@ -134,6 +134,11 @@ void pack_geometry_release(struct pack_geometry *geometry);
struct tempfile;
+enum repack_write_midx_mode {
+ REPACK_WRITE_MIDX_NONE,
+ REPACK_WRITE_MIDX_DEFAULT,
+};
+
struct repack_write_midx_opts {
struct existing_packs *existing;
struct pack_geometry *geometry;
@@ -143,10 +148,11 @@ struct repack_write_midx_opts {
int show_progress;
int write_bitmaps;
int midx_must_contain_cruft;
+ enum repack_write_midx_mode mode;
};
void midx_snapshot_refs(struct repository *repo, struct tempfile *f);
-int write_midx_included_packs(struct repack_write_midx_opts *opts);
+int repack_write_midx(struct repack_write_midx_opts *opts);
int write_filtered_pack(const struct write_pack_opts *opts,
struct existing_packs *existing,
--
2.54.0.175.g8bd0ec98dc3
^ permalink raw reply related
* [PATCH v4 13/16] packfile: ensure `close_pack_revindex()` frees in-memory revindex
From: Taylor Blau @ 2026-05-19 15:58 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1779206239.git.me@ttaylorr.com>
The following commit will introduce a case where we write a MIDX bitmap
over packs that do not themselves have on-disk *.rev files.
This case is supported within Git, and we will simply fall back to
generating the revindex in memory. But we don't ever release that
memory, causing a leak that is exposed by a test introduced in the
following commit.
(As far as I could find, we never free()'d memory allocated as a
byproduct of creating an in-memory revindex, likely because that code
predates the leak-checking niceties we have in the test suite now.)
Rectify this by calling `FREE_AND_NULL()` on the `p->revindex` field
when calling `close_pack_revindex()`.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
packfile.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/packfile.c b/packfile.c
index b012d648ada..a1e88fdb223 100644
--- a/packfile.c
+++ b/packfile.c
@@ -420,6 +420,8 @@ void close_pack_index(struct packed_git *p)
static void close_pack_revindex(struct packed_git *p)
{
+ FREE_AND_NULL(p->revindex);
+
if (!p->revindex_map)
return;
--
2.54.0.175.g8bd0ec98dc3
^ permalink raw reply related
* [PATCH v4 14/16] repack: implement incremental MIDX repacking
From: Taylor Blau @ 2026-05-19 15:58 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1779206239.git.me@ttaylorr.com>
Implement the `write_midx_incremental()` function, which builds and
maintains an incremental MIDX chain as part of the geometric repacking
process.
Unlike the default mode which writes a single flat MIDX, the incremental
mode constructs a compaction plan that determines which MIDX layers to
write, compact, or copy, and then executes each step using `git
multi-pack-index` subcommands with the --no-write-chain-file flag.
The repacking strategy works as follows:
* Acquire the lock guarding the multi-pack-index-chain.
* A new MIDX layer is always written containing the newly created
pack(s). If the tip MIDX layer was rewritten during geometric
repacking, any surviving packs from that layer are also included.
* Starting from the new layer, adjacent MIDX layers are merged together
as long as the accumulated object count exceeds half the object count
of the next deeper layer (controlled by 'repack.midxSplitFactor').
* Remaining layers in the chain are evaluated pairwise and either
compacted or copied as-is, following the same merging condition.
* Write the contents of the new multi-pack-index chain, atomically move
it into place, and then release the lock.
* Delete any now-unused MIDX layers.
After writing the new layer, the strategy is evaluated among the
existing MIDX layers in order from oldest to newest. Each step that
writes a new MIDX layer uses "--no-write-chain-file" to avoid updating
the multi-pack-index-chain file. After all steps are complete, the new
chain file is written and then atomically moved into place.
At present, this functionality is exposed behind a new enum value,
`REPACK_WRITE_MIDX_INCREMENTAL`, but has no external callers. A
subsequent commit will expose this mode via `git repack
--write-midx=incremental`.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
builtin/repack.c | 5 +
repack-midx.c | 593 +++++++++++++++++++++++++++++++++++++++++++++--
repack.h | 3 +
3 files changed, 588 insertions(+), 13 deletions(-)
diff --git a/builtin/repack.c b/builtin/repack.c
index 5d366340c34..75c57736780 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -42,6 +42,9 @@ static const char incremental_bitmap_conflict_error[] = N_(
"--no-write-bitmap-index or disable the pack.writeBitmaps configuration."
);
+#define DEFAULT_MIDX_SPLIT_FACTOR 2
+#define DEFAULT_MIDX_NEW_LAYER_THRESHOLD 8
+
struct repack_config_ctx {
struct pack_objects_args *po_args;
struct pack_objects_args *cruft_po_args;
@@ -555,6 +558,8 @@ int cmd_repack(int argc,
.show_progress = show_progress,
.write_bitmaps = write_bitmaps > 0,
.midx_must_contain_cruft = midx_must_contain_cruft,
+ .midx_split_factor = DEFAULT_MIDX_SPLIT_FACTOR,
+ .midx_new_layer_threshold = DEFAULT_MIDX_NEW_LAYER_THRESHOLD,
.mode = write_midx,
};
diff --git a/repack-midx.c b/repack-midx.c
index b1ca3797080..f97331fb1b7 100644
--- a/repack-midx.c
+++ b/repack-midx.c
@@ -2,12 +2,16 @@
#include "repack.h"
#include "hash.h"
#include "hex.h"
+#include "lockfile.h"
+#include "midx.h"
#include "odb.h"
#include "oidset.h"
#include "pack-bitmap.h"
+#include "path.h"
#include "refs.h"
#include "run-command.h"
#include "tempfile.h"
+#include "trace2.h"
struct midx_snapshot_ref_data {
struct repository *repo;
@@ -293,26 +297,30 @@ static void repack_prepare_midx_command(struct child_process *cmd,
}
static int repack_fill_midx_stdin_packs(struct child_process *cmd,
- struct string_list *include)
+ struct string_list *include,
+ struct string_list *out)
{
+ struct strbuf in_buf = STRBUF_INIT;
+ struct strbuf out_buf = STRBUF_INIT;
struct string_list_item *item;
- FILE *in;
int ret;
- cmd->in = -1;
-
strvec_push(&cmd->args, "--stdin-packs");
- ret = start_command(cmd);
- if (ret)
- return ret;
-
- in = xfdopen(cmd->in, "w");
for_each_string_list_item(item, include)
- fprintf(in, "%s\n", item->string);
- fclose(in);
+ strbuf_addf(&in_buf, "%s\n", item->string);
- return finish_command(cmd);
+ ret = pipe_command(cmd, in_buf.buf, in_buf.len,
+ out ? &out_buf : NULL, 0, NULL, 0);
+
+ if (out)
+ string_list_split_f(out, out_buf.buf, "\n", -1,
+ STRING_LIST_SPLIT_NONEMPTY);
+
+ strbuf_release(&in_buf);
+ strbuf_release(&out_buf);
+
+ return ret;
}
static int write_midx_included_packs(struct repack_write_midx_opts *opts)
@@ -369,7 +377,7 @@ static int write_midx_included_packs(struct repack_write_midx_opts *opts)
strvec_pushf(&cmd.args, "--refs-snapshot=%s",
opts->refs_snapshot);
- ret = repack_fill_midx_stdin_packs(&cmd, &include);
+ ret = repack_fill_midx_stdin_packs(&cmd, &include, NULL);
done:
if (!ret && opts->write_bitmaps)
remove_redundant_bitmaps(&include, opts->packdir);
@@ -379,6 +387,563 @@ static int write_midx_included_packs(struct repack_write_midx_opts *opts)
return ret;
}
+struct midx_compaction_step {
+ union {
+ struct multi_pack_index *copy;
+ struct string_list write;
+ struct {
+ struct multi_pack_index *from;
+ struct multi_pack_index *to;
+ } compact;
+ } u;
+
+ uint32_t objects_nr;
+ char *csum;
+
+ enum {
+ MIDX_COMPACTION_STEP_UNKNOWN,
+ MIDX_COMPACTION_STEP_COPY,
+ MIDX_COMPACTION_STEP_WRITE,
+ MIDX_COMPACTION_STEP_COMPACT,
+ } type;
+};
+
+static const char *midx_compaction_step_base(const struct midx_compaction_step *step)
+{
+ switch (step->type) {
+ case MIDX_COMPACTION_STEP_UNKNOWN:
+ BUG("cannot use UNKNOWN step as a base");
+ case MIDX_COMPACTION_STEP_COPY:
+ return midx_get_checksum_hex(step->u.copy);
+ case MIDX_COMPACTION_STEP_WRITE:
+ BUG("cannot use WRITE step as a base");
+ case MIDX_COMPACTION_STEP_COMPACT:
+ return midx_get_checksum_hex(step->u.compact.to);
+ default:
+ BUG("unhandled midx compaction step type %d", step->type);
+ }
+}
+
+static int midx_compaction_step_exec_copy(struct midx_compaction_step *step)
+{
+ step->csum = xstrdup(midx_get_checksum_hex(step->u.copy));
+ return 0;
+}
+
+static int midx_compaction_step_exec_write(struct midx_compaction_step *step,
+ struct repack_write_midx_opts *opts,
+ const char *base)
+{
+ struct child_process cmd = CHILD_PROCESS_INIT;
+ struct string_list hash = STRING_LIST_INIT_DUP;
+ struct string_list_item *item;
+ const char *preferred_pack = NULL;
+ int ret = 0;
+
+ if (!step->u.write.nr) {
+ ret = error(_("no packs to write MIDX during compaction"));
+ goto out;
+ }
+
+ for_each_string_list_item(item, &step->u.write) {
+ if (item->util)
+ preferred_pack = item->string;
+ }
+
+ repack_prepare_midx_command(&cmd, opts, "write");
+ strvec_pushl(&cmd.args, "--incremental", "--no-write-chain-file", NULL);
+ strvec_pushf(&cmd.args, "--base=%s", base ? base : "none");
+
+ if (preferred_pack) {
+ struct strbuf buf = STRBUF_INIT;
+
+ strbuf_addstr(&buf, preferred_pack);
+ strbuf_strip_suffix(&buf, ".idx");
+ strbuf_addstr(&buf, ".pack");
+
+ strvec_pushf(&cmd.args, "--preferred-pack=%s", buf.buf);
+
+ strbuf_release(&buf);
+ }
+
+ ret = repack_fill_midx_stdin_packs(&cmd, &step->u.write, &hash);
+ if (hash.nr != 1) {
+ ret = error(_("expected exactly one line during MIDX write, "
+ "got: %"PRIuMAX),
+ (uintmax_t)hash.nr);
+ goto out;
+ }
+
+ step->csum = xstrdup(hash.items[0].string);
+
+out:
+ string_list_clear(&hash, 0);
+
+ return ret;
+}
+
+static int midx_compaction_step_exec_compact(struct midx_compaction_step *step,
+ struct repack_write_midx_opts *opts)
+{
+ struct child_process cmd = CHILD_PROCESS_INIT;
+ struct strbuf buf = STRBUF_INIT;
+ FILE *out = NULL;
+ int ret;
+
+ repack_prepare_midx_command(&cmd, opts, "compact");
+ strvec_pushl(&cmd.args, "--incremental", "--no-write-chain-file",
+ midx_get_checksum_hex(step->u.compact.from),
+ midx_get_checksum_hex(step->u.compact.to), NULL);
+
+ cmd.out = -1;
+
+ ret = start_command(&cmd);
+ if (ret)
+ goto out;
+
+ out = xfdopen(cmd.out, "r");
+ while (strbuf_getline_lf(&buf, out) != EOF) {
+ if (step->csum) {
+ ret = error(_("unexpected MIDX output: '%s'"), buf.buf);
+ fclose(out);
+ out = NULL;
+ finish_command(&cmd);
+ goto out;
+ }
+ step->csum = strbuf_detach(&buf, NULL);
+ }
+
+ ret = finish_command(&cmd);
+
+out:
+ if (out)
+ fclose(out);
+ strbuf_release(&buf);
+
+ return ret;
+}
+
+static int midx_compaction_step_exec(struct midx_compaction_step *step,
+ struct repack_write_midx_opts *opts,
+ const char *base)
+{
+ switch (step->type) {
+ case MIDX_COMPACTION_STEP_UNKNOWN:
+ BUG("cannot execute UNKNOWN midx compaction step");
+ case MIDX_COMPACTION_STEP_COPY:
+ return midx_compaction_step_exec_copy(step);
+ case MIDX_COMPACTION_STEP_WRITE:
+ return midx_compaction_step_exec_write(step, opts, base);
+ case MIDX_COMPACTION_STEP_COMPACT:
+ return midx_compaction_step_exec_compact(step, opts);
+ default:
+ BUG("unhandled midx compaction step type %d", step->type);
+ }
+}
+
+static void midx_compaction_step_release(struct midx_compaction_step *step)
+{
+ if (step->type == MIDX_COMPACTION_STEP_WRITE)
+ string_list_clear(&step->u.write, 0);
+ free(step->csum);
+}
+
+static int repack_make_midx_compaction_plan(struct repack_write_midx_opts *opts,
+ struct midx_compaction_step **steps_p,
+ size_t *steps_nr_p)
+{
+ struct multi_pack_index *m;
+ struct midx_compaction_step *steps = NULL;
+ struct midx_compaction_step step = { 0 };
+ struct strbuf buf = STRBUF_INIT;
+ size_t steps_nr = 0, steps_alloc = 0;
+ uint32_t i;
+ int ret = 0;
+
+ trace2_region_enter("repack", "make_midx_compaction_plan",
+ opts->existing->repo);
+
+ odb_reprepare(opts->existing->repo->objects);
+ m = get_multi_pack_index(opts->existing->source);
+
+ for (i = 0; m && i < m->num_packs + m->num_packs_in_base; i++) {
+ if (prepare_midx_pack(m, i)) {
+ ret = error(_("could not load pack %"PRIu32" from MIDX"),
+ i);
+ goto out;
+ }
+ }
+
+ trace2_region_enter("repack", "steps:write", opts->existing->repo);
+
+ /*
+ * The first MIDX in the resulting chain is always going to be
+ * new.
+ *
+ * At a minimum, it will include all of the newly written packs.
+ * If there is an existing MIDX whose tip layer contains packs
+ * that were repacked, it will also include any of its packs
+ * which were *not* rolled up as part of the geometric repack
+ * (if any), and the previous tip will be replaced.
+ *
+ * It may grow to include the packs from zero or more MIDXs from
+ * the old chain, beginning either at the old tip (if the MIDX
+ * was *not* rewritten) or the old tip's base MIDX layer
+ * (otherwise).
+ */
+ step.type = MIDX_COMPACTION_STEP_WRITE;
+ string_list_init_dup(&step.u.write);
+
+ for (i = 0; i < opts->names->nr; i++) {
+ strbuf_reset(&buf);
+ strbuf_addf(&buf, "pack-%s.idx", opts->names->items[i].string);
+ string_list_append(&step.u.write, buf.buf);
+
+ trace2_data_string("repack", opts->existing->repo,
+ "include:fresh",
+ step.u.write.items[step.u.write.nr - 1].string);
+ }
+ for (i = 0; i < opts->geometry->split; i++) {
+ struct packed_git *p = opts->geometry->pack[i];
+ if (unsigned_add_overflows(step.objects_nr, p->num_objects)) {
+ ret = error(_("too many objects in MIDX compaction step"));
+ goto out;
+ }
+
+ step.objects_nr += p->num_objects;
+ }
+ trace2_data_intmax("repack", opts->existing->repo,
+ "include:fresh:objects_nr",
+ (uintmax_t)step.objects_nr);
+
+ /*
+ * Now handle any existing packs which were *not* rewritten.
+ *
+ * The list of packs in opts->geometry only contains MIDX'd
+ * packs from the newest layer when that layer has more than
+ * 'repack.midxNewLayerThreshold' number of packs.
+ *
+ * If the MIDX tip was rewritten (that is, one or more of those
+ * packs appear below the split line), then add all packs above
+ * the split line to the new layer, as the old one is no longer
+ * usable.
+ *
+ * If the MIDX tip was not rewritten (that is, all MIDX'd packs
+ * from the youngest layer appear below the split line, or were
+ * not included in the geometric repack at all because there
+ * were too few of them), ignore them since we'll retain the
+ * existing layer as-is.
+ */
+ for (i = opts->geometry->split; i < opts->geometry->pack_nr; i++) {
+ struct packed_git *p = opts->geometry->pack[i];
+ struct string_list_item *item;
+
+ strbuf_reset(&buf);
+ strbuf_addstr(&buf, pack_basename(p));
+ strbuf_strip_suffix(&buf, ".pack");
+ strbuf_addstr(&buf, ".idx");
+
+ if (p->multi_pack_index &&
+ !opts->geometry->midx_tip_rewritten) {
+ trace2_data_string("repack", opts->existing->repo,
+ "exclude:unmodified", buf.buf);
+ continue;
+ }
+
+ trace2_data_string("repack", opts->existing->repo,
+ "include:unmodified", buf.buf);
+ trace2_data_string("repack", opts->existing->repo,
+ "include:unmodified:midx",
+ p->multi_pack_index ? "true" : "false");
+
+ item = string_list_append(&step.u.write, buf.buf);
+ if (p->multi_pack_index || i == opts->geometry->pack_nr - 1)
+ item->util = (void *)1; /* mark as preferred */
+
+ if (unsigned_add_overflows(step.objects_nr, p->num_objects)) {
+ ret = error(_("too many objects in MIDX compaction step"));
+ goto out;
+ }
+
+ step.objects_nr += p->num_objects;
+ }
+ trace2_data_intmax("repack", opts->existing->repo,
+ "include:unmodified:objects_nr",
+ (uintmax_t)step.objects_nr);
+
+ /*
+ * If the MIDX tip was rewritten, then we no longer consider it
+ * a candidate for compaction, since it will not exist in the
+ * MIDX chain being built.
+ */
+ if (opts->geometry->midx_tip_rewritten)
+ m = m->base_midx;
+
+ trace2_data_string("repack", opts->existing->repo, "midx:rewrote-tip",
+ opts->geometry->midx_tip_rewritten ? "true" : "false");
+
+ trace2_region_enter("repack", "compact", opts->existing->repo);
+
+ /*
+ * Compact additional MIDX layers into this proposed one until
+ * the merging condition is violated.
+ */
+ while (m) {
+ uint32_t preferred_pack_idx;
+
+ trace2_data_string("repack", opts->existing->repo,
+ "candidate", midx_get_checksum_hex(m));
+
+ if (step.objects_nr < m->num_objects / opts->midx_split_factor) {
+ /*
+ * Stop compacting MIDX layer as soon as the
+ * merged size is less than half the size of the
+ * next layer in the chain.
+ */
+ trace2_data_string("repack", opts->existing->repo,
+ "compact", "violated");
+ trace2_data_intmax("repack", opts->existing->repo,
+ "objects_nr",
+ (uintmax_t)step.objects_nr);
+ trace2_data_intmax("repack", opts->existing->repo,
+ "next_objects_nr",
+ (uintmax_t)m->num_objects);
+ trace2_data_intmax("repack", opts->existing->repo,
+ "split_factor",
+ (uintmax_t)opts->midx_split_factor);
+
+ break;
+ }
+
+ if (midx_preferred_pack(m, &preferred_pack_idx) < 0) {
+ ret = error(_("could not find preferred pack for MIDX "
+ "%s"), midx_get_checksum_hex(m));
+ goto out;
+ }
+
+ for (i = 0; i < m->num_packs; i++) {
+ struct string_list_item *item;
+ uint32_t pack_int_id = i + m->num_packs_in_base;
+ struct packed_git *p = nth_midxed_pack(m, pack_int_id);
+
+ strbuf_reset(&buf);
+ strbuf_addstr(&buf, pack_basename(p));
+ strbuf_strip_suffix(&buf, ".pack");
+ strbuf_addstr(&buf, ".idx");
+
+ trace2_data_string("repack", opts->existing->repo,
+ "midx:pack", buf.buf);
+
+ item = string_list_append(&step.u.write, buf.buf);
+ if (pack_int_id == preferred_pack_idx)
+ item->util = (void *)1; /* mark as preferred */
+ }
+
+ if (unsigned_add_overflows(step.objects_nr, m->num_objects)) {
+ ret = error(_("too many objects in MIDX compaction step"));
+ goto out;
+ }
+ step.objects_nr += m->num_objects;
+
+ m = m->base_midx;
+ }
+
+ if (step.u.write.nr > 0) {
+ /*
+ * As long as there is at least one new pack to write
+ * (and thus the MIDX is non-empty), add it to the plan.
+ */
+ ALLOC_GROW(steps, steps_nr + 1, steps_alloc);
+ steps[steps_nr++] = step;
+ }
+
+ trace2_data_intmax("repack", opts->existing->repo,
+ "step:objects_nr", (uintmax_t)step.objects_nr);
+ trace2_data_intmax("repack", opts->existing->repo,
+ "step:packs_nr", (uintmax_t)step.u.write.nr);
+
+ trace2_region_leave("repack", "compact", opts->existing->repo);
+ trace2_region_leave("repack", "steps:write", opts->existing->repo);
+
+ trace2_region_enter("repack", "steps:rest", opts->existing->repo);
+
+ /*
+ * Then start over, repeat, and either compact or keep as-is
+ * each MIDX layer until we have exhausted the chain.
+ *
+ * Finally, evaluate the remainder of the chain (if any) and
+ * either compact a sequence of adjacent layers, or keep
+ * individual layers as-is according to the same merging
+ * condition as above.
+ */
+ while (m) {
+ struct multi_pack_index *next = m;
+
+ ALLOC_GROW(steps, steps_nr + 1, steps_alloc);
+
+ memset(&step, 0, sizeof(step));
+ step.type = MIDX_COMPACTION_STEP_UNKNOWN;
+
+ trace2_region_enter("repack", "step", opts->existing->repo);
+
+ trace2_data_string("repack", opts->existing->repo,
+ "from", midx_get_checksum_hex(m));
+
+ while (next) {
+ uint32_t proposed_objects_nr;
+ if (unsigned_add_overflows(step.objects_nr, next->num_objects)) {
+ ret = error(_("too many objects in MIDX compaction step"));
+ trace2_region_leave("repack", "step", opts->existing->repo);
+ goto out;
+ }
+
+ proposed_objects_nr = step.objects_nr + next->num_objects;
+
+ trace2_data_string("repack", opts->existing->repo,
+ "proposed",
+ midx_get_checksum_hex(next));
+ trace2_data_intmax("repack", opts->existing->repo,
+ "proposed:objects_nr",
+ (uintmax_t)next->num_objects);
+
+ if (!next->base_midx) {
+ /*
+ * If we are at the end of the MIDX
+ * chain, there is nothing to compact,
+ * so mark it and stop.
+ */
+ step.objects_nr = proposed_objects_nr;
+ break;
+ }
+
+ if (proposed_objects_nr < next->base_midx->num_objects / opts->midx_split_factor) {
+ /*
+ * If there is a MIDX following this
+ * one, but our accumulated size is less
+ * than half of its size, compacting
+ * them would violate the merging
+ * condition, so stop here.
+ */
+
+ trace2_data_string("repack", opts->existing->repo,
+ "compact:violated:at",
+ midx_get_checksum_hex(next->base_midx));
+ trace2_data_intmax("repack", opts->existing->repo,
+ "compact:violated:at:objects_nr",
+ (uintmax_t)next->base_midx->num_objects);
+ break;
+ }
+
+ /*
+ * Otherwise, it is OK to compact the next layer
+ * into this one. Do so, and then continue
+ * through the remainder of the chain.
+ */
+ step.objects_nr = proposed_objects_nr;
+ trace2_data_intmax("repack", opts->existing->repo,
+ "step:objects_nr",
+ (uintmax_t)step.objects_nr);
+ next = next->base_midx;
+ }
+
+ if (m == next) {
+ step.type = MIDX_COMPACTION_STEP_COPY;
+ step.u.copy = m;
+
+ trace2_data_string("repack", opts->existing->repo,
+ "type", "copy");
+ } else {
+ step.type = MIDX_COMPACTION_STEP_COMPACT;
+ step.u.compact.from = next;
+ step.u.compact.to = m;
+
+ trace2_data_string("repack", opts->existing->repo,
+ "to", midx_get_checksum_hex(m));
+ trace2_data_string("repack", opts->existing->repo,
+ "type", "compact");
+ }
+
+ m = next->base_midx;
+ steps[steps_nr++] = step;
+ trace2_region_leave("repack", "step", opts->existing->repo);
+ }
+
+ trace2_region_leave("repack", "steps:rest", opts->existing->repo);
+
+out:
+ *steps_p = steps;
+ *steps_nr_p = steps_nr;
+
+ strbuf_release(&buf);
+
+ trace2_region_leave("repack", "make_midx_compaction_plan",
+ opts->existing->repo);
+
+ return ret;
+}
+
+static int write_midx_incremental(struct repack_write_midx_opts *opts)
+{
+ struct midx_compaction_step *steps = NULL;
+ struct strbuf lock_name = STRBUF_INIT;
+ struct lock_file lf;
+ size_t steps_nr = 0;
+ size_t i;
+ int ret = 0;
+
+ get_midx_chain_filename(opts->existing->source, &lock_name);
+ if (safe_create_leading_directories(opts->existing->repo,
+ lock_name.buf))
+ die_errno(_("unable to create leading directories of %s"),
+ lock_name.buf);
+ hold_lock_file_for_update(&lf, lock_name.buf, LOCK_DIE_ON_ERROR);
+
+ if (!fdopen_lock_file(&lf, "w")) {
+ ret = error_errno(_("unable to open multi-pack-index chain file"));
+ goto done;
+ }
+
+ if (repack_make_midx_compaction_plan(opts, &steps, &steps_nr) < 0) {
+ ret = error(_("unable to generate compaction plan"));
+ goto done;
+ }
+
+ for (i = 0; i < steps_nr; i++) {
+ struct midx_compaction_step *step = &steps[i];
+ char *base = NULL;
+
+ if (i + 1 < steps_nr)
+ base = xstrdup(midx_compaction_step_base(&steps[i + 1]));
+
+ if (midx_compaction_step_exec(step, opts, base) < 0) {
+ ret = error(_("unable to execute compaction step %"PRIuMAX),
+ (uintmax_t)i);
+ free(base);
+ goto done;
+ }
+
+ free(base);
+ }
+
+ i = steps_nr;
+ while (i--) {
+ struct midx_compaction_step *step = &steps[i];
+ if (!step->csum)
+ BUG("missing result for compaction step %"PRIuMAX,
+ (uintmax_t)i);
+ fprintf(get_lock_file_fp(&lf), "%s\n", step->csum);
+ }
+
+ commit_lock_file(&lf);
+
+done:
+ strbuf_release(&lock_name);
+ for (i = 0; i < steps_nr; i++)
+ midx_compaction_step_release(&steps[i]);
+ free(steps);
+ return ret;
+}
+
int repack_write_midx(struct repack_write_midx_opts *opts)
{
switch (opts->mode) {
@@ -386,6 +951,8 @@ int repack_write_midx(struct repack_write_midx_opts *opts)
BUG("write_midx mode is NONE?");
case REPACK_WRITE_MIDX_DEFAULT:
return write_midx_included_packs(opts);
+ case REPACK_WRITE_MIDX_INCREMENTAL:
+ return write_midx_incremental(opts);
default:
BUG("unhandled write_midx mode: %d", opts->mode);
}
diff --git a/repack.h b/repack.h
index 81907fcce7f..831ccfb1c6c 100644
--- a/repack.h
+++ b/repack.h
@@ -137,6 +137,7 @@ struct tempfile;
enum repack_write_midx_mode {
REPACK_WRITE_MIDX_NONE,
REPACK_WRITE_MIDX_DEFAULT,
+ REPACK_WRITE_MIDX_INCREMENTAL,
};
struct repack_write_midx_opts {
@@ -148,6 +149,8 @@ struct repack_write_midx_opts {
int show_progress;
int write_bitmaps;
int midx_must_contain_cruft;
+ int midx_split_factor;
+ int midx_new_layer_threshold;
enum repack_write_midx_mode mode;
};
--
2.54.0.175.g8bd0ec98dc3
^ permalink raw reply related
* [PATCH v4 15/16] repack: introduce `--write-midx=incremental`
From: Taylor Blau @ 2026-05-19 15:58 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1779206239.git.me@ttaylorr.com>
Expose the incremental MIDX repacking mode (implemented in an earlier
commit) via a new --write-midx=incremental option for `git repack`.
Add "incremental" as a recognized argument to the --write-midx
OPT_CALLBACK, mapping it to REPACK_WRITE_MIDX_INCREMENTAL. When this
mode is active and --geometric is in use, set the midx_layer_threshold
on the pack geometry so that only packs in sufficiently large tip layers
are considered for repacking.
Two new configuration options control the compaction behavior:
- repack.midxSplitFactor (default: 2): the factor used in the
geometric merging condition for MIDX layers.
- repack.midxNewLayerThreshold (default: 8): the minimum number of
packs in the tip MIDX layer before its packs are considered as
candidates for geometric repacking.
Add tests exercising the new mode across a variety of scenarios
including basic geometric violations, multi-round chain integrity,
branching and merging histories, cross-layer object uniqueness, and
threshold-based compaction.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
Documentation/config/repack.adoc | 18 ++
Documentation/git-repack.adoc | 39 ++-
builtin/repack.c | 49 ++-
midx.c | 29 ++
midx.h | 3 +
repack-geometry.c | 13 +-
repack-midx.c | 5 +
repack.c | 56 +++-
repack.h | 10 +-
t/meson.build | 1 +
t/t7705-repack-incremental-midx.sh | 470 +++++++++++++++++++++++++++++
11 files changed, 669 insertions(+), 24 deletions(-)
create mode 100755 t/t7705-repack-incremental-midx.sh
diff --git a/Documentation/config/repack.adoc b/Documentation/config/repack.adoc
index e9e78dcb198..4c22a499f62 100644
--- a/Documentation/config/repack.adoc
+++ b/Documentation/config/repack.adoc
@@ -46,3 +46,21 @@ repack.midxMustContainCruft::
`--write-midx`. When false, cruft packs are only included in the MIDX
when necessary (e.g., because they might be required to form a
reachability closure with MIDX bitmaps). Defaults to true.
+
+repack.midxSplitFactor::
+ The factor used in the geometric merging condition when
+ compacting incremental MIDX layers during `git repack` when
+ invoked with the `--write-midx=incremental` option.
++
+Adjacent layers are merged when the accumulated object count of the
+newer layer exceeds `1/<N>` of the object count of the next deeper
+layer. Must be at least 2. Defaults to 2.
+
+repack.midxNewLayerThreshold::
+ The minimum number of packs in the tip MIDX layer before those
+ packs are considered as candidates for geometric repacking
+ during `git repack --write-midx=incremental`.
++
+When the tip layer has fewer packs than this threshold, those packs are
+excluded from the geometric repack entirely, and are thus left
+unmodified. Must be at least 1. Defaults to 8.
diff --git a/Documentation/git-repack.adoc b/Documentation/git-repack.adoc
index 673ce910837..27a99cc46f4 100644
--- a/Documentation/git-repack.adoc
+++ b/Documentation/git-repack.adoc
@@ -11,7 +11,7 @@ SYNOPSIS
[verse]
'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]
[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]
- [--write-midx] [--name-hash-version=<n>] [--path-walk]
+ [--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]
DESCRIPTION
-----------
@@ -250,9 +250,42 @@ pack as the preferred pack for object selection by the MIDX (see
linkgit:git-multi-pack-index[1]).
-m::
---write-midx::
+--write-midx[=<mode>]::
Write a multi-pack index (see linkgit:git-multi-pack-index[1])
- containing the non-redundant packs.
+ containing the non-redundant packs. The following modes are
+ available:
++
+--
+ `default`;;
+ Write a single MIDX covering all packs. This is the
+ default when `--write-midx` is given without an
+ explicit mode.
+
+ `incremental`;;
+ Write an incremental MIDX chain instead of a single
+ flat MIDX. This mode requires `--geometric`.
++
+The incremental mode maintains a chain of MIDX layers that is compacted
+over time using a geometric merging strategy. Each repack creates a new
+tip layer containing the newly written pack(s). Adjacent layers are then
+merged whenever the newer layer's object count exceeds
+`1/repack.midxSplitFactor` of the next deeper layer's count. Layers
+that do not meet this condition are retained as-is.
++
+The result is that newer (tip) layers tend to contain many small packs
+with relatively few objects, while older (deeper) layers contain fewer,
+larger packs covering more objects. Because compaction is driven by the
+tip of the chain, newer layers are also rewritten more frequently than
+older ones, which are only touched when enough objects have accumulated
+to justify merging into them. This keeps the total number of layers
+logarithmic relative to the total number of objects.
++
+Only packs in the tip MIDX layer are considered as candidates for the
+geometric repack; packs in deeper layers are left untouched. If the tip
+layer contains fewer packs than `repack.midxNewLayerThreshold`, those
+packs are excluded from the geometry entirely, and a new layer is
+created for any new pack(s) without disturbing the existing chain.
+--
--name-hash-version=<n>::
Provide this argument to the underlying `git pack-objects` process.
diff --git a/builtin/repack.c b/builtin/repack.c
index 75c57736780..5ffa18e085e 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -33,7 +33,7 @@ static int midx_must_contain_cruft = 1;
static const char *const git_repack_usage[] = {
N_("git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\n"
"[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]\n"
- "[--write-midx] [--name-hash-version=<n>] [--path-walk]"),
+ "[--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]"),
NULL
};
@@ -48,6 +48,8 @@ static const char incremental_bitmap_conflict_error[] = N_(
struct repack_config_ctx {
struct pack_objects_args *po_args;
struct pack_objects_args *cruft_po_args;
+ int midx_split_factor;
+ int midx_new_layer_threshold;
};
static int repack_config(const char *var, const char *value,
@@ -97,6 +99,16 @@ static int repack_config(const char *var, const char *value,
midx_must_contain_cruft = git_config_bool(var, value);
return 0;
}
+ if (!strcmp(var, "repack.midxsplitfactor")) {
+ repack_ctx->midx_split_factor = git_config_int(var, value,
+ ctx->kvi);
+ return 0;
+ }
+ if (!strcmp(var, "repack.midxnewlayerthreshold")) {
+ repack_ctx->midx_new_layer_threshold = git_config_int(var, value,
+ ctx->kvi);
+ return 0;
+ }
return git_default_config(var, value, ctx, cb);
}
@@ -112,6 +124,8 @@ static int option_parse_write_midx(const struct option *opt, const char *arg,
if (!arg || !*arg)
*cfg = REPACK_WRITE_MIDX_DEFAULT;
+ else if (!strcmp(arg, "incremental"))
+ *cfg = REPACK_WRITE_MIDX_INCREMENTAL;
else
return error(_("unknown value for %s: %s"), opt->long_name, arg);
@@ -226,6 +240,8 @@ int cmd_repack(int argc,
memset(&config_ctx, 0, sizeof(config_ctx));
config_ctx.po_args = &po_args;
config_ctx.cruft_po_args = &cruft_po_args;
+ config_ctx.midx_split_factor = DEFAULT_MIDX_SPLIT_FACTOR;
+ config_ctx.midx_new_layer_threshold = DEFAULT_MIDX_NEW_LAYER_THRESHOLD;
repo_config(repo, repack_config, &config_ctx);
@@ -247,6 +263,9 @@ int cmd_repack(int argc,
if (pack_everything & PACK_CRUFT)
pack_everything |= ALL_INTO_ONE;
+ if (write_midx == REPACK_WRITE_MIDX_INCREMENTAL && !geometry.split_factor)
+ die(_("--write-midx=incremental requires --geometric"));
+
if (write_bitmaps < 0) {
if (write_midx == REPACK_WRITE_MIDX_NONE &&
(!(pack_everything & ALL_INTO_ONE) || !is_bare_repository()))
@@ -273,6 +292,13 @@ int cmd_repack(int argc,
write_bitmaps = 0;
}
+ if (config_ctx.midx_split_factor < 2)
+ die(_("invalid value for %s: %d"), "--midx-split-factor",
+ config_ctx.midx_split_factor);
+ if (config_ctx.midx_new_layer_threshold < 1)
+ die(_("invalid value for %s: %d"), "--midx-new-layer-threshold",
+ config_ctx.midx_new_layer_threshold);
+
if (write_midx != REPACK_WRITE_MIDX_NONE && write_bitmaps) {
struct strbuf path = STRBUF_INIT;
@@ -296,6 +322,10 @@ int cmd_repack(int argc,
if (geometry.split_factor) {
if (pack_everything)
die(_("options '%s' and '%s' cannot be used together"), "--geometric", "-A/-a");
+ if (write_midx == REPACK_WRITE_MIDX_INCREMENTAL) {
+ geometry.midx_layer_threshold = config_ctx.midx_new_layer_threshold;
+ geometry.midx_layer_threshold_set = true;
+ }
pack_geometry_init(&geometry, &existing, &po_args);
pack_geometry_split(&geometry);
}
@@ -545,8 +575,11 @@ int cmd_repack(int argc,
packtmp);
/* End of pack replacement. */
- if (delete_redundant && pack_everything & ALL_INTO_ONE)
+ if (delete_redundant && pack_everything & ALL_INTO_ONE) {
+ if (write_midx == REPACK_WRITE_MIDX_INCREMENTAL)
+ existing_packs_retain_midx_packs(&existing);
existing_packs_mark_for_deletion(&existing, &names);
+ }
if (write_midx != REPACK_WRITE_MIDX_NONE) {
struct repack_write_midx_opts opts = {
@@ -558,8 +591,8 @@ int cmd_repack(int argc,
.show_progress = show_progress,
.write_bitmaps = write_bitmaps > 0,
.midx_must_contain_cruft = midx_must_contain_cruft,
- .midx_split_factor = DEFAULT_MIDX_SPLIT_FACTOR,
- .midx_new_layer_threshold = DEFAULT_MIDX_NEW_LAYER_THRESHOLD,
+ .midx_split_factor = config_ctx.midx_split_factor,
+ .midx_new_layer_threshold = config_ctx.midx_new_layer_threshold,
.mode = write_midx,
};
@@ -572,11 +605,15 @@ int cmd_repack(int argc,
if (delete_redundant) {
int opts = 0;
- existing_packs_remove_redundant(&existing, packdir);
+ bool wrote_incremental_midx = write_midx == REPACK_WRITE_MIDX_INCREMENTAL;
+
+ existing_packs_remove_redundant(&existing, packdir,
+ wrote_incremental_midx);
if (geometry.split_factor)
pack_geometry_remove_redundant(&geometry, &names,
- &existing, packdir);
+ &existing, packdir,
+ wrote_incremental_midx);
if (show_progress)
opts |= PRUNE_PACKED_VERBOSE;
prune_packed_objects(opts);
diff --git a/midx.c b/midx.c
index dc86c8e7fee..efbfbb13f41 100644
--- a/midx.c
+++ b/midx.c
@@ -850,6 +850,35 @@ void clear_midx_file(struct repository *r)
strbuf_release(&midx);
}
+void clear_incremental_midx_files(struct repository *r,
+ const struct strvec *keep_hashes)
+{
+ struct odb_source *source = r->objects->sources;
+ struct strbuf chain = STRBUF_INIT;
+
+ get_midx_chain_filename(source, &chain);
+
+ for (; source; source = source->next) {
+ struct odb_source_files *files = odb_source_files_downcast(source);
+ if (files->packed->midx)
+ close_midx(files->packed->midx);
+ files->packed->midx = NULL;
+ }
+
+ if (!keep_hashes && remove_path(chain.buf))
+ die(_("failed to clear multi-pack-index chain at %s"),
+ chain.buf);
+
+ clear_incremental_midx_files_ext(r->objects->sources, MIDX_EXT_BITMAP,
+ keep_hashes);
+ clear_incremental_midx_files_ext(r->objects->sources, MIDX_EXT_REV,
+ keep_hashes);
+ clear_incremental_midx_files_ext(r->objects->sources, MIDX_EXT_MIDX,
+ keep_hashes);
+
+ strbuf_release(&chain);
+}
+
static int verify_midx_error;
__attribute__((format (printf, 1, 2)))
diff --git a/midx.h b/midx.h
index 3ee12dd08ec..63853a03a47 100644
--- a/midx.h
+++ b/midx.h
@@ -9,6 +9,7 @@ struct repository;
struct bitmapped_pack;
struct git_hash_algo;
struct odb_source;
+struct strvec;
#define MIDX_SIGNATURE 0x4d494458 /* "MIDX" */
#define MIDX_VERSION_V1 1
@@ -143,6 +144,8 @@ int write_midx_file_compact(struct odb_source *source,
const char *incremental_base,
unsigned flags);
void clear_midx_file(struct repository *r);
+void clear_incremental_midx_files(struct repository *r,
+ const struct strvec *keep_hashes);
int verify_midx_file(struct odb_source *source, unsigned flags);
int expire_midx_packs(struct odb_source *source, unsigned flags);
int midx_repack(struct odb_source *source, size_t batch_size, unsigned flags);
diff --git a/repack-geometry.c b/repack-geometry.c
index 2408b8a3cc2..2064683dcfe 100644
--- a/repack-geometry.c
+++ b/repack-geometry.c
@@ -249,7 +249,8 @@ static void remove_redundant_packs(struct packed_git **pack,
uint32_t pack_nr,
struct string_list *names,
struct existing_packs *existing,
- const char *packdir)
+ const char *packdir,
+ bool wrote_incremental_midx)
{
const struct git_hash_algo *algop = existing->repo->hash_algo;
struct strbuf buf = STRBUF_INIT;
@@ -269,7 +270,8 @@ static void remove_redundant_packs(struct packed_git **pack,
(string_list_has_string(&existing->kept_packs, buf.buf)))
continue;
- repack_remove_redundant_pack(existing->repo, packdir, buf.buf);
+ repack_remove_redundant_pack(existing->repo, packdir, buf.buf,
+ wrote_incremental_midx);
}
strbuf_release(&buf);
@@ -278,12 +280,13 @@ static void remove_redundant_packs(struct packed_git **pack,
void pack_geometry_remove_redundant(struct pack_geometry *geometry,
struct string_list *names,
struct existing_packs *existing,
- const char *packdir)
+ const char *packdir,
+ bool wrote_incremental_midx)
{
remove_redundant_packs(geometry->pack, geometry->split,
- names, existing, packdir);
+ names, existing, packdir, wrote_incremental_midx);
remove_redundant_packs(geometry->promisor_pack, geometry->promisor_split,
- names, existing, packdir);
+ names, existing, packdir, wrote_incremental_midx);
}
void pack_geometry_release(struct pack_geometry *geometry)
diff --git a/repack-midx.c b/repack-midx.c
index f97331fb1b7..4f5deeb97bf 100644
--- a/repack-midx.c
+++ b/repack-midx.c
@@ -887,6 +887,7 @@ static int write_midx_incremental(struct repack_write_midx_opts *opts)
struct midx_compaction_step *steps = NULL;
struct strbuf lock_name = STRBUF_INIT;
struct lock_file lf;
+ struct strvec keep_hashes = STRVEC_INIT;
size_t steps_nr = 0;
size_t i;
int ret = 0;
@@ -932,11 +933,15 @@ static int write_midx_incremental(struct repack_write_midx_opts *opts)
BUG("missing result for compaction step %"PRIuMAX,
(uintmax_t)i);
fprintf(get_lock_file_fp(&lf), "%s\n", step->csum);
+ strvec_push(&keep_hashes, step->csum);
}
commit_lock_file(&lf);
+ clear_incremental_midx_files(opts->existing->repo, &keep_hashes);
+
done:
+ strvec_clear(&keep_hashes);
strbuf_release(&lock_name);
for (i = 0; i < steps_nr; i++)
midx_compaction_step_release(&steps[i]);
diff --git a/repack.c b/repack.c
index 2ee6b51420a..571dabb665e 100644
--- a/repack.c
+++ b/repack.c
@@ -55,14 +55,18 @@ void pack_objects_args_release(struct pack_objects_args *args)
}
void repack_remove_redundant_pack(struct repository *repo, const char *dir_name,
- const char *base_name)
+ const char *base_name,
+ bool wrote_incremental_midx)
{
struct strbuf buf = STRBUF_INIT;
struct odb_source *source = repo->objects->sources;
struct multi_pack_index *m = get_multi_pack_index(source);
strbuf_addf(&buf, "%s.pack", base_name);
- if (m && source->local && midx_contains_pack(m, buf.buf))
+ if (m && source->local && midx_contains_pack(m, buf.buf)) {
clear_midx_file(repo);
+ if (!wrote_incremental_midx)
+ clear_incremental_midx_files(repo, NULL);
+ }
strbuf_insertf(&buf, 0, "%s/", dir_name);
unlink_pack_path(buf.buf, 1);
strbuf_release(&buf);
@@ -250,25 +254,63 @@ void existing_packs_mark_for_deletion(struct existing_packs *existing,
&existing->cruft_packs);
}
+/*
+ * Mark every pack that is referenced by the existing MIDX chain as
+ * retained, so that a subsequent call to
+ * existing_packs_mark_for_deletion() will not mark them for deletion.
+ *
+ * This is used when writing an incremental MIDX layer on top of an
+ * existing chain: retained layers continue to reference the same
+ * packs on disk, so those packs must not be unlinked even if the
+ * freshly-written pack supersedes them.
+ */
+void existing_packs_retain_midx_packs(struct existing_packs *existing)
+{
+ struct string_list_item *item;
+ struct strbuf buf = STRBUF_INIT;
+
+ for_each_string_list_item(item, &existing->midx_packs) {
+ struct string_list_item *found;
+
+ strbuf_reset(&buf);
+ strbuf_addstr(&buf, item->string);
+ strbuf_strip_suffix(&buf, ".pack");
+ strbuf_strip_suffix(&buf, ".idx");
+
+ found = string_list_lookup(&existing->non_kept_packs, buf.buf);
+ if (found)
+ existing_packs_mark_retained(found);
+
+ found = string_list_lookup(&existing->cruft_packs, buf.buf);
+ if (found)
+ existing_packs_mark_retained(found);
+ }
+
+ strbuf_release(&buf);
+}
+
static void remove_redundant_packs_1(struct repository *repo,
struct string_list *packs,
- const char *packdir)
+ const char *packdir,
+ bool wrote_incremental_midx)
{
struct string_list_item *item;
for_each_string_list_item(item, packs) {
if (!existing_pack_is_marked_for_deletion(item))
continue;
- repack_remove_redundant_pack(repo, packdir, item->string);
+ repack_remove_redundant_pack(repo, packdir, item->string,
+ wrote_incremental_midx);
}
}
void existing_packs_remove_redundant(struct existing_packs *existing,
- const char *packdir)
+ const char *packdir,
+ bool wrote_incremental_midx)
{
remove_redundant_packs_1(existing->repo, &existing->non_kept_packs,
- packdir);
+ packdir, wrote_incremental_midx);
remove_redundant_packs_1(existing->repo, &existing->cruft_packs,
- packdir);
+ packdir, wrote_incremental_midx);
}
void existing_packs_release(struct existing_packs *existing)
diff --git a/repack.h b/repack.h
index 831ccfb1c6c..f9fbc895f02 100644
--- a/repack.h
+++ b/repack.h
@@ -34,7 +34,8 @@ void prepare_pack_objects(struct child_process *cmd,
void pack_objects_args_release(struct pack_objects_args *args);
void repack_remove_redundant_pack(struct repository *repo, const char *dir_name,
- const char *base_name);
+ const char *base_name,
+ bool wrote_incremental_midx);
struct write_pack_opts {
struct pack_objects_args *po_args;
@@ -83,8 +84,10 @@ void existing_packs_retain_cruft(struct existing_packs *existing,
struct packed_git *cruft);
void existing_packs_mark_for_deletion(struct existing_packs *existing,
struct string_list *names);
+void existing_packs_retain_midx_packs(struct existing_packs *existing);
void existing_packs_remove_redundant(struct existing_packs *existing,
- const char *packdir);
+ const char *packdir,
+ bool wrote_incremental_midx);
void existing_packs_release(struct existing_packs *existing);
struct generated_pack;
@@ -129,7 +132,8 @@ struct packed_git *pack_geometry_preferred_pack(struct pack_geometry *geometry);
void pack_geometry_remove_redundant(struct pack_geometry *geometry,
struct string_list *names,
struct existing_packs *existing,
- const char *packdir);
+ const char *packdir,
+ bool wrote_incremental_midx);
void pack_geometry_release(struct pack_geometry *geometry);
struct tempfile;
diff --git a/t/meson.build b/t/meson.build
index 7528e5cda5f..25f0d823d8e 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -951,6 +951,7 @@ integration_tests = [
't7702-repack-cyclic-alternate.sh',
't7703-repack-geometric.sh',
't7704-repack-cruft.sh',
+ 't7705-repack-incremental-midx.sh',
't7800-difftool.sh',
't7810-grep.sh',
't7811-grep-open.sh',
diff --git a/t/t7705-repack-incremental-midx.sh b/t/t7705-repack-incremental-midx.sh
new file mode 100755
index 00000000000..9e317ff6e8f
--- /dev/null
+++ b/t/t7705-repack-incremental-midx.sh
@@ -0,0 +1,470 @@
+#!/bin/sh
+
+test_description='git repack --write-midx=incremental'
+
+. ./test-lib.sh
+
+GIT_TEST_MULTI_PACK_INDEX=0
+GIT_TEST_MULTI_PACK_INDEX_WRITE_BITMAP=0
+GIT_TEST_MULTI_PACK_INDEX_WRITE_INCREMENTAL=0
+
+objdir=.git/objects
+packdir=$objdir/pack
+midxdir=$packdir/multi-pack-index.d
+midx_chain=$midxdir/multi-pack-index-chain
+
+# incrementally_repack N
+#
+# Make "N" new commits, each stored in their own pack, and then repacked
+# with the --write-midx=incremental strategy.
+incrementally_repack () {
+ for i in $(test_seq 1 "$1")
+ do
+ test_commit "$i" &&
+
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index &&
+ git multi-pack-index verify || return 1
+ done
+}
+
+# Create packs with geometrically increasing sizes so that they
+# satisfy the geometric progression and survive a --geometric=2
+# repack without being rolled up. Creates 3 packs containing 1,
+# 2, and 6 commits (3, 6, and 18 objects) respectively.
+create_geometric_packs () {
+ test_commit "small" &&
+ git repack -d &&
+
+ test_commit_bulk --message="medium" 2 &&
+ test_commit_bulk --message="large" 6 &&
+
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index
+}
+
+# create_layer <test_commit_bulk args>
+#
+# Creates a new MIDX layer with the contents of "test_commit_bulk $@".
+create_layer () {
+ test_commit_bulk "$@" &&
+
+ git multi-pack-index write --incremental --bitmap
+}
+
+# create_layers
+#
+# Reads lines of "<message> <nr>" from stdin and creates a new MIDX
+# layer for each line. See create_layer above for more.
+create_layers () {
+ while read msg nr
+ do
+ create_layer --message="$msg" "$nr" || return 1
+ done
+}
+
+test_expect_success '--write-midx=incremental requires --geometric' '
+ test_must_fail git repack --write-midx=incremental 2>err &&
+
+ test_grep -- "--write-midx=incremental requires --geometric" err
+'
+
+test_expect_success 'below layer threshold, tip packs excluded' '
+ git init below-layer-threshold-tip-packs-excluded &&
+ (
+ cd below-layer-threshold-tip-packs-excluded &&
+
+ git config maintenance.auto false &&
+ git config repack.midxnewlayerthreshold 4 &&
+ git config repack.midxsplitfactor 2 &&
+
+ # Create 3 packs forming a geometric progression by
+ # object count such that they are unmodified by the
+ # initial repack. The MIDX chain thusly contains a
+ # single layer with three packs.
+ create_geometric_packs &&
+ ls $packdir/pack-*.idx | sort >packs.before &&
+ test_line_count = 1 $midx_chain &&
+ cp $midx_chain $midx_chain.before &&
+
+ # Repack a new commit. Since the layer threshold is
+ # unmet, a new MIDX layer is added on top of the
+ # existing one.
+ test_commit extra &&
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index &&
+ git multi-pack-index verify &&
+
+ ls $packdir/pack-*.idx | sort >packs.after &&
+ comm -13 packs.before packs.after >packs.new &&
+ test_line_count = 1 packs.new &&
+
+ test_line_count = 2 "$midx_chain" &&
+ head -n 1 "$midx_chain.before" >expect &&
+ head -n 1 "$midx_chain" >actual &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success 'above layer threshold, tip packs repacked' '
+ git init above-layer-threshold-tip-packs-repacked &&
+ (
+ cd above-layer-threshold-tip-packs-repacked &&
+
+ git config maintenance.auto false &&
+ git config repack.midxnewlayerthreshold 2 &&
+ git config repack.midxsplitfactor 2 &&
+
+ # Same setup, but with the layer threshold set to 2.
+ # Since the tip MIDX layer meets that threshold, its
+ # packs are considered repack candidates.
+ create_geometric_packs &&
+ cp $midx_chain $midx_chain.before &&
+
+ # Perturb the existing progression such that it is
+ # rolled up into a single new pack, invalidating the
+ # existing MIDX layer and replacing it with a new one.
+ test_commit extra &&
+ git repack -d &&
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index &&
+
+ ! test_cmp $midx_chain.before $midx_chain &&
+ test_line_count = 1 $midx_chain &&
+
+ git multi-pack-index verify
+ )
+'
+
+test_expect_success 'above layer threshold, tip layer preserved' '
+ git init above-layer-threshold-tip-layer-preserved &&
+ (
+ cd above-layer-threshold-tip-layer-preserved &&
+
+ git config maintenance.auto false &&
+ git config repack.midxnewlayerthreshold 2 &&
+ git config repack.midxsplitfactor 2 &&
+
+ test_commit_bulk --message="medium" 2 &&
+ test_commit_bulk --message="large" 6 &&
+
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index &&
+
+ test_line_count = 1 "$midx_chain" &&
+ ls $packdir/pack-*.idx | sort >packs.before &&
+ cp $midx_chain $midx_chain.before &&
+
+ # Create objects to form a pack satisfying the geometric
+ # progression (thus preserving the tip layer), but not
+ # so large that it meets the layer merging condition.
+ test_commit_bulk --message="small" 1 &&
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index &&
+
+ ls $packdir/pack-*.idx | sort >packs.after &&
+ comm -13 packs.before packs.after >packs.new &&
+
+ test_line_count = 1 packs.new &&
+ test_line_count = 3 packs.after &&
+ test_line_count = 2 "$midx_chain" &&
+ head -n 1 "$midx_chain.before" >expect &&
+ head -n 1 "$midx_chain" >actual &&
+ test_cmp expect actual &&
+
+ git multi-pack-index verify
+ )
+'
+
+test_expect_success 'above layer threshold, tip packs preserved' '
+ git init above-layer-threshold-tip-packs-preserved &&
+ (
+ cd above-layer-threshold-tip-packs-preserved &&
+
+ git config maintenance.auto false &&
+ git config repack.midxnewlayerthreshold 2 &&
+ git config repack.midxsplitfactor 2 &&
+
+ create_geometric_packs &&
+ ls $packdir/pack-*.idx | sort >packs.before &&
+ cp $midx_chain $midx_chain.before &&
+
+ # Same setup as above, but this time the new objects do
+ # not satisfy the new layer merging condition, resulting
+ # in a new tip layer.
+ test_commit_bulk --message="huge" 18 &&
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index &&
+
+ ls $packdir/pack-*.idx | sort >packs.after &&
+ comm -13 packs.before packs.after >packs.new &&
+
+ ! test_cmp $midx_chain.before $midx_chain &&
+ test_line_count = 1 $midx_chain &&
+ test_line_count = 1 packs.new &&
+
+ git multi-pack-index verify
+ )
+'
+
+test_expect_success 'new tip absorbs multiple layers' '
+ git init new-tip-absorbs-multiple-layers &&
+ (
+ cd new-tip-absorbs-multiple-layers &&
+
+ git config maintenance.auto false &&
+ git config repack.midxnewlayerthreshold 1 &&
+ git config repack.midxsplitfactor 2 &&
+
+ # Build a 4-layer chain where each layer is too small to
+ # absorb the one below it. The sizes must satisfy L(n) <
+ # L(n-1)/2 for each adjacent pair:
+ #
+ # L0 (oldest): 75 obj (25 commits)
+ # L1: 21 obj (7 commits, 21 < 75/2)
+ # L2: 9 obj (3 commits, 9 < 21/2)
+ # L3 (tip): 3 obj (1 commit, 3 < 9/2)
+ create_layers <<-\EOF &&
+ L0 25
+ L1 7
+ L2 3
+ L3 1
+ EOF
+
+ test_line_count = 4 "$midx_chain" &&
+ cp $midx_chain $midx_chain.before &&
+
+ # Now add a new commit. The merging condition is
+ # satisfied between L3-L1, but violated at L0, which is
+ # too large relative to the accumulated size.
+ #
+ # As a result, the chain shrinks from 4 to 2 layers.
+ test_commit new &&
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index &&
+
+ ! test_cmp $midx_chain.before $midx_chain &&
+ test_line_count = 2 "$midx_chain" &&
+ git multi-pack-index verify
+ )
+'
+
+test_expect_success 'compaction of older layers' '
+ git init compaction-of-older-layers &&
+ (
+ cd compaction-of-older-layers &&
+
+ git config maintenance.auto false &&
+ git config repack.midxnewlayerthreshold 1 &&
+ git config repack.midxsplitfactor 2 &&
+
+ # Build a chain with two small layers at the bottom
+ # and a larger barrier layer on top, producing a
+ # chain that violates the compaction invariant, since
+ # the two small layers would normally have been merged.
+ create_layers <<-\EOF &&
+ one 2
+ two 4
+ barrier 54
+ EOF
+
+ cp $midx_chain $midx_chain.before &&
+
+ # Running an incremental repack compacts the two
+ # small layers at the bottom of the chain as a
+ # separate step in the compaction plan.
+ test_commit another &&
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index &&
+
+ test_line_count = 2 "$midx_chain" &&
+ git multi-pack-index verify
+ )
+'
+
+test_expect_success 'geometric rollup with surviving tip packs' '
+ git init geometric-rollup-with-surviving-tip-packs &&
+ (
+ cd geometric-rollup-with-surviving-tip-packs &&
+
+ git config maintenance.auto false &&
+ git config repack.midxnewlayerthreshold 1 &&
+ git config repack.midxsplitfactor 2 &&
+
+ # Create a pack large enough to anchor the geometric
+ # progression when small packs are added alongside it.
+ create_layer --message="big" 5 &&
+
+ test_line_count = 1 "$midx_chain" &&
+ cp $midx_chain $midx_chain.before &&
+
+ # Repack a small number of objects such that the
+ # progression is unbothered. Note that the existing pack
+ # is considered a repack candidate as the new layer
+ # threshold is set to 1.
+ test_commit small-1 &&
+ git repack -d &&
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index &&
+
+ ! test_cmp $midx_chain.before $midx_chain &&
+ cp $midx_chain $midx_chain.before
+ )
+'
+
+test_expect_success 'kept packs are excluded from repack' '
+ git init kept-packs-excluded-from-repack &&
+ (
+ cd kept-packs-excluded-from-repack &&
+
+ git config maintenance.auto false &&
+ git config repack.midxnewlayerthreshold 1 &&
+ git config repack.midxsplitfactor 2 &&
+
+ # Create two equal-sized packs, marking one as kept.
+ for i in A B
+ do
+ test_commit "$i" && git repack -d || return 1
+ done &&
+
+ keep=$(ls $packdir/pack-*.idx | head -n 1) &&
+ touch "${keep%.idx}.keep" &&
+
+ # The kept pack is excluded as a repacking candidate
+ # entirely, so no rollup occurs as there is only one
+ # non-kept pack. A new MIDX layer is written containing
+ # that pack.
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index &&
+
+ test-tool read-midx $objdir >actual &&
+ grep "^pack-.*\.idx$" actual >actual.packs &&
+ test_line_count = 1 actual.packs &&
+ test_grep ! "$keep" actual.packs &&
+
+ git multi-pack-index verify &&
+
+ # All objects (from both kept and non-kept packs)
+ # must still be accessible.
+ git fsck
+ )
+'
+
+test_expect_success 'incremental MIDX with --max-pack-size' '
+ git init incremental-midx-with--max-pack-size &&
+ (
+ cd incremental-midx-with--max-pack-size &&
+
+ git config maintenance.auto false &&
+ git config repack.midxnewlayerthreshold 1 &&
+ git config repack.midxsplitfactor 2 &&
+
+ create_layer --message="base" 1 &&
+
+ # Now add enough data that a small --max-pack-size will
+ # cause pack-objects to split its output. Create objects
+ # large enough to fill multiple packs.
+ test-tool genrandom foo 1M >big1 &&
+ test-tool genrandom bar 1M >big2 &&
+ git add big1 big2 &&
+ test_tick &&
+ git commit -a -m "big blobs" &&
+ git repack -d &&
+
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index --max-pack-size=1M &&
+
+ test_line_count = 1 "$midx_chain" &&
+ test-tool read-midx $objdir >actual &&
+ grep "^pack-.*\.idx$" actual >actual.packs &&
+ test_line_count -gt 1 actual.packs &&
+
+ git multi-pack-index verify
+ )
+'
+
+test_expect_success 'noop repack preserves valid MIDX chain' '
+ git init noop-repack-preserves-valid-midx-chain &&
+ (
+ cd noop-repack-preserves-valid-midx-chain &&
+
+ git config maintenance.auto false &&
+ git config repack.midxnewlayerthreshold 1 &&
+ git config repack.midxsplitfactor 2 &&
+
+ create_layer --message="base" 1 &&
+
+ git multi-pack-index verify &&
+ cp $midx_chain $midx_chain.before &&
+
+ # Running again with no new objects should not break
+ # the MIDX chain. It produces "Nothing new to pack."
+ git repack --geometric=2 -d --write-midx=incremental \
+ --write-bitmap-index &&
+
+ test_cmp $midx_chain.before $midx_chain &&
+
+ git multi-pack-index verify &&
+ git fsck
+ )
+'
+
+test_expect_success 'repack -ad removes stale incremental chain' '
+ git init repack--ad-removes-stale-incremental-chain &&
+ (
+ cd repack--ad-removes-stale-incremental-chain &&
+
+ git config maintenance.auto false &&
+ git config repack.midxnewlayerthreshold 1 &&
+ git config repack.midxsplitfactor 2 &&
+
+ create_layers <<-\EOF &&
+ one 1
+ two 1
+ EOF
+
+ test_path_is_file $midx_chain &&
+ test_line_count = 2 $midx_chain &&
+
+ git repack -ad &&
+
+ test_path_is_missing $packdir/multi-pack-index &&
+ test_dir_is_empty $midxdir
+ )
+'
+
+test_expect_success 'repack rejects invalid midxSplitFactor' '
+ test_when_finished "rm -fr bad-split-factor" &&
+ git init bad-split-factor &&
+ (
+ cd bad-split-factor &&
+ test_commit base &&
+
+ for v in 0 1 -1
+ do
+ test_must_fail git -c repack.midxSplitFactor=$v \
+ repack -d --geometric=2 --write-midx=incremental 2>err &&
+ test_grep "invalid value for --midx-split-factor" err ||
+ return 1
+ done
+ )
+'
+
+test_expect_success 'repack rejects invalid midxNewLayerThreshold' '
+ test_when_finished "rm -fr bad-layer-threshold" &&
+ git init bad-layer-threshold &&
+ (
+ cd bad-layer-threshold &&
+ test_commit base &&
+
+ for v in 0 -1
+ do
+ test_must_fail git -c repack.midxNewLayerThreshold=$v \
+ repack -d --geometric=2 --write-midx=incremental 2>err &&
+ test_grep "invalid value for --midx-new-layer-threshold" err ||
+ return 1
+ done
+ )
+'
+
+test_done
--
2.54.0.175.g8bd0ec98dc3
^ permalink raw reply related
* [PATCH v4 16/16] repack: allow `--write-midx=incremental` without `--geometric`
From: Taylor Blau @ 2026-05-19 15:58 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Patrick Steinhardt
In-Reply-To: <cover.1779206239.git.me@ttaylorr.com>
Previously, `--write-midx=incremental` required `--geometric` and would
die() without it. Relax this restriction so that incremental MIDX
repacking can be used independently.
Without `--geometric`, the behavior is append-only: a single new MIDX
layer is created containing whatever packs were written by the repack
and appended to the existing chain (or a new chain is started). Existing
layers are preserved as-is with no compaction or merging.
Implement this via a new repack_make_midx_append_plan() that builds a
plan consisting of a WRITE step for the freshly written packs followed
by COPY steps for every existing MIDX layer. The existing compaction
plan (repack_make_midx_compaction_plan) is used only when `--geometric`
is active.
Update the documentation to describe the behavior with and without
`--geometric`, and replace the test that enforced the old restriction
with one exercising append-only incremental MIDX repacking.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
Documentation/git-repack.adoc | 19 +++++----
builtin/repack.c | 3 --
repack-midx.c | 64 +++++++++++++++++++++++++++--
t/t7705-repack-incremental-midx.sh | 65 +++++++++++++++++++++++++++---
4 files changed, 133 insertions(+), 18 deletions(-)
diff --git a/Documentation/git-repack.adoc b/Documentation/git-repack.adoc
index 27a99cc46f4..72c42015e23 100644
--- a/Documentation/git-repack.adoc
+++ b/Documentation/git-repack.adoc
@@ -263,14 +263,19 @@ linkgit:git-multi-pack-index[1]).
`incremental`;;
Write an incremental MIDX chain instead of a single
- flat MIDX. This mode requires `--geometric`.
+ flat MIDX.
+
-The incremental mode maintains a chain of MIDX layers that is compacted
-over time using a geometric merging strategy. Each repack creates a new
-tip layer containing the newly written pack(s). Adjacent layers are then
-merged whenever the newer layer's object count exceeds
-`1/repack.midxSplitFactor` of the next deeper layer's count. Layers
-that do not meet this condition are retained as-is.
+Without `--geometric`, a new MIDX layer is appended to the existing
+chain (or a new chain is started) containing whatever packs were written
+by the repack. Existing layers are preserved as-is.
++
+When combined with `--geometric`, the incremental mode maintains a chain
+of MIDX layers that is compacted over time using a geometric merging
+strategy. Each repack creates a new tip layer containing the newly
+written pack(s). Adjacent layers are then merged whenever the newer
+layer's object count exceeds `1/repack.midxSplitFactor` of the next
+deeper layer's count. Layers that do not meet this condition are
+retained as-is.
+
The result is that newer (tip) layers tend to contain many small packs
with relatively few objects, while older (deeper) layers contain fewer,
diff --git a/builtin/repack.c b/builtin/repack.c
index 5ffa18e085e..1524a9c13ad 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -263,9 +263,6 @@ int cmd_repack(int argc,
if (pack_everything & PACK_CRUFT)
pack_everything |= ALL_INTO_ONE;
- if (write_midx == REPACK_WRITE_MIDX_INCREMENTAL && !geometry.split_factor)
- die(_("--write-midx=incremental requires --geometric"));
-
if (write_bitmaps < 0) {
if (write_midx == REPACK_WRITE_MIDX_NONE &&
(!(pack_everything & ALL_INTO_ONE) || !is_bare_repository()))
diff --git a/repack-midx.c b/repack-midx.c
index 4f5deeb97bf..b6b1de71805 100644
--- a/repack-midx.c
+++ b/repack-midx.c
@@ -548,6 +548,60 @@ static void midx_compaction_step_release(struct midx_compaction_step *step)
free(step->csum);
}
+/*
+ * Build an append-only MIDX plan: a single WRITE step for the freshly
+ * written packs, plus COPY steps for every existing layer. No
+ * compaction or merging is performed.
+ */
+static void repack_make_midx_append_plan(struct repack_write_midx_opts *opts,
+ struct midx_compaction_step **steps_p,
+ size_t *steps_nr_p)
+{
+ struct multi_pack_index *m;
+ struct midx_compaction_step *steps = NULL;
+ struct midx_compaction_step *step;
+ size_t steps_nr = 0, steps_alloc = 0;
+
+ odb_reprepare(opts->existing->repo->objects);
+ m = get_multi_pack_index(opts->existing->source);
+
+ if (opts->names->nr) {
+ struct strbuf buf = STRBUF_INIT;
+ uint32_t i;
+
+ ALLOC_GROW(steps, st_add(steps_nr, 1), steps_alloc);
+
+ step = &steps[steps_nr++];
+ memset(step, 0, sizeof(*step));
+
+ step->type = MIDX_COMPACTION_STEP_WRITE;
+ string_list_init_dup(&step->u.write);
+
+ for (i = 0; i < opts->names->nr; i++) {
+ strbuf_reset(&buf);
+ strbuf_addf(&buf, "pack-%s.idx",
+ opts->names->items[i].string);
+ string_list_append(&step->u.write, buf.buf);
+ }
+
+ strbuf_release(&buf);
+ }
+
+ for (; m; m = m->base_midx) {
+ ALLOC_GROW(steps, st_add(steps_nr, 1), steps_alloc);
+
+ step = &steps[steps_nr++];
+ memset(step, 0, sizeof(*step));
+
+ step->type = MIDX_COMPACTION_STEP_COPY;
+ step->u.copy = m;
+ step->objects_nr = m->num_objects;
+ }
+
+ *steps_p = steps;
+ *steps_nr_p = steps_nr;
+}
+
static int repack_make_midx_compaction_plan(struct repack_write_midx_opts *opts,
struct midx_compaction_step **steps_p,
size_t *steps_nr_p)
@@ -904,9 +958,13 @@ static int write_midx_incremental(struct repack_write_midx_opts *opts)
goto done;
}
- if (repack_make_midx_compaction_plan(opts, &steps, &steps_nr) < 0) {
- ret = error(_("unable to generate compaction plan"));
- goto done;
+ if (opts->geometry->split_factor) {
+ if (repack_make_midx_compaction_plan(opts, &steps, &steps_nr) < 0) {
+ ret = error(_("unable to generate compaction plan"));
+ goto done;
+ }
+ } else {
+ repack_make_midx_append_plan(opts, &steps, &steps_nr);
}
for (i = 0; i < steps_nr; i++) {
diff --git a/t/t7705-repack-incremental-midx.sh b/t/t7705-repack-incremental-midx.sh
index 9e317ff6e8f..25a8c40e8ee 100755
--- a/t/t7705-repack-incremental-midx.sh
+++ b/t/t7705-repack-incremental-midx.sh
@@ -63,10 +63,36 @@ create_layers () {
done
}
-test_expect_success '--write-midx=incremental requires --geometric' '
- test_must_fail git repack --write-midx=incremental 2>err &&
+test_expect_success '--write-midx=incremental without --geometric' '
+ git init incremental-without-geometric &&
+ (
+ cd incremental-without-geometric &&
- test_grep -- "--write-midx=incremental requires --geometric" err
+ git config maintenance.auto false &&
+
+ test_commit first &&
+ git repack -d &&
+
+ test_commit second &&
+ git repack --write-midx=incremental &&
+
+ git multi-pack-index verify &&
+ test_line_count = 1 $midx_chain &&
+ cp $midx_chain $midx_chain.before &&
+
+ # A second repack appends a new layer without
+ # disturbing the existing one.
+ test_commit third &&
+ git repack --write-midx=incremental &&
+
+ git multi-pack-index verify &&
+ test_line_count = 2 $midx_chain &&
+ head -n 1 $midx_chain.before >expect &&
+ head -n 1 $midx_chain >actual &&
+ test_cmp expect actual &&
+
+ git fsck
+ )
'
test_expect_success 'below layer threshold, tip packs excluded' '
@@ -334,8 +360,7 @@ test_expect_success 'kept packs are excluded from repack' '
# entirely, so no rollup occurs as there is only one
# non-kept pack. A new MIDX layer is written containing
# that pack.
- git repack --geometric=2 -d --write-midx=incremental \
- --write-bitmap-index &&
+ git repack --geometric=2 -d --write-midx=incremental &&
test-tool read-midx $objdir >actual &&
grep "^pack-.*\.idx$" actual >actual.packs &&
@@ -433,6 +458,36 @@ test_expect_success 'repack -ad removes stale incremental chain' '
)
'
+test_expect_success 'repack -ad --write-midx=incremental is safe' '
+ git init ad-incremental-midx &&
+ (
+ cd ad-incremental-midx &&
+
+ git config maintenance.auto false &&
+
+ # Build a MIDX chain with multiple layers referencing
+ # distinct packs.
+ test_commit first &&
+ git repack -d &&
+
+ test_commit second &&
+ git repack -d --write-midx=incremental &&
+
+ git multi-pack-index verify &&
+ test_line_count = 1 $midx_chain &&
+
+ # Now do a full -ad repack. The new pack contains all
+ # objects, but any retained MIDX layers still reference
+ # the now-deleted packs.
+ test_commit third &&
+ git repack -ad --write-midx=incremental &&
+
+ git multi-pack-index verify &&
+ git fsck &&
+ git rev-list --all --objects >/dev/null
+ )
+'
+
test_expect_success 'repack rejects invalid midxSplitFactor' '
test_when_finished "rm -fr bad-split-factor" &&
git init bad-split-factor &&
--
2.54.0.175.g8bd0ec98dc3
^ permalink raw reply related
* [PATCH 0/8] pack-bitmap-write: speed up bitmap generation
From: Taylor Blau @ 2026-05-19 16:12 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Derrick Stolee
Note to the maintainer:
* This series is based on 'tb/pseudo-merge-bugfixes', with
'ps/clang-w-glibc-2.43-and-_Generic' merged in. I suggest queueing it
as 'tb/bitmap-build-performance'.
The latter merge is only to avoid the current Clang/glibc 2.43 CI
breakage, and is unrelated to the bitmap changes themselves.
This series improves the performance of reachability bitmap generation,
focusing on very large repositories and the penalty to generate
pseudo-merge reachability bitmaps.
The first few patches address hot paths in the ordinary bitmap build:
- pass object positions into `fill_bitmap_tree()` so callers can avoid
redundant lookups,
- check subtree bits before recursing, which avoids many no-op
`fill_bitmap_tree()` calls,
- reuse already-stored selected bitmaps when `fill_bitmap_commit()`
reaches a selected ancestor, and
- add a small direct-mapped cache from object IDs to bitmap positions
to avoid repeated pack/MIDX lookups while filling bitmaps.
On the large repository that I have been using to benchmark these
changes (~4.8M commits and ~57M total objects), the no-pseudo-merge
bitmap generation case drops **from ~612.5 seconds to ~294.1 seconds**.
The next patch sorts selected bitmaps before choosing XOR offsets. This
does not change bitmap selection/coverage, but in the same repository it
shrinks the generated bitmap file **from ~635.5 MiB to ~176.4 MiB** by
putting related ancestor/descendant bitmaps close enough together for
the XOR search window to find them.
The final two patches focus on pseudo-merge bitmaps. The existing code
feeds pseudo-merges into the same maximal-commit selection machinery as
ordinary selected commits. That machinery works well for real history,
but not pseudo-merges.
Instead, this series builds ordinary selected bitmaps first, then builds
pseudo-merge bitmaps afterwards. The later pseudo-merge fill can still
reuse stored selected ancestor bitmaps, and can also reuse an existing
on-disk pseudo-merge bitmap when the parent set matches.
With the coarse pseudo-merge configuration used for testing:
[bitmapPseudoMerge "all"]
pattern=refs/
threshold=now
stableSize=10000000
maxMerges=8
, the optimized no-pseudo-merge case takes ~294.1 seconds, while the
**pseudo-merge case takes ~328.4 seconds**. Before the final change, the
same pseudo-merge configuration took ~575.0 seconds.
On our testing repository, it is faster at the end of this series to
generate bitmaps with pseudo-merges (~328 seconds as above) than it is
to generate bitmaps without pseudo-merges at the start of this series
(~612 seconds).
Thanks in advance for your review!
Taylor Blau (8):
pack-bitmap: pass object position to `fill_bitmap_tree()`
pack-bitmap: check subtree bits before recursing
pack-bitmap: reuse stored selected bitmaps
pack-bitmap: consolidate `find_object_pos()` success path
pack-bitmap: cache object positions during fill
pack-bitmap: sort bitmaps before XORing
pack-bitmap: remember pseudo-merge parents
pack-bitmap: build pseudo-merge bitmaps after regular bitmaps
pack-bitmap-write.c | 431 +++++++++++++++++++++++++++++++++++++-------
pack-bitmap.h | 7 +
2 files changed, 377 insertions(+), 61 deletions(-)
base-commit: c3d7ca7d982efc3a848fd85f34e867cfc0a99479
--
2.54.0.rc1.84.g30ce254312c
^ permalink raw reply
* [PATCH 1/8] pack-bitmap: pass object position to `fill_bitmap_tree()`
From: Taylor Blau @ 2026-05-19 16:12 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Derrick Stolee
In-Reply-To: <cover.1779207127.git.me@ttaylorr.com>
In the following commit, callers of `fill_bitmap_tree()` will be
required to check the bit corresponding to their tree before calling
that function. That change will reduce the overhead of setting up and
tearing down stack frames for trees whose bits are already set.
To prepare for that change, have callers pass in the tree's bit position
in `fill_bitmap_tree()`, which will make the next commit easier to read.
In the meantime, this change has a surprising and measurable benefit
during bitmap generation, particularly on very large repositories.
When processing sub-trees within `fill_bitmap_tree()`, the preimage of
this patch did the following:
while (tree_entry(&desc, entry)) {
switch (object_type(entry.mode)) {
case OBJ_TREE:
if (fill_bitmap_tree(writer, bitmap,
lookup_tree(writer->repo,
&entry.oid)) < 0) {
/* ... */
}
/* ... */
}
}
, first performing the object lookup via `lookup_tree()`, and then
locating its bit position within the recursive call. This patch
effectively reorders those two calls so that we first discover the
sub-tree's bit position, *then* load its tree.
By reordering these two operations, we spend fewer CPU cycles per
instruction, likely due to improved CPU dependency/cache/pipeline
behavior. Comparing the results of: running `perf stat` before and after
this commit, we have:
+--------------+-------------+-------------+-------------------+
| | HEAD^ | HEAD | Delta |
+--------------+-------------+-------------+-------------------+
| elapsed | 612.5 s | 582.4 s | -30.1 s (-4.9%) |
| cycles | 2,857.3 B | 2,713.3 B | -144.0 B (-5.0%) |
| instructions | 2,413.2 B | 2,415.5 B | +2.3 B (+0.1%) |
| CPI | 1.184 | 1.123 | -0.061 (-5.1%) |
+--------------+-------------+-------------+-------------------+
In a large repository with ~4.8M commit, and ~37.1M tree objects this
change improves timing from ~612.5 seconds down to ~582.4 seconds, or a
~4.9% improvement. More importantly, the number of CPU cycles spent
dropped off significantly as a result of this commit, lowering our
cycles-per-instruction ratio by about ~5.1%.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
pack-bitmap-write.c | 23 +++++++++++++++--------
1 file changed, 15 insertions(+), 8 deletions(-)
diff --git a/pack-bitmap-write.c b/pack-bitmap-write.c
index 1c8070f99c0..2d5ff8fd406 100644
--- a/pack-bitmap-write.c
+++ b/pack-bitmap-write.c
@@ -456,10 +456,10 @@ static void bitmap_builder_clear(struct bitmap_builder *bb)
static int fill_bitmap_tree(struct bitmap_writer *writer,
struct bitmap *bitmap,
- struct tree *tree)
+ struct tree *tree,
+ uint32_t pos)
{
int found;
- uint32_t pos;
struct tree_desc desc;
struct name_entry entry;
@@ -467,9 +467,6 @@ static int fill_bitmap_tree(struct bitmap_writer *writer,
* If our bit is already set, then there is nothing to do. Both this
* tree and all of its children will be set.
*/
- pos = find_object_pos(writer, &tree->object.oid, &found);
- if (!found)
- return -1;
if (bitmap_get(bitmap, pos))
return 0;
bitmap_set(bitmap, pos);
@@ -482,8 +479,12 @@ static int fill_bitmap_tree(struct bitmap_writer *writer,
while (tree_entry(&desc, &entry)) {
switch (object_type(entry.mode)) {
case OBJ_TREE:
+ pos = find_object_pos(writer, &entry.oid, &found);
+ if (!found)
+ return -1;
if (fill_bitmap_tree(writer, bitmap,
- lookup_tree(writer->repo, &entry.oid)) < 0)
+ lookup_tree(writer->repo,
+ &entry.oid), pos) < 0)
return -1;
break;
case OBJ_BLOB:
@@ -575,8 +576,14 @@ static int fill_bitmap_commit(struct bitmap_writer *writer,
}
while (tree_queue->nr) {
- if (fill_bitmap_tree(writer, ent->bitmap,
- prio_queue_get(tree_queue)) < 0)
+ struct tree *t = prio_queue_get(tree_queue);
+ int found;
+
+ pos = find_object_pos(writer, &t->object.oid, &found);
+ if (!found)
+ return -1;
+
+ if (fill_bitmap_tree(writer, ent->bitmap, t, pos) < 0)
return -1;
}
return 0;
--
2.54.0.rc1.84.g30ce254312c
^ permalink raw reply related
* [PATCH 2/8] pack-bitmap: check subtree bits before recursing
From: Taylor Blau @ 2026-05-19 16:12 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Derrick Stolee
In-Reply-To: <cover.1779207127.git.me@ttaylorr.com>
In the previous commit, we adjusted the callers of `fill_bitmap_tree()`
to pass in the bit position of the tree they wish to fill.
This commit makes use of that information at the call site to avoid
setting up a stack frame for fill_bitmap_tree() entirely whenever a
tree's bit position is already set.
Since this is such a hot path, the avoided cost of setting up and
tearing down stack frames for each noop'd call to `fill_bitmap_tree()`
is significant:
+--------------+-------------+-------------+-------------------+
| | HEAD^ | HEAD | Delta |
+--------------+-------------+-------------+-------------------+
| elapsed | 582.4 s | 562.8 s | -19.6 s (-3.4%) |
| cycles | 2,713.3 B | 2,621.3 B | -92.0 B (-3.4%) |
| instructions | 2,415.5 B | 2,348.9 B | -66.6 B (-2.8%) |
| CPI | 1.123 | 1.116 | -0.007 (-0.7%) |
+--------------+-------------+-------------+-------------------+
In the same repository as in the previous commit, our timings dropped
from ~582.4 seconds down to ~562.77 seconds.
While the cycles-per-instruction ratio is basically unchanged, we
execute significantly fewer instructions, and correspondingly fewer
cycles.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
pack-bitmap-write.c | 23 +++++++++++++++++------
1 file changed, 17 insertions(+), 6 deletions(-)
diff --git a/pack-bitmap-write.c b/pack-bitmap-write.c
index 2d5ff8fd406..72610397020 100644
--- a/pack-bitmap-write.c
+++ b/pack-bitmap-write.c
@@ -463,12 +463,6 @@ static int fill_bitmap_tree(struct bitmap_writer *writer,
struct tree_desc desc;
struct name_entry entry;
- /*
- * If our bit is already set, then there is nothing to do. Both this
- * tree and all of its children will be set.
- */
- if (bitmap_get(bitmap, pos))
- return 0;
bitmap_set(bitmap, pos);
if (repo_parse_tree(writer->repo, tree) < 0)
@@ -482,6 +476,15 @@ static int fill_bitmap_tree(struct bitmap_writer *writer,
pos = find_object_pos(writer, &entry.oid, &found);
if (!found)
return -1;
+ if (bitmap_get(bitmap, pos)) {
+ /*
+ * If our bit is already set, then there
+ * is nothing to do. Both this tree and
+ * all of its children will be set.
+ */
+ break;
+ }
+
if (fill_bitmap_tree(writer, bitmap,
lookup_tree(writer->repo,
&entry.oid), pos) < 0)
@@ -582,6 +585,14 @@ static int fill_bitmap_commit(struct bitmap_writer *writer,
pos = find_object_pos(writer, &t->object.oid, &found);
if (!found)
return -1;
+ if (bitmap_get(ent->bitmap, pos)) {
+ /*
+ * If our bit is already set, then there is
+ * nothing to do. Both this tree and all of its
+ * children will be set.
+ */
+ continue;
+ }
if (fill_bitmap_tree(writer, ent->bitmap, t, pos) < 0)
return -1;
--
2.54.0.rc1.84.g30ce254312c
^ permalink raw reply related
* [PATCH 3/8] pack-bitmap: reuse stored selected bitmaps
From: Taylor Blau @ 2026-05-19 16:12 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Derrick Stolee
In-Reply-To: <cover.1779207127.git.me@ttaylorr.com>
When `fill_bitmap_commit()` reaches an ancestor that was selected for
its own bitmap and processed earlier, its object closure is already
stored in `writer->bitmaps` as an EWAH bitmap. As a result, walking
through that commit's tree and parents again is redundant.
Teach `fill_bitmap_commit()` to notice that case. For non-root commits in
the walk, look for a stored selected bitmap and OR it into the bitmap
being built. If one exists, skip the commit, its tree, and its parents.
Building bitmaps from scratch on the same test repository from the
previous commits yields a significant speed-up:
+------------------+-------------+-------------+---------------------+
| | HEAD^ | HEAD | Delta |
+------------------+-------------+-------------+---------------------+
| elapsed | 562.8 s | 324.8 s | -237.9 s (-42.3%) |
| cycles | 2,621.3 B | 1,508.6 B | -1,112.7 B (-42.4%) |
| instructions | 2,348.9 B | 1,436.6 B | -912.3 B (-38.8%) |
| CPI | 1.116 | 1.050 | -0.066 (-5.9%) |
+------------------+-------------+-------------+---------------------+
In our testing repository, there are 1,261 commits selected for bitmap
coverage, and 1,382 maximal commits induced as a result of that. Of the
1,382 calls made to `fill_bitmap_commit()` (one per maximal commit), 131
of them can be short-circuited at some point during their traversal as a
consequence of this change.
In large repositories where the cost of filling the bitmap for any
individual commit is large, being able to short-circuit even ~9.5% of
the calls to `fill_bitmap_commit()` results in a significant savings.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
pack-bitmap-write.c | 34 ++++++++++++++++++++++++++++++++++
1 file changed, 34 insertions(+)
diff --git a/pack-bitmap-write.c b/pack-bitmap-write.c
index 72610397020..651ad467469 100644
--- a/pack-bitmap-write.c
+++ b/pack-bitmap-write.c
@@ -509,6 +509,9 @@ static int fill_bitmap_tree(struct bitmap_writer *writer,
static int reused_bitmaps_nr;
static int reused_pseudo_merge_bitmaps_nr;
+static int fill_bitmap_commit_calls_nr;
+static int fill_bitmap_commit_found_ancestor_nr;
+
static int fill_bitmap_commit(struct bitmap_writer *writer,
struct bb_commit *ent,
struct commit *commit,
@@ -519,6 +522,9 @@ static int fill_bitmap_commit(struct bitmap_writer *writer,
{
int found;
uint32_t pos;
+
+ fill_bitmap_commit_calls_nr++;
+
if (!ent->bitmap)
ent->bitmap = bitmap_new();
@@ -553,6 +559,28 @@ static int fill_bitmap_commit(struct bitmap_writer *writer,
bitmap_free(remapped);
}
+ /*
+ * If we encounter an ancestor for which we have already
+ * computed a bitmap during this build (i.e. a regular
+ * selected commit processed earlier in topo order), we can
+ * short-circuit the walk: its stored bitmap already covers
+ * the commit itself, its tree, and all of its ancestors.
+ */
+ if (c != commit) {
+ khiter_t hash_pos = kh_get_oid_map(writer->bitmaps,
+ c->object.oid);
+ if (hash_pos != kh_end(writer->bitmaps)) {
+ struct bitmapped_commit *stored =
+ kh_value(writer->bitmaps, hash_pos);
+ if (stored && stored->bitmap) {
+ fill_bitmap_commit_found_ancestor_nr++;
+ bitmap_or_ewah(ent->bitmap,
+ stored->bitmap);
+ continue;
+ }
+ }
+ }
+
/*
* Mark ourselves and queue our tree. The commit
* walk ensures we cover all parents.
@@ -692,6 +720,12 @@ int bitmap_writer_build(struct bitmap_writer *writer)
trace2_data_intmax("pack-bitmap-write", writer->repo,
"building_bitmaps_pseudo_merge_reused",
reused_pseudo_merge_bitmaps_nr);
+ trace2_data_intmax("pack-bitmap-write", writer->repo,
+ "fill_bitmap_commit_calls_nr",
+ fill_bitmap_commit_calls_nr);
+ trace2_data_intmax("pack-bitmap-write", writer->repo,
+ "fill_bitmap_commit_found_ancestor_nr",
+ fill_bitmap_commit_found_ancestor_nr);
stop_progress(&writer->progress);
--
2.54.0.rc1.84.g30ce254312c
^ permalink raw reply related
* [PATCH 4/8] pack-bitmap: consolidate `find_object_pos()` success path
From: Taylor Blau @ 2026-05-19 16:12 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Derrick Stolee
In-Reply-To: <cover.1779207127.git.me@ttaylorr.com>
Both sides of `find_object_pos()` report success in the same way by
setting the optional `found` out-parameter and return the resolved
bitmap position.
Prepare for adding more bookkeeping around object-position lookups by
storing the result in a local `pos` variable and sharing the success
return path between the packlist and MIDX cases.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
pack-bitmap-write.c | 17 ++++++++---------
1 file changed, 8 insertions(+), 9 deletions(-)
diff --git a/pack-bitmap-write.c b/pack-bitmap-write.c
index 651ad467469..6483fdc7daf 100644
--- a/pack-bitmap-write.c
+++ b/pack-bitmap-write.c
@@ -224,23 +224,22 @@ static uint32_t find_object_pos(struct bitmap_writer *writer,
if (writer->midx)
base_objects = writer->midx->num_objects +
writer->midx->num_objects_in_base;
-
- if (found)
- *found = 1;
- return oe_in_pack_pos(writer->to_pack, entry) + base_objects;
+ pos = oe_in_pack_pos(writer->to_pack, entry) + base_objects;
} else if (writer->midx) {
- uint32_t at, pos;
+ uint32_t at;
if (!bsearch_midx(oid, writer->midx, &at))
goto missing;
if (midx_to_pack_pos(writer->midx, at, &pos) < 0)
goto missing;
-
- if (found)
- *found = 1;
- return pos;
+ } else {
+ goto missing;
}
+ if (found)
+ *found = 1;
+ return pos;
+
missing:
if (found)
*found = 0;
--
2.54.0.rc1.84.g30ce254312c
^ permalink raw reply related
* [PATCH 5/8] pack-bitmap: cache object positions during fill
From: Taylor Blau @ 2026-05-19 16:12 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Derrick Stolee
In-Reply-To: <cover.1779207127.git.me@ttaylorr.com>
The previous commits removed some redundant work from bitmap generation
by avoiding unnecessary tree recursion and by reusing selected bitmaps
that have already been computed.
Even with those changes in place, there is still an extremely hot path
from `fill_bitmap_commit()` and `fill_bitmap_tree()` to translate object
IDs into their corresponding bit positions in order to generate their
bitmaps.
In a small repository, this overhead is not significant. However, in a
very large repository (e.g., the one that we have been using as a
benchmark over the past several commits with ~57M total objects), the
overhead of locating object bit positions (often repeatedly) adds up
significantly.
Combat this by adding a small, direct-mapped cache to the bitmap writer
which maps object IDs to their corresponding bit positions. Size the
cache according to the number of objects being written, with fixed lower
and upper bounds so small repositories do not pay for a large table and
large repositories can avoid most repeated packlist and MIDX lookups.
On my machine with (a somewhat outdated) GCC 15.2.0, each entry in the
cache is 40 bytes wide:
$ pahole -C bitmap_pos_cache_entry pack-bitmap-write.o
struct bitmap_pos_cache_entry {
struct object_id oid; /* 0 36 */
uint32_t pos; /* 36 4 */
/* size: 40, cachelines: 1, members: 2 */
/* last cacheline: 40 bytes */
};
, and we will allocate up to 2^21 entries for a maximum total of 80 MiB
of cache overhead.
In our example repository from above and in earlier commits, this
results in a ~9.4% reduction in runtime relative to the previous commit:
+------------------+-------------+-------------+---------------------+
| | HEAD^ | HEAD | Delta |
+------------------+-------------+-------------+---------------------+
| elapsed | 324.8 s | 294.1 s | -30.7 s (-9.4%) |
| cycles | 1,508.6 B | 1,365.5 B | -143.0 B (-9.5%) |
| instructions | 1,436.6 B | 1,389.8 B | -46.9 B (-3.3%) |
| CPI | 1.050 | 0.983 | -0.068 (-6.4%) |
+------------------+-------------+-------------+---------------------+
When generating bitmaps on this repository (to produce the above
timings), the cache grew to its maximum size of 80 MiB, and resulted in
1.024B cache hits and 59.957M cache misses.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
pack-bitmap-write.c | 89 ++++++++++++++++++++++++++++++++++++++++++++-
pack-bitmap.h | 7 ++++
2 files changed, 95 insertions(+), 1 deletion(-)
diff --git a/pack-bitmap-write.c b/pack-bitmap-write.c
index 6483fdc7daf..4b6fb07edd7 100644
--- a/pack-bitmap-write.c
+++ b/pack-bitmap-write.c
@@ -89,6 +89,7 @@ void bitmap_writer_free(struct bitmap_writer *writer)
ewah_free(writer->tags);
kh_destroy_oid_map(writer->bitmaps);
+ free(writer->pos_cache);
kh_foreach_value(writer->pseudo_merge_commits, idx,
free_pseudo_merge_commit_idx(idx));
@@ -213,14 +214,92 @@ void bitmap_writer_push_commit(struct bitmap_writer *writer,
writer->selected_nr++;
}
+struct bitmap_pos_cache_entry {
+ struct object_id oid;
+ uint32_t pos;
+};
+
+#define BITMAP_POS_MIN_CACHE_SIZE (1U << 10)
+#define BITMAP_POS_MAX_CACHE_SIZE (1U << 21)
+#define BITMAP_POS_CACHE_VALID (1U << 31)
+
+static void bitmap_writer_init_pos_cache(struct bitmap_writer *writer)
+{
+ if (writer->pos_cache)
+ return;
+
+ writer->pos_cache_nr = BITMAP_POS_MIN_CACHE_SIZE;
+
+ while (writer->pos_cache_nr < writer->to_pack->nr_objects &&
+ writer->pos_cache_nr < BITMAP_POS_MAX_CACHE_SIZE)
+ writer->pos_cache_nr <<= 1;
+
+ CALLOC_ARRAY(writer->pos_cache, writer->pos_cache_nr);
+}
+
+static size_t bitmap_writer_pos_cache_slot(struct bitmap_writer *writer,
+ const struct object_id *oid)
+{
+ return oidhash(oid) & (writer->pos_cache_nr - 1);
+}
+
+static bool bitmap_writer_pos_cache_valid(struct bitmap_writer *writer,
+ size_t slot)
+{
+ return !!(writer->pos_cache[slot].pos & BITMAP_POS_CACHE_VALID);
+}
+
+static int find_cached_object_pos(struct bitmap_writer *writer,
+ const struct object_id *oid, uint32_t *pos)
+{
+ size_t slot = bitmap_writer_pos_cache_slot(writer, oid);
+
+ if (bitmap_writer_pos_cache_valid(writer, slot) &&
+ oideq(&writer->pos_cache[slot].oid, oid)) {
+ writer->pos_cache_hits++;
+ *pos = writer->pos_cache[slot].pos & ~BITMAP_POS_CACHE_VALID;
+ return 1;
+ }
+
+ writer->pos_cache_misses++;
+ return 0;
+}
+
+static uint32_t store_cached_object_pos(struct bitmap_writer *writer,
+ const struct object_id *oid,
+ uint32_t pos)
+{
+ size_t slot;
+
+ if (pos & BITMAP_POS_CACHE_VALID)
+ return pos; /* too large to cache */
+
+ slot = bitmap_writer_pos_cache_slot(writer, oid);
+
+ oidcpy(&writer->pos_cache[slot].oid, oid);
+ writer->pos_cache[slot].pos = pos | BITMAP_POS_CACHE_VALID;
+
+ return pos;
+}
+
static uint32_t find_object_pos(struct bitmap_writer *writer,
const struct object_id *oid, int *found)
{
struct object_entry *entry;
+ uint32_t pos;
+
+ bitmap_writer_init_pos_cache(writer);
+
+ if (find_cached_object_pos(writer, oid, &pos)) {
+ if (found)
+ *found = 1;
+ return pos;
+ }
entry = packlist_find(writer->to_pack, oid);
if (entry) {
uint32_t base_objects = 0;
+
if (writer->midx)
base_objects = writer->midx->num_objects +
writer->midx->num_objects_in_base;
@@ -238,7 +317,7 @@ static uint32_t find_object_pos(struct bitmap_writer *writer,
if (found)
*found = 1;
- return pos;
+ return store_cached_object_pos(writer, oid, pos);
missing:
if (found)
@@ -661,6 +740,10 @@ int bitmap_writer_build(struct bitmap_writer *writer)
writer->progress = start_progress(writer->repo,
"Building bitmaps",
writer->selected_nr);
+
+ writer->pos_cache_hits = 0;
+ writer->pos_cache_misses = 0;
+
trace2_region_enter("pack-bitmap-write", "building_bitmaps_total",
writer->repo);
@@ -725,6 +808,10 @@ int bitmap_writer_build(struct bitmap_writer *writer)
trace2_data_intmax("pack-bitmap-write", writer->repo,
"fill_bitmap_commit_found_ancestor_nr",
fill_bitmap_commit_found_ancestor_nr);
+ trace2_data_intmax("pack-bitmap-write", writer->repo,
+ "bitmap_pos_cache_hits", writer->pos_cache_hits);
+ trace2_data_intmax("pack-bitmap-write", writer->repo,
+ "bitmap_pos_cache_misses", writer->pos_cache_misses);
stop_progress(&writer->progress);
diff --git a/pack-bitmap.h b/pack-bitmap.h
index a95e1c2d115..19a86554579 100644
--- a/pack-bitmap.h
+++ b/pack-bitmap.h
@@ -132,6 +132,8 @@ int bitmap_has_oid_in_uninteresting(struct bitmap_index *, const struct object_i
off_t get_disk_usage_from_bitmap(struct bitmap_index *, struct rev_info *);
+struct bitmap_pos_cache_entry;
+
struct bitmap_writer {
struct repository *repo;
struct ewah_bitmap *commits;
@@ -143,6 +145,11 @@ struct bitmap_writer {
struct packing_data *to_pack;
struct multi_pack_index *midx; /* if appending to a MIDX chain */
+ struct bitmap_pos_cache_entry *pos_cache;
+ size_t pos_cache_nr;
+ uint64_t pos_cache_hits;
+ uint64_t pos_cache_misses;
+
struct bitmapped_commit *selected;
unsigned int selected_nr, selected_alloc;
--
2.54.0.rc1.84.g30ce254312c
^ permalink raw reply related
* [PATCH 6/8] pack-bitmap: sort bitmaps before XORing
From: Taylor Blau @ 2026-05-19 16:12 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Derrick Stolee
In-Reply-To: <cover.1779207127.git.me@ttaylorr.com>
Reachability bitmaps may be stored as XORs against nearby bitmaps, up to
10 away. However, when callers provide selected commits in an arbitrary
order, the writer may miss good ancestor/descendant pairs and produce
much larger bitmap files without changing query coverage.
Sort the selected bitmaps in date order (from oldest to newest) before
computing XOR offsets, leaving pseudo-merge bitmaps alone (which we will
deal with separately in following commits).
On our same testing repository from previous commits, this change shrunk
our selection of 1,261 bitmaps from ~635.46 MiB to 176.4 MiB for a
~72.24% reduction in the on-disk size of our *.bitmap file. The time to
generate the smaller bitmap file decreased by ~3.69 seconds, though this
is likely mostly noise.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
pack-bitmap-write.c | 29 +++++++++++++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/pack-bitmap-write.c b/pack-bitmap-write.c
index 4b6fb07edd7..66282ea14b5 100644
--- a/pack-bitmap-write.c
+++ b/pack-bitmap-write.c
@@ -327,11 +327,40 @@ static uint32_t find_object_pos(struct bitmap_writer *writer,
return 0;
}
+static int bitmapped_commit_date_cmp(const void *_a, const void *_b)
+{
+ const struct bitmapped_commit *a = _a;
+ const struct bitmapped_commit *b = _b;
+
+ if (a->commit->date < b->commit->date)
+ return -1;
+ if (a->commit->date > b->commit->date)
+ return 1;
+ return 0;
+}
+
static void compute_xor_offsets(struct bitmap_writer *writer)
{
static const int MAX_XOR_OFFSET_SEARCH = 10;
int i, next = 0;
+ int nr = bitmap_writer_nr_selected_commits(writer);
+
+ if (nr > 1) {
+ QSORT(writer->selected, nr, bitmapped_commit_date_cmp);
+
+ for (i = 0; i < nr; i++) {
+ struct bitmapped_commit *stored = &writer->selected[i];
+ khiter_t hash_pos = kh_get_oid_map(writer->bitmaps,
+ stored->commit->object.oid);
+
+ if (hash_pos == kh_end(writer->bitmaps))
+ BUG("selected commit missing from bitmap map: %s",
+ oid_to_hex(&stored->commit->object.oid));
+
+ kh_value(writer->bitmaps, hash_pos) = stored;
+ }
+ }
while (next < writer->selected_nr) {
struct bitmapped_commit *stored = &writer->selected[next];
--
2.54.0.rc1.84.g30ce254312c
^ permalink raw reply related
* [PATCH 7/8] pack-bitmap: remember pseudo-merge parents
From: Taylor Blau @ 2026-05-19 16:12 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Derrick Stolee
In-Reply-To: <cover.1779207127.git.me@ttaylorr.com>
write_pseudo_merges() currently builds an array of temporary bitmaps for
the parent set of each pseudo-merge, then serializes those bitmaps later
while writing the extension.
Move those parent bitmaps onto the corresponding bitmapped_commit
entries instead. This keeps the on-disk output unchanged, but gives the
parent bitmap the same lifetime and access pattern that later changes
will use when pseudo-merge object bitmaps are built before the write
step.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
pack-bitmap-write.c | 30 +++++++++++++++++-------------
1 file changed, 17 insertions(+), 13 deletions(-)
diff --git a/pack-bitmap-write.c b/pack-bitmap-write.c
index 66282ea14b5..8200aed6101 100644
--- a/pack-bitmap-write.c
+++ b/pack-bitmap-write.c
@@ -32,6 +32,7 @@ struct bitmapped_commit {
struct commit *commit;
struct ewah_bitmap *bitmap;
struct ewah_bitmap *write_as;
+ struct ewah_bitmap *pseudo_merge_parents;
int flags;
int xor_offset;
uint32_t commit_pos;
@@ -102,6 +103,7 @@ void bitmap_writer_free(struct bitmap_writer *writer)
if (bc->write_as != bc->bitmap)
ewah_free(bc->write_as);
ewah_free(bc->bitmap);
+ ewah_free(bc->pseudo_merge_parents);
}
free(writer->selected);
}
@@ -210,6 +212,7 @@ void bitmap_writer_push_commit(struct bitmap_writer *writer,
writer->selected[writer->selected_nr].write_as = NULL;
writer->selected[writer->selected_nr].flags = 0;
writer->selected[writer->selected_nr].pseudo_merge = pseudo_merge;
+ writer->selected[writer->selected_nr].pseudo_merge_parents = NULL;
writer->selected_nr++;
}
@@ -1004,42 +1007,47 @@ static void write_pseudo_merges(struct bitmap_writer *writer,
struct hashfile *f)
{
struct oid_array commits = OID_ARRAY_INIT;
- struct bitmap **commits_bitmap = NULL;
off_t *pseudo_merge_ofs = NULL;
off_t start, table_start, next_ext;
uint32_t base = bitmap_writer_nr_selected_commits(writer);
size_t i, j = 0;
- CALLOC_ARRAY(commits_bitmap, writer->pseudo_merges_nr);
CALLOC_ARRAY(pseudo_merge_ofs, writer->pseudo_merges_nr);
for (i = 0; i < writer->pseudo_merges_nr; i++) {
struct bitmapped_commit *merge = &writer->selected[base + i];
struct commit_list *p;
+ struct bitmap *parents = bitmap_new();
if (!merge->pseudo_merge)
BUG("found non-pseudo merge commit at %"PRIuMAX, (uintmax_t)i);
- commits_bitmap[i] = bitmap_new();
-
for (p = merge->commit->parents; p; p = p->next)
- bitmap_set(commits_bitmap[i],
+ bitmap_set(parents,
find_object_pos(writer, &p->item->object.oid,
NULL));
+
+ merge->pseudo_merge_parents = bitmap_to_ewah(parents);
+ bitmap_free(parents);
}
start = hashfile_total(f);
for (i = 0; i < writer->pseudo_merges_nr; i++) {
- struct ewah_bitmap *commits_ewah = bitmap_to_ewah(commits_bitmap[i]);
+ struct bitmapped_commit *merge = &writer->selected[base + i];
+
+ if (!merge->pseudo_merge)
+ BUG("found non-pseudo merge commit at %"PRIuMAX, (uintmax_t)i);
+
+ if (!merge->pseudo_merge_parents)
+ BUG("missing pseudo-merge parents bitmap for commit %s",
+ oid_to_hex(&merge->commit->object.oid));
pseudo_merge_ofs[i] = hashfile_total(f);
- dump_bitmap(f, commits_ewah);
+ dump_bitmap(f, merge->pseudo_merge_parents);
dump_bitmap(f, writer->selected[base+i].write_as);
-
- ewah_free(commits_ewah);
}
next_ext = st_add(hashfile_total(f),
@@ -1122,12 +1130,8 @@ static void write_pseudo_merges(struct bitmap_writer *writer,
hashwrite_be64(f, table_start - start);
hashwrite_be64(f, hashfile_total(f) - start + sizeof(uint64_t));
- for (i = 0; i < writer->pseudo_merges_nr; i++)
- bitmap_free(commits_bitmap[i]);
-
oid_array_clear(&commits);
free(pseudo_merge_ofs);
- free(commits_bitmap);
}
static int table_cmp(const void *_va, const void *_vb, void *_data)
--
2.54.0.rc1.84.g30ce254312c
^ permalink raw reply related
* [PATCH 8/8] pack-bitmap: build pseudo-merge bitmaps after regular bitmaps
From: Taylor Blau @ 2026-05-19 16:12 UTC (permalink / raw)
To: git; +Cc: Junio C Hamano, Jeff King, Elijah Newren, Derrick Stolee
In-Reply-To: <cover.1779207127.git.me@ttaylorr.com>
When generating bitmaps, `bitmap_builder_init()` starts with an initial
selection of commits to receive bitmap coverage, and then determines a
set of "maximal" commits based on its input.
Commit 089f751360f (pack-bitmap-write: build fewer intermediate bitmaps,
2020-12-08) has extensive details, but the gist is as follows:
Each selected commit starts with one commit_mask bit in its "commit
mask" bitmap. Then, we walk the first-parent history in topological
order and OR each commit's mask into its (first) parent. Whenever that
OR results in the parent having more bits set, the child is deemed to be
non-maximal, and the frontier is pushed further back along the first
parent history.
That approach works extremely well for ordinary selected commits, whose
first-parent histories often describe real sharing between the bitmaps
we are going to write.
It struggles, however, to efficiently generate pseudo-merge bitmaps.
Unlike ordinary commits for which the above algorithm is designed,
pseudo-merges don't represent any "real" commit in history, just a
grouping of non-bitmapped reference tips. In that sense, their first
parent is just a part of a larger set, and treating them like ordinary
selected commits imposes a significant slow-down when generating bitmaps
with pseudo-merges enabled.
Consider partitioning all non-bitmapped reference tips into eight
individual pseudo-merges via the following configuration:
[bitmapPseudoMerge "all"]
pattern=refs/
threshold=now
stableSize=10000000
maxMerges=8
, the cost of generating a bitmap from scratch rises significantly:
+------------------+-----------------+---------------+---------------------+
| | no pseudo-merge | pseudo-merges | Delta |
| | | (HEAD^) | |
+------------------+-----------------+---------------+---------------------+
| elapsed | 294.1 s | 575.0 s | +280.9 s (+95.5%) |
| cycles | 1,365.5 B | 2,686.9 B | +1,321.4 B (+96.8%) |
| instructions | 1,389.8 B | 2,546.6 B | +1,156.8 B (+83.2%) |
| CPI | 0.983 | 1.055 | +0.073 (+7.4%) |
+------------------+-----------------+---------------+---------------------+
This is a particularly poor trade-off, because the time saved by these
pseudo-merges during, e.g.,
$ git rev-list --count --all --objects --use-bitmap-index
is only:
$ hyperfine -L v true,false -n 'pseudo-merges: {v}' '
GIT_TEST_USE_PSEUDO_MERGES={v} git.compile rev-list --count \
--objects --all --use-bitmap-index
'
Benchmark 1: pseudo-merges: true
Time (mean ± σ): 2.613 s ± 0.012 s [User: 2.308 s, System: 0.305 s]
Range (min … max): 2.594 s … 2.633 s 10 runs
Benchmark 2: pseudo-merges: false
Time (mean ± σ): 52.205 s ± 0.170 s [User: 51.500 s, System: 0.697 s]
Range (min … max): 51.956 s … 52.458 s 10 runs
Summary
pseudo-merges: true ran
19.98 ± 0.11 times faster than pseudo-merges: false
In other words, we pay a nearly ~5 minute penalty to generate
pseudo-merge bitmaps, but only save ~50 seconds during traversal.
The problem stems from injecting pseudo-merges into the bitmap builder
as if they were normal commits. The maximal commit selection algorithm
was simply not designed for that case, and performs predictably poorly.
The only reason we reused the maximal commit selection routine for
pseudo-merges alongside regular non-pseudo-merge commits is because we
represent them both as commit objects (where the pseudo-merge commits
just represent a made-up commit as opposed to one that actually exists
in a repository's object store).
Instead, build the regular selected commit bitmaps first, considering
only non-pseudo-merge commits in `bitmap_builder_init()`. Once those
bitmaps have been stored, build each pseudo-merge bitmap separately and
attach its parent and object bitmaps to the corresponding pseudo-merge
entry before writing the extension.
This keeps the regular bitmap build shaped like the no-pseudo-merge
case. The later pseudo-merge fill can still stop at stored selected
ancestor bitmaps, so it does not have to rewalk each pseudo-merge
closure from scratch.
When an existing bitmap has the same pseudo-merge parent set, reuse and
remap that whole pseudo-merge bitmap before falling back to
fill_bitmap_commit(). This preserves the benefit of stable pseudo-merges
while keeping the on-disk format and reader behavior unchanged.
As a result, the overhead cost for generating pseudo-merges in the above
configuration is much smaller:
+------------------+-----------------+---------------+-------------------+
| | no pseudo-merge | pseudo-merges | Delta |
| | | (HEAD) | |
+------------------+-----------------+---------------+-------------------+
| elapsed | 294.1 s | 328.4 s | +34.3 s (+11.7%) |
| cycles | 1,365.5 B | 1,529.3 B | +163.7 B (+12.0%) |
| instructions | 1,389.8 B | 1,552.8 B | +163.0 B (+11.7%) |
| CPI | 0.983 | 0.985 | +0.002 (+0.2%) |
+------------------+-----------------+---------------+-------------------+
Recall that at the start of this series, generating reachability bitmaps
took 612.5 seconds *without* pseudo-merges. With this commit, it is
still ~46.38% *faster* to generate reachability bitmaps *with*
pseudo-merges than it was to generate bitmaps wihtout them at the
beginning of this series.
The changes to implement this are mostly straightforward. We exclude
pseudo-merge commits from the existing bitmap generation, and walk over
them in a separate pass, by either reusing an existing on-disk
pseudo-merge, or passing the pseudo-merge commit itself back to the
existing routine in `fill_bitmap_commit()`.
(Note that the routine to build pseudo-merge bitmaps is the same both
before and after this change, the difference is only that we do not let
psuedo-merges participate in determining the set of maximal commits.)
The only wrinkle is that `fill_bitmap_commit()` must be taught to not
expect that all tree objects have been parsed, which is the case for any
portion of history reachable by one or more pseudo-merge(s), but not by
any non-pseudo-merge commit selected for bitmapping.
Now that we have decoupled how we generate pseudo-merges from their
representation, the following commits will improve the API around
specifying pseudo-merge groupings during bitmap generation.
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
pack-bitmap-write.c | 210 ++++++++++++++++++++++++++++++++++++--------
1 file changed, 174 insertions(+), 36 deletions(-)
diff --git a/pack-bitmap-write.c b/pack-bitmap-write.c
index 8200aed6101..1bcb3f98a42 100644
--- a/pack-bitmap-write.c
+++ b/pack-bitmap-write.c
@@ -446,13 +446,17 @@ static void bitmap_builder_init(struct bitmap_builder *bb,
revs.topo_order = 1;
revs.first_parent_only = 1;
- for (i = 0; i < writer->selected_nr; i++) {
+ for (i = 0; i < bitmap_writer_nr_selected_commits(writer); i++) {
struct bitmapped_commit *bc = &writer->selected[i];
struct bb_commit *ent = bb_data_at(&bb->data, bc->commit);
+ if (bc->pseudo_merge)
+ BUG("unexpected pseudo-merge at %"PRIuMAX,
+ (uintmax_t)i);
+
ent->selected = 1;
ent->maximal = 1;
- ent->pseudo_merge = bc->pseudo_merge;
+ ent->pseudo_merge = 0;
ent->idx = i;
ent->commit_mask = bitmap_new();
@@ -618,6 +622,8 @@ static int fill_bitmap_tree(struct bitmap_writer *writer,
static int reused_bitmaps_nr;
static int reused_pseudo_merge_bitmaps_nr;
+static int pseudo_merge_bitmap_nr;
+static int pseudo_merge_bitmap_parents;
static int fill_bitmap_commit_calls_nr;
static int fill_bitmap_commit_found_ancestor_nr;
@@ -631,8 +637,12 @@ static int fill_bitmap_commit(struct bitmap_writer *writer,
const uint32_t *mapping)
{
int found;
+ int from_pseudo_merge = commit->object.flags & BITMAP_PSEUDO_MERGE;
uint32_t pos;
+ if (ent->pseudo_merge)
+ BUG("unexpected pseudo-merge commit in fill_bitmap_commit()");
+
fill_bitmap_commit_calls_nr++;
if (!ent->bitmap)
@@ -648,10 +658,7 @@ static int fill_bitmap_commit(struct bitmap_writer *writer,
struct ewah_bitmap *old;
struct bitmap *remapped = bitmap_new();
- if (commit->object.flags & BITMAP_PSEUDO_MERGE)
- old = pseudo_merge_bitmap_for_commit(old_bitmap, c);
- else
- old = bitmap_for_commit(old_bitmap, c);
+ old = bitmap_for_commit(old_bitmap, c);
/*
* If this commit has an old bitmap, then translate that
* bitmap and add its bits to this one. No need to walk
@@ -660,10 +667,7 @@ static int fill_bitmap_commit(struct bitmap_writer *writer,
if (old && !rebuild_bitmap(mapping, old, remapped)) {
bitmap_or(ent->bitmap, remapped);
bitmap_free(remapped);
- if (commit->object.flags & BITMAP_PSEUDO_MERGE)
- reused_pseudo_merge_bitmaps_nr++;
- else
- reused_bitmaps_nr++;
+ reused_bitmaps_nr++;
continue;
}
bitmap_free(remapped);
@@ -696,12 +700,32 @@ static int fill_bitmap_commit(struct bitmap_writer *writer,
* walk ensures we cover all parents.
*/
if (!(c->object.flags & BITMAP_PSEUDO_MERGE)) {
+ struct tree *tree;
+
+ if (from_pseudo_merge && !c->object.parsed) {
+ /*
+ * Commits reachable from selected
+ * non-pseudo-merges are already parsed
+ * by the regular bitmap build.
+ *
+ * However, pseudo-merge fills can also
+ * reach commits that were not covered
+ * there, so parse any such leftovers
+ * before reading their tree or parents.
+ */
+ if (repo_parse_commit(writer->repo, c))
+ return -1;
+ }
+
pos = find_object_pos(writer, &c->object.oid, &found);
if (!found)
return -1;
bitmap_set(ent->bitmap, pos);
- prio_queue_put(tree_queue,
- repo_get_commit_tree(writer->repo, c));
+
+ tree = repo_get_commit_tree(writer->repo, c);
+ if (!tree)
+ return -1;
+ prio_queue_put(tree_queue, tree);
}
for (p = c->parents; p; p = p->next) {
@@ -738,6 +762,137 @@ static int fill_bitmap_commit(struct bitmap_writer *writer,
return 0;
}
+static int reuse_pseudo_merge_bitmap(struct bitmap_index *old_bitmap,
+ const uint32_t *mapping,
+ struct commit *merge,
+ struct ewah_bitmap **out)
+{
+ struct ewah_bitmap *old;
+ struct bitmap *remapped;
+
+ if (!old_bitmap || !mapping)
+ return 0;
+
+ old = pseudo_merge_bitmap_for_commit(old_bitmap, merge);
+ if (!old)
+ return 0;
+
+ remapped = bitmap_new();
+ if (rebuild_bitmap(mapping, old, remapped) < 0) {
+ bitmap_free(remapped);
+ return 0;
+ }
+
+ *out = bitmap_to_ewah(remapped);
+ bitmap_free(remapped);
+ reused_pseudo_merge_bitmaps_nr++;
+ return 1;
+}
+
+static int build_pseudo_merge_bitmap(struct bitmap_writer *writer,
+ struct bitmap_index *old_bitmap,
+ const uint32_t *mapping,
+ struct commit *merge,
+ struct ewah_bitmap **out)
+{
+ struct bb_commit ent = { 0 };
+ struct prio_queue queue = { NULL };
+ struct prio_queue tree_queue = { NULL };
+ unsigned parents = commit_list_count(merge->parents);
+ int ret;
+
+ ent.bitmap = bitmap_new();
+
+ pseudo_merge_bitmap_nr++;
+ pseudo_merge_bitmap_parents += parents;
+
+ if (reuse_pseudo_merge_bitmap(old_bitmap, mapping, merge, out)) {
+ ret = 0;
+ goto done;
+ }
+
+ ret = fill_bitmap_commit(writer, &ent, merge, &queue, &tree_queue,
+ old_bitmap, mapping);
+
+ if (!ret)
+ *out = bitmap_to_ewah(ent.bitmap);
+
+done:
+ bitmap_free(ent.bitmap);
+ clear_prio_queue(&queue);
+ clear_prio_queue(&tree_queue);
+
+ return ret;
+}
+
+static int build_pseudo_merge_bitmaps(struct bitmap_writer *writer,
+ struct bitmap_index *old_bitmap,
+ const uint32_t *mapping,
+ int *nr_stored)
+{
+ size_t i = bitmap_writer_nr_selected_commits(writer);
+ int ret = 0;
+
+ if (!writer->pseudo_merges_nr)
+ return 0;
+
+ trace2_region_enter("pack-bitmap-write", "building_pseudo_merge_bitmaps",
+ writer->repo);
+
+ for (; i < writer->selected_nr; i++) {
+ struct bitmapped_commit *merge = &writer->selected[i];
+ struct commit_list *p;
+ struct bitmap *parents = bitmap_new();
+ struct ewah_bitmap *objects = NULL;
+
+ if (!merge->pseudo_merge)
+ BUG("found non-pseudo merge commit at %"PRIuMAX,
+ (uintmax_t)i);
+
+ for (p = merge->commit->parents; p; p = p->next) {
+ int found;
+ uint32_t pos = find_object_pos(writer,
+ &p->item->object.oid,
+ &found);
+ if (!found) {
+ bitmap_free(parents);
+ ret = -1;
+ goto done;
+ }
+ bitmap_set(parents, pos);
+ }
+
+ merge->pseudo_merge_parents = bitmap_to_ewah(parents);
+ bitmap_free(parents);
+
+ if (build_pseudo_merge_bitmap(writer, old_bitmap, mapping,
+ merge->commit, &objects) < 0) {
+ ret = -1;
+ goto done;
+ }
+ merge->bitmap = objects;
+
+ (*nr_stored)++;
+ display_progress(writer->progress, *nr_stored);
+ }
+
+done:
+ trace2_region_leave("pack-bitmap-write", "building_pseudo_merge_bitmaps",
+ writer->repo);
+
+ trace2_data_intmax("pack-bitmap-write", writer->repo,
+ "pseudo_merge_bitmap_nr",
+ pseudo_merge_bitmap_nr);
+ trace2_data_intmax("pack-bitmap-write", writer->repo,
+ "building_bitmaps_pseudo_merge_reused",
+ reused_pseudo_merge_bitmaps_nr);
+ trace2_data_intmax("pack-bitmap-write", writer->repo,
+ "pseudo_merge_bitmap_parents",
+ pseudo_merge_bitmap_parents);
+
+ return ret;
+}
+
static void store_selected(struct bitmap_writer *writer,
struct bb_commit *ent, struct commit *commit)
{
@@ -821,6 +976,10 @@ int bitmap_writer_build(struct bitmap_writer *writer)
bitmap_free(ent->bitmap);
ent->bitmap = NULL;
}
+ if (closed &&
+ build_pseudo_merge_bitmaps(writer, old_bitmap, mapping,
+ &nr_stored) < 0)
+ closed = 0;
clear_prio_queue(&queue);
clear_prio_queue(&tree_queue);
bitmap_builder_clear(&bb);
@@ -831,9 +990,6 @@ int bitmap_writer_build(struct bitmap_writer *writer)
writer->repo);
trace2_data_intmax("pack-bitmap-write", writer->repo,
"building_bitmaps_reused", reused_bitmaps_nr);
- trace2_data_intmax("pack-bitmap-write", writer->repo,
- "building_bitmaps_pseudo_merge_reused",
- reused_pseudo_merge_bitmaps_nr);
trace2_data_intmax("pack-bitmap-write", writer->repo,
"fill_bitmap_commit_calls_nr",
fill_bitmap_commit_calls_nr);
@@ -1015,23 +1171,6 @@ static void write_pseudo_merges(struct bitmap_writer *writer,
CALLOC_ARRAY(pseudo_merge_ofs, writer->pseudo_merges_nr);
- for (i = 0; i < writer->pseudo_merges_nr; i++) {
- struct bitmapped_commit *merge = &writer->selected[base + i];
- struct commit_list *p;
- struct bitmap *parents = bitmap_new();
-
- if (!merge->pseudo_merge)
- BUG("found non-pseudo merge commit at %"PRIuMAX, (uintmax_t)i);
-
- for (p = merge->commit->parents; p; p = p->next)
- bitmap_set(parents,
- find_object_pos(writer, &p->item->object.oid,
- NULL));
-
- merge->pseudo_merge_parents = bitmap_to_ewah(parents);
- bitmap_free(parents);
- }
-
start = hashfile_total(f);
for (i = 0; i < writer->pseudo_merges_nr; i++) {
@@ -1040,14 +1179,13 @@ static void write_pseudo_merges(struct bitmap_writer *writer,
if (!merge->pseudo_merge)
BUG("found non-pseudo merge commit at %"PRIuMAX, (uintmax_t)i);
- if (!merge->pseudo_merge_parents)
- BUG("missing pseudo-merge parents bitmap for commit %s",
+ if (!merge->pseudo_merge_parents || !merge->bitmap)
+ BUG("missing pseudo-merge bitmap for commit %s",
oid_to_hex(&merge->commit->object.oid));
pseudo_merge_ofs[i] = hashfile_total(f);
-
dump_bitmap(f, merge->pseudo_merge_parents);
- dump_bitmap(f, writer->selected[base+i].write_as);
+ dump_bitmap(f, merge->bitmap);
}
next_ext = st_add(hashfile_total(f),
--
2.54.0.rc1.84.g30ce254312c
^ permalink raw reply related
* [PATCH v6 0/8] fetch: rework negotiation tip options
From: Derrick Stolee via GitGitGadget @ 2026-05-19 16:24 UTC (permalink / raw)
To: git; +Cc: gitster, ps, Matthew John Cheetham, Derrick Stolee
In-Reply-To: <pull.2085.v5.git.1779135575.gitgitgadget@gmail.com>
Fetch negotiation aims to find enough information from haves and wants such
that the server can be reasonably confident that it will send all necessary
objects and not too many "extra" objects that the client already has.
However, this can break down if there are too many references, since Git
truncates the list of haves based on a few factors (a 256 count limit or the
server sending an ACK at the right time).
We already have the --negotiation-tip feature to focus the set of references
that are used in negotiation, but I feel like this is designed backwards.
I'd rather that we have a way to say "this is an important set of refs, but
feel free to add more refs if needed" than "only use these refs for
negotiation".
Here's an example that demonstrates the problem. In an internal monorepo,
developers work off of the 'main' branch so there are thousands of user
branches that each add a few commits different from the 'main' branch.
However, there is also a long-lived 'release' branch. This branch has a
first-parent history that is parallel to 'main' and each of those commits is
a merge whose second parent is a commit from 'main' that had a successful CI
run. There are additional changes in the 'release' branch merge commits that
add some changelog data, so there is a nontrivial set of novel blob content
in that branch and not just a different set of commits.
The problem we had was that our georeplication system was regularly fetching
from the origin and trying to get all data from all reachable branches. When
the 'release' branch updated, the client would run out of haves before
advertising its copy of the 'release' branch, but it would still list the
new 'release' tip as a want. The server would then think that the client had
never fetched that branch before and would send all of the changelog data
from the whole history of the repo. (This led to a lot of downstream
problems; we mitigated by setting a refspec that stopped fetching the
'release' branch, but this is not ideal.)
What I'd like is a mechanism to say "always advertise the client's version
of 'main' and 'release' but also opportunistically include some user
branches".
Based on my understanding, the '--negotiation-tip' option is close but not
quite what I want. I could have the client only advertise 'release' and
'main' and never advertise any user branches. But then we'd download all
content from each user branch every time it updates. Perhaps this would
happen even with opportunistic inclusion of more haves, but I'd like to
explore this area more.
There's also an issue that the '--negotiation-tip' feature doesn't seem to
have a config key that enables it without CLI arguments. This is something
that we could consider independently.
This patch series adds a new '--negotiation-include' option that does what I
want: it makes sure that these references are included as 'have's during
negotiation. In order to help clarify the difference between this and
'--negotiation-tip', I first create a synonym called
'--negotiation-restrict'.
Both of these options get 'remote.*.negotiation(Include|Restrict)' config
options that enable their behavior by default.
During development, I had briefly considered only using config values, but
that required some strange changes to care about the remote name in the
transport layer. This was most different in the 'git push' integration. When
I discovered the '--negotiation-tip' feature during the process, that gave
me a clear pattern to follow with the addition of a config on top.
Updates in v2
=============
This version is a near-complete rewrite based on feedback around the names
of the previous option and config. The --negotiation-restrict option is new
and the ability to set it via config is also new.
I did try to be more careful around translatable error messages, too.
Updates in v3
=============
* --negotiation-tip is now an alias of --negotiation-restrict.
* More translatable strings use %s to isolate non-translatable options from
translatable words.
* The string_list named negotiation_tip is now renamed to
negotiation_restrict.
* The config options now allow an empty value to reset the list.
* The --negotiation-require option is now called --negotiation-include.
* Similarly, the config option is renamed and all code references.
* The included haves now mark their commits as COMMON so commits that they
can reach are not included in the negotiation walk if they are reached
from the restricted commits.
* The ref iterators are more careful about failing on bad references (ref
exists but object doesn't) and ignoring missing references (perhaps
config is erroneous?).
* When sending tips during push negotiation, use the --negotiation-restrict
option instead of -tip.
Updates in v4
=============
Thanks, Matthew, for the detailed review! There are some big changes in this
version.
* Expanded commit message to cite the commit that introduced the bug
(3f763ddf28).
* Renamed --negotiation-tip to --negotiation-restrict throughout docs/code
(including send-pack.c, transport-helper.c, builtin/pull.c). Added
OPT_ALIAS in git-pull.
* Switched config parsing to use parse_transport_option() helper. Removed
git push from docs (not implemented yet). Restructured --negotiate-only
validation flow.
* NEW Patch 5: Added have_sent() interface to negotiators, so included
haves can be de-duplicated properly by the negotiation algorithm.
* Replaced COMMON flag hack with negotiator->have_sent() calls. Moved
ref-pattern resolution into builtin/fetch.c (add_negotiation_tips()) so
fetch-pack receives pre-resolved oid_array instead of string_list. Added
test for --negotiation-tip ignoring missing refs. Added
duplicate-avoidance test for v0. Accepts commit hashes in addition to ref
names/globs.
* Use parse_transport_option() for config. Updated docs to mention commit
hashes. Removed git push from config docs. Fixed test to use correct
restrict/include combinations.
* In the last patch, add doc notes that remote config values also apply
during git push with push.negotiate, now that they are integrated by that
change.
Updates in v5
=============
Responded to small comments.
Updates in v6
=============
Corrected reviewed-by annotations in commit messages.
Thanks, -Stolee
Derrick Stolee (8):
t5516: fix test order flakiness
fetch: add --negotiation-restrict option
transport: rename negotiation_tips
remote: add remote.*.negotiationRestrict config
negotiator: add have_sent() interface
fetch: add --negotiation-include option for negotiation
remote: add remote.*.negotiationInclude config
send-pack: pass negotiation config in push
Documentation/config/fetch.adoc | 2 +-
Documentation/config/remote.adoc | 49 ++++++++
Documentation/fetch-options.adoc | 29 ++++-
builtin/fetch.c | 87 +++++++++++---
builtin/pull.c | 6 +-
fetch-negotiator.h | 9 ++
fetch-pack.c | 99 +++++++++++++---
fetch-pack.h | 10 +-
negotiator/default.c | 8 ++
negotiator/noop.c | 7 ++
negotiator/skipping.c | 8 ++
remote.c | 10 ++
remote.h | 2 +
send-pack.c | 39 +++++--
send-pack.h | 2 +
t/t5510-fetch.sh | 191 +++++++++++++++++++++++++++++++
t/t5516-fetch-push.sh | 32 +++++-
t/t5702-protocol-v2.sh | 4 +-
transport-helper.c | 5 +-
transport.c | 20 +++-
transport.h | 7 +-
21 files changed, 564 insertions(+), 62 deletions(-)
base-commit: 6e8d538aab8fe4dd07ba9fb87b5c7edcfa5706ad
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2085%2Fderrickstolee%2Fmust-have-v6
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2085/derrickstolee/must-have-v6
Pull-Request: https://github.com/gitgitgadget/git/pull/2085
Range-diff vs v5:
1: 538913a327 ! 1: c8c422f646 t5516: fix test order flakiness
@@ Commit message
Use 'sort -k 3' to match the actual number of columns in the output.
- Reviewed-by: Matthew John Cheetham <mcheetham@outlook.com>
+ Reviewed-by: Matthew John Cheetham <mjcheetham@outlook.com>
Signed-off-by: Derrick Stolee <stolee@gmail.com>
## t/t5516-fetch-push.sh ##
2: 580aa58943 ! 2: ac3e8f74d9 fetch: add --negotiation-restrict option
@@ Commit message
translatable with the option name inserted by formatting. At least one
of these messages will be reused later for a new option.
- Reviewed-by: Matthew John Cheetham <mcheetham@outlook.com>
+ Reviewed-by: Matthew John Cheetham <mjcheetham@outlook.com>
Signed-off-by: Derrick Stolee <stolee@gmail.com>
## Documentation/config/fetch.adoc ##
3: eee0543647 ! 3: 5206640b8b transport: rename negotiation_tips
@@ Commit message
Also update the string_list used to store the inputs from command-line
options.
- Reviewed-by: Matthew John Cheetham <mcheetham@outlook.com>
+ Reviewed-by: Matthew John Cheetham <mjcheetham@outlook.com>
Signed-off-by: Derrick Stolee <stolee@gmail.com>
## builtin/fetch.c ##
4: 63c675e93e ! 4: eec0f90e02 remote: add remote.*.negotiationRestrict config
@@ Commit message
An empty value resets the value list to allow ignoring earlier config
values, such as those that might be set in system or global config.
- Reviewed-by: Matthew John Cheetham <mcheetham@outlook.com>
+ Reviewed-by: Matthew John Cheetham <mjcheetham@outlook.com>
Signed-off-by: Derrick Stolee <stolee@gmail.com>
## Documentation/config/remote.adoc ##
5: d423c56283 ! 5: 840db1d957 negotiator: add have_sent() interface
@@ Commit message
common, so the implementation is quite simple. This logic will be exercised
in the next change.
- Reviewed-by: Matthew John Cheetham <mcheetham@outlook.com>
+ Reviewed-by: Matthew John Cheetham <mjcheetham@outlook.com>
Signed-off-by: Derrick Stolee <stolee@gmail.com>
## fetch-negotiator.h ##
6: e86c9791e2 ! 6: 62e5ef1a4b fetch: add --negotiation-include option for negotiation
@@ Commit message
Also add --negotiation-include to 'git pull' passthrough options.
- Reviewed-by: Matthew John Cheetham <mcheetham@outlook.com>
+ Reviewed-by: Matthew John Cheetham <mjcheetham@outlook.com>
Signed-off-by: Derrick Stolee <stolee@gmail.com>
## Documentation/fetch-options.adoc ##
7: e5714115b5 ! 7: 05a4b69b9b remote: add remote.*.negotiationInclude config
@@ Commit message
list to allow ignoring earlier config values, such as those that might be
set in system or global config.
- Reviewed-by: Matthew John Cheetham <mcheetham@outlook.com>
+ Reviewed-by: Matthew John Cheetham <mjcheetham@outlook.com>
Signed-off-by: Derrick Stolee <stolee@gmail.com>
## Documentation/config/remote.adoc ##
8: ed0be32e2c ! 8: c69ca2e919 send-pack: pass negotiation config in push
@@ Commit message
are passed as --negotiation-include to ensure their tips are always
sent as 'have' lines during push negotiation.
- Reviewed-by: Matthew John Cheetham <mcheetham@outlook.com>
+ Reviewed-by: Matthew John Cheetham <mjcheetham@outlook.com>
Signed-off-by: Derrick Stolee <stolee@gmail.com>
## Documentation/config/remote.adoc ##
--
gitgitgadget
^ permalink raw reply
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox