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>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Taylor Blau committedMay 19, 2026 at 11:58 UTC938af8926099882ff87f8ffa4115c34ed63d9e8b
11 files changed+669-23
Documentation/config/repack.adoc
+18
index e9e78dcb19..4c22a499f6 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.
Documentation/git-repack.adoc
+36-2
index 673ce91083..27a99cc46f 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.
builtin/repack.c
+43-6
index 75c5773678..5ffa18e085 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);