{"thread":{"id":"65876","subject":"[RFC PATCH 00/10] repack: combine '--geometric' and '--cruft'","startedAt":"2026-06-26T19:02:12Z","lastAt":"2026-07-27T15:04:17Z","messageCount":18,"participants":["Taylor Blau","Junio C Hamano"],"isPatch":true,"patchVersion":1,"patchTotal":10},"messages":[{"id":"546499","messageId":"cover.1782500507.git.me@ttaylorr.com","threadId":"65876","inReplyTo":null,"subject":"[RFC PATCH 00/10] repack: combine '--geometric' and '--cruft'","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-06-26T19:02:10Z","receivedAt":"2026-06-26T19:02:12Z","isPatch":true,"body":"First, a short note. This series is an RFC because I have not had the\nchance to review and test it as thoroughly as I normally would, and\nbecause we are deep in the -rc phase.\n\nI wanted to get this series off my backlog since I have decided to leave\nGitHub at the end of the month for a new role. I will still be\ncontributing to Git in my new role (which I will start in the early part\nof July), but wanted to get this off my backlog nonetheless.\n\nThis series teaches `git repack` how to combine `--geometric` and\n`--cruft`.\n\nToday these two modes are mutually exclusive, since `--cruft` implies\n`-a`, and `-a` is fundamentally incompatible with `--geometric`. As a\nresult, repositories have to choose between keeping reachable objects in\na geometric progression of packs and collecting unreachable objects into\ncruft packs.\n\nThe goal of this series is to to be able to do both simultaneously. When\nboth options are given, 'git repack' rolls up the selected non-cruft\npacks as usual while collecting unreachable objects separately into a\ncruft pack. That means a command like\n\n    $ git repack -d --geometric=2 --cruft --combine-cruft-below-size=1G\n\nwill keep reachable non-cruft packs in a geometric progression, while\ncombining sufficiently-small cruft packs (along with newly-discovered\nunreachable objects) into a fresh cruft pack.\n\nThe series is structured roughly as follows:\n\n * The first two patches prepare the cruft pack machinery for the later\n   changes by making non-kept pack exclusion unconditional and\n   extracting a helper for looking up packs in an `existing_packs` list.\n\n * The next four patches route geometric pack deletion through the\n   common `existing_packs` machinery. They mark packs above the\n   geometric split as retained, teach incremental-MIDX retention not to\n   keep packs that are being rolled up, switch geometric repacks over to\n   the common deletion path, and then remove the old geometry-specific\n   deletion helper.\n\n * The next three patches teach `pack-objects` the new pieces needed by\n   this mode. The main addition is `--stdin-packs=follow-reachable`,\n   which walks from reference tips and includes only reachable objects\n   from the selected packs, while still allowing traversal through\n   excluded-open packs and stopping at excluded-closed ones. The\n   following patch teaches that mode to use `--refs-snapshot`, so that\n   `pack-objects` and the MIDX bitmap writer can agree on the same set\n   of tips.\n\n * The final patch wires everything together in `git repack`, including\n   teaching the cruft writer how to interpret the geometric split when\n   choosing which packs to include or exclude.\n\nThanks in advance for your review!\n\nTaylor Blau (10):\n  repack: unconditionally exclude non-kept packs\n  repack: extract `locate_existing_pack()` helper\n  repack: mark geometric progression of packs as retained\n  repack: teach MIDX retention about geometric rollups\n  repack: delete geometric packs via existing_packs\n  repack-geometry: drop unused redundant-pack removal\n  pack-objects: extract `stdin_packs_add_all_pack_entries()`\n  pack-objects: introduce '--stdin-packs=follow-reachable'\n  pack-objects: support '--refs-snapshot' with 'follow-reachable'\n  repack: support combining '--geometric' with '--cruft'\n\n Documentation/git-pack-objects.adoc |  25 +++\n Documentation/git-repack.adoc       |  11 ++\n builtin/pack-objects.c              | 276 ++++++++++++++++++++++++----\n builtin/repack.c                    |  38 ++--\n repack-cruft.c                      |  29 ++-\n repack-geometry.c                   |  44 -----\n repack.c                            | 101 +++++++++-\n repack.h                            |  15 +-\n t/t5331-pack-objects-stdin.sh       | 201 ++++++++++++++++++++\n t/t7704-repack-cruft.sh             | 251 +++++++++++++++++++++++++\n 10 files changed, 878 insertions(+), 113 deletions(-)\n\n\nbase-commit: ab776a62a78576513ee121424adb19597fbb7613\n-- \n2.55.0.rc2.10.g29e31820dce\n"},{"id":"546500","messageId":"7e607b7b64eb347dbf92a0dee3883aaa34bbb177.1782500507.git.me@ttaylorr.com","threadId":"65876","inReplyTo":"cover.1782500507.git.me@ttaylorr.com","subject":"[RFC PATCH 01/10] repack: unconditionally exclude non-kept packs","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-06-26T19:02:13Z","receivedAt":"2026-06-26T19:02:16Z","isPatch":true,"body":"In `write_cruft_pack()`, we handle excluding objects found in non-kept\npacks from being included in the cruft pack via two code paths:\n\n * When using '--combine-cruft-below-size' (provided that we are not\n   expiring cruft objects), we use the aptly-named\n   `combine_small_cruft_packs()` function.\n\n * In all other cases, we handle it directly in the 'else' branch of the\n   same conditional.\n\nSimplify this by moving the non-kept pack exclusion out of the\nconditional entirely, so that non-kept packs are always excluded\nregardless of whether we are combining small cruft packs or not.\n\nThis is a preparatory refactor for a subsequent change that will use the\npack_geometry struct when available to determine which non-kept packs to\nexclude.\n\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\n---\n repack-cruft.c | 14 ++++----------\n 1 file changed, 4 insertions(+), 10 deletions(-)\n\ndiff --git a/repack-cruft.c b/repack-cruft.c\nindex 0653e887923..6a040e98017 100644\n--- a/repack-cruft.c\n+++ b/repack-cruft.c\n@@ -9,7 +9,6 @@ static void combine_small_cruft_packs(FILE *in, off_t combine_cruft_below_size,\n {\n \tstruct packed_git *p;\n \tstruct strbuf buf = STRBUF_INIT;\n-\tsize_t i;\n \n \trepo_for_each_pack(existing->repo, p) {\n \t\tif (!(p->is_cruft && p->pack_local))\n@@ -30,10 +29,6 @@ static void combine_small_cruft_packs(FILE *in, off_t combine_cruft_below_size,\n \t\t}\n \t}\n \n-\tfor (i = 0; i < existing->non_kept_packs.nr; i++)\n-\t\tfprintf(in, \"-%s.pack\\n\",\n-\t\t\texisting->non_kept_packs.items[i].string);\n-\n \tstrbuf_release(&buf);\n }\n \n@@ -80,15 +75,14 @@ int write_cruft_pack(const struct write_pack_opts *opts,\n \tin = xfdopen(cmd.in, \"w\");\n \tfor_each_string_list_item(item, names)\n \t\tfprintf(in, \"%s-%s.pack\\n\", pack_prefix, item->string);\n-\tif (combine_cruft_below_size && !cruft_expiration) {\n+\tif (combine_cruft_below_size && !cruft_expiration)\n \t\tcombine_small_cruft_packs(in, combine_cruft_below_size,\n \t\t\t\t\t  existing);\n-\t} else {\n-\t\tfor_each_string_list_item(item, &existing->non_kept_packs)\n-\t\t\tfprintf(in, \"-%s.pack\\n\", item->string);\n+\telse\n \t\tfor_each_string_list_item(item, &existing->cruft_packs)\n \t\t\tfprintf(in, \"-%s.pack\\n\", item->string);\n-\t}\n+\tfor_each_string_list_item(item, &existing->non_kept_packs)\n+\t\tfprintf(in, \"-%s.pack\\n\", item->string);\n \tfor_each_string_list_item(item, &existing->kept_packs)\n \t\tfprintf(in, \"%s.pack\\n\", item->string);\n \tfclose(in);\n-- \n2.55.0.rc2.10.g29e31820dce\n\n"},{"id":"546501","messageId":"b0cf30444a64de190d0bbfc0eb5d01f88a0ed49b.1782500507.git.me@ttaylorr.com","threadId":"65876","inReplyTo":"cover.1782500507.git.me@ttaylorr.com","subject":"[RFC PATCH 03/10] repack: mark geometric progression of packs as retained","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-06-26T19:02:20Z","receivedAt":"2026-06-26T19:02:22Z","isPatch":true,"body":"In non-geometric repacks, any packs which repack wishes to delete are\nhandled via the `existing_packs` struct, which has a mechanism to retain\nwould-be-deleted packs (e.g., if we happened to write a new pack\nidentical to one otherwise marked for deletion).\n\nIn geometric repacks, repack removes any rewritten packs (alternatively,\nany packs which were combined in order to restore a geometric\nprogression) by enumerating them via `pack_geometry_remove_redundant()`.\n\nPrepare to use the `existing_packs` deletion machinery for geometric\nrepacks by marking any non-kept packs above the geometric split line as\nretained. Do the same for promisor packs, which have their own split\npoint.\n\nThis commit only records which packs the later deletion pass must keep;\nit does not change which packs are written or removed.\n\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\n---\n builtin/repack.c |  2 ++\n repack.c         | 27 +++++++++++++++++++++++++++\n repack.h         |  3 +++\n 3 files changed, 32 insertions(+)\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex 1524a9c13ad..ce979d86d96 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -325,6 +325,8 @@ int cmd_repack(int argc,\n \t\t}\n \t\tpack_geometry_init(&geometry, &existing, &po_args);\n \t\tpack_geometry_split(&geometry);\n+\n+\t\texisting_packs_retain_from_geometry(&existing, &geometry);\n \t}\n \n \tprepare_pack_objects(&cmd, &po_args, packtmp);\ndiff --git a/repack.c b/repack.c\nindex 986c74ac7e8..9b3cb425431 100644\n--- a/repack.c\n+++ b/repack.c\n@@ -254,6 +254,33 @@ void existing_packs_retain_cruft(struct existing_packs *existing,\n \texisting_packs_mark_retained(item);\n }\n \n+static void existing_packs_retain_non_kept(struct existing_packs *existing,\n+\t\t\t\t\t   struct packed_git *p)\n+{\n+\tstruct string_list_item *item;\n+\n+\tif (!p->pack_local)\n+\t\treturn;\n+\n+\titem = locate_existing_pack(&existing->non_kept_packs, p);\n+\tif (!item)\n+\t\tBUG(\"could not find non-kept pack '%s'\", pack_basename(p));\n+\n+\texisting_packs_mark_retained(item);\n+}\n+\n+void existing_packs_retain_from_geometry(struct existing_packs *existing,\n+\t\t\t\t\t const struct pack_geometry *geometry)\n+{\n+\tuint32_t i;\n+\n+\tfor (i = geometry->split; i < geometry->pack_nr; i++)\n+\t\texisting_packs_retain_non_kept(existing, geometry->pack[i]);\n+\tfor (i = geometry->promisor_split; i < geometry->promisor_pack_nr; i++)\n+\t\texisting_packs_retain_non_kept(existing,\n+\t\t\t\t\t       geometry->promisor_pack[i]);\n+}\n+\n void existing_packs_mark_for_deletion(struct existing_packs *existing,\n \t\t\t\t      struct string_list *names)\n \ndiff --git a/repack.h b/repack.h\nindex f9fbc895f02..bb4c944d0cb 100644\n--- a/repack.h\n+++ b/repack.h\n@@ -54,6 +54,7 @@ int finish_pack_objects_cmd(const struct git_hash_algo *algop,\n \n struct repository;\n struct packed_git;\n+struct pack_geometry;\n \n struct existing_packs {\n \tstruct repository *repo;\n@@ -82,6 +83,8 @@ int existing_packs_has_non_kept(const struct existing_packs *existing);\n int existing_pack_is_marked_for_deletion(struct string_list_item *item);\n void existing_packs_retain_cruft(struct existing_packs *existing,\n \t\t\t\t struct packed_git *cruft);\n+void existing_packs_retain_from_geometry(struct existing_packs *existing,\n+\t\t\t\t\t const struct pack_geometry *geometry);\n void existing_packs_mark_for_deletion(struct existing_packs *existing,\n \t\t\t\t      struct string_list *names);\n void existing_packs_retain_midx_packs(struct existing_packs *existing);\n-- \n2.55.0.rc2.10.g29e31820dce\n\n"},{"id":"546502","messageId":"ad76f06fc7ed304af97c73a5931e1ebc5f2d3895.1782500507.git.me@ttaylorr.com","threadId":"65876","inReplyTo":"cover.1782500507.git.me@ttaylorr.com","subject":"[RFC PATCH 04/10] repack: teach MIDX retention about geometric rollups","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-06-26T19:02:23Z","receivedAt":"2026-06-26T19:02:27Z","isPatch":true,"body":"When writing an incremental MIDX, existing_packs_retain_midx_packs()\nmarks packs in the existing MIDX chain as retained. This keeps them from\nbeing deleted by the later existing_packs deletion pass, since retained\nMIDX layers may still refer to those packs.\n\nGeometric repacks need a narrower rule. Packs below the split are rolled\nup into the newly-written pack, and should remain eligible for deletion\neven if the old MIDX chain mentions them. Packs above the split were\nmarked as retained by the previous commit.\n\nTeach existing_packs_retain_midx_packs() to skip packs which are part of\nthe geometric rollup. This does not change the current caller's behavior,\nsince geometric repacks do not yet use the existing_packs deletion path.\n\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\n---\n builtin/repack.c |  2 +-\n repack.c         | 43 +++++++++++++++++++++++++++++++++++++++++--\n repack.h         |  3 ++-\n 3 files changed, 44 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex ce979d86d96..66b46b86896 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -576,7 +576,7 @@ int cmd_repack(int argc,\n \n \tif (delete_redundant && pack_everything & ALL_INTO_ONE) {\n \t\tif (write_midx == REPACK_WRITE_MIDX_INCREMENTAL)\n-\t\t\texisting_packs_retain_midx_packs(&existing);\n+\t\t\texisting_packs_retain_midx_packs(&existing, &geometry);\n \t\texisting_packs_mark_for_deletion(&existing, &names);\n \t}\n \ndiff --git a/repack.c b/repack.c\nindex 9b3cb425431..c7b79a3c113 100644\n--- a/repack.c\n+++ b/repack.c\n@@ -292,6 +292,39 @@ void existing_packs_mark_for_deletion(struct existing_packs *existing,\n \t\t\t\t\t   &existing->cruft_packs);\n }\n \n+static int pack_geometry_contains_pack(struct packed_git **packs,\n+\t\t\t\t       uint32_t packs_nr,\n+\t\t\t\t       const char *base)\n+{\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tuint32_t i;\n+\n+\tfor (i = 0; i < packs_nr; i++) {\n+\t\tstrbuf_reset(&buf);\n+\t\tstrbuf_addstr(&buf, pack_basename(packs[i]));\n+\t\tstrbuf_strip_suffix(&buf, \".pack\");\n+\n+\t\tif (!strcmp(buf.buf, base)) {\n+\t\t\tstrbuf_release(&buf);\n+\t\t\treturn 1;\n+\t\t}\n+\t}\n+\n+\tstrbuf_release(&buf);\n+\treturn 0;\n+}\n+\n+static int pack_geometry_contains_rollup(const struct pack_geometry *geometry,\n+\t\t\t\t\t const char *base)\n+{\n+\tif (!geometry || !geometry->split_factor)\n+\t\treturn 0;\n+\n+\treturn pack_geometry_contains_pack(geometry->pack, geometry->split, base) ||\n+\t       pack_geometry_contains_pack(geometry->promisor_pack,\n+\t\t\t\t\t   geometry->promisor_split, base);\n+}\n+\n /*\n  * Mark every pack that is referenced by the existing MIDX chain as\n  * retained, so that a subsequent call to\n@@ -300,9 +333,12 @@ void existing_packs_mark_for_deletion(struct existing_packs *existing,\n  * This is used when writing an incremental MIDX layer on top of an\n  * existing chain: retained layers continue to reference the same\n  * packs on disk, so those packs must not be unlinked even if the\n- * freshly-written pack supersedes them.\n+ * freshly-written pack supersedes them. When doing a geometric repack,\n+ * packs below the split are rewritten into the new MIDX tip and should\n+ * remain eligible for deletion.\n  */\n-void existing_packs_retain_midx_packs(struct existing_packs *existing)\n+void existing_packs_retain_midx_packs(struct existing_packs *existing,\n+\t\t\t\t      const struct pack_geometry *geometry)\n {\n \tstruct string_list_item *item;\n \tstruct strbuf buf = STRBUF_INIT;\n@@ -315,6 +351,9 @@ void existing_packs_retain_midx_packs(struct existing_packs *existing)\n \t\tstrbuf_strip_suffix(&buf, \".pack\");\n \t\tstrbuf_strip_suffix(&buf, \".idx\");\n \n+\t\tif (pack_geometry_contains_rollup(geometry, buf.buf))\n+\t\t\tcontinue;\n+\n \t\tfound = string_list_lookup(&existing->non_kept_packs, buf.buf);\n \t\tif (found)\n \t\t\texisting_packs_mark_retained(found);\ndiff --git a/repack.h b/repack.h\nindex bb4c944d0cb..f0d082df9e8 100644\n--- a/repack.h\n+++ b/repack.h\n@@ -87,7 +87,8 @@ void existing_packs_retain_from_geometry(struct existing_packs *existing,\n \t\t\t\t\t const struct pack_geometry *geometry);\n void existing_packs_mark_for_deletion(struct existing_packs *existing,\n \t\t\t\t      struct string_list *names);\n-void existing_packs_retain_midx_packs(struct existing_packs *existing);\n+void existing_packs_retain_midx_packs(struct existing_packs *existing,\n+\t\t\t\t      const struct pack_geometry *geometry);\n void existing_packs_remove_redundant(struct existing_packs *existing,\n \t\t\t\t     const char *packdir,\n \t\t\t\t     bool wrote_incremental_midx);\n-- \n2.55.0.rc2.10.g29e31820dce\n\n"},{"id":"546503","messageId":"51752a0ef9f0ac1a4027eee67375ce0914daff28.1782500507.git.me@ttaylorr.com","threadId":"65876","inReplyTo":"cover.1782500507.git.me@ttaylorr.com","subject":"[RFC PATCH 05/10] repack: delete geometric packs via existing_packs","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-06-26T19:02:27Z","receivedAt":"2026-06-26T19:02:30Z","isPatch":true,"body":"Now that packs above the geometric split are marked as retained, teach\ngeometric repacks to use the existing_packs deletion machinery instead of\ncalling pack_geometry_remove_redundant().\n\nThis lets geometric repacks share the same mark-then-remove path as\nall-into-one repacks: packs below the split are marked for deletion, and\npacks above the split are ignored because they were retained earlier.\n\nWhen doing a geometric repack without --combine-cruft-below-size, retain\nall cruft packs before marking anything for deletion. Geometric repacks do\nnot rewrite cruft packs in that mode, so the common deletion path must not\nremove them.\n\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\n---\n builtin/repack.c | 11 +++++------\n repack.c         |  8 ++++++++\n repack.h         |  1 +\n 3 files changed, 14 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex 66b46b86896..dfb6fed231d 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -574,10 +574,13 @@ int cmd_repack(int argc,\n \t\t\t\t       packtmp);\n \t/* End of pack replacement. */\n \n-\tif (delete_redundant && pack_everything & ALL_INTO_ONE) {\n+\tif (delete_redundant) {\n \t\tif (write_midx == REPACK_WRITE_MIDX_INCREMENTAL)\n \t\t\texisting_packs_retain_midx_packs(&existing, &geometry);\n-\t\texisting_packs_mark_for_deletion(&existing, &names);\n+\t\tif (geometry.split_factor && !combine_cruft_below_size)\n+\t\t\texisting_packs_retain_all_cruft(&existing);\n+\t\tif (pack_everything & ALL_INTO_ONE || geometry.split_factor)\n+\t\t\texisting_packs_mark_for_deletion(&existing, &names);\n \t}\n \n \tif (write_midx != REPACK_WRITE_MIDX_NONE) {\n@@ -609,10 +612,6 @@ int cmd_repack(int argc,\n \t\texisting_packs_remove_redundant(&existing, packdir,\n \t\t\t\t\t\twrote_incremental_midx);\n \n-\t\tif (geometry.split_factor)\n-\t\t\tpack_geometry_remove_redundant(&geometry, &names,\n-\t\t\t\t\t\t       &existing, packdir,\n-\t\t\t\t\t\t       wrote_incremental_midx);\n \t\tif (show_progress)\n \t\t\topts |= PRUNE_PACKED_VERBOSE;\n \t\tprune_packed_objects(opts);\ndiff --git a/repack.c b/repack.c\nindex c7b79a3c113..90797561954 100644\n--- a/repack.c\n+++ b/repack.c\n@@ -242,6 +242,14 @@ static struct string_list_item *locate_existing_pack(struct string_list *list,\n \treturn item;\n }\n \n+void existing_packs_retain_all_cruft(struct existing_packs *existing)\n+{\n+\tstruct string_list_item *item;\n+\n+\tfor_each_string_list_item(item, &existing->cruft_packs)\n+\t\texisting_packs_mark_retained(item);\n+}\n+\n void existing_packs_retain_cruft(struct existing_packs *existing,\n \t\t\t\t struct packed_git *cruft)\n {\ndiff --git a/repack.h b/repack.h\nindex f0d082df9e8..90c89630ef8 100644\n--- a/repack.h\n+++ b/repack.h\n@@ -81,6 +81,7 @@ void existing_packs_collect(struct existing_packs *existing,\n \t\t\t    const struct string_list *extra_keep);\n int existing_packs_has_non_kept(const struct existing_packs *existing);\n int existing_pack_is_marked_for_deletion(struct string_list_item *item);\n+void existing_packs_retain_all_cruft(struct existing_packs *existing);\n void existing_packs_retain_cruft(struct existing_packs *existing,\n \t\t\t\t struct packed_git *cruft);\n void existing_packs_retain_from_geometry(struct existing_packs *existing,\n-- \n2.55.0.rc2.10.g29e31820dce\n\n"},{"id":"546504","messageId":"ce7e4bfb3c7b3775def25d9fc78827e98952e3f5.1782500507.git.me@ttaylorr.com","threadId":"65876","inReplyTo":"cover.1782500507.git.me@ttaylorr.com","subject":"[RFC PATCH 06/10] repack-geometry: drop unused redundant-pack removal","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-06-26T19:02:30Z","receivedAt":"2026-06-26T19:02:32Z","isPatch":true,"body":"The previous commit stopped using pack_geometry_remove_redundant() when\ndeleting packs after a geometric repack. The existing_packs machinery now\nhandles the same removal after geometric packs are marked for deletion.\n\nRemove the unused geometry-specific helper and its declaration.\n\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\n---\n repack-geometry.c | 44 --------------------------------------------\n repack.h          |  5 -----\n 2 files changed, 49 deletions(-)\n\ndiff --git a/repack-geometry.c b/repack-geometry.c\nindex 2064683dcfe..c75fa508612 100644\n--- a/repack-geometry.c\n+++ b/repack-geometry.c\n@@ -245,50 +245,6 @@ struct packed_git *pack_geometry_preferred_pack(struct pack_geometry *geometry)\n \treturn NULL;\n }\n \n-static void remove_redundant_packs(struct packed_git **pack,\n-\t\t\t\t   uint32_t pack_nr,\n-\t\t\t\t   struct string_list *names,\n-\t\t\t\t   struct existing_packs *existing,\n-\t\t\t\t   const char *packdir,\n-\t\t\t\t   bool wrote_incremental_midx)\n-{\n-\tconst struct git_hash_algo *algop = existing->repo->hash_algo;\n-\tstruct strbuf buf = STRBUF_INIT;\n-\tuint32_t i;\n-\n-\tfor (i = 0; i < pack_nr; i++) {\n-\t\tstruct packed_git *p = pack[i];\n-\t\tif (string_list_has_string(names, hash_to_hex_algop(p->hash,\n-\t\t\t\t\t\t\t\t    algop)))\n-\t\t\tcontinue;\n-\n-\t\tstrbuf_reset(&buf);\n-\t\tstrbuf_addstr(&buf, pack_basename(p));\n-\t\tstrbuf_strip_suffix(&buf, \".pack\");\n-\n-\t\tif ((p->pack_keep) ||\n-\t\t    (string_list_has_string(&existing->kept_packs, buf.buf)))\n-\t\t\tcontinue;\n-\n-\t\trepack_remove_redundant_pack(existing->repo, packdir, buf.buf,\n-\t\t\t\t\t     wrote_incremental_midx);\n-\t}\n-\n-\tstrbuf_release(&buf);\n-}\n-\n-void pack_geometry_remove_redundant(struct pack_geometry *geometry,\n-\t\t\t\t    struct string_list *names,\n-\t\t\t\t    struct existing_packs *existing,\n-\t\t\t\t    const char *packdir,\n-\t\t\t\t    bool wrote_incremental_midx)\n-{\n-\tremove_redundant_packs(geometry->pack, geometry->split,\n-\t\t\t       names, existing, packdir, wrote_incremental_midx);\n-\tremove_redundant_packs(geometry->promisor_pack, geometry->promisor_split,\n-\t\t\t       names, existing, packdir, wrote_incremental_midx);\n-}\n-\n void pack_geometry_release(struct pack_geometry *geometry)\n {\n \tif (!geometry)\ndiff --git a/repack.h b/repack.h\nindex 90c89630ef8..4295829cea0 100644\n--- a/repack.h\n+++ b/repack.h\n@@ -134,11 +134,6 @@ void pack_geometry_init(struct pack_geometry *geometry,\n \t\t\tconst struct pack_objects_args *args);\n void pack_geometry_split(struct pack_geometry *geometry);\n struct packed_git *pack_geometry_preferred_pack(struct pack_geometry *geometry);\n-void pack_geometry_remove_redundant(struct pack_geometry *geometry,\n-\t\t\t\t    struct string_list *names,\n-\t\t\t\t    struct existing_packs *existing,\n-\t\t\t\t    const char *packdir,\n-\t\t\t\t    bool wrote_incremental_midx);\n void pack_geometry_release(struct pack_geometry *geometry);\n \n struct tempfile;\n-- \n2.55.0.rc2.10.g29e31820dce\n\n"},{"id":"546505","messageId":"1493023dccfd3e231ddc6a155c7fb5295a9631ec.1782500507.git.me@ttaylorr.com","threadId":"65876","inReplyTo":"cover.1782500507.git.me@ttaylorr.com","subject":"[RFC PATCH 02/10] repack: extract `locate_existing_pack()` helper","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-06-26T19:02:17Z","receivedAt":"2026-06-26T19:02:33Z","isPatch":true,"body":"Factor out the lookup from `existing_packs_retain_cruft()` that converts\na pack basename to a `string_list_item` into a reusable static helper\nfunction, `locate_existing_pack()`.\n\nA subsequent commit will introduce a new function which will need to\nperform this same lookup against a different `string_list`.\n\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\n---\n repack.c | 23 +++++++++++++++++------\n 1 file changed, 17 insertions(+), 6 deletions(-)\n\ndiff --git a/repack.c b/repack.c\nindex 571dabb665e..986c74ac7e8 100644\n--- a/repack.c\n+++ b/repack.c\n@@ -226,21 +226,32 @@ static void existing_packs_mark_for_deletion_1(const struct git_hash_algo *algop\n \t}\n }\n \n+static struct string_list_item *locate_existing_pack(struct string_list *list,\n+\t\t\t\t\t\t     struct packed_git *p)\n+{\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tstruct string_list_item *item;\n+\n+\tstrbuf_addstr(&buf, pack_basename(p));\n+\tstrbuf_strip_suffix(&buf, \".pack\");\n+\n+\titem = string_list_lookup(list, buf.buf);\n+\n+\tstrbuf_release(&buf);\n+\n+\treturn item;\n+}\n+\n void existing_packs_retain_cruft(struct existing_packs *existing,\n \t\t\t\t struct packed_git *cruft)\n {\n-\tstruct strbuf buf = STRBUF_INIT;\n \tstruct string_list_item *item;\n \n-\tstrbuf_addstr(&buf, pack_basename(cruft));\n-\tstrbuf_strip_suffix(&buf, \".pack\");\n-\n-\titem = string_list_lookup(&existing->cruft_packs, buf.buf);\n+\titem = locate_existing_pack(&existing->cruft_packs, cruft);\n \tif (!item)\n \t\tBUG(\"could not find cruft pack '%s'\", pack_basename(cruft));\n \n \texisting_packs_mark_retained(item);\n-\tstrbuf_release(&buf);\n }\n \n void existing_packs_mark_for_deletion(struct existing_packs *existing,\n-- \n2.55.0.rc2.10.g29e31820dce\n\n"},{"id":"546506","messageId":"ded6e1110fedd20a960da2df27c55cdd9f741881.1782500507.git.me@ttaylorr.com","threadId":"65876","inReplyTo":"cover.1782500507.git.me@ttaylorr.com","subject":"[RFC PATCH 07/10] pack-objects: extract `stdin_packs_add_all_pack_entries()`","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-06-26T19:02:33Z","receivedAt":"2026-06-26T19:02:37Z","isPatch":true,"body":"Extract the pack enumeration loop from stdin_packs_add_pack_entries()\ninto a separate stdin_packs_add_all_pack_entries() helper, and have the\ncaller dispatch to it based on the stdin_packs_mode.\n\nThis prepares for a subsequent commit which will introduce an alternate\ncode path for '--stdin-packs=follow-reachable' that determines the set\nof objects to include via a reachability walk rather than eagerly adding\nall objects from included packs.\n\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\n---\n builtin/pack-objects.c | 49 ++++++++++++++++++++++++------------------\n 1 file changed, 28 insertions(+), 21 deletions(-)\n\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex 27048bbb4dd..29e43abb51e 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -3933,30 +3933,12 @@ static int stdin_packs_include_check(struct commit *commit, void *data)\n \treturn stdin_packs_include_check_obj((struct object *)commit, data);\n }\n \n-static void stdin_packs_add_pack_entries(struct strmap *packs,\n-\t\t\t\t\t struct rev_info *revs)\n+static void stdin_packs_add_all_pack_entries(struct string_list *keys,\n+\t\t\t\t\t     struct rev_info *revs)\n {\n-\tstruct string_list keys = STRING_LIST_INIT_NODUP;\n \tstruct string_list_item *item;\n-\tstruct hashmap_iter iter;\n-\tstruct strmap_entry *entry;\n \n-\tstrmap_for_each_entry(packs, &iter, entry) {\n-\t\tstruct stdin_pack_info *info = entry->value;\n-\t\tif (!info->p)\n-\t\t\tdie(_(\"could not find pack '%s'\"), entry->key);\n-\n-\t\tstring_list_append(&keys, entry->key)->util = info;\n-\t}\n-\n-\t/*\n-\t * Order packs by ascending mtime; use QSORT directly to access the\n-\t * string_list_item's ->util pointer, which string_list_sort() does not\n-\t * provide.\n-\t */\n-\tQSORT(keys.items, keys.nr, pack_mtime_cmp);\n-\n-\tfor_each_string_list_item(item, &keys) {\n+\tfor_each_string_list_item(item, keys) {\n \t\tstruct stdin_pack_info *info = item->util;\n \n \t\tif (info->kind & STDIN_PACK_EXCLUDE_OPEN) {\n@@ -3977,6 +3959,31 @@ static void stdin_packs_add_pack_entries(struct strmap *packs,\n \t\t\t\t\t\trevs,\n \t\t\t\t\t\tODB_FOR_EACH_OBJECT_PACK_ORDER);\n \t}\n+}\n+\n+static void stdin_packs_add_pack_entries(struct strmap *packs,\n+\t\t\t\t\t struct rev_info *revs)\n+{\n+\tstruct string_list keys = STRING_LIST_INIT_NODUP;\n+\tstruct hashmap_iter iter;\n+\tstruct strmap_entry *entry;\n+\n+\tstrmap_for_each_entry(packs, &iter, entry) {\n+\t\tstruct stdin_pack_info *info = entry->value;\n+\t\tif (!info->p)\n+\t\t\tdie(_(\"could not find pack '%s'\"), entry->key);\n+\n+\t\tstring_list_append(&keys, entry->key)->util = info;\n+\t}\n+\n+\t/*\n+\t * Order packs by ascending mtime; use QSORT directly to access the\n+\t * string_list_item's ->util pointer, which string_list_sort() does not\n+\t * provide.\n+\t */\n+\tQSORT(keys.items, keys.nr, pack_mtime_cmp);\n+\n+\tstdin_packs_add_all_pack_entries(&keys, revs);\n \n \tstring_list_clear(&keys, 0);\n }\n-- \n2.55.0.rc2.10.g29e31820dce\n\n"},{"id":"546507","messageId":"e3d2e46443d0b32ce29215563dde04ebcf850679.1782500507.git.me@ttaylorr.com","threadId":"65876","inReplyTo":"cover.1782500507.git.me@ttaylorr.com","subject":"[RFC PATCH 08/10] pack-objects: introduce '--stdin-packs=follow-reachable'","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-06-26T19:02:37Z","receivedAt":"2026-06-26T19:02:39Z","isPatch":true,"body":"Introduce a new '--stdin-packs=follow-reachable' mode. Like\n'--stdin-packs=follow', this mode recognizes the '!' (excluded-open)\npack prefix and halts at '^' (excluded-closed) packs.\n\nUnlike 'follow', which eagerly includes all objects from listed packs\nand then walks reachability to rescue additional objects, the new\n'follow-reachable' mode uses reference tips as its traversal starting\npoints and only includes objects that are both reachable AND belong to\nan included pack (or are reachable from a commit or tag in one):\n\n - Objects in included packs: added to the output if reachable.\n\n - Objects reachable from included-pack commits but in unknown packs:\n   added to the output (rescued).\n\n - Objects in excluded-open ('!') packs: not included, but the traversal\n   continues through them.\n\n - Objects in excluded-closed ('^') packs: not included, and the\n   traversal halts.\n\nThe implementation uses a two-phase approach:\n\n 1. In the first phase, commits and tags in included packs (and loose,\n    when --unpacked is given) are marked with a flag bit\n    (IN_INCLUDED_PACK). A commit-only walk from ref tips then identifies\n    which marked objects are reachable, halting at excluded-closed\n    packs.\n\n 2. In the second phase, every reachable marked object (from the\n    previous step) becomes a tip for a full object traversal whose\n    `show_object_pack_hint()` and `show_commit_pack_hint()` callbacks\n    add discovered objects (obeying the usual constraints imposed by\n    `want_object_in_pack()`).\n\nWhen '--unpacked' is given, reachable loose objects are included in the\noutput while unreachable loose objects are left alone. This is achieved\nby marking loose commits and tags with IN_INCLUDED_PACK during the first\nphase, so the pre-walk discovers them naturally.\n\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\n---\n Documentation/git-pack-objects.adoc |  17 +++\n builtin/pack-objects.c              | 185 +++++++++++++++++++++++--\n t/t5331-pack-objects-stdin.sh       | 201 ++++++++++++++++++++++++++++\n 3 files changed, 393 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex 8a27aa19fd3..d7b2e39e76c 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -113,6 +113,23 @@ This mode is useful, for example, to resurrect once-unreachable\n objects found in cruft packs to generate packs which are closed under\n reachability up to the boundary set by the excluded packs.\n +\n+When `mode` is \"follow-reachable\", the same pack prefixes are recognized\n+as in \"follow\" (`!` for excluded-open, `^` for excluded-closed). However,\n+instead of including all objects from included packs, only objects that\n+are reachable from reference tips AND belong to an included pack (or are\n+reachable from a commit in one) are included. Objects in excluded-open\n+packs are traversed but not included; objects in excluded-closed packs\n+halt the traversal.\n++\n+This mode is designed for geometric repacking with cruft packs, where\n+the output pack should contain only reachable objects so that unreachable\n+ones can be collected separately.\n++\n+When `--unpacked` is given alongside `--stdin-packs=follow-reachable`,\n+reachable loose objects are also included in the output pack, while\n+unreachable loose objects are left alone. This includes both loose\n+commits and annotated tag objects.\n++\n Incompatible with `--revs`, or options that imply `--revs` (such as\n `--all`), with the exception of `--unpacked`, which is compatible.\n \ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex 29e43abb51e..5d96757b645 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -290,6 +290,7 @@ enum stdin_packs_mode {\n \tSTDIN_PACKS_MODE_NONE,\n \tSTDIN_PACKS_MODE_STANDARD,\n \tSTDIN_PACKS_MODE_FOLLOW,\n+\tSTDIN_PACKS_MODE_FOLLOW_REACHABLE,\n };\n \n /**\n@@ -3835,7 +3836,8 @@ static void show_object_pack_hint(struct object *object, const char *name,\n \t\t\t\t  void *data)\n {\n \tenum stdin_packs_mode mode = *(enum stdin_packs_mode *)data;\n-\tif (mode == STDIN_PACKS_MODE_FOLLOW) {\n+\tif (mode == STDIN_PACKS_MODE_FOLLOW ||\n+\t    mode == STDIN_PACKS_MODE_FOLLOW_REACHABLE) {\n \t\tif (object->type == OBJ_BLOB &&\n \t\t    !odb_has_object(the_repository->objects, &object->oid, 0))\n \t\t\treturn;\n@@ -3866,7 +3868,8 @@ static void show_commit_pack_hint(struct commit *commit, void *data)\n {\n \tenum stdin_packs_mode mode = *(enum stdin_packs_mode *)data;\n \n-\tif (mode == STDIN_PACKS_MODE_FOLLOW) {\n+\tif (mode == STDIN_PACKS_MODE_FOLLOW ||\n+\t    mode == STDIN_PACKS_MODE_FOLLOW_REACHABLE) {\n \t\tshow_object_pack_hint((struct object *)commit, \"\", data);\n \t\treturn;\n \t}\n@@ -3933,6 +3936,156 @@ static int stdin_packs_include_check(struct commit *commit, void *data)\n \treturn stdin_packs_include_check_obj((struct object *)commit, data);\n }\n \n+/*\n+ * Flag bit set on commits that belong to an included pack during\n+ * '--stdin-packs=follow-reachable'. Used by the pre-walk to\n+ * identify which reachable commits should be tips for the main\n+ * object traversal.\n+ */\n+#define IN_INCLUDED_PACK (1u<<11)\n+\n+static int mark_included_pack_tip(const struct object_id *oid,\n+\t\t\t\t  struct packed_git *p,\n+\t\t\t\t  uint32_t pos,\n+\t\t\t\t  void *data)\n+{\n+\tstruct rev_info *main_revs = data;\n+\toff_t ofs = nth_packed_object_offset(p, pos);\n+\tenum object_type type;\n+\tstruct object_info oi = OBJECT_INFO_INIT;\n+\tstruct object *obj;\n+\n+\toi.typep = &type;\n+\tif (packed_object_info(p, ofs, &oi) < 0)\n+\t\treturn 0;\n+\tif (type != OBJ_COMMIT && type != OBJ_TAG)\n+\t\treturn 0;\n+\n+\tobj = parse_object(the_repository, oid);\n+\tif (!obj)\n+\t\treturn 0;\n+\n+\tobj->flags |= IN_INCLUDED_PACK;\n+\n+\tif (type == OBJ_TAG && main_revs)\n+\t\tadd_pending_object(main_revs, obj, \"\");\n+\treturn 0;\n+}\n+\n+static int mark_loose_object_tip(const struct object_id *oid,\n+\t\t\t\t struct object_info *oi UNUSED,\n+\t\t\t\t void *data)\n+{\n+\tstruct rev_info *main_revs = data;\n+\tstruct object *obj;\n+\tenum object_type type;\n+\n+\ttype = odb_read_object_info(the_repository->objects, oid, NULL);\n+\tif (type != OBJ_COMMIT && type != OBJ_TAG)\n+\t\treturn 0;\n+\n+\tobj = parse_object(the_repository, oid);\n+\tif (!obj)\n+\t\treturn 0;\n+\n+\tobj->flags |= IN_INCLUDED_PACK;\n+\n+\tif (type == OBJ_TAG && main_revs)\n+\t\tadd_pending_object(main_revs, obj, \"\");\n+\n+\treturn 0;\n+}\n+\n+static int add_ref_to_pending(const struct reference *ref, void *cb_data)\n+{\n+\tstruct rev_info *revs = cb_data;\n+\tstruct object *object;\n+\n+\tobject = parse_object(the_repository, ref->oid);\n+\tif (!object)\n+\t\treturn 0;\n+\n+\tadd_pending_object(revs, object, \"\");\n+\treturn 0;\n+}\n+\n+static void stdin_packs_add_reachable_pack_entries(struct string_list *keys,\n+\t\t\t\t\t\t   struct rev_info *revs,\n+\t\t\t\t\t\t   int rev_list_unpacked)\n+{\n+\tstruct rev_info pre_walk;\n+\tstruct commit *commit;\n+\tstruct string_list_item *item;\n+\n+\t/*\n+\t * Phase 1: mark commits in included packs, then walk from\n+\t * ref tips to discover which of them are reachable. The walk\n+\t * halts at excluded-closed packs (via no_kept_objects) and\n+\t * continues through excluded-open ones.\n+\t *\n+\t * Also set include_check on the outer revs so that phase 2\n+\t * (the main object traversal) halts at closed packs.\n+\t */\n+\trevs->include_check = stdin_packs_include_check;\n+\trevs->include_check_obj = stdin_packs_include_check_obj;\n+\n+\tfor_each_string_list_item(item, keys) {\n+\t\tstruct stdin_pack_info *info = item->util;\n+\t\tif (info->kind & STDIN_PACK_INCLUDE)\n+\t\t\tfor_each_object_in_pack(info->p,\n+\t\t\t\t\t\tmark_included_pack_tip,\n+\t\t\t\t\t\trevs,\n+\t\t\t\t\t\tODB_FOR_EACH_OBJECT_PACK_ORDER);\n+\t}\n+\n+\tif (rev_list_unpacked) {\n+\t\t/*\n+\t\t * With '--stdin-packs=follow-reachable', specifying\n+\t\t * '--unpacked' instructs pack-objects to pack any loose\n+\t\t * objects which are reachable.\n+\t\t *\n+\t\t * Pretend as if all loose objects are in an included\n+\t\t * pack in order to make them eligible for packing.\n+\t\t */\n+\t\tstruct odb_source *source = revs->repo->objects->sources;\n+\t\tfor (; source; source = source->next) {\n+\t\t\tstruct odb_source_files *files = odb_source_files_downcast(source);\n+\t\t\tstruct odb_for_each_object_options opts = { 0 };\n+\t\t\tif (local)\n+\t\t\t\topts.flags |= ODB_FOR_EACH_OBJECT_LOCAL_ONLY;\n+\n+\t\t\todb_source_for_each_object(&files->loose->base, NULL,\n+\t\t\t\t\t\t   mark_loose_object_tip,\n+\t\t\t\t\t\t   revs, &opts);\n+\t\t}\n+\t}\n+\n+\trepo_init_revisions(the_repository, &pre_walk, NULL);\n+\tpre_walk.no_kept_objects = 1;\n+\tpre_walk.keep_pack_cache_flags |= KEPT_PACK_IN_CORE;\n+\tpre_walk.ignore_missing_links = 1;\n+\n+\trefs_for_each_ref(get_main_ref_store(the_repository),\n+\t\t\t  add_ref_to_pending, &pre_walk);\n+\n+\tif (prepare_revision_walk(&pre_walk))\n+\t\tdie(_(\"revision walk setup failed\"));\n+\n+\t/*\n+\t * Phase 2 tips: every reachable commit that is in an\n+\t * included pack becomes a starting point for the main\n+\t * object traversal.\n+\t */\n+\twhile ((commit = get_revision(&pre_walk)) != NULL) {\n+\t\tif (commit->object.flags & IN_INCLUDED_PACK)\n+\t\t\tadd_pending_oid(revs, NULL,\n+\t\t\t\t\t&commit->object.oid, 0);\n+\t}\n+\n+\treset_revision_walk();\n+\trelease_revisions(&pre_walk);\n+}\n+\n static void stdin_packs_add_all_pack_entries(struct string_list *keys,\n \t\t\t\t\t     struct rev_info *revs)\n {\n@@ -3962,7 +4115,9 @@ static void stdin_packs_add_all_pack_entries(struct string_list *keys,\n }\n \n static void stdin_packs_add_pack_entries(struct strmap *packs,\n-\t\t\t\t\t struct rev_info *revs)\n+\t\t\t\t\t struct rev_info *revs,\n+\t\t\t\t\t enum stdin_packs_mode mode,\n+\t\t\t\t\t int rev_list_unpacked)\n {\n \tstruct string_list keys = STRING_LIST_INIT_NODUP;\n \tstruct hashmap_iter iter;\n@@ -3983,13 +4138,18 @@ static void stdin_packs_add_pack_entries(struct strmap *packs,\n \t */\n \tQSORT(keys.items, keys.nr, pack_mtime_cmp);\n \n-\tstdin_packs_add_all_pack_entries(&keys, revs);\n+\tif (mode == STDIN_PACKS_MODE_FOLLOW_REACHABLE)\n+\t\tstdin_packs_add_reachable_pack_entries(&keys, revs,\n+\t\t\t\t\t\t       rev_list_unpacked);\n+\telse\n+\t\tstdin_packs_add_all_pack_entries(&keys, revs);\n \n \tstring_list_clear(&keys, 0);\n }\n \n static void stdin_packs_read_input(struct rev_info *revs,\n-\t\t\t\t   enum stdin_packs_mode mode)\n+\t\t\t\t   enum stdin_packs_mode mode,\n+\t\t\t\t   int rev_list_unpacked)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n \tstruct strmap packs = STRMAP_INIT;\n@@ -4004,7 +4164,9 @@ static void stdin_packs_read_input(struct rev_info *revs,\n \t\t\tcontinue;\n \t\telse if (*key == '^')\n \t\t\tkind = STDIN_PACK_EXCLUDE_CLOSED;\n-\t\telse if (*key == '!' && mode == STDIN_PACKS_MODE_FOLLOW)\n+\t\telse if (*key == '!' &&\n+\t\t\t (mode == STDIN_PACKS_MODE_FOLLOW ||\n+\t\t\t  mode == STDIN_PACKS_MODE_FOLLOW_REACHABLE))\n \t\t\tkind = STDIN_PACK_EXCLUDE_OPEN;\n \n \t\tif (kind != STDIN_PACK_INCLUDE)\n@@ -4069,7 +4231,7 @@ static void stdin_packs_read_input(struct rev_info *revs,\n \t\tinfo->p = p;\n \t}\n \n-\tstdin_packs_add_pack_entries(&packs, revs);\n+\tstdin_packs_add_pack_entries(&packs, revs, mode, rev_list_unpacked);\n \n \tstrbuf_release(&buf);\n \tstrmap_clear(&packs, 1);\n@@ -4109,7 +4271,8 @@ static void read_stdin_packs(enum stdin_packs_mode mode, int rev_list_unpacked)\n \n \t/* avoids adding objects in excluded packs */\n \tignore_packed_keep_in_core = 1;\n-\tif (mode == STDIN_PACKS_MODE_FOLLOW) {\n+\tif (mode == STDIN_PACKS_MODE_FOLLOW ||\n+\t    mode == STDIN_PACKS_MODE_FOLLOW_REACHABLE) {\n \t\t/*\n \t\t * In '--stdin-packs=follow' mode, additionally ignore\n \t\t * objects in excluded-open packs to prevent them from\n@@ -4117,8 +4280,8 @@ static void read_stdin_packs(enum stdin_packs_mode mode, int rev_list_unpacked)\n \t\t */\n \t\tignore_packed_keep_in_core_open = 1;\n \t}\n-\tstdin_packs_read_input(&revs, mode);\n-\tif (rev_list_unpacked)\n+\tstdin_packs_read_input(&revs, mode, rev_list_unpacked);\n+\tif (rev_list_unpacked && mode != STDIN_PACKS_MODE_FOLLOW_REACHABLE)\n \t\tadd_unreachable_loose_objects(&revs);\n \n \tif (prepare_revision_walk(&revs))\n@@ -5027,6 +5190,8 @@ static int parse_stdin_packs_mode(const struct option *opt, const char *arg,\n \t\t*mode = STDIN_PACKS_MODE_STANDARD;\n \telse if (!strcmp(arg, \"follow\"))\n \t\t*mode = STDIN_PACKS_MODE_FOLLOW;\n+\telse if (!strcmp(arg, \"follow-reachable\"))\n+\t\t*mode = STDIN_PACKS_MODE_FOLLOW_REACHABLE;\n \telse\n \t\tdie(_(\"invalid value for '%s': '%s'\"), opt->long_name, arg);\n \ndiff --git a/t/t5331-pack-objects-stdin.sh b/t/t5331-pack-objects-stdin.sh\nindex c74b5861af3..443d855291a 100755\n--- a/t/t5331-pack-objects-stdin.sh\n+++ b/t/t5331-pack-objects-stdin.sh\n@@ -520,4 +520,205 @@ test_expect_success '--stdin-packs with !-delimited pack without follow' '\n \t)\n '\n \n+test_expect_success '--stdin-packs=follow-reachable excludes unreachable objects' '\n+\ttest_when_finished \"rm -fr repo\" &&\n+\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tgit config set maintenance.auto false &&\n+\n+\t\tgit branch -M main &&\n+\n+\t\t# Create the following commit structure:\n+\t\t#\n+\t\t#   A <-- B <-- C     (main)\n+\t\t#         ^\n+\t\t#          \\\n+\t\t#           U         (unreachable, no ref)\n+\t\ttest_commit A &&\n+\t\ttest_commit B &&\n+\t\ttest_commit U &&\n+\t\tU_TIP=\"$(git rev-parse HEAD)\" &&\n+\t\tgit reset --hard HEAD^ &&\n+\t\tgit tag -d U &&\n+\t\tgit reflog expire --all --expire=all &&\n+\n+\t\ttest_commit C &&\n+\n+\t\tA=\"$(echo A | git pack-objects --revs $packdir/pack)\" &&\n+\t\tB=\"$(echo A..B | git pack-objects --revs $packdir/pack)\" &&\n+\t\tC=\"$(echo B..C | git pack-objects --revs $packdir/pack)\" &&\n+\t\tU=\"$(echo \"$U_TIP\" | git pack-objects $packdir/pack)\" &&\n+\n+\t\tgit prune-packed &&\n+\n+\t\t# Include packs A and C, exclude B as open (since B\n+\t\t# may not have closure), leave U as unknown.\n+\t\t#\n+\t\t# With follow-reachable:\n+\t\t#  - objects from A and C are included (reachable from\n+\t\t#    main, through excluded-open B, and in included\n+\t\t#    packs)\n+\t\t#  - objects from B are excluded (excluded-open)\n+\t\t#  - objects from U are NOT included (not reachable\n+\t\t#    from any ref, even though the pack exists)\n+\t\tP=$(git pack-objects --stdin-packs=follow-reachable \\\n+\t\t\t$packdir/pack <<-EOF\n+\t\tpack-$A.pack\n+\t\t!pack-$B.pack\n+\t\tpack-$C.pack\n+\t\tEOF\n+\t\t) &&\n+\n+\t\tobjects_in_packs $A $C >expect &&\n+\t\tobjects_in_packs $P >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '--stdin-packs=follow-reachable with open-excluded packs' '\n+\ttest_when_finished \"rm -fr repo\" &&\n+\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tgit config set maintenance.auto false &&\n+\n+\t\tgit branch -M main &&\n+\n+\t\t# Create the following commit structure:\n+\t\t#\n+\t\t#   A <-- B <-- C <-- D    (main)\n+\t\t#\n+\t\t# Pack each commit separately, then use follow-reachable\n+\t\t# with B excluded-open and A excluded-closed. Since B is\n+\t\t# open, the traversal continues through it, but since A\n+\t\t# is closed, it halts there.\n+\t\ttest_commit A &&\n+\t\ttest_commit B &&\n+\t\ttest_commit C &&\n+\t\ttest_commit D &&\n+\n+\t\tA=\"$(echo A | git pack-objects --revs $packdir/pack)\" &&\n+\t\tB=\"$(echo A..B | git pack-objects --revs $packdir/pack)\" &&\n+\t\tC=\"$(echo B..C | git pack-objects --revs $packdir/pack)\" &&\n+\t\tD=\"$(echo C..D | git pack-objects --revs $packdir/pack)\" &&\n+\n+\t\tgit prune-packed &&\n+\n+\t\t# Include C and D, B excluded-open, A excluded-closed.\n+\t\t#\n+\t\t# The traversal starts at main (D), walks:\n+\t\t#  D (included) -> C (included) -> B (open, continue\n+\t\t#  but do not include) -> A (closed, halt).\n+\t\t#\n+\t\t# Objects from C and D are in the output (reachable,\n+\t\t# included). B.t is also rescued (reachable via\n+\t\t# C^{tree} or similar). A and its objects are NOT\n+\t\t# (behind the closed boundary).\n+\t\tP=$(git pack-objects --stdin-packs=follow-reachable \\\n+\t\t\t$packdir/pack <<-EOF\n+\t\tpack-$C.pack\n+\t\tpack-$D.pack\n+\t\t!pack-$B.pack\n+\t\t^pack-$A.pack\n+\t\tEOF\n+\t\t) &&\n+\n+\t\tobjects_in_packs $C $D >expect &&\n+\t\tobjects_in_packs $P >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '--stdin-packs=follow-reachable with --unpacked and loose objects' '\n+\ttest_when_finished \"rm -fr repo\" &&\n+\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tgit config set maintenance.auto false &&\n+\n+\t\tgit branch -M main &&\n+\n+\t\ttest_commit A &&\n+\t\ttest_commit B &&\n+\n+\t\tA=\"$(echo A | git pack-objects --revs $packdir/pack)\" &&\n+\t\tB=\"$(echo A..B | git pack-objects --revs $packdir/pack)\" &&\n+\n+\t\tgit prune-packed &&\n+\n+\t\t# Create a reachable loose commit on top of B.\n+\t\ttest_commit C &&\n+\n+\t\t# Create an unreachable loose object.\n+\t\tunreachable=\"$(echo \"unreachable\" | git hash-object -w --stdin)\" &&\n+\n+\t\t# Include A and B, no excluded packs. With --unpacked,\n+\t\t# the reachable loose objects from C should be included\n+\t\t# in the output but the unreachable blob should not.\n+\t\tP=$(git pack-objects --stdin-packs=follow-reachable \\\n+\t\t\t--unpacked $packdir/pack <<-EOF\n+\t\tpack-$A.pack\n+\t\tpack-$B.pack\n+\t\tEOF\n+\t\t) &&\n+\n+\t\t# The output should contain objects from A, B, and C.\n+\t\t{\n+\t\t\tobjects_in_packs $A $B &&\n+\t\t\tgit rev-list --objects --no-object-names B..C\n+\t\t} >expect.raw &&\n+\t\tsort expect.raw >expect &&\n+\n+\t\tobjects_in_packs $P >actual &&\n+\n+\t\t# The unreachable blob should NOT be in the output.\n+\t\t! grep $unreachable actual &&\n+\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '--stdin-packs=follow-reachable with --unpacked and loose annotated tag' '\n+\ttest_when_finished \"rm -fr repo\" &&\n+\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\tgit config set maintenance.auto false &&\n+\n+\t\tgit branch -M main &&\n+\n+\t\ttest_commit A &&\n+\n+\t\tA=\"$(echo A | git pack-objects --revs $packdir/pack)\" &&\n+\n+\t\tgit prune-packed &&\n+\n+\t\t# Create a loose annotated tag pointing at A.\n+\t\tgit tag -a -m \"annotated\" annotated-tag A &&\n+\t\ttag_oid=\"$(git rev-parse annotated-tag)\" &&\n+\n+\t\tP=$(git pack-objects --stdin-packs=follow-reachable \\\n+\t\t\t--unpacked $packdir/pack <<-EOF\n+\t\tpack-$A.pack\n+\t\tEOF\n+\t\t) &&\n+\n+\t\t# The output should contain objects from A plus the\n+\t\t# loose annotated tag object.\n+\t\t{\n+\t\t\tobjects_in_packs $A &&\n+\t\t\techo $tag_oid\n+\t\t} >expect.raw &&\n+\t\tsort expect.raw >expect &&\n+\n+\t\tobjects_in_packs $P >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n test_done\n-- \n2.55.0.rc2.10.g29e31820dce\n\n"},{"id":"546508","messageId":"c9efbad60c773f0def850ecf107ddfaeb2860a16.1782500507.git.me@ttaylorr.com","threadId":"65876","inReplyTo":"cover.1782500507.git.me@ttaylorr.com","subject":"[RFC PATCH 09/10] pack-objects: support '--refs-snapshot' with 'follow-reachable'","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-06-26T19:02:40Z","receivedAt":"2026-06-26T19:02:42Z","isPatch":true,"body":"The '--stdin-packs=follow-reachable' mode walks from reference tips to\ndetermine which objects in included packs are reachable. Without a\nsnapshot, pack-objects discovers refs by iterating live references,\nwhich may change between the time the repack writes the geometric pack\nand the time it writes the MIDX bitmap.\n\nIf a reference is updated during that window, the set of reachable\nobjects seen by pack-objects may differ from the set seen by the MIDX\nbitmap writer. This can cause reachable objects to end up in the cruft\npack (because pack-objects did not see the reference that makes them\nreachable) rather than the geometric pack. While this does not cause\ndata loss, it has two undesirable consequences:\n\n - Reachable objects in the cruft pack cannot receive bitmap coverage\n   (since the cruft pack may be excluded from the MIDX when\n   'repack.midxMustContainCruft' is false).\n\n - Serving fetches that need those objects requires loading the cruft\n   pack, which may contain many unrelated unreachable objects.\n\nTo avoid this, teach pack-objects to accept '--refs-snapshot=<path>'\nwhen used with '--stdin-packs=follow-reachable'. The snapshot file uses\nthe same format as the MIDX bitmap writer: one hex OID per line, with\nan optional '+' prefix for preferred bitmap commits.\n\n'pack-objects' happily ignores the '+' prefix for indicating preferred\nbitmap commits as a convenience, so that the ref-snapshot can be shared\nbetween the MIDX generation machinery and 'pack-objects'.\n\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\n---\n Documentation/git-pack-objects.adoc |  8 +++++\n builtin/pack-objects.c              | 46 +++++++++++++++++++++++++++--\n 2 files changed, 52 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex d7b2e39e76c..4ebe407cfaf 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -133,6 +133,14 @@ commits and annotated tag objects.\n Incompatible with `--revs`, or options that imply `--revs` (such as\n `--all`), with the exception of `--unpacked`, which is compatible.\n \n+--refs-snapshot=<path>::\n+\tWhen used with `--stdin-packs=follow-reachable`, read reference\n+\ttips from `<path>` instead of iterating live references. The file\n+\tformat is one hex object ID per line, with an optional `+` prefix\n+\t(for preferred bitmap commits). This ensures a consistent view of\n+\treferences when the same snapshot is shared with other tools (e.g.,\n+\tthe MIDX bitmap writer).\n+\n --cruft::\n \tPacks unreachable objects into a separate \"cruft\" pack, denoted\n \tby the existence of a `.mtimes` file. Typically used by `git\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex 5d96757b645..082ff760abc 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -219,6 +219,7 @@ static int incremental;\n static int ignore_packed_keep_on_disk;\n static int ignore_packed_keep_in_core;\n static int ignore_packed_keep_in_core_open;\n+static const char *stdin_packs_refs_snapshot;\n static int ignore_packed_keep_in_core_has_cruft;\n static int allow_ofs_delta;\n static struct pack_idx_option pack_idx_opts;\n@@ -4009,6 +4010,38 @@ static int add_ref_to_pending(const struct reference *ref, void *cb_data)\n \treturn 0;\n }\n \n+static void read_refs_snapshot(const char *refs_snapshot,\n+\t\t\t      struct rev_info *revs)\n+{\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tstruct object_id oid;\n+\tFILE *f = xfopen(refs_snapshot, \"r\");\n+\n+\twhile (strbuf_getline(&buf, f) != EOF) {\n+\t\tstruct object *object;\n+\t\tconst char *hex = buf.buf;\n+\t\tconst char *end = NULL;\n+\n+\t\tif (*hex == '+')\n+\t\t\thex++;\n+\n+\t\tif (parse_oid_hex_algop(hex, &oid, &end,\n+\t\t\t\t\tthe_repository->hash_algo) < 0)\n+\t\t\tdie(_(\"could not parse line: %s\"), buf.buf);\n+\t\tif (*end)\n+\t\t\tdie(_(\"malformed line: %s\"), buf.buf);\n+\n+\t\tobject = parse_object(the_repository, &oid);\n+\t\tif (!object)\n+\t\t\tcontinue;\n+\n+\t\tadd_pending_object(revs, object, \"\");\n+\t}\n+\n+\tfclose(f);\n+\tstrbuf_release(&buf);\n+}\n+\n static void stdin_packs_add_reachable_pack_entries(struct string_list *keys,\n \t\t\t\t\t\t   struct rev_info *revs,\n \t\t\t\t\t\t   int rev_list_unpacked)\n@@ -4065,8 +4098,11 @@ static void stdin_packs_add_reachable_pack_entries(struct string_list *keys,\n \tpre_walk.keep_pack_cache_flags |= KEPT_PACK_IN_CORE;\n \tpre_walk.ignore_missing_links = 1;\n \n-\trefs_for_each_ref(get_main_ref_store(the_repository),\n-\t\t\t  add_ref_to_pending, &pre_walk);\n+\tif (stdin_packs_refs_snapshot)\n+\t\tread_refs_snapshot(stdin_packs_refs_snapshot, &pre_walk);\n+\telse\n+\t\trefs_for_each_ref(get_main_ref_store(the_repository),\n+\t\t\t\t  add_ref_to_pending, &pre_walk);\n \n \tif (prepare_revision_walk(&pre_walk))\n \t\tdie(_(\"revision walk setup failed\"));\n@@ -5267,6 +5303,8 @@ int cmd_pack_objects(int argc,\n \t\tOPT_CALLBACK_F(0, \"stdin-packs\", &stdin_packs, N_(\"mode\"),\n \t\t\t     N_(\"read packs from stdin\"),\n \t\t\t     PARSE_OPT_OPTARG, parse_stdin_packs_mode),\n+\t\tOPT_FILENAME(0, \"refs-snapshot\", &stdin_packs_refs_snapshot,\n+\t\t\t     N_(\"refs snapshot for follow-reachable traversal\")),\n \t\tOPT_BOOL(0, \"stdout\", &pack_to_stdout,\n \t\t\t N_(\"output pack to stdout\")),\n \t\tOPT_BOOL(0, \"include-tag\", &include_tag,\n@@ -5484,6 +5522,10 @@ int cmd_pack_objects(int argc,\n \tif (stdin_packs && use_internal_rev_list)\n \t\tdie(_(\"cannot use internal rev list with --stdin-packs\"));\n \n+\tif (stdin_packs_refs_snapshot &&\n+\t    stdin_packs != STDIN_PACKS_MODE_FOLLOW_REACHABLE)\n+\t\tdie(_(\"--refs-snapshot can only be used with --stdin-packs=follow-reachable\"));\n+\n \tif (cruft) {\n \t\tif (use_internal_rev_list)\n \t\t\tdie(_(\"cannot use internal rev list with --cruft\"));\n-- \n2.55.0.rc2.10.g29e31820dce\n\n"},{"id":"546509","messageId":"488ccd62aab9a059f82643a6ca46cd78f5b83a7e.1782500507.git.me@ttaylorr.com","threadId":"65876","inReplyTo":"cover.1782500507.git.me@ttaylorr.com","subject":"[RFC PATCH 10/10] repack: support combining '--geometric' with '--cruft'","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-06-26T19:02:43Z","receivedAt":"2026-06-26T19:02:46Z","isPatch":true,"body":"Teach 'git repack' to accept '--geometric' and '--cruft' together. When\nboth are given, the geometric repack rolls up non-cruft packs as usual,\nand a separate cruft pack is written to collect unreachable objects.\n\nPreviously, '--cruft' implied `ALL_INTO_ONE`, which is fundamentally\nincompatible with geometric repacking. Relax this so that '--cruft' only\nimplies `ALL_INTO_ONE` when '--geometric' is not also given.\n\nWhen combining the two modes:\n\n - Use the new '--stdin-packs=follow-reachable' mode so that only\n   reachable objects from the rolled-up packs (and any reachable loose\n   objects) appear in the geometric pack. Unreachable objects are left\n   for the cruft writer to collect.\n\n - Plumb our `pack_geometry` into `write_cruft_pack()`, so that the\n   latter can tell 'pack-objects' which non-kept packs are below the\n   split (excluded, so their unreachable objects are candidates for the\n   cruft pack) versus above the split (included, so they are treated as\n   reachable).\n\n - Handle promisor packs in the cruft writer's geometry path, since\n   promisor packs have their own split point.\n\n - Use the refs snapshot (when available) so that pack-objects and the\n   MIDX bitmap writer see the same set of reference tips.\n\nSigned-off-by: Taylor Blau <me@ttaylorr.com>\n---\n Documentation/git-repack.adoc |  11 ++\n builtin/repack.c              |  23 +++-\n repack-cruft.c                |  23 +++-\n repack.h                      |   3 +-\n t/t7704-repack-cruft.sh       | 251 ++++++++++++++++++++++++++++++++++\n 5 files changed, 300 insertions(+), 11 deletions(-)\n\ndiff --git a/Documentation/git-repack.adoc b/Documentation/git-repack.adoc\nindex 72c42015e23..e9df7713278 100644\n--- a/Documentation/git-repack.adoc\n+++ b/Documentation/git-repack.adoc\n@@ -70,6 +70,11 @@ to the new separate pack will be written.\n \tare packed into a separate cruft pack. Unreachable objects can\n \tbe pruned using the normal expiry rules with the next `git gc`\n \tinvocation (see linkgit:git-gc[1]). Incompatible with `-k`.\n++\n+When combined with `--geometric`, `--cruft` does not imply `-a`. Instead,\n+the geometric repack rolls up packs as usual, and a separate cruft pack is\n+written to collect unreachable objects. Only reachable objects from the\n+rolled-up packs are included in the resulting geometric pack.\n \n --cruft-expiration=<approxidate>::\n \tExpire unreachable objects older than `<approxidate>`\n@@ -245,6 +250,12 @@ progression.\n Loose objects are implicitly included in this \"roll-up\", without respect to\n their reachability. This is subject to change in the future.\n +\n+When combined with `--cruft`, only reachable objects from rolled-up packs\n+are included in the geometric pack, along with any reachable loose objects.\n+Unreachable objects (both from rolled-up packs and loose) are collected\n+into a separate cruft pack. Existing cruft packs are retained. See\n+`--cruft` above for details.\n++\n When writing a multi-pack bitmap, `git repack` selects the largest resulting\n pack as the preferred pack for object selection by the MIDX (see\n linkgit:git-multi-pack-index[1]).\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex dfb6fed231d..165cfff75cd 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -260,7 +260,7 @@ int cmd_repack(int argc,\n \t\t\t\t  keep_unreachable, \"-k/--keep-unreachable\",\n \t\t\t\t  pack_everything & PACK_CRUFT, \"--cruft\");\n \n-\tif (pack_everything & PACK_CRUFT)\n+\tif (pack_everything & PACK_CRUFT && !geometry.split_factor)\n \t\tpack_everything |= ALL_INTO_ONE;\n \n \tif (write_bitmaps < 0) {\n@@ -296,7 +296,8 @@ int cmd_repack(int argc,\n \t\tdie(_(\"invalid value for %s: %d\"), \"--midx-new-layer-threshold\",\n \t\t    config_ctx.midx_new_layer_threshold);\n \n-\tif (write_midx != REPACK_WRITE_MIDX_NONE && write_bitmaps) {\n+\tif ((write_midx != REPACK_WRITE_MIDX_NONE && write_bitmaps) ||\n+\t    (geometry.split_factor && (pack_everything & PACK_CRUFT))) {\n \t\tstruct strbuf path = STRBUF_INIT;\n \n \t\tstrbuf_addf(&path, \"%s/%s_XXXXXX\",\n@@ -317,7 +318,7 @@ int cmd_repack(int argc,\n \texisting_packs_collect(&existing, &keep_pack_list);\n \n \tif (geometry.split_factor) {\n-\t\tif (pack_everything)\n+\t\tif (pack_everything & ~PACK_CRUFT)\n \t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--geometric\", \"-A/-a\");\n \t\tif (write_midx == REPACK_WRITE_MIDX_INCREMENTAL) {\n \t\t\tgeometry.midx_layer_threshold = config_ctx.midx_new_layer_threshold;\n@@ -393,10 +394,16 @@ int cmd_repack(int argc,\n \t\tpack_geometry_repack_promisors(repo, &po_args, &geometry,\n \t\t\t\t\t       &names, packtmp);\n \n-\t\tif (midx_must_contain_cruft)\n+\t\tif (pack_everything & PACK_CRUFT) {\n+\t\t\tstrvec_push(&cmd.args, \"--stdin-packs=follow-reachable\");\n+\t\t\tif (refs_snapshot)\n+\t\t\t\tstrvec_pushf(&cmd.args, \"--refs-snapshot=%s\",\n+\t\t\t\t\t     get_tempfile_path(refs_snapshot));\n+\t\t} else if (midx_must_contain_cruft)\n \t\t\tstrvec_push(&cmd.args, \"--stdin-packs\");\n \t\telse\n \t\t\tstrvec_push(&cmd.args, \"--stdin-packs=follow\");\n+\n \t\tstrvec_push(&cmd.args, \"--unpacked\");\n \t} else {\n \t\tstrvec_push(&cmd.args, \"--unpacked\");\n@@ -431,7 +438,8 @@ int cmd_repack(int argc,\n \t\t\tconst char *basename = pack_basename(geometry.pack[i]);\n \t\t\tchar marker = '^';\n \n-\t\t\tif (!midx_must_contain_cruft &&\n+\t\t\tif ((pack_everything & PACK_CRUFT ||\n+\t\t\t     !midx_must_contain_cruft) &&\n \t\t\t    !string_list_has_string(&existing.midx_packs,\n \t\t\t\t\t\t    basename)) {\n \t\t\t\t/*\n@@ -505,7 +513,8 @@ int cmd_repack(int argc,\n \n \t\tret = write_cruft_pack(&opts, cruft_expiration,\n \t\t\t\t       combine_cruft_below_size, &names,\n-\t\t\t\t       &existing);\n+\t\t\t\t       &existing,\n+\t\t\t\t       geometry.split_factor ? &geometry : NULL);\n \t\tif (ret)\n \t\t\tgoto cleanup;\n \n@@ -540,7 +549,7 @@ int cmd_repack(int argc,\n \t\t\t */\n \t\t\topts.destination = expire_to;\n \t\t\tret = write_cruft_pack(&opts, NULL, 0ul, &names,\n-\t\t\t\t\t       &existing);\n+\t\t\t\t\t       &existing, NULL);\n \t\t\tif (ret)\n \t\t\t\tgoto cleanup;\n \t\t}\ndiff --git a/repack-cruft.c b/repack-cruft.c\nindex 6a040e98017..6c553bbb0b5 100644\n--- a/repack-cruft.c\n+++ b/repack-cruft.c\n@@ -36,7 +36,8 @@ int write_cruft_pack(const struct write_pack_opts *opts,\n \t\t     const char *cruft_expiration,\n \t\t     unsigned long combine_cruft_below_size,\n \t\t     struct string_list *names,\n-\t\t     struct existing_packs *existing)\n+\t\t     struct existing_packs *existing,\n+\t\t     struct pack_geometry *geometry)\n {\n \tstruct child_process cmd = CHILD_PROCESS_INIT;\n \tstruct string_list_item *item;\n@@ -81,8 +82,24 @@ int write_cruft_pack(const struct write_pack_opts *opts,\n \telse\n \t\tfor_each_string_list_item(item, &existing->cruft_packs)\n \t\t\tfprintf(in, \"-%s.pack\\n\", item->string);\n-\tfor_each_string_list_item(item, &existing->non_kept_packs)\n-\t\tfprintf(in, \"-%s.pack\\n\", item->string);\n+\tif (geometry) {\n+\t\tuint32_t j;\n+\t\tfor (j = 0; j < geometry->split; j++)\n+\t\t\tfprintf(in, \"-%s\\n\",\n+\t\t\t\tpack_basename(geometry->pack[j]));\n+\t\tfor (; j < geometry->pack_nr; j++)\n+\t\t\tfprintf(in, \"%s\\n\",\n+\t\t\t\tpack_basename(geometry->pack[j]));\n+\t\tfor (j = 0; j < geometry->promisor_split; j++)\n+\t\t\tfprintf(in, \"-%s\\n\",\n+\t\t\t\tpack_basename(geometry->promisor_pack[j]));\n+\t\tfor (; j < geometry->promisor_pack_nr; j++)\n+\t\t\tfprintf(in, \"%s\\n\",\n+\t\t\t\tpack_basename(geometry->promisor_pack[j]));\n+\t} else {\n+\t\tfor_each_string_list_item(item, &existing->non_kept_packs)\n+\t\t\tfprintf(in, \"-%s.pack\\n\", item->string);\n+\t}\n \tfor_each_string_list_item(item, &existing->kept_packs)\n \t\tfprintf(in, \"%s.pack\\n\", item->string);\n \tfclose(in);\ndiff --git a/repack.h b/repack.h\nindex 4295829cea0..872a503fbd1 100644\n--- a/repack.h\n+++ b/repack.h\n@@ -169,6 +169,7 @@ int write_cruft_pack(const struct write_pack_opts *opts,\n \t\t     const char *cruft_expiration,\n \t\t     unsigned long combine_cruft_below_size,\n \t\t     struct string_list *names,\n-\t\t     struct existing_packs *existing);\n+\t\t     struct existing_packs *existing,\n+\t\t     struct pack_geometry *geometry);\n \n #endif /* REPACK_H */\ndiff --git a/t/t7704-repack-cruft.sh b/t/t7704-repack-cruft.sh\nindex 9e03b04315d..5e2b776e7ba 100755\n--- a/t/t7704-repack-cruft.sh\n+++ b/t/t7704-repack-cruft.sh\n@@ -891,4 +891,255 @@ test_expect_success 'repack rescues once-cruft objects above geometric split' '\n \tgit repack --geometric=2 -d --write-midx --write-bitmap-index\n '\n \n+test_expect_success 'repack --geometric --cruft combines packs and writes cruft' '\n+\tgit init geometric-cruft-basic &&\n+\t(\n+\t\tcd geometric-cruft-basic &&\n+\n+\t\ttest_commit A &&\n+\t\ttest_commit B &&\n+\n+\t\tB=\"$(git rev-parse B)\" &&\n+\n+\t\tgit reset --hard $B^ &&\n+\t\tgit tag -d B &&\n+\t\tgit reflog expire --all --expire=all &&\n+\n+\t\t# Initial state: one non-cruft pack, one cruft pack.\n+\t\tgit repack -d --cruft &&\n+\n+\t\tls $packdir/pack-*.mtimes >cruft.before &&\n+\t\ttest_line_count = 1 cruft.before &&\n+\n+\t\ttest_commit C &&\n+\t\tgit repack &&\n+\n+\t\t# At this point we have three packs:\n+\t\t#   - the non-cruft pack from A\n+\t\t#   - the cruft pack from B\n+\t\t#   - a new non-cruft pack from C\n+\t\t#\n+\t\t# The two non-cruft packs are not in a geometric\n+\t\t# progression, so they should be rolled up.\n+\t\tgit repack -d --geometric=2 --cruft &&\n+\n+\t\t# The old cruft pack for B is retained, since the\n+\t\t# geometric repack does not touch cruft packs.\n+\t\tls $packdir/pack-*.mtimes >cruft.after &&\n+\t\ttest_line_count = 1 cruft.after &&\n+\n+\t\t# Ensure that all reachable objects are present.\n+\t\tgit fsck\n+\t)\n+'\n+\n+test_expect_success 'repack --geometric --cruft writes new cruft for loose unreachable' '\n+\tgit init geometric-cruft-new-cruft &&\n+\t(\n+\t\tcd geometric-cruft-new-cruft &&\n+\n+\t\tgit config set maintenance.auto false &&\n+\n+\t\ttest_commit A &&\n+\t\tgit repack &&\n+\n+\t\ttest_commit B &&\n+\t\tgit repack &&\n+\n+\t\t# Create an unreachable commit whose objects are\n+\t\t# still loose (never packed).\n+\t\ttest_commit C &&\n+\t\tC=\"$(git rev-parse C)\" &&\n+\t\tgit reset --hard $C^ &&\n+\t\tgit tag -d C &&\n+\t\tgit reflog expire --all --expire=all &&\n+\n+\t\t# At this point we have two non-cruft packs of\n+\t\t# similar size that are not in geometric progression,\n+\t\t# and loose unreachable objects from commit C.\n+\t\tls $packdir/pack-*.idx >packs.before &&\n+\t\ttest_line_count = 2 packs.before &&\n+\n+\t\t# Geometric+cruft repack should roll up the two\n+\t\t# non-cruft packs and write a new cruft pack for C\n+\t\t# (whose objects are loose and unreachable).\n+\t\tgit repack -d --geometric=2 --cruft &&\n+\n+\t\tls $packdir/pack-*.mtimes >cruft.after &&\n+\t\ttest_line_count = 1 cruft.after &&\n+\n+\t\tgit fsck\n+\t)\n+'\n+\n+test_expect_success 'repack --geometric --cruft -d deletes rolled-up packs' '\n+\tgit init geometric-cruft-delete &&\n+\t(\n+\t\tcd geometric-cruft-delete &&\n+\n+\t\ttest_commit A &&\n+\t\tgit repack -d &&\n+\n+\t\ttest_commit B &&\n+\t\tgit repack -d &&\n+\n+\t\tls $packdir/pack-*.idx >before &&\n+\n+\t\tgit repack -d --geometric=2 --cruft &&\n+\n+\t\t# Two packs should have been rolled into one. No cruft\n+\t\t# pack is written because there are no unreachable objects.\n+\t\tls $packdir/pack-*.idx >after &&\n+\t\ttest_line_count = 1 after &&\n+\n+\t\t# The rolled-up packs should be gone.\n+\t\t! test_cmp before after\n+\t)\n+'\n+\n+test_expect_success 'repack --geometric --cruft collects loose unreachable objects' '\n+\tgit init geometric-cruft-loose &&\n+\t(\n+\t\tcd geometric-cruft-loose &&\n+\n+\t\ttest_commit A &&\n+\t\tgit repack -d &&\n+\n+\t\ttest_commit B &&\n+\t\tgit repack &&\n+\n+\t\t# Create a loose unreachable object by making it\n+\t\t# orphaned (not in any pack).\n+\t\tloose=\"$(echo \"cruft object\" | git hash-object -w --stdin)\" &&\n+\n+\t\t# We have two non-cruft packs and a loose unreachable\n+\t\t# object. The geometric+cruft repack should roll up\n+\t\t# the packs AND write a cruft pack for the loose\n+\t\t# unreachable object.\n+\t\tgit repack -d --geometric=2 --cruft &&\n+\n+\t\tls $packdir/pack-*.mtimes >cruft.packs &&\n+\t\ttest_line_count = 1 cruft.packs &&\n+\n+\t\tgit fsck\n+\t)\n+'\n+\n+test_expect_success 'repack --geometric --cruft accumulates cruft packs' '\n+\tgit init geometric-cruft-accumulate &&\n+\t(\n+\t\tcd geometric-cruft-accumulate &&\n+\n+\t\tgit config set maintenance.auto false &&\n+\n+\t\ttest_commit A &&\n+\t\tgit repack &&\n+\n+\t\t# First round: create unreachable objects and do a\n+\t\t# geometric+cruft repack.\n+\t\tunreachable_1=\"$(echo \"cruft 1\" | git hash-object -w --stdin)\" &&\n+\t\tgit repack -d --geometric=2 --cruft &&\n+\n+\t\tls $packdir/pack-*.mtimes >cruft.1 &&\n+\t\ttest_line_count = 1 cruft.1 &&\n+\n+\t\ttest_commit B &&\n+\t\tgit repack &&\n+\n+\t\t# Second round: create more unreachable objects and\n+\t\t# repack again. The old cruft pack should be retained\n+\t\t# and a new one written.\n+\t\tunreachable_2=\"$(echo \"cruft 2\" | git hash-object -w --stdin)\" &&\n+\t\tgit repack -d --geometric=2 --cruft &&\n+\n+\t\tls $packdir/pack-*.mtimes >cruft.2 &&\n+\t\ttest_line_count = 2 cruft.2 &&\n+\n+\t\tgit fsck\n+\t)\n+'\n+\n+test_expect_success 'repack --geometric --cruft --combine-cruft-below-size' '\n+\tgit init geometric-cruft-combine &&\n+\t(\n+\t\tcd geometric-cruft-combine &&\n+\n+\t\tgit config set maintenance.auto false &&\n+\n+\t\ttest_commit A &&\n+\t\tgit repack &&\n+\n+\t\t# Create a small cruft pack.\n+\t\tunreachable_1=\"$(echo \"cruft 1\" | git hash-object -w --stdin)\" &&\n+\t\tgit repack -d --geometric=2 --cruft &&\n+\n+\t\tls $packdir/pack-*.mtimes >cruft.before &&\n+\t\ttest_line_count = 1 cruft.before &&\n+\n+\t\ttest_commit B &&\n+\t\tgit repack &&\n+\n+\t\t# Create another small cruft pack.\n+\t\tunreachable_2=\"$(echo \"cruft 2\" | git hash-object -w --stdin)\" &&\n+\t\tgit repack -d --geometric=2 --cruft &&\n+\n+\t\tls $packdir/pack-*.mtimes >cruft.mid &&\n+\t\ttest_line_count = 2 cruft.mid &&\n+\n+\t\ttest_commit C &&\n+\t\tgit repack &&\n+\n+\t\t# With --combine-cruft-below-size, the two small cruft\n+\t\t# packs should be combined into one.\n+\t\tunreachable_3=\"$(echo \"cruft 3\" | git hash-object -w --stdin)\" &&\n+\t\tgit repack -d --geometric=2 --cruft \\\n+\t\t\t--combine-cruft-below-size=10M &&\n+\n+\t\tls $packdir/pack-*.mtimes >cruft.after &&\n+\t\ttest_line_count = 1 cruft.after &&\n+\n+\t\tgit fsck\n+\t)\n+'\n+\n+test_expect_success 'repack --geometric --cruft --expire-to' '\n+\tgit init geometric-cruft-expire-to &&\n+\t(\n+\t\tcd geometric-cruft-expire-to &&\n+\n+\t\tgit config set maintenance.auto false &&\n+\n+\t\ttest_commit A &&\n+\t\tgit repack &&\n+\n+\t\ttest_commit B &&\n+\t\tgit repack &&\n+\n+\t\t# Create unreachable objects and record them.\n+\t\ttest_commit C &&\n+\t\tC=\"$(git rev-parse C)\" &&\n+\t\tgit rev-list --objects --no-object-names B..C >unreachable.raw &&\n+\t\tsort unreachable.raw >unreachable.want &&\n+\n+\t\tgit reset --hard $C^ &&\n+\t\tgit tag -d C &&\n+\t\tgit reflog expire --all --expire=all &&\n+\n+\t\tgit init --bare expired.git &&\n+\t\tgit repack -d --geometric=2 --cruft \\\n+\t\t\t--cruft-expiration=now \\\n+\t\t\t--expire-to=\"expired.git/objects/pack/pack\" &&\n+\n+\t\t# The expired objects should appear in the\n+\t\t# expire-to location.\n+\t\texpired=\"$(ls expired.git/objects/pack/pack-*.idx)\" &&\n+\t\ttest_path_is_file \"${expired%.idx}.mtimes\" &&\n+\t\tgit show-index <\"$expired\" >expired.raw &&\n+\t\tcut -d\" \" -f2 expired.raw | sort >expired.objects &&\n+\t\ttest_cmp unreachable.want expired.objects &&\n+\n+\t\tgit fsck\n+\t)\n+'\n+\n test_done\n-- \n2.55.0.rc2.10.g29e31820dce\n"},{"id":"546515","messageId":"xmqqwlvl56vh.fsf@gitster.g","threadId":"65876","inReplyTo":"ad76f06fc7ed304af97c73a5931e1ebc5f2d3895.1782500507.git.me@ttaylorr.com","subject":"Re: [RFC PATCH 04/10] repack: teach MIDX retention about geometric rollups","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-26T21:28:18Z","receivedAt":"2026-06-26T21:28:21Z","isPatch":true,"body":"Taylor Blau <me@ttaylorr.com> writes:\n\n> +static int pack_geometry_contains_pack(struct packed_git **packs,\n> +\t\t\t\t       uint32_t packs_nr,\n> +\t\t\t\t       const char *base)\n> +{\n> +\tstruct strbuf buf = STRBUF_INIT;\n> +\tuint32_t i;\n> +\n> +\tfor (i = 0; i < packs_nr; i++) {\n> +\t\tstrbuf_reset(&buf);\n> +\t\tstrbuf_addstr(&buf, pack_basename(packs[i]));\n> +\t\tstrbuf_strip_suffix(&buf, \".pack\");\n> +\n> +\t\tif (!strcmp(buf.buf, base)) {\n> +\t\t\tstrbuf_release(&buf);\n> +\t\t\treturn 1;\n> +\t\t}\n> +\t}\n> +\n> +\tstrbuf_release(&buf);\n> +\treturn 0;\n> +}\n\nIt feels slightly inefficient to repeatedly strbuf_reset(),\nstrbuf_addstr(), and strbuf_strip_suffix() in the loop.  I do not\nknow if my understanding of what existing_packs_retain_midx_packs()\npasses down in buf.buf as base is correct or not, but if so,\nwouldn't it equivalent to\n\n\tfor (uint32_t i = 0; i < packs_nr; i++) {\n                const char *pack_name = pack_basename(packs[i]);\n                const char *suffix;\n\n                if (skip_prefix(pack_name, base, &suffix) &&\n                    !strcmp(suffix, \".pack\"))\n                        return 1;\n\t}\n\nperhaps?\n\nStarting from \"/path/to/objects/pack/pack-deadbeef.pack\", you take\nthe basename of it to have \"pack-deadbeef.pack\" in buf, strip out\nthe \".pack\" suffix to get \"pack-deadbeef\" in buf and then compare it\nwith the base.\n\nInstead, pack_name in the rewitten one becomes the basename of the\npackfile path, i.e., \"pack-deadbeef.pack\", then we see if it begins\nwith base and take the remainder in suffix, and finally we check if\nthat remaining suffix is \".pack\".\n\nWhich should be equivalent.\n\n> + * freshly-written pack supersedes them. When doing a geometric repack,\n> + * packs below the split are rewritten into the new MIDX tip and should\n> + * remain eligible for deletion.\n>   */\n> -void existing_packs_retain_midx_packs(struct existing_packs *existing)\n> +void existing_packs_retain_midx_packs(struct existing_packs *existing,\n> +\t\t\t\t      const struct pack_geometry *geometry)\n>  {\n>  \tstruct string_list_item *item;\n>  \tstruct strbuf buf = STRBUF_INIT;\n> @@ -315,6 +351,9 @@ void existing_packs_retain_midx_packs(struct existing_packs *existing)\n>  \t\tstrbuf_strip_suffix(&buf, \".pack\");\n>  \t\tstrbuf_strip_suffix(&buf, \".idx\");\n\nNot a fault of this patch, but it makes the hairs on the back of my\nhead tingle to see that a bogus input like \"pack-foobar.idx.pack\"\nhappily is taken, while \"pack-foobar.pack.idx\", an equally bogus\ninput, is not.\n\n> +\t\tif (pack_geometry_contains_rollup(geometry, buf.buf))\n> +\t\t\tcontinue;\n"},{"id":"546516","messageId":"xmqqpl1d56dd.fsf@gitster.g","threadId":"65876","inReplyTo":"e3d2e46443d0b32ce29215563dde04ebcf850679.1782500507.git.me@ttaylorr.com","subject":"Re: [RFC PATCH 08/10] pack-objects: introduce '--stdin-packs=follow-reachable'","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-26T21:39:10Z","receivedAt":"2026-06-26T21:39:12Z","isPatch":true,"body":"Taylor Blau <me@ttaylorr.com> writes:\n\n> +/*\n> + * Flag bit set on commits that belong to an included pack during\n> + * '--stdin-packs=follow-reachable'. Used by the pre-walk to\n> + * identify which reachable commits should be tips for the main\n> + * object traversal.\n> + */\n> +#define IN_INCLUDED_PACK (1u<<11)\n> +\n> +static int mark_included_pack_tip(const struct object_id *oid,\n> +\t\t\t\t  struct packed_git *p,\n> +\t\t\t\t  uint32_t pos,\n> +\t\t\t\t  void *data)\n> +{\n> +\tstruct rev_info *main_revs = data;\n> +\toff_t ofs = nth_packed_object_offset(p, pos);\n> +\tenum object_type type;\n> +\tstruct object_info oi = OBJECT_INFO_INIT;\n> +\tstruct object *obj;\n> +\n> +\toi.typep = &type;\n> +\tif (packed_object_info(p, ofs, &oi) < 0)\n> +\t\treturn 0;\n> +\tif (type != OBJ_COMMIT && type != OBJ_TAG)\n> +\t\treturn 0;\n\nWe do not care about non commits, non tags.\n\n> +\tobj = parse_object(the_repository, oid);\n> +\tif (!obj)\n> +\t\treturn 0;\n> +\n> +\tobj->flags |= IN_INCLUDED_PACK;\n> +\n> +\tif (type == OBJ_TAG && main_revs)\n> +\t\tadd_pending_object(main_revs, obj, \"\");\n\nAny tag object is added to the pending list of the second phase\ntraversal here.  Doesn't this retain unreachable tags and\n(unreachable) objects that are only reachable from these unreachable\ntags found in the packfile?  Don't we want to limit this code to add\nonly tags that actually are reachable from refs, or something?\n\n> +\treturn 0;\n> +}\n\nThe other function ...\n\n> +static int mark_loose_object_tip(const struct object_id *oid,\n> +\t\t\t\t struct object_info *oi UNUSED,\n> +\t\t\t\t void *data)\n\n... is structured in a very similar way, and gives the same\npuzzlement to me.\n"},{"id":"546524","messageId":"aj8cOhH6hGVZIFft@nand.local","threadId":"65876","inReplyTo":"xmqqpl1d56dd.fsf@gitster.g","subject":"Re: [RFC PATCH 08/10] pack-objects: introduce '--stdin-packs=follow-reachable'","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-06-27T00:41:30Z","receivedAt":"2026-06-27T00:41:38Z","isPatch":true,"body":"On Fri, Jun 26, 2026 at 02:39:10PM -0700, Junio C Hamano wrote:\n> Taylor Blau <me@ttaylorr.com> writes:\n>\n> > +/*\n> > + * Flag bit set on commits that belong to an included pack during\n> > + * '--stdin-packs=follow-reachable'. Used by the pre-walk to\n> > + * identify which reachable commits should be tips for the main\n> > + * object traversal.\n> > + */\n> > +#define IN_INCLUDED_PACK (1u<<11)\n> > +\n> > +static int mark_included_pack_tip(const struct object_id *oid,\n> > +\t\t\t\t  struct packed_git *p,\n> > +\t\t\t\t  uint32_t pos,\n> > +\t\t\t\t  void *data)\n> > +{\n> > +\tstruct rev_info *main_revs = data;\n> > +\toff_t ofs = nth_packed_object_offset(p, pos);\n> > +\tenum object_type type;\n> > +\tstruct object_info oi = OBJECT_INFO_INIT;\n> > +\tstruct object *obj;\n> > +\n> > +\toi.typep = &type;\n> > +\tif (packed_object_info(p, ofs, &oi) < 0)\n> > +\t\treturn 0;\n> > +\tif (type != OBJ_COMMIT && type != OBJ_TAG)\n> > +\t\treturn 0;\n>\n> We do not care about non commits, non tags.\n\nThis should not be an &&, but rather an ||. We only want to handle\nobjects which are either commits *or* tags via this function.\n\n> > +\tobj = parse_object(the_repository, oid);\n> > +\tif (!obj)\n> > +\t\treturn 0;\n> > +\n> > +\tobj->flags |= IN_INCLUDED_PACK;\n> > +\n> > +\tif (type == OBJ_TAG && main_revs)\n> > +\t\tadd_pending_object(main_revs, obj, \"\");\n>\n> Any tag object is added to the pending list of the second phase\n> traversal here.  Doesn't this retain unreachable tags and\n> (unreachable) objects that are only reachable from these unreachable\n> tags found in the packfile?  Don't we want to limit this code to add\n> only tags that actually are reachable from refs, or something?\n\nHmm. You're right, if we have an unreachable annotated tag object in one\nof the STDIN_PACK_INCLUDE packs, then we'll pick it up and place it in\nthe non-cruft pack.\n\nI'd have to think more about how to handle this properly to ensure that\n*only* reachable objects are included. We probably want to re-order this\ncode so that we start a traversal from the reference tips, then mark\nreachable objects as starting points based on whether or not they appear\nin an included pack (and correspondingly do not appear in an excluded\none).\n\nThat adjusts how the traversal would work, I think, since we should be\nable to pick up the resulting set of objects in a single pass. But I'd\nhave to think more about it...\n\nThanks,\nTaylor\n"},{"id":"546525","messageId":"aj8cvDCMcw+RayyO@nand.local","threadId":"65876","inReplyTo":"xmqqwlvl56vh.fsf@gitster.g","subject":"Re: [RFC PATCH 04/10] repack: teach MIDX retention about geometric rollups","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-06-27T00:43:40Z","receivedAt":"2026-06-27T00:43:43Z","isPatch":true,"body":"On Fri, Jun 26, 2026 at 02:28:18PM -0700, Junio C Hamano wrote:\n> Taylor Blau <me@ttaylorr.com> writes:\n>\n> > +static int pack_geometry_contains_pack(struct packed_git **packs,\n> > +\t\t\t\t       uint32_t packs_nr,\n> > +\t\t\t\t       const char *base)\n> > +{\n> > +\tstruct strbuf buf = STRBUF_INIT;\n> > +\tuint32_t i;\n> > +\n> > +\tfor (i = 0; i < packs_nr; i++) {\n> > +\t\tstrbuf_reset(&buf);\n> > +\t\tstrbuf_addstr(&buf, pack_basename(packs[i]));\n> > +\t\tstrbuf_strip_suffix(&buf, \".pack\");\n> > +\n> > +\t\tif (!strcmp(buf.buf, base)) {\n> > +\t\t\tstrbuf_release(&buf);\n> > +\t\t\treturn 1;\n> > +\t\t}\n> > +\t}\n> > +\n> > +\tstrbuf_release(&buf);\n> > +\treturn 0;\n> > +}\n>\n> It feels slightly inefficient to repeatedly strbuf_reset(),\n> strbuf_addstr(), and strbuf_strip_suffix() in the loop.  I do not\n> know if my understanding of what existing_packs_retain_midx_packs()\n> passes down in buf.buf as base is correct or not, but if so,\n> wouldn't it equivalent to\n>\n> \tfor (uint32_t i = 0; i < packs_nr; i++) {\n>                 const char *pack_name = pack_basename(packs[i]);\n>                 const char *suffix;\n>\n>                 if (skip_prefix(pack_name, base, &suffix) &&\n>                     !strcmp(suffix, \".pack\"))\n>                         return 1;\n> \t}\n>\n> perhaps?\n>\n> Starting from \"/path/to/objects/pack/pack-deadbeef.pack\", you take\n> the basename of it to have \"pack-deadbeef.pack\" in buf, strip out\n> the \".pack\" suffix to get \"pack-deadbeef\" in buf and then compare it\n> with the base.\n\nI think that this would work nicely. I think that the skip_prefix()\nvariant is easy enough to read, and is clearly more efficient.\n\n> Instead, pack_name in the rewitten one becomes the basename of the\n> packfile path, i.e., \"pack-deadbeef.pack\", then we see if it begins\n> with base and take the remainder in suffix, and finally we check if\n> that remaining suffix is \".pack\".\n>\n> Which should be equivalent.\n>\n> > + * freshly-written pack supersedes them. When doing a geometric repack,\n> > + * packs below the split are rewritten into the new MIDX tip and should\n> > + * remain eligible for deletion.\n> >   */\n> > -void existing_packs_retain_midx_packs(struct existing_packs *existing)\n> > +void existing_packs_retain_midx_packs(struct existing_packs *existing,\n> > +\t\t\t\t      const struct pack_geometry *geometry)\n> >  {\n> >  \tstruct string_list_item *item;\n> >  \tstruct strbuf buf = STRBUF_INIT;\n> > @@ -315,6 +351,9 @@ void existing_packs_retain_midx_packs(struct existing_packs *existing)\n> >  \t\tstrbuf_strip_suffix(&buf, \".pack\");\n> >  \t\tstrbuf_strip_suffix(&buf, \".idx\");\n>\n> Not a fault of this patch, but it makes the hairs on the back of my\n> head tingle to see that a bogus input like \"pack-foobar.idx.pack\"\n> happily is taken, while \"pack-foobar.pack.idx\", an equally bogus\n> input, is not.\n\nYeah, this is gross (and my fault). Presumably I was swapping out one\nvariant for another and didn't stage the removal of one of the\nstrip_suffix() calls.\n\nThanks,\nTaylor\n"},{"id":"546526","messageId":"xmqq8q8068f7.fsf@gitster.g","threadId":"65876","inReplyTo":"aj8cOhH6hGVZIFft@nand.local","subject":"Re: [RFC PATCH 08/10] pack-objects: introduce '--stdin-packs=follow-reachable'","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-27T02:09:32Z","receivedAt":"2026-06-27T02:09:35Z","isPatch":true,"body":"Taylor Blau <me@ttaylorr.com> writes:\n\n>> > +\tif (packed_object_info(p, ofs, &oi) < 0)\n>> > +\t\treturn 0;\n>> > +\tif (type != OBJ_COMMIT && type != OBJ_TAG)\n>> > +\t\treturn 0;\n>>\n>> We do not care about non commits, non tags.\n>\n> This should not be an &&, but rather an ||. We only want to handle\n> objects which are either commits *or* tags via this function.\n\nMy comment above did not mean to say anything is wrong in the code;\nI was just thinking aloud.  We return for blob or tree because they\nare not commit and they are not tag.  If we say \"||\" then we return\nfor everybody, because anything that is a commit (failing the LHS of\nthe \"||\") cannot be a tag (satisfying the RHS of the \"||\").\n\n"},{"id":"546527","messageId":"aj801vo5+MZ2C3Va@nand.local","threadId":"65876","inReplyTo":"xmqq8q8068f7.fsf@gitster.g","subject":"Re: [RFC PATCH 08/10] pack-objects: introduce '--stdin-packs=follow-reachable'","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2026-06-27T02:26:30Z","receivedAt":"2026-06-27T02:26:34Z","isPatch":true,"body":"On Fri, Jun 26, 2026 at 07:09:32PM -0700, Junio C Hamano wrote:\n> Taylor Blau <me@ttaylorr.com> writes:\n>\n> >> > +\tif (packed_object_info(p, ofs, &oi) < 0)\n> >> > +\t\treturn 0;\n> >> > +\tif (type != OBJ_COMMIT && type != OBJ_TAG)\n> >> > +\t\treturn 0;\n> >>\n> >> We do not care about non commits, non tags.\n> >\n> > This should not be an &&, but rather an ||. We only want to handle\n> > objects which are either commits *or* tags via this function.\n>\n> My comment above did not mean to say anything is wrong in the code;\n> I was just thinking aloud.  We return for blob or tree because they\n> are not commit and they are not tag. [...]\n\nAbsolutely. Sorry about that, I have no idea how I tricked myself into\nmisreading the patch here. The second sentence of what I wrote above is\ncorrect, but not the first.\n\nThanks,\nTaylor\n"},{"id":"549088","messageId":"xmqqwlugo4nk.fsf@gitster.g","threadId":"65876","inReplyTo":"cover.1782500507.git.me@ttaylorr.com","subject":"Re: [RFC PATCH 00/10] repack: combine '--geometric' and '--cruft'","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-27T15:04:15Z","receivedAt":"2026-07-27T15:04:17Z","isPatch":true,"body":"Taylor Blau <me@ttaylorr.com> writes:\n\n> First, a short note. This series is an RFC because I have not had the\n> chance to review and test it as thoroughly as I normally would, and\n> because we are deep in the -rc phase.\n\nAnd a release was made.  Has the topic had a chance to be further\npolished, or did it turn out that it was perfect to begin with?\n"}]}