pack-objects: support '--refs-snapshot' with 'follow-reachable'

The '--stdin-packs=follow-reachable' mode walks from reference tips to determine which objects in included packs are reachable. Without a snapshot, pack-objects discovers refs by iterating live references, which may change between the time the repack writes the geometric pack and the time it writes the MIDX bitmap. If a reference is updated during that window, the set of reachable objects seen by pack-objects may differ from the set seen by the MIDX bitmap writer. This can cause reachable objects to end up in the cruft pack (because pack-objects did not see the reference that makes them reachable) rather than the geometric pack. While this does not cause data loss, it has two undesirable consequences: - Reachable objects in the cruft pack cannot receive bitmap coverage (since the cruft pack may be excluded from the MIDX when 'repack.midxMustContainCruft' is false). - Serving fetches that need those objects requires loading the cruft pack, which may contain many unrelated unreachable objects. To avoid this, teach pack-objects to accept '--refs-snapshot=<path>' when used with '--stdin-packs=follow-reachable'. The snapshot file uses the same format as the MIDX bitmap writer: one hex OID per line, with an optional '+' prefix for preferred bitmap commits. 'pack-objects' happily ignores the '+' prefix for indicating preferred bitmap commits as a convenience, so that the ref-snapshot can be shared between the MIDX generation machinery and 'pack-objects'. Signed-off-by: Taylor Blau <me@ttaylorr.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Taylor Blau committed Jun 26, 2026 at 15:02 UTC e6a6b7e9fce005463ddbda3d21173fab0114a200
2 files changed +52 -2
Documentation/git-pack-objects.adoc
+8
@@ -133,6 +133,14 @@ commits and annotated tag objects.
133 Incompatible with `--revs`, or options that imply `--revs` (such as
134 `--all`), with the exception of `--unpacked`, which is compatible.
135
136 +--refs-snapshot=<path>::
137 + When used with `--stdin-packs=follow-reachable`, read reference
138 + tips from `<path>` instead of iterating live references. The file
139 + format is one hex object ID per line, with an optional `+` prefix
140 + (for preferred bitmap commits). This ensures a consistent view of
141 + references when the same snapshot is shared with other tools (e.g.,
142 + the MIDX bitmap writer).
143 +
144 --cruft::
145 Packs unreachable objects into a separate "cruft" pack, denoted
146 by the existence of a `.mtimes` file. Typically used by `git
builtin/pack-objects.c
+44 -2
@@ -219,6 +219,7 @@ static int incremental;
219 static int ignore_packed_keep_on_disk;
220 static int ignore_packed_keep_in_core;
221 static int ignore_packed_keep_in_core_open;
222 +static const char *stdin_packs_refs_snapshot;
223 static int ignore_packed_keep_in_core_has_cruft;
224 static int allow_ofs_delta;
225 static struct pack_idx_option pack_idx_opts;
@@ -4009,6 +4010,38 @@ static int add_ref_to_pending(const struct reference *ref, void *cb_data)
4010 return 0;
4011 }
4012
4013 +static void read_refs_snapshot(const char *refs_snapshot,
4014 + struct rev_info *revs)
4015 +{
4016 + struct strbuf buf = STRBUF_INIT;
4017 + struct object_id oid;
4018 + FILE *f = xfopen(refs_snapshot, "r");
4019 +
4020 + while (strbuf_getline(&buf, f) != EOF) {
4021 + struct object *object;
4022 + const char *hex = buf.buf;
4023 + const char *end = NULL;
4024 +
4025 + if (*hex == '+')
4026 + hex++;
4027 +
4028 + if (parse_oid_hex_algop(hex, &oid, &end,
4029 + the_repository->hash_algo) < 0)
4030 + die(_("could not parse line: %s"), buf.buf);
4031 + if (*end)
4032 + die(_("malformed line: %s"), buf.buf);
4033 +
4034 + object = parse_object(the_repository, &oid);
4035 + if (!object)
4036 + continue;
4037 +
4038 + add_pending_object(revs, object, "");
4039 + }
4040 +
4041 + fclose(f);
4042 + strbuf_release(&buf);
4043 +}
4044 +
4045 static void stdin_packs_add_reachable_pack_entries(struct string_list *keys,
4046 struct rev_info *revs,
4047 int rev_list_unpacked)
@@ -4065,8 +4098,11 @@ static void stdin_packs_add_reachable_pack_entries(struct string_list *keys,
4098 pre_walk.keep_pack_cache_flags |= KEPT_PACK_IN_CORE;
4099 pre_walk.ignore_missing_links = 1;
4100
4068 - refs_for_each_ref(get_main_ref_store(the_repository),
4069 - add_ref_to_pending, &pre_walk);
4101 + if (stdin_packs_refs_snapshot)
4102 + read_refs_snapshot(stdin_packs_refs_snapshot, &pre_walk);
4103 + else
4104 + refs_for_each_ref(get_main_ref_store(the_repository),
4105 + add_ref_to_pending, &pre_walk);
4106
4107 if (prepare_revision_walk(&pre_walk))
4108 die(_("revision walk setup failed"));
@@ -5267,6 +5303,8 @@ int cmd_pack_objects(int argc,
5303 OPT_CALLBACK_F(0, "stdin-packs", &stdin_packs, N_("mode"),
5304 N_("read packs from stdin"),
5305 PARSE_OPT_OPTARG, parse_stdin_packs_mode),
5306 + OPT_FILENAME(0, "refs-snapshot", &stdin_packs_refs_snapshot,
5307 + N_("refs snapshot for follow-reachable traversal")),
5308 OPT_BOOL(0, "stdout", &pack_to_stdout,
5309 N_("output pack to stdout")),
5310 OPT_BOOL(0, "include-tag", &include_tag,
@@ -5484,6 +5522,10 @@ int cmd_pack_objects(int argc,
5522 if (stdin_packs && use_internal_rev_list)
5523 die(_("cannot use internal rev list with --stdin-packs"));
5524
5525 + if (stdin_packs_refs_snapshot &&
5526 + stdin_packs != STDIN_PACKS_MODE_FOLLOW_REACHABLE)
5527 + die(_("--refs-snapshot can only be used with --stdin-packs=follow-reachable"));
5528 +
5529 if (cruft) {
5530 if (use_internal_rev_list)
5531 die(_("cannot use internal rev list with --cruft"));