{"thread":{"id":"65983","subject":"[PATCH 1/4] t/helper: teach pack-deltas to list delta entries","startedAt":"2026-07-13T01:11:56Z","lastAt":"2026-07-13T01:12:11Z","messageCount":4,"participants":["Taylor Blau"],"isPatch":true,"patchVersion":1,"patchTotal":4},"messages":[{"id":"547925","messageId":"alQ7WKITYDXfiVn9@com-79390","threadId":"65983","inReplyTo":"cover.1783905084.git.ttaylorr@openai.com","subject":"[PATCH 1/4] t/helper: teach pack-deltas to list delta entries","fromName":"Taylor Blau","fromEmail":"ttaylorr@openai.com","sentAt":"2026-07-13T01:11:53Z","receivedAt":"2026-07-13T01:11:56Z","isPatch":true,"body":"In the following commit(s), some tests will need to distinguish between\n`REF_DELTA`s and `OFS_DELTA`s to exercise a new '--no-ref-delta' option\nfor 'pack-objects'.\n\nExisting tools report delta relationships, but not how their bases are\nrepresented in the pack.\n\nTeach 'test-tool pack-deltas' a '--list-deltas' mode. For each delta\nentry, print the object ID, its REF_DELTA or OFS_DELTA type, and the\nbase object ID or pack offset, respectively. This lets tests inspect\npack headers without open-coding a parser.\n\nSigned-off-by: Taylor Blau <ttaylorr@openai.com>\n---\n t/helper/test-pack-deltas.c | 69 +++++++++++++++++++++++++++++++++++++\n t/t5300-pack-object.sh      |  8 +++--\n 2 files changed, 75 insertions(+), 2 deletions(-)\n\ndiff --git a/t/helper/test-pack-deltas.c b/t/helper/test-pack-deltas.c\nindex 840797cf0d..4ba6fe2dd3 100644\n--- a/t/helper/test-pack-deltas.c\n+++ b/t/helper/test-pack-deltas.c\n@@ -7,6 +7,7 @@\n #include \"hash.h\"\n #include \"hex.h\"\n #include \"pack.h\"\n+#include \"packfile.h\"\n #include \"pack-objects.h\"\n #include \"parse-options.h\"\n #include \"setup.h\"\n@@ -15,6 +16,7 @@\n \n static const char *usage_str[] = {\n \t\"test-tool pack-deltas --num-objects <num-objects>\",\n+\t\"test-tool pack-deltas --list-deltas <pack>.idx\",\n \tNULL\n };\n \n@@ -80,19 +82,86 @@ static void write_ref_delta(struct hashfile *f,\n \tfree(delta_buf);\n }\n \n+static int list_delta(const struct object_id *oid,\n+\t\t      struct packed_git *p,\n+\t\t      uint32_t pos,\n+\t\t      void *_w_curs)\n+{\n+\tstruct pack_window **w_curs = _w_curs;\n+\toff_t obj_offset = nth_packed_object_offset(p, pos);\n+\toff_t cur = obj_offset;\n+\tsize_t size;\n+\tenum object_type type = unpack_object_header(p, w_curs, &cur,\n+\t\t\t\t\t\t      &size);\n+\n+\tif (type < 0)\n+\t\tdie(\"unable to parse object at position %\"PRIu32, pos);\n+\tif (type != OBJ_REF_DELTA && type != OBJ_OFS_DELTA)\n+\t\treturn 0;\n+\n+\tif (type == OBJ_REF_DELTA) {\n+\t\tstruct object_id base_oid;\n+\t\tconst unsigned char *base = use_pack(p, w_curs, cur,\n+\t\t\t\t\t\t     NULL);\n+\n+\t\toidread(&base_oid, base, p->repo->hash_algo);\n+\t\tprintf(\"%s REF_DELTA %s\\n\", oid_to_hex(oid),\n+\t\t       oid_to_hex(&base_oid));\n+\t} else {\n+\t\toff_t base_offset = get_delta_base(p, w_curs, &cur,\n+\t\t\t\t\t\t   type, obj_offset);\n+\n+\t\tif (!base_offset)\n+\t\t\tdie(\"unable to read base of object %s\", oid_to_hex(oid));\n+\t\tprintf(\"%s OFS_DELTA %\"PRIuMAX\"\\n\", oid_to_hex(oid),\n+\t\t       (uintmax_t)base_offset);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static void list_deltas(const char *idx_name)\n+{\n+\tstruct packed_git *p;\n+\tstruct pack_window *w_curs = NULL;\n+\n+\tp = add_packed_git(the_repository, idx_name, strlen(idx_name), 1);\n+\tif (!p || open_pack_index(p))\n+\t\tdie(\"unable to open pack index %s\", idx_name);\n+\n+\tif (for_each_object_in_pack(p, list_delta, &w_curs,\n+\t\t\t\t    ODB_FOR_EACH_OBJECT_PACK_ORDER))\n+\t\tdie(\"unable to iterate over objects in %s\", idx_name);\n+\n+\tunuse_pack(&w_curs);\n+\tclose_pack(p);\n+\tfree(p);\n+}\n+\n int cmd__pack_deltas(int argc, const char **argv)\n {\n \tint num_objects = -1;\n+\tint list_deltas_mode = 0;\n \tstruct hashfile *f;\n \tstruct strbuf line = STRBUF_INIT;\n \tstruct option options[] = {\n \t\tOPT_INTEGER('n', \"num-objects\", &num_objects, N_(\"the number of objects to write\")),\n+\t\tOPT_BOOL(0, \"list-deltas\", &list_deltas_mode,\n+\t\t\t N_(\"list REF_DELTA and OFS_DELTA entries\")),\n \t\tOPT_END()\n \t};\n \n \targc = parse_options(argc, argv, NULL,\n \t\t\t     options, usage_str, 0);\n \n+\tif (list_deltas_mode) {\n+\t\tif (argc != 1 || num_objects >= 0)\n+\t\t\tusage_with_options(usage_str, options);\n+\t\tsetup_git_directory(the_repository);\n+\t\tlist_deltas(argv[0]);\n+\t\treturn 0;\n+\t}\n+\n \tif (argc || num_objects < 0)\n \t\tusage_with_options(usage_str, options);\n \ndiff --git a/t/t5300-pack-object.sh b/t/t5300-pack-object.sh\nindex 73445782e7..4bee490ff6 100755\n--- a/t/t5300-pack-object.sh\n+++ b/t/t5300-pack-object.sh\n@@ -190,7 +190,9 @@ test_expect_success 'unpack without delta (core.fsyncmethod=batch)' '\n \n test_expect_success 'pack with REF_DELTA' '\n \tpackname_2=$(git pack-objects --progress test-2 <obj-list 2>stderr) &&\n-\tcheck_deltas stderr -gt 0\n+\tcheck_deltas stderr -gt 0 &&\n+\ttest-tool pack-deltas --list-deltas test-2-$packname_2.idx >deltas &&\n+\ttest_grep \" REF_DELTA \" deltas\n '\n \n test_expect_success 'unpack with REF_DELTA' '\n@@ -204,7 +206,9 @@ test_expect_success 'unpack with REF_DELTA (core.fsyncmethod=batch)' '\n test_expect_success 'pack with OFS_DELTA' '\n \tpackname_3=$(git pack-objects --progress --delta-base-offset test-3 \\\n \t\t\t<obj-list 2>stderr) &&\n-\tcheck_deltas stderr -gt 0\n+\tcheck_deltas stderr -gt 0 &&\n+\ttest-tool pack-deltas --list-deltas test-3-$packname_3.idx >deltas &&\n+\ttest_grep \" OFS_DELTA \" deltas\n '\n \n test_expect_success 'unpack with OFS_DELTA' '\n-- \n2.55.0\n\n"},{"id":"547926","messageId":"alQ7XVq5CYD8CyE8@com-79390","threadId":"65983","inReplyTo":"cover.1783905084.git.ttaylorr@openai.com","subject":"[PATCH 2/4] pack-objects: introduce `--no-ref-delta`","fromName":"Taylor Blau","fromEmail":"ttaylorr@openai.com","sentAt":"2026-07-13T01:11:57Z","receivedAt":"2026-07-13T01:12:02Z","isPatch":true,"body":"Some consumers of 'pack-objects' may wish to avoid packs which contain\n`REF_DELTA` entries. For instance, a 'receive-pack' implementation which\nretains the resulting pack without building an index of object IDs may\nprefer every delta base to be discoverable from an earlier entry in the\nsame pack.\n\nTeach 'pack-objects' a new `--no-ref-delta` option to avoid writing\n`REF_DELTA` entries, without changing whether `OFS_DELTA` is allowed.\n\nWhen used without `--delta-base-offset`, no delta representation\nremains, so avoid delta search entirely. Otherwise, allow new deltas\nwhose bases appear earlier in the same pack.\n\nFor now, disable delta- and bitmap-reuse under `--no-ref-delta`, since\neither may copy an existing `REF_DELTA` entry. This is overly\npessimistic, but simplifies the changes in this commit. The next commit\nre-enables reuse in the cases which do not require `REF_DELTA`.\n\nSigned-off-by: Taylor Blau <ttaylorr@openai.com>\n---\n Documentation/git-pack-objects.adoc |  8 ++++-\n builtin/pack-objects.c              | 16 ++++++---\n t/t5300-pack-object.sh              | 52 +++++++++++++++++++++++++++++\n 3 files changed, 71 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/git-pack-objects.adoc b/Documentation/git-pack-objects.adoc\nindex 65cd00c152..5e42e4429d 100644\n--- a/Documentation/git-pack-objects.adoc\n+++ b/Documentation/git-pack-objects.adoc\n@@ -10,7 +10,8 @@ SYNOPSIS\n --------\n [verse]\n 'git pack-objects' [-q | --progress | --all-progress] [--all-progress-implied]\n-\t\t   [--no-reuse-delta] [--delta-base-offset] [--non-empty]\n+\t\t   [--no-reuse-delta] [--delta-base-offset] [--no-ref-delta]\n+\t\t   [--non-empty]\n \t\t   [--local] [--incremental] [--window=<n>] [--depth=<n>]\n \t\t   [--revs [--unpacked | --all]] [--keep-pack=<pack-name>]\n \t\t   [--cruft] [--cruft-expiration=<time>]\n@@ -297,6 +298,11 @@ Note: Porcelain commands such as `git gc` (see linkgit:git-gc[1]),\n in modern Git when they put objects in your repository into pack files.\n So does `git bundle` (see linkgit:git-bundle[1]) when it creates a bundle.\n \n+--no-ref-delta::\n+\tDo not emit deltas which represent their base by their literal\n+\tobject ID. This is independent of `--delta-base-offset`;\n+\twithout that option, no deltas are emitted.\n+\n --threads=<n>::\n \tSpecifies the number of threads to spawn when searching for best\n \tdelta matches.  This requires that pack-objects be compiled with\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex e3760b3492..c3574fcb8a 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -190,7 +190,8 @@ static inline void oe_set_delta_size(struct packing_data *pack,\n \n static const char *const pack_usage[] = {\n \tN_(\"git pack-objects [-q | --progress | --all-progress] [--all-progress-implied]\\n\"\n-\t   \"                 [--no-reuse-delta] [--delta-base-offset] [--non-empty]\\n\"\n+\t   \"                 [--no-reuse-delta] [--delta-base-offset] [--no-ref-delta]\\n\"\n+\t   \"                 [--non-empty]\\n\"\n \t   \"                 [--local] [--incremental] [--window=<n>] [--depth=<n>]\\n\"\n \t   \"                 [--revs [--unpacked | --all]] [--keep-pack=<pack-name>]\\n\"\n \t   \"                 [--cruft] [--cruft-expiration=<time>]\\n\"\n@@ -221,6 +222,7 @@ static int ignore_packed_keep_in_core;\n static int ignore_packed_keep_in_core_open;\n static int ignore_packed_keep_in_core_has_cruft;\n static int allow_ofs_delta;\n+static int allow_ref_delta = 1;\n static struct pack_idx_option pack_idx_opts;\n static const char *base_name;\n static int progress = 1;\n@@ -3405,6 +3407,9 @@ static int should_attempt_deltas(struct object_entry *entry)\n \tif (entry->no_try_delta)\n \t\treturn 0;\n \n+\tif (entry->preferred_base && !allow_ref_delta)\n+\t\treturn 0;\n+\n \tif (!entry->preferred_base) {\n \t\tif (oe_type(entry) < 0)\n \t\t\tdie(_(\"unable to get type of object %s\"),\n@@ -3647,7 +3652,8 @@ static void prepare_pack(int window, int depth)\n \tif (!pack_to_stdout)\n \t\tdo_check_packed_object_crc = 1;\n \n-\tif (!to_pack.nr_objects || !window || !depth)\n+\tif (!to_pack.nr_objects || !window || !depth ||\n+\t    (!allow_ref_delta && !allow_ofs_delta))\n \t\treturn;\n \n \tif (path_walk)\n@@ -4662,7 +4668,7 @@ static int pack_options_allow_reuse(void)\n \t       !ignore_packed_keep_on_disk &&\n \t       !ignore_packed_keep_in_core &&\n \t       (!local || !have_non_local_packs) &&\n-\t       !incremental;\n+\t       !incremental && allow_ref_delta;\n }\n \n static int get_object_list_from_bitmap(struct rev_info *revs)\n@@ -5111,6 +5117,8 @@ int cmd_pack_objects(int argc,\n \t\t\t N_(\"reuse existing objects\")),\n \t\tOPT_BOOL(0, \"delta-base-offset\", &allow_ofs_delta,\n \t\t\t N_(\"use OFS_DELTA objects\")),\n+\t\tOPT_BOOL(0, \"ref-delta\", &allow_ref_delta,\n+\t\t\t N_(\"use REF_DELTA objects\")),\n \t\tOPT_INTEGER(0, \"threads\", &delta_search_threads,\n \t\t\t    N_(\"use threads when searching for best delta matches\")),\n \t\tOPT_BOOL(0, \"non-empty\", &non_empty,\n@@ -5309,7 +5317,7 @@ int cmd_pack_objects(int argc,\n \tif (unpack_unreachable || keep_unreachable || pack_loose_unreachable)\n \t\tuse_internal_rev_list = 1;\n \n-\tif (!reuse_object)\n+\tif (!reuse_object || !allow_ref_delta)\n \t\treuse_delta = 0;\n \tif (cfg->pack_compression_level == -1)\n \t\tcfg->pack_compression_level = Z_DEFAULT_COMPRESSION;\ndiff --git a/t/t5300-pack-object.sh b/t/t5300-pack-object.sh\nindex 4bee490ff6..b9e36044b9 100755\n--- a/t/t5300-pack-object.sh\n+++ b/t/t5300-pack-object.sh\n@@ -211,6 +211,58 @@ test_expect_success 'pack with OFS_DELTA' '\n \ttest_grep \" OFS_DELTA \" deltas\n '\n \n+test_expect_success 'pack without REF_DELTA' '\n+\tgit pack-objects --no-ref-delta --stdout <obj-list >no-ref.pack &&\n+\tgit index-pack -o no-ref.idx no-ref.pack &&\n+\n+\ttest-tool pack-deltas --list-deltas no-ref.idx >deltas &&\n+\ttest_must_be_empty deltas\n+'\n+\n+test_expect_success 'pack without REF_DELTA with OFS_DELTA' '\n+\tgit pack-objects --delta-base-offset --no-ref-delta --stdout \\\n+\t\t<obj-list >no-ref-ofs.pack &&\n+\tgit index-pack -o no-ref-ofs.idx no-ref-ofs.pack &&\n+\n+\ttest-tool pack-deltas --list-deltas no-ref-ofs.idx >deltas &&\n+\ttest_grep \" OFS_DELTA \" deltas &&\n+\ttest_grep ! \" REF_DELTA \" deltas\n+'\n+\n+test_expect_success 'pack without REF_DELTA skips excluded delta bases' '\n+\ttest_when_finished \"git read-tree $tree\" &&\n+\n+\techo bar >>d &&\n+\tgit update-index --add d &&\n+\tthin_tree=$(git write-tree) &&\n+\tthin_commit=$(git commit-tree $thin_tree -p $commit </dev/null) &&\n+\n+\t{\n+\t\techo $thin_commit &&\n+\t\techo ^$commit\n+\t} >thin-revs &&\n+\n+\t# Each type appears only once in the output, so any delta must\n+\t# use an excluded base and therefore be a REF_DELTA.\n+\tgit pack-objects --thin --stdout --revs \\\n+\t\t<thin-revs >thin.pack &&\n+\tgit index-pack --fix-thin --stdin thin-fixed.pack \\\n+\t\t<thin.pack >/dev/null &&\n+\n+\ttest-tool pack-deltas --list-deltas thin-fixed.idx >deltas &&\n+\ttest_grep ! \" OFS_DELTA \" deltas &&\n+\ttest_grep \" REF_DELTA \" deltas &&\n+\n+\tgit pack-objects --thin --stdout --revs \\\n+\t\t--delta-base-offset --no-ref-delta \\\n+\t\t<thin-revs >no-ref-thin.pack &&\n+\tgit index-pack --fix-thin --stdin no-ref-thin-fixed.pack \\\n+\t\t<no-ref-thin.pack >/dev/null &&\n+\n+\ttest-tool pack-deltas --list-deltas no-ref-thin-fixed.idx >deltas &&\n+\ttest_must_be_empty deltas\n+'\n+\n test_expect_success 'unpack with OFS_DELTA' '\n \tcheck_unpack test-3-${packname_3} obj-list\n '\n-- \n2.55.0\n\n"},{"id":"547927","messageId":"alQ7YmnVeEjYG6Wt@com-79390","threadId":"65983","inReplyTo":"cover.1783905084.git.ttaylorr@openai.com","subject":"[PATCH 3/4] pack-objects: support reuse with `--no-ref-delta`","fromName":"Taylor Blau","fromEmail":"ttaylorr@openai.com","sentAt":"2026-07-13T01:12:02Z","receivedAt":"2026-07-13T01:12:07Z","isPatch":true,"body":"The previous commit disables delta- and bitmap-reuse entirely whenever\npack-objects is given '--no-ref-delta' for the sake of simplicity. This\nis overly pessimistic.\n\nWhen '--delta-base-offset' is also given, delta reuse can remain\nenabled. A reused delta whose base is written earlier in the output can\nbe encoded as an `OFS_DELTA`, even when its source copy was encoded as a\n`REF_DELTA`.\n\nPreferred bases and external thin-pack bases are different: neither\nappears in the output, so deltas against either still require encoding\nthe object as a `REF_DELTA`, and thus cannot be reused.\n\nWithout '--delta-base-offset', delta reuse remains disabled, since no\ndelta representation remains.\n\nBitmap reuse follows a different path, since selected entries may be\ncopied without passing through the code which chooses a delta\nrepresentation. When given '--no-ref-delta', we must inspect candidate\nobjects individually, and leave `REF_DELTA` entries to the normal object\npath outside of pack-reuse.\n\nWe must likewise avoid the special-case for reusing either the single or\npreferred pack corresponding to the bitmap by whole `eword_t`'s at a\ntime.\n\nSigned-off-by: Taylor Blau <ttaylorr@openai.com>\n---\n builtin/pack-objects.c      | 17 +++++++++++++----\n pack-bitmap.c               | 30 ++++++++++++++++++++----------\n pack-bitmap.h               |  3 ++-\n t/t5300-pack-object.sh      | 21 ++++++++++++++++++++-\n t/t5332-multi-pack-reuse.sh | 16 ++++++++++++++++\n 5 files changed, 71 insertions(+), 16 deletions(-)\n\ndiff --git a/builtin/pack-objects.c b/builtin/pack-objects.c\nindex c3574fcb8a..43cd4be2e5 100644\n--- a/builtin/pack-objects.c\n+++ b/builtin/pack-objects.c\n@@ -2207,6 +2207,13 @@ static int can_reuse_delta(const struct object_id *base_oid,\n \t */\n \tbase = packlist_find(&to_pack, base_oid);\n \tif (base) {\n+\t\t/*\n+\t\t * A preferred base is omitted from the resulting pack, so it\n+\t\t * can only be referenced by object ID.\n+\t\t */\n+\t\tif (base->preferred_base && !allow_ref_delta)\n+\t\t\treturn 0;\n+\n \t\tif (!in_same_island(&delta->idx.oid, &base->idx.oid))\n \t\t\treturn 0;\n \t\t*base_out = base;\n@@ -2218,7 +2225,8 @@ static int can_reuse_delta(const struct object_id *base_oid,\n \t * even if it was buried too deep in history to make it into the\n \t * packing list.\n \t */\n-\tif (thin && bitmap_has_oid_in_uninteresting(bitmap_git, base_oid)) {\n+\tif (allow_ref_delta && thin &&\n+\t    bitmap_has_oid_in_uninteresting(bitmap_git, base_oid)) {\n \t\tif (use_delta_islands) {\n \t\t\tif (!in_same_island(&delta->idx.oid, base_oid))\n \t\t\t\treturn 0;\n@@ -4668,7 +4676,7 @@ static int pack_options_allow_reuse(void)\n \t       !ignore_packed_keep_on_disk &&\n \t       !ignore_packed_keep_in_core &&\n \t       (!local || !have_non_local_packs) &&\n-\t       !incremental && allow_ref_delta;\n+\t       !incremental && (allow_ref_delta || allow_ofs_delta);\n }\n \n static int get_object_list_from_bitmap(struct rev_info *revs)\n@@ -4690,7 +4698,8 @@ static int get_object_list_from_bitmap(struct rev_info *revs)\n \t\t\t\t\t\t   &reuse_packfiles,\n \t\t\t\t\t\t   &reuse_packfiles_nr,\n \t\t\t\t\t\t   &reuse_packfile_bitmap,\n-\t\t\t\t\t\t   allow_pack_reuse == MULTI_PACK_REUSE);\n+\t\t\t\t\t\t   allow_pack_reuse == MULTI_PACK_REUSE,\n+\t\t\t\t\t\t   allow_ref_delta);\n \n \tif (reuse_packfiles) {\n \t\treuse_packfile_objects = bitmap_popcount(reuse_packfile_bitmap);\n@@ -5317,7 +5326,7 @@ int cmd_pack_objects(int argc,\n \tif (unpack_unreachable || keep_unreachable || pack_loose_unreachable)\n \t\tuse_internal_rev_list = 1;\n \n-\tif (!reuse_object || !allow_ref_delta)\n+\tif (!reuse_object || (!allow_ref_delta && !allow_ofs_delta))\n \t\treuse_delta = 0;\n \tif (cfg->pack_compression_level == -1)\n \t\tcfg->pack_compression_level = Z_DEFAULT_COMPRESSION;\ndiff --git a/pack-bitmap.c b/pack-bitmap.c\nindex 83eb47a28b..36cb02e374 100644\n--- a/pack-bitmap.c\n+++ b/pack-bitmap.c\n@@ -2267,7 +2267,8 @@ static int try_partial_reuse(struct bitmap_index *bitmap_git,\n \t\t\t     uint32_t pack_pos,\n \t\t\t     off_t offset,\n \t\t\t     struct bitmap *reuse,\n-\t\t\t     struct pack_window **w_curs)\n+\t\t\t     struct pack_window **w_curs,\n+\t\t\t     int allow_ref_delta)\n {\n \toff_t delta_obj_offset;\n \tenum object_type type;\n@@ -2286,6 +2287,9 @@ static int try_partial_reuse(struct bitmap_index *bitmap_git,\n \t\tuint32_t base_pos;\n \t\tuint32_t base_bitmap_pos;\n \n+\t\tif (type == OBJ_REF_DELTA && !allow_ref_delta)\n+\t\t\treturn 0;\n+\n \t\t/*\n \t\t * Find the position of the base object so we can look it up\n \t\t * in our bitmaps. If we can't come up with an offset, or if\n@@ -2358,20 +2362,19 @@ static int try_partial_reuse(struct bitmap_index *bitmap_git,\n \n static void reuse_partial_packfile_from_bitmap_1(struct bitmap_index *bitmap_git,\n \t\t\t\t\t\t struct bitmapped_pack *pack,\n-\t\t\t\t\t\t struct bitmap *reuse)\n+\t\t\t\t\t\t struct bitmap *reuse,\n+\t\t\t\t\t\t int allow_ref_delta)\n {\n \tstruct bitmap *result = bitmap_git->result;\n \tstruct pack_window *w_curs = NULL;\n \tsize_t pos = pack->bitmap_pos / BITS_IN_EWORD;\n \n-\tif (!pack->bitmap_pos) {\n+\tif (allow_ref_delta && !pack->bitmap_pos) {\n \t\t/*\n \t\t * If we're processing the first (in the case of a MIDX, the\n \t\t * preferred pack) or the only (in the case of single-pack\n-\t\t * bitmaps) pack, then we can reuse whole words at a time.\n-\t\t *\n-\t\t * This is because we know that any deltas in this range *must*\n-\t\t * have their bases chosen from the same pack, since:\n+\t\t * bitmaps) pack, then any delta in this range must have its\n+\t\t * base chosen from the same pack:\n \t\t *\n \t\t * - In the single pack case, there is no other pack to choose\n \t\t *   them from.\n@@ -2380,6 +2383,10 @@ static void reuse_partial_packfile_from_bitmap_1(struct bitmap_index *bitmap_git\n \t\t *   all ties are broken in favor of that pack (i.e. the one\n \t\t *   we're currently processing). So any duplicate bases will be\n \t\t *   resolved in favor of the pack we're processing.\n+\t\t *\n+\t\t * When REF_DELTAs are allowed, we can therefore reuse whole\n+\t\t * words at a time without inspecting object headers. Otherwise,\n+\t\t * inspect each object below to avoid reusing a REF_DELTA entry.\n \t\t */\n \t\twhile (pos < result->word_alloc &&\n \t\t       pos < pack->bitmap_nr / BITS_IN_EWORD &&\n@@ -2429,7 +2436,8 @@ static void reuse_partial_packfile_from_bitmap_1(struct bitmap_index *bitmap_git\n \t\t\t}\n \n \t\t\tif (try_partial_reuse(bitmap_git, pack, bit_pos,\n-\t\t\t\t\t      pack_pos, ofs, reuse, &w_curs) < 0) {\n+\t\t\t\t\t      pack_pos, ofs, reuse, &w_curs,\n+\t\t\t\t\t      allow_ref_delta) < 0) {\n \t\t\t\t/*\n \t\t\t\t * try_partial_reuse indicated we couldn't reuse\n \t\t\t\t * any bits, so there is no point in trying more\n@@ -2464,7 +2472,8 @@ void reuse_partial_packfile_from_bitmap(struct bitmap_index *bitmap_git,\n \t\t\t\t\tstruct bitmapped_pack **packs_out,\n \t\t\t\t\tsize_t *packs_nr_out,\n \t\t\t\t\tstruct bitmap **reuse_out,\n-\t\t\t\t\tint multi_pack_reuse)\n+\t\t\t\t\tint multi_pack_reuse,\n+\t\t\t\t\tint allow_ref_delta)\n {\n \tstruct repository *r = bitmap_repo(bitmap_git);\n \tstruct bitmapped_pack *packs = NULL;\n@@ -2559,7 +2568,8 @@ void reuse_partial_packfile_from_bitmap(struct bitmap_index *bitmap_git,\n \treuse = bitmap_word_alloc(word_alloc);\n \n \tfor (i = 0; i < packs_nr; i++)\n-\t\treuse_partial_packfile_from_bitmap_1(bitmap_git, &packs[i], reuse);\n+\t\treuse_partial_packfile_from_bitmap_1(bitmap_git, &packs[i], reuse,\n+\t\t\t\t\t\t     allow_ref_delta);\n \n \tif (bitmap_is_empty(reuse)) {\n \t\tfree(packs);\ndiff --git a/pack-bitmap.h b/pack-bitmap.h\nindex 19a8655457..39b6309736 100644\n--- a/pack-bitmap.h\n+++ b/pack-bitmap.h\n@@ -116,7 +116,8 @@ void reuse_partial_packfile_from_bitmap(struct bitmap_index *bitmap_git,\n \t\t\t\t\tstruct bitmapped_pack **packs_out,\n \t\t\t\t\tsize_t *packs_nr_out,\n \t\t\t\t\tstruct bitmap **reuse_out,\n-\t\t\t\t\tint multi_pack_reuse);\n+\t\t\t\t\tint multi_pack_reuse,\n+\t\t\t\t\tint allow_ref_delta);\n int rebuild_existing_bitmaps(struct bitmap_index *, struct packing_data *mapping,\n \t\t\t     kh_oid_map_t *reused_bitmaps, int show_progress);\n void free_bitmap_index(struct bitmap_index *);\ndiff --git a/t/t5300-pack-object.sh b/t/t5300-pack-object.sh\nindex b9e36044b9..02c09e3f7d 100755\n--- a/t/t5300-pack-object.sh\n+++ b/t/t5300-pack-object.sh\n@@ -229,6 +229,20 @@ test_expect_success 'pack without REF_DELTA with OFS_DELTA' '\n \ttest_grep ! \" REF_DELTA \" deltas\n '\n \n+test_expect_success 'pack without REF_DELTA reuses deltas as OFS_DELTA' '\n+\t# Install the REF_DELTA pack above and disable delta search, so any\n+\t# output delta must be a reused REF_DELTA rewritten as OFS_DELTA.\n+\ttest_when_finished \"rm -f .git/objects/pack/pack-$packname_2.*\" &&\n+\tgit index-pack --stdin <test-2-${packname_2}.pack >/dev/null &&\n+\n+\tgit pack-objects --window=0 --delta-base-offset \\\n+\t\t--no-ref-delta --stdout <obj-list >reused.pack &&\n+\tgit index-pack -o reused.idx reused.pack &&\n+\ttest-tool pack-deltas --list-deltas reused.idx >deltas &&\n+\ttest_grep \" OFS_DELTA \" deltas &&\n+\ttest_grep ! \" REF_DELTA \" deltas\n+'\n+\n test_expect_success 'pack without REF_DELTA skips excluded delta bases' '\n \ttest_when_finished \"git read-tree $tree\" &&\n \n@@ -253,7 +267,12 @@ test_expect_success 'pack without REF_DELTA skips excluded delta bases' '\n \ttest_grep ! \" OFS_DELTA \" deltas &&\n \ttest_grep \" REF_DELTA \" deltas &&\n \n-\tgit pack-objects --thin --stdout --revs \\\n+\t# Store the REF_DELTA entries above and disable delta search below,\n+\t# so any output delta would have to reuse an excluded-base\n+\t# REF_DELTA.\n+\tgit index-pack --stdin <thin-fixed.pack >/dev/null &&\n+\n+\tgit pack-objects --thin --window=0 --stdout --revs \\\n \t\t--delta-base-offset --no-ref-delta \\\n \t\t<thin-revs >no-ref-thin.pack &&\n \tgit index-pack --fix-thin --stdin no-ref-thin-fixed.pack \\\ndiff --git a/t/t5332-multi-pack-reuse.sh b/t/t5332-multi-pack-reuse.sh\nindex 881ce668e1..bc479653ec 100755\n--- a/t/t5332-multi-pack-reuse.sh\n+++ b/t/t5332-multi-pack-reuse.sh\n@@ -111,6 +111,22 @@ test_expect_success 'reuse all objects from all packs' '\n \ttest_pack_objects_reused_all 9 3\n '\n \n+test_expect_success '--no-ref-delta reuses REF_DELTA-free bitmapped packs' '\n+\t# Whole-word reuse is unavailable under --no-ref-delta, so reusing\n+\t# every object below exercises the per-object bitmap path.\n+\t: >trace2.txt &&\n+\tGIT_TRACE2_EVENT=\"$PWD/trace2.txt\" \\\n+\t\tgit pack-objects --stdout --revs --all --delta-base-offset \\\n+\t\t--no-ref-delta >got.pack &&\n+\n+\ttest_pack_reused 9 <trace2.txt &&\n+\ttest_packs_reused 3 <trace2.txt &&\n+\n+\tgit index-pack --strict -o got.idx got.pack &&\n+\ttest-tool pack-deltas --list-deltas got.idx >deltas &&\n+\ttest_grep ! \" REF_DELTA \" deltas\n+'\n+\n test_expect_success 'reuse objects from first pack with middle gap' '\n \tfor i in D E F\n \tdo\n-- \n2.55.0\n\n"},{"id":"547928","messageId":"alQ7Z8V9gJq2Pq33@com-79390","threadId":"65983","inReplyTo":"cover.1783905084.git.ttaylorr@openai.com","subject":"[PATCH 4/4] send-pack: honor `no-ref-delta` capability","fromName":"Taylor Blau","fromEmail":"ttaylorr@openai.com","sentAt":"2026-07-13T01:12:07Z","receivedAt":"2026-07-13T01:12:11Z","isPatch":true,"body":"Add a 'no-ref-delta' receive-pack capability and teach send-pack to pass\n'--no-ref-delta' to 'pack-objects' when the server advertises it.\n\nKeep this separate from 'ofs-delta' so that a server may request that\n`send-pack` omit `REF_DELTA` without also accepting `OFS_DELTA`.\n\nSigned-off-by: Taylor Blau <ttaylorr@openai.com>\n---\n Documentation/gitprotocol-capabilities.adoc | 17 ++++++++++++++---\n builtin/receive-pack.c                      |  5 +++++\n send-pack.c                                 |  4 ++++\n send-pack.h                                 |  1 +\n t/t5516-fetch-push.sh                       | 14 ++++++++++++++\n 5 files changed, 38 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/gitprotocol-capabilities.adoc b/Documentation/gitprotocol-capabilities.adoc\nindex 2cf7735be4..bbe88defdf 100644\n--- a/Documentation/gitprotocol-capabilities.adoc\n+++ b/Documentation/gitprotocol-capabilities.adoc\n@@ -34,9 +34,9 @@ were sent.  Server MUST NOT ignore capabilities that client requested\n and server advertised.  As a consequence of these rules, server MUST\n NOT advertise capabilities it does not understand.\n \n-The 'atomic', 'report-status', 'report-status-v2', 'delete-refs', 'quiet',\n-and 'push-cert' capabilities are sent and recognized by the receive-pack\n-(push to server) process.\n+The 'atomic', 'report-status', 'report-status-v2', 'delete-refs',\n+'no-ref-delta', 'quiet', and 'push-cert' capabilities are sent and\n+recognized by the receive-pack (push to server) process.\n \n The 'ofs-delta' and 'side-band-64k' capabilities are sent and recognized\n by both upload-pack and receive-pack protocols.  The 'agent' and 'session-id'\n@@ -174,6 +174,17 @@ The server can send, and the client can understand, PACKv2 with delta referring\n its base by position in pack rather than by an obj-id.  That is, they can\n send/read OBJ_OFS_DELTA (aka type 6) in a packfile.\n \n+no-ref-delta\n+------------\n+\n+The receive-pack server can request, and the client can send, PACKv2\n+without deltas referring to their bases by an obj-id. That is, the\n+client MUST NOT send OBJ_REF_DELTA (aka type 7) in a packfile when the\n+server advertises this capability.\n+\n+This does not imply that the server understands OBJ_OFS_DELTA entries;\n+that is negotiated separately with the 'ofs-delta' capability.\n+\n agent\n -----\n \ndiff --git a/builtin/receive-pack.c b/builtin/receive-pack.c\nindex 19eb6a1b61..1c516cbdc6 100644\n--- a/builtin/receive-pack.c\n+++ b/builtin/receive-pack.c\n@@ -66,6 +66,7 @@ static struct strbuf fsck_msg_types = STRBUF_INIT;\n static int receive_unpack_limit = -1;\n static int transfer_unpack_limit = -1;\n static int advertise_atomic_push = 1;\n+static int advertise_no_ref_delta;\n static int advertise_push_options;\n static int advertise_sid;\n static int unpack_limit = 100;\n@@ -290,6 +291,8 @@ static void show_ref(const char *path, const struct object_id *oid)\n \t\t\tstrbuf_addstr(&cap, \" atomic\");\n \t\tif (prefer_ofs_delta)\n \t\t\tstrbuf_addstr(&cap, \" ofs-delta\");\n+\t\tif (advertise_no_ref_delta)\n+\t\t\tstrbuf_addstr(&cap, \" no-ref-delta\");\n \t\tif (push_cert_nonce)\n \t\t\tstrbuf_addf(&cap, \" push-cert=%s\", push_cert_nonce);\n \t\tif (advertise_push_options)\n@@ -2631,6 +2634,8 @@ int cmd_receive_pack(int argc,\n \t\tOPT_HIDDEN_BOOL(0, \"http-backend-info-refs\", &advertise_refs, NULL),\n \t\tOPT_ALIAS(0, \"advertise-refs\", \"http-backend-info-refs\"),\n \t\tOPT_HIDDEN_BOOL(0, \"reject-thin-pack-for-testing\", &reject_thin, NULL),\n+\t\tOPT_HIDDEN_BOOL(0, \"advertise-no-ref-delta-for-testing\",\n+\t\t\t\t&advertise_no_ref_delta, NULL),\n \t\tOPT_END()\n \t};\n \ndiff --git a/send-pack.c b/send-pack.c\nindex 3bb5afc687..2beb1c4be9 100644\n--- a/send-pack.c\n+++ b/send-pack.c\n@@ -80,6 +80,8 @@ static int pack_objects(struct repository *r,\n \t\tstrvec_push(&po.args, \"--thin\");\n \tif (args->use_ofs_delta)\n \t\tstrvec_push(&po.args, \"--delta-base-offset\");\n+\tif (args->no_ref_delta)\n+\t\tstrvec_push(&po.args, \"--no-ref-delta\");\n \tif (args->quiet || !args->progress)\n \t\tstrvec_push(&po.args, \"-q\");\n \tif (args->progress)\n@@ -570,6 +572,8 @@ int send_pack(struct repository *r,\n \t\tallow_deleting_refs = 1;\n \tif (server_supports(\"ofs-delta\"))\n \t\targs->use_ofs_delta = 1;\n+\tif (server_supports(\"no-ref-delta\"))\n+\t\targs->no_ref_delta = 1;\n \tif (server_supports(\"side-band-64k\"))\n \t\tuse_sideband = 1;\n \tif (server_supports(\"quiet\"))\ndiff --git a/send-pack.h b/send-pack.h\nindex 13850c98bb..30be2be0f2 100644\n--- a/send-pack.h\n+++ b/send-pack.h\n@@ -28,6 +28,7 @@ struct send_pack_args {\n \t\tforce_update:1,\n \t\tuse_thin_pack:1,\n \t\tuse_ofs_delta:1,\n+\t\tno_ref_delta:1,\n \t\tdry_run:1,\n \t\t/* One of the SEND_PACK_PUSH_CERT_* constants. */\n \t\tpush_cert:2,\ndiff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh\nindex 1b986349a8..c00074afe8 100755\n--- a/t/t5516-fetch-push.sh\n+++ b/t/t5516-fetch-push.sh\n@@ -1548,6 +1548,20 @@ EOF\n \tgit push --no-thin --receive-pack=\"$rcvpck\" no-thin/.git refs/heads/main:refs/heads/foo\n '\n \n+test_expect_success 'push honors no-ref-delta capability' '\n+\ttest_commit no-ref-delta &&\n+\n+\trcvpck=\"git receive-pack --advertise-no-ref-delta-for-testing\" &&\n+\n+\tGIT_TRACE2_EVENT=\"$PWD/no-ref-delta\" \\\n+\tgit push --receive-pack=\"$rcvpck\" no-thin/.git \\\n+\t\trefs/heads/main:refs/heads/bar &&\n+\n+\ttest_subcommand git pack-objects --all-progress-implied --revs \\\n+\t\t--stdout --thin --delta-base-offset --no-ref-delta -q \\\n+\t\t<no-ref-delta\n+'\n+\n test_expect_success 'pushing a tag pushes the tagged object' '\n \tblob=$(echo unreferenced | git hash-object -w --stdin) &&\n \tgit tag -m foo tag-of-blob $blob &&\n-- \n2.55.0\n"}]}