From 56f410ea377a3984494db30b94cf3b851ba3326c Mon Sep 17 00:00:00 2001 From: Siddharth Shrimali Date: Thu, 6 Aug 2026 16:51:56 +0530 Subject: [PATCH 1/8] builtin/repack.c: add --drop-filtered and --dry-run options Add two new command-line options to 'git-repack': --drop-filtered: intended to eventually delete objects that match the filter specification. Requires --filter and -a, and is incompatible with --filter-to. --dry-run: show which objects would be dropped without making any changes. Only meaningful with --drop-filtered. Keep --dry-run as a separate option rather than folding it into --drop-filtered (e.g --drop-filtered=dry-run), to stay consistent with the --dry-run option other Git commands already provide and to leave room for it to describe other repack behavior later. A --drop-filtered= form can still be added later if more drop-specific modes are needed. --drop-filtered also requires a promisor remote to be configured, since dropping objects without a remote to fetch them back from would be permanent data loss. --drop-filtered is incompatible with bitmap writing: filtering breaks the "all objects in one pack" closure that bitmaps require. Detect an explicit -b/--write-bitmap-index on the command line with a dedicated option callback that sets a "write_bitmaps_given" flag, so it can be distinguished from a repack.writeBitmaps configuration value even when config already enables bitmaps. An explicit -b is reported as a conflict, while a config-provided default is silently disabled for the duration of the command. These options currently only perform validation. The actual enumeration and deletion will be added in follow-up commits. Mentored-by: Christian Couder Mentored-by: Siddharth Asthana Signed-off-by: Siddharth Shrimali Signed-off-by: Junio C Hamano --- builtin/repack.c | 71 ++++++++++++++++++++++++++++++++- t/meson.build | 1 + t/t7706-repack-drop-filtered.sh | 55 +++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 2 deletions(-) create mode 100755 t/t7706-repack-drop-filtered.sh diff --git a/builtin/repack.c b/builtin/repack.c index db504d673f..2e8b7ea45c 100644 --- a/builtin/repack.c +++ b/builtin/repack.c @@ -14,6 +14,7 @@ #include "promisor-remote.h" #include "repack.h" #include "shallow.h" +#include "list-objects-filter-options.h" #define ALL_INTO_ONE 1 #define LOOSEN_UNREACHABLE 2 @@ -28,6 +29,9 @@ static int use_delta_islands; static int run_update_server_info = 1; static char *packdir, *packtmp_name, *packtmp; static int midx_must_contain_cruft = 1; +static int drop_filtered; +static int dry_run; +static int write_bitmaps_given; static const char *const git_repack_usage[] = { N_("git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\n" @@ -111,6 +115,21 @@ static int repack_config(const char *var, const char *value, return git_default_config(var, value, ctx, cb); } +static int option_parse_write_bitmaps(const struct option *opt, const char *arg, + int unset) +{ + int *value = opt->value; + + BUG_ON_OPT_ARG(arg); + if (unset) + *value = 0; + else + *value = 1; + + write_bitmaps_given = 1; + return 0; +} + static int option_parse_write_midx(const struct option *opt, const char *arg, int unset) { @@ -194,8 +213,9 @@ int cmd_repack(int argc, OPT__QUIET(&po_args.quiet, N_("be quiet")), OPT_BOOL('l', "local", &po_args.local, N_("pass --local to git-pack-objects")), - OPT_BOOL('b', "write-bitmap-index", &write_bitmaps, - N_("write bitmap index")), + OPT_CALLBACK_F('b', "write-bitmap-index", &write_bitmaps, NULL, + N_("write bitmap index"), + PARSE_OPT_NOARG, option_parse_write_bitmaps), OPT_BOOL('i', "delta-islands", &use_delta_islands, N_("pass --delta-islands to git-pack-objects")), OPT_STRING(0, "unpack-unreachable", &unpack_unreachable, N_("approxidate"), @@ -231,6 +251,10 @@ int cmd_repack(int argc, N_("pack prefix to store a pack containing pruned objects")), OPT_STRING(0, "filter-to", &filter_to, N_("dir"), N_("pack prefix to store a pack containing filtered out objects")), + OPT_BOOL(0, "drop-filtered", &drop_filtered, + N_("delete filtered out objects (requires --filter)")), + OPT_BOOL(0, "dry-run", &dry_run, + N_("only show which objects would be dropped")), OPT_END() }; @@ -252,6 +276,49 @@ int cmd_repack(int argc, po_args.depth = xstrdup_or_null(opt_depth); po_args.threads = xstrdup_or_null(opt_threads); + die_for_incompatible_opt2(drop_filtered, "--drop-filtered", + !!filter_to, "--filter-to"); + + if (dry_run && !drop_filtered) + die(_("--dry-run only takes effect with --drop-filtered")); + + if (drop_filtered) { + if (!dry_run) + die(_("--drop-filtered doesn't work without --dry-run yet")); + + if (!po_args.filter_options.choice) + die(_("--drop-filtered requires --filter")); + + if (!(pack_everything & ALL_INTO_ONE)) + die(_("--drop-filtered requires -a")); + + /* + * Only blob:limit= is supported for now. Reject other + * filter choices early, before walking the object database. + */ + if (po_args.filter_options.choice != LOFC_BLOB_LIMIT) + die(_("--drop-filtered only supports --filter=blob:limit= for now")); + + /* + * an explicit -b on the command line is a conflict we have to + * report, a bitmap setting from config is silently overridden + * for the duration of the command + */ + if (write_bitmaps_given && write_bitmaps > 0) + die(_("options '%s' and '%s' cannot be used together"), + "--drop-filtered", "--write-bitmap-index"); + + /* + * Without a promisor remote there is nowhere to re-fetch the + * dropped objects from, so dropping them would be permanent + * data loss. + */ + if (!repo_has_promisor_remote(repo)) + die(_("--drop-filtered requires a promisor remote")); + + write_bitmaps = 0; + } + if (delete_redundant && repo->repository_format_precious_objects) die(_("cannot delete packs in a precious-objects repo")); diff --git a/t/meson.build b/t/meson.build index a25f37d2f5..92352e43c4 100644 --- a/t/meson.build +++ b/t/meson.build @@ -964,6 +964,7 @@ integration_tests = [ 't7703-repack-geometric.sh', 't7704-repack-cruft.sh', 't7705-repack-incremental-midx.sh', + 't7706-repack-drop-filtered.sh', 't7800-difftool.sh', 't7810-grep.sh', 't7811-grep-open.sh', diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh new file mode 100755 index 0000000000..f27b09a30e --- /dev/null +++ b/t/t7706-repack-drop-filtered.sh @@ -0,0 +1,55 @@ +#!/bin/sh + +test_description='git repack --drop-filtered option validation' + +. ./test-lib.sh + +# checks for options validations before any promisor walk +test_expect_success 'setup plain repo for validation' ' + git init plain && + test_commit -C plain initial && + git clone --bare plain plain.git && + git -C plain.git repack -a -d +' + +test_expect_success '--drop-filtered requires --filter' ' + test_must_fail git -C plain.git repack --drop-filtered --dry-run -a 2>err && + test_grep "drop-filtered requires --filter" err +' + +test_expect_success '--drop-filtered cannot be used with --filter-to' ' + test_must_fail git -C plain.git repack --drop-filtered \ + --filter=blob:limit=1k --filter-to=./filter-out 2>err && + test_grep "options .--drop-filtered. and .--filter-to. cannot be used together" err +' + +test_expect_success '--dry-run only takes effect with --drop-filtered' ' + test_must_fail git -C plain.git repack --dry-run 2>err && + test_grep "dry-run only takes effect with --drop-filtered" err +' + +test_expect_success '--drop-filtered requires -a' ' + test_must_fail git -C plain.git repack --drop-filtered \ + --filter=blob:limit=1k --dry-run 2>err && + test_grep "drop-filtered requires -a" err +' + +test_expect_success '--drop-filtered fails with --write-bitmap-index' ' + test_must_fail git -C plain.git repack --drop-filtered \ + --filter=blob:limit=1k --dry-run -a -b 2>err && + test_grep "options .--drop-filtered. and .--write-bitmap-index. cannot be used together" err +' + +test_expect_success '--drop-filtered rejects explicit -b even when repack.writeBitmaps=true' ' + test_must_fail git -C plain.git -c repack.writeBitmaps=true \ + repack --drop-filtered --filter=blob:limit=1k --dry-run -a -b 2>err && + test_grep "options .--drop-filtered. and .--write-bitmap-index. cannot be used together" err +' + +test_expect_success '--drop-filtered fails without a promisor remote' ' + test_must_fail git -C plain.git repack --drop-filtered \ + --filter=blob:limit=1k --dry-run -a 2>err && + test_grep "drop-filtered requires a promisor remote" err +' + +test_done From ba07f620d59aa5f4eed8054219e27cd5d67eef46 Mon Sep 17 00:00:00 2001 From: Siddharth Shrimali Date: Thu, 6 Aug 2026 16:51:57 +0530 Subject: [PATCH 2/8] list-objects-filter: add list_objects_filter__filter_oidset() The existing filter entry point, list_objects_filter__filter_object(), is built around the object-walk path: it expects traversal context and provisional omit sets, and is meant to be called as objects are visited during a walk. A caller that already has a set of OIDs in hand and only wants to know which ones a filter would select has no usable entry point into the filter API. --drop-filtered is exactly such a caller: it collects promisor blobs into an oidset and needs to know which of them exceed the filter threshold, without performing an object walk. Add a helper, list_objects_filter__filter_oidset(), that takes a set of OIDs and populates an "omitted" set with those that would be filtered out by the given filter options. Only blob:limit=N filters are supported for now. This helper does not actually reuse the existing filter machinery. It reimplements the blob:limit size check directly. That machinery is tied to the object-walk path and cannot easily be driven from a plain oidset. A NEEDSWORK comment marks this so the helper can later be refactored to reuse the real filter logic instead of duplicating it. OBJECT_INFO_SKIP_FETCH_OBJECT is passed when reading object info so the helper never triggers a lazy fetch. Mentored-by: Christian Couder Mentored-by: Siddharth Asthana Signed-off-by: Siddharth Shrimali Signed-off-by: Junio C Hamano --- list-objects-filter.c | 45 +++++++++++++++++++++++++++++++++++++++++++ list-objects-filter.h | 16 +++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/list-objects-filter.c b/list-objects-filter.c index c912ff3079..6a2e9d5b24 100644 --- a/list-objects-filter.c +++ b/list-objects-filter.c @@ -828,3 +828,48 @@ void list_objects_filter__free(struct filter *filter) filter->free_fn(filter->filter_data); free(filter); } + +/* + * NEEDSWORK: this reimplements the blob:limit size check rather than + * reusing the existing filter machinery in + * list_objects_filter__filter_object(). That machinery is currently + * tied to the object-walk path and cannot easily be driven from a + * plain oidset. It would be nice to refactor the filter code so this + * helper can reuse it instead of duplicating the size check. + */ +int list_objects_filter__filter_oidset(struct repository *r, + struct list_objects_filter_options *opts, + const struct oidset *in, + struct oidset *omitted) +{ + struct oidset_iter iter; + const struct object_id *oid; + + if (opts->choice != LOFC_BLOB_LIMIT) + return error(_("filter_oidset: only blob:limit filters are supported")); + + oidset_iter_init(in, &iter); + while ((oid = oidset_iter_next(&iter))) { + struct object_info info = OBJECT_INFO_INIT; + enum object_type type; + unsigned long size; + + info.typep = &type; + info.sizep = &size; + + /* + * Use OBJECT_INFO_SKIP_FETCH_OBJECT to avoid triggering + * a lazy fetch while inspecting candidates for removal. + */ + if (odb_read_object_info_extended(r->objects, oid, &info, + OBJECT_INFO_SKIP_FETCH_OBJECT) < 0) + continue; + + if (type != OBJ_BLOB) + continue; + + if (size >= opts->blob_limit_value) + oidset_insert(omitted, oid); + } + return 0; +} diff --git a/list-objects-filter.h b/list-objects-filter.h index 9e98814111..56a2d87aa0 100644 --- a/list-objects-filter.h +++ b/list-objects-filter.h @@ -94,4 +94,20 @@ enum list_objects_filter_result list_objects_filter__filter_object( */ void list_objects_filter__free(struct filter *filter); +/* + * Given a set of OIDs in 'in', populate 'omitted' with those that + * would be filtered by 'opts'. Currently only blob:limit=N is + * supported. Objects that cannot be read are silently skipped. + * + * NEEDSWORK: this reimplements the blob:limit size check rather than + * reusing the existing filter machinery. See the matching comment in + * list-objects-filter.c. + * + * Return 0 on success, -1 if the filter is not supported. + */ +int list_objects_filter__filter_oidset(struct repository *r, + struct list_objects_filter_options *opts, + const struct oidset *in, + struct oidset *omitted); + #endif /* LIST_OBJECTS_FILTER_H */ From 948c80041800e5a792243a410ce6edc1b5b54fad Mon Sep 17 00:00:00 2001 From: Siddharth Shrimali Date: Thu, 6 Aug 2026 16:51:58 +0530 Subject: [PATCH 3/8] repack-promisor: allow excluding objects from the rebuilt promisor pack Add a to_drop oidset parameter to repack_promisor_objects(). When it is non-NULL, write_oid() omits those objects from the rebuilt promisor pack. This is the mechanism --drop-filtered will use to remove promisor blobs, i.e. rebuild the promisor pack without them. All existing callers pass NULL, so behavior is unchanged. Mentored-by: Christian Couder Mentored-by: Siddharth Asthana Signed-off-by: Siddharth Shrimali Signed-off-by: Junio C Hamano --- builtin/repack.c | 2 +- repack-promisor.c | 15 ++++++++++++++- repack.h | 4 +++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/builtin/repack.c b/builtin/repack.c index 2e8b7ea45c..0a4dadb896 100644 --- a/builtin/repack.c +++ b/builtin/repack.c @@ -429,7 +429,7 @@ int cmd_repack(int argc, strvec_push(&cmd.args, "--delta-islands"); if (pack_everything & ALL_INTO_ONE) { - repack_promisor_objects(repo, &po_args, &names, packtmp); + repack_promisor_objects(repo, &po_args, &names, packtmp, NULL); if (existing_packs_has_non_kept(&existing) && delete_redundant && diff --git a/repack-promisor.c b/repack-promisor.c index 90318ce150..fabfdc168a 100644 --- a/repack-promisor.c +++ b/repack-promisor.c @@ -6,10 +6,12 @@ #include "path.h" #include "repository.h" #include "run-command.h" +#include "oidset.h" struct write_oid_context { struct child_process *cmd; const struct git_hash_algo *algop; + const struct oidset *to_drop; }; /* @@ -23,6 +25,15 @@ static int write_oid(const struct object_id *oid, struct write_oid_context *ctx = data; struct child_process *cmd = ctx->cmd; + /* + * Objects in to_drop are being removed from the repository, so + * omit them from the rebuilt promisor pack. Each such object is a + * promisor object and therefore remains recoverable from the + * promisor remote. + */ + if (ctx->to_drop && oidset_contains(ctx->to_drop, oid)) + return 0; + if (cmd->in == -1) { if (start_command(cmd)) die(_("could not start pack-objects to repack promisor objects")); @@ -81,7 +92,8 @@ static void finish_repacking_promisor_objects(struct repository *repo, void repack_promisor_objects(struct repository *repo, const struct pack_objects_args *args, - struct string_list *names, const char *packtmp) + struct string_list *names, const char *packtmp, + const struct oidset *to_drop) { struct write_oid_context ctx; struct child_process cmd = CHILD_PROCESS_INIT; @@ -98,6 +110,7 @@ void repack_promisor_objects(struct repository *repo, */ ctx.cmd = &cmd; ctx.algop = repo->hash_algo; + ctx.to_drop = to_drop; odb_for_each_object(repo->objects, NULL, write_oid, &ctx, ODB_FOR_EACH_OBJECT_PROMISOR_ONLY); diff --git a/repack.h b/repack.h index f9fbc895f0..a5a3f7c6ba 100644 --- a/repack.h +++ b/repack.h @@ -3,6 +3,7 @@ #include "list-objects-filter-options.h" #include "string-list.h" +#include "oidset.h" struct pack_objects_args { char *window; @@ -100,7 +101,8 @@ void generated_pack_install(struct generated_pack *pack, const char *name, void repack_promisor_objects(struct repository *repo, const struct pack_objects_args *args, - struct string_list *names, const char *packtmp); + struct string_list *names, const char *packtmp, + const struct oidset *to_drop); struct pack_geometry { struct packed_git **pack; From 0bed9fda6c0fdfe2c57440cb71f9665ebc3db616 Mon Sep 17 00:00:00 2001 From: Siddharth Shrimali Date: Thu, 6 Aug 2026 16:51:59 +0530 Subject: [PATCH 4/8] builtin/repack: enumerate promisor blobs for --drop-filtered Add enumeration logic for --drop-filtered. In --dry-run mode, print the OIDs of locally-held promisor blobs that exceed the filter threshold, as candidates for removal. Reading from write_filtered_pack() cannot work for partial clones. git repack routes promisor objects through a separate path: repack_promisor_objects() repacks them first, and the main pack-objects run uses --exclude-promisor-objects. By the time write_filtered_pack() runs, the promisor blobs are already consumed by the main pack. The filtered pack is always empty on a partial clone. Instead, walk promisor objects directly via odb_for_each_object() with ODB_FOR_EACH_OBJECT_PROMISOR_ONLY, collecting all promisor blobs into an oidset. The blobs exceeding the filter threshold are then selected using list_objects_filter__filter_oidset(). Every object enumerated this way is a promisor object, so it is recoverable from the promisor remote in the same sense as the rest of a partial clone, as long as the remote still has it. This holds without a separate is_promisor_object() check. A future implementation can verify availability against the remote directly once a client-side remote-object-info query exists. OBJECT_INFO_SKIP_FETCH_OBJECT is passed to every object info query so enumeration never triggers a lazy fetch. The enumeration collects candidates into a caller-provided oidset and --dry-run prints them. Actually removing the objects, together with the required promisor-remote verification, is written in a later commit. Mentored-by: Christian Couder Mentored-by: Siddharth Asthana Signed-off-by: Siddharth Shrimali Signed-off-by: Junio C Hamano --- builtin/repack.c | 20 +++++++- repack-filtered.c | 81 +++++++++++++++++++++++++++++++ repack.h | 4 ++ t/t7706-repack-drop-filtered.sh | 84 ++++++++++++++++++++++++++++++++- 4 files changed, 187 insertions(+), 2 deletions(-) diff --git a/builtin/repack.c b/builtin/repack.c index 0a4dadb896..c5f39cef00 100644 --- a/builtin/repack.c +++ b/builtin/repack.c @@ -15,6 +15,8 @@ #include "repack.h" #include "shallow.h" #include "list-objects-filter-options.h" +#include "oidset.h" +#include "hex.h" #define ALL_INTO_ONE 1 #define LOOSEN_UNREACHABLE 2 @@ -159,6 +161,7 @@ int cmd_repack(int argc, struct string_list_item *item; struct string_list names = STRING_LIST_INIT_DUP; struct existing_packs existing = EXISTING_PACKS_INIT; + struct oidset drop_oids = OIDSET_INIT; struct pack_geometry geometry = { 0 }; struct tempfile *refs_snapshot = NULL; int i, ret; @@ -317,6 +320,20 @@ int cmd_repack(int argc, die(_("--drop-filtered requires a promisor remote")); write_bitmaps = 0; + + ret = enumerate_promisor_blobs(repo, &po_args.filter_options, &drop_oids); + + if (ret) + goto cleanup; + + if (dry_run) { + struct oidset_iter iter; + const struct object_id *oid; + + oidset_iter_init(&drop_oids, &iter); + while ((oid = oidset_iter_next(&iter))) + printf("%s\n", oid_to_hex(oid)); + } } if (delete_redundant && repo->repository_format_precious_objects) @@ -612,7 +629,7 @@ int cmd_repack(int argc, } } - if (po_args.filter_options.choice) { + if (po_args.filter_options.choice && !drop_filtered) { struct write_pack_opts opts = { .po_args = &po_args, .destination = filter_to, @@ -705,6 +722,7 @@ int cmd_repack(int argc, cleanup: string_list_clear(&keep_pack_list, 0); string_list_clear(&names, 1); + oidset_clear(&drop_oids); existing_packs_release(&existing); pack_geometry_release(&geometry); pack_objects_args_release(&po_args); diff --git a/repack-filtered.c b/repack-filtered.c index edcf7667c5..79ba6d90aa 100644 --- a/repack-filtered.c +++ b/repack-filtered.c @@ -3,6 +3,12 @@ #include "repository.h" #include "run-command.h" #include "string-list.h" +#include "hex.h" +#include "packfile.h" +#include "list-objects-filter-options.h" +#include "list-objects-filter.h" +#include "odb.h" +#include "promisor-remote.h" int write_filtered_pack(const struct write_pack_opts *opts, struct existing_packs *existing, @@ -49,3 +55,78 @@ int write_filtered_pack(const struct write_pack_opts *opts, return finish_pack_objects_cmd(existing->repo->hash_algo, opts, &cmd, names); } + +struct collect_cb_data { + struct repository *repo; + struct oidset *set; +}; + +static int collect_promisor_blob(const struct object_id *oid, + struct object_info *oi UNUSED, + void *cb_data) +{ + struct collect_cb_data *data = cb_data; + struct object_info info = OBJECT_INFO_INIT; + enum object_type type; + + info.typep = &type; + + /* + * Use OBJECT_INFO_SKIP_FETCH_OBJECT to avoid triggering a + * lazy fetch while collecting promisor blobs. + */ + if (odb_read_object_info_extended(data->repo->objects, oid, &info, + OBJECT_INFO_SKIP_FETCH_OBJECT) < 0) + return 0; + + if (type == OBJ_BLOB) + oidset_insert(data->set, oid); + + return 0; +} + +int enumerate_promisor_blobs(struct repository *repo, + const struct list_objects_filter_options *filter, + struct oidset *to_drop) +{ + struct oidset all_promisor_blobs = OIDSET_INIT; + struct collect_cb_data cb = { + .repo = repo, + .set = &all_promisor_blobs + }; + int ret = 0; + + /* + * The caller (cmd_repack) is responsible for validating that a + * blob:limit filter and a promisor remote are present before + * calling this function. + * + * Walk only promisor objects. every object visited here is a + * promisor object, so it is recoverable from the promisor remote + * as long as the remote still has it, the same assumption the rest + * of partial clone relies on + + * We do not use write_filtered_pack() here because git repack + * routes promisor objects through repack_promisor_objects() + * before the filter machinery runs, so the filtered pack never + * contains promisor blobs. Direct enumeration via + * ODB_FOR_EACH_OBJECT_PROMISOR_ONLY is the correct approach. + */ + ret = odb_for_each_object(repo->objects, NULL, + collect_promisor_blob, &cb, + ODB_FOR_EACH_OBJECT_PROMISOR_ONLY); + if (ret) + goto cleanup; + + /* + * Apply the filter to find which blobs exceed the threshold. + */ + ret = list_objects_filter__filter_oidset(repo, + (struct list_objects_filter_options *)filter, + &all_promisor_blobs, + to_drop); + +cleanup: + oidset_clear(&all_promisor_blobs); + return ret; +} diff --git a/repack.h b/repack.h index a5a3f7c6ba..61e554e4ed 100644 --- a/repack.h +++ b/repack.h @@ -167,6 +167,10 @@ int write_filtered_pack(const struct write_pack_opts *opts, struct existing_packs *existing, struct string_list *names); +int enumerate_promisor_blobs(struct repository *repo, + const struct list_objects_filter_options *filter, + struct oidset *to_drop); + int write_cruft_pack(const struct write_pack_opts *opts, const char *cruft_expiration, unsigned long combine_cruft_below_size, diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh index f27b09a30e..453053cc18 100755 --- a/t/t7706-repack-drop-filtered.sh +++ b/t/t7706-repack-drop-filtered.sh @@ -1,9 +1,36 @@ #!/bin/sh -test_description='git repack --drop-filtered option validation' +test_description='git repack --drop-filtered enumerates filtered promisor blobs' . ./test-lib.sh +delete_object () { + local repo="$1" && + local obj="$2" && + local path="$repo/.git/objects/$(test_oid_to_path "$obj")" && + rm "$path" +} + +# pack the objects into a promisor pack inside "repo". it is a pack +# accompanied by an empty ".promisor" marker file. objects +# in such a pack are treated as recoverable from the promisor remote. +pack_as_from_promisor () { + HASH=$(git -C repo pack-objects .git/objects/pack/pack) && + >repo/.git/objects/pack/pack-$HASH.promisor && + echo $HASH +} + +# write a blob of $1 bytes into "repo", record it as coming from the +# promisor remote, and remove the loose copy so the object is only +# present in the promisor pack +promisor_blob () { + test-tool genrandom "$1" "$2" >blob_content && + OID=$(git -C repo hash-object -w --stdin /dev/null && + delete_object repo "$OID" && + echo "$OID" +} + # checks for options validations before any promisor walk test_expect_success 'setup plain repo for validation' ' git init plain && @@ -52,4 +79,59 @@ test_expect_success '--drop-filtered fails without a promisor remote' ' test_grep "drop-filtered requires a promisor remote" err ' +# enumeration tests using promisor pack +test_expect_success 'setup repo with a promisor remote' ' + rm -rf repo && + test_create_repo repo && + test_commit -C repo base && + + # mark the repo as a partial clone with a promisor remote so the + # promisor walk and the safety guard are satisfied + git -C repo config core.repositoryformatversion 1 && + git -C repo config extensions.partialclone origin && + git -C repo config remote.origin.promisor true && + git -C repo config remote.origin.url "." && + + BIG=$(promisor_blob big 3072) && + SMALL=$(promisor_blob small 512) && + echo "$BIG" >big_oid && + echo "$SMALL" >small_oid +' + +test_expect_success 'promisor blob over the threshold is listed' ' + BIG=$(cat big_oid) && + SMALL=$(cat small_oid) && + + git -C repo -c repack.writeBitmaps=false \ + repack --drop-filtered --filter=blob:limit=1k --dry-run -a >out && + + test_grep "$BIG" out && + test_grep ! "$SMALL" out +' + +test_expect_success 'locally created blob is never listed' ' + BIG=$(cat big_oid) && + + # large blob that exists only locally must never be a drop candidate. + # dropping it would be unrecoverable + test-tool genrandom local 4096 >local_content && + LOCAL=$(git -C repo hash-object -w --stdin out && + + test_grep "$BIG" out && + test_grep ! "$LOCAL" out +' + +test_expect_success '--dry-run does not remove the filtered objects' ' + BIG=$(cat big_oid) && + + git -C repo -c repack.writeBitmaps=false \ + repack --drop-filtered --filter=blob:limit=1k --dry-run -a >out && + + # candidate blob must still be present after a dry run + git -C repo cat-file -e "$BIG" +' + test_done From 79f361aeb10fe775dd96be6a6a5d7de94be1b6bf Mon Sep 17 00:00:00 2001 From: Siddharth Shrimali Date: Thu, 6 Aug 2026 16:52:00 +0530 Subject: [PATCH 5/8] builtin/repack: actually drop filtered promisor blobs Make --drop-filtered remove the enumerated promisor blobs instead of only listing them. The drop set is computed before repack_promisor_objects() runs, and on a real run it is passed in so the rebuilt promisor pack omits those blobs. --drop-filtered implies -d so the old promisor packs, which still contain the dropped blobs, are removed. Without this the blobs would survive in the redundant packs. The existing repack machinery performs the write-before-delete and fsync, so the drop is crash-safe. The dropped blobs become absent locally but remain recoverable from the promisor remote, so a later access lazy-fetches them back transparently. --dry-run keeps its previous behavior, i.e. it lists the candidates and changes nothing. Mentored-by: Christian Couder Mentored-by: Siddharth Asthana Signed-off-by: Siddharth Shrimali Signed-off-by: Junio C Hamano --- builtin/repack.c | 14 ++++++++++---- repack-filtered.c | 1 + t/t7706-repack-drop-filtered.sh | 12 ++++++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/builtin/repack.c b/builtin/repack.c index c5f39cef00..a20589a7ae 100644 --- a/builtin/repack.c +++ b/builtin/repack.c @@ -286,9 +286,6 @@ int cmd_repack(int argc, die(_("--dry-run only takes effect with --drop-filtered")); if (drop_filtered) { - if (!dry_run) - die(_("--drop-filtered doesn't work without --dry-run yet")); - if (!po_args.filter_options.choice) die(_("--drop-filtered requires --filter")); @@ -321,6 +318,14 @@ int cmd_repack(int argc, write_bitmaps = 0; + /* + * Dropping objects means rebuilding the promisor packs + * without them and then removing the old packs, so the + * redundant packs must be deleted. Imply -d on a real run. + */ + if (!dry_run) + delete_redundant = 1; + ret = enumerate_promisor_blobs(repo, &po_args.filter_options, &drop_oids); if (ret) @@ -446,7 +451,8 @@ int cmd_repack(int argc, strvec_push(&cmd.args, "--delta-islands"); if (pack_everything & ALL_INTO_ONE) { - repack_promisor_objects(repo, &po_args, &names, packtmp, NULL); + repack_promisor_objects(repo, &po_args, &names, packtmp, + (drop_filtered && !dry_run) ? &drop_oids : NULL); if (existing_packs_has_non_kept(&existing) && delete_redundant && diff --git a/repack-filtered.c b/repack-filtered.c index 79ba6d90aa..e6c35c23de 100644 --- a/repack-filtered.c +++ b/repack-filtered.c @@ -120,6 +120,7 @@ int enumerate_promisor_blobs(struct repository *repo, /* * Apply the filter to find which blobs exceed the threshold. + * The caller has to_drop and is responsible for clearing it. */ ret = list_objects_filter__filter_oidset(repo, (struct list_objects_filter_options *)filter, diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh index 453053cc18..88c2bb0857 100755 --- a/t/t7706-repack-drop-filtered.sh +++ b/t/t7706-repack-drop-filtered.sh @@ -134,4 +134,16 @@ test_expect_success '--dry-run does not remove the filtered objects' ' git -C repo cat-file -e "$BIG" ' +test_expect_success '--drop-filtered removes the promisor blob locally' ' + BIG=$(cat big_oid) && + SMALL=$(cat small_oid) && + + git -C repo -c repack.writeBitmaps=false \ + repack --drop-filtered --filter=blob:limit=1k -a && + + git -C repo cat-file --batch-all-objects --batch-check="%(objectname)" >present && + ! grep -q "$BIG" present && + grep -q "$SMALL" present +' + test_done From a0478d439a80c44153412ef1207e5820bea8523e Mon Sep 17 00:00:00 2001 From: Siddharth Shrimali Date: Thu, 6 Aug 2026 16:52:01 +0530 Subject: [PATCH 6/8] builtin/repack: add guards for --drop-filtered --drop-filtered removes local promisor blobs. That is only safe when the repository is not mid-operation and when the blobs are not actively in use, so add two guards, both skipped for bare repositories which have neither a worktree nor an index. First, refuse to run while a merge, rebase, am, cherry-pick, revert, or bisect is in progress. During these operations the working tree and index are in an intermediate state, and rewriting packs and deleting objects underneath a half-finished operation is unsafe. Second, refuse to drop a blob that the current index references. Such a blob is needed by the working tree, so dropping it would only cause the next command that touches the worktree to lazy-fetch it straight back, reclaiming nothing. The offending path is reported so the user can see why the drop was refused. Mentored-by: Christian Couder Mentored-by: Siddharth Asthana Signed-off-by: Siddharth Shrimali Signed-off-by: Junio C Hamano --- builtin/repack.c | 49 +++++++++++++++++++++++++++++++++ t/t7706-repack-drop-filtered.sh | 36 ++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/builtin/repack.c b/builtin/repack.c index a20589a7ae..04ad03d69d 100644 --- a/builtin/repack.c +++ b/builtin/repack.c @@ -17,6 +17,8 @@ #include "list-objects-filter-options.h" #include "oidset.h" #include "hex.h" +#include "wt-status.h" +#include "read-cache-ll.h" #define ALL_INTO_ONE 1 #define LOOSEN_UNREACHABLE 2 @@ -316,6 +318,30 @@ int cmd_repack(int argc, if (!repo_has_promisor_remote(repo)) die(_("--drop-filtered requires a promisor remote")); + /* + * refuse to run while another operation is in progress. A + * dropped object would just be lazily re-fetched when the + * operation resumes, but triggering a network fetch in the + * middle of a half-finished + * merge/rebase/cherry-pick/revert/bisect is a poor + * experience, so this is a UX convenience rather than a + * safety measure. Bare repositories have no such state, so + * the check is skipped there. + */ + if (!is_bare_repository(repo)) { + struct wt_status_state state = { 0 }; + + wt_status_get_state(repo, &state, 0); + if (state.merge_in_progress || state.revert_in_progress || + state.rebase_in_progress ||state.bisect_in_progress || + state.cherry_pick_in_progress ||state.am_in_progress|| + state.rebase_interactive_in_progress) { + wt_status_state_free_buffers(&state); + die(_("--drop-filtered cannot be used while another operation is in progress")); + } + wt_status_state_free_buffers(&state); + } + write_bitmaps = 0; /* @@ -331,6 +357,29 @@ int cmd_repack(int argc, if (ret) goto cleanup; + /* + * refuse to drop blobs that the current index references. + * such a blob would only be lazily re-fetched by the next + * command that touches the worktree, so dropping it reclaims + * nothing. This guard just avoids that churn. bare + * repositories have no index, so the check is skipped there. + */ + if (!is_bare_repository(repo) && oidset_size(&drop_oids)) { + struct index_state *istate = repo->index; + unsigned int i; + + if (repo_read_index(repo) < 0) + die(_("could not read the index")); + + for (i = 0; i < istate->cache_nr; i++) { + const struct cache_entry *ce = istate->cache[i]; + + if (oidset_contains(&drop_oids, &ce->oid)) + die(_("cannot drop '%s' (%s): it is referenced by the current index"), + ce->name, oid_to_hex(&ce->oid)); + } + } + if (dry_run) { struct oidset_iter iter; const struct object_id *oid; diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh index 88c2bb0857..6774886f1e 100755 --- a/t/t7706-repack-drop-filtered.sh +++ b/t/t7706-repack-drop-filtered.sh @@ -146,4 +146,40 @@ test_expect_success '--drop-filtered removes the promisor blob locally' ' grep -q "$SMALL" present ' +test_expect_success '--drop-filtered refuses when a merge is in progress' ' + test_when_finished "git -C repo merge --abort || :" && + + # creat a conflicting merge so wt_status reports it + git -C repo checkout -B mergebase base && + echo one >repo/conflict.txt && + git -C repo add conflict.txt && + git -C repo commit -m one && + + git -C repo checkout -B mergeother base && + echo two >repo/conflict.txt && + git -C repo add conflict.txt && + git -C repo commit -m two && + + test_must_fail git -C repo merge mergebase && + + test_must_fail git -C repo -c repack.writeBitmaps=false \ + repack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err && + test_grep "in progress" err +' + + +test_expect_success '--drop-filtered refuses to drop an index-referenced blob' ' + # create a large blob, add it to the index and make it a promisor object + # so the index references it and enumeration picks it up + test-tool genrandom idx 4096 >repo/tracked-big.bin && + git -C repo add tracked-big.bin && + OID=$(git -C repo rev-parse :tracked-big.bin) && + printf "%s\n" "$OID" | pack_as_from_promisor >/dev/null && + delete_object repo "$OID" && + + test_must_fail git -C repo -c repack.writeBitmaps=false \ + repack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err && + test_grep "referenced by the current index" err +' + test_done From 7e4c047af995c134dac3270c013290fc701ce5f7 Mon Sep 17 00:00:00 2001 From: Siddharth Shrimali Date: Thu, 6 Aug 2026 16:52:02 +0530 Subject: [PATCH 7/8] Documentation/git-repack: document --drop-filtered and --dry-run Describe the new --drop-filtered and --dry-run options: what they do, only blob:limit filters are supported for now, a promisor remote is required, --drop-filtered requires -a and implies -d so the redundant packs are actually removed, its incompatibilities with --filter-to and bitmap writing, and the safety guards that refuse to run mid-operation or to drop index-referenced blobs. Mentored-by: Christian Couder Mentored-by: Siddharth Asthana Signed-off-by: Siddharth Shrimali Signed-off-by: Junio C Hamano --- Documentation/git-repack.adoc | 37 +++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Documentation/git-repack.adoc b/Documentation/git-repack.adoc index 72c42015e2..4c6aa3bc18 100644 --- a/Documentation/git-repack.adoc +++ b/Documentation/git-repack.adoc @@ -12,6 +12,7 @@ SYNOPSIS 'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m] [--window=] [--depth=] [--threads=] [--keep-pack=] [--write-midx[=]] [--name-hash-version=] [--path-walk] + [--filter=] [--drop-filtered [--dry-run]]] DESCRIPTION ----------- @@ -182,6 +183,42 @@ depth is 4095. `objects` and `objects/info/alternates` sections of linkgit:gitrepository-layout[5]. +--drop-filtered:: + Delete the local objects that match the `--filter` specification + instead of keeping them in a separate packfile, reclaiming the + disk space they occupy. This is intended for partial clones, + where the filtered objects are promisor objects that remain + recoverable from the promisor remote and are lazily re-fetched + on demand when they are next needed. ++ +Only large blobs are supported for now, so `--filter=blob:limit=` +is currently the only accepted filter. Because dropped objects must be +recoverable, this option requires a promisor remote to be configured +and refuses to run otherwise. ++ +This option requires `-a`, and implies `-d`: the objects are dropped by +rebuilding the promisor pack without them and then removing the now +redundant old packs, so the redundant packs must be deleted for the +space to actually be reclaimed. It is incompatible with `--filter-to` +and with bitmap writing (`-b`/`--write-bitmap-index`), since filtering +breaks the single-pack closure that bitmaps require. A bitmap setting +coming from configuration is silently disabled for the duration of the +command. ++ +As a convenience since dropped objects remain recoverable by lazy fetch, +`--drop-filtered` refuses to run while another operation +(merge, rebase, am, cherry-pick, revert, or bisect) is in progress, to +avoid a surprising network fetch mid-operation, and refuses to drop any +blob that the current index references, since such a blob would only be +lazily re-fetched by the next command that inspects the working tree. +These checks are skipped in bare repositories, which have neither a +working tree nor an index. + +--dry-run:: + Only meaningful with `--drop-filtered`. List the objects that + would be dropped, one object ID per line, without rebuilding any + pack or deleting anything. + -b:: --write-bitmap-index:: Write a reachability bitmap index as part of the repack. This From 8f2ee04a2e8f14dedd59be902d1822b7951ecae0 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Thu, 6 Aug 2026 15:40:23 -0700 Subject: [PATCH 8/8] SQUASH??? --- Documentation/git-repack.adoc | 2 +- builtin/repack.c | 3 ++- t/t7706-repack-drop-filtered.sh | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Documentation/git-repack.adoc b/Documentation/git-repack.adoc index 4c6aa3bc18..63943b078c 100644 --- a/Documentation/git-repack.adoc +++ b/Documentation/git-repack.adoc @@ -12,7 +12,7 @@ SYNOPSIS 'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m] [--window=] [--depth=] [--threads=] [--keep-pack=] [--write-midx[=]] [--name-hash-version=] [--path-walk] - [--filter=] [--drop-filtered [--dry-run]]] + [--filter=] [--drop-filtered [--dry-run]] DESCRIPTION ----------- diff --git a/builtin/repack.c b/builtin/repack.c index 04ad03d69d..0a4a88b89c 100644 --- a/builtin/repack.c +++ b/builtin/repack.c @@ -40,7 +40,8 @@ static int write_bitmaps_given; static const char *const git_repack_usage[] = { N_("git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\n" "[--window=] [--depth=] [--threads=] [--keep-pack=]\n" - "[--write-midx[=]] [--name-hash-version=] [--path-walk]"), + "[--write-midx[=]] [--name-hash-version=] [--path-walk]\n" + "[--filter=] [--drop-filtered [--dry-run]]"), NULL }; diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh index 6774886f1e..05d58fa456 100755 --- a/t/t7706-repack-drop-filtered.sh +++ b/t/t7706-repack-drop-filtered.sh @@ -142,8 +142,8 @@ test_expect_success '--drop-filtered removes the promisor blob locally' ' repack --drop-filtered --filter=blob:limit=1k -a && git -C repo cat-file --batch-all-objects --batch-check="%(objectname)" >present && - ! grep -q "$BIG" present && - grep -q "$SMALL" present + test_grep ! "$BIG" present && + test_grep "$SMALL" present ' test_expect_success '--drop-filtered refuses when a merge is in progress' '