pack-objects: introduce `--no-ref-delta`

Some consumers of 'pack-objects' may wish to avoid packs which contain `REF_DELTA` entries. For instance, a 'receive-pack' implementation which retains the resulting pack without building an index of object IDs may prefer every delta base to be discoverable from an earlier entry in the same pack. Teach 'pack-objects' a new `--no-ref-delta` option to avoid writing `REF_DELTA` entries, without changing whether `OFS_DELTA` is allowed. When used without `--delta-base-offset`, no delta representation remains, so avoid delta search entirely. Otherwise, allow new deltas whose bases appear earlier in the same pack. For now, disable delta- and bitmap-reuse under `--no-ref-delta`, since either may copy an existing `REF_DELTA` entry. This is overly pessimistic, but simplifies the changes in this commit. The next commit re-enables reuse in the cases which do not require `REF_DELTA`. Signed-off-by: Taylor Blau <ttaylorr@openai.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Taylor Blau committed Jul 12, 2026 at 18:11 UTC 5e4971b28ec3953094dbfe874fdd7baa7f8d1d47
3 files changed +71 -5
Documentation/git-pack-objects.adoc
+7 -1
@@ -10,7 +10,8 @@ SYNOPSIS
10 --------
11 [verse]
12 'git pack-objects' [-q | --progress | --all-progress] [--all-progress-implied]
13 - [--no-reuse-delta] [--delta-base-offset] [--non-empty]
13 + [--no-reuse-delta] [--delta-base-offset] [--no-ref-delta]
14 + [--non-empty]
15 [--local] [--incremental] [--window=<n>] [--depth=<n>]
16 [--revs [--unpacked | --all]] [--keep-pack=<pack-name>]
17 [--cruft] [--cruft-expiration=<time>]
@@ -297,6 +298,11 @@ Note: Porcelain commands such as `git gc` (see linkgit:git-gc[1]),
298 in modern Git when they put objects in your repository into pack files.
299 So does `git bundle` (see linkgit:git-bundle[1]) when it creates a bundle.
300
301 +--no-ref-delta::
302 + Do not emit deltas which represent their base by their literal
303 + object ID. This is independent of `--delta-base-offset`;
304 + without that option, no deltas are emitted.
305 +
306 --threads=<n>::
307 Specifies the number of threads to spawn when searching for best
308 delta matches. This requires that pack-objects be compiled with
builtin/pack-objects.c
+12 -4
@@ -190,7 +190,8 @@ static inline void oe_set_delta_size(struct packing_data *pack,
190
191 static const char *const pack_usage[] = {
192 N_("git pack-objects [-q | --progress | --all-progress] [--all-progress-implied]\n"
193 - " [--no-reuse-delta] [--delta-base-offset] [--non-empty]\n"
193 + " [--no-reuse-delta] [--delta-base-offset] [--no-ref-delta]\n"
194 + " [--non-empty]\n"
195 " [--local] [--incremental] [--window=<n>] [--depth=<n>]\n"
196 " [--revs [--unpacked | --all]] [--keep-pack=<pack-name>]\n"
197 " [--cruft] [--cruft-expiration=<time>]\n"
@@ -221,6 +222,7 @@ static int ignore_packed_keep_in_core;
222 static int ignore_packed_keep_in_core_open;
223 static int ignore_packed_keep_in_core_has_cruft;
224 static int allow_ofs_delta;
225 +static int allow_ref_delta = 1;
226 static struct pack_idx_option pack_idx_opts;
227 static const char *base_name;
228 static int progress = 1;
@@ -3405,6 +3407,9 @@ static int should_attempt_deltas(struct object_entry *entry)
3407 if (entry->no_try_delta)
3408 return 0;
3409
3410 + if (entry->preferred_base && !allow_ref_delta)
3411 + return 0;
3412 +
3413 if (!entry->preferred_base) {
3414 if (oe_type(entry) < 0)
3415 die(_("unable to get type of object %s"),
@@ -3647,7 +3652,8 @@ static void prepare_pack(int window, int depth)
3652 if (!pack_to_stdout)
3653 do_check_packed_object_crc = 1;
3654
3650 - if (!to_pack.nr_objects || !window || !depth)
3655 + if (!to_pack.nr_objects || !window || !depth ||
3656 + (!allow_ref_delta && !allow_ofs_delta))
3657 return;
3658
3659 if (path_walk)
@@ -4662,7 +4668,7 @@ static int pack_options_allow_reuse(void)
4668 !ignore_packed_keep_on_disk &&
4669 !ignore_packed_keep_in_core &&
4670 (!local || !have_non_local_packs) &&
4665 - !incremental;
4671 + !incremental && allow_ref_delta;
4672 }
4673
4674 static int get_object_list_from_bitmap(struct rev_info *revs)
@@ -5111,6 +5117,8 @@ int cmd_pack_objects(int argc,
5117 N_("reuse existing objects")),
5118 OPT_BOOL(0, "delta-base-offset", &allow_ofs_delta,
5119 N_("use OFS_DELTA objects")),
5120 + OPT_BOOL(0, "ref-delta", &allow_ref_delta,
5121 + N_("use REF_DELTA objects")),
5122 OPT_INTEGER(0, "threads", &delta_search_threads,
5123 N_("use threads when searching for best delta matches")),
5124 OPT_BOOL(0, "non-empty", &non_empty,
@@ -5309,7 +5317,7 @@ int cmd_pack_objects(int argc,
5317 if (unpack_unreachable || keep_unreachable || pack_loose_unreachable)
5318 use_internal_rev_list = 1;
5319
5312 - if (!reuse_object)
5320 + if (!reuse_object || !allow_ref_delta)
5321 reuse_delta = 0;
5322 if (cfg->pack_compression_level == -1)
5323 cfg->pack_compression_level = Z_DEFAULT_COMPRESSION;
t/t5300-pack-object.sh
+52
@@ -211,6 +211,58 @@ test_expect_success 'pack with OFS_DELTA' '
211 test_grep " OFS_DELTA " deltas
212 '
213
214 +test_expect_success 'pack without REF_DELTA' '
215 + git pack-objects --no-ref-delta --stdout <obj-list >no-ref.pack &&
216 + git index-pack -o no-ref.idx no-ref.pack &&
217 +
218 + test-tool pack-deltas --list-deltas no-ref.idx >deltas &&
219 + test_must_be_empty deltas
220 +'
221 +
222 +test_expect_success 'pack without REF_DELTA with OFS_DELTA' '
223 + git pack-objects --delta-base-offset --no-ref-delta --stdout \
224 + <obj-list >no-ref-ofs.pack &&
225 + git index-pack -o no-ref-ofs.idx no-ref-ofs.pack &&
226 +
227 + test-tool pack-deltas --list-deltas no-ref-ofs.idx >deltas &&
228 + test_grep " OFS_DELTA " deltas &&
229 + test_grep ! " REF_DELTA " deltas
230 +'
231 +
232 +test_expect_success 'pack without REF_DELTA skips excluded delta bases' '
233 + test_when_finished "git read-tree $tree" &&
234 +
235 + echo bar >>d &&
236 + git update-index --add d &&
237 + thin_tree=$(git write-tree) &&
238 + thin_commit=$(git commit-tree $thin_tree -p $commit </dev/null) &&
239 +
240 + {
241 + echo $thin_commit &&
242 + echo ^$commit
243 + } >thin-revs &&
244 +
245 + # Each type appears only once in the output, so any delta must
246 + # use an excluded base and therefore be a REF_DELTA.
247 + git pack-objects --thin --stdout --revs \
248 + <thin-revs >thin.pack &&
249 + git index-pack --fix-thin --stdin thin-fixed.pack \
250 + <thin.pack >/dev/null &&
251 +
252 + test-tool pack-deltas --list-deltas thin-fixed.idx >deltas &&
253 + test_grep ! " OFS_DELTA " deltas &&
254 + test_grep " REF_DELTA " deltas &&
255 +
256 + git pack-objects --thin --stdout --revs \
257 + --delta-base-offset --no-ref-delta \
258 + <thin-revs >no-ref-thin.pack &&
259 + git index-pack --fix-thin --stdin no-ref-thin-fixed.pack \
260 + <no-ref-thin.pack >/dev/null &&
261 +
262 + test-tool pack-deltas --list-deltas no-ref-thin-fixed.idx >deltas &&
263 + test_must_be_empty deltas
264 +'
265 +
266 test_expect_success 'unpack with OFS_DELTA' '
267 check_unpack test-3-${packname_3} obj-list
268 '