repack: allow `--write-midx=incremental` without `--geometric`

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> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Taylor Blau committed May 19, 2026 at 11:58 UTC 06733a50eeec4205011d210d3932c5b708a665e9
4 files changed +133 -18
Documentation/git-repack.adoc
+12 -7
@@ -263,14 +263,19 @@ linkgit:git-multi-pack-index[1]).
263
264 `incremental`;;
265 Write an incremental MIDX chain instead of a single
266 - flat MIDX. This mode requires `--geometric`.
266 + flat MIDX.
267 +
268 -The incremental mode maintains a chain of MIDX layers that is compacted
269 -over time using a geometric merging strategy. Each repack creates a new
270 -tip layer containing the newly written pack(s). Adjacent layers are then
271 -merged whenever the newer layer's object count exceeds
272 -`1/repack.midxSplitFactor` of the next deeper layer's count. Layers
273 -that do not meet this condition are retained as-is.
268 +Without `--geometric`, a new MIDX layer is appended to the existing
269 +chain (or a new chain is started) containing whatever packs were written
270 +by the repack. Existing layers are preserved as-is.
271 ++
272 +When combined with `--geometric`, the incremental mode maintains a chain
273 +of MIDX layers that is compacted over time using a geometric merging
274 +strategy. Each repack creates a new tip layer containing the newly
275 +written pack(s). Adjacent layers are then merged whenever the newer
276 +layer's object count exceeds `1/repack.midxSplitFactor` of the next
277 +deeper layer's count. Layers that do not meet this condition are
278 +retained as-is.
279 +
280 The result is that newer (tip) layers tend to contain many small packs
281 with relatively few objects, while older (deeper) layers contain fewer,
builtin/repack.c
-3
@@ -263,9 +263,6 @@ int cmd_repack(int argc,
263 if (pack_everything & PACK_CRUFT)
264 pack_everything |= ALL_INTO_ONE;
265
266 - if (write_midx == REPACK_WRITE_MIDX_INCREMENTAL && !geometry.split_factor)
267 - die(_("--write-midx=incremental requires --geometric"));
268 -
266 if (write_bitmaps < 0) {
267 if (write_midx == REPACK_WRITE_MIDX_NONE &&
268 (!(pack_everything & ALL_INTO_ONE) || !is_bare_repository()))
repack-midx.c
+61 -3
@@ -548,6 +548,60 @@ static void midx_compaction_step_release(struct midx_compaction_step *step)
548 free(step->csum);
549 }
550
551 +/*
552 + * Build an append-only MIDX plan: a single WRITE step for the freshly
553 + * written packs, plus COPY steps for every existing layer. No
554 + * compaction or merging is performed.
555 + */
556 +static void repack_make_midx_append_plan(struct repack_write_midx_opts *opts,
557 + struct midx_compaction_step **steps_p,
558 + size_t *steps_nr_p)
559 +{
560 + struct multi_pack_index *m;
561 + struct midx_compaction_step *steps = NULL;
562 + struct midx_compaction_step *step;
563 + size_t steps_nr = 0, steps_alloc = 0;
564 +
565 + odb_reprepare(opts->existing->repo->objects);
566 + m = get_multi_pack_index(opts->existing->source);
567 +
568 + if (opts->names->nr) {
569 + struct strbuf buf = STRBUF_INIT;
570 + uint32_t i;
571 +
572 + ALLOC_GROW(steps, st_add(steps_nr, 1), steps_alloc);
573 +
574 + step = &steps[steps_nr++];
575 + memset(step, 0, sizeof(*step));
576 +
577 + step->type = MIDX_COMPACTION_STEP_WRITE;
578 + string_list_init_dup(&step->u.write);
579 +
580 + for (i = 0; i < opts->names->nr; i++) {
581 + strbuf_reset(&buf);
582 + strbuf_addf(&buf, "pack-%s.idx",
583 + opts->names->items[i].string);
584 + string_list_append(&step->u.write, buf.buf);
585 + }
586 +
587 + strbuf_release(&buf);
588 + }
589 +
590 + for (; m; m = m->base_midx) {
591 + ALLOC_GROW(steps, st_add(steps_nr, 1), steps_alloc);
592 +
593 + step = &steps[steps_nr++];
594 + memset(step, 0, sizeof(*step));
595 +
596 + step->type = MIDX_COMPACTION_STEP_COPY;
597 + step->u.copy = m;
598 + step->objects_nr = m->num_objects;
599 + }
600 +
601 + *steps_p = steps;
602 + *steps_nr_p = steps_nr;
603 +}
604 +
605 static int repack_make_midx_compaction_plan(struct repack_write_midx_opts *opts,
606 struct midx_compaction_step **steps_p,
607 size_t *steps_nr_p)
@@ -904,9 +958,13 @@ static int write_midx_incremental(struct repack_write_midx_opts *opts)
958 goto done;
959 }
960
907 - if (repack_make_midx_compaction_plan(opts, &steps, &steps_nr) < 0) {
908 - ret = error(_("unable to generate compaction plan"));
909 - goto done;
961 + if (opts->geometry->split_factor) {
962 + if (repack_make_midx_compaction_plan(opts, &steps, &steps_nr) < 0) {
963 + ret = error(_("unable to generate compaction plan"));
964 + goto done;
965 + }
966 + } else {
967 + repack_make_midx_append_plan(opts, &steps, &steps_nr);
968 }
969
970 for (i = 0; i < steps_nr; i++) {
t/t7705-repack-incremental-midx.sh
+60 -5
@@ -63,10 +63,36 @@ create_layers () {
63 done
64 }
65
66 -test_expect_success '--write-midx=incremental requires --geometric' '
67 - test_must_fail git repack --write-midx=incremental 2>err &&
66 +test_expect_success '--write-midx=incremental without --geometric' '
67 + git init incremental-without-geometric &&
68 + (
69 + cd incremental-without-geometric &&
70 +
71 + git config maintenance.auto false &&
72 +
73 + test_commit first &&
74 + git repack -d &&
75
69 - test_grep -- "--write-midx=incremental requires --geometric" err
76 + test_commit second &&
77 + git repack --write-midx=incremental &&
78 +
79 + git multi-pack-index verify &&
80 + test_line_count = 1 $midx_chain &&
81 + cp $midx_chain $midx_chain.before &&
82 +
83 + # A second repack appends a new layer without
84 + # disturbing the existing one.
85 + test_commit third &&
86 + git repack --write-midx=incremental &&
87 +
88 + git multi-pack-index verify &&
89 + test_line_count = 2 $midx_chain &&
90 + head -n 1 $midx_chain.before >expect &&
91 + head -n 1 $midx_chain >actual &&
92 + test_cmp expect actual &&
93 +
94 + git fsck
95 + )
96 '
97
98 test_expect_success 'below layer threshold, tip packs excluded' '
@@ -334,8 +360,7 @@ test_expect_success 'kept packs are excluded from repack' '
360 # entirely, so no rollup occurs as there is only one
361 # non-kept pack. A new MIDX layer is written containing
362 # that pack.
337 - git repack --geometric=2 -d --write-midx=incremental \
338 - --write-bitmap-index &&
363 + git repack --geometric=2 -d --write-midx=incremental &&
364
365 test-tool read-midx $objdir >actual &&
366 grep "^pack-.*\.idx$" actual >actual.packs &&
@@ -433,6 +458,36 @@ test_expect_success 'repack -ad removes stale incremental chain' '
458 )
459 '
460
461 +test_expect_success 'repack -ad --write-midx=incremental is safe' '
462 + git init ad-incremental-midx &&
463 + (
464 + cd ad-incremental-midx &&
465 +
466 + git config maintenance.auto false &&
467 +
468 + # Build a MIDX chain with multiple layers referencing
469 + # distinct packs.
470 + test_commit first &&
471 + git repack -d &&
472 +
473 + test_commit second &&
474 + git repack -d --write-midx=incremental &&
475 +
476 + git multi-pack-index verify &&
477 + test_line_count = 1 $midx_chain &&
478 +
479 + # Now do a full -ad repack. The new pack contains all
480 + # objects, but any retained MIDX layers still reference
481 + # the now-deleted packs.
482 + test_commit third &&
483 + git repack -ad --write-midx=incremental &&
484 +
485 + git multi-pack-index verify &&
486 + git fsck &&
487 + git rev-list --all --objects >/dev/null
488 + )
489 +'
490 +
491 test_expect_success 'repack rejects invalid midxSplitFactor' '
492 test_when_finished "rm -fr bad-split-factor" &&
493 git init bad-split-factor &&