{"thread":{"id":"66019","subject":"[RFC PATCH 0/7] repack: add --drop-filtered to reclaim space in partial clones","startedAt":"2026-07-16T13:29:09Z","lastAt":"2026-09-04T10:52:12Z","messageCount":76,"participants":["Siddharth Shrimali","Junio C Hamano","Christian Couder","Siddharth Asthana","Samuel Bronson"],"isPatch":true,"patchVersion":1,"patchTotal":7},"messages":[{"id":"548404","messageId":"20260716132848.95982-1-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":null,"subject":"[RFC PATCH 0/7] repack: add --drop-filtered to reclaim space in partial clones","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-07-16T13:28:41Z","receivedAt":"2026-07-16T13:29:09Z","isPatch":true,"body":"This is an RFC series seeking feedback on the design and approach.\nSeveral pieces are still missing (noted below) and the commit\norganization needs cleanup.\n\nPartial clones let you work with large repositories without downloading\nevery blob up front and the missing blobs are lazily fetched from the promisor\nremote on demand. Over time, though, these lazily-fetched blobs\naccumulate locally and there is currently no safe, built-in way to\nreclaim that disk space instead of re-cloning.\n\nThis series adds a \"git repack --drop-filtered --filter=<spec>\" command\nthat removes large, locally-held promisor blobs that are recoverable\nfrom the promisor remote. The dropped blobs become absent locally but\nremain lazily re-fetchable, making the partial-clone still reversible.\n\nHow it works:\n  * Enumerate promisor objects directly (ODB_FOR_EACH_OBJECT_PROMISOR_ONLY)\n    and select the blobs exceeding the filter threshold. Because every\n    enumerated object is a promisor object, it is guaranteed recoverable and\n    locally-created objects are never candidates.\n\n  * Rebuild the promisor pack without the selected blobs, reusing the\n    existing repack machinery, so the drop is crash-safe.\n\n  * Record each dropped object in a drop log\n    ($GIT_DIR/objects/info/promisor-dropped) so a later change can\n    explain a failed lazy fetch (when it was dropped, which filter\n    matched, which remotes) instead of a bare \"could not fetch\" error.\n\n  * --dry-run lists the candidates and changes nothing.\n\nPlanned follow-ups:\n  * Safety guards: refuse to run while a merge/rebase/cherry-pick is in\n    progress, and refuse to drop blobs referenced by the current index.\n\n  * Authoritative remote verification: the drop log currently lists all\n    configured promisor remotes rather than the exact remote each object\n    is recoverable from, because there is no client-side way to query a\n    remote for object availability yet. A \"remote-object-info\" command\n    is being added to the \"git cat-file --batch\" protocol for this. Once\n    available, the exact remote can be recorded.\n\nKnown issues to address in v2:\n  * There is churn between \"enumerate promisor blobs\" and \"actually drop\n    filtered promisor blobs\". The former introduces\n    enumerate_promisor_blobs() with an interim signature that the latter\n    rewrites. These will be reorganized so the function is introduced\n    in its final form.\n\n  * The tests are in a standalone commit. They will instead be\n    distributed into the commits that introduce the behavior they test.\n\nSiddharth Shrimali (7):\n  builtin/repack.c: add --drop-filtered and --dry-run options\n  list-objects-filter: add list_objects_filter__filter_oidset()\n  repack-promisor: allow excluding objects from the rebuilt promisor\n    pack\n  builtin/repack: enumerate promisor blobs for --drop-filtered\n  t7706: test --drop-filtered enumeration and validation\n  builtin/repack: actually drop filtered promisor blobs\n  repack-promisor: record dropped objects in a drop log\n\n builtin/repack.c                |  76 ++++++++++++++++-\n list-objects-filter.c           |  45 ++++++++++\n list-objects-filter.h           |  16 ++++\n repack-filtered.c               |  81 ++++++++++++++++++\n repack-promisor.c               | 106 ++++++++++++++++++++++-\n repack.h                        |  12 ++-\n t/meson.build                   |   1 +\n t/t7706-repack-drop-filtered.sh | 145 ++++++++++++++++++++++++++++++++\n 8 files changed, 478 insertions(+), 4 deletions(-)\n create mode 100755 t/t7706-repack-drop-filtered.sh\n\n-- \n2.54.0\n\n"},{"id":"548405","messageId":"20260716132848.95982-2-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260716132848.95982-1-r.siddharth.shrimali@gmail.com","subject":"[RFC PATCH 1/7] builtin/repack.c: add --drop-filtered and --dry-run options","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-07-16T13:28:42Z","receivedAt":"2026-07-16T13:29:15Z","isPatch":true,"body":"Add two new command-line options to 'git-repack':\n\n  --drop-filtered: intended to eventually delete objects that match\n                   the filter specification. Requires --filter and -a,\n                   and is incompatible with --filter-to.\n  --dry-run: show which objects would be dropped without making any\n             changes. Only meaningful with --drop-filtered.\n\n--drop-filtered also requires a promisor remote to be configured,\nsince dropping objects without a remote to fetch them back from would\nbe permanent data loss.\n\n--drop-filtered is incompatible with bitmap writing: filtering breaks\nthe \"all objects in one pack\" closure that bitmaps require. An explicit\n-b is rejected with a clear error and a default-on bitmap configuration is\nsilently disabled for the duration of the command.\n\nThese options currently only perform validation. The actual enumeration\nand deletion will be added in follow-up commits.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n builtin/repack.c | 44 ++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 44 insertions(+)\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex db504d673f..f4db0fc535 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -14,6 +14,7 @@\n #include \"promisor-remote.h\"\n #include \"repack.h\"\n #include \"shallow.h\"\n+#include \"list-objects-filter-options.h\"\n \n #define ALL_INTO_ONE 1\n #define LOOSEN_UNREACHABLE 2\n@@ -28,6 +29,8 @@ static int use_delta_islands;\n static int run_update_server_info = 1;\n static char *packdir, *packtmp_name, *packtmp;\n static int midx_must_contain_cruft = 1;\n+static int drop_filtered;\n+static int dry_run;\n \n static const char *const git_repack_usage[] = {\n \tN_(\"git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\\n\"\n@@ -231,6 +234,10 @@ int cmd_repack(int argc,\n \t\t\t   N_(\"pack prefix to store a pack containing pruned objects\")),\n \t\tOPT_STRING(0, \"filter-to\", &filter_to, N_(\"dir\"),\n \t\t\t   N_(\"pack prefix to store a pack containing filtered out objects\")),\n+\t\tOPT_BOOL(0, \"drop-filtered\", &drop_filtered,\n+\t\t\t\tN_(\"delete filtered out objects (requires --filter)\")),\n+\t\tOPT_BOOL(0, \"dry-run\", &dry_run,\n+\t\t\t\tN_(\"only show which objects would be dropped\")),\n \t\tOPT_END()\n \t};\n \n@@ -252,6 +259,43 @@ int cmd_repack(int argc,\n \tpo_args.depth = xstrdup_or_null(opt_depth);\n \tpo_args.threads = xstrdup_or_null(opt_threads);\n \n+\tdie_for_incompatible_opt2(drop_filtered, \"--drop-filtered\",\n+\t\t!!filter_to, \"--filter-to\");\n+\n+\tdie_for_incompatible_opt2(drop_filtered, \"--drop-filtered\",\n+\t\twrite_bitmaps > 0, \"--write-bitmap-index\");\n+\n+\tif (dry_run && !drop_filtered)\n+\t\tdie(_(\"--dry-run only takes effect with --drop-filtered\"));\n+\n+\tif (drop_filtered) {\n+\t\tif (!dry_run)\n+\t\t\tdie(_(\"--drop-filtered doesn't work without --dry-run yet\"));\n+\n+\t\tif (!po_args.filter_options.choice)\n+\t\t\tdie(_(\"--drop-filtered requires --filter\"));\n+\n+\t\tif (!(pack_everything & ALL_INTO_ONE))\n+\t\t\tdie(_(\"--drop-filtered requires -a\"));\n+\n+\t\t/*\n+\t\t * Only blob:limit=<n> is supported for now. Reject other\n+\t\t * filter choices early, before walking the object database.\n+\t\t */\n+\t\tif (po_args.filter_options.choice != LOFC_BLOB_LIMIT)\n+\t\t\tdie(_(\"--drop-filtered only supports --filter=blob:limit=<n> for now\"));\n+\n+\t\t/*\n+\t\t * Without a promisor remote there is nowhere to re-fetch the\n+\t\t * dropped objects from, so dropping them would be permanent\n+\t\t * data loss.\n+\t\t */\n+\t\tif (!repo_has_promisor_remote(repo))\n+\t\t\tdie(_(\"--drop-filtered requires a promisor remote\"));\n+\n+\t\twrite_bitmaps = 0;\n+\t}\n+\n \tif (delete_redundant && repo->repository_format_precious_objects)\n \t\tdie(_(\"cannot delete packs in a precious-objects repo\"));\n \n-- \n2.54.0\n\n"},{"id":"548406","messageId":"20260716132848.95982-3-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260716132848.95982-1-r.siddharth.shrimali@gmail.com","subject":"[RFC PATCH 2/7] list-objects-filter: add list_objects_filter__filter_oidset()","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-07-16T13:28:43Z","receivedAt":"2026-07-16T13:29:24Z","isPatch":true,"body":"The existing filter entry point, list_objects_filter__filter_object(),\nis built around the object-walk path: it expects traversal context and\nprovisional omit sets, and is meant to be called as objects are\nvisited during a walk. A caller that already has a set of OIDs in hand\nand only wants to know which ones a filter would select has no usable\nentry point into the filter API.\n\n--drop-filtered is exactly such a caller: it collects promisor blobs\ninto an oidset and needs to know which of them exceed the filter\nthreshold, without performing an object walk.\n\nAdd a helper, list_objects_filter__filter_oidset(), that takes a set\nof OIDs and populates an \"omitted\" set with those that would be\nfiltered out by the given filter options. Only blob:limit=N filters\nare supported for now.\n\nThis helper does not actually reuse the existing filter machinery.\nIt reimplements the blob:limit size check directly. That machinery\nis tied to the object-walk path and cannot easily be driven\nfrom a plain oidset. A NEEDSWORK comment marks this so the helper can\nlater be refactored to reuse the real filter logic instead of\nduplicating it.\n\nOBJECT_INFO_SKIP_FETCH_OBJECT is passed when reading object info so\nthe helper never triggers a lazy fetch.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n list-objects-filter.c | 45 +++++++++++++++++++++++++++++++++++++++++++\n list-objects-filter.h | 16 +++++++++++++++\n 2 files changed, 61 insertions(+)\n\ndiff --git a/list-objects-filter.c b/list-objects-filter.c\nindex c912ff3079..6a2e9d5b24 100644\n--- a/list-objects-filter.c\n+++ b/list-objects-filter.c\n@@ -828,3 +828,48 @@ void list_objects_filter__free(struct filter *filter)\n \tfilter->free_fn(filter->filter_data);\n \tfree(filter);\n }\n+\n+/*\n+ * NEEDSWORK: this reimplements the blob:limit size check rather than\n+ * reusing the existing filter machinery in\n+ * list_objects_filter__filter_object(). That machinery is currently\n+ * tied to the object-walk path and cannot easily be driven from a\n+ * plain oidset. It would be nice to refactor the filter code so this\n+ * helper can reuse it instead of duplicating the size check.\n+ */\n+int list_objects_filter__filter_oidset(struct repository *r,\n+\tstruct list_objects_filter_options *opts,\n+\tconst struct oidset *in,\n+\tstruct oidset *omitted)\n+{\n+\tstruct oidset_iter iter;\n+\tconst struct object_id *oid;\n+\n+\tif (opts->choice != LOFC_BLOB_LIMIT)\n+\t\treturn error(_(\"filter_oidset: only blob:limit filters are supported\"));\n+\n+\toidset_iter_init(in, &iter);\n+\twhile ((oid = oidset_iter_next(&iter))) {\n+\t\tstruct object_info info = OBJECT_INFO_INIT;\n+\t\tenum object_type type;\n+\t\tunsigned long size;\n+\n+\t\tinfo.typep = &type;\n+\t\tinfo.sizep = &size;\n+\n+\t\t/*\n+\t\t * Use OBJECT_INFO_SKIP_FETCH_OBJECT to avoid triggering\n+\t\t * a lazy fetch while inspecting candidates for removal.\n+\t\t */\n+\t\tif (odb_read_object_info_extended(r->objects, oid, &info,\n+\t\t\t\tOBJECT_INFO_SKIP_FETCH_OBJECT) < 0)\n+\t\t\tcontinue;\n+\n+\t\tif (type != OBJ_BLOB)\n+\t\t\tcontinue;\n+\n+\t\tif (size >= opts->blob_limit_value)\n+\t\t\toidset_insert(omitted, oid);\n+\t}\n+\treturn 0;\n+}\ndiff --git a/list-objects-filter.h b/list-objects-filter.h\nindex 9e98814111..56a2d87aa0 100644\n--- a/list-objects-filter.h\n+++ b/list-objects-filter.h\n@@ -94,4 +94,20 @@ enum list_objects_filter_result list_objects_filter__filter_object(\n  */\n void list_objects_filter__free(struct filter *filter);\n \n+/*\n+ * Given a set of OIDs in 'in', populate 'omitted' with those that\n+ * would be filtered by 'opts'. Currently only blob:limit=N is\n+ * supported. Objects that cannot be read are silently skipped.\n+ *\n+ * NEEDSWORK: this reimplements the blob:limit size check rather than\n+ * reusing the existing filter machinery. See the matching comment in\n+ * list-objects-filter.c.\n+ *\n+ * Return 0 on success, -1 if the filter is not supported.\n+ */\n+int list_objects_filter__filter_oidset(struct repository *r,\n+\tstruct list_objects_filter_options *opts,\n+\tconst struct oidset *in,\n+\tstruct oidset *omitted);\n+\n #endif /* LIST_OBJECTS_FILTER_H */\n-- \n2.54.0\n\n"},{"id":"548407","messageId":"20260716132848.95982-4-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260716132848.95982-1-r.siddharth.shrimali@gmail.com","subject":"[RFC PATCH 3/7] repack-promisor: allow excluding objects from the rebuilt promisor pack","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-07-16T13:28:44Z","receivedAt":"2026-07-16T13:29:30Z","isPatch":true,"body":"Add a to_drop oidset parameter to repack_promisor_objects(). When it is\nnon-NULL, write_oid() omits those objects from the rebuilt promisor\npack. This is the mechanism --drop-filtered will use to remove promisor\nblobs, i.e. rebuild the promisor pack without them.\n\nAll existing callers pass NULL, so behavior is unchanged.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n builtin/repack.c  |  2 +-\n repack-promisor.c | 15 ++++++++++++++-\n repack.h          |  4 +++-\n 3 files changed, 18 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex f4db0fc535..433b2c8205 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -406,7 +406,7 @@ int cmd_repack(int argc,\n \t\tstrvec_push(&cmd.args, \"--delta-islands\");\n \n \tif (pack_everything & ALL_INTO_ONE) {\n-\t\trepack_promisor_objects(repo, &po_args, &names, packtmp);\n+\t\trepack_promisor_objects(repo, &po_args, &names, packtmp, NULL);\n \n \t\tif (existing_packs_has_non_kept(&existing) &&\n \t\t    delete_redundant &&\ndiff --git a/repack-promisor.c b/repack-promisor.c\nindex 90318ce150..fabfdc168a 100644\n--- a/repack-promisor.c\n+++ b/repack-promisor.c\n@@ -6,10 +6,12 @@\n #include \"path.h\"\n #include \"repository.h\"\n #include \"run-command.h\"\n+#include \"oidset.h\"\n \n struct write_oid_context {\n \tstruct child_process *cmd;\n \tconst struct git_hash_algo *algop;\n+\tconst struct oidset *to_drop;\n };\n \n /*\n@@ -23,6 +25,15 @@ static int write_oid(const struct object_id *oid,\n \tstruct write_oid_context *ctx = data;\n \tstruct child_process *cmd = ctx->cmd;\n \n+\t/*\n+\t * Objects in to_drop are being removed from the repository, so\n+\t * omit them from the rebuilt promisor pack. Each such object is a\n+\t * promisor object and therefore remains recoverable from the\n+\t * promisor remote.\n+\t */\n+\tif (ctx->to_drop && oidset_contains(ctx->to_drop, oid))\n+\t\treturn 0;\n+\n \tif (cmd->in == -1) {\n \t\tif (start_command(cmd))\n \t\t\tdie(_(\"could not start pack-objects to repack promisor objects\"));\n@@ -81,7 +92,8 @@ static void finish_repacking_promisor_objects(struct repository *repo,\n \n void repack_promisor_objects(struct repository *repo,\n \t\t\t     const struct pack_objects_args *args,\n-\t\t\t     struct string_list *names, const char *packtmp)\n+\t\t\t     struct string_list *names, const char *packtmp,\n+\t\t\t     const struct oidset *to_drop)\n {\n \tstruct write_oid_context ctx;\n \tstruct child_process cmd = CHILD_PROCESS_INIT;\n@@ -98,6 +110,7 @@ void repack_promisor_objects(struct repository *repo,\n \t */\n \tctx.cmd = &cmd;\n \tctx.algop = repo->hash_algo;\n+\tctx.to_drop = to_drop;\n \todb_for_each_object(repo->objects, NULL, write_oid, &ctx,\n \t\t\t    ODB_FOR_EACH_OBJECT_PROMISOR_ONLY);\n \ndiff --git a/repack.h b/repack.h\nindex f9fbc895f0..a5a3f7c6ba 100644\n--- a/repack.h\n+++ b/repack.h\n@@ -3,6 +3,7 @@\n \n #include \"list-objects-filter-options.h\"\n #include \"string-list.h\"\n+#include \"oidset.h\"\n \n struct pack_objects_args {\n \tchar *window;\n@@ -100,7 +101,8 @@ void generated_pack_install(struct generated_pack *pack, const char *name,\n \n void repack_promisor_objects(struct repository *repo,\n \t\t\t     const struct pack_objects_args *args,\n-\t\t\t     struct string_list *names, const char *packtmp);\n+\t\t\t     struct string_list *names, const char *packtmp,\n+\t\t\t     const struct oidset *to_drop);\n \n struct pack_geometry {\n \tstruct packed_git **pack;\n-- \n2.54.0\n\n"},{"id":"548408","messageId":"20260716132848.95982-5-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260716132848.95982-1-r.siddharth.shrimali@gmail.com","subject":"[RFC PATCH 4/7] builtin/repack: enumerate promisor blobs for --drop-filtered","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-07-16T13:28:45Z","receivedAt":"2026-07-16T13:29:38Z","isPatch":true,"body":"Add enumeration logic for --drop-filtered. In --dry-run mode, print\nthe OIDs of locally-held promisor blobs that exceed the filter\nthreshold, as candidates for removal.\n\nReading from write_filtered_pack() cannot work for partial clones.\ngit repack routes promisor objects through a separate path:\nrepack_promisor_objects() repacks them first, and the main\npack-objects run uses --exclude-promisor-objects. By the time\nwrite_filtered_pack() runs, the promisor blobs are already consumed by\nthe main pack. The filtered pack is always empty on a partial clone.\n\nInstead, walk promisor objects directly via odb_for_each_object() with\nODB_FOR_EACH_OBJECT_PROMISOR_ONLY, collecting all promisor blobs into\nan oidset. The blobs exceeding the filter threshold are then selected\nusing list_objects_filter__filter_oidset().\n\nEvery object enumerated this way is a promisor object by construction,\nso it is guaranteed to be recoverable from the promisor remote and is\nsafe to drop. No separate is_promisor_object() check is needed.\n\nOBJECT_INFO_SKIP_FETCH_OBJECT is passed to every object info query so\nenumeration never triggers a lazy fetch.\n\nDeletion of the enumerated objects, together with the required\npromisor-remote verification, will be added separately.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n builtin/repack.c  | 41 ++++++++++++++-------\n repack-filtered.c | 92 +++++++++++++++++++++++++++++++++++++++++++++++\n repack.h          |  4 +++\n 3 files changed, 124 insertions(+), 13 deletions(-)\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex 433b2c8205..c2b07477d2 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -590,19 +590,34 @@ int cmd_repack(int argc,\n \t}\n \n \tif (po_args.filter_options.choice) {\n-\t\tstruct write_pack_opts opts = {\n-\t\t\t.po_args = &po_args,\n-\t\t\t.destination = filter_to,\n-\t\t\t.packdir = packdir,\n-\t\t\t.packtmp = packtmp,\n-\t\t};\n-\n-\t\tif (!opts.destination)\n-\t\t\topts.destination = packtmp;\n-\n-\t\tret = write_filtered_pack(&opts, &existing, &names);\n-\t\tif (ret)\n-\t\t\tgoto cleanup;\n+\t\tif (drop_filtered) {\n+\t\t\t/*\n+\t\t\t * Enumerate promisor objects directly rather than\n+\t\t\t * going through write_filtered_pack(). The filter\n+\t\t\t * machinery cannot see promisor objects because\n+\t\t\t * repack_promisor_objects() handles them separately\n+\t\t\t * before the filter runs.\n+\t\t\t */\n+\t\t\tret = enumerate_promisor_blobs(repo,\n+\t\t\t\t\t&po_args.filter_options,\n+\t\t\t\t\tdry_run);\n+\t\t\tif (ret)\n+\t\t\t\tgoto cleanup;\n+\t\t} else {\n+\t\t\tstruct write_pack_opts opts = {\n+\t\t\t\t.po_args = &po_args,\n+\t\t\t\t.destination = filter_to,\n+\t\t\t\t.packdir = packdir,\n+\t\t\t\t.packtmp = packtmp,\n+\t\t\t};\n+\n+\t\t\tif (!opts.destination)\n+\t\t\t\topts.destination = packtmp;\n+\n+\t\t\tret = write_filtered_pack(&opts, &existing, &names);\n+\t\t\tif (ret)\n+\t\t\t\tgoto cleanup;\n+\t\t}\n \t}\n \n \tstring_list_sort(&names);\ndiff --git a/repack-filtered.c b/repack-filtered.c\nindex edcf7667c5..f5a1dae5b1 100644\n--- a/repack-filtered.c\n+++ b/repack-filtered.c\n@@ -3,6 +3,12 @@\n #include \"repository.h\"\n #include \"run-command.h\"\n #include \"string-list.h\"\n+#include \"hex.h\"\n+#include \"packfile.h\"\n+#include \"list-objects-filter-options.h\"\n+#include \"list-objects-filter.h\"\n+#include \"odb.h\"\n+#include \"promisor-remote.h\"\n \n int write_filtered_pack(const struct write_pack_opts *opts,\n \t\t\tstruct existing_packs *existing,\n@@ -49,3 +55,89 @@ int write_filtered_pack(const struct write_pack_opts *opts,\n \treturn finish_pack_objects_cmd(existing->repo->hash_algo, opts, &cmd,\n \t\t\t\t       names);\n }\n+\n+struct collect_cb_data {\n+\tstruct repository *repo;\n+\tstruct oidset *set;\n+};\n+\n+static int collect_promisor_blob(const struct object_id *oid,\n+\t\tstruct object_info *oi UNUSED,\n+\t\tvoid *cb_data)\n+{\n+\tstruct collect_cb_data *data = cb_data;\n+\tstruct object_info info = OBJECT_INFO_INIT;\n+\tenum object_type type;\n+\n+\tinfo.typep = &type;\n+\n+\t/*\n+\t * Use OBJECT_INFO_SKIP_FETCH_OBJECT to avoid triggering a\n+\t * lazy fetch while collecting promisor blobs.\n+\t */\n+\tif (odb_read_object_info_extended(data->repo->objects, oid, &info,\n+\t\t\tOBJECT_INFO_SKIP_FETCH_OBJECT) < 0)\n+\t\treturn 0;\n+\n+\tif (type == OBJ_BLOB)\n+\t\toidset_insert(data->set, oid);\n+\n+\treturn 0;\n+}\n+\n+int enumerate_promisor_blobs(struct repository *repo,\n+\t\t\tconst struct list_objects_filter_options *filter,\n+\t\t\tint dry_run)\n+{\n+\tstruct oidset all_promisor_blobs = OIDSET_INIT;\n+\tstruct oidset to_drop = OIDSET_INIT;\n+\tstruct collect_cb_data cb = {\n+\t\t.repo = repo,\n+\t\t.set = &all_promisor_blobs\n+\t};\n+\tstruct oidset_iter iter;\n+\tconst struct object_id *oid;\n+\tint ret = 0;\n+\n+\t/*\n+\t * The caller (cmd_repack) is responsible for validating that a\n+\t * blob:limit filter and a promisor remote are present before\n+\t * calling this function.\n+\t *\n+\t * Walk only promisor objects. Every object visited here is\n+\t * guaranteed to be recoverable from the promisor remote, so\n+\t * it is safe to drop.\n+\t *\n+\t * We do not use write_filtered_pack() here because git repack\n+\t * routes promisor objects through repack_promisor_objects()\n+\t * before the filter machinery runs, so the filtered pack never\n+\t * contains promisor blobs. Direct enumeration via\n+\t * ODB_FOR_EACH_OBJECT_PROMISOR_ONLY is the correct approach.\n+\t */\n+\tret = odb_for_each_object(repo->objects, NULL,\n+\t\t\tcollect_promisor_blob, &cb,\n+\t\t\tODB_FOR_EACH_OBJECT_PROMISOR_ONLY);\n+\tif (ret)\n+\t\tgoto cleanup;\n+\n+\t/*\n+\t * Apply the filter to find which blobs exceed the threshold.\n+\t */\n+\tret = list_objects_filter__filter_oidset(repo,\n+\t\t(struct list_objects_filter_options *)filter,\n+\t\t&all_promisor_blobs,\n+\t\t&to_drop);\n+\tif (ret)\n+\t\tgoto cleanup;\n+\n+\tif (dry_run) {\n+\t\toidset_iter_init(&to_drop, &iter);\n+\t\twhile ((oid = oidset_iter_next(&iter)))\n+\t\t\tprintf(\"%s\\n\", oid_to_hex(oid));\n+\t}\n+\n+cleanup:\n+\toidset_clear(&all_promisor_blobs);\n+\toidset_clear(&to_drop);\n+\treturn ret;\n+}\ndiff --git a/repack.h b/repack.h\nindex a5a3f7c6ba..d08e25b852 100644\n--- a/repack.h\n+++ b/repack.h\n@@ -167,6 +167,10 @@ int write_filtered_pack(const struct write_pack_opts *opts,\n \t\t\tstruct existing_packs *existing,\n \t\t\tstruct string_list *names);\n \n+int enumerate_promisor_blobs(struct repository *repo,\n+\t\t\t       const struct list_objects_filter_options *filter,\n+\t\t\t       int dry_run);\n+\n 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-- \n2.54.0\n\n"},{"id":"548409","messageId":"20260716132848.95982-6-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260716132848.95982-1-r.siddharth.shrimali@gmail.com","subject":"[RFC PATCH 5/7] t7706: test --drop-filtered enumeration and validation","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-07-16T13:28:46Z","receivedAt":"2026-07-16T13:29:43Z","isPatch":true,"body":"Add tests for the --drop-filtered option.\n\n  * Validation: --drop-filtered requires --filter and -a, is\n    incompatible with --filter-to and --write-bitmap-index, --dry-run\n    only takes effect with --drop-filtered, and --drop-filtered\n    requires a promisor remote.\n\n  * Enumeration: in a repository with a promisor remote, --dry-run\n    lists promisor blobs above the filter threshold and excludes\n    smaller ones. Promisor blobs are created with a synthetic promisor\n    pack, following the helper pattern used in t0410.\n\n  * Safety: a locally created large blob, which is not a promisor\n    object and therefore not recoverable from the remote, is never\n    listed as a drop candidate.\n\n  * Non-destructiveness: --dry-run leaves the filtered objects intact\n    in the repository.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n t/meson.build                   |   1 +\n t/t7706-repack-drop-filtered.sh | 139 ++++++++++++++++++++++++++++++++\n 2 files changed, 140 insertions(+)\n create mode 100755 t/t7706-repack-drop-filtered.sh\n\ndiff --git a/t/meson.build b/t/meson.build\nindex 8ae6ab6c5f..37f272d7f4 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -962,6 +962,7 @@ integration_tests = [\n   't7703-repack-geometric.sh',\n   't7704-repack-cruft.sh',\n   't7705-repack-incremental-midx.sh',\n+  't7706-repack-drop-filtered.sh',\n   't7800-difftool.sh',\n   't7810-grep.sh',\n   't7811-grep-open.sh',\ndiff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh\nnew file mode 100755\nindex 0000000000..b558807847\n--- /dev/null\n+++ b/t/t7706-repack-drop-filtered.sh\n@@ -0,0 +1,139 @@\n+#!/bin/sh\n+\n+test_description='git repack --drop-filtered enumerates\n+filtered promisor blobs'\n+\n+. ./test-lib.sh\n+\n+delete_object () {\n+\tlocal repo=\"$1\" &&\n+\tlocal obj=\"$2\" &&\n+\tlocal path=\"$repo/.git/objects/$(test_oid_to_path \"$obj\")\" &&\n+\trm \"$path\"\n+}\n+\n+# pack the objects into a promisor pack inside \"repo\",\n+# it is a pack accompanied by an empty \".promisor\" marker file. objects\n+# in such a pack are treated as recoverable from the promisor remote.\n+pack_as_from_promisor () {\n+\tHASH=$(git -C repo pack-objects .git/objects/pack/pack) &&\n+\t>repo/.git/objects/pack/pack-$HASH.promisor &&\n+\techo $HASH\n+}\n+\n+# write a blob of $1 bytes into \"repo\", record it as coming from the\n+# promisor remote (promisor pack), and remove the loose copy so the\n+# object is only present in the promisor pack.\n+promisor_blob () {\n+\ttest-tool genrandom \"$1\" \"$2\" >blob_content &&\n+\tOID=$(git -C repo hash-object -w --stdin <blob_content) &&\n+\tprintf \"%s\\n\" \"$OID\" | pack_as_from_promisor >/dev/null &&\n+\tdelete_object repo \"$OID\" &&\n+\techo \"$OID\"\n+}\n+\n+# checks for options validations before any promisor walk\n+test_expect_success 'setup plain repo for validation' '\n+\tgit init plain &&\n+\ttest_commit -C plain initial &&\n+\tgit clone --bare plain plain.git &&\n+\tgit -C plain.git repack -a -d\n+'\n+\n+test_expect_success '--drop-filtered requires --filter' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered --dry-run -a 2>err &&\n+\ttest_grep \"drop-filtered requires --filter\" err\n+'\n+\n+test_expect_success '--drop-filtered cannot be used with --filter-to' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered \\\n+\t\t--filter=blob:limit=1k --filter-to=./filter-out 2>err &&\n+\ttest_grep \"options .--drop-filtered. and .--filter-to. cannot be used together\" err\n+'\n+\n+test_expect_success '--dry-run only takes effect with --drop-filtered' '\n+\ttest_must_fail git -C plain.git repack --dry-run 2>err &&\n+\ttest_grep \"dry-run only takes effect with --drop-filtered\" err\n+'\n+\n+test_expect_success '--drop-filtered without --dry-run is rejected' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered \\\n+\t\t--filter=blob:limit=1k -a 2>err &&\n+\ttest_grep \"drop-filtered doesn.t work without --dry-run yet\" err\n+'\n+\n+test_expect_success '--drop-filtered requires -a' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered \\\n+\t\t--filter=blob:limit=1k --dry-run 2>err &&\n+\ttest_grep \"drop-filtered requires -a\" err\n+'\n+\n+test_expect_success '--drop-filtered fails with --write-bitmap-index' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered \\\n+\t\t--filter=blob:limit=1k --dry-run -a -b 2>err &&\n+\ttest_grep \"options .--drop-filtered. and .--write-bitmap-index. cannot be used together\" err\n+'\n+\n+test_expect_success '--drop-filtered fails without a promisor remote' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered \\\n+\t\t--filter=blob:limit=1k --dry-run -a 2>err &&\n+\ttest_grep \"drop-filtered requires a promisor remote\" err\n+'\n+\n+# enumeration and safety tests using promisor packs\n+test_expect_success 'setup repo with a promisor remote' '\n+\trm -rf repo &&\n+\ttest_create_repo repo &&\n+\ttest_commit -C repo base &&\n+\n+\t# mark the repo as a partial clone with a promisor remote so the\n+\t# promisor walk and the safety guard are satisfied.\n+\tgit -C repo config core.repositoryformatversion 1 &&\n+\tgit -C repo config extensions.partialclone origin &&\n+\tgit -C repo config remote.origin.promisor true &&\n+\tgit -C repo config remote.origin.url \".\" &&\n+\n+\tBIG=$(promisor_blob big 3072) &&\n+\tSMALL=$(promisor_blob small 512) &&\n+\techo \"$BIG\" >big_oid &&\n+\techo \"$SMALL\" >small_oid\n+'\n+\n+test_expect_success 'promisor blob over the threshold is listed' '\n+\tBIG=$(cat big_oid) &&\n+\tSMALL=$(cat small_oid) &&\n+\n+\tgit -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a >out &&\n+\n+\ttest_grep \"$BIG\" out &&\n+\ttest_grep ! \"$SMALL\" out\n+'\n+\n+test_expect_success 'locally created blob is never listed' '\n+\tBIG=$(cat big_oid) &&\n+\n+\t# large blob that exists only locally (no promisor pack) must\n+\t# never be a drop candidate: dropping it would be unrecoverable\n+\t# data loss.\n+\ttest-tool genrandom local 4096 >local_content &&\n+\tLOCAL=$(git -C repo hash-object -w --stdin <local_content) &&\n+\n+\tgit -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a >out &&\n+\n+\ttest_grep \"$BIG\" out &&\n+\ttest_grep ! \"$LOCAL\" out\n+'\n+\n+test_expect_success '--dry-run does not remove the filtered objects' '\n+\tBIG=$(cat big_oid) &&\n+\n+\tgit -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a >out &&\n+\n+\t# candidate blob must still be present after a dry run.\n+\tgit -C repo cat-file -e \"$BIG\"\n+'\n+\n+test_done\n-- \n2.54.0\n\n"},{"id":"548410","messageId":"20260716132848.95982-7-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260716132848.95982-1-r.siddharth.shrimali@gmail.com","subject":"[RFC PATCH 6/7] builtin/repack: actually drop filtered promisor blobs","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-07-16T13:28:47Z","receivedAt":"2026-07-16T13:29:48Z","isPatch":true,"body":"Make --drop-filtered remove the enumerated promisor blobs instead of\nonly listing them.\n\nThe drop set is computed before repack_promisor_objects() runs, and on\na real run it is passed in so the rebuilt promisor pack omits those\nblobs. --drop-filtered implies -d so the old promisor packs, which\nstill contain the dropped blobs, are removed. Without this the blobs\nwould survive in the redundant packs. The existing repack machinery\nperforms the write-before-delete and fsync, so the drop is crash-safe.\n\nThe dropped blobs become absent locally but remain recoverable from the\npromisor remote, so a later access lazy-fetches them back\ntransparently. --dry-run keeps its previous behavior, i.e. it lists the\ncandidates and changes nothing.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n builtin/repack.c                | 75 ++++++++++++++++++---------------\n repack-filtered.c               | 17 ++------\n repack.h                        |  4 +-\n t/t7706-repack-drop-filtered.sh | 18 +++++---\n 4 files changed, 59 insertions(+), 55 deletions(-)\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex c2b07477d2..aa3257a98a 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -15,6 +15,8 @@\n #include \"repack.h\"\n #include \"shallow.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"oidset.h\"\n+#include \"hex.h\"\n \n #define ALL_INTO_ONE 1\n #define LOOSEN_UNREACHABLE 2\n@@ -143,6 +145,7 @@ int cmd_repack(int argc,\n \tstruct string_list_item *item;\n \tstruct string_list names = STRING_LIST_INIT_DUP;\n \tstruct existing_packs existing = EXISTING_PACKS_INIT;\n+\tstruct oidset drop_oids = OIDSET_INIT;\n \tstruct pack_geometry geometry = { 0 };\n \tstruct tempfile *refs_snapshot = NULL;\n \tint i, ret;\n@@ -269,9 +272,6 @@ int cmd_repack(int argc,\n \t\tdie(_(\"--dry-run only takes effect with --drop-filtered\"));\n \n \tif (drop_filtered) {\n-\t\tif (!dry_run)\n-\t\t\tdie(_(\"--drop-filtered doesn't work without --dry-run yet\"));\n-\n \t\tif (!po_args.filter_options.choice)\n \t\t\tdie(_(\"--drop-filtered requires --filter\"));\n \n@@ -294,6 +294,28 @@ int cmd_repack(int argc,\n \t\t\tdie(_(\"--drop-filtered requires a promisor remote\"));\n \n \t\twrite_bitmaps = 0;\n+\n+\t\t/*\n+\t\t * Dropping objects means rebuilding the promisor packs\n+\t\t * without them and then removing the old packs, so the\n+\t\t * redundant packs must be deleted. Imply -d on a real run.\n+\t\t */\n+\t\tif (!dry_run)\n+\t\t\tdelete_redundant = 1;\n+\n+\t\tret = enumerate_promisor_blobs(repo, &po_args.filter_options, &drop_oids);\n+\n+\t\tif (ret)\n+\t\t\tgoto cleanup;\n+\n+\t\tif (dry_run) {\n+\t\t\tstruct oidset_iter iter;\n+\t\t\tconst struct object_id *oid;\n+\n+\t\t\toidset_iter_init(&drop_oids, &iter);\n+\t\t\twhile ((oid = oidset_iter_next(&iter)))\n+\t\t\t\tprintf(\"%s\\n\", oid_to_hex(oid));\n+\t\t}\n \t}\n \n \tif (delete_redundant && repo->repository_format_precious_objects)\n@@ -406,7 +428,8 @@ int cmd_repack(int argc,\n \t\tstrvec_push(&cmd.args, \"--delta-islands\");\n \n \tif (pack_everything & ALL_INTO_ONE) {\n-\t\trepack_promisor_objects(repo, &po_args, &names, packtmp, NULL);\n+\t\trepack_promisor_objects(repo, &po_args, &names, packtmp,\n+\t\t\t(drop_filtered && !dry_run) ? &drop_oids : NULL);\n \n \t\tif (existing_packs_has_non_kept(&existing) &&\n \t\t    delete_redundant &&\n@@ -589,35 +612,20 @@ int cmd_repack(int argc,\n \t\t}\n \t}\n \n-\tif (po_args.filter_options.choice) {\n-\t\tif (drop_filtered) {\n-\t\t\t/*\n-\t\t\t * Enumerate promisor objects directly rather than\n-\t\t\t * going through write_filtered_pack(). The filter\n-\t\t\t * machinery cannot see promisor objects because\n-\t\t\t * repack_promisor_objects() handles them separately\n-\t\t\t * before the filter runs.\n-\t\t\t */\n-\t\t\tret = enumerate_promisor_blobs(repo,\n-\t\t\t\t\t&po_args.filter_options,\n-\t\t\t\t\tdry_run);\n-\t\t\tif (ret)\n-\t\t\t\tgoto cleanup;\n-\t\t} else {\n-\t\t\tstruct write_pack_opts opts = {\n-\t\t\t\t.po_args = &po_args,\n-\t\t\t\t.destination = filter_to,\n-\t\t\t\t.packdir = packdir,\n-\t\t\t\t.packtmp = packtmp,\n-\t\t\t};\n-\n-\t\t\tif (!opts.destination)\n-\t\t\t\topts.destination = packtmp;\n-\n-\t\t\tret = write_filtered_pack(&opts, &existing, &names);\n-\t\t\tif (ret)\n-\t\t\t\tgoto cleanup;\n-\t\t}\n+\tif (po_args.filter_options.choice && !drop_filtered) {\n+\t\tstruct write_pack_opts opts = {\n+\t\t\t.po_args = &po_args,\n+\t\t\t.destination = filter_to,\n+\t\t\t.packdir = packdir,\n+\t\t\t.packtmp = packtmp,\n+\t\t};\n+\n+\t\tif (!opts.destination)\n+\t\t\topts.destination = packtmp;\n+\n+\t\tret = write_filtered_pack(&opts, &existing, &names);\n+\t\tif (ret)\n+\t\t\tgoto cleanup;\n \t}\n \n \tstring_list_sort(&names);\n@@ -697,6 +705,7 @@ int cmd_repack(int argc,\n cleanup:\n \tstring_list_clear(&keep_pack_list, 0);\n \tstring_list_clear(&names, 1);\n+\toidset_clear(&drop_oids);\n \texisting_packs_release(&existing);\n \tpack_geometry_release(&geometry);\n \tpack_objects_args_release(&po_args);\ndiff --git a/repack-filtered.c b/repack-filtered.c\nindex f5a1dae5b1..6f0cecca9b 100644\n--- a/repack-filtered.c\n+++ b/repack-filtered.c\n@@ -87,16 +87,13 @@ static int collect_promisor_blob(const struct object_id *oid,\n \n int enumerate_promisor_blobs(struct repository *repo,\n \t\t\tconst struct list_objects_filter_options *filter,\n-\t\t\tint dry_run)\n+\t\t\tstruct oidset *to_drop)\n {\n \tstruct oidset all_promisor_blobs = OIDSET_INIT;\n-\tstruct oidset to_drop = OIDSET_INIT;\n \tstruct collect_cb_data cb = {\n \t\t.repo = repo,\n \t\t.set = &all_promisor_blobs\n \t};\n-\tstruct oidset_iter iter;\n-\tconst struct object_id *oid;\n \tint ret = 0;\n \n \t/*\n@@ -122,22 +119,14 @@ int enumerate_promisor_blobs(struct repository *repo,\n \n \t/*\n \t * Apply the filter to find which blobs exceed the threshold.\n+\t * The caller has to_drop and is responsible for clearing it.\n \t */\n \tret = list_objects_filter__filter_oidset(repo,\n \t\t(struct list_objects_filter_options *)filter,\n \t\t&all_promisor_blobs,\n-\t\t&to_drop);\n-\tif (ret)\n-\t\tgoto cleanup;\n-\n-\tif (dry_run) {\n-\t\toidset_iter_init(&to_drop, &iter);\n-\t\twhile ((oid = oidset_iter_next(&iter)))\n-\t\t\tprintf(\"%s\\n\", oid_to_hex(oid));\n-\t}\n+\t\tto_drop);\n \n cleanup:\n \toidset_clear(&all_promisor_blobs);\n-\toidset_clear(&to_drop);\n \treturn ret;\n }\ndiff --git a/repack.h b/repack.h\nindex d08e25b852..61e554e4ed 100644\n--- a/repack.h\n+++ b/repack.h\n@@ -168,8 +168,8 @@ int write_filtered_pack(const struct write_pack_opts *opts,\n \t\t\tstruct string_list *names);\n \n int enumerate_promisor_blobs(struct repository *repo,\n-\t\t\t       const struct list_objects_filter_options *filter,\n-\t\t\t       int dry_run);\n+\t\t\t     const struct list_objects_filter_options *filter,\n+\t\t\t     struct oidset *to_drop);\n \n int write_cruft_pack(const struct write_pack_opts *opts,\n \t\t     const char *cruft_expiration,\ndiff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh\nindex b558807847..41e7941799 100755\n--- a/t/t7706-repack-drop-filtered.sh\n+++ b/t/t7706-repack-drop-filtered.sh\n@@ -56,12 +56,6 @@ test_expect_success '--dry-run only takes effect with --drop-filtered' '\n \ttest_grep \"dry-run only takes effect with --drop-filtered\" err\n '\n \n-test_expect_success '--drop-filtered without --dry-run is rejected' '\n-\ttest_must_fail git -C plain.git repack --drop-filtered \\\n-\t\t--filter=blob:limit=1k -a 2>err &&\n-\ttest_grep \"drop-filtered doesn.t work without --dry-run yet\" err\n-'\n-\n test_expect_success '--drop-filtered requires -a' '\n \ttest_must_fail git -C plain.git repack --drop-filtered \\\n \t\t--filter=blob:limit=1k --dry-run 2>err &&\n@@ -136,4 +130,16 @@ test_expect_success '--dry-run does not remove the filtered objects' '\n \tgit -C repo cat-file -e \"$BIG\"\n '\n \n+test_expect_success '--drop-filtered removes the promisor blob locally' '\n+\tBIG=$(cat big_oid) &&\n+\tSMALL=$(cat small_oid) &&\n+\n+\tgit -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k -a &&\n+\n+\tgit -C repo cat-file --batch-all-objects --batch-check=\"%(objectname)\" >present &&\n+\t! grep -q \"$BIG\" present &&\n+\tgrep -q \"$SMALL\" present\n+'\n+\n test_done\n-- \n2.54.0\n\n"},{"id":"548411","messageId":"20260716132848.95982-8-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260716132848.95982-1-r.siddharth.shrimali@gmail.com","subject":"[RFC PATCH 7/7] repack-promisor: record dropped objects in a drop log","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-07-16T13:28:48Z","receivedAt":"2026-07-16T13:29:53Z","isPatch":true,"body":"After --drop-filtered removes promisor blobs, append a record of each\ndropped object to $GIT_DIR/objects/info/promisor-dropped. Each line\nrecords the object ID, a reflog-style timestamp (Unix seconds and\ntimezone), the filter spec, and the promisor remote it was attested\nrecoverable from like the following:\n\n  <oid> <time> <tz> filter=<spec> remote=<name>\n\nIf a dropped object later becomes unrecoverable (for example, the\nbranch holding it is deleted on the promisor remote), a lazy fetch\nfails with a generic error. This persistent record lets a later change\nexplain that the object was dropped deliberately, when, under which\nfilter, and from which remote it was expected to be recoverable.\n\nThe remote field lists all configured promisor remotes rather than the\nspecific one each dropped object is recoverable from. Determining the\nexact remote would require asking the remote whether it has the object.\nA \"remote-object-info\" command is being added to the \"git cat-file\n--batch\" protocol for this kind of query, but it is not available yet.\nA NEEDSWORK marks this for a follow-up.\n\nThe log is written only on a real run, i.e. --dry-run changes nothing.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n builtin/repack.c  |  4 +++\n repack-promisor.c | 91 +++++++++++++++++++++++++++++++++++++++++++++++\n repack.h          |  4 +++\n 3 files changed, 99 insertions(+)\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex aa3257a98a..49dcbbc567 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -702,6 +702,10 @@ int cmd_repack(int argc,\n \t\twrite_midx_file(files->packed, NULL, NULL, flags);\n \t}\n \n+\tif (drop_filtered && !dry_run)\n+\t\tappend_drop_log(repo, &drop_oids,\n+\t\t\texpand_list_objects_filter_spec(&po_args.filter_options));\n+\n cleanup:\n \tstring_list_clear(&keep_pack_list, 0);\n \tstring_list_clear(&names, 1);\ndiff --git a/repack-promisor.c b/repack-promisor.c\nindex fabfdc168a..60913a5150 100644\n--- a/repack-promisor.c\n+++ b/repack-promisor.c\n@@ -7,6 +7,97 @@\n #include \"repository.h\"\n #include \"run-command.h\"\n #include \"oidset.h\"\n+#include \"date.h\"\n+#include \"promisor-remote.h\"\n+#include \"strbuf.h\"\n+\n+/*\n+ * Append the drop-log entries to the already-computed path.\n+ * Returns -1 on any I/O failure so the caller can warn once.\n+ * Keeping this in a separate helper avoids goto-based cleanup\n+ * in append_drop_log();\n+ */\n+static int write_to_drop_log(struct repository *repo,\n+\t\t\t     const char *path,\n+\t\t\t     const struct oidset *dropped,\n+\t\t\t     const char *stamp,\n+\t\t\t     const char *filter_spec,\n+\t\t\t     const char *remotes)\n+{\n+\tstruct oidset_iter iter;\n+\tconst struct object_id *oid;\n+\tFILE *fp;\n+\n+\tif (safe_create_leading_directories(repo, (char *)path)) {\n+\t\twarning(_(\"could not create leading directories for '%s'\"), path);\n+\t\treturn -1;\n+\t}\n+\n+\tfp = fopen(path, \"a\");\n+\tif (!fp) {\n+\t\twarning_errno(_(\"could not open '%s'\"), path);\n+\t\treturn -1;\n+\t}\n+\n+\toidset_iter_init(dropped, &iter);\n+\twhile ((oid = oidset_iter_next(&iter))) {\n+\t\tif (fprintf(fp, \"%s %s filter=%s remote=%s\\n\",\n+\t\t\t\toid_to_hex(oid), stamp,\n+\t\t\t\tfilter_spec ? filter_spec : \"\",\n+\t\t\t\tremotes) < 0) {\n+\t\t\twarning(_(\"could not write to '%s'\"), path);\n+\t\t\tfclose(fp);\n+\t\t\treturn -1;\n+\t\t}\n+\t}\n+\n+\tif (fclose(fp)) {\n+\t\twarning_errno(_(\"could not close '%s'\"), path);\n+\t\treturn -1;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+void append_drop_log(struct repository *repo,\n+\t\t     const struct oidset *dropped,\n+\t\t     const char *filter_spec)\n+{\n+\tchar *path;\n+\tstruct strbuf stamp = STRBUF_INIT;\n+\tstruct strbuf remotes = STRBUF_INIT;\n+\tstruct promisor_remote *pr;\n+\n+\tif (!oidset_size(dropped))\n+\t\treturn;\n+\n+\tdatestamp(&stamp);\n+\n+\t/*\n+\t * NEEDSWORK: we temporarily record all configured promisor remotes rather\n+\t * than the specific one a given object is recoverable from because there\n+\t * is currently no way to determine that locally. it would require\n+\t * asking the remote whether it has the object. A \"remote-object-info\"\n+\t * command is being added to the \"git cat-file --batch\" protocol for\n+\t * this kind of query. Once it is merged in the codebase, this should\n+\t * record the exact promisor remote that has each dropped object.\n+\t */\n+\tfor (pr = repo_promisor_remote_find(repo, NULL); pr; pr = pr->next) {\n+\t\tif (remotes.len)\n+\t\t\tstrbuf_addch(&remotes, ',');\n+\t\tstrbuf_addstr(&remotes, pr->name);\n+\t}\n+\n+\tpath = repo_git_path(repo, \"objects/info/promisor-dropped\");\n+\n+\tif (write_to_drop_log(repo, path, dropped, stamp.buf,\n+\t\t\tfilter_spec, remotes.buf))\n+\t\twarning(_(\"could not record all dropped objects in the drop log\"));\n+\n+\tstrbuf_release(&stamp);\n+\tstrbuf_release(&remotes);\n+\tfree(path);\n+}\n \n struct write_oid_context {\n \tstruct child_process *cmd;\ndiff --git a/repack.h b/repack.h\nindex 61e554e4ed..33309548ce 100644\n--- a/repack.h\n+++ b/repack.h\n@@ -171,6 +171,10 @@ int enumerate_promisor_blobs(struct repository *repo,\n \t\t\t     const struct list_objects_filter_options *filter,\n \t\t\t     struct oidset *to_drop);\n \n+void append_drop_log(struct repository *repo,\n+\t\t     const struct oidset *dropped,\n+\t\t     const char *filter_spec);\n+\n 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-- \n2.54.0\n\n"},{"id":"548458","messageId":"xmqqh5lyej6f.fsf@gitster.g","threadId":"66019","inReplyTo":"20260716132848.95982-2-r.siddharth.shrimali@gmail.com","subject":"Re: [RFC PATCH 1/7] builtin/repack.c: add --drop-filtered and --dry-run options","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-16T21:08:56Z","receivedAt":"2026-07-16T21:08:59Z","isPatch":true,"body":"Siddharth Shrimali <r.siddharth.shrimali@gmail.com> writes:\n\n> --drop-filtered is incompatible with bitmap writing: filtering breaks\n> the \"all objects in one pack\" closure that bitmaps require. An explicit\n> -b is rejected with a clear error and a default-on bitmap configuration is\n> silently disabled for the duration of the command.\n\nThat is very well intentioned.\n\n> @@ -231,6 +234,10 @@ int cmd_repack(int argc,\n>  \t\t\t   N_(\"pack prefix to store a pack containing pruned objects\")),\n>  \t\tOPT_STRING(0, \"filter-to\", &filter_to, N_(\"dir\"),\n>  \t\t\t   N_(\"pack prefix to store a pack containing filtered out objects\")),\n> +\t\tOPT_BOOL(0, \"drop-filtered\", &drop_filtered,\n> +\t\t\t\tN_(\"delete filtered out objects (requires --filter)\")),\n> +\t\tOPT_BOOL(0, \"dry-run\", &dry_run,\n> +\t\t\t\tN_(\"only show which objects would be dropped\")),\n>  \t\tOPT_END()\n>  \t};\n>  \n> @@ -252,6 +259,43 @@ int cmd_repack(int argc,\n>  \tpo_args.depth = xstrdup_or_null(opt_depth);\n>  \tpo_args.threads = xstrdup_or_null(opt_threads);\n>  \n> +\tdie_for_incompatible_opt2(drop_filtered, \"--drop-filtered\",\n> +\t\t!!filter_to, \"--filter-to\");\n> +\n> +\tdie_for_incompatible_opt2(drop_filtered, \"--drop-filtered\",\n> +\t\twrite_bitmaps > 0, \"--write-bitmap-index\");\n\nHmph.  Since this step does not change the parsing or configuration\nfor write_bitmaps, we cannot tell if (write_bitmaps == 1) at this\npoint in the execution came from the command line (e.g., an earlier\ncall to parse_options() around line 247 of builtin/repack.c) or from\nthe configuration files (e.g., a call to repo_config() around\nline 245).  In other words, wouldn't it be ...\n\n> +\tif (dry_run && !drop_filtered)\n> +\t\tdie(_(\"--dry-run only takes effect with --drop-filtered\"));\n> +\n> +\tif (drop_filtered) {\n> +\t\tif (!dry_run)\n> +\t\t\tdie(_(\"--drop-filtered doesn't work without --dry-run yet\"));\n> +\n> +\t\tif (!po_args.filter_options.choice)\n> +\t\t\tdie(_(\"--drop-filtered requires --filter\"));\n> +\n> +\t\tif (!(pack_everything & ALL_INTO_ONE))\n> +\t\t\tdie(_(\"--drop-filtered requires -a\"));\n> +\n> +\t\t/*\n> +\t\t * Only blob:limit=<n> is supported for now. Reject other\n> +\t\t * filter choices early, before walking the object database.\n> +\t\t */\n> +\t\tif (po_args.filter_options.choice != LOFC_BLOB_LIMIT)\n> +\t\t\tdie(_(\"--drop-filtered only supports --filter=blob:limit=<n> for now\"));\n> +\n> +\t\t/*\n> +\t\t * Without a promisor remote there is nowhere to re-fetch the\n> +\t\t * dropped objects from, so dropping them would be permanent\n> +\t\t * data loss.\n> +\t\t */\n> +\t\tif (!repo_has_promisor_remote(repo))\n> +\t\t\tdie(_(\"--drop-filtered requires a promisor remote\"));\n> +\n> +\t\twrite_bitmaps = 0;\n\n... way too late to drop the flag here?\n\n> +\t}\n> +\n>  \tif (delete_redundant && repo->repository_format_precious_objects)\n>  \t\tdie(_(\"cannot delete packs in a precious-objects repo\"));\n"},{"id":"548551","messageId":"CAGWgyh8qFiyrQtQDOH17jnkbDrZCVGX3ETjKLZ1dV4xkofRU0w@mail.gmail.com","threadId":"66019","inReplyTo":"xmqqh5lyej6f.fsf@gitster.g","subject":"Re: [RFC PATCH 1/7] builtin/repack.c: add --drop-filtered and --dry-run options","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-07-17T18:00:33Z","receivedAt":"2026-07-17T18:01:10Z","isPatch":true,"body":"On Fri, 17 Jul 2026 at 02:38, Junio C Hamano <gitster@pobox.com> wrote:\n>\n> > +     die_for_incompatible_opt2(drop_filtered, \"--drop-filtered\",\n> > +             !!filter_to, \"--filter-to\");\n> > +\n> > +     die_for_incompatible_opt2(drop_filtered, \"--drop-filtered\",\n> > +             write_bitmaps > 0, \"--write-bitmap-index\");\n>\n> Hmph.  Since this step does not change the parsing or configuration\n> for write_bitmaps, we cannot tell if (write_bitmaps == 1) at this\n> point in the execution came from the command line (e.g., an earlier\n> call to parse_options() around line 247 of builtin/repack.c) or from\n> the configuration files (e.g., a call to repo_config() around\n> line 245).  In other words, wouldn't it be ...\n>\n> > +             write_bitmaps = 0;\n>\n> ... way too late to drop the flag here?\n>\n\nright, thanks! The commit message claims I distinguish an explicit\n-b/--write-bitmap-index from a config-provided default, but the code\nonly tests write_bitmaps > 0, which cannot tell the two apart at this point\n\nFor v2, alongside other changes, i'll distinguish the two, so that an explicit\n-b on the command line errors out, while a config-provided default is\nsilently disabled for the duration of the command..\n\nThanks!\nSiddharth Shrimali\n"},{"id":"548585","messageId":"CAP8UFD2ZNmWh4fjh+vFvKCihfebg2yif9=xLjqpKZFgF-O0RSg@mail.gmail.com","threadId":"66019","inReplyTo":"20260716132848.95982-2-r.siddharth.shrimali@gmail.com","subject":"Re: [RFC PATCH 1/7] builtin/repack.c: add --drop-filtered and --dry-run options","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-07-18T12:30:52Z","receivedAt":"2026-07-18T12:31:04Z","isPatch":true,"body":"On Thu, Jul 16, 2026 at 3:29 PM Siddharth Shrimali\n<r.siddharth.shrimali@gmail.com> wrote:\n>\n> Add two new command-line options to 'git-repack':\n>\n>   --drop-filtered: intended to eventually delete objects that match\n>                    the filter specification. Requires --filter and -a,\n>                    and is incompatible with --filter-to.\n>   --dry-run: show which objects would be dropped without making any\n>              changes. Only meaningful with --drop-filtered.\n\nAn alternative would be `--drop-filtered[=dry-run]`, which might be\nextended with other `--drop-filtered` specific options later.\n\nI think separating `--dry-run` from `--drop-filtered` like this patch\ndoes makes sense though if we think that `--dry-run` could be useful\nlater without `--drop-filtered`. The fact that a number of other\ncommands already have a `--dry-run` option might be a good sign.\n\nAnyway it would be nice if the commit message explained a bit the\nchoice to have a separate `--dry-run` option.\n"},{"id":"548671","messageId":"CAGWgyh8rCg0eP6Og31RPRn6oXZ5K09VHtH2UsvMSQs9-CJ=pgg@mail.gmail.com","threadId":"66019","inReplyTo":"CAP8UFD2ZNmWh4fjh+vFvKCihfebg2yif9=xLjqpKZFgF-O0RSg@mail.gmail.com","subject":"Re: [RFC PATCH 1/7] builtin/repack.c: add --drop-filtered and --dry-run options","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-07-20T10:19:28Z","receivedAt":"2026-07-20T10:20:05Z","isPatch":true,"body":"Hey Christian,\n\nOn Sat, 18 Jul 2026 at 18:01, Christian Couder\n<christian.couder@gmail.com> wrote:\n> An alternative would be `--drop-filtered[=dry-run]`, which might be\n> extended with other `--drop-filtered` specific options later.\n>\n> I think separating `--dry-run` from `--drop-filtered` like this patch\n> does makes sense though if we think that `--dry-run` could be useful\n> later without `--drop-filtered`. The fact that a number of other\n> commands already have a `--dry-run` option might be a good sign.\n\nAgreed. I chose a separate --dry-run mainly for consistency with the many other\nGit commands that already use it that way, and because it leaves room\nfor --dry-run\nto apply to other repack behavior later rather than being tied to\n--drop-filtered.\n\nThat said, I do like the --drop-filtered[=dry-run] form for keeping future\n--drop-filtered-specific options together, and it might end up being the tidier\nchoice.\n\nSince this is an RFC, I'd genuinely welcome others' thoughts on which one\nholds up better in the long run, before settling on any one command-line.\n\n>\n> Anyway it would be nice if the commit message explained a bit the\n> choice to have a separate `--dry-run` option.\n\nEither way, for v2 I'll add a note to the commit message explaining\nthe final choice.\n\nThanks!\n"},{"id":"548822","messageId":"f9c26c07-1dde-4bb6-a919-37d5229642f5@gmail.com","threadId":"66019","inReplyTo":"20260716132848.95982-1-r.siddharth.shrimali@gmail.com","subject":"Re: [RFC PATCH 0/7] repack: add --drop-filtered to reclaim space in partial clones","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-07-23T19:26:02Z","receivedAt":"2026-07-23T19:26:09Z","isPatch":true,"body":"\n\nOn 16/07/26 18:58, Siddharth Shrimali wrote:\n> This is an RFC series seeking feedback on the design and approach.\n> Several pieces are still missing (noted below) and the commit\n> organization needs cleanup.\n> \n> Partial clones let you work with large repositories without downloading\n> every blob up front and the missing blobs are lazily fetched from the promisor\n> remote on demand. Over time, though, these lazily-fetched blobs\n> accumulate locally and there is currently no safe, built-in way to\n> reclaim that disk space instead of re-cloning.\n> \n> This series adds a \"git repack --drop-filtered --filter=<spec>\" command\n> that removes large, locally-held promisor blobs that are recoverable\n> from the promisor remote. The dropped blobs become absent locally but\n> remain lazily re-fetchable, making the partial-clone still reversible.\n> \n> How it works:\n>    * Enumerate promisor objects directly (ODB_FOR_EACH_OBJECT_PROMISOR_ONLY)\n>      and select the blobs exceeding the filter threshold. Because every\n>      enumerated object is a promisor object, it is guaranteed recoverable and\n>      locally-created objects are never candidates.\n\n\nThis looks like the right approach to me. Going through the promisor \nrepack path instead of write_filtered_pack() matches how repack already \nsplits promisor objects out.\n\n\n> \n>    * Rebuild the promisor pack without the selected blobs, reusing the\n>      existing repack machinery, so the drop is crash-safe.\n> \n>    * Record each dropped object in a drop log\n>      ($GIT_DIR/objects/info/promisor-dropped) so a later change can\n>      explain a failed lazy fetch (when it was dropped, which filter\n>      matched, which remotes) instead of a bare \"could not fetch\" error.\n> \n>    * --dry-run lists the candidates and changes nothing.\n> \n> Planned follow-ups:\n>    * Safety guards: refuse to run while a merge/rebase/cherry-pick is in\n>      progress, and refuse to drop blobs referenced by the current index.\n\n\nI think these matter before we present this as a real space-reclaim\ntool. Without the index guard especially, users may drop blobs and then\nimmediately fetch them back on the next command that needs the worktree.\n\nThe drop log and remote-object-info can wait. I would not block the\nnext RFC round on them.\n\nOn the UI, I am fine with a separate --dry-run for now (same as\nChristian). We can revisit a --drop-filtered=<mode> form later if we\ngrow more drop-specific options.\n\nThanks.\nSiddharth\n\n\n> \n>    * Authoritative remote verification: the drop log currently lists all\n>      configured promisor remotes rather than the exact remote each object\n>      is recoverable from, because there is no client-side way to query a\n>      remote for object availability yet. A \"remote-object-info\" command\n>      is being added to the \"git cat-file --batch\" protocol for this. Once\n>      available, the exact remote can be recorded.\n> \n> Known issues to address in v2:\n>    * There is churn between \"enumerate promisor blobs\" and \"actually drop\n>      filtered promisor blobs\". The former introduces\n>      enumerate_promisor_blobs() with an interim signature that the latter\n>      rewrites. These will be reorganized so the function is introduced\n>      in its final form.\n> \n>    * The tests are in a standalone commit. They will instead be\n>      distributed into the commits that introduce the behavior they test.\n> \n> Siddharth Shrimali (7):\n>    builtin/repack.c: add --drop-filtered and --dry-run options\n>    list-objects-filter: add list_objects_filter__filter_oidset()\n>    repack-promisor: allow excluding objects from the rebuilt promisor\n>      pack\n>    builtin/repack: enumerate promisor blobs for --drop-filtered\n>    t7706: test --drop-filtered enumeration and validation\n>    builtin/repack: actually drop filtered promisor blobs\n>    repack-promisor: record dropped objects in a drop log\n> \n>   builtin/repack.c                |  76 ++++++++++++++++-\n>   list-objects-filter.c           |  45 ++++++++++\n>   list-objects-filter.h           |  16 ++++\n>   repack-filtered.c               |  81 ++++++++++++++++++\n>   repack-promisor.c               | 106 ++++++++++++++++++++++-\n>   repack.h                        |  12 ++-\n>   t/meson.build                   |   1 +\n>   t/t7706-repack-drop-filtered.sh | 145 ++++++++++++++++++++++++++++++++\n>   8 files changed, 478 insertions(+), 4 deletions(-)\n>   create mode 100755 t/t7706-repack-drop-filtered.sh\n> \n\n"},{"id":"548823","messageId":"5bde84d0-d612-4dd6-b1f5-1b40f41c1d8d@gmail.com","threadId":"66019","inReplyTo":"xmqqh5lyej6f.fsf@gitster.g","subject":"Re: [RFC PATCH 1/7] builtin/repack.c: add --drop-filtered and --dry-run options","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-07-23T19:31:47Z","receivedAt":"2026-07-23T19:31:52Z","isPatch":true,"body":"\n\nOn 17/07/26 02:38, Junio C Hamano wrote:\n> Siddharth Shrimali <r.siddharth.shrimali@gmail.com> writes:\n> \n>> --drop-filtered is incompatible with bitmap writing: filtering breaks\n>> the \"all objects in one pack\" closure that bitmaps require. An explicit\n>> -b is rejected with a clear error and a default-on bitmap configuration is\n>> silently disabled for the duration of the command.\n> \n> That is very well intentioned.\n> \n>> @@ -231,6 +234,10 @@ int cmd_repack(int argc,\n>>   \t\t\t   N_(\"pack prefix to store a pack containing pruned objects\")),\n>>   \t\tOPT_STRING(0, \"filter-to\", &filter_to, N_(\"dir\"),\n>>   \t\t\t   N_(\"pack prefix to store a pack containing filtered out objects\")),\n>> +\t\tOPT_BOOL(0, \"drop-filtered\", &drop_filtered,\n>> +\t\t\t\tN_(\"delete filtered out objects (requires --filter)\")),\n>> +\t\tOPT_BOOL(0, \"dry-run\", &dry_run,\n>> +\t\t\t\tN_(\"only show which objects would be dropped\")),\n>>   \t\tOPT_END()\n>>   \t};\n>>   \n>> @@ -252,6 +259,43 @@ int cmd_repack(int argc,\n>>   \tpo_args.depth = xstrdup_or_null(opt_depth);\n>>   \tpo_args.threads = xstrdup_or_null(opt_threads);\n>>   \n>> +\tdie_for_incompatible_opt2(drop_filtered, \"--drop-filtered\",\n>> +\t\t!!filter_to, \"--filter-to\");\n>> +\n>> +\tdie_for_incompatible_opt2(drop_filtered, \"--drop-filtered\",\n>> +\t\twrite_bitmaps > 0, \"--write-bitmap-index\");\n> \n> Hmph.  Since this step does not change the parsing or configuration\n> for write_bitmaps, we cannot tell if (write_bitmaps == 1) at this\n> point in the execution came from the command line (e.g., an earlier\n> call to parse_options() around line 247 of builtin/repack.c) or from\n> the configuration files (e.g., a call to repo_config() around\n> line 245).  In other words, wouldn't it be ...\n> \n>> +\tif (dry_run && !drop_filtered)\n>> +\t\tdie(_(\"--dry-run only takes effect with --drop-filtered\"));\n>> +\n>> +\tif (drop_filtered) {\n>> +\t\tif (!dry_run)\n>> +\t\t\tdie(_(\"--drop-filtered doesn't work without --dry-run yet\"));\n>> +\n>> +\t\tif (!po_args.filter_options.choice)\n>> +\t\t\tdie(_(\"--drop-filtered requires --filter\"));\n>> +\n>> +\t\tif (!(pack_everything & ALL_INTO_ONE))\n>> +\t\t\tdie(_(\"--drop-filtered requires -a\"));\n>> +\n>> +\t\t/*\n>> +\t\t * Only blob:limit=<n> is supported for now. Reject other\n>> +\t\t * filter choices early, before walking the object database.\n>> +\t\t */\n>> +\t\tif (po_args.filter_options.choice != LOFC_BLOB_LIMIT)\n>> +\t\t\tdie(_(\"--drop-filtered only supports --filter=blob:limit=<n> for now\"));\n>> +\n>> +\t\t/*\n>> +\t\t * Without a promisor remote there is nowhere to re-fetch the\n>> +\t\t * dropped objects from, so dropping them would be permanent\n>> +\t\t * data loss.\n>> +\t\t */\n>> +\t\tif (!repo_has_promisor_remote(repo))\n>> +\t\t\tdie(_(\"--drop-filtered requires a promisor remote\"));\n>> +\n>> +\t\twrite_bitmaps = 0;\n> \n> ... way too late to drop the flag here?\n\n\n\nYes, I agree. At that point write_bitmaps > 0 can come from either\n-b/--write-bitmap-index or repack.writeBitmaps, so we cannot both\nerror on an explicit -b and silently clear a config default with the\nsame check.\n\nFor v2 it would be nice to treat those two cases differently.\n\nThanks.\nSiddharth\n\n\n> \n>> +\t}\n>> +\n>>   \tif (delete_redundant && repo->repository_format_precious_objects)\n>>   \t\tdie(_(\"cannot delete packs in a precious-objects repo\"));\n\n"},{"id":"548824","messageId":"e8d63121-fedc-49da-ad1a-0cbcf1a35a0b@gmail.com","threadId":"66019","inReplyTo":"20260716132848.95982-8-r.siddharth.shrimali@gmail.com","subject":"Re: [RFC PATCH 7/7] repack-promisor: record dropped objects in a drop log","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-07-23T19:41:24Z","receivedAt":"2026-07-23T19:41:34Z","isPatch":true,"body":"\n\nOn 16/07/26 18:58, Siddharth Shrimali wrote:\n> After --drop-filtered removes promisor blobs, append a record of each\n> dropped object to $GIT_DIR/objects/info/promisor-dropped. Each line\n> records the object ID, a reflog-style timestamp (Unix seconds and\n> timezone), the filter spec, and the promisor remote it was attested\n> recoverable from like the following:\n> \n>    <oid> <time> <tz> filter=<spec> remote=<name>\n> \n> If a dropped object later becomes unrecoverable (for example, the\n> branch holding it is deleted on the promisor remote), a lazy fetch\n> fails with a generic error. This persistent record lets a later change\n> explain that the object was dropped deliberately, when, under which\n> filter, and from which remote it was expected to be recoverable.\n\n\nI like the idea of better errors when a later lazy fetch fails.\n\nAn alternative would be to wait until we actually have that error-path\nchange in the same series, so we do not grow an on-disk format that\nnothing reads yet. I think keeping the log in the RFC is fine though\nif you find it useful while developing; I would not treat it as\nrequired for the first mergeable version.\n\n\n\n> \n> The remote field lists all configured promisor remotes rather than the\n> specific one each dropped object is recoverable from. Determining the\n> exact remote would require asking the remote whether it has the object.\n> A \"remote-object-info\" command is being added to the \"git cat-file\n> --batch\" protocol for this kind of query, but it is not available yet.\n> A NEEDSWORK marks this for a follow-up.\n> \n> The log is written only on a real run, i.e. --dry-run changes nothing.\n> \n> Mentored-by: Christian Couder <christian.couder@gmail.com>\n> Mentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n> Signed-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n> ---\n>   builtin/repack.c  |  4 +++\n>   repack-promisor.c | 91 +++++++++++++++++++++++++++++++++++++++++++++++\n>   repack.h          |  4 +++\n>   3 files changed, 99 insertions(+)\n> \n> diff --git a/builtin/repack.c b/builtin/repack.c\n> index aa3257a98a..49dcbbc567 100644\n> --- a/builtin/repack.c\n> +++ b/builtin/repack.c\n> @@ -702,6 +702,10 @@ int cmd_repack(int argc,\n>   \t\twrite_midx_file(files->packed, NULL, NULL, flags);\n>   \t}\n>   \n> +\tif (drop_filtered && !dry_run)\n> +\t\tappend_drop_log(repo, &drop_oids,\n> +\t\t\texpand_list_objects_filter_spec(&po_args.filter_options));\n> +\n>   cleanup:\n>   \tstring_list_clear(&keep_pack_list, 0);\n>   \tstring_list_clear(&names, 1);\n> diff --git a/repack-promisor.c b/repack-promisor.c\n> index fabfdc168a..60913a5150 100644\n> --- a/repack-promisor.c\n> +++ b/repack-promisor.c\n> @@ -7,6 +7,97 @@\n>   #include \"repository.h\"\n>   #include \"run-command.h\"\n>   #include \"oidset.h\"\n> +#include \"date.h\"\n> +#include \"promisor-remote.h\"\n> +#include \"strbuf.h\"\n> +\n> +/*\n> + * Append the drop-log entries to the already-computed path.\n> + * Returns -1 on any I/O failure so the caller can warn once.\n> + * Keeping this in a separate helper avoids goto-based cleanup\n> + * in append_drop_log();\n> + */\n> +static int write_to_drop_log(struct repository *repo,\n> +\t\t\t     const char *path,\n> +\t\t\t     const struct oidset *dropped,\n> +\t\t\t     const char *stamp,\n> +\t\t\t     const char *filter_spec,\n> +\t\t\t     const char *remotes)\n> +{\n> +\tstruct oidset_iter iter;\n> +\tconst struct object_id *oid;\n> +\tFILE *fp;\n> +\n> +\tif (safe_create_leading_directories(repo, (char *)path)) {\n> +\t\twarning(_(\"could not create leading directories for '%s'\"), path);\n> +\t\treturn -1;\n> +\t}\n> +\n> +\tfp = fopen(path, \"a\");\n> +\tif (!fp) {\n> +\t\twarning_errno(_(\"could not open '%s'\"), path);\n> +\t\treturn -1;\n> +\t}\n> +\n> +\toidset_iter_init(dropped, &iter);\n> +\twhile ((oid = oidset_iter_next(&iter))) {\n> +\t\tif (fprintf(fp, \"%s %s filter=%s remote=%s\\n\",\n> +\t\t\t\toid_to_hex(oid), stamp,\n> +\t\t\t\tfilter_spec ? filter_spec : \"\",\n> +\t\t\t\tremotes) < 0) {\n> +\t\t\twarning(_(\"could not write to '%s'\"), path);\n> +\t\t\tfclose(fp);\n> +\t\t\treturn -1;\n> +\t\t}\n> +\t}\n> +\n> +\tif (fclose(fp)) {\n> +\t\twarning_errno(_(\"could not close '%s'\"), path);\n> +\t\treturn -1;\n> +\t}\n> +\n> +\treturn 0;\n> +}\n> +\n> +void append_drop_log(struct repository *repo,\n> +\t\t     const struct oidset *dropped,\n> +\t\t     const char *filter_spec)\n> +{\n> +\tchar *path;\n> +\tstruct strbuf stamp = STRBUF_INIT;\n> +\tstruct strbuf remotes = STRBUF_INIT;\n> +\tstruct promisor_remote *pr;\n> +\n> +\tif (!oidset_size(dropped))\n> +\t\treturn;\n> +\n> +\tdatestamp(&stamp);\n> +\n> +\t/*\n> +\t * NEEDSWORK: we temporarily record all configured promisor remotes rather\n> +\t * than the specific one a given object is recoverable from because there\n\n\nRecording all promisor remotes for now looks OK to me with that\nNEEDSWORK. I would not block this on remote-object-info.\n\n\n> +\t * is currently no way to determine that locally. it would require\n> +\t * asking the remote whether it has the object. A \"remote-object-info\"\n> +\t * command is being added to the \"git cat-file --batch\" protocol for\n> +\t * this kind of query. Once it is merged in the codebase, this should\n> +\t * record the exact promisor remote that has each dropped object.\n> +\t */\n> +\tfor (pr = repo_promisor_remote_find(repo, NULL); pr; pr = pr->next) {\n> +\t\tif (remotes.len)\n> +\t\t\tstrbuf_addch(&remotes, ',');\n> +\t\tstrbuf_addstr(&remotes, pr->name);\n> +\t}\n> +\n> +\tpath = repo_git_path(repo, \"objects/info/promisor-dropped\");\n\n\nIf we keep it, it would be nice to document this path (for example in\ngitrepository-layout) and to have a small test that a real drop appends\na line.\n\n\nThanks\nSiddharth\n\n\n> +\n> +\tif (write_to_drop_log(repo, path, dropped, stamp.buf,\n> +\t\t\tfilter_spec, remotes.buf))\n> +\t\twarning(_(\"could not record all dropped objects in the drop log\"));\n> +\n> +\tstrbuf_release(&stamp);\n> +\tstrbuf_release(&remotes);\n> +\tfree(path);\n> +}\n>   \n>   struct write_oid_context {\n>   \tstruct child_process *cmd;\n> diff --git a/repack.h b/repack.h\n> index 61e554e4ed..33309548ce 100644\n> --- a/repack.h\n> +++ b/repack.h\n> @@ -171,6 +171,10 @@ int enumerate_promisor_blobs(struct repository *repo,\n>   \t\t\t     const struct list_objects_filter_options *filter,\n>   \t\t\t     struct oidset *to_drop);\n>   \n> +void append_drop_log(struct repository *repo,\n> +\t\t     const struct oidset *dropped,\n> +\t\t     const char *filter_spec);\n> +\n>   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\n"},{"id":"548825","messageId":"ec546f71-3412-47ef-a4cf-98558889a90f@gmail.com","threadId":"66019","inReplyTo":"20260716132848.95982-7-r.siddharth.shrimali@gmail.com","subject":"Re: [RFC PATCH 6/7] builtin/repack: actually drop filtered promisor blobs","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-07-23T19:42:47Z","receivedAt":"2026-07-23T19:42:54Z","isPatch":true,"body":"\n\nOn 16/07/26 18:58, Siddharth Shrimali wrote:\n> Make --drop-filtered remove the enumerated promisor blobs instead of\n> only listing them.\n> \n> The drop set is computed before repack_promisor_objects() runs, and on\n> a real run it is passed in so the rebuilt promisor pack omits those\n> blobs. --drop-filtered implies -d so the old promisor packs, which\n> still contain the dropped blobs, are removed. Without this the blobs\n> would survive in the redundant packs. The existing repack machinery\n> performs the write-before-delete and fsync, so the drop is crash-safe.\n> \n> The dropped blobs become absent locally but remain recoverable from the\n> promisor remote, so a later access lazy-fetches them back\n> transparently. --dry-run keeps its previous behavior, i.e. it lists the\n> candidates and changes nothing.\n> \n> Mentored-by: Christian Couder <christian.couder@gmail.com>\n> Mentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n> Signed-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n> ---\n>   builtin/repack.c                | 75 ++++++++++++++++++---------------\n>   repack-filtered.c               | 17 ++------\n>   repack.h                        |  4 +-\n>   t/t7706-repack-drop-filtered.sh | 18 +++++---\n>   4 files changed, 59 insertions(+), 55 deletions(-)\n> \n> diff --git a/builtin/repack.c b/builtin/repack.c\n> index c2b07477d2..aa3257a98a 100644\n> --- a/builtin/repack.c\n> +++ b/builtin/repack.c\n> @@ -15,6 +15,8 @@\n>   #include \"repack.h\"\n>   #include \"shallow.h\"\n>   #include \"list-objects-filter-options.h\"\n> +#include \"oidset.h\"\n> +#include \"hex.h\"\n>   \n>   #define ALL_INTO_ONE 1\n>   #define LOOSEN_UNREACHABLE 2\n> @@ -143,6 +145,7 @@ int cmd_repack(int argc,\n>   \tstruct string_list_item *item;\n>   \tstruct string_list names = STRING_LIST_INIT_DUP;\n>   \tstruct existing_packs existing = EXISTING_PACKS_INIT;\n> +\tstruct oidset drop_oids = OIDSET_INIT;\n>   \tstruct pack_geometry geometry = { 0 };\n>   \tstruct tempfile *refs_snapshot = NULL;\n>   \tint i, ret;\n> @@ -269,9 +272,6 @@ int cmd_repack(int argc,\n>   \t\tdie(_(\"--dry-run only takes effect with --drop-filtered\"));\n>   \n>   \tif (drop_filtered) {\n> -\t\tif (!dry_run)\n> -\t\t\tdie(_(\"--drop-filtered doesn't work without --dry-run yet\"));\n> -\n>   \t\tif (!po_args.filter_options.choice)\n>   \t\t\tdie(_(\"--drop-filtered requires --filter\"));\n>   \n> @@ -294,6 +294,28 @@ int cmd_repack(int argc,\n>   \t\t\tdie(_(\"--drop-filtered requires a promisor remote\"));\n>   \n>   \t\twrite_bitmaps = 0;\n> +\n> +\t\t/*\n> +\t\t * Dropping objects means rebuilding the promisor packs\n> +\t\t * without them and then removing the old packs, so the\n> +\t\t * redundant packs must be deleted. Imply -d on a real run.\n> +\t\t */\n> +\t\tif (!dry_run)\n> +\t\t\tdelete_redundant = 1;\n\n\nYes, without that the drop would not actually reclaim space.\n\nIt would be nice if the documentation mentioned that a real\n--drop-filtered run implies -d.\n\n\nThanks\n\n\n> +\n> +\t\tret = enumerate_promisor_blobs(repo, &po_args.filter_options, &drop_oids);\n> +\n> +\t\tif (ret)\n> +\t\t\tgoto cleanup;\n> +\n> +\t\tif (dry_run) {\n> +\t\t\tstruct oidset_iter iter;\n> +\t\t\tconst struct object_id *oid;\n> +\n> +\t\t\toidset_iter_init(&drop_oids, &iter);\n> +\t\t\twhile ((oid = oidset_iter_next(&iter)))\n> +\t\t\t\tprintf(\"%s\\n\", oid_to_hex(oid));\n> +\t\t}\n>   \t}\n>   \n>   \tif (delete_redundant && repo->repository_format_precious_objects)\n> @@ -406,7 +428,8 @@ int cmd_repack(int argc,\n>   \t\tstrvec_push(&cmd.args, \"--delta-islands\");\n>   \n>   \tif (pack_everything & ALL_INTO_ONE) {\n> -\t\trepack_promisor_objects(repo, &po_args, &names, packtmp, NULL);\n> +\t\trepack_promisor_objects(repo, &po_args, &names, packtmp,\n> +\t\t\t(drop_filtered && !dry_run) ? &drop_oids : NULL);\n>   \n>   \t\tif (existing_packs_has_non_kept(&existing) &&\n>   \t\t    delete_redundant &&\n> @@ -589,35 +612,20 @@ int cmd_repack(int argc,\n>   \t\t}\n>   \t}\n>   \n> -\tif (po_args.filter_options.choice) {\n> -\t\tif (drop_filtered) {\n> -\t\t\t/*\n> -\t\t\t * Enumerate promisor objects directly rather than\n> -\t\t\t * going through write_filtered_pack(). The filter\n> -\t\t\t * machinery cannot see promisor objects because\n> -\t\t\t * repack_promisor_objects() handles them separately\n> -\t\t\t * before the filter runs.\n> -\t\t\t */\n> -\t\t\tret = enumerate_promisor_blobs(repo,\n> -\t\t\t\t\t&po_args.filter_options,\n> -\t\t\t\t\tdry_run);\n> -\t\t\tif (ret)\n> -\t\t\t\tgoto cleanup;\n> -\t\t} else {\n> -\t\t\tstruct write_pack_opts opts = {\n> -\t\t\t\t.po_args = &po_args,\n> -\t\t\t\t.destination = filter_to,\n> -\t\t\t\t.packdir = packdir,\n> -\t\t\t\t.packtmp = packtmp,\n> -\t\t\t};\n> -\n> -\t\t\tif (!opts.destination)\n> -\t\t\t\topts.destination = packtmp;\n> -\n> -\t\t\tret = write_filtered_pack(&opts, &existing, &names);\n> -\t\t\tif (ret)\n> -\t\t\t\tgoto cleanup;\n> -\t\t}\n> +\tif (po_args.filter_options.choice && !drop_filtered) {\n> +\t\tstruct write_pack_opts opts = {\n> +\t\t\t.po_args = &po_args,\n> +\t\t\t.destination = filter_to,\n> +\t\t\t.packdir = packdir,\n> +\t\t\t.packtmp = packtmp,\n> +\t\t};\n> +\n> +\t\tif (!opts.destination)\n> +\t\t\topts.destination = packtmp;\n> +\n> +\t\tret = write_filtered_pack(&opts, &existing, &names);\n> +\t\tif (ret)\n> +\t\t\tgoto cleanup;\n>   \t}\n>   \n>   \tstring_list_sort(&names);\n> @@ -697,6 +705,7 @@ int cmd_repack(int argc,\n>   cleanup:\n>   \tstring_list_clear(&keep_pack_list, 0);\n>   \tstring_list_clear(&names, 1);\n> +\toidset_clear(&drop_oids);\n>   \texisting_packs_release(&existing);\n>   \tpack_geometry_release(&geometry);\n>   \tpack_objects_args_release(&po_args);\n> diff --git a/repack-filtered.c b/repack-filtered.c\n> index f5a1dae5b1..6f0cecca9b 100644\n> --- a/repack-filtered.c\n> +++ b/repack-filtered.c\n> @@ -87,16 +87,13 @@ static int collect_promisor_blob(const struct object_id *oid,\n>   \n>   int enumerate_promisor_blobs(struct repository *repo,\n>   \t\t\tconst struct list_objects_filter_options *filter,\n> -\t\t\tint dry_run)\n> +\t\t\tstruct oidset *to_drop)\n>   {\n>   \tstruct oidset all_promisor_blobs = OIDSET_INIT;\n> -\tstruct oidset to_drop = OIDSET_INIT;\n>   \tstruct collect_cb_data cb = {\n>   \t\t.repo = repo,\n>   \t\t.set = &all_promisor_blobs\n>   \t};\n> -\tstruct oidset_iter iter;\n> -\tconst struct object_id *oid;\n>   \tint ret = 0;\n>   \n>   \t/*\n> @@ -122,22 +119,14 @@ int enumerate_promisor_blobs(struct repository *repo,\n>   \n>   \t/*\n>   \t * Apply the filter to find which blobs exceed the threshold.\n> +\t * The caller has to_drop and is responsible for clearing it.\n>   \t */\n>   \tret = list_objects_filter__filter_oidset(repo,\n>   \t\t(struct list_objects_filter_options *)filter,\n>   \t\t&all_promisor_blobs,\n> -\t\t&to_drop);\n> -\tif (ret)\n> -\t\tgoto cleanup;\n> -\n> -\tif (dry_run) {\n> -\t\toidset_iter_init(&to_drop, &iter);\n> -\t\twhile ((oid = oidset_iter_next(&iter)))\n> -\t\t\tprintf(\"%s\\n\", oid_to_hex(oid));\n> -\t}\n> +\t\tto_drop);\n>   \n>   cleanup:\n>   \toidset_clear(&all_promisor_blobs);\n> -\toidset_clear(&to_drop);\n>   \treturn ret;\n>   }\n> diff --git a/repack.h b/repack.h\n> index d08e25b852..61e554e4ed 100644\n> --- a/repack.h\n> +++ b/repack.h\n> @@ -168,8 +168,8 @@ int write_filtered_pack(const struct write_pack_opts *opts,\n>   \t\t\tstruct string_list *names);\n>   \n>   int enumerate_promisor_blobs(struct repository *repo,\n> -\t\t\t       const struct list_objects_filter_options *filter,\n> -\t\t\t       int dry_run);\n> +\t\t\t     const struct list_objects_filter_options *filter,\n> +\t\t\t     struct oidset *to_drop);\n>   \n>   int write_cruft_pack(const struct write_pack_opts *opts,\n>   \t\t     const char *cruft_expiration,\n> diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh\n> index b558807847..41e7941799 100755\n> --- a/t/t7706-repack-drop-filtered.sh\n> +++ b/t/t7706-repack-drop-filtered.sh\n> @@ -56,12 +56,6 @@ test_expect_success '--dry-run only takes effect with --drop-filtered' '\n>   \ttest_grep \"dry-run only takes effect with --drop-filtered\" err\n>   '\n>   \n> -test_expect_success '--drop-filtered without --dry-run is rejected' '\n> -\ttest_must_fail git -C plain.git repack --drop-filtered \\\n> -\t\t--filter=blob:limit=1k -a 2>err &&\n> -\ttest_grep \"drop-filtered doesn.t work without --dry-run yet\" err\n> -'\n> -\n>   test_expect_success '--drop-filtered requires -a' '\n>   \ttest_must_fail git -C plain.git repack --drop-filtered \\\n>   \t\t--filter=blob:limit=1k --dry-run 2>err &&\n> @@ -136,4 +130,16 @@ test_expect_success '--dry-run does not remove the filtered objects' '\n>   \tgit -C repo cat-file -e \"$BIG\"\n>   '\n>   \n> +test_expect_success '--drop-filtered removes the promisor blob locally' '\n> +\tBIG=$(cat big_oid) &&\n> +\tSMALL=$(cat small_oid) &&\n> +\n> +\tgit -C repo -c repack.writeBitmaps=false \\\n> +\t\trepack --drop-filtered --filter=blob:limit=1k -a &&\n> +\n> +\tgit -C repo cat-file --batch-all-objects --batch-check=\"%(objectname)\" >present &&\n> +\t! grep -q \"$BIG\" present &&\n> +\tgrep -q \"$SMALL\" present\n> +'\n> +\n>   test_done\n\n"},{"id":"548975","messageId":"CAGWgyh_3xnVXLvBwGPTQfhBK3FzXuC50Xoy6xBN_+KQafDi-4w@mail.gmail.com","threadId":"66019","inReplyTo":"f9c26c07-1dde-4bb6-a919-37d5229642f5@gmail.com","subject":"Re: [RFC PATCH 0/7] repack: add --drop-filtered to reclaim space in partial clones","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-07-25T19:23:17Z","receivedAt":"2026-07-25T19:23:55Z","isPatch":true,"body":"Hi Siddharth,\n\nOn Fri, 24 Jul 2026 at 00:56, Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n> I think these matter before we present this as a real space-reclaim\n> tool. Without the index guard especially, users may drop blobs and then\n> immediately fetch them back on the next command that needs the worktree.\n\nright, I will move the safety guards into v2; refuse to run\nmid-merge/rebase/cherry-pick,\nand refuse to drop blobs referenced by the current index\n\n>\n> The drop log and remote-object-info can wait. I would not block the\n> next RFC round on them.\n\nsounds good, I can pull the drop log out of the core series and keep\nremote-object-info as a follow-up that upgrades the remote attribution\nonce it is available.\n\n\n> On the UI, I am fine with a separate --dry-run for now (same as\n> Christian). We can revisit a --drop-filtered=<mode> form later if we\n> grow more drop-specific options.\n\ni will keep the separate --dry-run for v2 and add a note to the commit message\nexplaining the choice, so the --drop-filtered=<mode> option stays open for later\nwithout committing to it now.\n"},{"id":"548977","messageId":"CAGWgyh_cNL=K-8JceMqZ6C9aEpKohnFhXEBxpMecnsHyoW3xXA@mail.gmail.com","threadId":"66019","inReplyTo":"ec546f71-3412-47ef-a4cf-98558889a90f@gmail.com","subject":"Re: [RFC PATCH 6/7] builtin/repack: actually drop filtered promisor blobs","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-07-25T19:30:11Z","receivedAt":"2026-07-25T19:30:48Z","isPatch":true,"body":"On 24/07/26 01:12, Siddharth Asthana wrote:\n> Yes, without that the drop would not actually reclaim space.\n>\n> It would be nice if the documentation mentioned that a real\n> --drop-filtered run implies -d.\n\nsounds good.. there is no git-repack documentation for --drop-filtered yet,\nso when I add it in v2 I'll call out explicitly that a real\n(non-dry-run) --drop-filtered run implies -d.\n"},{"id":"548978","messageId":"CAGWgyh8qYJzSX-SVNiQLVp8zuDHPpjXukEngY1_0WbF8NtyYdw@mail.gmail.com","threadId":"66019","inReplyTo":"e8d63121-fedc-49da-ad1a-0cbcf1a35a0b@gmail.com","subject":"Re: [RFC PATCH 7/7] repack-promisor: record dropped objects in a drop log","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-07-25T19:35:05Z","receivedAt":"2026-07-25T19:35:42Z","isPatch":true,"body":"On Fri, 24 Jul 2026 at 01:11, Siddharth Asthana\n<siddharthasthana31@gmail.com> wrote:\n> Recording all promisor remotes for now looks OK to me with that\n> NEEDSWORK. I would not block this on remote-object-info.\n>\n>\n> > +      * is currently no way to determine that locally. it would require\n> > +      * asking the remote whether it has the object. A \"remote-object-info\"\n> > +      * command is being added to the \"git cat-file --batch\" protocol for\n> > +      * this kind of query. Once it is merged in the codebase, this should\n> > +      * record the exact promisor remote that has each dropped object.\n> > +      */\n> > +     for (pr = repo_promisor_remote_find(repo, NULL); pr; pr = pr->next) {\n> > +             if (remotes.len)\n> > +                     strbuf_addch(&remotes, ',');\n> > +             strbuf_addstr(&remotes, pr->name);\n> > +     }\n> > +\n> > +     path = repo_git_path(repo, \"objects/info/promisor-dropped\");\n>\n>\n> If we keep it, it would be nice to document this path (for example in\n> gitrepository-layout) and to have a small test that a real drop appends\n> a line.\n\nsounds good, when the log is finalized, whether as a min.  version now\nor alongside the\nerror-path change later, I can document objects/info/promisor-dropped in\ngitrepository-layout then\n\nThanks,\nSiddharth Shrimali\n"},{"id":"549312","messageId":"20260730174153.9949-1-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260716132848.95982-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v2 0/7] repack: add --drop-filtered to reclaim space in partial clones","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-07-30T17:41:46Z","receivedAt":"2026-07-30T17:42:23Z","isPatch":true,"body":"This is v2 of the series adding \"git repack --drop-filtered\" to reclaim\ndisk space in partial clones by dropping large, locally-held promisor\nblobs that remain recoverable from the promisor remote. v1 was sent as\nan RFC [1].\n\nPartial clones let you work with large repositories without downloading\nevery blob up front. Mising blobs are lazily fetched from the promisor\nremote on demand. Over time these accumulate locally and there is\ncurrently no safe, built-in way to reclaim that space short of\nre-cloning. This series adds that reverse direction: enumerate promisor\nblobs over a size threshold, drop them locally, and rely on the existing\nlazy-fetch machinery to bring them back transparently when needed.\n\nHow it works:\n  * Enumerate promisor objects directly (ODB_FOR_EACH_OBJECT_PROMISOR_ONLY)\n    and select the blobs exceeding the filter threshold. Every enumerated\n    object is a promisor object by construction, so it is guaranteed\n    recoverable and locally-created objects are never candidates.\n  * Rebuild the promisor pack without the selected blobs, reusing the\n    existing repack machinery, so the drop is crash-safe (write, fsync,\n    install, then delete the old pack).\n  * --dry-run lists the candidates and changes nothing.\n\nSafety guards refuse to run while a merge, rebase, am, cherry-pick,\nrevert, or bisect is in progress, and refuse to drop a blob referenced\nby the current index (it would only be lazily re-fetched by the next\nworktree command). Both are skipped for bare repositories.\n\nChanges since v1:\n  * distinguish an explicit -b/--write-bitmap-index on the command line\n    (reported as a conflict) from a repack.writeBitmaps config value\n    (silently disabled for the command). This addresses Junio's review\n    that the previous check could not tell the two apart\n  * documented the choice to keep --dry-run as a separate option rather\n    than --drop-filtered=<mode>\n  * implemented the safety guards\n  * Added git-repack documentation for --drop-filtered and --dry-run\n  * Reorganised so enumerate_promisor_blobs() is introduced in its final\n    signature\n  * Distributed the tests into the commits that introduce each behavior,\n    instead of a single standalone test commit.\n  * Dropped the drop-log commit from this series\n\nTo do:\n  * Remote verification: verifying against the remote awaits the \"remote-object-info\"\n    cat-file protocol command.\n  * Drop log: introduce with the error-path change that reads it.\n  * --verbose: space-reclaimed reporting.\n\n[1] https://lore.kernel.org/git/20260716132848.95982-1-r.siddharth.shrimali@gmail.com/\n\nSiddharth Shrimali (7):\n  builtin/repack.c: add --drop-filtered and --dry-run options\n  list-objects-filter: add list_objects_filter__filter_oidset()\n  repack-promisor: allow excluding objects from the rebuilt promisor\n    pack\n  builtin/repack: enumerate promisor blobs for --drop-filtered\n  builtin/repack: actually drop filtered promisor blobs\n  builtin/repack: add safety guards for --drop-filtered\n  Documentation/git-repack: document --drop-filtered and --dry-run\n\n Documentation/git-repack.adoc   |  35 +++++++\n builtin/repack.c                | 135 +++++++++++++++++++++++-\n list-objects-filter.c           |  45 ++++++++\n list-objects-filter.h           |  16 +++\n repack-filtered.c               |  81 +++++++++++++++\n repack-promisor.c               |  15 ++-\n repack.h                        |   8 +-\n t/meson.build                   |   1 +\n t/t7706-repack-drop-filtered.sh | 179 ++++++++++++++++++++++++++++++++\n 9 files changed, 511 insertions(+), 4 deletions(-)\n create mode 100755 t/t7706-repack-drop-filtered.sh\n\n-- \n2.54.0\n\n"},{"id":"549313","messageId":"20260730174153.9949-2-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260730174153.9949-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v2 1/7] builtin/repack.c: add --drop-filtered and --dry-run options","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-07-30T17:41:47Z","receivedAt":"2026-07-30T17:42:28Z","isPatch":true,"body":"Add two new command-line options to 'git-repack':\n\n  --drop-filtered: intended to eventually delete objects that match\n                   the filter specification. Requires --filter and -a,\n                   and is incompatible with --filter-to.\n  --dry-run: show which objects would be dropped without making any\n             changes. Only meaningful with --drop-filtered.\n\nKeep --dry-run as a separate option rather than folding it into\n--drop-filtered (e.g --drop-filtered=dry-run), to stay consistent with\nthe --dry-run option other Git commands already provide and to leave\nroom for it to describe other repack behavior later. A\n--drop-filtered=<mode> form can still be added later if more\ndrop-specific modes are needed.\n\n--drop-filtered also requires a promisor remote to be configured, since\ndropping objects without a remote to fetch them back from would be\npermanent data loss.\n\n--drop-filtered is incompatible with bitmap writing: filtering breaks\nthe \"all objects in one pack\" closure that bitmaps require. Snapshot\nthe bitmap setting after config but before option parsing so an\nexplicit -b/--write-bitmap-index on the command line can be told apart\nfrom a repack.writeBitmaps configuration value. An explicit -b is\nreported as a conflict, while a config-provided default is silently\ndisabled for the duration of the command.\n\nThese options currently only perform validation. The actual enumeration\nand deletion will be added in follow-up commits.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n builtin/repack.c                | 63 +++++++++++++++++++++++++++++++++\n t/meson.build                   |  1 +\n t/t7706-repack-drop-filtered.sh | 49 +++++++++++++++++++++++++\n 3 files changed, 113 insertions(+)\n create mode 100755 t/t7706-repack-drop-filtered.sh\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex db504d673f..322b01cb3e 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -14,6 +14,7 @@\n #include \"promisor-remote.h\"\n #include \"repack.h\"\n #include \"shallow.h\"\n+#include \"list-objects-filter-options.h\"\n \n #define ALL_INTO_ONE 1\n #define LOOSEN_UNREACHABLE 2\n@@ -28,6 +29,8 @@ static int use_delta_islands;\n static int run_update_server_info = 1;\n static char *packdir, *packtmp_name, *packtmp;\n static int midx_must_contain_cruft = 1;\n+static int drop_filtered;\n+static int dry_run;\n \n static const char *const git_repack_usage[] = {\n \tN_(\"git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\\n\"\n@@ -148,6 +151,7 @@ int cmd_repack(int argc,\n \t/* variables to be filled by option parsing */\n \tstruct repack_config_ctx config_ctx;\n \tint delete_redundant = 0;\n+\tint write_bitmaps_before_parse;\n \tconst char *unpack_unreachable = NULL;\n \tint keep_unreachable = 0;\n \tstruct string_list keep_pack_list = STRING_LIST_INIT_NODUP;\n@@ -231,6 +235,10 @@ int cmd_repack(int argc,\n \t\t\t   N_(\"pack prefix to store a pack containing pruned objects\")),\n \t\tOPT_STRING(0, \"filter-to\", &filter_to, N_(\"dir\"),\n \t\t\t   N_(\"pack prefix to store a pack containing filtered out objects\")),\n+\t\tOPT_BOOL(0, \"drop-filtered\", &drop_filtered,\n+\t\t\t\tN_(\"delete filtered out objects (requires --filter)\")),\n+\t\tOPT_BOOL(0, \"dry-run\", &dry_run,\n+\t\t\t\tN_(\"only show which objects would be dropped\")),\n \t\tOPT_END()\n \t};\n \n@@ -244,6 +252,13 @@ int cmd_repack(int argc,\n \n \trepo_config(repo, repack_config, &config_ctx);\n \n+\t/*\n+\t * update the bitmap setting after config but before command line\n+\t * parsing, so we can later tell whether -b/--write-bitmap-index was\n+\t * given explicitly on the command line or not\n+\t */\n+\twrite_bitmaps_before_parse = write_bitmaps;\n+\n \targc = parse_options(argc, argv, prefix, builtin_repack_options,\n \t\t\t\tgit_repack_usage, 0);\n \n@@ -252,6 +267,54 @@ int cmd_repack(int argc,\n \tpo_args.depth = xstrdup_or_null(opt_depth);\n \tpo_args.threads = xstrdup_or_null(opt_threads);\n \n+\tdie_for_incompatible_opt2(drop_filtered, \"--drop-filtered\",\n+\t\t!!filter_to, \"--filter-to\");\n+\n+\tdie_for_incompatible_opt2(drop_filtered, \"--drop-filtered\",\n+\t\twrite_bitmaps > 0, \"--write-bitmap-index\");\n+\n+\tif (dry_run && !drop_filtered)\n+\t\tdie(_(\"--dry-run only takes effect with --drop-filtered\"));\n+\n+\tif (drop_filtered) {\n+\t\tint bitmaps_from_cmdline = (write_bitmaps != write_bitmaps_before_parse);\n+\n+\t\tif (!dry_run)\n+\t\t\tdie(_(\"--drop-filtered doesn't work without --dry-run yet\"));\n+\n+\t\tif (!po_args.filter_options.choice)\n+\t\t\tdie(_(\"--drop-filtered requires --filter\"));\n+\n+\t\tif (!(pack_everything & ALL_INTO_ONE))\n+\t\t\tdie(_(\"--drop-filtered requires -a\"));\n+\n+\t\t/*\n+\t\t * Only blob:limit=<n> is supported for now. Reject other\n+\t\t * filter choices early, before walking the object database.\n+\t\t */\n+\t\tif (po_args.filter_options.choice != LOFC_BLOB_LIMIT)\n+\t\t\tdie(_(\"--drop-filtered only supports --filter=blob:limit=<n> for now\"));\n+\n+\t\t/*\n+\t\t * an explicit -b on the command line is a conflict we have to\n+\t\t * report, a bitmap setting from config is silently overridden\n+\t\t * for the duration of the command\n+\t\t */\n+\t\tif (bitmaps_from_cmdline && write_bitmaps > 0)\n+\t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n+\t\t\t\t\"--drop-filtered\", \"--write-bitmap-index\");\n+\n+\t\t/*\n+\t\t * Without a promisor remote there is nowhere to re-fetch the\n+\t\t * dropped objects from, so dropping them would be permanent\n+\t\t * data loss.\n+\t\t */\n+\t\tif (!repo_has_promisor_remote(repo))\n+\t\t\tdie(_(\"--drop-filtered requires a promisor remote\"));\n+\n+\t\twrite_bitmaps = 0;\n+\t}\n+\n \tif (delete_redundant && repo->repository_format_precious_objects)\n \t\tdie(_(\"cannot delete packs in a precious-objects repo\"));\n \ndiff --git a/t/meson.build b/t/meson.build\nindex d8161c368b..c2bf60d129 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -963,6 +963,7 @@ integration_tests = [\n   't7703-repack-geometric.sh',\n   't7704-repack-cruft.sh',\n   't7705-repack-incremental-midx.sh',\n+  't7706-repack-drop-filtered.sh',\n   't7800-difftool.sh',\n   't7810-grep.sh',\n   't7811-grep-open.sh',\ndiff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh\nnew file mode 100755\nindex 0000000000..65be756e33\n--- /dev/null\n+++ b/t/t7706-repack-drop-filtered.sh\n@@ -0,0 +1,49 @@\n+#!/bin/sh\n+\n+test_description='git repack --drop-filtered option validation'\n+\n+. ./test-lib.sh\n+\n+# checks for options validations before any promisor walk\n+test_expect_success 'setup plain repo for validation' '\n+\tgit init plain &&\n+\ttest_commit -C plain initial &&\n+\tgit clone --bare plain plain.git &&\n+\tgit -C plain.git repack -a -d\n+'\n+\n+test_expect_success '--drop-filtered requires --filter' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered --dry-run -a 2>err &&\n+\ttest_grep \"drop-filtered requires --filter\" err\n+'\n+\n+test_expect_success '--drop-filtered cannot be used with --filter-to' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered \\\n+\t\t--filter=blob:limit=1k --filter-to=./filter-out 2>err &&\n+\ttest_grep \"options .--drop-filtered. and .--filter-to. cannot be used together\" err\n+'\n+\n+test_expect_success '--dry-run only takes effect with --drop-filtered' '\n+\ttest_must_fail git -C plain.git repack --dry-run 2>err &&\n+\ttest_grep \"dry-run only takes effect with --drop-filtered\" err\n+'\n+\n+test_expect_success '--drop-filtered requires -a' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered \\\n+\t\t--filter=blob:limit=1k --dry-run 2>err &&\n+\ttest_grep \"drop-filtered requires -a\" err\n+'\n+\n+test_expect_success '--drop-filtered fails with --write-bitmap-index' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered \\\n+\t\t--filter=blob:limit=1k --dry-run -a -b 2>err &&\n+\ttest_grep \"options .--drop-filtered. and .--write-bitmap-index. cannot be used together\" err\n+'\n+\n+test_expect_success '--drop-filtered fails without a promisor remote' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered \\\n+\t\t--filter=blob:limit=1k --dry-run -a 2>err &&\n+\ttest_grep \"drop-filtered requires a promisor remote\" err\n+'\n+\n+test_done\n-- \n2.54.0\n\n"},{"id":"549314","messageId":"20260730174153.9949-3-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260730174153.9949-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v2 2/7] list-objects-filter: add list_objects_filter__filter_oidset()","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-07-30T17:41:48Z","receivedAt":"2026-07-30T17:42:32Z","isPatch":true,"body":"The existing filter entry point, list_objects_filter__filter_object(),\nis built around the object-walk path: it expects traversal context and\nprovisional omit sets, and is meant to be called as objects are\nvisited during a walk. A caller that already has a set of OIDs in hand\nand only wants to know which ones a filter would select has no usable\nentry point into the filter API.\n\n--drop-filtered is exactly such a caller: it collects promisor blobs\ninto an oidset and needs to know which of them exceed the filter\nthreshold, without performing an object walk.\n\nAdd a helper, list_objects_filter__filter_oidset(), that takes a set\nof OIDs and populates an \"omitted\" set with those that would be\nfiltered out by the given filter options. Only blob:limit=N filters\nare supported for now.\n\nThis helper does not actually reuse the existing filter machinery.\nIt reimplements the blob:limit size check directly. That machinery\nis tied to the object-walk path and cannot easily be driven\nfrom a plain oidset. A NEEDSWORK comment marks this so the helper can\nlater be refactored to reuse the real filter logic instead of\nduplicating it.\n\nOBJECT_INFO_SKIP_FETCH_OBJECT is passed when reading object info so\nthe helper never triggers a lazy fetch.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n list-objects-filter.c | 45 +++++++++++++++++++++++++++++++++++++++++++\n list-objects-filter.h | 16 +++++++++++++++\n 2 files changed, 61 insertions(+)\n\ndiff --git a/list-objects-filter.c b/list-objects-filter.c\nindex c912ff3079..6a2e9d5b24 100644\n--- a/list-objects-filter.c\n+++ b/list-objects-filter.c\n@@ -828,3 +828,48 @@ void list_objects_filter__free(struct filter *filter)\n \tfilter->free_fn(filter->filter_data);\n \tfree(filter);\n }\n+\n+/*\n+ * NEEDSWORK: this reimplements the blob:limit size check rather than\n+ * reusing the existing filter machinery in\n+ * list_objects_filter__filter_object(). That machinery is currently\n+ * tied to the object-walk path and cannot easily be driven from a\n+ * plain oidset. It would be nice to refactor the filter code so this\n+ * helper can reuse it instead of duplicating the size check.\n+ */\n+int list_objects_filter__filter_oidset(struct repository *r,\n+\tstruct list_objects_filter_options *opts,\n+\tconst struct oidset *in,\n+\tstruct oidset *omitted)\n+{\n+\tstruct oidset_iter iter;\n+\tconst struct object_id *oid;\n+\n+\tif (opts->choice != LOFC_BLOB_LIMIT)\n+\t\treturn error(_(\"filter_oidset: only blob:limit filters are supported\"));\n+\n+\toidset_iter_init(in, &iter);\n+\twhile ((oid = oidset_iter_next(&iter))) {\n+\t\tstruct object_info info = OBJECT_INFO_INIT;\n+\t\tenum object_type type;\n+\t\tunsigned long size;\n+\n+\t\tinfo.typep = &type;\n+\t\tinfo.sizep = &size;\n+\n+\t\t/*\n+\t\t * Use OBJECT_INFO_SKIP_FETCH_OBJECT to avoid triggering\n+\t\t * a lazy fetch while inspecting candidates for removal.\n+\t\t */\n+\t\tif (odb_read_object_info_extended(r->objects, oid, &info,\n+\t\t\t\tOBJECT_INFO_SKIP_FETCH_OBJECT) < 0)\n+\t\t\tcontinue;\n+\n+\t\tif (type != OBJ_BLOB)\n+\t\t\tcontinue;\n+\n+\t\tif (size >= opts->blob_limit_value)\n+\t\t\toidset_insert(omitted, oid);\n+\t}\n+\treturn 0;\n+}\ndiff --git a/list-objects-filter.h b/list-objects-filter.h\nindex 9e98814111..56a2d87aa0 100644\n--- a/list-objects-filter.h\n+++ b/list-objects-filter.h\n@@ -94,4 +94,20 @@ enum list_objects_filter_result list_objects_filter__filter_object(\n  */\n void list_objects_filter__free(struct filter *filter);\n \n+/*\n+ * Given a set of OIDs in 'in', populate 'omitted' with those that\n+ * would be filtered by 'opts'. Currently only blob:limit=N is\n+ * supported. Objects that cannot be read are silently skipped.\n+ *\n+ * NEEDSWORK: this reimplements the blob:limit size check rather than\n+ * reusing the existing filter machinery. See the matching comment in\n+ * list-objects-filter.c.\n+ *\n+ * Return 0 on success, -1 if the filter is not supported.\n+ */\n+int list_objects_filter__filter_oidset(struct repository *r,\n+\tstruct list_objects_filter_options *opts,\n+\tconst struct oidset *in,\n+\tstruct oidset *omitted);\n+\n #endif /* LIST_OBJECTS_FILTER_H */\n-- \n2.54.0\n\n"},{"id":"549315","messageId":"20260730174153.9949-4-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260730174153.9949-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v2 3/7] repack-promisor: allow excluding objects from the rebuilt promisor pack","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-07-30T17:41:49Z","receivedAt":"2026-07-30T17:42:38Z","isPatch":true,"body":"Add a to_drop oidset parameter to repack_promisor_objects(). When it is\nnon-NULL, write_oid() omits those objects from the rebuilt promisor\npack. This is the mechanism --drop-filtered will use to remove promisor\nblobs, i.e. rebuild the promisor pack without them.\n\nAll existing callers pass NULL, so behavior is unchanged.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n builtin/repack.c  |  2 +-\n repack-promisor.c | 15 ++++++++++++++-\n repack.h          |  4 +++-\n 3 files changed, 18 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex 322b01cb3e..f25d189b07 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -425,7 +425,7 @@ int cmd_repack(int argc,\n \t\tstrvec_push(&cmd.args, \"--delta-islands\");\n \n \tif (pack_everything & ALL_INTO_ONE) {\n-\t\trepack_promisor_objects(repo, &po_args, &names, packtmp);\n+\t\trepack_promisor_objects(repo, &po_args, &names, packtmp, NULL);\n \n \t\tif (existing_packs_has_non_kept(&existing) &&\n \t\t    delete_redundant &&\ndiff --git a/repack-promisor.c b/repack-promisor.c\nindex 90318ce150..fabfdc168a 100644\n--- a/repack-promisor.c\n+++ b/repack-promisor.c\n@@ -6,10 +6,12 @@\n #include \"path.h\"\n #include \"repository.h\"\n #include \"run-command.h\"\n+#include \"oidset.h\"\n \n struct write_oid_context {\n \tstruct child_process *cmd;\n \tconst struct git_hash_algo *algop;\n+\tconst struct oidset *to_drop;\n };\n \n /*\n@@ -23,6 +25,15 @@ static int write_oid(const struct object_id *oid,\n \tstruct write_oid_context *ctx = data;\n \tstruct child_process *cmd = ctx->cmd;\n \n+\t/*\n+\t * Objects in to_drop are being removed from the repository, so\n+\t * omit them from the rebuilt promisor pack. Each such object is a\n+\t * promisor object and therefore remains recoverable from the\n+\t * promisor remote.\n+\t */\n+\tif (ctx->to_drop && oidset_contains(ctx->to_drop, oid))\n+\t\treturn 0;\n+\n \tif (cmd->in == -1) {\n \t\tif (start_command(cmd))\n \t\t\tdie(_(\"could not start pack-objects to repack promisor objects\"));\n@@ -81,7 +92,8 @@ static void finish_repacking_promisor_objects(struct repository *repo,\n \n void repack_promisor_objects(struct repository *repo,\n \t\t\t     const struct pack_objects_args *args,\n-\t\t\t     struct string_list *names, const char *packtmp)\n+\t\t\t     struct string_list *names, const char *packtmp,\n+\t\t\t     const struct oidset *to_drop)\n {\n \tstruct write_oid_context ctx;\n \tstruct child_process cmd = CHILD_PROCESS_INIT;\n@@ -98,6 +110,7 @@ void repack_promisor_objects(struct repository *repo,\n \t */\n \tctx.cmd = &cmd;\n \tctx.algop = repo->hash_algo;\n+\tctx.to_drop = to_drop;\n \todb_for_each_object(repo->objects, NULL, write_oid, &ctx,\n \t\t\t    ODB_FOR_EACH_OBJECT_PROMISOR_ONLY);\n \ndiff --git a/repack.h b/repack.h\nindex f9fbc895f0..a5a3f7c6ba 100644\n--- a/repack.h\n+++ b/repack.h\n@@ -3,6 +3,7 @@\n \n #include \"list-objects-filter-options.h\"\n #include \"string-list.h\"\n+#include \"oidset.h\"\n \n struct pack_objects_args {\n \tchar *window;\n@@ -100,7 +101,8 @@ void generated_pack_install(struct generated_pack *pack, const char *name,\n \n void repack_promisor_objects(struct repository *repo,\n \t\t\t     const struct pack_objects_args *args,\n-\t\t\t     struct string_list *names, const char *packtmp);\n+\t\t\t     struct string_list *names, const char *packtmp,\n+\t\t\t     const struct oidset *to_drop);\n \n struct pack_geometry {\n \tstruct packed_git **pack;\n-- \n2.54.0\n\n"},{"id":"549316","messageId":"20260730174153.9949-5-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260730174153.9949-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v2 4/7] builtin/repack: enumerate promisor blobs for --drop-filtered","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-07-30T17:41:50Z","receivedAt":"2026-07-30T17:42:44Z","isPatch":true,"body":"Add enumeration logic for --drop-filtered. In --dry-run mode, print\nthe OIDs of locally-held promisor blobs that exceed the filter\nthreshold, as candidates for removal.\n\nReading from write_filtered_pack() cannot work for partial clones.\ngit repack routes promisor objects through a separate path:\nrepack_promisor_objects() repacks them first, and the main\npack-objects run uses --exclude-promisor-objects. By the time\nwrite_filtered_pack() runs, the promisor blobs are already consumed by\nthe main pack. The filtered pack is always empty on a partial clone.\n\nInstead, walk promisor objects directly via odb_for_each_object() with\nODB_FOR_EACH_OBJECT_PROMISOR_ONLY, collecting all promisor blobs into\nan oidset. The blobs exceeding the filter threshold are then selected\nusing list_objects_filter__filter_oidset().\n\nEvery object enumerated this way is a promisor object by construction,\nso it is guaranteed to be recoverable from the promisor remote and is\nsafe to drop. No separate is_promisor_object() check is needed.\n\nOBJECT_INFO_SKIP_FETCH_OBJECT is passed to every object info query so\nenumeration never triggers a lazy fetch.\n\nThe enumeration collects candidates into a caller-provided oidset and\n--dry-run prints them. Actually removing the objects, together with the\nrequired promisor-remote verification, is written in a later commit.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n builtin/repack.c                | 20 +++++++-\n repack-filtered.c               | 80 +++++++++++++++++++++++++++++++\n repack.h                        |  4 ++\n t/t7706-repack-drop-filtered.sh | 84 ++++++++++++++++++++++++++++++++-\n 4 files changed, 186 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex f25d189b07..8cb92d1a62 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -15,6 +15,8 @@\n #include \"repack.h\"\n #include \"shallow.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"oidset.h\"\n+#include \"hex.h\"\n \n #define ALL_INTO_ONE 1\n #define LOOSEN_UNREACHABLE 2\n@@ -143,6 +145,7 @@ int cmd_repack(int argc,\n \tstruct string_list_item *item;\n \tstruct string_list names = STRING_LIST_INIT_DUP;\n \tstruct existing_packs existing = EXISTING_PACKS_INIT;\n+\tstruct oidset drop_oids = OIDSET_INIT;\n \tstruct pack_geometry geometry = { 0 };\n \tstruct tempfile *refs_snapshot = NULL;\n \tint i, ret;\n@@ -313,6 +316,20 @@ int cmd_repack(int argc,\n \t\t\tdie(_(\"--drop-filtered requires a promisor remote\"));\n \n \t\twrite_bitmaps = 0;\n+\n+\t\tret = enumerate_promisor_blobs(repo, &po_args.filter_options, &drop_oids);\n+\n+\t\tif (ret)\n+\t\t\tgoto cleanup;\n+\n+\t\tif (dry_run) {\n+\t\t\tstruct oidset_iter iter;\n+\t\t\tconst struct object_id *oid;\n+\n+\t\t\toidset_iter_init(&drop_oids, &iter);\n+\t\t\twhile ((oid = oidset_iter_next(&iter)))\n+\t\t\t\tprintf(\"%s\\n\", oid_to_hex(oid));\n+\t\t}\n \t}\n \n \tif (delete_redundant && repo->repository_format_precious_objects)\n@@ -608,7 +625,7 @@ int cmd_repack(int argc,\n \t\t}\n \t}\n \n-\tif (po_args.filter_options.choice) {\n+\tif (po_args.filter_options.choice && !drop_filtered) {\n \t\tstruct write_pack_opts opts = {\n \t\t\t.po_args = &po_args,\n \t\t\t.destination = filter_to,\n@@ -701,6 +718,7 @@ int cmd_repack(int argc,\n cleanup:\n \tstring_list_clear(&keep_pack_list, 0);\n \tstring_list_clear(&names, 1);\n+\toidset_clear(&drop_oids);\n \texisting_packs_release(&existing);\n \tpack_geometry_release(&geometry);\n \tpack_objects_args_release(&po_args);\ndiff --git a/repack-filtered.c b/repack-filtered.c\nindex edcf7667c5..217fc54d7b 100644\n--- a/repack-filtered.c\n+++ b/repack-filtered.c\n@@ -3,6 +3,12 @@\n #include \"repository.h\"\n #include \"run-command.h\"\n #include \"string-list.h\"\n+#include \"hex.h\"\n+#include \"packfile.h\"\n+#include \"list-objects-filter-options.h\"\n+#include \"list-objects-filter.h\"\n+#include \"odb.h\"\n+#include \"promisor-remote.h\"\n \n int write_filtered_pack(const struct write_pack_opts *opts,\n \t\t\tstruct existing_packs *existing,\n@@ -49,3 +55,77 @@ int write_filtered_pack(const struct write_pack_opts *opts,\n \treturn finish_pack_objects_cmd(existing->repo->hash_algo, opts, &cmd,\n \t\t\t\t       names);\n }\n+\n+struct collect_cb_data {\n+\tstruct repository *repo;\n+\tstruct oidset *set;\n+};\n+\n+static int collect_promisor_blob(const struct object_id *oid,\n+\t\t\t\t struct object_info *oi UNUSED,\n+\t\t\t\t void *cb_data)\n+{\n+\tstruct collect_cb_data *data = cb_data;\n+\tstruct object_info info = OBJECT_INFO_INIT;\n+\tenum object_type type;\n+\n+\tinfo.typep = &type;\n+\n+\t/*\n+\t * Use OBJECT_INFO_SKIP_FETCH_OBJECT to avoid triggering a\n+\t * lazy fetch while collecting promisor blobs.\n+\t */\n+\tif (odb_read_object_info_extended(data->repo->objects, oid, &info,\n+\t\t\tOBJECT_INFO_SKIP_FETCH_OBJECT) < 0)\n+\t\treturn 0;\n+\n+\tif (type == OBJ_BLOB)\n+\t\toidset_insert(data->set, oid);\n+\n+\treturn 0;\n+}\n+\n+int enumerate_promisor_blobs(struct repository *repo,\n+\t\t\t     const struct list_objects_filter_options *filter,\n+\t\t\t     struct oidset *to_drop)\n+{\n+\tstruct oidset all_promisor_blobs = OIDSET_INIT;\n+\tstruct collect_cb_data cb = {\n+\t\t.repo = repo,\n+\t\t.set = &all_promisor_blobs\n+\t};\n+\tint ret = 0;\n+\n+\t/*\n+\t * The caller (cmd_repack) is responsible for validating that a\n+\t * blob:limit filter and a promisor remote are present before\n+\t * calling this function.\n+\t *\n+\t * Walk only promisor objects. Every object visited here is\n+\t * guaranteed to be recoverable from the promisor remote, so\n+\t * it is safe to drop.\n+\t *\n+\t * We do not use write_filtered_pack() here because git repack\n+\t * routes promisor objects through repack_promisor_objects()\n+\t * before the filter machinery runs, so the filtered pack never\n+\t * contains promisor blobs. Direct enumeration via\n+\t * ODB_FOR_EACH_OBJECT_PROMISOR_ONLY is the correct approach.\n+\t */\n+\tret = odb_for_each_object(repo->objects, NULL,\n+\t\t\tcollect_promisor_blob, &cb,\n+\t\t\tODB_FOR_EACH_OBJECT_PROMISOR_ONLY);\n+\tif (ret)\n+\t\tgoto cleanup;\n+\n+\t/*\n+\t * Apply the filter to find which blobs exceed the threshold.\n+\t */\n+\tret = list_objects_filter__filter_oidset(repo,\n+\t\t(struct list_objects_filter_options *)filter,\n+\t\t&all_promisor_blobs,\n+\t\tto_drop);\n+\n+cleanup:\n+\toidset_clear(&all_promisor_blobs);\n+\treturn ret;\n+}\ndiff --git a/repack.h b/repack.h\nindex a5a3f7c6ba..61e554e4ed 100644\n--- a/repack.h\n+++ b/repack.h\n@@ -167,6 +167,10 @@ int write_filtered_pack(const struct write_pack_opts *opts,\n \t\t\tstruct existing_packs *existing,\n \t\t\tstruct string_list *names);\n \n+int enumerate_promisor_blobs(struct repository *repo,\n+\t\t\t     const struct list_objects_filter_options *filter,\n+\t\t\t     struct oidset *to_drop);\n+\n 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,\ndiff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh\nindex 65be756e33..cbdb580702 100755\n--- a/t/t7706-repack-drop-filtered.sh\n+++ b/t/t7706-repack-drop-filtered.sh\n@@ -1,9 +1,36 @@\n #!/bin/sh\n \n-test_description='git repack --drop-filtered option validation'\n+test_description='git repack --drop-filtered enumerates filtered promisor blobs'\n \n . ./test-lib.sh\n \n+delete_object () {\n+\tlocal repo=\"$1\" &&\n+\tlocal obj=\"$2\" &&\n+\tlocal path=\"$repo/.git/objects/$(test_oid_to_path \"$obj\")\" &&\n+\trm \"$path\"\n+}\n+\n+# pack the objects into a promisor pack inside \"repo\". it is a pack\n+# accompanied by an empty \".promisor\" marker file. objects\n+# in such a pack are treated as recoverable from the promisor remote.\n+pack_as_from_promisor () {\n+\tHASH=$(git -C repo pack-objects .git/objects/pack/pack) &&\n+\t>repo/.git/objects/pack/pack-$HASH.promisor &&\n+\techo $HASH\n+}\n+\n+# write a blob of $1 bytes into \"repo\", record it as coming from the\n+# promisor remote, and remove the loose copy so the object is only\n+# present in the promisor pack\n+promisor_blob () {\n+\ttest-tool genrandom \"$1\" \"$2\" >blob_content &&\n+\tOID=$(git -C repo hash-object -w --stdin <blob_content) &&\n+\tprintf \"%s\\n\" \"$OID\" | pack_as_from_promisor >/dev/null &&\n+\tdelete_object repo \"$OID\" &&\n+\techo \"$OID\"\n+}\n+\n # checks for options validations before any promisor walk\n test_expect_success 'setup plain repo for validation' '\n \tgit init plain &&\n@@ -46,4 +73,59 @@ test_expect_success '--drop-filtered fails without a promisor remote' '\n \ttest_grep \"drop-filtered requires a promisor remote\" err\n '\n \n+# enumeration tests using promisor pack\n+test_expect_success 'setup repo with a promisor remote' '\n+\trm -rf repo &&\n+\ttest_create_repo repo &&\n+\ttest_commit -C repo base &&\n+\n+\t# mark the repo as a partial clone with a promisor remote so the\n+\t# promisor walk and the safety guard are satisfied\n+\tgit -C repo config core.repositoryformatversion 1 &&\n+\tgit -C repo config extensions.partialclone origin &&\n+\tgit -C repo config remote.origin.promisor true &&\n+\tgit -C repo config remote.origin.url \".\" &&\n+\n+\tBIG=$(promisor_blob big 3072) &&\n+\tSMALL=$(promisor_blob small 512) &&\n+\techo \"$BIG\" >big_oid &&\n+\techo \"$SMALL\" >small_oid\n+'\n+\n+test_expect_success 'promisor blob over the threshold is listed' '\n+\tBIG=$(cat big_oid) &&\n+\tSMALL=$(cat small_oid) &&\n+\n+\tgit -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a >out &&\n+\n+\ttest_grep \"$BIG\" out &&\n+\ttest_grep ! \"$SMALL\" out\n+'\n+\n+test_expect_success 'locally created blob is never listed' '\n+\tBIG=$(cat big_oid) &&\n+\n+\t# large blob that exists only locally must never be a drop candidate.\n+\t# dropping it would be unrecoverable\n+\ttest-tool genrandom local 4096 >local_content &&\n+\tLOCAL=$(git -C repo hash-object -w --stdin <local_content) &&\n+\n+\tgit -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a >out &&\n+\n+\ttest_grep \"$BIG\" out &&\n+\ttest_grep ! \"$LOCAL\" out\n+'\n+\n+test_expect_success '--dry-run does not remove the filtered objects' '\n+\tBIG=$(cat big_oid) &&\n+\n+\tgit -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a >out &&\n+\n+\t# candidate blob must still be present after a dry run\n+\tgit -C repo cat-file -e \"$BIG\"\n+'\n+\n test_done\n-- \n2.54.0\n\n"},{"id":"549317","messageId":"20260730174153.9949-6-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260730174153.9949-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v2 5/7] builtin/repack: actually drop filtered promisor blobs","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-07-30T17:41:51Z","receivedAt":"2026-07-30T17:42:50Z","isPatch":true,"body":"Make --drop-filtered remove the enumerated promisor blobs instead of\nonly listing them.\n\nThe drop set is computed before repack_promisor_objects() runs, and on\na real run it is passed in so the rebuilt promisor pack omits those\nblobs. --drop-filtered implies -d so the old promisor packs, which\nstill contain the dropped blobs, are removed. Without this the blobs\nwould survive in the redundant packs. The existing repack machinery\nperforms the write-before-delete and fsync, so the drop is crash-safe.\n\nThe dropped blobs become absent locally but remain recoverable from the\npromisor remote, so a later access lazy-fetches them back\ntransparently. --dry-run keeps its previous behavior, i.e. it lists the\ncandidates and changes nothing.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n builtin/repack.c                | 17 ++++++++++-------\n repack-filtered.c               |  1 +\n t/t7706-repack-drop-filtered.sh | 12 ++++++++++++\n 3 files changed, 23 insertions(+), 7 deletions(-)\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex 8cb92d1a62..9a15ab1f2a 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -273,18 +273,12 @@ int cmd_repack(int argc,\n \tdie_for_incompatible_opt2(drop_filtered, \"--drop-filtered\",\n \t\t!!filter_to, \"--filter-to\");\n \n-\tdie_for_incompatible_opt2(drop_filtered, \"--drop-filtered\",\n-\t\twrite_bitmaps > 0, \"--write-bitmap-index\");\n-\n \tif (dry_run && !drop_filtered)\n \t\tdie(_(\"--dry-run only takes effect with --drop-filtered\"));\n \n \tif (drop_filtered) {\n \t\tint bitmaps_from_cmdline = (write_bitmaps != write_bitmaps_before_parse);\n \n-\t\tif (!dry_run)\n-\t\t\tdie(_(\"--drop-filtered doesn't work without --dry-run yet\"));\n-\n \t\tif (!po_args.filter_options.choice)\n \t\t\tdie(_(\"--drop-filtered requires --filter\"));\n \n@@ -317,6 +311,14 @@ int cmd_repack(int argc,\n \n \t\twrite_bitmaps = 0;\n \n+\t\t/*\n+\t\t * Dropping objects means rebuilding the promisor packs\n+\t\t * without them and then removing the old packs, so the\n+\t\t * redundant packs must be deleted. Imply -d on a real run.\n+\t\t */\n+\t\tif (!dry_run)\n+\t\t\tdelete_redundant = 1;\n+\n \t\tret = enumerate_promisor_blobs(repo, &po_args.filter_options, &drop_oids);\n \n \t\tif (ret)\n@@ -442,7 +444,8 @@ int cmd_repack(int argc,\n \t\tstrvec_push(&cmd.args, \"--delta-islands\");\n \n \tif (pack_everything & ALL_INTO_ONE) {\n-\t\trepack_promisor_objects(repo, &po_args, &names, packtmp, NULL);\n+\t\trepack_promisor_objects(repo, &po_args, &names, packtmp,\n+\t\t\t(drop_filtered && !dry_run) ? &drop_oids : NULL);\n \n \t\tif (existing_packs_has_non_kept(&existing) &&\n \t\t    delete_redundant &&\ndiff --git a/repack-filtered.c b/repack-filtered.c\nindex 217fc54d7b..08796818d8 100644\n--- a/repack-filtered.c\n+++ b/repack-filtered.c\n@@ -119,6 +119,7 @@ int enumerate_promisor_blobs(struct repository *repo,\n \n \t/*\n \t * Apply the filter to find which blobs exceed the threshold.\n+\t * The caller has to_drop and is responsible for clearing it.\n \t */\n \tret = list_objects_filter__filter_oidset(repo,\n \t\t(struct list_objects_filter_options *)filter,\ndiff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh\nindex cbdb580702..b3e493e851 100755\n--- a/t/t7706-repack-drop-filtered.sh\n+++ b/t/t7706-repack-drop-filtered.sh\n@@ -128,4 +128,16 @@ test_expect_success '--dry-run does not remove the filtered objects' '\n \tgit -C repo cat-file -e \"$BIG\"\n '\n \n+test_expect_success '--drop-filtered removes the promisor blob locally' '\n+\tBIG=$(cat big_oid) &&\n+\tSMALL=$(cat small_oid) &&\n+\n+\tgit -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k -a &&\n+\n+\tgit -C repo cat-file --batch-all-objects --batch-check=\"%(objectname)\" >present &&\n+\t! grep -q \"$BIG\" present &&\n+\tgrep -q \"$SMALL\" present\n+'\n+\n test_done\n-- \n2.54.0\n\n"},{"id":"549318","messageId":"20260730174153.9949-7-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260730174153.9949-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v2 6/7] builtin/repack: add safety guards for --drop-filtered","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-07-30T17:41:52Z","receivedAt":"2026-07-30T17:42:56Z","isPatch":true,"body":"--drop-filtered removes local promisor blobs. That is only safe when the\nrepository is not mid-operation and when the blobs are not actively in\nuse, so add two guards, both skipped for bare repositories which have\nneither a worktree nor an index.\n\nFirst, refuse to run while a merge, rebase, am, cherry-pick, revert, or\nbisect is in progress. During these operations the working tree and\nindex are in an intermediate state, and rewriting packs and deleting\nobjects underneath a half-finished operation is unsafe.\n\nSecond, refuse to drop a blob that the current index references. Such a\nblob is needed by the working tree, so dropping it would only cause the\nnext command that touches the worktree to lazy-fetch it straight back,\nreclaiming nothing. The offending path is reported so the user can see\nwhy the drop was refused.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n builtin/repack.c                | 47 +++++++++++++++++++++++++++++++++\n t/t7706-repack-drop-filtered.sh | 36 +++++++++++++++++++++++++\n 2 files changed, 83 insertions(+)\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex 9a15ab1f2a..2339bcaac4 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -17,6 +17,8 @@\n #include \"list-objects-filter-options.h\"\n #include \"oidset.h\"\n #include \"hex.h\"\n+#include \"wt-status.h\"\n+#include \"read-cache-ll.h\"\n \n #define ALL_INTO_ONE 1\n #define LOOSEN_UNREACHABLE 2\n@@ -309,6 +311,28 @@ int cmd_repack(int argc,\n \t\tif (!repo_has_promisor_remote(repo))\n \t\t\tdie(_(\"--drop-filtered requires a promisor remote\"));\n \n+\t\t/*\n+\t\t * refuse to drop objects while another operation is in\n+\t\t * progress. the working tree and index are in an\n+\t\t * intermediate state, and rewriting packs in a half-finished\n+\t\t * merge/rebase/cherry-pick/revert/bisect is unsafe\n+\t\t * bare repositories have no such state, so the check\n+\t\t * is skipped there\n+\t\t */\n+\t\tif (!is_bare_repository(repo)) {\n+\t\t\tstruct wt_status_state state = { 0 };\n+\n+\t\t\twt_status_get_state(repo, &state, 0);\n+\t\t\tif (state.merge_in_progress || state.revert_in_progress ||\n+\t\t\t    state.rebase_in_progress ||state.bisect_in_progress ||\n+\t\t\t    state.cherry_pick_in_progress ||state.am_in_progress||\n+\t\t\t    state.rebase_interactive_in_progress) {\n+\t\t\t\twt_status_state_free_buffers(&state);\n+\t\t\t\tdie(_(\"--drop-filtered cannot be used while another operation is in progress\"));\n+\t\t\t}\n+\t\t\twt_status_state_free_buffers(&state);\n+\t\t}\n+\n \t\twrite_bitmaps = 0;\n \n \t\t/*\n@@ -324,6 +348,29 @@ int cmd_repack(int argc,\n \t\tif (ret)\n \t\t\tgoto cleanup;\n \n+\t\t/*\n+\t\t * refuse to drop blobs that the current index references.\n+\t\t * dropping such a blob would cause the very next command\n+\t\t * that touches the worktree to lazy-fetch it straight back, so\n+\t\t * the drop would reclaim nothing. bare repositories have no\n+\t\t * index, so the check is skipped there.\n+\t\t */\n+\t\tif (!is_bare_repository(repo) && oidset_size(&drop_oids)) {\n+\t\t\tstruct index_state *istate = repo->index;\n+\t\t\tunsigned int i;\n+\n+\t\t\tif (repo_read_index(repo) < 0)\n+\t\t\t\tdie(_(\"could not read the index\"));\n+\n+\t\t\tfor (i = 0; i < istate->cache_nr; i++) {\n+\t\t\t\tconst struct cache_entry *ce = istate->cache[i];\n+\n+\t\t\t\tif (oidset_contains(&drop_oids, &ce->oid))\n+\t\t\t\t\tdie(_(\"cannot drop '%s' (%s): it is referenced by the current index\"),\n+\t\t\t\t\t\tce->name, oid_to_hex(&ce->oid));\n+\t\t\t}\n+\t\t}\n+\n \t\tif (dry_run) {\n \t\t\tstruct oidset_iter iter;\n \t\t\tconst struct object_id *oid;\ndiff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh\nindex b3e493e851..dabed97541 100755\n--- a/t/t7706-repack-drop-filtered.sh\n+++ b/t/t7706-repack-drop-filtered.sh\n@@ -140,4 +140,40 @@ test_expect_success '--drop-filtered removes the promisor blob locally' '\n \tgrep -q \"$SMALL\" present\n '\n \n+test_expect_success '--drop-filtered refuses when a merge is in progress' '\n+\ttest_when_finished \"git -C repo merge --abort || :\" &&\n+\n+\t# creat a conflicting merge so wt_status reports it\n+\tgit -C repo checkout -B mergebase base &&\n+\techo one >repo/conflict.txt &&\n+\tgit -C repo add conflict.txt &&\n+\tgit -C repo commit -m one &&\n+\n+\tgit -C repo checkout -B mergeother base &&\n+\techo two >repo/conflict.txt &&\n+\tgit -C repo add conflict.txt &&\n+\tgit -C repo commit -m two &&\n+\n+\ttest_must_fail git -C repo merge mergebase &&\n+\n+\ttest_must_fail git -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err &&\n+\ttest_grep \"in progress\" err\n+'\n+\n+\n+test_expect_success '--drop-filtered refuses to drop an index-referenced blob' '\n+\t# create a large blob, add it to the index and make it a promisor object\n+\t# so the index references it and enumeration picks it up\n+\ttest-tool genrandom idx 4096 >repo/tracked-big.bin &&\n+\tgit -C repo add tracked-big.bin &&\n+\tOID=$(git -C repo rev-parse :tracked-big.bin) &&\n+\tprintf \"%s\\n\" \"$OID\" | pack_as_from_promisor >/dev/null &&\n+\tdelete_object repo \"$OID\" &&\n+\n+\ttest_must_fail git -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err &&\n+\ttest_grep \"referenced by the current index\" err\n+'\n+\n test_done\n-- \n2.54.0\n\n"},{"id":"549319","messageId":"20260730174153.9949-8-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260730174153.9949-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v2 7/7] Documentation/git-repack: document --drop-filtered and --dry-run","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-07-30T17:41:53Z","receivedAt":"2026-07-30T17:43:01Z","isPatch":true,"body":"Describe the new --drop-filtered and --dry-run options: what they do,\nonly blob:limit filters are supported for now, a promisor remote is\nrequired, --drop-filtered requires -a and implies -d so the redundant\npacks are actually removed, its incompatibilities with --filter-to and\nbitmap writing, and the safety guards that refuse to run mid-operation\nor to drop index-referenced blobs.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n Documentation/git-repack.adoc | 35 +++++++++++++++++++++++++++++++++++\n 1 file changed, 35 insertions(+)\n\ndiff --git a/Documentation/git-repack.adoc b/Documentation/git-repack.adoc\nindex 72c42015e2..9efff838f2 100644\n--- a/Documentation/git-repack.adoc\n+++ b/Documentation/git-repack.adoc\n@@ -12,6 +12,7 @@ SYNOPSIS\n 'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\n \t[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]\n \t[--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]\n+\t[--filter=<filter-spec>] [--drop-filtered [--dry-run]]]\n \n DESCRIPTION\n -----------\n@@ -182,6 +183,40 @@ depth is 4095.\n \t`objects` and `objects/info/alternates` sections of\n \tlinkgit:gitrepository-layout[5].\n \n+--drop-filtered::\n+\tDelete the local objects that match the `--filter` specification\n+\tinstead of keeping them in a separate packfile, reclaiming the\n+\tdisk space they occupy. This is intended for partial clones,\n+\twhere the filtered objects are promisor objects that remain\n+\trecoverable from the promisor remote and are lazily re-fetched\n+\ton demand when they are next needed.\n++\n+Only large blobs are supported for now, so `--filter=blob:limit=<n>`\n+is currently the only accepted filter. Because dropped objects must be\n+recoverable, this option requires a promisor remote to be configured\n+and refuses to run otherwise.\n++\n+This option requires `-a`, and implies `-d`: the objects are dropped by\n+rebuilding the promisor pack without them and then removing the now\n+redundant old packs, so the redundant packs must be deleted for the\n+space to actually be reclaimed. It is incompatible with `--filter-to`\n+and with bitmap writing (`-b`/`--write-bitmap-index`), since filtering\n+breaks the single-pack closure that bitmaps require. A bitmap setting\n+coming from configuration is silently disabled for the duration of the\n+command.\n++\n+As a safety measure, `--drop-filtered` refuses to run while another\n+operation (merge, rebase, am, cherry-pick, revert, or bisect) is in\n+progress, and refuses to drop any blob that the current index\n+references, since such a blob would only be lazily re-fetched by the\n+next command that inspects the working tree. These checks are skipped\n+in bare repositories, which have neither a working tree nor an index.\n+\n+--dry-run::\n+\tOnly meaningful with `--drop-filtered`. List the objects that\n+\twould be dropped, one object ID per line, without rebuilding any\n+\tpack or deleting anything.\n+\n -b::\n --write-bitmap-index::\n \tWrite a reachability bitmap index as part of the repack. This\n-- \n2.54.0\n\n"},{"id":"549353","messageId":"xmqqcxw3dvh5.fsf@gitster.g","threadId":"66019","inReplyTo":"20260730174153.9949-1-r.siddharth.shrimali@gmail.com","subject":"Re: [GSoC PATCH v2 0/7] repack: add --drop-filtered to reclaim space in partial clones","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-31T15:33:58Z","receivedAt":"2026-07-31T15:34:01Z","isPatch":true,"body":"Siddharth Shrimali <r.siddharth.shrimali@gmail.com> writes:\n\n> How it works:\n>   * Enumerate promisor objects directly (ODB_FOR_EACH_OBJECT_PROMISOR_ONLY)\n>     and select the blobs exceeding the filter threshold. Every enumerated\n>     object is a promisor object by construction, so it is guaranteed\n>     recoverable and locally-created objects are never candidates.\n\nBy 'by construction', do you mean 'It is guaranteed recoverable, as\nlong as ODB_FOR_EACH_OBJECT_PROMISOR_ONLY is working correctly'?\nSince I do not use it, I do not personally trust promisor-based\ntraversal all that much, and it would be great if we could hear from\nother practitioners that this really works well.\n\n>   * Rebuild the promisor pack without the selected blobs, reusing the\n>     existing repack machinery, so the drop is crash-safe (write, fsync,\n>     install, then delete the old pack).\n\nThis is sensible, as long as this repacking is done only with\nlocally available data, without dynamically pulling in lazy objects\nfrom the promisor (which would defeat the whole point ;-)).\nPresumably, this rebuilding is done without an extra traversal,\ndriven instead by the list of enumerated promisor objects we\nconstructed above (excluding the unwanted ones)?\n\n>   * --dry-run lists the candidates and changes nothing.\n\nI wonder whether size is the only criterion we would want to use\nwhen choosing what to discard among objects we know the promisor can\ngive us on-demand.  It is, of course, perfectly fine to make it the\nonly condition in this first effort, but it would help to imagine\nwhat other criteria we might want in the future and how they would\nfit into the framework you establish with this series.  Ensuring\nthat the framework is easily extensible with a future set of rules\nwill keep us from painting ourselves into a corner.\n\n> Safety guards refuse to run while a merge, rebase, am, cherry-pick,\n> revert, or bisect is in progress, and refuse to drop a blob referenced\n> by the current index (it would only be lazily re-fetched by the next\n> worktree command). Both are skipped for bare repositories.\n\nI assume you do not mean a race where an operation wants to write a\nblob, finds that an identical one that came from the promisor remote\nalready exists locally, refrains from writing another copy, and the\ndrop-filtered operation removes the blob at the right moment.\nRather, you likely have in mind an operation that stops, gives\ncontrol back to the user, and, while the user ponders the situation,\nthe drop-filtered operation kicks in and removes the blobs involved\nin the operation in progress.  Am I reading you correctly?\n\nEven in either of these situations, I do not quite see why the\nsafeguards are necessary.  The operation completes, or stays stopped\nin the middle.  The user's next move (whether they issue a new\ncommand after completion or resume the interrupted operation) will\nautomatically lazy-refetch what the drop-filtered operation\ndiscarded as needed, will it not?\n"},{"id":"549409","messageId":"CAGWgyh8EPSufBZrk0xCqTr4gz6MtJHkfCy6JQKxCqKSPZ3gEgw@mail.gmail.com","threadId":"66019","inReplyTo":"xmqqcxw3dvh5.fsf@gitster.g","subject":"Re: [GSoC PATCH v2 0/7] repack: add --drop-filtered to reclaim space in partial clones","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-01T18:19:11Z","receivedAt":"2026-08-01T18:19:48Z","isPatch":true,"body":"Hi,\nthanks for the review Junio!\n\nOn Fri, 31 Jul 2026 at 21:04, Junio C Hamano <gitster@pobox.com> wrote:\n> By 'by construction', do you mean 'It is guaranteed recoverable, as\n> long as ODB_FOR_EACH_OBJECT_PROMISOR_ONLY is working correctly'?\n\nyes, that is what I meant. the object is recoverable because it came from a\npromisor pack (due to a .promisor file), so the remote has promised to\ngive it back.\n\"by construction\" means \"recoverable as long as the promisor-only walk correctly\npicks out promisor objects\".\n\n> Since I do not use it, I do not personally trust promisor-based\n> traversal all that much, and it would be great if we could hear from\n> other practitioners that this really works well.\n\ni'll also be glad to hear from people who actually use partial clone\nabout whether\nleaning on the promisor-only walk here is a good idea for now, until the\nremote-object-info side of the cat-file protocol lands, which would let us\nverify against the remote directly.\n\n> This is sensible, as long as this repacking is done only with\n> locally available data, without dynamically pulling in lazy objects\n> from the promisor (which would defeat the whole point ;-)).\n\nright, i made sure of that :)\nenumeration passes OBJECT_INFO_SKIP_FETCH_OBJECT on\nevery object-info lookup, so it never triggers a lazy fetch.\nThe rebuild is local too: it only repacks promisor objects that are\nalready present.\nI confirmed this by tracing a real drop and by moving the promisor remote away\nentirely before a drop, it still completed, so it clearly did not need\nthe remote\n\n> Presumably, this rebuilding is done without an extra traversal,\n> driven instead by the list of enumerated promisor objects we\n> constructed above (excluding the unwanted ones)?\n\nnot quite, there are two walks right now. First,\nenumerate_promisor_blobs() walks the\npromisor objects to figure out what to drop. Then\nrepack_promisor_objects() does its own\npromisor-only walk to rebuild the pack, skipping anything in that drop set.\nso the rebuild does use the drop set, but through a second walk, not by directly\nreusing the first list.\n\n> I wonder whether size is the only criterion we would want to use\n> when choosing what to discard among objects we know the promisor can\n> give us on-demand.  It is, of course, perfectly fine to make it the\n> only condition in this first effort, but it would help to imagine\n> what other criteria we might want in the future and how they would\n> fit into the framework you establish with this series.  Ensuring\n> that the framework is easily extensible with a future set of rules\n> will keep us from painting ourselves into a corner.\n\ntrue, i agree..\nsize (blob:limit) is the only rule for now, but its easy to imagine others:\nhow old an object is or when it was last used, its path, its type, or whether\nits still reachable from the current branch. The design should handle\nthose without\nmuch trouble. Enumeration builds a set of promisor objects, and then one step\nnarrows that set down to what we actually drop.\nRight now that step is just the blob:limit filter. A new rule would\nplug in at the same spot,\nnarrowing the same set, so the overall \"list them, then pick what to drop\" shape\nwould not change\n\n> I assume you do not mean a race where an operation wants to write a\n> blob, finds that an identical one that came from the promisor remote\n> already exists locally, refrains from writing another copy, and the\n> drop-filtered operation removes the blob at the right moment.\n> Rather, you likely have in mind an operation that stops, gives\n> control back to the user, and, while the user ponders the situation,\n> the drop-filtered operation kicks in and removes the blobs involved\n> in the operation in progress.  Am I reading you correctly?\n\num yes, the case i had in mind is the second one: an operation stops halfway,\nhands control back to the user, and drop-filtered runs in that gap and\nremoves blobs the paused operation was using\n\n> Even in either of these situations, I do not quite see why the\n> safeguards are necessary.  The operation completes, or stays stopped\n> in the middle.  The user's next move (whether they issue a new\n> command after completion or resume the interrupted operation) will\n> automatically lazy-refetch what the drop-filtered operation\n> discarded as needed, will it not?\n\nyes, you got that right. Since the objects are promised, whatever gets dropped\nwill just be  lazy-refetched when the user runs the next command or resumes the\noperation.\nthe guards avoid immediately re-downloading something we just dropped,\n(which we can call as some wasted work : )), and a network fetch in\nthe middle of,\nsay, resolving a merge.\nThe index guard is the same story- the blob it protects would just be\nre-fetched by\nthe next command anyway.\nSo they are a convenience to avoid pointless re-fetching, not a\ncorrectness measure.\nI am happy to drop them or keep them clearly documented as just that,\nwhichever the list prefers.\n\nThanks,\nSiddharth Shrimali\n"},{"id":"549425","messageId":"xmqqa4r55kpl.fsf@gitster.g","threadId":"66019","inReplyTo":"CAGWgyh8EPSufBZrk0xCqTr4gz6MtJHkfCy6JQKxCqKSPZ3gEgw@mail.gmail.com","subject":"Re: [GSoC PATCH v2 0/7] repack: add --drop-filtered to reclaim space in partial clones","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-02T02:18:14Z","receivedAt":"2026-08-02T02:18:17Z","isPatch":true,"body":"Siddharth Shrimali <r.siddharth.shrimali@gmail.com> writes:\n\n> So they are a convenience to avoid pointless re-fetching, not a\n> correctness measure.\n> I am happy to drop them or keep them clearly documented as just that,\n> whichever the list prefers.\n\nDoesn't it suggest that the \"cull anything refetchable\" feature can\ngain a bit more smart?  Given an object you know you fetched from a\npromisor remote, are there cheap ways to determine how long you had\nit in your repository?  \"This large blob can be refetched if we\nwanted to, but we downloaded it just 20 minutes ago, so let's not\ncull it just yet\", or something like that, perhaps?\n"},{"id":"549435","messageId":"CAGWgyh91Bh-YOs9WVdbq7cEVkM0guu255GmXqJy2PhydQpNetg@mail.gmail.com","threadId":"66019","inReplyTo":"xmqqa4r55kpl.fsf@gitster.g","subject":"Re: [GSoC PATCH v2 0/7] repack: add --drop-filtered to reclaim space in partial clones","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-02T11:28:37Z","receivedAt":"2026-08-02T11:29:14Z","isPatch":true,"body":"On Sun, 2 Aug 2026 at 07:48, Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Siddharth Shrimali <r.siddharth.shrimali@gmail.com> writes:\n> Doesn't it suggest that the \"cull anything refetchable\" feature can\n> gain a bit more smart?  Given an object you know you fetched from a\n> promisor remote, are there cheap ways to determine how long you had\n> it in your repository?\n\nagreed,\nI like this a lot, recency is what the index guard was reaching for: dont\ndrop something you are likely to want again right away.\n\nthe tricky part is that droppable objects are always in packs (a lazy fetch\nproduces a promisor pack, never a loose object, in every config i\ntried), and packed objects dont carry a per-object timestamp on their\nown.\nThe cheap signal available is the promisor packs own mtime: since\neach lazy fetch writes its own pack, early on that mtime is a decent\nproxy for \"when did this object arrive\". The catch is that once a repack\nconsolidates packs, that per-fetch granularity is lost and you only know\nthe age of the combined pack.\n\nGit does already track per-object mtimes for cruft packs (via the\n.mtimes file used for --cruft-expiration), so there is precedent for\nage-based culling. whether something similar is worth doing for promisor\nobjects, so age survives repacking, would be a larger discussion\n\neither way a \"dont cull objects younger than <time>\" rule fits the same\nenumerate-then-select framework as just another predicate narrowing the\ncandidate set, so I'll note it as a promising follow-up criterion\n\nThanks,\nSiddharth Shrimali\n\n> \"This large blob can be refetched if we\n> wanted to, but we downloaded it just 20 minutes ago, so let's not\n> cull it just yet\", or something like that, perhaps?\n"},{"id":"549620","messageId":"81939944-9d8a-4bcd-a6ba-f3a7728b33bd@gmail.com","threadId":"66019","inReplyTo":"20260730174153.9949-1-r.siddharth.shrimali@gmail.com","subject":"Re: [GSoC PATCH v2 0/7] repack: add --drop-filtered to reclaim space in partial clones","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-08-04T21:12:03Z","receivedAt":"2026-08-04T21:12:10Z","isPatch":true,"body":"\n\nOn 30/07/26 23:11, Siddharth Shrimali wrote:\n> This is v2 of the series adding \"git repack --drop-filtered\" to reclaim\n> disk space in partial clones by dropping large, locally-held promisor\n> blobs that remain recoverable from the promisor remote. v1 was sent as\n> an RFC [1].\n> \n> Partial clones let you work with large repositories without downloading\n> every blob up front. Mising blobs are lazily fetched from the promisor\n> remote on demand. Over time these accumulate locally and there is\n> currently no safe, built-in way to reclaim that space short of\n> re-cloning. This series adds that reverse direction: enumerate promisor\n> blobs over a size threshold, drop them locally, and rely on the existing\n> lazy-fetch machinery to bring them back transparently when needed.\n> \n> How it works:\n>    * Enumerate promisor objects directly (ODB_FOR_EACH_OBJECT_PROMISOR_ONLY)\n>      and select the blobs exceeding the filter threshold. Every enumerated\n>      object is a promisor object by construction, so it is guaranteed\n>      recoverable and locally-created objects are never candidates.\n\n\n\nThanks for v2. Guards, docs, and dropping the drop-log all match what we \nhave talked about on the RFC.\n\nOn \"guaranteed\": I would soften that a bit in next round. It is \nrecoverable in the same sense as the rest of partial clone, as long as \nthe promisor remote still has it. Fine for now, just a bit strong\nwithout a remote check.\n\nFor the promisor-only walk: that matches how we already treat those\nobjects, so using it here looks right to me.\n\nOn the guards you already covered Junio's point well. I still like the\nindex one so we do not drop something and fetch it straight back. Mid-op\nis more UX. Docs/cover can just say that clearly.\n\nThanks.\nSiddharth\n\n\n>    * Rebuild the promisor pack without the selected blobs, reusing the\n>      existing repack machinery, so the drop is crash-safe (write, fsync,\n>      install, then delete the old pack).\n>    * --dry-run lists the candidates and changes nothing.\n> \n> Safety guards refuse to run while a merge, rebase, am, cherry-pick,\n> revert, or bisect is in progress, and refuse to drop a blob referenced\n> by the current index (it would only be lazily re-fetched by the next\n> worktree command). Both are skipped for bare repositories.\n> \n> Changes since v1:\n>    * distinguish an explicit -b/--write-bitmap-index on the command line\n>      (reported as a conflict) from a repack.writeBitmaps config value\n>      (silently disabled for the command). This addresses Junio's review\n>      that the previous check could not tell the two apart\n>    * documented the choice to keep --dry-run as a separate option rather\n>      than --drop-filtered=<mode>\n>    * implemented the safety guards\n>    * Added git-repack documentation for --drop-filtered and --dry-run\n>    * Reorganised so enumerate_promisor_blobs() is introduced in its final\n>      signature\n>    * Distributed the tests into the commits that introduce each behavior,\n>      instead of a single standalone test commit.\n>    * Dropped the drop-log commit from this series\n> \n> To do:\n>    * Remote verification: verifying against the remote awaits the \"remote-object-info\"\n>      cat-file protocol command.\n>    * Drop log: introduce with the error-path change that reads it.\n>    * --verbose: space-reclaimed reporting.\n> \n> [1] https://lore.kernel.org/git/20260716132848.95982-1-r.siddharth.shrimali@gmail.com/\n> \n> Siddharth Shrimali (7):\n>    builtin/repack.c: add --drop-filtered and --dry-run options\n>    list-objects-filter: add list_objects_filter__filter_oidset()\n>    repack-promisor: allow excluding objects from the rebuilt promisor\n>      pack\n>    builtin/repack: enumerate promisor blobs for --drop-filtered\n>    builtin/repack: actually drop filtered promisor blobs\n>    builtin/repack: add safety guards for --drop-filtered\n>    Documentation/git-repack: document --drop-filtered and --dry-run\n> \n>   Documentation/git-repack.adoc   |  35 +++++++\n>   builtin/repack.c                | 135 +++++++++++++++++++++++-\n>   list-objects-filter.c           |  45 ++++++++\n>   list-objects-filter.h           |  16 +++\n>   repack-filtered.c               |  81 +++++++++++++++\n>   repack-promisor.c               |  15 ++-\n>   repack.h                        |   8 +-\n>   t/meson.build                   |   1 +\n>   t/t7706-repack-drop-filtered.sh | 179 ++++++++++++++++++++++++++++++++\n>   9 files changed, 511 insertions(+), 4 deletions(-)\n>   create mode 100755 t/t7706-repack-drop-filtered.sh\n> \n\n"},{"id":"549622","messageId":"16614424-7b1a-48b5-9a85-bc90cbc4c6ab@gmail.com","threadId":"66019","inReplyTo":"20260730174153.9949-2-r.siddharth.shrimali@gmail.com","subject":"Re: [GSoC PATCH v2 1/7] builtin/repack.c: add --drop-filtered and --dry-run options","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-08-04T21:13:07Z","receivedAt":"2026-08-04T21:13:15Z","isPatch":true,"body":"\n\nOn 30/07/26 23:11, Siddharth Shrimali wrote:\n> Add two new command-line options to 'git-repack':\n> \n>    --drop-filtered: intended to eventually delete objects that match\n>                     the filter specification. Requires --filter and -a,\n>                     and is incompatible with --filter-to.\n>    --dry-run: show which objects would be dropped without making any\n>               changes. Only meaningful with --drop-filtered.\n> \n> Keep --dry-run as a separate option rather than folding it into\n> --drop-filtered (e.g --drop-filtered=dry-run), to stay consistent with\n> the --dry-run option other Git commands already provide and to leave\n> room for it to describe other repack behavior later. A\n> --drop-filtered=<mode> form can still be added later if more\n> drop-specific modes are needed.\n> \n> --drop-filtered also requires a promisor remote to be configured, since\n> dropping objects without a remote to fetch them back from would be\n> permanent data loss.\n> \n> --drop-filtered is incompatible with bitmap writing: filtering breaks\n> the \"all objects in one pack\" closure that bitmaps require. Snapshot\n> the bitmap setting after config but before option parsing so an\n> explicit -b/--write-bitmap-index on the command line can be told apart\n> from a repack.writeBitmaps configuration value. An explicit -b is\n> reported as a conflict, while a config-provided default is silently\n> disabled for the duration of the command.\n> \n> These options currently only perform validation. The actual enumeration\n> and deletion will be added in follow-up commits.\n> \n> Mentored-by: Christian Couder <christian.couder@gmail.com>\n> Mentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n> Signed-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n> ---\n>   builtin/repack.c                | 63 +++++++++++++++++++++++++++++++++\n>   t/meson.build                   |  1 +\n>   t/t7706-repack-drop-filtered.sh | 49 +++++++++++++++++++++++++\n>   3 files changed, 113 insertions(+)\n>   create mode 100755 t/t7706-repack-drop-filtered.sh\n> \n> diff --git a/builtin/repack.c b/builtin/repack.c\n> index db504d673f..322b01cb3e 100644\n> --- a/builtin/repack.c\n> +++ b/builtin/repack.c\n> @@ -14,6 +14,7 @@\n>   #include \"promisor-remote.h\"\n>   #include \"repack.h\"\n>   #include \"shallow.h\"\n> +#include \"list-objects-filter-options.h\"\n>   \n>   #define ALL_INTO_ONE 1\n>   #define LOOSEN_UNREACHABLE 2\n> @@ -28,6 +29,8 @@ static int use_delta_islands;\n>   static int run_update_server_info = 1;\n>   static char *packdir, *packtmp_name, *packtmp;\n>   static int midx_must_contain_cruft = 1;\n> +static int drop_filtered;\n> +static int dry_run;\n>   \n>   static const char *const git_repack_usage[] = {\n>   \tN_(\"git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\\n\"\n> @@ -148,6 +151,7 @@ int cmd_repack(int argc,\n>   \t/* variables to be filled by option parsing */\n>   \tstruct repack_config_ctx config_ctx;\n>   \tint delete_redundant = 0;\n> +\tint write_bitmaps_before_parse;\n>   \tconst char *unpack_unreachable = NULL;\n>   \tint keep_unreachable = 0;\n>   \tstruct string_list keep_pack_list = STRING_LIST_INIT_NODUP;\n> @@ -231,6 +235,10 @@ int cmd_repack(int argc,\n>   \t\t\t   N_(\"pack prefix to store a pack containing pruned objects\")),\n>   \t\tOPT_STRING(0, \"filter-to\", &filter_to, N_(\"dir\"),\n>   \t\t\t   N_(\"pack prefix to store a pack containing filtered out objects\")),\n> +\t\tOPT_BOOL(0, \"drop-filtered\", &drop_filtered,\n> +\t\t\t\tN_(\"delete filtered out objects (requires --filter)\")),\n> +\t\tOPT_BOOL(0, \"dry-run\", &dry_run,\n> +\t\t\t\tN_(\"only show which objects would be dropped\")),\n>   \t\tOPT_END()\n>   \t};\n>   \n> @@ -244,6 +252,13 @@ int cmd_repack(int argc,\n>   \n>   \trepo_config(repo, repack_config, &config_ctx);\n>   \n> +\t/*\n> +\t * update the bitmap setting after config but before command line\n> +\t * parsing, so we can later tell whether -b/--write-bitmap-index was\n> +\t * given explicitly on the command line or not\n> +\t */\n> +\twrite_bitmaps_before_parse = write_bitmaps;\n> +\n>   \targc = parse_options(argc, argv, prefix, builtin_repack_options,\n>   \t\t\t\tgit_repack_usage, 0);\n>   \n> @@ -252,6 +267,54 @@ int cmd_repack(int argc,\n>   \tpo_args.depth = xstrdup_or_null(opt_depth);\n>   \tpo_args.threads = xstrdup_or_null(opt_threads);\n>   \n> +\tdie_for_incompatible_opt2(drop_filtered, \"--drop-filtered\",\n> +\t\t!!filter_to, \"--filter-to\");\n> +\n> +\tdie_for_incompatible_opt2(drop_filtered, \"--drop-filtered\",\n> +\t\twrite_bitmaps > 0, \"--write-bitmap-index\");\n> +\n> +\tif (dry_run && !drop_filtered)\n> +\t\tdie(_(\"--dry-run only takes effect with --drop-filtered\"));\n> +\n> +\tif (drop_filtered) {\n> +\t\tint bitmaps_from_cmdline = (write_bitmaps != write_bitmaps_before_parse);\n> +\n> +\t\tif (!dry_run)\n> +\t\t\tdie(_(\"--drop-filtered doesn't work without --dry-run yet\"));\n> +\n> +\t\tif (!po_args.filter_options.choice)\n> +\t\t\tdie(_(\"--drop-filtered requires --filter\"));\n> +\n> +\t\tif (!(pack_everything & ALL_INTO_ONE))\n> +\t\t\tdie(_(\"--drop-filtered requires -a\"));\n> +\n> +\t\t/*\n> +\t\t * Only blob:limit=<n> is supported for now. Reject other\n> +\t\t * filter choices early, before walking the object database.\n> +\t\t */\n> +\t\tif (po_args.filter_options.choice != LOFC_BLOB_LIMIT)\n> +\t\t\tdie(_(\"--drop-filtered only supports --filter=blob:limit=<n> for now\"));\n> +\n> +\t\t/*\n> +\t\t * an explicit -b on the command line is a conflict we have to\n> +\t\t * report, a bitmap setting from config is silently overridden\n> +\t\t * for the duration of the command\n> +\t\t */\n> +\t\tif (bitmaps_from_cmdline && write_bitmaps > 0)\n> +\t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n> +\t\t\t\t\"--drop-filtered\", \"--write-bitmap-index\");\n\n\nThanks for tackling the bitmap CLI vs config split.\n\nOne case still looks off: if repack.writeBitmaps is already true and\nthe user also passes -b, write_bitmaps is 1 before and after parse, so\nbitmaps_from_cmdline stays false and we never error. I think explicit\n-b should still be rejected there.\n\nThanks.\nSiddharth\n\n\n\n> +\n> +\t\t/*\n> +\t\t * Without a promisor remote there is nowhere to re-fetch the\n> +\t\t * dropped objects from, so dropping them would be permanent\n> +\t\t * data loss.\n> +\t\t */\n> +\t\tif (!repo_has_promisor_remote(repo))\n> +\t\t\tdie(_(\"--drop-filtered requires a promisor remote\"));\n> +\n> +\t\twrite_bitmaps = 0;\n> +\t}\n> +\n>   \tif (delete_redundant && repo->repository_format_precious_objects)\n>   \t\tdie(_(\"cannot delete packs in a precious-objects repo\"));\n>   \n> diff --git a/t/meson.build b/t/meson.build\n> index d8161c368b..c2bf60d129 100644\n> --- a/t/meson.build\n> +++ b/t/meson.build\n> @@ -963,6 +963,7 @@ integration_tests = [\n>     't7703-repack-geometric.sh',\n>     't7704-repack-cruft.sh',\n>     't7705-repack-incremental-midx.sh',\n> +  't7706-repack-drop-filtered.sh',\n>     't7800-difftool.sh',\n>     't7810-grep.sh',\n>     't7811-grep-open.sh',\n> diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh\n> new file mode 100755\n> index 0000000000..65be756e33\n> --- /dev/null\n> +++ b/t/t7706-repack-drop-filtered.sh\n> @@ -0,0 +1,49 @@\n> +#!/bin/sh\n> +\n> +test_description='git repack --drop-filtered option validation'\n> +\n> +. ./test-lib.sh\n> +\n> +# checks for options validations before any promisor walk\n> +test_expect_success 'setup plain repo for validation' '\n> +\tgit init plain &&\n> +\ttest_commit -C plain initial &&\n> +\tgit clone --bare plain plain.git &&\n> +\tgit -C plain.git repack -a -d\n> +'\n> +\n> +test_expect_success '--drop-filtered requires --filter' '\n> +\ttest_must_fail git -C plain.git repack --drop-filtered --dry-run -a 2>err &&\n> +\ttest_grep \"drop-filtered requires --filter\" err\n> +'\n> +\n> +test_expect_success '--drop-filtered cannot be used with --filter-to' '\n> +\ttest_must_fail git -C plain.git repack --drop-filtered \\\n> +\t\t--filter=blob:limit=1k --filter-to=./filter-out 2>err &&\n> +\ttest_grep \"options .--drop-filtered. and .--filter-to. cannot be used together\" err\n> +'\n> +\n> +test_expect_success '--dry-run only takes effect with --drop-filtered' '\n> +\ttest_must_fail git -C plain.git repack --dry-run 2>err &&\n> +\ttest_grep \"dry-run only takes effect with --drop-filtered\" err\n> +'\n> +\n> +test_expect_success '--drop-filtered requires -a' '\n> +\ttest_must_fail git -C plain.git repack --drop-filtered \\\n> +\t\t--filter=blob:limit=1k --dry-run 2>err &&\n> +\ttest_grep \"drop-filtered requires -a\" err\n> +'\n> +\n> +test_expect_success '--drop-filtered fails with --write-bitmap-index' '\n> +\ttest_must_fail git -C plain.git repack --drop-filtered \\\n> +\t\t--filter=blob:limit=1k --dry-run -a -b 2>err &&\n> +\ttest_grep \"options .--drop-filtered. and .--write-bitmap-index. cannot be used together\" err\n> +'\n> +\n> +test_expect_success '--drop-filtered fails without a promisor remote' '\n> +\ttest_must_fail git -C plain.git repack --drop-filtered \\\n> +\t\t--filter=blob:limit=1k --dry-run -a 2>err &&\n> +\ttest_grep \"drop-filtered requires a promisor remote\" err\n> +'\n> +\n> +test_done\n\n"},{"id":"549623","messageId":"a81fc1be-914e-4045-87a0-cee88257fad5@gmail.com","threadId":"66019","inReplyTo":"20260730174153.9949-7-r.siddharth.shrimali@gmail.com","subject":"Re: [GSoC PATCH v2 6/7] builtin/repack: add safety guards for --drop-filtered","fromName":"Siddharth Asthana","fromEmail":"siddharthasthana31@gmail.com","sentAt":"2026-08-04T21:13:47Z","receivedAt":"2026-08-04T21:13:54Z","isPatch":true,"body":"\n\nOn 30/07/26 23:11, Siddharth Shrimali wrote:\n> --drop-filtered removes local promisor blobs. That is only safe when the\n> repository is not mid-operation and when the blobs are not actively in\n> use, so add two guards, both skipped for bare repositories which have\n> neither a worktree nor an index.\n> \n> First, refuse to run while a merge, rebase, am, cherry-pick, revert, or\n> bisect is in progress. During these operations the working tree and\n> index are in an intermediate state, and rewriting packs and deleting\n> objects underneath a half-finished operation is unsafe.\n> \n> Second, refuse to drop a blob that the current index references. Such a\n\n\n\nIndex guard looks good to me. Same idea as on the RFC.\n\nThanks.\nSiddharth\n\n\n> blob is needed by the working tree, so dropping it would only cause the\n> next command that touches the worktree to lazy-fetch it straight back,\n> reclaiming nothing. The offending path is reported so the user can see\n> why the drop was refused.\n> \n> Mentored-by: Christian Couder <christian.couder@gmail.com>\n> Mentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\n> Signed-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n> ---\n>   builtin/repack.c                | 47 +++++++++++++++++++++++++++++++++\n>   t/t7706-repack-drop-filtered.sh | 36 +++++++++++++++++++++++++\n>   2 files changed, 83 insertions(+)\n> \n> diff --git a/builtin/repack.c b/builtin/repack.c\n> index 9a15ab1f2a..2339bcaac4 100644\n> --- a/builtin/repack.c\n> +++ b/builtin/repack.c\n> @@ -17,6 +17,8 @@\n>   #include \"list-objects-filter-options.h\"\n>   #include \"oidset.h\"\n>   #include \"hex.h\"\n> +#include \"wt-status.h\"\n> +#include \"read-cache-ll.h\"\n>   \n>   #define ALL_INTO_ONE 1\n>   #define LOOSEN_UNREACHABLE 2\n> @@ -309,6 +311,28 @@ int cmd_repack(int argc,\n>   \t\tif (!repo_has_promisor_remote(repo))\n>   \t\t\tdie(_(\"--drop-filtered requires a promisor remote\"));\n>   \n> +\t\t/*\n> +\t\t * refuse to drop objects while another operation is in\n> +\t\t * progress. the working tree and index are in an\n> +\t\t * intermediate state, and rewriting packs in a half-finished\n> +\t\t * merge/rebase/cherry-pick/revert/bisect is unsafe\n> +\t\t * bare repositories have no such state, so the check\n> +\t\t * is skipped there\n> +\t\t */\n> +\t\tif (!is_bare_repository(repo)) {\n> +\t\t\tstruct wt_status_state state = { 0 };\n> +\n> +\t\t\twt_status_get_state(repo, &state, 0);\n> +\t\t\tif (state.merge_in_progress || state.revert_in_progress ||\n> +\t\t\t    state.rebase_in_progress ||state.bisect_in_progress ||\n> +\t\t\t    state.cherry_pick_in_progress ||state.am_in_progress||\n> +\t\t\t    state.rebase_interactive_in_progress) {\n> +\t\t\t\twt_status_state_free_buffers(&state);\n> +\t\t\t\tdie(_(\"--drop-filtered cannot be used while another operation is in progress\"));\n> +\t\t\t}\n> +\t\t\twt_status_state_free_buffers(&state);\n> +\t\t}\n> +\n>   \t\twrite_bitmaps = 0;\n>   \n>   \t\t/*\n> @@ -324,6 +348,29 @@ int cmd_repack(int argc,\n>   \t\tif (ret)\n>   \t\t\tgoto cleanup;\n>   \n> +\t\t/*\n> +\t\t * refuse to drop blobs that the current index references.\n> +\t\t * dropping such a blob would cause the very next command\n> +\t\t * that touches the worktree to lazy-fetch it straight back, so\n> +\t\t * the drop would reclaim nothing. bare repositories have no\n> +\t\t * index, so the check is skipped there.\n> +\t\t */\n> +\t\tif (!is_bare_repository(repo) && oidset_size(&drop_oids)) {\n> +\t\t\tstruct index_state *istate = repo->index;\n> +\t\t\tunsigned int i;\n> +\n> +\t\t\tif (repo_read_index(repo) < 0)\n> +\t\t\t\tdie(_(\"could not read the index\"));\n> +\n> +\t\t\tfor (i = 0; i < istate->cache_nr; i++) {\n> +\t\t\t\tconst struct cache_entry *ce = istate->cache[i];\n> +\n> +\t\t\t\tif (oidset_contains(&drop_oids, &ce->oid))\n> +\t\t\t\t\tdie(_(\"cannot drop '%s' (%s): it is referenced by the current index\"),\n> +\t\t\t\t\t\tce->name, oid_to_hex(&ce->oid));\n> +\t\t\t}\n> +\t\t}\n> +\n>   \t\tif (dry_run) {\n>   \t\t\tstruct oidset_iter iter;\n>   \t\t\tconst struct object_id *oid;\n> diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh\n> index b3e493e851..dabed97541 100755\n> --- a/t/t7706-repack-drop-filtered.sh\n> +++ b/t/t7706-repack-drop-filtered.sh\n> @@ -140,4 +140,40 @@ test_expect_success '--drop-filtered removes the promisor blob locally' '\n>   \tgrep -q \"$SMALL\" present\n>   '\n>   \n> +test_expect_success '--drop-filtered refuses when a merge is in progress' '\n> +\ttest_when_finished \"git -C repo merge --abort || :\" &&\n> +\n> +\t# creat a conflicting merge so wt_status reports it\n> +\tgit -C repo checkout -B mergebase base &&\n> +\techo one >repo/conflict.txt &&\n> +\tgit -C repo add conflict.txt &&\n> +\tgit -C repo commit -m one &&\n> +\n> +\tgit -C repo checkout -B mergeother base &&\n> +\techo two >repo/conflict.txt &&\n> +\tgit -C repo add conflict.txt &&\n> +\tgit -C repo commit -m two &&\n> +\n> +\ttest_must_fail git -C repo merge mergebase &&\n> +\n> +\ttest_must_fail git -C repo -c repack.writeBitmaps=false \\\n> +\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err &&\n> +\ttest_grep \"in progress\" err\n> +'\n> +\n> +\n> +test_expect_success '--drop-filtered refuses to drop an index-referenced blob' '\n> +\t# create a large blob, add it to the index and make it a promisor object\n> +\t# so the index references it and enumeration picks it up\n> +\ttest-tool genrandom idx 4096 >repo/tracked-big.bin &&\n> +\tgit -C repo add tracked-big.bin &&\n> +\tOID=$(git -C repo rev-parse :tracked-big.bin) &&\n> +\tprintf \"%s\\n\" \"$OID\" | pack_as_from_promisor >/dev/null &&\n> +\tdelete_object repo \"$OID\" &&\n> +\n> +\ttest_must_fail git -C repo -c repack.writeBitmaps=false \\\n> +\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err &&\n> +\ttest_grep \"referenced by the current index\" err\n> +'\n> +\n>   test_done\n\n"},{"id":"549838","messageId":"20260806112202.75067-1-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260730174153.9949-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v3 0/7] repack: add --drop-filtered to reclaim space in partial clones","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-06T11:21:55Z","receivedAt":"2026-08-06T11:22:17Z","isPatch":true,"body":"This is v3 of the series adding \"git repack --drop-filtered\" to reclaim\ndisk space in partial clones by dropping large, locally-held promisor\nblobs that remain recoverable from the promisor remote. v2 was at [1].\n\nPartial clones let you work with large repositories without downloading\nevery blob up front. Missing blobs are lazily fetched from the promisor\nremote on demand. over time these accumulate locally and there is\ncurrently no safe, built-in way to reclaim that space short of\nre-cloning. This series adds that reverse direction: enumerate promisor\nblobs over a size threshold, drop them locally, and rely on the existing\nlazy-fetch machinery to bring them back transparently when needed.\n\nHow it works:\n  * enumerate promisor objects directly (ODB_FOR_EACH_OBJECT_PROMISOR_ONLY)\n    and select the blobs exceeding the filter threshold. Every enumerated\n    object is a promisor object, so it is recoverable from the promisor\n    remote as long as the remote still has it, the same assumption the\n    rest of partial clone relies on\n  * rebuild the promisor pack without the selected blobs, reusing the\n    existing repack machinery, so the drop is crash-safe (write, fsync,\n    install, then delete the old pack)\n  * --dry-run lists the candidates and changes nothing\n\nthe guards refuse to run while a merge, rebase, am, cherry-pick, revert,\nor bisect is in progress, and refuse to drop a blob referenced by the\ncurrent index (it would only be lazily re-fetched by the next worktree\ncommand). Dropped objects stay recoverable via lazy fetch, so these are\na convenience (avoid pointless re-fetch and a surprising mid-operation\nfetch) rather than a correctness measure. Both are skipped for bare\nrepositories\n\nChanges since v2:\n  * bitmap config: detect an explicit -b/--write-bitmap-index\n    with an option callback instead of a before/after snapshot, so an\n    explicit -b is always reported as a conflict, even when\n    repack.writeBitmaps is already true in config\n  * softened \"guaranteed recoverable\" to \"recoverable as long as the\n    remote still has it\"\n  * reframed the guards in the commit message and docs as a convenience\n    rather than a safety measure\n\nTo do:\n  * Remote verification: verifying against the remote awaits the \"remote-object-info\"\n    cat-file protocol command\n  * Recency: a \"don't cull recently-fetched objects\" rule as another\n    selection criterion alongside size\n  * Drop log: introduce with the error-path change that reads it.\n\n[1] https://lore.kernel.org/git/20260730174153.9949-1-r.siddharth.shrimali@gmail.com/\n\nSiddharth Shrimali (7):\n  builtin/repack.c: add --drop-filtered and --dry-run options\n  list-objects-filter: add list_objects_filter__filter_oidset()\n  repack-promisor: allow excluding objects from the rebuilt promisor\n    pack\n  builtin/repack: enumerate promisor blobs for --drop-filtered\n  builtin/repack: actually drop filtered promisor blobs\n  builtin/repack: add guards for --drop-filtered\n  Documentation/git-repack: document --drop-filtered and --dry-run\n\n Documentation/git-repack.adoc   |  37 +++++++\n builtin/repack.c                | 148 ++++++++++++++++++++++++-\n list-objects-filter.c           |  45 ++++++++\n list-objects-filter.h           |  16 +++\n repack-filtered.c               |  82 ++++++++++++++\n repack-promisor.c               |  15 ++-\n repack.h                        |   8 +-\n t/meson.build                   |   1 +\n t/t7706-repack-drop-filtered.sh | 185 ++++++++++++++++++++++++++++++++\n 9 files changed, 531 insertions(+), 6 deletions(-)\n create mode 100755 t/t7706-repack-drop-filtered.sh\n\n-- \n2.54.0\n\n"},{"id":"549839","messageId":"20260806112202.75067-2-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260806112202.75067-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v3 1/7] builtin/repack.c: add --drop-filtered and --dry-run options","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-06T11:21:56Z","receivedAt":"2026-08-06T11:22:21Z","isPatch":true,"body":"Add two new command-line options to 'git-repack':\n\n  --drop-filtered: intended to eventually delete objects that match\n                   the filter specification. Requires --filter and -a,\n                   and is incompatible with --filter-to.\n  --dry-run: show which objects would be dropped without making any\n             changes. Only meaningful with --drop-filtered.\n\nKeep --dry-run as a separate option rather than folding it into\n--drop-filtered (e.g --drop-filtered=dry-run), to stay consistent with\nthe --dry-run option other Git commands already provide and to leave\nroom for it to describe other repack behavior later. A\n--drop-filtered=<mode> form can still be added later if more\ndrop-specific modes are needed.\n\n--drop-filtered also requires a promisor remote to be configured, since\ndropping objects without a remote to fetch them back from would be\npermanent data loss.\n\n--drop-filtered is incompatible with bitmap writing: filtering breaks\nthe \"all objects in one pack\" closure that bitmaps require. Detect an\nexplicit -b/--write-bitmap-index on the command line with a dedicated\noption callback that sets a \"write_bitmaps_given\" flag, so it can be\ndistinguished from a repack.writeBitmaps configuration value even when\nconfig already enables bitmaps. An explicit -b is reported as a conflict,\nwhile a config-provided default is silently disabled for the duration\nof the command.\n\nThese options currently only perform validation. The actual enumeration\nand deletion will be added in follow-up commits.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n builtin/repack.c                | 71 ++++++++++++++++++++++++++++++++-\n t/meson.build                   |  1 +\n t/t7706-repack-drop-filtered.sh | 55 +++++++++++++++++++++++++\n 3 files changed, 125 insertions(+), 2 deletions(-)\n create mode 100755 t/t7706-repack-drop-filtered.sh\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex db504d673f..2e8b7ea45c 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -14,6 +14,7 @@\n #include \"promisor-remote.h\"\n #include \"repack.h\"\n #include \"shallow.h\"\n+#include \"list-objects-filter-options.h\"\n \n #define ALL_INTO_ONE 1\n #define LOOSEN_UNREACHABLE 2\n@@ -28,6 +29,9 @@ static int use_delta_islands;\n static int run_update_server_info = 1;\n static char *packdir, *packtmp_name, *packtmp;\n static int midx_must_contain_cruft = 1;\n+static int drop_filtered;\n+static int dry_run;\n+static int write_bitmaps_given;\n \n static const char *const git_repack_usage[] = {\n \tN_(\"git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\\n\"\n@@ -111,6 +115,21 @@ static int repack_config(const char *var, const char *value,\n \treturn git_default_config(var, value, ctx, cb);\n }\n \n+static int option_parse_write_bitmaps(const struct option *opt, const char *arg,\n+\t\t\t\t      int unset)\n+{\n+\tint *value = opt->value;\n+\n+\tBUG_ON_OPT_ARG(arg);\n+\tif (unset)\n+\t\t*value = 0;\n+\telse\n+\t\t*value = 1;\n+\n+\twrite_bitmaps_given = 1;\n+\treturn 0;\n+}\n+\n static int option_parse_write_midx(const struct option *opt, const char *arg,\n \t\t\t\t   int unset)\n {\n@@ -194,8 +213,9 @@ int cmd_repack(int argc,\n \t\tOPT__QUIET(&po_args.quiet, N_(\"be quiet\")),\n \t\tOPT_BOOL('l', \"local\", &po_args.local,\n \t\t\t\tN_(\"pass --local to git-pack-objects\")),\n-\t\tOPT_BOOL('b', \"write-bitmap-index\", &write_bitmaps,\n-\t\t\t\tN_(\"write bitmap index\")),\n+\t\tOPT_CALLBACK_F('b', \"write-bitmap-index\", &write_bitmaps, NULL,\n+\t\t\t        N_(\"write bitmap index\"),\n+\t\t\t       PARSE_OPT_NOARG, option_parse_write_bitmaps),\n \t\tOPT_BOOL('i', \"delta-islands\", &use_delta_islands,\n \t\t\t\tN_(\"pass --delta-islands to git-pack-objects\")),\n \t\tOPT_STRING(0, \"unpack-unreachable\", &unpack_unreachable, N_(\"approxidate\"),\n@@ -231,6 +251,10 @@ int cmd_repack(int argc,\n \t\t\t   N_(\"pack prefix to store a pack containing pruned objects\")),\n \t\tOPT_STRING(0, \"filter-to\", &filter_to, N_(\"dir\"),\n \t\t\t   N_(\"pack prefix to store a pack containing filtered out objects\")),\n+\t\tOPT_BOOL(0, \"drop-filtered\", &drop_filtered,\n+\t\t\t\tN_(\"delete filtered out objects (requires --filter)\")),\n+\t\tOPT_BOOL(0, \"dry-run\", &dry_run,\n+\t\t\t\tN_(\"only show which objects would be dropped\")),\n \t\tOPT_END()\n \t};\n \n@@ -252,6 +276,49 @@ int cmd_repack(int argc,\n \tpo_args.depth = xstrdup_or_null(opt_depth);\n \tpo_args.threads = xstrdup_or_null(opt_threads);\n \n+\tdie_for_incompatible_opt2(drop_filtered, \"--drop-filtered\",\n+\t\t!!filter_to, \"--filter-to\");\n+\n+\tif (dry_run && !drop_filtered)\n+\t\tdie(_(\"--dry-run only takes effect with --drop-filtered\"));\n+\n+\tif (drop_filtered) {\n+\t\tif (!dry_run)\n+\t\t\tdie(_(\"--drop-filtered doesn't work without --dry-run yet\"));\n+\n+\t\tif (!po_args.filter_options.choice)\n+\t\t\tdie(_(\"--drop-filtered requires --filter\"));\n+\n+\t\tif (!(pack_everything & ALL_INTO_ONE))\n+\t\t\tdie(_(\"--drop-filtered requires -a\"));\n+\n+\t\t/*\n+\t\t * Only blob:limit=<n> is supported for now. Reject other\n+\t\t * filter choices early, before walking the object database.\n+\t\t */\n+\t\tif (po_args.filter_options.choice != LOFC_BLOB_LIMIT)\n+\t\t\tdie(_(\"--drop-filtered only supports --filter=blob:limit=<n> for now\"));\n+\n+\t\t/*\n+\t\t * an explicit -b on the command line is a conflict we have to\n+\t\t * report, a bitmap setting from config is silently overridden\n+\t\t * for the duration of the command\n+\t\t */\n+\t\tif (write_bitmaps_given && write_bitmaps > 0)\n+\t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n+\t\t\t\t\"--drop-filtered\", \"--write-bitmap-index\");\n+\n+\t\t/*\n+\t\t * Without a promisor remote there is nowhere to re-fetch the\n+\t\t * dropped objects from, so dropping them would be permanent\n+\t\t * data loss.\n+\t\t */\n+\t\tif (!repo_has_promisor_remote(repo))\n+\t\t\tdie(_(\"--drop-filtered requires a promisor remote\"));\n+\n+\t\twrite_bitmaps = 0;\n+\t}\n+\n \tif (delete_redundant && repo->repository_format_precious_objects)\n \t\tdie(_(\"cannot delete packs in a precious-objects repo\"));\n \ndiff --git a/t/meson.build b/t/meson.build\nindex a25f37d2f5..92352e43c4 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -964,6 +964,7 @@ integration_tests = [\n   't7703-repack-geometric.sh',\n   't7704-repack-cruft.sh',\n   't7705-repack-incremental-midx.sh',\n+  't7706-repack-drop-filtered.sh',\n   't7800-difftool.sh',\n   't7810-grep.sh',\n   't7811-grep-open.sh',\ndiff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh\nnew file mode 100755\nindex 0000000000..f27b09a30e\n--- /dev/null\n+++ b/t/t7706-repack-drop-filtered.sh\n@@ -0,0 +1,55 @@\n+#!/bin/sh\n+\n+test_description='git repack --drop-filtered option validation'\n+\n+. ./test-lib.sh\n+\n+# checks for options validations before any promisor walk\n+test_expect_success 'setup plain repo for validation' '\n+\tgit init plain &&\n+\ttest_commit -C plain initial &&\n+\tgit clone --bare plain plain.git &&\n+\tgit -C plain.git repack -a -d\n+'\n+\n+test_expect_success '--drop-filtered requires --filter' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered --dry-run -a 2>err &&\n+\ttest_grep \"drop-filtered requires --filter\" err\n+'\n+\n+test_expect_success '--drop-filtered cannot be used with --filter-to' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered \\\n+\t\t--filter=blob:limit=1k --filter-to=./filter-out 2>err &&\n+\ttest_grep \"options .--drop-filtered. and .--filter-to. cannot be used together\" err\n+'\n+\n+test_expect_success '--dry-run only takes effect with --drop-filtered' '\n+\ttest_must_fail git -C plain.git repack --dry-run 2>err &&\n+\ttest_grep \"dry-run only takes effect with --drop-filtered\" err\n+'\n+\n+test_expect_success '--drop-filtered requires -a' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered \\\n+\t\t--filter=blob:limit=1k --dry-run 2>err &&\n+\ttest_grep \"drop-filtered requires -a\" err\n+'\n+\n+test_expect_success '--drop-filtered fails with --write-bitmap-index' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered \\\n+\t\t--filter=blob:limit=1k --dry-run -a -b 2>err &&\n+\ttest_grep \"options .--drop-filtered. and .--write-bitmap-index. cannot be used together\" err\n+'\n+\n+test_expect_success '--drop-filtered rejects explicit -b even when repack.writeBitmaps=true' '\n+\ttest_must_fail git -C plain.git -c repack.writeBitmaps=true \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a -b 2>err &&\n+\ttest_grep \"options .--drop-filtered. and .--write-bitmap-index. cannot be used together\" err\n+'\n+\n+test_expect_success '--drop-filtered fails without a promisor remote' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered \\\n+\t\t--filter=blob:limit=1k --dry-run -a 2>err &&\n+\ttest_grep \"drop-filtered requires a promisor remote\" err\n+'\n+\n+test_done\n-- \n2.54.0\n\n"},{"id":"549840","messageId":"20260806112202.75067-3-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260806112202.75067-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v3 2/7] list-objects-filter: add list_objects_filter__filter_oidset()","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-06T11:21:57Z","receivedAt":"2026-08-06T11:22:26Z","isPatch":true,"body":"The existing filter entry point, list_objects_filter__filter_object(),\nis built around the object-walk path: it expects traversal context and\nprovisional omit sets, and is meant to be called as objects are\nvisited during a walk. A caller that already has a set of OIDs in hand\nand only wants to know which ones a filter would select has no usable\nentry point into the filter API.\n\n--drop-filtered is exactly such a caller: it collects promisor blobs\ninto an oidset and needs to know which of them exceed the filter\nthreshold, without performing an object walk.\n\nAdd a helper, list_objects_filter__filter_oidset(), that takes a set\nof OIDs and populates an \"omitted\" set with those that would be\nfiltered out by the given filter options. Only blob:limit=N filters\nare supported for now.\n\nThis helper does not actually reuse the existing filter machinery.\nIt reimplements the blob:limit size check directly. That machinery\nis tied to the object-walk path and cannot easily be driven\nfrom a plain oidset. A NEEDSWORK comment marks this so the helper can\nlater be refactored to reuse the real filter logic instead of\nduplicating it.\n\nOBJECT_INFO_SKIP_FETCH_OBJECT is passed when reading object info so\nthe helper never triggers a lazy fetch.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n list-objects-filter.c | 45 +++++++++++++++++++++++++++++++++++++++++++\n list-objects-filter.h | 16 +++++++++++++++\n 2 files changed, 61 insertions(+)\n\ndiff --git a/list-objects-filter.c b/list-objects-filter.c\nindex c912ff3079..6a2e9d5b24 100644\n--- a/list-objects-filter.c\n+++ b/list-objects-filter.c\n@@ -828,3 +828,48 @@ void list_objects_filter__free(struct filter *filter)\n \tfilter->free_fn(filter->filter_data);\n \tfree(filter);\n }\n+\n+/*\n+ * NEEDSWORK: this reimplements the blob:limit size check rather than\n+ * reusing the existing filter machinery in\n+ * list_objects_filter__filter_object(). That machinery is currently\n+ * tied to the object-walk path and cannot easily be driven from a\n+ * plain oidset. It would be nice to refactor the filter code so this\n+ * helper can reuse it instead of duplicating the size check.\n+ */\n+int list_objects_filter__filter_oidset(struct repository *r,\n+\tstruct list_objects_filter_options *opts,\n+\tconst struct oidset *in,\n+\tstruct oidset *omitted)\n+{\n+\tstruct oidset_iter iter;\n+\tconst struct object_id *oid;\n+\n+\tif (opts->choice != LOFC_BLOB_LIMIT)\n+\t\treturn error(_(\"filter_oidset: only blob:limit filters are supported\"));\n+\n+\toidset_iter_init(in, &iter);\n+\twhile ((oid = oidset_iter_next(&iter))) {\n+\t\tstruct object_info info = OBJECT_INFO_INIT;\n+\t\tenum object_type type;\n+\t\tunsigned long size;\n+\n+\t\tinfo.typep = &type;\n+\t\tinfo.sizep = &size;\n+\n+\t\t/*\n+\t\t * Use OBJECT_INFO_SKIP_FETCH_OBJECT to avoid triggering\n+\t\t * a lazy fetch while inspecting candidates for removal.\n+\t\t */\n+\t\tif (odb_read_object_info_extended(r->objects, oid, &info,\n+\t\t\t\tOBJECT_INFO_SKIP_FETCH_OBJECT) < 0)\n+\t\t\tcontinue;\n+\n+\t\tif (type != OBJ_BLOB)\n+\t\t\tcontinue;\n+\n+\t\tif (size >= opts->blob_limit_value)\n+\t\t\toidset_insert(omitted, oid);\n+\t}\n+\treturn 0;\n+}\ndiff --git a/list-objects-filter.h b/list-objects-filter.h\nindex 9e98814111..56a2d87aa0 100644\n--- a/list-objects-filter.h\n+++ b/list-objects-filter.h\n@@ -94,4 +94,20 @@ enum list_objects_filter_result list_objects_filter__filter_object(\n  */\n void list_objects_filter__free(struct filter *filter);\n \n+/*\n+ * Given a set of OIDs in 'in', populate 'omitted' with those that\n+ * would be filtered by 'opts'. Currently only blob:limit=N is\n+ * supported. Objects that cannot be read are silently skipped.\n+ *\n+ * NEEDSWORK: this reimplements the blob:limit size check rather than\n+ * reusing the existing filter machinery. See the matching comment in\n+ * list-objects-filter.c.\n+ *\n+ * Return 0 on success, -1 if the filter is not supported.\n+ */\n+int list_objects_filter__filter_oidset(struct repository *r,\n+\tstruct list_objects_filter_options *opts,\n+\tconst struct oidset *in,\n+\tstruct oidset *omitted);\n+\n #endif /* LIST_OBJECTS_FILTER_H */\n-- \n2.54.0\n\n"},{"id":"549841","messageId":"20260806112202.75067-4-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260806112202.75067-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v3 3/7] repack-promisor: allow excluding objects from the rebuilt promisor pack","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-06T11:21:58Z","receivedAt":"2026-08-06T11:22:31Z","isPatch":true,"body":"Add a to_drop oidset parameter to repack_promisor_objects(). When it is\nnon-NULL, write_oid() omits those objects from the rebuilt promisor\npack. This is the mechanism --drop-filtered will use to remove promisor\nblobs, i.e. rebuild the promisor pack without them.\n\nAll existing callers pass NULL, so behavior is unchanged.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n builtin/repack.c  |  2 +-\n repack-promisor.c | 15 ++++++++++++++-\n repack.h          |  4 +++-\n 3 files changed, 18 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex 2e8b7ea45c..0a4dadb896 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -429,7 +429,7 @@ int cmd_repack(int argc,\n \t\tstrvec_push(&cmd.args, \"--delta-islands\");\n \n \tif (pack_everything & ALL_INTO_ONE) {\n-\t\trepack_promisor_objects(repo, &po_args, &names, packtmp);\n+\t\trepack_promisor_objects(repo, &po_args, &names, packtmp, NULL);\n \n \t\tif (existing_packs_has_non_kept(&existing) &&\n \t\t    delete_redundant &&\ndiff --git a/repack-promisor.c b/repack-promisor.c\nindex 90318ce150..fabfdc168a 100644\n--- a/repack-promisor.c\n+++ b/repack-promisor.c\n@@ -6,10 +6,12 @@\n #include \"path.h\"\n #include \"repository.h\"\n #include \"run-command.h\"\n+#include \"oidset.h\"\n \n struct write_oid_context {\n \tstruct child_process *cmd;\n \tconst struct git_hash_algo *algop;\n+\tconst struct oidset *to_drop;\n };\n \n /*\n@@ -23,6 +25,15 @@ static int write_oid(const struct object_id *oid,\n \tstruct write_oid_context *ctx = data;\n \tstruct child_process *cmd = ctx->cmd;\n \n+\t/*\n+\t * Objects in to_drop are being removed from the repository, so\n+\t * omit them from the rebuilt promisor pack. Each such object is a\n+\t * promisor object and therefore remains recoverable from the\n+\t * promisor remote.\n+\t */\n+\tif (ctx->to_drop && oidset_contains(ctx->to_drop, oid))\n+\t\treturn 0;\n+\n \tif (cmd->in == -1) {\n \t\tif (start_command(cmd))\n \t\t\tdie(_(\"could not start pack-objects to repack promisor objects\"));\n@@ -81,7 +92,8 @@ static void finish_repacking_promisor_objects(struct repository *repo,\n \n void repack_promisor_objects(struct repository *repo,\n \t\t\t     const struct pack_objects_args *args,\n-\t\t\t     struct string_list *names, const char *packtmp)\n+\t\t\t     struct string_list *names, const char *packtmp,\n+\t\t\t     const struct oidset *to_drop)\n {\n \tstruct write_oid_context ctx;\n \tstruct child_process cmd = CHILD_PROCESS_INIT;\n@@ -98,6 +110,7 @@ void repack_promisor_objects(struct repository *repo,\n \t */\n \tctx.cmd = &cmd;\n \tctx.algop = repo->hash_algo;\n+\tctx.to_drop = to_drop;\n \todb_for_each_object(repo->objects, NULL, write_oid, &ctx,\n \t\t\t    ODB_FOR_EACH_OBJECT_PROMISOR_ONLY);\n \ndiff --git a/repack.h b/repack.h\nindex f9fbc895f0..a5a3f7c6ba 100644\n--- a/repack.h\n+++ b/repack.h\n@@ -3,6 +3,7 @@\n \n #include \"list-objects-filter-options.h\"\n #include \"string-list.h\"\n+#include \"oidset.h\"\n \n struct pack_objects_args {\n \tchar *window;\n@@ -100,7 +101,8 @@ void generated_pack_install(struct generated_pack *pack, const char *name,\n \n void repack_promisor_objects(struct repository *repo,\n \t\t\t     const struct pack_objects_args *args,\n-\t\t\t     struct string_list *names, const char *packtmp);\n+\t\t\t     struct string_list *names, const char *packtmp,\n+\t\t\t     const struct oidset *to_drop);\n \n struct pack_geometry {\n \tstruct packed_git **pack;\n-- \n2.54.0\n\n"},{"id":"549842","messageId":"20260806112202.75067-5-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260806112202.75067-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v3 4/7] builtin/repack: enumerate promisor blobs for --drop-filtered","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-06T11:21:59Z","receivedAt":"2026-08-06T11:22:36Z","isPatch":true,"body":"Add enumeration logic for --drop-filtered. In --dry-run mode, print\nthe OIDs of locally-held promisor blobs that exceed the filter\nthreshold, as candidates for removal.\n\nReading from write_filtered_pack() cannot work for partial clones.\ngit repack routes promisor objects through a separate path:\nrepack_promisor_objects() repacks them first, and the main\npack-objects run uses --exclude-promisor-objects. By the time\nwrite_filtered_pack() runs, the promisor blobs are already consumed by\nthe main pack. The filtered pack is always empty on a partial clone.\n\nInstead, walk promisor objects directly via odb_for_each_object() with\nODB_FOR_EACH_OBJECT_PROMISOR_ONLY, collecting all promisor blobs into\nan oidset. The blobs exceeding the filter threshold are then selected\nusing list_objects_filter__filter_oidset().\n\nEvery object enumerated this way is a promisor object, so it is\nrecoverable from the promisor remote in the same sense as the rest of a\npartial clone, as long as the remote still has it. This holds without a\nseparate is_promisor_object() check. A future implementation can verify\navailability against the remote directly once a client-side\nremote-object-info query exists.\n\nOBJECT_INFO_SKIP_FETCH_OBJECT is passed to every object info query so\nenumeration never triggers a lazy fetch.\n\nThe enumeration collects candidates into a caller-provided oidset and\n--dry-run prints them. Actually removing the objects, together with the\nrequired promisor-remote verification, is written in a later commit.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n builtin/repack.c                | 20 +++++++-\n repack-filtered.c               | 81 +++++++++++++++++++++++++++++++\n repack.h                        |  4 ++\n t/t7706-repack-drop-filtered.sh | 84 ++++++++++++++++++++++++++++++++-\n 4 files changed, 187 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex 0a4dadb896..c5f39cef00 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -15,6 +15,8 @@\n #include \"repack.h\"\n #include \"shallow.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"oidset.h\"\n+#include \"hex.h\"\n \n #define ALL_INTO_ONE 1\n #define LOOSEN_UNREACHABLE 2\n@@ -159,6 +161,7 @@ int cmd_repack(int argc,\n \tstruct string_list_item *item;\n \tstruct string_list names = STRING_LIST_INIT_DUP;\n \tstruct existing_packs existing = EXISTING_PACKS_INIT;\n+\tstruct oidset drop_oids = OIDSET_INIT;\n \tstruct pack_geometry geometry = { 0 };\n \tstruct tempfile *refs_snapshot = NULL;\n \tint i, ret;\n@@ -317,6 +320,20 @@ int cmd_repack(int argc,\n \t\t\tdie(_(\"--drop-filtered requires a promisor remote\"));\n \n \t\twrite_bitmaps = 0;\n+\n+\t\tret = enumerate_promisor_blobs(repo, &po_args.filter_options, &drop_oids);\n+\n+\t\tif (ret)\n+\t\t\tgoto cleanup;\n+\n+\t\tif (dry_run) {\n+\t\t\tstruct oidset_iter iter;\n+\t\t\tconst struct object_id *oid;\n+\n+\t\t\toidset_iter_init(&drop_oids, &iter);\n+\t\t\twhile ((oid = oidset_iter_next(&iter)))\n+\t\t\t\tprintf(\"%s\\n\", oid_to_hex(oid));\n+\t\t}\n \t}\n \n \tif (delete_redundant && repo->repository_format_precious_objects)\n@@ -612,7 +629,7 @@ int cmd_repack(int argc,\n \t\t}\n \t}\n \n-\tif (po_args.filter_options.choice) {\n+\tif (po_args.filter_options.choice && !drop_filtered) {\n \t\tstruct write_pack_opts opts = {\n \t\t\t.po_args = &po_args,\n \t\t\t.destination = filter_to,\n@@ -705,6 +722,7 @@ int cmd_repack(int argc,\n cleanup:\n \tstring_list_clear(&keep_pack_list, 0);\n \tstring_list_clear(&names, 1);\n+\toidset_clear(&drop_oids);\n \texisting_packs_release(&existing);\n \tpack_geometry_release(&geometry);\n \tpack_objects_args_release(&po_args);\ndiff --git a/repack-filtered.c b/repack-filtered.c\nindex edcf7667c5..79ba6d90aa 100644\n--- a/repack-filtered.c\n+++ b/repack-filtered.c\n@@ -3,6 +3,12 @@\n #include \"repository.h\"\n #include \"run-command.h\"\n #include \"string-list.h\"\n+#include \"hex.h\"\n+#include \"packfile.h\"\n+#include \"list-objects-filter-options.h\"\n+#include \"list-objects-filter.h\"\n+#include \"odb.h\"\n+#include \"promisor-remote.h\"\n \n int write_filtered_pack(const struct write_pack_opts *opts,\n \t\t\tstruct existing_packs *existing,\n@@ -49,3 +55,78 @@ int write_filtered_pack(const struct write_pack_opts *opts,\n \treturn finish_pack_objects_cmd(existing->repo->hash_algo, opts, &cmd,\n \t\t\t\t       names);\n }\n+\n+struct collect_cb_data {\n+\tstruct repository *repo;\n+\tstruct oidset *set;\n+};\n+\n+static int collect_promisor_blob(const struct object_id *oid,\n+\t\t\t\t struct object_info *oi UNUSED,\n+\t\t\t\t void *cb_data)\n+{\n+\tstruct collect_cb_data *data = cb_data;\n+\tstruct object_info info = OBJECT_INFO_INIT;\n+\tenum object_type type;\n+\n+\tinfo.typep = &type;\n+\n+\t/*\n+\t * Use OBJECT_INFO_SKIP_FETCH_OBJECT to avoid triggering a\n+\t * lazy fetch while collecting promisor blobs.\n+\t */\n+\tif (odb_read_object_info_extended(data->repo->objects, oid, &info,\n+\t\t\tOBJECT_INFO_SKIP_FETCH_OBJECT) < 0)\n+\t\treturn 0;\n+\n+\tif (type == OBJ_BLOB)\n+\t\toidset_insert(data->set, oid);\n+\n+\treturn 0;\n+}\n+\n+int enumerate_promisor_blobs(struct repository *repo,\n+\t\t\t     const struct list_objects_filter_options *filter,\n+\t\t\t     struct oidset *to_drop)\n+{\n+\tstruct oidset all_promisor_blobs = OIDSET_INIT;\n+\tstruct collect_cb_data cb = {\n+\t\t.repo = repo,\n+\t\t.set = &all_promisor_blobs\n+\t};\n+\tint ret = 0;\n+\n+\t/*\n+\t * The caller (cmd_repack) is responsible for validating that a\n+\t * blob:limit filter and a promisor remote are present before\n+\t * calling this function.\n+\t *\n+\t * Walk only promisor objects. every object visited here is a\n+\t * promisor object, so it is recoverable from the promisor remote\n+\t * as long as the remote still has it, the same assumption the rest\n+\t * of partial clone relies on\n+\n+\t * We do not use write_filtered_pack() here because git repack\n+\t * routes promisor objects through repack_promisor_objects()\n+\t * before the filter machinery runs, so the filtered pack never\n+\t * contains promisor blobs. Direct enumeration via\n+\t * ODB_FOR_EACH_OBJECT_PROMISOR_ONLY is the correct approach.\n+\t */\n+\tret = odb_for_each_object(repo->objects, NULL,\n+\t\t\tcollect_promisor_blob, &cb,\n+\t\t\tODB_FOR_EACH_OBJECT_PROMISOR_ONLY);\n+\tif (ret)\n+\t\tgoto cleanup;\n+\n+\t/*\n+\t * Apply the filter to find which blobs exceed the threshold.\n+\t */\n+\tret = list_objects_filter__filter_oidset(repo,\n+\t\t(struct list_objects_filter_options *)filter,\n+\t\t&all_promisor_blobs,\n+\t\tto_drop);\n+\n+cleanup:\n+\toidset_clear(&all_promisor_blobs);\n+\treturn ret;\n+}\ndiff --git a/repack.h b/repack.h\nindex a5a3f7c6ba..61e554e4ed 100644\n--- a/repack.h\n+++ b/repack.h\n@@ -167,6 +167,10 @@ int write_filtered_pack(const struct write_pack_opts *opts,\n \t\t\tstruct existing_packs *existing,\n \t\t\tstruct string_list *names);\n \n+int enumerate_promisor_blobs(struct repository *repo,\n+\t\t\t     const struct list_objects_filter_options *filter,\n+\t\t\t     struct oidset *to_drop);\n+\n 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,\ndiff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh\nindex f27b09a30e..453053cc18 100755\n--- a/t/t7706-repack-drop-filtered.sh\n+++ b/t/t7706-repack-drop-filtered.sh\n@@ -1,9 +1,36 @@\n #!/bin/sh\n \n-test_description='git repack --drop-filtered option validation'\n+test_description='git repack --drop-filtered enumerates filtered promisor blobs'\n \n . ./test-lib.sh\n \n+delete_object () {\n+\tlocal repo=\"$1\" &&\n+\tlocal obj=\"$2\" &&\n+\tlocal path=\"$repo/.git/objects/$(test_oid_to_path \"$obj\")\" &&\n+\trm \"$path\"\n+}\n+\n+# pack the objects into a promisor pack inside \"repo\". it is a pack\n+# accompanied by an empty \".promisor\" marker file. objects\n+# in such a pack are treated as recoverable from the promisor remote.\n+pack_as_from_promisor () {\n+\tHASH=$(git -C repo pack-objects .git/objects/pack/pack) &&\n+\t>repo/.git/objects/pack/pack-$HASH.promisor &&\n+\techo $HASH\n+}\n+\n+# write a blob of $1 bytes into \"repo\", record it as coming from the\n+# promisor remote, and remove the loose copy so the object is only\n+# present in the promisor pack\n+promisor_blob () {\n+\ttest-tool genrandom \"$1\" \"$2\" >blob_content &&\n+\tOID=$(git -C repo hash-object -w --stdin <blob_content) &&\n+\tprintf \"%s\\n\" \"$OID\" | pack_as_from_promisor >/dev/null &&\n+\tdelete_object repo \"$OID\" &&\n+\techo \"$OID\"\n+}\n+\n # checks for options validations before any promisor walk\n test_expect_success 'setup plain repo for validation' '\n \tgit init plain &&\n@@ -52,4 +79,59 @@ test_expect_success '--drop-filtered fails without a promisor remote' '\n \ttest_grep \"drop-filtered requires a promisor remote\" err\n '\n \n+# enumeration tests using promisor pack\n+test_expect_success 'setup repo with a promisor remote' '\n+\trm -rf repo &&\n+\ttest_create_repo repo &&\n+\ttest_commit -C repo base &&\n+\n+\t# mark the repo as a partial clone with a promisor remote so the\n+\t# promisor walk and the safety guard are satisfied\n+\tgit -C repo config core.repositoryformatversion 1 &&\n+\tgit -C repo config extensions.partialclone origin &&\n+\tgit -C repo config remote.origin.promisor true &&\n+\tgit -C repo config remote.origin.url \".\" &&\n+\n+\tBIG=$(promisor_blob big 3072) &&\n+\tSMALL=$(promisor_blob small 512) &&\n+\techo \"$BIG\" >big_oid &&\n+\techo \"$SMALL\" >small_oid\n+'\n+\n+test_expect_success 'promisor blob over the threshold is listed' '\n+\tBIG=$(cat big_oid) &&\n+\tSMALL=$(cat small_oid) &&\n+\n+\tgit -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a >out &&\n+\n+\ttest_grep \"$BIG\" out &&\n+\ttest_grep ! \"$SMALL\" out\n+'\n+\n+test_expect_success 'locally created blob is never listed' '\n+\tBIG=$(cat big_oid) &&\n+\n+\t# large blob that exists only locally must never be a drop candidate.\n+\t# dropping it would be unrecoverable\n+\ttest-tool genrandom local 4096 >local_content &&\n+\tLOCAL=$(git -C repo hash-object -w --stdin <local_content) &&\n+\n+\tgit -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a >out &&\n+\n+\ttest_grep \"$BIG\" out &&\n+\ttest_grep ! \"$LOCAL\" out\n+'\n+\n+test_expect_success '--dry-run does not remove the filtered objects' '\n+\tBIG=$(cat big_oid) &&\n+\n+\tgit -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a >out &&\n+\n+\t# candidate blob must still be present after a dry run\n+\tgit -C repo cat-file -e \"$BIG\"\n+'\n+\n test_done\n-- \n2.54.0\n\n"},{"id":"549843","messageId":"20260806112202.75067-6-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260806112202.75067-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v3 5/7] builtin/repack: actually drop filtered promisor blobs","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-06T11:22:00Z","receivedAt":"2026-08-06T11:22:40Z","isPatch":true,"body":"Make --drop-filtered remove the enumerated promisor blobs instead of\nonly listing them.\n\nThe drop set is computed before repack_promisor_objects() runs, and on\na real run it is passed in so the rebuilt promisor pack omits those\nblobs. --drop-filtered implies -d so the old promisor packs, which\nstill contain the dropped blobs, are removed. Without this the blobs\nwould survive in the redundant packs. The existing repack machinery\nperforms the write-before-delete and fsync, so the drop is crash-safe.\n\nThe dropped blobs become absent locally but remain recoverable from the\npromisor remote, so a later access lazy-fetches them back\ntransparently. --dry-run keeps its previous behavior, i.e. it lists the\ncandidates and changes nothing.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n builtin/repack.c                | 14 ++++++++++----\n repack-filtered.c               |  1 +\n t/t7706-repack-drop-filtered.sh | 12 ++++++++++++\n 3 files changed, 23 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex c5f39cef00..a20589a7ae 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -286,9 +286,6 @@ int cmd_repack(int argc,\n \t\tdie(_(\"--dry-run only takes effect with --drop-filtered\"));\n \n \tif (drop_filtered) {\n-\t\tif (!dry_run)\n-\t\t\tdie(_(\"--drop-filtered doesn't work without --dry-run yet\"));\n-\n \t\tif (!po_args.filter_options.choice)\n \t\t\tdie(_(\"--drop-filtered requires --filter\"));\n \n@@ -321,6 +318,14 @@ int cmd_repack(int argc,\n \n \t\twrite_bitmaps = 0;\n \n+\t\t/*\n+\t\t * Dropping objects means rebuilding the promisor packs\n+\t\t * without them and then removing the old packs, so the\n+\t\t * redundant packs must be deleted. Imply -d on a real run.\n+\t\t */\n+\t\tif (!dry_run)\n+\t\t\tdelete_redundant = 1;\n+\n \t\tret = enumerate_promisor_blobs(repo, &po_args.filter_options, &drop_oids);\n \n \t\tif (ret)\n@@ -446,7 +451,8 @@ int cmd_repack(int argc,\n \t\tstrvec_push(&cmd.args, \"--delta-islands\");\n \n \tif (pack_everything & ALL_INTO_ONE) {\n-\t\trepack_promisor_objects(repo, &po_args, &names, packtmp, NULL);\n+\t\trepack_promisor_objects(repo, &po_args, &names, packtmp,\n+\t\t\t(drop_filtered && !dry_run) ? &drop_oids : NULL);\n \n \t\tif (existing_packs_has_non_kept(&existing) &&\n \t\t    delete_redundant &&\ndiff --git a/repack-filtered.c b/repack-filtered.c\nindex 79ba6d90aa..e6c35c23de 100644\n--- a/repack-filtered.c\n+++ b/repack-filtered.c\n@@ -120,6 +120,7 @@ int enumerate_promisor_blobs(struct repository *repo,\n \n \t/*\n \t * Apply the filter to find which blobs exceed the threshold.\n+\t * The caller has to_drop and is responsible for clearing it.\n \t */\n \tret = list_objects_filter__filter_oidset(repo,\n \t\t(struct list_objects_filter_options *)filter,\ndiff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh\nindex 453053cc18..88c2bb0857 100755\n--- a/t/t7706-repack-drop-filtered.sh\n+++ b/t/t7706-repack-drop-filtered.sh\n@@ -134,4 +134,16 @@ test_expect_success '--dry-run does not remove the filtered objects' '\n \tgit -C repo cat-file -e \"$BIG\"\n '\n \n+test_expect_success '--drop-filtered removes the promisor blob locally' '\n+\tBIG=$(cat big_oid) &&\n+\tSMALL=$(cat small_oid) &&\n+\n+\tgit -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k -a &&\n+\n+\tgit -C repo cat-file --batch-all-objects --batch-check=\"%(objectname)\" >present &&\n+\t! grep -q \"$BIG\" present &&\n+\tgrep -q \"$SMALL\" present\n+'\n+\n test_done\n-- \n2.54.0\n\n"},{"id":"549844","messageId":"20260806112202.75067-7-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260806112202.75067-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v3 6/7] builtin/repack: add guards for --drop-filtered","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-06T11:22:01Z","receivedAt":"2026-08-06T11:22:45Z","isPatch":true,"body":"--drop-filtered removes local promisor blobs. That is only safe when the\nrepository is not mid-operation and when the blobs are not actively in\nuse, so add two guards, both skipped for bare repositories which have\nneither a worktree nor an index.\n\nFirst, refuse to run while a merge, rebase, am, cherry-pick, revert, or\nbisect is in progress. During these operations the working tree and\nindex are in an intermediate state, and rewriting packs and deleting\nobjects underneath a half-finished operation is unsafe.\n\nSecond, refuse to drop a blob that the current index references. Such a\nblob is needed by the working tree, so dropping it would only cause the\nnext command that touches the worktree to lazy-fetch it straight back,\nreclaiming nothing. The offending path is reported so the user can see\nwhy the drop was refused.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n builtin/repack.c                | 49 +++++++++++++++++++++++++++++++++\n t/t7706-repack-drop-filtered.sh | 36 ++++++++++++++++++++++++\n 2 files changed, 85 insertions(+)\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex a20589a7ae..170e94f8bd 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -17,6 +17,8 @@\n #include \"list-objects-filter-options.h\"\n #include \"oidset.h\"\n #include \"hex.h\"\n+#include \"wt-status.h\"\n+#include \"read-cache-ll.h\"\n \n #define ALL_INTO_ONE 1\n #define LOOSEN_UNREACHABLE 2\n@@ -316,6 +318,30 @@ int cmd_repack(int argc,\n \t\tif (!repo_has_promisor_remote(repo))\n \t\t\tdie(_(\"--drop-filtered requires a promisor remote\"));\n \n+\t\t/*\n+ \t\t * refuse to run while another operation is in progress. A\n+\t\t * dropped object would just be lazily re-fetched when the\n+\t\t * operation resumes, but triggering a network fetch in the\n+\t\t * middle of a half-finished\n+\t\t * merge/rebase/cherry-pick/revert/bisect is a poor\n+\t\t * experience, so this is a UX convenience rather than a\n+\t\t * safety measure. Bare repositories have no such state, so\n+\t\t * the check is skipped there.\n+\t\t */\n+\t\tif (!is_bare_repository(repo)) {\n+\t\t\tstruct wt_status_state state = { 0 };\n+\n+\t\t\twt_status_get_state(repo, &state, 0);\n+\t\t\tif (state.merge_in_progress || state.revert_in_progress ||\n+\t\t\t    state.rebase_in_progress ||state.bisect_in_progress ||\n+\t\t\t    state.cherry_pick_in_progress ||state.am_in_progress||\n+\t\t\t    state.rebase_interactive_in_progress) {\n+\t\t\t\twt_status_state_free_buffers(&state);\n+\t\t\t\tdie(_(\"--drop-filtered cannot be used while another operation is in progress\"));\n+\t\t\t}\n+\t\t\twt_status_state_free_buffers(&state);\n+\t\t}\n+\n \t\twrite_bitmaps = 0;\n \n \t\t/*\n@@ -331,6 +357,29 @@ int cmd_repack(int argc,\n \t\tif (ret)\n \t\t\tgoto cleanup;\n \n+\t\t/*\n+ \t\t * refuse to drop blobs that the current index references.\n+\t\t * such a blob would only be lazily re-fetched by the next\n+\t\t * command that touches the worktree, so dropping it reclaims\n+\t\t * nothing. This guard just avoids that churn. bare\n+\t\t * repositories have no index, so the check is skipped there.\n+\t\t */\n+\t\tif (!is_bare_repository(repo) && oidset_size(&drop_oids)) {\n+\t\t\tstruct index_state *istate = repo->index;\n+\t\t\tunsigned int i;\n+\n+\t\t\tif (repo_read_index(repo) < 0)\n+\t\t\t\tdie(_(\"could not read the index\"));\n+\n+\t\t\tfor (i = 0; i < istate->cache_nr; i++) {\n+\t\t\t\tconst struct cache_entry *ce = istate->cache[i];\n+\n+\t\t\t\tif (oidset_contains(&drop_oids, &ce->oid))\n+\t\t\t\t\tdie(_(\"cannot drop '%s' (%s): it is referenced by the current index\"),\n+\t\t\t\t\t\tce->name, oid_to_hex(&ce->oid));\n+\t\t\t}\n+\t\t}\n+\n \t\tif (dry_run) {\n \t\t\tstruct oidset_iter iter;\n \t\t\tconst struct object_id *oid;\ndiff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh\nindex 88c2bb0857..6774886f1e 100755\n--- a/t/t7706-repack-drop-filtered.sh\n+++ b/t/t7706-repack-drop-filtered.sh\n@@ -146,4 +146,40 @@ test_expect_success '--drop-filtered removes the promisor blob locally' '\n \tgrep -q \"$SMALL\" present\n '\n \n+test_expect_success '--drop-filtered refuses when a merge is in progress' '\n+\ttest_when_finished \"git -C repo merge --abort || :\" &&\n+\n+\t# creat a conflicting merge so wt_status reports it\n+\tgit -C repo checkout -B mergebase base &&\n+\techo one >repo/conflict.txt &&\n+\tgit -C repo add conflict.txt &&\n+\tgit -C repo commit -m one &&\n+\n+\tgit -C repo checkout -B mergeother base &&\n+\techo two >repo/conflict.txt &&\n+\tgit -C repo add conflict.txt &&\n+\tgit -C repo commit -m two &&\n+\n+\ttest_must_fail git -C repo merge mergebase &&\n+\n+\ttest_must_fail git -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err &&\n+\ttest_grep \"in progress\" err\n+'\n+\n+\n+test_expect_success '--drop-filtered refuses to drop an index-referenced blob' '\n+\t# create a large blob, add it to the index and make it a promisor object\n+\t# so the index references it and enumeration picks it up\n+\ttest-tool genrandom idx 4096 >repo/tracked-big.bin &&\n+\tgit -C repo add tracked-big.bin &&\n+\tOID=$(git -C repo rev-parse :tracked-big.bin) &&\n+\tprintf \"%s\\n\" \"$OID\" | pack_as_from_promisor >/dev/null &&\n+\tdelete_object repo \"$OID\" &&\n+\n+\ttest_must_fail git -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err &&\n+\ttest_grep \"referenced by the current index\" err\n+'\n+\n test_done\n-- \n2.54.0\n\n"},{"id":"549845","messageId":"20260806112202.75067-8-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260806112202.75067-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v3 7/7] Documentation/git-repack: document --drop-filtered and --dry-run","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-06T11:22:02Z","receivedAt":"2026-08-06T11:22:49Z","isPatch":true,"body":"Describe the new --drop-filtered and --dry-run options: what they do,\nonly blob:limit filters are supported for now, a promisor remote is\nrequired, --drop-filtered requires -a and implies -d so the redundant\npacks are actually removed, its incompatibilities with --filter-to and\nbitmap writing, and the safety guards that refuse to run mid-operation\nor to drop index-referenced blobs.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n Documentation/git-repack.adoc | 37 +++++++++++++++++++++++++++++++++++\n 1 file changed, 37 insertions(+)\n\ndiff --git a/Documentation/git-repack.adoc b/Documentation/git-repack.adoc\nindex 72c42015e2..4c6aa3bc18 100644\n--- a/Documentation/git-repack.adoc\n+++ b/Documentation/git-repack.adoc\n@@ -12,6 +12,7 @@ SYNOPSIS\n 'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\n \t[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]\n \t[--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]\n+\t[--filter=<filter-spec>] [--drop-filtered [--dry-run]]]\n \n DESCRIPTION\n -----------\n@@ -182,6 +183,42 @@ depth is 4095.\n \t`objects` and `objects/info/alternates` sections of\n \tlinkgit:gitrepository-layout[5].\n \n+--drop-filtered::\n+\tDelete the local objects that match the `--filter` specification\n+\tinstead of keeping them in a separate packfile, reclaiming the\n+\tdisk space they occupy. This is intended for partial clones,\n+\twhere the filtered objects are promisor objects that remain\n+\trecoverable from the promisor remote and are lazily re-fetched\n+\ton demand when they are next needed.\n++\n+Only large blobs are supported for now, so `--filter=blob:limit=<n>`\n+is currently the only accepted filter. Because dropped objects must be\n+recoverable, this option requires a promisor remote to be configured\n+and refuses to run otherwise.\n++\n+This option requires `-a`, and implies `-d`: the objects are dropped by\n+rebuilding the promisor pack without them and then removing the now\n+redundant old packs, so the redundant packs must be deleted for the\n+space to actually be reclaimed. It is incompatible with `--filter-to`\n+and with bitmap writing (`-b`/`--write-bitmap-index`), since filtering\n+breaks the single-pack closure that bitmaps require. A bitmap setting\n+coming from configuration is silently disabled for the duration of the\n+command.\n++\n+As a convenience since dropped objects remain recoverable by lazy fetch,\n+`--drop-filtered` refuses to run while another operation\n+(merge, rebase, am, cherry-pick, revert, or bisect) is in progress, to\n+avoid a surprising network fetch mid-operation, and refuses to drop any\n+blob that the current index references, since such a blob would only be\n+lazily re-fetched by the next command that inspects the working tree.\n+These checks are skipped in bare repositories, which have neither a\n+working tree nor an index.\n+\n+--dry-run::\n+\tOnly meaningful with `--drop-filtered`. List the objects that\n+\twould be dropped, one object ID per line, without rebuilding any\n+\tpack or deleting anything.\n+\n -b::\n --write-bitmap-index::\n \tWrite a reachability bitmap index as part of the repack. This\n-- \n2.54.0\n\n"},{"id":"549890","messageId":"xmqq4ih7hr0f.fsf@gitster.g","threadId":"66019","inReplyTo":"20260806112202.75067-1-r.siddharth.shrimali@gmail.com","subject":"Re: [GSoC PATCH v3 0/7] repack: add --drop-filtered to reclaim space in partial clones","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-06T21:34:56Z","receivedAt":"2026-08-06T21:34:59Z","isPatch":true,"body":"You would need something like the attached patch.  \n\nDidn't you get these when you ran \"make test\"?\n\nt7706-repack-drop-filtered.sh:145: error: bare grep outside pipeline (use test_grep)\nt7706-repack-drop-filtered.sh:146: error: bare grep outside pipeline (use test_grep)\n\n t/t7706-repack-drop-filtered.sh | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git i/t/t7706-repack-drop-filtered.sh w/t/t7706-repack-drop-filtered.sh\nindex 6774886f1e..05d58fa456 100755\n--- i/t/t7706-repack-drop-filtered.sh\n+++ w/t/t7706-repack-drop-filtered.sh\n@@ -142,8 +142,8 @@ test_expect_success '--drop-filtered removes the promisor blob locally' '\n \t\trepack --drop-filtered --filter=blob:limit=1k -a &&\n \n \tgit -C repo cat-file --batch-all-objects --batch-check=\"%(objectname)\" >present &&\n-\t! grep -q \"$BIG\" present &&\n-\tgrep -q \"$SMALL\" present\n+\ttest_grep ! \"$BIG\" present &&\n+\ttest_grep \"$SMALL\" present\n '\n \n test_expect_success '--drop-filtered refuses when a merge is in progress' '\n\n\n"},{"id":"549898","messageId":"xmqqpkzuhoyr.fsf@gitster.g","threadId":"66019","inReplyTo":"20260806112202.75067-1-r.siddharth.shrimali@gmail.com","subject":"Re: [GSoC PATCH v3 0/7] repack: add --drop-filtered to reclaim space in partial clones","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-06T22:19:08Z","receivedAt":"2026-08-06T22:19:11Z","isPatch":true,"body":"Siddharth Shrimali <r.siddharth.shrimali@gmail.com> writes:\n\n> This is v3 of the series adding \"git repack --drop-filtered\" to reclaim\n> disk space in partial clones by dropping large, locally-held promisor\n> blobs that remain recoverable from the promisor remote. v2 was at [1].\n\nAlso I am getting a failure from t0450.\n\n--- adoc        2026-08-06 22:05:39.038464944 +0000\n+++ help        2026-08-06 22:05:39.046464970 +0000\n@@ -1,4 +1,3 @@\n git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\n            [--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]\n            [--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]\n-           [--filter=<filter-spec>] [--drop-filtered [--dry-run]]]\nnot ok 650 - repack -h output and SYNOPSIS agree\n#\n#                       t2s=\"$(adoc_to_synopsis \"$builtin\")\" &&\n#                       if test \"$builtin\" = \"merge-tree\"\n#                       then\n#                               test_when_finished \"rm -f t2s.new\" &&\n#                               sed -e 's/ (deprecated)$//g' <\"$t2s\" >t2s.new\n#                               t2s=t2s.new\n#                       fi &&\n#                       h2s=\"$(help_to_synopsis \"$builtin\")\" &&\n#\n#                       # The *.adoc and -h use different spacing for the\n#                       # alignment of continued usage output, normalize it.\n#                       align_after_nl \"$builtin\" <\"$t2s\" >adoc &&\n#                       align_after_nl \"$builtin\" <\"$h2s\" >help &&\n#                       test_cmp adoc help\n#\n1..650\n\n\nHave these patches been reviewed and tested?  Is this a new breakage\nin v3?\n\nI think the accumulated fixes so far I have are as follows, but I\nsuspect they need to be split and squashed into multiple patches (I\ndidn't check).\n\n Documentation/git-repack.adoc   | 2 +-\n builtin/repack.c                | 3 ++-\n t/t7706-repack-drop-filtered.sh | 4 ++--\n 3 files changed, 5 insertions(+), 4 deletions(-)\n\ndiff --git i/Documentation/git-repack.adoc w/Documentation/git-repack.adoc\nindex 1364d6cd49..1775fb7645 100644\n--- i/Documentation/git-repack.adoc\n+++ w/Documentation/git-repack.adoc\n@@ -12,7 +12,7 @@ SYNOPSIS\n 'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\n \t[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]\n \t[--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]\n-\t[--filter=<filter-spec>] [--drop-filtered [--dry-run]]]\n+\t[--filter=<filter-spec>] [--drop-filtered [--dry-run]]\n \n DESCRIPTION\n -----------\ndiff --git i/builtin/repack.c w/builtin/repack.c\nindex 9473342843..81ec093808 100644\n--- i/builtin/repack.c\n+++ w/builtin/repack.c\n@@ -40,7 +40,8 @@ static int write_bitmaps_given;\n static const char *const git_repack_usage[] = {\n \tN_(\"git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\\n\"\n \t   \"[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]\\n\"\n-\t   \"[--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]\"),\n+\t   \"[--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]\\n\"\n+\t   \"[--filter=<filter-spec>] [--drop-filtered [--dry-run]]\"),\n \tNULL\n };\n \ndiff --git i/t/t7706-repack-drop-filtered.sh w/t/t7706-repack-drop-filtered.sh\nindex 6774886f1e..05d58fa456 100755\n--- i/t/t7706-repack-drop-filtered.sh\n+++ w/t/t7706-repack-drop-filtered.sh\n@@ -142,8 +142,8 @@ test_expect_success '--drop-filtered removes the promisor blob locally' '\n \t\trepack --drop-filtered --filter=blob:limit=1k -a &&\n \n \tgit -C repo cat-file --batch-all-objects --batch-check=\"%(objectname)\" >present &&\n-\t! grep -q \"$BIG\" present &&\n-\tgrep -q \"$SMALL\" present\n+\ttest_grep ! \"$BIG\" present &&\n+\ttest_grep \"$SMALL\" present\n '\n \n test_expect_success '--drop-filtered refuses when a merge is in progress' '\n"},{"id":"549966","messageId":"CAGWgyh9sDrHb2nXw+mmgQg4-x_H-_mLcwtpAt7QFYHydWNDg+w@mail.gmail.com","threadId":"66019","inReplyTo":"xmqqpkzuhoyr.fsf@gitster.g","subject":"Re: [GSoC PATCH v3 0/7] repack: add --drop-filtered to reclaim space in partial clones","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-07T09:06:39Z","receivedAt":"2026-08-07T09:07:16Z","isPatch":true,"body":"On Fri, 7 Aug 2026 at 03:49, Junio C Hamano <gitster@pobox.com> wrote:\n> Have these patches been reviewed and tested?  Is this a new breakage\n> in v3?\n>\n> I think the accumulated fixes so far I have are as follows, but I\n> suspect they need to be split and squashed into multiple patches (I\n> didn't check).\nsorry for the trouble, these are my mistakes: i ran t7706 but not the full\ntest suite, so i missed the t0450 SYNOPSIS check and the lint errors.\nThe -h usage string wasnt updated to match the new .adoc synopsis,\nand i left a bare grep in the test. I'll fix all three, before sending a v4\n\nthanks for catching these, and for the fixups.\n\nThanks,\nSiddharth Shrimali\n"},{"id":"550050","messageId":"xmqqbjbdejrd.fsf@gitster.g","threadId":"66019","inReplyTo":"CAGWgyh9sDrHb2nXw+mmgQg4-x_H-_mLcwtpAt7QFYHydWNDg+w@mail.gmail.com","subject":"Re: [GSoC PATCH v3 0/7] repack: add --drop-filtered to reclaim space in partial clones","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-07T20:52:06Z","receivedAt":"2026-08-07T20:52:09Z","isPatch":true,"body":"Siddharth Shrimali <r.siddharth.shrimali@gmail.com> writes:\n\n> On Fri, 7 Aug 2026 at 03:49, Junio C Hamano <gitster@pobox.com> wrote:\n>> Have these patches been reviewed and tested?  Is this a new breakage\n>> in v3?\n>>\n>> I think the accumulated fixes so far I have are as follows, but I\n>> suspect they need to be split and squashed into multiple patches (I\n>> didn't check).\n> sorry for the trouble, these are my mistakes: i ran t7706 but not the full\n> test suite, so i missed the t0450 SYNOPSIS check and the lint errors.\n> The -h usage string wasnt updated to match the new .adoc synopsis,\n> and i left a bare grep in the test. I'll fix all three, before sending a v4\n\nPlease do not limit yourself to \"all three\".  Do not expect\nreviewers to be exhaustive.  You are expected to be.\n\nIOW, do not just run a selected few tests.  Run the full testsuite,\nand then some more, like making a trial merge to 'next' and to\n'seen' and run full testsuite on the results.\n\nThanks.\n"},{"id":"550110","messageId":"CAGWgyh8YDULkNMFsyTmdrm+7mkj2rCvLu=BL7j1DkA0+B65s6Q@mail.gmail.com","threadId":"66019","inReplyTo":"xmqqbjbdejrd.fsf@gitster.g","subject":"Re: [GSoC PATCH v3 0/7] repack: add --drop-filtered to reclaim space in partial clones","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-08T20:07:12Z","receivedAt":"2026-08-08T20:07:49Z","isPatch":true,"body":"On Sat, 8 Aug 2026 at 02:22, Junio C Hamano <gitster@pobox.com> wrote:\n> Please do not limit yourself to \"all three\".  Do not expect\n> reviewers to be exhaustive.  You are expected to be.\n>\n> IOW, do not just run a selected few tests.  Run the full testsuite,\n> and then some more, like making a trial merge to 'next' and to\n> 'seen' and run full testsuite on the results.\n>\n> Thanks.\n\nsure, thanks for the advice!! :)\ni'll run the full suite and trial-merge onto 'next' and 'seen' before\nv4.\n\nThanks,\nSiddharth Shrimali\n"},{"id":"550202","messageId":"20260810174047.6524-1-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260806112202.75067-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v4 0/7] repack: add --drop-filtered to reclaim space in partial clones","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-10T17:40:40Z","receivedAt":"2026-08-10T17:41:07Z","isPatch":true,"body":"This is v4. v3 was at [1].\n\nPartial clones let you work with large repositories without downloading\nevery blob up front. Missing blobs are lazily fetched from the promisor\nremote on demand. Over time these accumulate locally and there is\ncurrently no safe, built-in way to reclaim that space short of\nre-cloning. This series adds that reverse direction: enumerate promisor\nblobs over a size threshold, drop them locally, and rely on the existing\nlazy-fetch machinery to bring them back transparently when needed.\n\nHow it works:\n  * Enumerate promisor objects directly (ODB_FOR_EACH_OBJECT_PROMISOR_ONLY)\n    and select the blobs exceeding the filter threshold. Every enumerated\n    object is a promisor object, so it is recoverable from the promisor\n    remote as long as the remote still has it, the same assumption the\n    rest of partial clone relies on.\n  * Rebuild the promisor pack without the selected blobs, reusing the\n    existing repack machinery, so the drop is crash-safe (write, fsync,\n    install, then delete the old pack).\n  * --dry-run lists the candidates and changes nothing.\n\nThe guards refuse to run while a merge, rebase, am, cherry-pick, revert,\nor bisect is in progress, and refuse to drop a blob referenced by the\ncurrent index. Dropped objects stay recoverable via lazy fetch, so these\nare a convenience (avoid pointless re-fetch and a surprising\nmid-operation fetch) rather than a correctness measure. Both are skipped\nfor bare repositories.\n\nChanges since v3:\n  * fixed the git repack -h usage string to include --drop-filtered and\n    --dry-run, so it matches the SYNOPSIS in the documentation (t0450\n    was failing - caught by Junio)\n  * converted a bare grep in the test to test_grep (test-lint error -\n    caught by Junio)\n  * removed a stray bracket in the documentation SYNOPSIS\n\nTo do:\n  * remote verification: verifying against the remote awaits the\n    \"remote-object-info\" cat-file protocol command\n  * recency: a \"don't cull recently-fetched objects\" rule as another\n    selection criterion alongside size\n  * drop log: introduce with the error-path change that reads it\n\n[1] https://lore.kernel.org/git/20260806112202.75067-1-r.siddharth.shrimali@gmail.com/\n\nSiddharth Shrimali (7):\n  builtin/repack.c: add --drop-filtered and --dry-run options\n  list-objects-filter: add list_objects_filter__filter_oidset()\n  repack-promisor: allow excluding objects from the rebuilt promisor\n    pack\n  builtin/repack: enumerate promisor blobs for --drop-filtered\n  builtin/repack: actually drop filtered promisor blobs\n  builtin/repack: add guards for --drop-filtered\n  Documentation/git-repack: document --drop-filtered and --dry-run\n\n Documentation/git-repack.adoc   |  37 +++++++\n builtin/repack.c                | 151 +++++++++++++++++++++++++-\n list-objects-filter.c           |  45 ++++++++\n list-objects-filter.h           |  16 +++\n repack-filtered.c               |  82 ++++++++++++++\n repack-promisor.c               |  15 ++-\n repack.h                        |   8 +-\n t/meson.build                   |   1 +\n t/t7706-repack-drop-filtered.sh | 185 ++++++++++++++++++++++++++++++++\n 9 files changed, 533 insertions(+), 7 deletions(-)\n create mode 100755 t/t7706-repack-drop-filtered.sh\n\n-- \n2.54.0\n\n"},{"id":"550203","messageId":"20260810174047.6524-2-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260810174047.6524-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v4 1/7] builtin/repack.c: add --drop-filtered and --dry-run options","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-10T17:40:41Z","receivedAt":"2026-08-10T17:41:12Z","isPatch":true,"body":"Add two new command-line options to 'git-repack':\n\n  --drop-filtered: intended to eventually delete objects that match\n                   the filter specification. Requires --filter and -a,\n                   and is incompatible with --filter-to.\n  --dry-run: show which objects would be dropped without making any\n             changes. Only meaningful with --drop-filtered.\n\nKeep --dry-run as a separate option rather than folding it into\n--drop-filtered (e.g --drop-filtered=dry-run), to stay consistent with\nthe --dry-run option other Git commands already provide and to leave\nroom for it to describe other repack behavior later. A\n--drop-filtered=<mode> form can still be added later if more\ndrop-specific modes are needed.\n\n--drop-filtered also requires a promisor remote to be configured, since\ndropping objects without a remote to fetch them back from would be\npermanent data loss.\n\n--drop-filtered is incompatible with bitmap writing: filtering breaks\nthe \"all objects in one pack\" closure that bitmaps require. Detect an\nexplicit -b/--write-bitmap-index on the command line with a dedicated\noption callback that sets a \"write_bitmaps_given\" flag, so it can be\ndistinguished from a repack.writeBitmaps configuration value even when\nconfig already enables bitmaps. An explicit -b is reported as a conflict,\nwhile a config-provided default is silently disabled for the duration\nof the command.\n\nThese options currently only perform validation. The actual enumeration\nand deletion will be added in follow-up commits.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n builtin/repack.c                | 74 +++++++++++++++++++++++++++++++--\n t/meson.build                   |  1 +\n t/t7706-repack-drop-filtered.sh | 55 ++++++++++++++++++++++++\n 3 files changed, 127 insertions(+), 3 deletions(-)\n create mode 100755 t/t7706-repack-drop-filtered.sh\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex db504d673f..19b26ca723 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -14,6 +14,7 @@\n #include \"promisor-remote.h\"\n #include \"repack.h\"\n #include \"shallow.h\"\n+#include \"list-objects-filter-options.h\"\n \n #define ALL_INTO_ONE 1\n #define LOOSEN_UNREACHABLE 2\n@@ -28,11 +29,15 @@ static int use_delta_islands;\n static int run_update_server_info = 1;\n static char *packdir, *packtmp_name, *packtmp;\n static int midx_must_contain_cruft = 1;\n+static int drop_filtered;\n+static int dry_run;\n+static int write_bitmaps_given;\n \n static const char *const git_repack_usage[] = {\n \tN_(\"git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\\n\"\n \t   \"[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]\\n\"\n-\t   \"[--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]\"),\n+\t   \"[--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]\\n\"\n+\t   \"[--filter=<filter-spec>] [--drop-filtered [--dry-run]]\"),\n \tNULL\n };\n \n@@ -111,6 +116,21 @@ static int repack_config(const char *var, const char *value,\n \treturn git_default_config(var, value, ctx, cb);\n }\n \n+static int option_parse_write_bitmaps(const struct option *opt, const char *arg,\n+\t\t\t\t      int unset)\n+{\n+\tint *value = opt->value;\n+\n+\tBUG_ON_OPT_ARG(arg);\n+\tif (unset)\n+\t\t*value = 0;\n+\telse\n+\t\t*value = 1;\n+\n+\twrite_bitmaps_given = 1;\n+\treturn 0;\n+}\n+\n static int option_parse_write_midx(const struct option *opt, const char *arg,\n \t\t\t\t   int unset)\n {\n@@ -194,8 +214,9 @@ int cmd_repack(int argc,\n \t\tOPT__QUIET(&po_args.quiet, N_(\"be quiet\")),\n \t\tOPT_BOOL('l', \"local\", &po_args.local,\n \t\t\t\tN_(\"pass --local to git-pack-objects\")),\n-\t\tOPT_BOOL('b', \"write-bitmap-index\", &write_bitmaps,\n-\t\t\t\tN_(\"write bitmap index\")),\n+\t\tOPT_CALLBACK_F('b', \"write-bitmap-index\", &write_bitmaps, NULL,\n+\t\t\t        N_(\"write bitmap index\"),\n+\t\t\t       PARSE_OPT_NOARG, option_parse_write_bitmaps),\n \t\tOPT_BOOL('i', \"delta-islands\", &use_delta_islands,\n \t\t\t\tN_(\"pass --delta-islands to git-pack-objects\")),\n \t\tOPT_STRING(0, \"unpack-unreachable\", &unpack_unreachable, N_(\"approxidate\"),\n@@ -231,6 +252,10 @@ int cmd_repack(int argc,\n \t\t\t   N_(\"pack prefix to store a pack containing pruned objects\")),\n \t\tOPT_STRING(0, \"filter-to\", &filter_to, N_(\"dir\"),\n \t\t\t   N_(\"pack prefix to store a pack containing filtered out objects\")),\n+\t\tOPT_BOOL(0, \"drop-filtered\", &drop_filtered,\n+\t\t\t\tN_(\"delete filtered out objects (requires --filter)\")),\n+\t\tOPT_BOOL(0, \"dry-run\", &dry_run,\n+\t\t\t\tN_(\"only show which objects would be dropped\")),\n \t\tOPT_END()\n \t};\n \n@@ -252,6 +277,49 @@ int cmd_repack(int argc,\n \tpo_args.depth = xstrdup_or_null(opt_depth);\n \tpo_args.threads = xstrdup_or_null(opt_threads);\n \n+\tdie_for_incompatible_opt2(drop_filtered, \"--drop-filtered\",\n+\t\t!!filter_to, \"--filter-to\");\n+\n+\tif (dry_run && !drop_filtered)\n+\t\tdie(_(\"--dry-run only takes effect with --drop-filtered\"));\n+\n+\tif (drop_filtered) {\n+\t\tif (!dry_run)\n+\t\t\tdie(_(\"--drop-filtered doesn't work without --dry-run yet\"));\n+\n+\t\tif (!po_args.filter_options.choice)\n+\t\t\tdie(_(\"--drop-filtered requires --filter\"));\n+\n+\t\tif (!(pack_everything & ALL_INTO_ONE))\n+\t\t\tdie(_(\"--drop-filtered requires -a\"));\n+\n+\t\t/*\n+\t\t * Only blob:limit=<n> is supported for now. Reject other\n+\t\t * filter choices early, before walking the object database.\n+\t\t */\n+\t\tif (po_args.filter_options.choice != LOFC_BLOB_LIMIT)\n+\t\t\tdie(_(\"--drop-filtered only supports --filter=blob:limit=<n> for now\"));\n+\n+\t\t/*\n+\t\t * an explicit -b on the command line is a conflict we have to\n+\t\t * report, a bitmap setting from config is silently overridden\n+\t\t * for the duration of the command\n+\t\t */\n+\t\tif (write_bitmaps_given && write_bitmaps > 0)\n+\t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n+\t\t\t\t\"--drop-filtered\", \"--write-bitmap-index\");\n+\n+\t\t/*\n+\t\t * Without a promisor remote there is nowhere to re-fetch the\n+\t\t * dropped objects from, so dropping them would be permanent\n+\t\t * data loss.\n+\t\t */\n+\t\tif (!repo_has_promisor_remote(repo))\n+\t\t\tdie(_(\"--drop-filtered requires a promisor remote\"));\n+\n+\t\twrite_bitmaps = 0;\n+\t}\n+\n \tif (delete_redundant && repo->repository_format_precious_objects)\n \t\tdie(_(\"cannot delete packs in a precious-objects repo\"));\n \ndiff --git a/t/meson.build b/t/meson.build\nindex a25f37d2f5..92352e43c4 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -964,6 +964,7 @@ integration_tests = [\n   't7703-repack-geometric.sh',\n   't7704-repack-cruft.sh',\n   't7705-repack-incremental-midx.sh',\n+  't7706-repack-drop-filtered.sh',\n   't7800-difftool.sh',\n   't7810-grep.sh',\n   't7811-grep-open.sh',\ndiff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh\nnew file mode 100755\nindex 0000000000..f27b09a30e\n--- /dev/null\n+++ b/t/t7706-repack-drop-filtered.sh\n@@ -0,0 +1,55 @@\n+#!/bin/sh\n+\n+test_description='git repack --drop-filtered option validation'\n+\n+. ./test-lib.sh\n+\n+# checks for options validations before any promisor walk\n+test_expect_success 'setup plain repo for validation' '\n+\tgit init plain &&\n+\ttest_commit -C plain initial &&\n+\tgit clone --bare plain plain.git &&\n+\tgit -C plain.git repack -a -d\n+'\n+\n+test_expect_success '--drop-filtered requires --filter' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered --dry-run -a 2>err &&\n+\ttest_grep \"drop-filtered requires --filter\" err\n+'\n+\n+test_expect_success '--drop-filtered cannot be used with --filter-to' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered \\\n+\t\t--filter=blob:limit=1k --filter-to=./filter-out 2>err &&\n+\ttest_grep \"options .--drop-filtered. and .--filter-to. cannot be used together\" err\n+'\n+\n+test_expect_success '--dry-run only takes effect with --drop-filtered' '\n+\ttest_must_fail git -C plain.git repack --dry-run 2>err &&\n+\ttest_grep \"dry-run only takes effect with --drop-filtered\" err\n+'\n+\n+test_expect_success '--drop-filtered requires -a' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered \\\n+\t\t--filter=blob:limit=1k --dry-run 2>err &&\n+\ttest_grep \"drop-filtered requires -a\" err\n+'\n+\n+test_expect_success '--drop-filtered fails with --write-bitmap-index' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered \\\n+\t\t--filter=blob:limit=1k --dry-run -a -b 2>err &&\n+\ttest_grep \"options .--drop-filtered. and .--write-bitmap-index. cannot be used together\" err\n+'\n+\n+test_expect_success '--drop-filtered rejects explicit -b even when repack.writeBitmaps=true' '\n+\ttest_must_fail git -C plain.git -c repack.writeBitmaps=true \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a -b 2>err &&\n+\ttest_grep \"options .--drop-filtered. and .--write-bitmap-index. cannot be used together\" err\n+'\n+\n+test_expect_success '--drop-filtered fails without a promisor remote' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered \\\n+\t\t--filter=blob:limit=1k --dry-run -a 2>err &&\n+\ttest_grep \"drop-filtered requires a promisor remote\" err\n+'\n+\n+test_done\n-- \n2.54.0\n\n"},{"id":"550204","messageId":"20260810174047.6524-3-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260810174047.6524-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v4 2/7] list-objects-filter: add list_objects_filter__filter_oidset()","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-10T17:40:42Z","receivedAt":"2026-08-10T17:41:18Z","isPatch":true,"body":"The existing filter entry point, list_objects_filter__filter_object(),\nis built around the object-walk path: it expects traversal context and\nprovisional omit sets, and is meant to be called as objects are\nvisited during a walk. A caller that already has a set of OIDs in hand\nand only wants to know which ones a filter would select has no usable\nentry point into the filter API.\n\n--drop-filtered is exactly such a caller: it collects promisor blobs\ninto an oidset and needs to know which of them exceed the filter\nthreshold, without performing an object walk.\n\nAdd a helper, list_objects_filter__filter_oidset(), that takes a set\nof OIDs and populates an \"omitted\" set with those that would be\nfiltered out by the given filter options. Only blob:limit=N filters\nare supported for now.\n\nThis helper does not actually reuse the existing filter machinery.\nIt reimplements the blob:limit size check directly. That machinery\nis tied to the object-walk path and cannot easily be driven\nfrom a plain oidset. A NEEDSWORK comment marks this so the helper can\nlater be refactored to reuse the real filter logic instead of\nduplicating it.\n\nOBJECT_INFO_SKIP_FETCH_OBJECT is passed when reading object info so\nthe helper never triggers a lazy fetch.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n list-objects-filter.c | 45 +++++++++++++++++++++++++++++++++++++++++++\n list-objects-filter.h | 16 +++++++++++++++\n 2 files changed, 61 insertions(+)\n\ndiff --git a/list-objects-filter.c b/list-objects-filter.c\nindex c912ff3079..6a2e9d5b24 100644\n--- a/list-objects-filter.c\n+++ b/list-objects-filter.c\n@@ -828,3 +828,48 @@ void list_objects_filter__free(struct filter *filter)\n \tfilter->free_fn(filter->filter_data);\n \tfree(filter);\n }\n+\n+/*\n+ * NEEDSWORK: this reimplements the blob:limit size check rather than\n+ * reusing the existing filter machinery in\n+ * list_objects_filter__filter_object(). That machinery is currently\n+ * tied to the object-walk path and cannot easily be driven from a\n+ * plain oidset. It would be nice to refactor the filter code so this\n+ * helper can reuse it instead of duplicating the size check.\n+ */\n+int list_objects_filter__filter_oidset(struct repository *r,\n+\tstruct list_objects_filter_options *opts,\n+\tconst struct oidset *in,\n+\tstruct oidset *omitted)\n+{\n+\tstruct oidset_iter iter;\n+\tconst struct object_id *oid;\n+\n+\tif (opts->choice != LOFC_BLOB_LIMIT)\n+\t\treturn error(_(\"filter_oidset: only blob:limit filters are supported\"));\n+\n+\toidset_iter_init(in, &iter);\n+\twhile ((oid = oidset_iter_next(&iter))) {\n+\t\tstruct object_info info = OBJECT_INFO_INIT;\n+\t\tenum object_type type;\n+\t\tunsigned long size;\n+\n+\t\tinfo.typep = &type;\n+\t\tinfo.sizep = &size;\n+\n+\t\t/*\n+\t\t * Use OBJECT_INFO_SKIP_FETCH_OBJECT to avoid triggering\n+\t\t * a lazy fetch while inspecting candidates for removal.\n+\t\t */\n+\t\tif (odb_read_object_info_extended(r->objects, oid, &info,\n+\t\t\t\tOBJECT_INFO_SKIP_FETCH_OBJECT) < 0)\n+\t\t\tcontinue;\n+\n+\t\tif (type != OBJ_BLOB)\n+\t\t\tcontinue;\n+\n+\t\tif (size >= opts->blob_limit_value)\n+\t\t\toidset_insert(omitted, oid);\n+\t}\n+\treturn 0;\n+}\ndiff --git a/list-objects-filter.h b/list-objects-filter.h\nindex 9e98814111..56a2d87aa0 100644\n--- a/list-objects-filter.h\n+++ b/list-objects-filter.h\n@@ -94,4 +94,20 @@ enum list_objects_filter_result list_objects_filter__filter_object(\n  */\n void list_objects_filter__free(struct filter *filter);\n \n+/*\n+ * Given a set of OIDs in 'in', populate 'omitted' with those that\n+ * would be filtered by 'opts'. Currently only blob:limit=N is\n+ * supported. Objects that cannot be read are silently skipped.\n+ *\n+ * NEEDSWORK: this reimplements the blob:limit size check rather than\n+ * reusing the existing filter machinery. See the matching comment in\n+ * list-objects-filter.c.\n+ *\n+ * Return 0 on success, -1 if the filter is not supported.\n+ */\n+int list_objects_filter__filter_oidset(struct repository *r,\n+\tstruct list_objects_filter_options *opts,\n+\tconst struct oidset *in,\n+\tstruct oidset *omitted);\n+\n #endif /* LIST_OBJECTS_FILTER_H */\n-- \n2.54.0\n\n"},{"id":"550205","messageId":"20260810174047.6524-4-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260810174047.6524-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v4 3/7] repack-promisor: allow excluding objects from the rebuilt promisor pack","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-10T17:40:43Z","receivedAt":"2026-08-10T17:41:23Z","isPatch":true,"body":"Add a to_drop oidset parameter to repack_promisor_objects(). When it is\nnon-NULL, write_oid() omits those objects from the rebuilt promisor\npack. This is the mechanism --drop-filtered will use to remove promisor\nblobs, i.e. rebuild the promisor pack without them.\n\nAll existing callers pass NULL, so behavior is unchanged.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n builtin/repack.c  |  2 +-\n repack-promisor.c | 15 ++++++++++++++-\n repack.h          |  4 +++-\n 3 files changed, 18 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex 19b26ca723..e1c283e255 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -430,7 +430,7 @@ int cmd_repack(int argc,\n \t\tstrvec_push(&cmd.args, \"--delta-islands\");\n \n \tif (pack_everything & ALL_INTO_ONE) {\n-\t\trepack_promisor_objects(repo, &po_args, &names, packtmp);\n+\t\trepack_promisor_objects(repo, &po_args, &names, packtmp, NULL);\n \n \t\tif (existing_packs_has_non_kept(&existing) &&\n \t\t    delete_redundant &&\ndiff --git a/repack-promisor.c b/repack-promisor.c\nindex 90318ce150..fabfdc168a 100644\n--- a/repack-promisor.c\n+++ b/repack-promisor.c\n@@ -6,10 +6,12 @@\n #include \"path.h\"\n #include \"repository.h\"\n #include \"run-command.h\"\n+#include \"oidset.h\"\n \n struct write_oid_context {\n \tstruct child_process *cmd;\n \tconst struct git_hash_algo *algop;\n+\tconst struct oidset *to_drop;\n };\n \n /*\n@@ -23,6 +25,15 @@ static int write_oid(const struct object_id *oid,\n \tstruct write_oid_context *ctx = data;\n \tstruct child_process *cmd = ctx->cmd;\n \n+\t/*\n+\t * Objects in to_drop are being removed from the repository, so\n+\t * omit them from the rebuilt promisor pack. Each such object is a\n+\t * promisor object and therefore remains recoverable from the\n+\t * promisor remote.\n+\t */\n+\tif (ctx->to_drop && oidset_contains(ctx->to_drop, oid))\n+\t\treturn 0;\n+\n \tif (cmd->in == -1) {\n \t\tif (start_command(cmd))\n \t\t\tdie(_(\"could not start pack-objects to repack promisor objects\"));\n@@ -81,7 +92,8 @@ static void finish_repacking_promisor_objects(struct repository *repo,\n \n void repack_promisor_objects(struct repository *repo,\n \t\t\t     const struct pack_objects_args *args,\n-\t\t\t     struct string_list *names, const char *packtmp)\n+\t\t\t     struct string_list *names, const char *packtmp,\n+\t\t\t     const struct oidset *to_drop)\n {\n \tstruct write_oid_context ctx;\n \tstruct child_process cmd = CHILD_PROCESS_INIT;\n@@ -98,6 +110,7 @@ void repack_promisor_objects(struct repository *repo,\n \t */\n \tctx.cmd = &cmd;\n \tctx.algop = repo->hash_algo;\n+\tctx.to_drop = to_drop;\n \todb_for_each_object(repo->objects, NULL, write_oid, &ctx,\n \t\t\t    ODB_FOR_EACH_OBJECT_PROMISOR_ONLY);\n \ndiff --git a/repack.h b/repack.h\nindex f9fbc895f0..a5a3f7c6ba 100644\n--- a/repack.h\n+++ b/repack.h\n@@ -3,6 +3,7 @@\n \n #include \"list-objects-filter-options.h\"\n #include \"string-list.h\"\n+#include \"oidset.h\"\n \n struct pack_objects_args {\n \tchar *window;\n@@ -100,7 +101,8 @@ void generated_pack_install(struct generated_pack *pack, const char *name,\n \n void repack_promisor_objects(struct repository *repo,\n \t\t\t     const struct pack_objects_args *args,\n-\t\t\t     struct string_list *names, const char *packtmp);\n+\t\t\t     struct string_list *names, const char *packtmp,\n+\t\t\t     const struct oidset *to_drop);\n \n struct pack_geometry {\n \tstruct packed_git **pack;\n-- \n2.54.0\n\n"},{"id":"550206","messageId":"20260810174047.6524-5-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260810174047.6524-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v4 4/7] builtin/repack: enumerate promisor blobs for --drop-filtered","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-10T17:40:44Z","receivedAt":"2026-08-10T17:41:28Z","isPatch":true,"body":"Add enumeration logic for --drop-filtered. In --dry-run mode, print\nthe OIDs of locally-held promisor blobs that exceed the filter\nthreshold, as candidates for removal.\n\nReading from write_filtered_pack() cannot work for partial clones.\ngit repack routes promisor objects through a separate path:\nrepack_promisor_objects() repacks them first, and the main\npack-objects run uses --exclude-promisor-objects. By the time\nwrite_filtered_pack() runs, the promisor blobs are already consumed by\nthe main pack. The filtered pack is always empty on a partial clone.\n\nInstead, walk promisor objects directly via odb_for_each_object() with\nODB_FOR_EACH_OBJECT_PROMISOR_ONLY, collecting all promisor blobs into\nan oidset. The blobs exceeding the filter threshold are then selected\nusing list_objects_filter__filter_oidset().\n\nEvery object enumerated this way is a promisor object, so it is\nrecoverable from the promisor remote in the same sense as the rest of a\npartial clone, as long as the remote still has it. This holds without a\nseparate is_promisor_object() check. A future implementation can verify\navailability against the remote directly once a client-side\nremote-object-info query exists.\n\nOBJECT_INFO_SKIP_FETCH_OBJECT is passed to every object info query so\nenumeration never triggers a lazy fetch.\n\nThe enumeration collects candidates into a caller-provided oidset and\n--dry-run prints them. Actually removing the objects, together with the\nrequired promisor-remote verification, is written in a later commit.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n builtin/repack.c                | 20 +++++++-\n repack-filtered.c               | 81 +++++++++++++++++++++++++++++++\n repack.h                        |  4 ++\n t/t7706-repack-drop-filtered.sh | 84 ++++++++++++++++++++++++++++++++-\n 4 files changed, 187 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex e1c283e255..a47e64d91e 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -15,6 +15,8 @@\n #include \"repack.h\"\n #include \"shallow.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"oidset.h\"\n+#include \"hex.h\"\n \n #define ALL_INTO_ONE 1\n #define LOOSEN_UNREACHABLE 2\n@@ -160,6 +162,7 @@ int cmd_repack(int argc,\n \tstruct string_list_item *item;\n \tstruct string_list names = STRING_LIST_INIT_DUP;\n \tstruct existing_packs existing = EXISTING_PACKS_INIT;\n+\tstruct oidset drop_oids = OIDSET_INIT;\n \tstruct pack_geometry geometry = { 0 };\n \tstruct tempfile *refs_snapshot = NULL;\n \tint i, ret;\n@@ -318,6 +321,20 @@ int cmd_repack(int argc,\n \t\t\tdie(_(\"--drop-filtered requires a promisor remote\"));\n \n \t\twrite_bitmaps = 0;\n+\n+\t\tret = enumerate_promisor_blobs(repo, &po_args.filter_options, &drop_oids);\n+\n+\t\tif (ret)\n+\t\t\tgoto cleanup;\n+\n+\t\tif (dry_run) {\n+\t\t\tstruct oidset_iter iter;\n+\t\t\tconst struct object_id *oid;\n+\n+\t\t\toidset_iter_init(&drop_oids, &iter);\n+\t\t\twhile ((oid = oidset_iter_next(&iter)))\n+\t\t\t\tprintf(\"%s\\n\", oid_to_hex(oid));\n+\t\t}\n \t}\n \n \tif (delete_redundant && repo->repository_format_precious_objects)\n@@ -613,7 +630,7 @@ int cmd_repack(int argc,\n \t\t}\n \t}\n \n-\tif (po_args.filter_options.choice) {\n+\tif (po_args.filter_options.choice && !drop_filtered) {\n \t\tstruct write_pack_opts opts = {\n \t\t\t.po_args = &po_args,\n \t\t\t.destination = filter_to,\n@@ -706,6 +723,7 @@ int cmd_repack(int argc,\n cleanup:\n \tstring_list_clear(&keep_pack_list, 0);\n \tstring_list_clear(&names, 1);\n+\toidset_clear(&drop_oids);\n \texisting_packs_release(&existing);\n \tpack_geometry_release(&geometry);\n \tpack_objects_args_release(&po_args);\ndiff --git a/repack-filtered.c b/repack-filtered.c\nindex edcf7667c5..79ba6d90aa 100644\n--- a/repack-filtered.c\n+++ b/repack-filtered.c\n@@ -3,6 +3,12 @@\n #include \"repository.h\"\n #include \"run-command.h\"\n #include \"string-list.h\"\n+#include \"hex.h\"\n+#include \"packfile.h\"\n+#include \"list-objects-filter-options.h\"\n+#include \"list-objects-filter.h\"\n+#include \"odb.h\"\n+#include \"promisor-remote.h\"\n \n int write_filtered_pack(const struct write_pack_opts *opts,\n \t\t\tstruct existing_packs *existing,\n@@ -49,3 +55,78 @@ int write_filtered_pack(const struct write_pack_opts *opts,\n \treturn finish_pack_objects_cmd(existing->repo->hash_algo, opts, &cmd,\n \t\t\t\t       names);\n }\n+\n+struct collect_cb_data {\n+\tstruct repository *repo;\n+\tstruct oidset *set;\n+};\n+\n+static int collect_promisor_blob(const struct object_id *oid,\n+\t\t\t\t struct object_info *oi UNUSED,\n+\t\t\t\t void *cb_data)\n+{\n+\tstruct collect_cb_data *data = cb_data;\n+\tstruct object_info info = OBJECT_INFO_INIT;\n+\tenum object_type type;\n+\n+\tinfo.typep = &type;\n+\n+\t/*\n+\t * Use OBJECT_INFO_SKIP_FETCH_OBJECT to avoid triggering a\n+\t * lazy fetch while collecting promisor blobs.\n+\t */\n+\tif (odb_read_object_info_extended(data->repo->objects, oid, &info,\n+\t\t\tOBJECT_INFO_SKIP_FETCH_OBJECT) < 0)\n+\t\treturn 0;\n+\n+\tif (type == OBJ_BLOB)\n+\t\toidset_insert(data->set, oid);\n+\n+\treturn 0;\n+}\n+\n+int enumerate_promisor_blobs(struct repository *repo,\n+\t\t\t     const struct list_objects_filter_options *filter,\n+\t\t\t     struct oidset *to_drop)\n+{\n+\tstruct oidset all_promisor_blobs = OIDSET_INIT;\n+\tstruct collect_cb_data cb = {\n+\t\t.repo = repo,\n+\t\t.set = &all_promisor_blobs\n+\t};\n+\tint ret = 0;\n+\n+\t/*\n+\t * The caller (cmd_repack) is responsible for validating that a\n+\t * blob:limit filter and a promisor remote are present before\n+\t * calling this function.\n+\t *\n+\t * Walk only promisor objects. every object visited here is a\n+\t * promisor object, so it is recoverable from the promisor remote\n+\t * as long as the remote still has it, the same assumption the rest\n+\t * of partial clone relies on\n+\n+\t * We do not use write_filtered_pack() here because git repack\n+\t * routes promisor objects through repack_promisor_objects()\n+\t * before the filter machinery runs, so the filtered pack never\n+\t * contains promisor blobs. Direct enumeration via\n+\t * ODB_FOR_EACH_OBJECT_PROMISOR_ONLY is the correct approach.\n+\t */\n+\tret = odb_for_each_object(repo->objects, NULL,\n+\t\t\tcollect_promisor_blob, &cb,\n+\t\t\tODB_FOR_EACH_OBJECT_PROMISOR_ONLY);\n+\tif (ret)\n+\t\tgoto cleanup;\n+\n+\t/*\n+\t * Apply the filter to find which blobs exceed the threshold.\n+\t */\n+\tret = list_objects_filter__filter_oidset(repo,\n+\t\t(struct list_objects_filter_options *)filter,\n+\t\t&all_promisor_blobs,\n+\t\tto_drop);\n+\n+cleanup:\n+\toidset_clear(&all_promisor_blobs);\n+\treturn ret;\n+}\ndiff --git a/repack.h b/repack.h\nindex a5a3f7c6ba..61e554e4ed 100644\n--- a/repack.h\n+++ b/repack.h\n@@ -167,6 +167,10 @@ int write_filtered_pack(const struct write_pack_opts *opts,\n \t\t\tstruct existing_packs *existing,\n \t\t\tstruct string_list *names);\n \n+int enumerate_promisor_blobs(struct repository *repo,\n+\t\t\t     const struct list_objects_filter_options *filter,\n+\t\t\t     struct oidset *to_drop);\n+\n 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,\ndiff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh\nindex f27b09a30e..453053cc18 100755\n--- a/t/t7706-repack-drop-filtered.sh\n+++ b/t/t7706-repack-drop-filtered.sh\n@@ -1,9 +1,36 @@\n #!/bin/sh\n \n-test_description='git repack --drop-filtered option validation'\n+test_description='git repack --drop-filtered enumerates filtered promisor blobs'\n \n . ./test-lib.sh\n \n+delete_object () {\n+\tlocal repo=\"$1\" &&\n+\tlocal obj=\"$2\" &&\n+\tlocal path=\"$repo/.git/objects/$(test_oid_to_path \"$obj\")\" &&\n+\trm \"$path\"\n+}\n+\n+# pack the objects into a promisor pack inside \"repo\". it is a pack\n+# accompanied by an empty \".promisor\" marker file. objects\n+# in such a pack are treated as recoverable from the promisor remote.\n+pack_as_from_promisor () {\n+\tHASH=$(git -C repo pack-objects .git/objects/pack/pack) &&\n+\t>repo/.git/objects/pack/pack-$HASH.promisor &&\n+\techo $HASH\n+}\n+\n+# write a blob of $1 bytes into \"repo\", record it as coming from the\n+# promisor remote, and remove the loose copy so the object is only\n+# present in the promisor pack\n+promisor_blob () {\n+\ttest-tool genrandom \"$1\" \"$2\" >blob_content &&\n+\tOID=$(git -C repo hash-object -w --stdin <blob_content) &&\n+\tprintf \"%s\\n\" \"$OID\" | pack_as_from_promisor >/dev/null &&\n+\tdelete_object repo \"$OID\" &&\n+\techo \"$OID\"\n+}\n+\n # checks for options validations before any promisor walk\n test_expect_success 'setup plain repo for validation' '\n \tgit init plain &&\n@@ -52,4 +79,59 @@ test_expect_success '--drop-filtered fails without a promisor remote' '\n \ttest_grep \"drop-filtered requires a promisor remote\" err\n '\n \n+# enumeration tests using promisor pack\n+test_expect_success 'setup repo with a promisor remote' '\n+\trm -rf repo &&\n+\ttest_create_repo repo &&\n+\ttest_commit -C repo base &&\n+\n+\t# mark the repo as a partial clone with a promisor remote so the\n+\t# promisor walk and the safety guard are satisfied\n+\tgit -C repo config core.repositoryformatversion 1 &&\n+\tgit -C repo config extensions.partialclone origin &&\n+\tgit -C repo config remote.origin.promisor true &&\n+\tgit -C repo config remote.origin.url \".\" &&\n+\n+\tBIG=$(promisor_blob big 3072) &&\n+\tSMALL=$(promisor_blob small 512) &&\n+\techo \"$BIG\" >big_oid &&\n+\techo \"$SMALL\" >small_oid\n+'\n+\n+test_expect_success 'promisor blob over the threshold is listed' '\n+\tBIG=$(cat big_oid) &&\n+\tSMALL=$(cat small_oid) &&\n+\n+\tgit -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a >out &&\n+\n+\ttest_grep \"$BIG\" out &&\n+\ttest_grep ! \"$SMALL\" out\n+'\n+\n+test_expect_success 'locally created blob is never listed' '\n+\tBIG=$(cat big_oid) &&\n+\n+\t# large blob that exists only locally must never be a drop candidate.\n+\t# dropping it would be unrecoverable\n+\ttest-tool genrandom local 4096 >local_content &&\n+\tLOCAL=$(git -C repo hash-object -w --stdin <local_content) &&\n+\n+\tgit -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a >out &&\n+\n+\ttest_grep \"$BIG\" out &&\n+\ttest_grep ! \"$LOCAL\" out\n+'\n+\n+test_expect_success '--dry-run does not remove the filtered objects' '\n+\tBIG=$(cat big_oid) &&\n+\n+\tgit -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a >out &&\n+\n+\t# candidate blob must still be present after a dry run\n+\tgit -C repo cat-file -e \"$BIG\"\n+'\n+\n test_done\n-- \n2.54.0\n\n"},{"id":"550207","messageId":"20260810174047.6524-6-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260810174047.6524-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v4 5/7] builtin/repack: actually drop filtered promisor blobs","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-10T17:40:45Z","receivedAt":"2026-08-10T17:41:34Z","isPatch":true,"body":"Make --drop-filtered remove the enumerated promisor blobs instead of\nonly listing them.\n\nThe drop set is computed before repack_promisor_objects() runs, and on\na real run it is passed in so the rebuilt promisor pack omits those\nblobs. --drop-filtered implies -d so the old promisor packs, which\nstill contain the dropped blobs, are removed. Without this the blobs\nwould survive in the redundant packs. The existing repack machinery\nperforms the write-before-delete and fsync, so the drop is crash-safe.\n\nThe dropped blobs become absent locally but remain recoverable from the\npromisor remote, so a later access lazy-fetches them back\ntransparently. --dry-run keeps its previous behavior, i.e. it lists the\ncandidates and changes nothing.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n builtin/repack.c                | 14 ++++++++++----\n repack-filtered.c               |  1 +\n t/t7706-repack-drop-filtered.sh | 12 ++++++++++++\n 3 files changed, 23 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex a47e64d91e..e90016a33e 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -287,9 +287,6 @@ int cmd_repack(int argc,\n \t\tdie(_(\"--dry-run only takes effect with --drop-filtered\"));\n \n \tif (drop_filtered) {\n-\t\tif (!dry_run)\n-\t\t\tdie(_(\"--drop-filtered doesn't work without --dry-run yet\"));\n-\n \t\tif (!po_args.filter_options.choice)\n \t\t\tdie(_(\"--drop-filtered requires --filter\"));\n \n@@ -322,6 +319,14 @@ int cmd_repack(int argc,\n \n \t\twrite_bitmaps = 0;\n \n+\t\t/*\n+\t\t * Dropping objects means rebuilding the promisor packs\n+\t\t * without them and then removing the old packs, so the\n+\t\t * redundant packs must be deleted. Imply -d on a real run.\n+\t\t */\n+\t\tif (!dry_run)\n+\t\t\tdelete_redundant = 1;\n+\n \t\tret = enumerate_promisor_blobs(repo, &po_args.filter_options, &drop_oids);\n \n \t\tif (ret)\n@@ -447,7 +452,8 @@ int cmd_repack(int argc,\n \t\tstrvec_push(&cmd.args, \"--delta-islands\");\n \n \tif (pack_everything & ALL_INTO_ONE) {\n-\t\trepack_promisor_objects(repo, &po_args, &names, packtmp, NULL);\n+\t\trepack_promisor_objects(repo, &po_args, &names, packtmp,\n+\t\t\t(drop_filtered && !dry_run) ? &drop_oids : NULL);\n \n \t\tif (existing_packs_has_non_kept(&existing) &&\n \t\t    delete_redundant &&\ndiff --git a/repack-filtered.c b/repack-filtered.c\nindex 79ba6d90aa..e6c35c23de 100644\n--- a/repack-filtered.c\n+++ b/repack-filtered.c\n@@ -120,6 +120,7 @@ int enumerate_promisor_blobs(struct repository *repo,\n \n \t/*\n \t * Apply the filter to find which blobs exceed the threshold.\n+\t * The caller has to_drop and is responsible for clearing it.\n \t */\n \tret = list_objects_filter__filter_oidset(repo,\n \t\t(struct list_objects_filter_options *)filter,\ndiff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh\nindex 453053cc18..ba00239c9d 100755\n--- a/t/t7706-repack-drop-filtered.sh\n+++ b/t/t7706-repack-drop-filtered.sh\n@@ -134,4 +134,16 @@ test_expect_success '--dry-run does not remove the filtered objects' '\n \tgit -C repo cat-file -e \"$BIG\"\n '\n \n+test_expect_success '--drop-filtered removes the promisor blob locally' '\n+\tBIG=$(cat big_oid) &&\n+\tSMALL=$(cat small_oid) &&\n+\n+\tgit -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k -a &&\n+\n+\tgit -C repo cat-file --batch-all-objects --batch-check=\"%(objectname)\" >present &&\n+\ttest_grep ! \"$BIG\" present &&\n+\ttest_grep \"$SMALL\" present\n+'\n+\n test_done\n-- \n2.54.0\n\n"},{"id":"550208","messageId":"20260810174047.6524-7-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260810174047.6524-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v4 6/7] builtin/repack: add guards for --drop-filtered","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-10T17:40:46Z","receivedAt":"2026-08-10T17:41:40Z","isPatch":true,"body":"--drop-filtered removes local promisor blobs. That is only safe when the\nrepository is not mid-operation and when the blobs are not actively in\nuse, so add two guards, both skipped for bare repositories which have\nneither a worktree nor an index.\n\nFirst, refuse to run while a merge, rebase, am, cherry-pick, revert, or\nbisect is in progress. During these operations the working tree and\nindex are in an intermediate state, and rewriting packs and deleting\nobjects underneath a half-finished operation is unsafe.\n\nSecond, refuse to drop a blob that the current index references. Such a\nblob is needed by the working tree, so dropping it would only cause the\nnext command that touches the worktree to lazy-fetch it straight back,\nreclaiming nothing. The offending path is reported so the user can see\nwhy the drop was refused.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n builtin/repack.c                | 49 +++++++++++++++++++++++++++++++++\n t/t7706-repack-drop-filtered.sh | 36 ++++++++++++++++++++++++\n 2 files changed, 85 insertions(+)\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex e90016a33e..6ef4973038 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -17,6 +17,8 @@\n #include \"list-objects-filter-options.h\"\n #include \"oidset.h\"\n #include \"hex.h\"\n+#include \"wt-status.h\"\n+#include \"read-cache-ll.h\"\n \n #define ALL_INTO_ONE 1\n #define LOOSEN_UNREACHABLE 2\n@@ -317,6 +319,30 @@ int cmd_repack(int argc,\n \t\tif (!repo_has_promisor_remote(repo))\n \t\t\tdie(_(\"--drop-filtered requires a promisor remote\"));\n \n+\t\t/*\n+ \t\t * refuse to run while another operation is in progress. A\n+\t\t * dropped object would just be lazily re-fetched when the\n+\t\t * operation resumes, but triggering a network fetch in the\n+\t\t * middle of a half-finished\n+\t\t * merge/rebase/cherry-pick/revert/bisect is a poor\n+\t\t * experience, so this is a UX convenience rather than a\n+\t\t * safety measure. Bare repositories have no such state, so\n+\t\t * the check is skipped there.\n+\t\t */\n+\t\tif (!is_bare_repository(repo)) {\n+\t\t\tstruct wt_status_state state = { 0 };\n+\n+\t\t\twt_status_get_state(repo, &state, 0);\n+\t\t\tif (state.merge_in_progress || state.revert_in_progress ||\n+\t\t\t    state.rebase_in_progress ||state.bisect_in_progress ||\n+\t\t\t    state.cherry_pick_in_progress ||state.am_in_progress||\n+\t\t\t    state.rebase_interactive_in_progress) {\n+\t\t\t\twt_status_state_free_buffers(&state);\n+\t\t\t\tdie(_(\"--drop-filtered cannot be used while another operation is in progress\"));\n+\t\t\t}\n+\t\t\twt_status_state_free_buffers(&state);\n+\t\t}\n+\n \t\twrite_bitmaps = 0;\n \n \t\t/*\n@@ -332,6 +358,29 @@ int cmd_repack(int argc,\n \t\tif (ret)\n \t\t\tgoto cleanup;\n \n+\t\t/*\n+ \t\t * refuse to drop blobs that the current index references.\n+\t\t * such a blob would only be lazily re-fetched by the next\n+\t\t * command that touches the worktree, so dropping it reclaims\n+\t\t * nothing. This guard just avoids that churn. bare\n+\t\t * repositories have no index, so the check is skipped there.\n+\t\t */\n+\t\tif (!is_bare_repository(repo) && oidset_size(&drop_oids)) {\n+\t\t\tstruct index_state *istate = repo->index;\n+\t\t\tunsigned int i;\n+\n+\t\t\tif (repo_read_index(repo) < 0)\n+\t\t\t\tdie(_(\"could not read the index\"));\n+\n+\t\t\tfor (i = 0; i < istate->cache_nr; i++) {\n+\t\t\t\tconst struct cache_entry *ce = istate->cache[i];\n+\n+\t\t\t\tif (oidset_contains(&drop_oids, &ce->oid))\n+\t\t\t\t\tdie(_(\"cannot drop '%s' (%s): it is referenced by the current index\"),\n+\t\t\t\t\t\tce->name, oid_to_hex(&ce->oid));\n+\t\t\t}\n+\t\t}\n+\n \t\tif (dry_run) {\n \t\t\tstruct oidset_iter iter;\n \t\t\tconst struct object_id *oid;\ndiff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh\nindex ba00239c9d..05d58fa456 100755\n--- a/t/t7706-repack-drop-filtered.sh\n+++ b/t/t7706-repack-drop-filtered.sh\n@@ -146,4 +146,40 @@ test_expect_success '--drop-filtered removes the promisor blob locally' '\n \ttest_grep \"$SMALL\" present\n '\n \n+test_expect_success '--drop-filtered refuses when a merge is in progress' '\n+\ttest_when_finished \"git -C repo merge --abort || :\" &&\n+\n+\t# creat a conflicting merge so wt_status reports it\n+\tgit -C repo checkout -B mergebase base &&\n+\techo one >repo/conflict.txt &&\n+\tgit -C repo add conflict.txt &&\n+\tgit -C repo commit -m one &&\n+\n+\tgit -C repo checkout -B mergeother base &&\n+\techo two >repo/conflict.txt &&\n+\tgit -C repo add conflict.txt &&\n+\tgit -C repo commit -m two &&\n+\n+\ttest_must_fail git -C repo merge mergebase &&\n+\n+\ttest_must_fail git -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err &&\n+\ttest_grep \"in progress\" err\n+'\n+\n+\n+test_expect_success '--drop-filtered refuses to drop an index-referenced blob' '\n+\t# create a large blob, add it to the index and make it a promisor object\n+\t# so the index references it and enumeration picks it up\n+\ttest-tool genrandom idx 4096 >repo/tracked-big.bin &&\n+\tgit -C repo add tracked-big.bin &&\n+\tOID=$(git -C repo rev-parse :tracked-big.bin) &&\n+\tprintf \"%s\\n\" \"$OID\" | pack_as_from_promisor >/dev/null &&\n+\tdelete_object repo \"$OID\" &&\n+\n+\ttest_must_fail git -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err &&\n+\ttest_grep \"referenced by the current index\" err\n+'\n+\n test_done\n-- \n2.54.0\n\n"},{"id":"550209","messageId":"20260810174047.6524-8-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260810174047.6524-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v4 7/7] Documentation/git-repack: document --drop-filtered and --dry-run","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-10T17:40:47Z","receivedAt":"2026-08-10T17:41:46Z","isPatch":true,"body":"Describe the new --drop-filtered and --dry-run options: what they do,\nonly blob:limit filters are supported for now, a promisor remote is\nrequired, --drop-filtered requires -a and implies -d so the redundant\npacks are actually removed, its incompatibilities with --filter-to and\nbitmap writing, and the safety guards that refuse to run mid-operation\nor to drop index-referenced blobs.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n Documentation/git-repack.adoc | 37 +++++++++++++++++++++++++++++++++++\n 1 file changed, 37 insertions(+)\n\ndiff --git a/Documentation/git-repack.adoc b/Documentation/git-repack.adoc\nindex 72c42015e2..63943b078c 100644\n--- a/Documentation/git-repack.adoc\n+++ b/Documentation/git-repack.adoc\n@@ -12,6 +12,7 @@ SYNOPSIS\n 'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\n \t[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]\n \t[--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]\n+\t[--filter=<filter-spec>] [--drop-filtered [--dry-run]]\n \n DESCRIPTION\n -----------\n@@ -182,6 +183,42 @@ depth is 4095.\n \t`objects` and `objects/info/alternates` sections of\n \tlinkgit:gitrepository-layout[5].\n \n+--drop-filtered::\n+\tDelete the local objects that match the `--filter` specification\n+\tinstead of keeping them in a separate packfile, reclaiming the\n+\tdisk space they occupy. This is intended for partial clones,\n+\twhere the filtered objects are promisor objects that remain\n+\trecoverable from the promisor remote and are lazily re-fetched\n+\ton demand when they are next needed.\n++\n+Only large blobs are supported for now, so `--filter=blob:limit=<n>`\n+is currently the only accepted filter. Because dropped objects must be\n+recoverable, this option requires a promisor remote to be configured\n+and refuses to run otherwise.\n++\n+This option requires `-a`, and implies `-d`: the objects are dropped by\n+rebuilding the promisor pack without them and then removing the now\n+redundant old packs, so the redundant packs must be deleted for the\n+space to actually be reclaimed. It is incompatible with `--filter-to`\n+and with bitmap writing (`-b`/`--write-bitmap-index`), since filtering\n+breaks the single-pack closure that bitmaps require. A bitmap setting\n+coming from configuration is silently disabled for the duration of the\n+command.\n++\n+As a convenience since dropped objects remain recoverable by lazy fetch,\n+`--drop-filtered` refuses to run while another operation\n+(merge, rebase, am, cherry-pick, revert, or bisect) is in progress, to\n+avoid a surprising network fetch mid-operation, and refuses to drop any\n+blob that the current index references, since such a blob would only be\n+lazily re-fetched by the next command that inspects the working tree.\n+These checks are skipped in bare repositories, which have neither a\n+working tree nor an index.\n+\n+--dry-run::\n+\tOnly meaningful with `--drop-filtered`. List the objects that\n+\twould be dropped, one object ID per line, without rebuilding any\n+\tpack or deleting anything.\n+\n -b::\n --write-bitmap-index::\n \tWrite a reachability bitmap index as part of the repack. This\n-- \n2.54.0\n\n"},{"id":"550307","messageId":"xmqq7blw35su.fsf@gitster.g","threadId":"66019","inReplyTo":"20260810174047.6524-1-r.siddharth.shrimali@gmail.com","subject":"Re: [GSoC PATCH v4 0/7] repack: add --drop-filtered to reclaim space in partial clones","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-11T17:50:25Z","receivedAt":"2026-08-11T17:50:27Z","isPatch":true,"body":"Siddharth Shrimali <r.siddharth.shrimali@gmail.com> writes:\n\n> Changes since v3:\n>   * fixed the git repack -h usage string to include --drop-filtered and\n>     --dry-run, so it matches the SYNOPSIS in the documentation (t0450\n>     was failing - caught by Junio)\n>   * converted a bare grep in the test to test_grep (test-lint error -\n>     caught by Junio)\n>   * removed a stray bracket in the documentation SYNOPSIS\n\nThese three match what I locally fixed for the patches from the\nprevious iteration, so they are good.  I do not know if there needs\nother changes to the patches, though.\n\n> To do:\n>   * remote verification: verifying against the remote awaits the\n>     \"remote-object-info\" cat-file protocol command\n>   * recency: a \"don't cull recently-fetched objects\" rule as another\n>     selection criterion alongside size\n>   * drop log: introduce with the error-path change that reads it\n\nAre these \"planned for longer term, material for separate sets of\npatches to come on top, after this series graduates\"?  Or do you\nmean \"v5 and later rounds need to do these three things before the\nseries can be called complete\"?\n\nThanks.\n\n>\n> [1] https://lore.kernel.org/git/20260806112202.75067-1-r.siddharth.shrimali@gmail.com/\n>\n> Siddharth Shrimali (7):\n>   builtin/repack.c: add --drop-filtered and --dry-run options\n>   list-objects-filter: add list_objects_filter__filter_oidset()\n>   repack-promisor: allow excluding objects from the rebuilt promisor\n>     pack\n>   builtin/repack: enumerate promisor blobs for --drop-filtered\n>   builtin/repack: actually drop filtered promisor blobs\n>   builtin/repack: add guards for --drop-filtered\n>   Documentation/git-repack: document --drop-filtered and --dry-run\n>\n>  Documentation/git-repack.adoc   |  37 +++++++\n>  builtin/repack.c                | 151 +++++++++++++++++++++++++-\n>  list-objects-filter.c           |  45 ++++++++\n>  list-objects-filter.h           |  16 +++\n>  repack-filtered.c               |  82 ++++++++++++++\n>  repack-promisor.c               |  15 ++-\n>  repack.h                        |   8 +-\n>  t/meson.build                   |   1 +\n>  t/t7706-repack-drop-filtered.sh | 185 ++++++++++++++++++++++++++++++++\n>  9 files changed, 533 insertions(+), 7 deletions(-)\n>  create mode 100755 t/t7706-repack-drop-filtered.sh\n"},{"id":"550351","messageId":"CAGWgyh84QuzUxtCX1Z73uZT9RfamthfyuHYgvd8gX3MYGOm7Fg@mail.gmail.com","threadId":"66019","inReplyTo":"xmqq7blw35su.fsf@gitster.g","subject":"Re: [GSoC PATCH v4 0/7] repack: add --drop-filtered to reclaim space in partial clones","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-12T07:35:34Z","receivedAt":"2026-08-12T07:36:13Z","isPatch":true,"body":"Hi Junio, thanks for the review!\nOn Tue, 11 Aug 2026 at 23:20, Junio C Hamano <gitster@pobox.com> wrote:\n> Are these \"planned for longer term, material for separate sets of\n> patches to come on top, after this series graduates\"?  Or do you\n> mean \"v5 and later rounds need to do these three things before the\n> series can be called complete\"?\n\nAll of these are follow-up work meant to come on top, after this series\ngraduates, not prerequisites for it:\n\n1. remote verification depends on the \"remote-object-info\" cat-file\n   protocol command, which has landed recently, so a follow-up series\n   can build the remote-side verification on top of it with the other\n   future additions. This series deliberately uses the local promisor\n   check\n\n2. recency is an additional selection criterion. The feature is\n   complete and useful with size alone, and recency can be layered on\n   later without changing the design\n\n3. the drop log was intentionally dropped from this series and will\n   return together with the error-path change that actually reads it\n\nso I consider the core --drop-filtered functionality complete as scoped\nhere, with the above three as separate future series.\n\nThanks,\nSiddharth Shrimali\n"},{"id":"550429","messageId":"CAP8UFD0i6zo1pLLeKS4oGismNvadZ2Xc_QC1tt_9KuJiMJq40Q@mail.gmail.com","threadId":"66019","inReplyTo":"20260810174047.6524-3-r.siddharth.shrimali@gmail.com","subject":"Re: [GSoC PATCH v4 2/7] list-objects-filter: add list_objects_filter__filter_oidset()","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-12T17:09:11Z","receivedAt":"2026-08-12T17:09:25Z","isPatch":true,"body":"On Mon, Aug 10, 2026 at 7:41 PM Siddharth Shrimali\n<r.siddharth.shrimali@gmail.com> wrote:\n\n[...]\n\n> diff --git a/list-objects-filter.c b/list-objects-filter.c\n> index c912ff3079..6a2e9d5b24 100644\n> --- a/list-objects-filter.c\n> +++ b/list-objects-filter.c\n> @@ -828,3 +828,48 @@ void list_objects_filter__free(struct filter *filter)\n>         filter->free_fn(filter->filter_data);\n>         free(filter);\n>  }\n> +\n> +/*\n> + * NEEDSWORK: this reimplements the blob:limit size check rather than\n> + * reusing the existing filter machinery in\n> + * list_objects_filter__filter_object(). That machinery is currently\n> + * tied to the object-walk path and cannot easily be driven from a\n> + * plain oidset. It would be nice to refactor the filter code so this\n> + * helper can reuse it instead of duplicating the size check.\n> + */\n> +int list_objects_filter__filter_oidset(struct repository *r,\n> +       struct list_objects_filter_options *opts,\n\nI think this could be \"const\" like \"const struct\nlist_objects_filter_options *opts,\" which could avoid a cast in a\nfollowing patch...\n\n> +       const struct oidset *in,\n> +       struct oidset *omitted)\n> +{\n> +       struct oidset_iter iter;\n> +       const struct object_id *oid;\n> +\n> +       if (opts->choice != LOFC_BLOB_LIMIT)\n> +               return error(_(\"filter_oidset: only blob:limit filters are supported\"));\n> +\n> +       oidset_iter_init(in, &iter);\n> +       while ((oid = oidset_iter_next(&iter))) {\n> +               struct object_info info = OBJECT_INFO_INIT;\n> +               enum object_type type;\n> +               unsigned long size;\n> +\n> +               info.typep = &type;\n> +               info.sizep = &size;\n> +\n> +               /*\n> +                * Use OBJECT_INFO_SKIP_FETCH_OBJECT to avoid triggering\n> +                * a lazy fetch while inspecting candidates for removal.\n> +                */\n> +               if (odb_read_object_info_extended(r->objects, oid, &info,\n> +                               OBJECT_INFO_SKIP_FETCH_OBJECT) < 0)\n> +                       continue;\n> +\n> +               if (type != OBJ_BLOB)\n> +                       continue;\n> +\n> +               if (size >= opts->blob_limit_value)\n> +                       oidset_insert(omitted, oid);\n> +       }\n> +       return 0;\n> +}\n\n... as opts is only used to check `opts->choice != LOFC_BLOB_LIMIT`\nand `size >= opts->blob_limit_value`.\n"},{"id":"550431","messageId":"CAP8UFD3fF+Ka0Sqs3BSstur_HdqU3s==3ash91Ewwi_F-t2oDA@mail.gmail.com","threadId":"66019","inReplyTo":"20260810174047.6524-5-r.siddharth.shrimali@gmail.com","subject":"Re: [GSoC PATCH v4 4/7] builtin/repack: enumerate promisor blobs for --drop-filtered","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-12T17:21:35Z","receivedAt":"2026-08-12T17:21:49Z","isPatch":true,"body":"On Mon, Aug 10, 2026 at 7:41 PM Siddharth Shrimali\n<r.siddharth.shrimali@gmail.com> wrote:\n\n> +int enumerate_promisor_blobs(struct repository *repo,\n> +                            const struct list_objects_filter_options *filter,\n> +                            struct oidset *to_drop)\n> +{\n> +       struct oidset all_promisor_blobs = OIDSET_INIT;\n> +       struct collect_cb_data cb = {\n> +               .repo = repo,\n> +               .set = &all_promisor_blobs\n> +       };\n> +       int ret = 0;\n> +\n> +       /*\n> +        * The caller (cmd_repack) is responsible for validating that a\n> +        * blob:limit filter and a promisor remote are present before\n> +        * calling this function.\n> +        *\n> +        * Walk only promisor objects. every object visited here is a\n> +        * promisor object, so it is recoverable from the promisor remote\n> +        * as long as the remote still has it, the same assumption the rest\n> +        * of partial clone relies on\n> +\n\nThe sentence that ends with \"of partial clone relies on\" should be\nfinished with a full stop. And the next line should have a \"*\" as it\nis part of a code comment.\n\n> +        * We do not use write_filtered_pack() here because git repack\n> +        * routes promisor objects through repack_promisor_objects()\n> +        * before the filter machinery runs, so the filtered pack never\n> +        * contains promisor blobs. Direct enumeration via\n> +        * ODB_FOR_EACH_OBJECT_PROMISOR_ONLY is the correct approach.\n> +        */\n> +       ret = odb_for_each_object(repo->objects, NULL,\n> +                       collect_promisor_blob, &cb,\n> +                       ODB_FOR_EACH_OBJECT_PROMISOR_ONLY);\n> +       if (ret)\n> +               goto cleanup;\n> +\n> +       /*\n> +        * Apply the filter to find which blobs exceed the threshold.\n> +        */\n> +       ret = list_objects_filter__filter_oidset(repo,\n> +               (struct list_objects_filter_options *)filter,\n> +               &all_promisor_blobs,\n> +               to_drop);\n> +\n> +cleanup:\n> +       oidset_clear(&all_promisor_blobs);\n> +       return ret;\n> +}\n\n[...]\n\n> diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh\n> index f27b09a30e..453053cc18 100755\n> --- a/t/t7706-repack-drop-filtered.sh\n> +++ b/t/t7706-repack-drop-filtered.sh\n> @@ -1,9 +1,36 @@\n>  #!/bin/sh\n>\n> -test_description='git repack --drop-filtered option validation'\n> +test_description='git repack --drop-filtered enumerates filtered promisor blobs'\n>\n>  . ./test-lib.sh\n>\n> +delete_object () {\n> +       local repo=\"$1\" &&\n> +       local obj=\"$2\" &&\n> +       local path=\"$repo/.git/objects/$(test_oid_to_path \"$obj\")\" &&\n> +       rm \"$path\"\n> +}\n> +\n> +# pack the objects into a promisor pack inside \"repo\". it is a pack\n> +# accompanied by an empty \".promisor\" marker file. objects\n> +# in such a pack are treated as recoverable from the promisor remote.\n\nHere and in other places in this and other patches, code comments\nwould read better if they were proper sentences starting with an\nuppercase letter (and ending with a full stop). Like:\n\n# Pack the objects into a promisor pack inside \"repo\". It is a pack\n# accompanied by an empty \".promisor\" marker file. Objects\n# in such a pack are treated as recoverable from the promisor remote.\n\n> +pack_as_from_promisor () {\n> +       HASH=$(git -C repo pack-objects .git/objects/pack/pack) &&\n> +       >repo/.git/objects/pack/pack-$HASH.promisor &&\n> +       echo $HASH\n> +}\n"},{"id":"550435","messageId":"CAP8UFD2ii8C77jWpyHRYG=H7y4t-PFtR5FQB3on4bmB5FQyG4A@mail.gmail.com","threadId":"66019","inReplyTo":"20260810174047.6524-7-r.siddharth.shrimali@gmail.com","subject":"Re: [GSoC PATCH v4 6/7] builtin/repack: add guards for --drop-filtered","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-12T17:41:15Z","receivedAt":"2026-08-12T17:41:27Z","isPatch":true,"body":"On Mon, Aug 10, 2026 at 7:41 PM Siddharth Shrimali\n<r.siddharth.shrimali@gmail.com> wrote:\n\n[...]\n\n> @@ -317,6 +319,30 @@ int cmd_repack(int argc,\n>                 if (!repo_has_promisor_remote(repo))\n>                         die(_(\"--drop-filtered requires a promisor remote\"));\n>\n> +               /*\n> +                * refuse to run while another operation is in progress. A\n\ns/refuse/Refuse/\n\n> +                * dropped object would just be lazily re-fetched when the\n> +                * operation resumes, but triggering a network fetch in the\n> +                * middle of a half-finished\n> +                * merge/rebase/cherry-pick/revert/bisect is a poor\n> +                * experience, so this is a UX convenience rather than a\n> +                * safety measure. Bare repositories have no such state, so\n> +                * the check is skipped there.\n> +                */\n> +               if (!is_bare_repository(repo)) {\n> +                       struct wt_status_state state = { 0 };\n> +\n> +                       wt_status_get_state(repo, &state, 0);\n> +                       if (state.merge_in_progress || state.revert_in_progress ||\n> +                           state.rebase_in_progress ||state.bisect_in_progress ||\n> +                           state.cherry_pick_in_progress ||state.am_in_progress||\n> +                           state.rebase_interactive_in_progress) {\n> +                               wt_status_state_free_buffers(&state);\n> +                               die(_(\"--drop-filtered cannot be used while another operation is in progress\"));\n\nNit: I wonder if something like die_if_some_operation_in_progress()\nfrom builtin/checkout.c could be used to improve on the error message.\n\n> +                       }\n> +                       wt_status_state_free_buffers(&state);\n> +               }\n> +\n>                 write_bitmaps = 0;\n>\n>                 /*\n> @@ -332,6 +358,29 @@ int cmd_repack(int argc,\n>                 if (ret)\n>                         goto cleanup;\n>\n> +               /*\n> +                * refuse to drop blobs that the current index references.\n\ns/refuse/Refuse/\n\n> +                * such a blob would only be lazily re-fetched by the next\n> +                * command that touches the worktree, so dropping it reclaims\n> +                * nothing. This guard just avoids that churn. bare\n\ns/bare/Bare/\n\n> +                * repositories have no index, so the check is skipped there.\n> +                */\n> +               if (!is_bare_repository(repo) && oidset_size(&drop_oids)) {\n> +                       struct index_state *istate = repo->index;\n> +                       unsigned int i;\n> +\n> +                       if (repo_read_index(repo) < 0)\n> +                               die(_(\"could not read the index\"));\n> +\n> +                       for (i = 0; i < istate->cache_nr; i++) {\n> +                               const struct cache_entry *ce = istate->cache[i];\n> +\n> +                               if (oidset_contains(&drop_oids, &ce->oid))\n> +                                       die(_(\"cannot drop '%s' (%s): it is referenced by the current index\"),\n> +                                               ce->name, oid_to_hex(&ce->oid));\n> +                       }\n> +               }\n> +\n\n\n> diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh\n> index ba00239c9d..05d58fa456 100755\n> --- a/t/t7706-repack-drop-filtered.sh\n> +++ b/t/t7706-repack-drop-filtered.sh\n> @@ -146,4 +146,40 @@ test_expect_success '--drop-filtered removes the promisor blob locally' '\n>         test_grep \"$SMALL\" present\n>  '\n>\n> +test_expect_success '--drop-filtered refuses when a merge is in progress' '\n> +       test_when_finished \"git -C repo merge --abort || :\" &&\n> +\n> +       # creat a conflicting merge so wt_status reports it\n\ns/creat/Create/\n\n> +       git -C repo checkout -B mergebase base &&\n> +       echo one >repo/conflict.txt &&\n> +       git -C repo add conflict.txt &&\n> +       git -C repo commit -m one &&\n> +\n> +       git -C repo checkout -B mergeother base &&\n> +       echo two >repo/conflict.txt &&\n> +       git -C repo add conflict.txt &&\n> +       git -C repo commit -m two &&\n> +\n> +       test_must_fail git -C repo merge mergebase &&\n> +\n> +       test_must_fail git -C repo -c repack.writeBitmaps=false \\\n> +               repack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err &&\n> +       test_grep \"in progress\" err\n> +'\n"},{"id":"550436","messageId":"CAP8UFD3Zvm1rCy6iaviK4jjAXuL4Rkkr7yCTEv-pCdaJYgL_Nw@mail.gmail.com","threadId":"66019","inReplyTo":"20260810174047.6524-8-r.siddharth.shrimali@gmail.com","subject":"Re: [GSoC PATCH v4 7/7] Documentation/git-repack: document --drop-filtered and --dry-run","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-12T17:51:14Z","receivedAt":"2026-08-12T17:51:28Z","isPatch":true,"body":"On Mon, Aug 10, 2026 at 7:41 PM Siddharth Shrimali\n<r.siddharth.shrimali@gmail.com> wrote:\n\n>  Documentation/git-repack.adoc | 37 +++++++++++++++++++++++++++++++++++\n>  1 file changed, 37 insertions(+)\n>\n> diff --git a/Documentation/git-repack.adoc b/Documentation/git-repack.adoc\n> index 72c42015e2..63943b078c 100644\n> --- a/Documentation/git-repack.adoc\n> +++ b/Documentation/git-repack.adoc\n> @@ -12,6 +12,7 @@ SYNOPSIS\n>  'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\n>         [--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]\n>         [--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]\n> +       [--filter=<filter-spec>] [--drop-filtered [--dry-run]]\n>\n>  DESCRIPTION\n>  -----------\n> @@ -182,6 +183,42 @@ depth is 4095.\n>         `objects` and `objects/info/alternates` sections of\n>         linkgit:gitrepository-layout[5].\n>\n> +--drop-filtered::\n> +       Delete the local objects that match the `--filter` specification\n> +       instead of keeping them in a separate packfile, reclaiming the\n> +       disk space they occupy. This is intended for partial clones,\n> +       where the filtered objects are promisor objects that remain\n> +       recoverable from the promisor remote and are lazily re-fetched\n> +       on demand when they are next needed.\n> ++\n> +Only large blobs are supported for now, so `--filter=blob:limit=<n>`\n> +is currently the only accepted filter. Because dropped objects must be\n> +recoverable, this option requires a promisor remote to be configured\n> +and refuses to run otherwise.\n> ++\n> +This option requires `-a`, and implies `-d`: the objects are dropped by\n> +rebuilding the promisor pack without them and then removing the now\n> +redundant old packs, so the redundant packs must be deleted for the\n> +space to actually be reclaimed. It is incompatible with `--filter-to`\n> +and with bitmap writing (`-b`/`--write-bitmap-index`), since filtering\n> +breaks the single-pack closure that bitmaps require. A bitmap setting\n> +coming from configuration is silently disabled for the duration of the\n> +command.\n> ++\n> +As a convenience since dropped objects remain recoverable by lazy fetch,\n> +`--drop-filtered` refuses to run while another operation\n> +(merge, rebase, am, cherry-pick, revert, or bisect) is in progress, to\n> +avoid a surprising network fetch mid-operation, and refuses to drop any\n> +blob that the current index references, since such a blob would only be\n> +lazily re-fetched by the next command that inspects the working tree.\n> +These checks are skipped in bare repositories, which have neither a\n> +working tree nor an index.\n\nI think this patch could be squashed into patch 5/7 (\"builtin/repack:\nactually drop filtered promisor blobs\") except the above paragraph\nwhich could be added by patch 6/7 (\"builtin/repack: add guards for\n--drop-filtered\").\n\nThanks.\n"},{"id":"550438","messageId":"xmqq1pc3xlju.fsf@gitster.g","threadId":"66019","inReplyTo":"20260810174047.6524-3-r.siddharth.shrimali@gmail.com","subject":"Re: [GSoC PATCH v4 2/7] list-objects-filter: add list_objects_filter__filter_oidset()","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-12T18:04:21Z","receivedAt":"2026-08-12T18:04:25Z","isPatch":true,"body":"Siddharth Shrimali <r.siddharth.shrimali@gmail.com> writes:\n\n> diff --git a/list-objects-filter.c b/list-objects-filter.c\n> index c912ff3079..6a2e9d5b24 100644\n> --- a/list-objects-filter.c\n> +++ b/list-objects-filter.c\n> @@ -828,3 +828,48 @@ void list_objects_filter__free(struct filter *filter)\n> ...\n> +\twhile ((oid = oidset_iter_next(&iter))) {\n> +\t\tstruct object_info info = OBJECT_INFO_INIT;\n> +\t\tenum object_type type;\n> +\t\tunsigned long size;\n> +\n> +\t\tinfo.typep = &type;\n> +\t\tinfo.sizep = &size;\n\nHere \"struct object_info\" is defined as\n\n        struct object_info {\n                /* The object type. */\n                enum object_type *typep;\n\n                /* The inflated object size in bytes. */\n                size_t *sizep;\n                ...\n\nbut local \"size\" whose address is assigned to is of type \"unsigned\nlong\".\n"},{"id":"550443","messageId":"CAGWgyh9bJ=BSq1B8LSEaQth_4fQHB_YHDgHmf5m5MYcENAkxrw@mail.gmail.com","threadId":"66019","inReplyTo":"xmqq1pc3xlju.fsf@gitster.g","subject":"Re: [GSoC PATCH v4 2/7] list-objects-filter: add list_objects_filter__filter_oidset()","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-12T19:22:19Z","receivedAt":"2026-08-12T19:22:56Z","isPatch":true,"body":"On Wed, 12 Aug 2026 at 23:34, Junio C Hamano <gitster@pobox.com> wrote:\n> Here \"struct object_info\" is defined as\n>\n>         struct object_info {\n>                 /* The object type. */\n>                 enum object_type *typep;\n>\n>                 /* The inflated object size in bytes. */\n>                 size_t *sizep;\n>                 ...\n>\n> but local \"size\" whose address is assigned to is of type \"unsigned\n> long\".\n\nOh right, thanks for catching it!\nI used \"unsigned long\" because that is the type the\nolder object-size interfaces use(odb_read_object()), so I copied that\npattern without noticing that object_info.sizep is \"size_t *\".\n\nI will change the local to \"size_t\" in v5 so it matches sizep.\n\nThanks,\nSiddharth Shrimali\n"},{"id":"550446","messageId":"CAGWgyh8Azg1SKyPGtjai-FLbV5Ngn+dC3kxZgU+0FYFdDoOEVw@mail.gmail.com","threadId":"66019","inReplyTo":"CAP8UFD2ii8C77jWpyHRYG=H7y4t-PFtR5FQB3on4bmB5FQyG4A@mail.gmail.com","subject":"Re: [GSoC PATCH v4 6/7] builtin/repack: add guards for --drop-filtered","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-12T19:45:30Z","receivedAt":"2026-08-12T19:46:08Z","isPatch":true,"body":"Hey Christian,\nOn Wed, 12 Aug 2026 at 23:11, Christian Couder\n<christian.couder@gmail.com> wrote:\n>\n> On Mon, Aug 10, 2026 at 7:41 PM Siddharth Shrimali\n> <r.siddharth.shrimali@gmail.com> wrote:\n> > +                               die(_(\"--drop-filtered cannot be used while another operation is in progress\"));\n>\n> Nit: I wonder if something like die_if_some_operation_in_progress()\n> from builtin/checkout.c could be used to improve on the error message.\n\ndie_if_some_operation_in_progress() does check the set of in-progress states,\nso it is a good fit conceptually, but it is currently \"static\" in\nbuiltin/checkout.c\nand uses the_repository, whereas the repack guard works on an explicit\n\"repo\".\nIf we want to use it, we could extract it to a shared location(wt-status.c),\nmake it non-static, and take a \"struct repository *\", which does not feel\nlike a part of this series.\n\nSo for v5, I will keep the explicit repo-based check but improve the error\nmessage, and we could do the extraction as a separate follow-up by first\ncreating a new patch, and then use it with the othr future changes, if\nthat sounds\ngood :)\n\nThanks,\nSiddharth Shrimali\n"},{"id":"550567","messageId":"20260813200830.84348-1-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260810174047.6524-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v5 0/6] repack: add --drop-filtered to reclaim space in partial clones","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-13T20:08:24Z","receivedAt":"2026-08-13T20:08:51Z","isPatch":true,"body":"This is v5. v4 was at [1].\n\nPartial clones let you work with large repositories without downloading\nevery blob up front. Missing blobs are lazily fetched from the promisor\nremote on demand. Over time these accumulate locally and there is\ncurrently no safe, built-in way to reclaim that space short of\nre-cloning. This series adds that reverse direction: enumerate promisor\nblobs over a size threshold, drop them locally, and rely on the existing\nlazy-fetch machinery to bring them back transparently when needed.\n\nHow it works:\n  * Enumerate promisor objects directly (ODB_FOR_EACH_OBJECT_PROMISOR_ONLY)\n    and select the blobs exceeding the filter threshold. Every enumerated\n    object is a promisor object, so it is recoverable from the promisor\n    remote as long as the remote still has it, the same assumption the\n    rest of partial clone relies on.\n  * Rebuild the promisor pack without the selected blobs, reusing the\n    existing repack machinery, so the drop is crash-safe (write, fsync,\n    install, then delete the old pack).\n  * --dry-run lists the candidates and changes nothing.\n\nThe guards refuse to run while a merge, rebase, am, cherry-pick, revert,\nor bisect is in progress, and refuse to drop a blob referenced by the\ncurrent index. Dropped objects stay recoverable via lazy fetch, so these\nare a convenience (avoid pointless re-fetch and a surprising\nmid-operation fetch) rather than a correctness measure. Both are skipped\nfor bare repositories.\n\nChanges since v4:\n  * list-objects-filter: fixed the object-size local from \"unsigned\n    long\" to \"size_t\" to match object_info.sizep - caught by Junio\n  * list-objects-filter: made the filter options parameter const and\n    dropped the now-unnecessary cast at the call site - caught by Christian\n  * repack-filtered: comment fixes\n  * squashed the documentation into the patches it describes: the option\n    description now lands with \"actually drop filtered promisor blobs\"\n    and the guards paragraph with \"add guards for --drop-filtered\",\n    dropping the separate documentation patch, so the series is now 6\n    patches - Suggested by Christian\n  * improved the \"operation in progress\" error message to name the\n    operations (merge, rebase, am, cherry-pick, revert, or bisect)\n  * swept test comments to be proper sentences and fixed tab or space\n    indentation\n\nTo do:\n  * remote verification: verifying against the remote awaits the\n    \"remote-object-info\" cat-file protocol command\n  * recency: a \"don't cull recently-fetched objects\" rule as another\n    selection criterion alongside size\n  * drop log: introduce with the error-path change that reads it\n\n[1] https://lore.kernel.org/git/20260810174047.6524-1-r.siddharth.shrimali@gmail.com/\n\nSiddharth Shrimali (6):\n  builtin/repack: add --drop-filtered and --dry-run options\n  list-objects-filter: add list_objects_filter__filter_oidset()\n  repack-promisor: allow excluding objects from the rebuilt promisor\n    pack\n  builtin/repack: enumerate promisor blobs for --drop-filtered\n  builtin/repack: actually drop filtered promisor blobs\n  builtin/repack: add guards for --drop-filtered\n\n Documentation/git-repack.adoc   |  37 +++++++\n builtin/repack.c                | 154 +++++++++++++++++++++++++-\n list-objects-filter.c           |  45 ++++++++\n list-objects-filter.h           |  16 +++\n repack-filtered.c               |  82 ++++++++++++++\n repack-promisor.c               |  15 ++-\n repack.h                        |   8 +-\n t/meson.build                   |   1 +\n t/t7706-repack-drop-filtered.sh | 185 ++++++++++++++++++++++++++++++++\n 9 files changed, 536 insertions(+), 7 deletions(-)\n create mode 100755 t/t7706-repack-drop-filtered.sh\n\n-- \n2.54.0\n\n"},{"id":"550568","messageId":"20260813200830.84348-2-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260813200830.84348-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v5 1/6] builtin/repack: add --drop-filtered and --dry-run options","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-13T20:08:25Z","receivedAt":"2026-08-13T20:08:55Z","isPatch":true,"body":"Add two new command-line options to 'git-repack':\n\n  --drop-filtered: intended to eventually delete objects that match\n                   the filter specification. Requires --filter and -a,\n                   and is incompatible with --filter-to.\n  --dry-run: show which objects would be dropped without making any\n             changes. Only meaningful with --drop-filtered.\n\nKeep --dry-run as a separate option rather than folding it into\n--drop-filtered (e.g. --drop-filtered=dry-run), to stay consistent with\nthe --dry-run option other Git commands already provide and to leave\nroom for it to describe other repack behavior later. A\n--drop-filtered=<mode> form can still be added later if more\ndrop-specific modes are needed.\n\n--drop-filtered also requires a promisor remote to be configured, since\ndropping objects without a remote to fetch them back from would be\npermanent data loss.\n\n--drop-filtered is incompatible with bitmap writing: filtering breaks\nthe \"all objects in one pack\" closure that bitmaps require. Detect an\nexplicit -b/--write-bitmap-index on the command line with a dedicated\noption callback that sets a \"write_bitmaps_given\" flag, so it can be\ndistinguished from a repack.writeBitmaps configuration value even when\nconfig already enables bitmaps. An explicit -b is reported as a conflict,\nwhile a config-provided default is silently disabled for the duration\nof the command.\n\nThese options currently only perform validation. The actual enumeration\nand deletion will be added in follow-up commits.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n builtin/repack.c                | 74 +++++++++++++++++++++++++++++++--\n t/meson.build                   |  1 +\n t/t7706-repack-drop-filtered.sh | 55 ++++++++++++++++++++++++\n 3 files changed, 127 insertions(+), 3 deletions(-)\n create mode 100755 t/t7706-repack-drop-filtered.sh\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex db504d673f..ed79c04e13 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -14,6 +14,7 @@\n #include \"promisor-remote.h\"\n #include \"repack.h\"\n #include \"shallow.h\"\n+#include \"list-objects-filter-options.h\"\n \n #define ALL_INTO_ONE 1\n #define LOOSEN_UNREACHABLE 2\n@@ -28,11 +29,15 @@ static int use_delta_islands;\n static int run_update_server_info = 1;\n static char *packdir, *packtmp_name, *packtmp;\n static int midx_must_contain_cruft = 1;\n+static int drop_filtered;\n+static int dry_run;\n+static int write_bitmaps_given;\n \n static const char *const git_repack_usage[] = {\n \tN_(\"git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\\n\"\n \t   \"[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]\\n\"\n-\t   \"[--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]\"),\n+\t   \"[--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]\\n\"\n+\t   \"[--filter=<filter-spec>] [--drop-filtered [--dry-run]]\"),\n \tNULL\n };\n \n@@ -111,6 +116,21 @@ static int repack_config(const char *var, const char *value,\n \treturn git_default_config(var, value, ctx, cb);\n }\n \n+static int option_parse_write_bitmaps(const struct option *opt, const char *arg,\n+\t\t\t\t      int unset)\n+{\n+\tint *value = opt->value;\n+\n+\tBUG_ON_OPT_ARG(arg);\n+\tif (unset)\n+\t\t*value = 0;\n+\telse\n+\t\t*value = 1;\n+\n+\twrite_bitmaps_given = 1;\n+\treturn 0;\n+}\n+\n static int option_parse_write_midx(const struct option *opt, const char *arg,\n \t\t\t\t   int unset)\n {\n@@ -194,8 +214,9 @@ int cmd_repack(int argc,\n \t\tOPT__QUIET(&po_args.quiet, N_(\"be quiet\")),\n \t\tOPT_BOOL('l', \"local\", &po_args.local,\n \t\t\t\tN_(\"pass --local to git-pack-objects\")),\n-\t\tOPT_BOOL('b', \"write-bitmap-index\", &write_bitmaps,\n-\t\t\t\tN_(\"write bitmap index\")),\n+\t\tOPT_CALLBACK_F('b', \"write-bitmap-index\", &write_bitmaps, NULL,\n+\t\t\t\tN_(\"write bitmap index\"),\n+\t\t\t\tPARSE_OPT_NOARG, option_parse_write_bitmaps),\n \t\tOPT_BOOL('i', \"delta-islands\", &use_delta_islands,\n \t\t\t\tN_(\"pass --delta-islands to git-pack-objects\")),\n \t\tOPT_STRING(0, \"unpack-unreachable\", &unpack_unreachable, N_(\"approxidate\"),\n@@ -231,6 +252,10 @@ int cmd_repack(int argc,\n \t\t\t   N_(\"pack prefix to store a pack containing pruned objects\")),\n \t\tOPT_STRING(0, \"filter-to\", &filter_to, N_(\"dir\"),\n \t\t\t   N_(\"pack prefix to store a pack containing filtered out objects\")),\n+\t\tOPT_BOOL(0, \"drop-filtered\", &drop_filtered,\n+\t\t\t\tN_(\"delete filtered out objects (requires --filter)\")),\n+\t\tOPT_BOOL(0, \"dry-run\", &dry_run,\n+\t\t\t\tN_(\"only show which objects would be dropped\")),\n \t\tOPT_END()\n \t};\n \n@@ -252,6 +277,49 @@ int cmd_repack(int argc,\n \tpo_args.depth = xstrdup_or_null(opt_depth);\n \tpo_args.threads = xstrdup_or_null(opt_threads);\n \n+\tdie_for_incompatible_opt2(drop_filtered, \"--drop-filtered\",\n+\t\t!!filter_to, \"--filter-to\");\n+\n+\tif (dry_run && !drop_filtered)\n+\t\tdie(_(\"--dry-run only takes effect with --drop-filtered\"));\n+\n+\tif (drop_filtered) {\n+\t\tif (!dry_run)\n+\t\t\tdie(_(\"--drop-filtered does not work without --dry-run yet\"));\n+\n+\t\tif (!po_args.filter_options.choice)\n+\t\t\tdie(_(\"--drop-filtered requires --filter\"));\n+\n+\t\tif (!(pack_everything & ALL_INTO_ONE))\n+\t\t\tdie(_(\"--drop-filtered requires -a\"));\n+\n+\t\t/*\n+\t\t * Only blob:limit=<n> is supported for now. Reject other\n+\t\t * filter choices early, before walking the object database.\n+\t\t */\n+\t\tif (po_args.filter_options.choice != LOFC_BLOB_LIMIT)\n+\t\t\tdie(_(\"--drop-filtered only supports --filter=blob:limit=<n> for now\"));\n+\n+\t\t/*\n+\t\t * An explicit -b on the command line is a conflict we have to\n+\t\t * report; a bitmap setting from config is silently overridden\n+\t\t * for the duration of the command.\n+\t\t */\n+\t\tif (write_bitmaps_given && write_bitmaps > 0)\n+\t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"),\n+\t\t\t\t\"--drop-filtered\", \"--write-bitmap-index\");\n+\n+\t\t/*\n+\t\t * Without a promisor remote there is nowhere to re-fetch the\n+\t\t * dropped objects from, so dropping them would be permanent\n+\t\t * data loss.\n+\t\t */\n+\t\tif (!repo_has_promisor_remote(repo))\n+\t\t\tdie(_(\"--drop-filtered requires a promisor remote\"));\n+\n+\t\twrite_bitmaps = 0;\n+\t}\n+\n \tif (delete_redundant && repo->repository_format_precious_objects)\n \t\tdie(_(\"cannot delete packs in a precious-objects repo\"));\n \ndiff --git a/t/meson.build b/t/meson.build\nindex a25f37d2f5..92352e43c4 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -964,6 +964,7 @@ integration_tests = [\n   't7703-repack-geometric.sh',\n   't7704-repack-cruft.sh',\n   't7705-repack-incremental-midx.sh',\n+  't7706-repack-drop-filtered.sh',\n   't7800-difftool.sh',\n   't7810-grep.sh',\n   't7811-grep-open.sh',\ndiff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh\nnew file mode 100755\nindex 0000000000..07a976874a\n--- /dev/null\n+++ b/t/t7706-repack-drop-filtered.sh\n@@ -0,0 +1,55 @@\n+#!/bin/sh\n+\n+test_description='git repack --drop-filtered option validation'\n+\n+. ./test-lib.sh\n+\n+# Check option validation before any promisor walk\n+test_expect_success 'setup plain repo for validation' '\n+\tgit init plain &&\n+\ttest_commit -C plain initial &&\n+\tgit clone --bare plain plain.git &&\n+\tgit -C plain.git repack -a -d\n+'\n+\n+test_expect_success '--drop-filtered requires --filter' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered --dry-run -a 2>err &&\n+\ttest_grep \"drop-filtered requires --filter\" err\n+'\n+\n+test_expect_success '--drop-filtered cannot be used with --filter-to' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered \\\n+\t\t--filter=blob:limit=1k --filter-to=./filter-out 2>err &&\n+\ttest_grep \"options .--drop-filtered. and .--filter-to. cannot be used together\" err\n+'\n+\n+test_expect_success '--dry-run only takes effect with --drop-filtered' '\n+\ttest_must_fail git -C plain.git repack --dry-run 2>err &&\n+\ttest_grep \"dry-run only takes effect with --drop-filtered\" err\n+'\n+\n+test_expect_success '--drop-filtered requires -a' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered \\\n+\t\t--filter=blob:limit=1k --dry-run 2>err &&\n+\ttest_grep \"drop-filtered requires -a\" err\n+'\n+\n+test_expect_success '--drop-filtered fails with --write-bitmap-index' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered \\\n+\t\t--filter=blob:limit=1k --dry-run -a -b 2>err &&\n+\ttest_grep \"options .--drop-filtered. and .--write-bitmap-index. cannot be used together\" err\n+'\n+\n+test_expect_success '--drop-filtered rejects explicit -b even when repack.writeBitmaps=true' '\n+\ttest_must_fail git -C plain.git -c repack.writeBitmaps=true \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a -b 2>err &&\n+\ttest_grep \"options .--drop-filtered. and .--write-bitmap-index. cannot be used together\" err\n+'\n+\n+test_expect_success '--drop-filtered fails without a promisor remote' '\n+\ttest_must_fail git -C plain.git repack --drop-filtered \\\n+\t\t--filter=blob:limit=1k --dry-run -a 2>err &&\n+\ttest_grep \"drop-filtered requires a promisor remote\" err\n+'\n+\n+test_done\n-- \n2.54.0\n\n"},{"id":"550569","messageId":"20260813200830.84348-3-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260813200830.84348-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v5 2/6] list-objects-filter: add list_objects_filter__filter_oidset()","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-13T20:08:26Z","receivedAt":"2026-08-13T20:09:01Z","isPatch":true,"body":"The existing filter entry point, list_objects_filter__filter_object(),\nis built around the object-walk path: it expects traversal context and\nprovisional omit sets, and is meant to be called as objects are\nvisited during a walk. A caller that already has a set of OIDs in hand\nand only wants to know which ones a filter would select has no usable\nentry point into the filter API.\n\n--drop-filtered is exactly such a caller: it collects promisor blobs\ninto an oidset and needs to know which of them exceed the filter\nthreshold, without performing an object walk.\n\nAdd a helper, list_objects_filter__filter_oidset(), that takes a set\nof OIDs and populates an \"omitted\" set with those that would be\nfiltered out by the given filter options. Only blob:limit=N filters\nare supported for now.\n\nThis helper does not actually reuse the existing filter machinery.\nIt reimplements the blob:limit size check directly. That machinery\nis tied to the object-walk path and cannot easily be driven\nfrom a plain oidset. A NEEDSWORK comment marks this so the helper can\nlater be refactored to reuse the real filter logic instead of\nduplicating it.\n\nOBJECT_INFO_SKIP_FETCH_OBJECT is passed when reading object info so\nthe helper never triggers a lazy fetch.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n list-objects-filter.c | 45 +++++++++++++++++++++++++++++++++++++++++++\n list-objects-filter.h | 16 +++++++++++++++\n 2 files changed, 61 insertions(+)\n\ndiff --git a/list-objects-filter.c b/list-objects-filter.c\nindex c912ff3079..10e44f1357 100644\n--- a/list-objects-filter.c\n+++ b/list-objects-filter.c\n@@ -828,3 +828,48 @@ void list_objects_filter__free(struct filter *filter)\n \tfilter->free_fn(filter->filter_data);\n \tfree(filter);\n }\n+\n+/*\n+ * NEEDSWORK: this reimplements the blob:limit size check rather than\n+ * reusing the existing filter machinery in\n+ * list_objects_filter__filter_object(). That machinery is currently\n+ * tied to the object-walk path and cannot easily be driven from a\n+ * plain oidset. It would be nice to refactor the filter code so this\n+ * helper can reuse it instead of duplicating the size check.\n+ */\n+int list_objects_filter__filter_oidset(struct repository *r,\n+\tconst struct list_objects_filter_options *opts,\n+\tconst struct oidset *in,\n+\tstruct oidset *omitted)\n+{\n+\tstruct oidset_iter iter;\n+\tconst struct object_id *oid;\n+\n+\tif (opts->choice != LOFC_BLOB_LIMIT)\n+\t\treturn error(_(\"filter_oidset: only blob:limit filters are supported\"));\n+\n+\toidset_iter_init(in, &iter);\n+\twhile ((oid = oidset_iter_next(&iter))) {\n+\t\tstruct object_info info = OBJECT_INFO_INIT;\n+\t\tenum object_type type;\n+\t\tsize_t size;\n+\n+\t\tinfo.typep = &type;\n+\t\tinfo.sizep = &size;\n+\n+\t\t/*\n+\t\t * Use OBJECT_INFO_SKIP_FETCH_OBJECT to avoid triggering\n+\t\t * a lazy fetch while inspecting candidates for removal.\n+\t\t */\n+\t\tif (odb_read_object_info_extended(r->objects, oid, &info,\n+\t\t\t\tOBJECT_INFO_SKIP_FETCH_OBJECT) < 0)\n+\t\t\tcontinue;\n+\n+\t\tif (type != OBJ_BLOB)\n+\t\t\tcontinue;\n+\n+\t\tif (size >= opts->blob_limit_value)\n+\t\t\toidset_insert(omitted, oid);\n+\t}\n+\treturn 0;\n+}\ndiff --git a/list-objects-filter.h b/list-objects-filter.h\nindex 9e98814111..5207ab70a9 100644\n--- a/list-objects-filter.h\n+++ b/list-objects-filter.h\n@@ -94,4 +94,20 @@ enum list_objects_filter_result list_objects_filter__filter_object(\n  */\n void list_objects_filter__free(struct filter *filter);\n \n+/*\n+ * Given a set of OIDs in 'in', populate 'omitted' with those that\n+ * would be filtered by 'opts'. Currently only blob:limit=N is\n+ * supported. Objects that cannot be read are silently skipped.\n+ *\n+ * NEEDSWORK: this reimplements the blob:limit size check rather than\n+ * reusing the existing filter machinery. See the matching comment in\n+ * list-objects-filter.c.\n+ *\n+ * Return 0 on success, -1 if the filter is not supported.\n+ */\n+int list_objects_filter__filter_oidset(struct repository *r,\n+\tconst struct list_objects_filter_options *opts,\n+\tconst struct oidset *in,\n+\tstruct oidset *omitted);\n+\n #endif /* LIST_OBJECTS_FILTER_H */\n-- \n2.54.0\n\n"},{"id":"550570","messageId":"20260813200830.84348-4-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260813200830.84348-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v5 3/6] repack-promisor: allow excluding objects from the rebuilt promisor pack","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-13T20:08:27Z","receivedAt":"2026-08-13T20:09:06Z","isPatch":true,"body":"Add a to_drop oidset parameter to repack_promisor_objects(). When it is\nnon-NULL, write_oid() omits those objects from the rebuilt promisor\npack. This is the mechanism --drop-filtered will use to remove promisor\nblobs, i.e. rebuild the promisor pack without them.\n\nAll existing callers pass NULL, so behavior is unchanged.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n builtin/repack.c  |  2 +-\n repack-promisor.c | 15 ++++++++++++++-\n repack.h          |  4 +++-\n 3 files changed, 18 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex ed79c04e13..2ad6358535 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -430,7 +430,7 @@ int cmd_repack(int argc,\n \t\tstrvec_push(&cmd.args, \"--delta-islands\");\n \n \tif (pack_everything & ALL_INTO_ONE) {\n-\t\trepack_promisor_objects(repo, &po_args, &names, packtmp);\n+\t\trepack_promisor_objects(repo, &po_args, &names, packtmp, NULL);\n \n \t\tif (existing_packs_has_non_kept(&existing) &&\n \t\t    delete_redundant &&\ndiff --git a/repack-promisor.c b/repack-promisor.c\nindex 90318ce150..fabfdc168a 100644\n--- a/repack-promisor.c\n+++ b/repack-promisor.c\n@@ -6,10 +6,12 @@\n #include \"path.h\"\n #include \"repository.h\"\n #include \"run-command.h\"\n+#include \"oidset.h\"\n \n struct write_oid_context {\n \tstruct child_process *cmd;\n \tconst struct git_hash_algo *algop;\n+\tconst struct oidset *to_drop;\n };\n \n /*\n@@ -23,6 +25,15 @@ static int write_oid(const struct object_id *oid,\n \tstruct write_oid_context *ctx = data;\n \tstruct child_process *cmd = ctx->cmd;\n \n+\t/*\n+\t * Objects in to_drop are being removed from the repository, so\n+\t * omit them from the rebuilt promisor pack. Each such object is a\n+\t * promisor object and therefore remains recoverable from the\n+\t * promisor remote.\n+\t */\n+\tif (ctx->to_drop && oidset_contains(ctx->to_drop, oid))\n+\t\treturn 0;\n+\n \tif (cmd->in == -1) {\n \t\tif (start_command(cmd))\n \t\t\tdie(_(\"could not start pack-objects to repack promisor objects\"));\n@@ -81,7 +92,8 @@ static void finish_repacking_promisor_objects(struct repository *repo,\n \n void repack_promisor_objects(struct repository *repo,\n \t\t\t     const struct pack_objects_args *args,\n-\t\t\t     struct string_list *names, const char *packtmp)\n+\t\t\t     struct string_list *names, const char *packtmp,\n+\t\t\t     const struct oidset *to_drop)\n {\n \tstruct write_oid_context ctx;\n \tstruct child_process cmd = CHILD_PROCESS_INIT;\n@@ -98,6 +110,7 @@ void repack_promisor_objects(struct repository *repo,\n \t */\n \tctx.cmd = &cmd;\n \tctx.algop = repo->hash_algo;\n+\tctx.to_drop = to_drop;\n \todb_for_each_object(repo->objects, NULL, write_oid, &ctx,\n \t\t\t    ODB_FOR_EACH_OBJECT_PROMISOR_ONLY);\n \ndiff --git a/repack.h b/repack.h\nindex f9fbc895f0..a5a3f7c6ba 100644\n--- a/repack.h\n+++ b/repack.h\n@@ -3,6 +3,7 @@\n \n #include \"list-objects-filter-options.h\"\n #include \"string-list.h\"\n+#include \"oidset.h\"\n \n struct pack_objects_args {\n \tchar *window;\n@@ -100,7 +101,8 @@ void generated_pack_install(struct generated_pack *pack, const char *name,\n \n void repack_promisor_objects(struct repository *repo,\n \t\t\t     const struct pack_objects_args *args,\n-\t\t\t     struct string_list *names, const char *packtmp);\n+\t\t\t     struct string_list *names, const char *packtmp,\n+\t\t\t     const struct oidset *to_drop);\n \n struct pack_geometry {\n \tstruct packed_git **pack;\n-- \n2.54.0\n\n"},{"id":"550571","messageId":"20260813200830.84348-5-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260813200830.84348-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v5 4/6] builtin/repack: enumerate promisor blobs for --drop-filtered","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-13T20:08:28Z","receivedAt":"2026-08-13T20:09:10Z","isPatch":true,"body":"Add enumeration logic for --drop-filtered. In --dry-run mode, print\nthe OIDs of locally-held promisor blobs that exceed the filter\nthreshold, as candidates for removal.\n\nReading from write_filtered_pack() cannot work for partial clones.\ngit repack routes promisor objects through a separate path:\nrepack_promisor_objects() repacks them first, and the main\npack-objects run uses --exclude-promisor-objects. By the time\nwrite_filtered_pack() runs, the promisor blobs are already consumed by\nthe main pack. The filtered pack is always empty on a partial clone.\n\nInstead, walk promisor objects directly via odb_for_each_object() with\nODB_FOR_EACH_OBJECT_PROMISOR_ONLY, collecting all promisor blobs into\nan oidset. The blobs exceeding the filter threshold are then selected\nusing list_objects_filter__filter_oidset().\n\nEvery object enumerated this way is a promisor object, so it is\nrecoverable from the promisor remote in the same sense as the rest of a\npartial clone, as long as the remote still has it. This holds without a\nseparate is_promisor_object() check. A future implementation can verify\navailability against the remote directly once a client-side\nremote-object-info query exists.\n\nOBJECT_INFO_SKIP_FETCH_OBJECT is passed to every object info query so\nenumeration never triggers a lazy fetch.\n\nThe enumeration collects candidates into a caller-provided oidset and\n--dry-run prints them. Actually removing the objects, together with the\nrequired promisor-remote verification, is written in a later commit.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n builtin/repack.c                | 20 +++++++-\n repack-filtered.c               | 82 +++++++++++++++++++++++++++++++\n repack.h                        |  4 ++\n t/t7706-repack-drop-filtered.sh | 85 ++++++++++++++++++++++++++++++++-\n 4 files changed, 189 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex 2ad6358535..3633b17ce8 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -15,6 +15,8 @@\n #include \"repack.h\"\n #include \"shallow.h\"\n #include \"list-objects-filter-options.h\"\n+#include \"oidset.h\"\n+#include \"hex.h\"\n \n #define ALL_INTO_ONE 1\n #define LOOSEN_UNREACHABLE 2\n@@ -160,6 +162,7 @@ int cmd_repack(int argc,\n \tstruct string_list_item *item;\n \tstruct string_list names = STRING_LIST_INIT_DUP;\n \tstruct existing_packs existing = EXISTING_PACKS_INIT;\n+\tstruct oidset drop_oids = OIDSET_INIT;\n \tstruct pack_geometry geometry = { 0 };\n \tstruct tempfile *refs_snapshot = NULL;\n \tint i, ret;\n@@ -318,6 +321,20 @@ int cmd_repack(int argc,\n \t\t\tdie(_(\"--drop-filtered requires a promisor remote\"));\n \n \t\twrite_bitmaps = 0;\n+\n+\t\tret = enumerate_promisor_blobs(repo, &po_args.filter_options, &drop_oids);\n+\n+\t\tif (ret)\n+\t\t\tgoto cleanup;\n+\n+\t\tif (dry_run) {\n+\t\t\tstruct oidset_iter iter;\n+\t\t\tconst struct object_id *oid;\n+\n+\t\t\toidset_iter_init(&drop_oids, &iter);\n+\t\t\twhile ((oid = oidset_iter_next(&iter)))\n+\t\t\t\tprintf(\"%s\\n\", oid_to_hex(oid));\n+\t\t}\n \t}\n \n \tif (delete_redundant && repo->repository_format_precious_objects)\n@@ -613,7 +630,7 @@ int cmd_repack(int argc,\n \t\t}\n \t}\n \n-\tif (po_args.filter_options.choice) {\n+\tif (po_args.filter_options.choice && !drop_filtered) {\n \t\tstruct write_pack_opts opts = {\n \t\t\t.po_args = &po_args,\n \t\t\t.destination = filter_to,\n@@ -706,6 +723,7 @@ int cmd_repack(int argc,\n cleanup:\n \tstring_list_clear(&keep_pack_list, 0);\n \tstring_list_clear(&names, 1);\n+\toidset_clear(&drop_oids);\n \texisting_packs_release(&existing);\n \tpack_geometry_release(&geometry);\n \tpack_objects_args_release(&po_args);\ndiff --git a/repack-filtered.c b/repack-filtered.c\nindex edcf7667c5..869b9fc6e3 100644\n--- a/repack-filtered.c\n+++ b/repack-filtered.c\n@@ -3,6 +3,12 @@\n #include \"repository.h\"\n #include \"run-command.h\"\n #include \"string-list.h\"\n+#include \"hex.h\"\n+#include \"packfile.h\"\n+#include \"list-objects-filter-options.h\"\n+#include \"list-objects-filter.h\"\n+#include \"odb.h\"\n+#include \"promisor-remote.h\"\n \n int write_filtered_pack(const struct write_pack_opts *opts,\n \t\t\tstruct existing_packs *existing,\n@@ -49,3 +55,79 @@ int write_filtered_pack(const struct write_pack_opts *opts,\n \treturn finish_pack_objects_cmd(existing->repo->hash_algo, opts, &cmd,\n \t\t\t\t       names);\n }\n+\n+struct collect_cb_data {\n+\tstruct repository *repo;\n+\tstruct oidset *set;\n+};\n+\n+static int collect_promisor_blob(const struct object_id *oid,\n+\t\t\t\t struct object_info *oi UNUSED,\n+\t\t\t\t void *cb_data)\n+{\n+\tstruct collect_cb_data *data = cb_data;\n+\tstruct object_info info = OBJECT_INFO_INIT;\n+\tenum object_type type;\n+\n+\tinfo.typep = &type;\n+\n+\t/*\n+\t * Use OBJECT_INFO_SKIP_FETCH_OBJECT to avoid triggering a\n+\t * lazy fetch while collecting promisor blobs.\n+\t */\n+\tif (odb_read_object_info_extended(data->repo->objects, oid, &info,\n+\t\t\tOBJECT_INFO_SKIP_FETCH_OBJECT) < 0)\n+\t\treturn 0;\n+\n+\tif (type == OBJ_BLOB)\n+\t\toidset_insert(data->set, oid);\n+\n+\treturn 0;\n+}\n+\n+int enumerate_promisor_blobs(struct repository *repo,\n+\t\t\t     const struct list_objects_filter_options *filter,\n+\t\t\t     struct oidset *to_drop)\n+{\n+\tstruct oidset all_promisor_blobs = OIDSET_INIT;\n+\tstruct collect_cb_data cb = {\n+\t\t.repo = repo,\n+\t\t.set = &all_promisor_blobs\n+\t};\n+\tint ret = 0;\n+\n+\t/*\n+\t * The caller (cmd_repack) is responsible for validating that a\n+\t * blob:limit filter and a promisor remote are present before\n+\t * calling this function.\n+\t *\n+\t * Walk only promisor objects. Every object visited here is a\n+\t * promisor object, so it is recoverable from the promisor remote\n+\t * as long as the remote still has it, the same assumption the rest\n+\t * of partial clone relies on.\n+\t *\n+\t * We do not use write_filtered_pack() here because git repack\n+\t * routes promisor objects through repack_promisor_objects()\n+\t * before the filter machinery runs, so the filtered pack never\n+\t * contains promisor blobs. Direct enumeration via\n+\t * ODB_FOR_EACH_OBJECT_PROMISOR_ONLY is the correct approach.\n+\t */\n+\tret = odb_for_each_object(repo->objects, NULL,\n+\t\t\tcollect_promisor_blob, &cb,\n+\t\t\tODB_FOR_EACH_OBJECT_PROMISOR_ONLY);\n+\tif (ret)\n+\t\tgoto cleanup;\n+\n+\t/*\n+\t * Apply the filter to find which blobs exceed the threshold.\n+\t * The caller has to_drop and is responsible for clearing it.\n+\t */\n+\tret = list_objects_filter__filter_oidset(repo,\n+\t\tfilter,\n+\t\t&all_promisor_blobs,\n+\t\tto_drop);\n+\n+cleanup:\n+\toidset_clear(&all_promisor_blobs);\n+\treturn ret;\n+}\ndiff --git a/repack.h b/repack.h\nindex a5a3f7c6ba..61e554e4ed 100644\n--- a/repack.h\n+++ b/repack.h\n@@ -167,6 +167,10 @@ int write_filtered_pack(const struct write_pack_opts *opts,\n \t\t\tstruct existing_packs *existing,\n \t\t\tstruct string_list *names);\n \n+int enumerate_promisor_blobs(struct repository *repo,\n+\t\t\t     const struct list_objects_filter_options *filter,\n+\t\t\t     struct oidset *to_drop);\n+\n 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,\ndiff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh\nindex 07a976874a..6352f1fdce 100755\n--- a/t/t7706-repack-drop-filtered.sh\n+++ b/t/t7706-repack-drop-filtered.sh\n@@ -1,9 +1,37 @@\n #!/bin/sh\n \n-test_description='git repack --drop-filtered option validation'\n+test_description='git repack --drop-filtered enumerates filtered promisor blobs'\n \n . ./test-lib.sh\n \n+# Delete a loose or packed object from \"repo\".\n+delete_object () {\n+\tlocal repo=\"$1\" &&\n+\tlocal obj=\"$2\" &&\n+\tlocal path=\"$repo/.git/objects/$(test_oid_to_path \"$obj\")\" &&\n+\trm \"$path\"\n+}\n+\n+# Pack the objects into a promisor pack inside \"repo\". It is a pack\n+# accompanied by an empty \".promisor\" marker file. Objects\n+# in such a pack are treated as recoverable from the promisor remote.\n+pack_as_from_promisor () {\n+\tHASH=$(git -C repo pack-objects .git/objects/pack/pack) &&\n+\t>repo/.git/objects/pack/pack-$HASH.promisor &&\n+\techo $HASH\n+}\n+\n+# Write a blob of $1 bytes into \"repo\", record it as coming from the\n+# promisor remote, and remove the loose copy so the object is only\n+# present in the promisor pack.\n+promisor_blob () {\n+\ttest-tool genrandom \"$1\" \"$2\" >blob_content &&\n+\tOID=$(git -C repo hash-object -w --stdin <blob_content) &&\n+\tprintf \"%s\\n\" \"$OID\" | pack_as_from_promisor >/dev/null &&\n+\tdelete_object repo \"$OID\" &&\n+\techo \"$OID\"\n+}\n+\n # Check option validation before any promisor walk\n test_expect_success 'setup plain repo for validation' '\n \tgit init plain &&\n@@ -52,4 +80,59 @@ test_expect_success '--drop-filtered fails without a promisor remote' '\n \ttest_grep \"drop-filtered requires a promisor remote\" err\n '\n \n+# Enumeration tests using promisor pack\n+test_expect_success 'setup repo with a promisor remote' '\n+\trm -rf repo &&\n+\ttest_create_repo repo &&\n+\ttest_commit -C repo base &&\n+\n+\t# Mark the repo as a partial clone with a promisor remote so the\n+\t# promisor walk and the safety guard are satisfied.\n+\tgit -C repo config core.repositoryformatversion 1 &&\n+\tgit -C repo config extensions.partialclone origin &&\n+\tgit -C repo config remote.origin.promisor true &&\n+\tgit -C repo config remote.origin.url \".\" &&\n+\n+\tBIG=$(promisor_blob big 3072) &&\n+\tSMALL=$(promisor_blob small 512) &&\n+\techo \"$BIG\" >big_oid &&\n+\techo \"$SMALL\" >small_oid\n+'\n+\n+test_expect_success 'promisor blob over the threshold is listed' '\n+\tBIG=$(cat big_oid) &&\n+\tSMALL=$(cat small_oid) &&\n+\n+\tgit -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a >out &&\n+\n+\ttest_grep \"$BIG\" out &&\n+\ttest_grep ! \"$SMALL\" out\n+'\n+\n+test_expect_success 'locally created blob is never listed' '\n+\tBIG=$(cat big_oid) &&\n+\n+\t# Large blob that exists only locally must never be a drop candidate.\n+\t# Dropping it would be unrecoverable.\n+\ttest-tool genrandom local 4096 >local_content &&\n+\tLOCAL=$(git -C repo hash-object -w --stdin <local_content) &&\n+\n+\tgit -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a >out &&\n+\n+\ttest_grep \"$BIG\" out &&\n+\ttest_grep ! \"$LOCAL\" out\n+'\n+\n+test_expect_success '--dry-run does not remove the filtered objects' '\n+\tBIG=$(cat big_oid) &&\n+\n+\tgit -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a >out &&\n+\n+\t# Candidate blob must still be present after a dry run.\n+\tgit -C repo cat-file -e \"$BIG\"\n+'\n+\n test_done\n-- \n2.54.0\n\n"},{"id":"550572","messageId":"20260813200830.84348-6-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260813200830.84348-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v5 5/6] builtin/repack: actually drop filtered promisor blobs","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-13T20:08:29Z","receivedAt":"2026-08-13T20:09:16Z","isPatch":true,"body":"Make --drop-filtered remove the enumerated promisor blobs instead of\nonly listing them.\n\nThe drop set is computed before repack_promisor_objects() runs, and on\na real run it is passed in so the rebuilt promisor pack omits those\nblobs. --drop-filtered implies -d so the old promisor packs, which\nstill contain the dropped blobs, are removed. Without this the blobs\nwould survive in the redundant packs. The existing repack machinery\nperforms the write-before-delete and fsync, so the drop is crash-safe.\n\nThe dropped blobs become absent locally but remain recoverable from the\npromisor remote, so a later access lazy-fetches them back\ntransparently. --dry-run keeps its previous behavior, i.e. it lists the\ncandidates and changes nothing.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n Documentation/git-repack.adoc   | 28 ++++++++++++++++++++++++++++\n builtin/repack.c                | 14 ++++++++++----\n t/t7706-repack-drop-filtered.sh | 12 ++++++++++++\n 3 files changed, 50 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/git-repack.adoc b/Documentation/git-repack.adoc\nindex 72c42015e2..130249a139 100644\n--- a/Documentation/git-repack.adoc\n+++ b/Documentation/git-repack.adoc\n@@ -12,6 +12,7 @@ SYNOPSIS\n 'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\n \t[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]\n \t[--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]\n+\t[--filter=<filter-spec>] [--drop-filtered [--dry-run]]\n \n DESCRIPTION\n -----------\n@@ -182,6 +183,33 @@ depth is 4095.\n \t`objects` and `objects/info/alternates` sections of\n \tlinkgit:gitrepository-layout[5].\n \n+--drop-filtered::\n+\tDelete the local objects that match the `--filter` specification\n+\tinstead of keeping them in a separate packfile, reclaiming the\n+\tdisk space they occupy. This is intended for partial clones,\n+\twhere the filtered objects are promisor objects that remain\n+\trecoverable from the promisor remote and are lazily re-fetched\n+\ton demand when they are next needed.\n++\n+Only large blobs are supported for now, so `--filter=blob:limit=<n>`\n+is currently the only accepted filter. Because dropped objects must be\n+recoverable, this option requires a promisor remote to be configured\n+and refuses to run otherwise.\n++\n+This option requires `-a`, and implies `-d`: the objects are dropped by\n+rebuilding the promisor pack without them and then removing the now\n+redundant old packs, so the redundant packs must be deleted for the\n+space to actually be reclaimed. It is incompatible with `--filter-to`\n+and with bitmap writing (`-b`/`--write-bitmap-index`), since filtering\n+breaks the single-pack closure that bitmaps require. A bitmap setting\n+coming from configuration is silently disabled for the duration of the\n+command.\n+\n+--dry-run::\n+\tOnly meaningful with `--drop-filtered`. List the objects that\n+\twould be dropped, one object ID per line, without rebuilding any\n+\tpack or deleting anything.\n+\n -b::\n --write-bitmap-index::\n \tWrite a reachability bitmap index as part of the repack. This\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex 3633b17ce8..a5f13fdd87 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -287,9 +287,6 @@ int cmd_repack(int argc,\n \t\tdie(_(\"--dry-run only takes effect with --drop-filtered\"));\n \n \tif (drop_filtered) {\n-\t\tif (!dry_run)\n-\t\t\tdie(_(\"--drop-filtered does not work without --dry-run yet\"));\n-\n \t\tif (!po_args.filter_options.choice)\n \t\t\tdie(_(\"--drop-filtered requires --filter\"));\n \n@@ -322,6 +319,14 @@ int cmd_repack(int argc,\n \n \t\twrite_bitmaps = 0;\n \n+\t\t/*\n+\t\t * Dropping objects means rebuilding the promisor packs\n+\t\t * without them and then removing the old packs, so the\n+\t\t * redundant packs must be deleted. Imply -d on a real run.\n+\t\t */\n+\t\tif (!dry_run)\n+\t\t\tdelete_redundant = 1;\n+\n \t\tret = enumerate_promisor_blobs(repo, &po_args.filter_options, &drop_oids);\n \n \t\tif (ret)\n@@ -447,7 +452,8 @@ int cmd_repack(int argc,\n \t\tstrvec_push(&cmd.args, \"--delta-islands\");\n \n \tif (pack_everything & ALL_INTO_ONE) {\n-\t\trepack_promisor_objects(repo, &po_args, &names, packtmp, NULL);\n+\t\trepack_promisor_objects(repo, &po_args, &names, packtmp,\n+\t\t\t(drop_filtered && !dry_run) ? &drop_oids : NULL);\n \n \t\tif (existing_packs_has_non_kept(&existing) &&\n \t\t    delete_redundant &&\ndiff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh\nindex 6352f1fdce..80c695742f 100755\n--- a/t/t7706-repack-drop-filtered.sh\n+++ b/t/t7706-repack-drop-filtered.sh\n@@ -135,4 +135,16 @@ test_expect_success '--dry-run does not remove the filtered objects' '\n \tgit -C repo cat-file -e \"$BIG\"\n '\n \n+test_expect_success '--drop-filtered removes the promisor blob locally' '\n+\tBIG=$(cat big_oid) &&\n+\tSMALL=$(cat small_oid) &&\n+\n+\tgit -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k -a &&\n+\n+\tgit -C repo cat-file --batch-all-objects --batch-check=\"%(objectname)\" >present &&\n+\ttest_grep ! \"$BIG\" present &&\n+\ttest_grep \"$SMALL\" present\n+'\n+\n test_done\n-- \n2.54.0\n\n"},{"id":"550573","messageId":"20260813200830.84348-7-r.siddharth.shrimali@gmail.com","threadId":"66019","inReplyTo":"20260813200830.84348-1-r.siddharth.shrimali@gmail.com","subject":"[GSoC PATCH v5 6/6] builtin/repack: add guards for --drop-filtered","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-13T20:08:30Z","receivedAt":"2026-08-13T20:09:20Z","isPatch":true,"body":"--drop-filtered removes local promisor blobs. That is only safe when the\nrepository is not mid-operation and when the blobs are not actively in\nuse, so add two guards, both skipped for bare repositories which have\nneither a worktree nor an index.\n\nFirst, refuse to run while a merge, rebase, am, cherry-pick, revert, or\nbisect is in progress. During these operations the working tree and\nindex are in an intermediate state, and rewriting packs and deleting\nobjects underneath a half-finished operation is unsafe.\n\nSecond, refuse to drop a blob that the current index references. Such a\nblob is needed by the working tree, so dropping it would only cause the\nnext command that touches the worktree to lazy-fetch it straight back,\nreclaiming nothing. The offending path is reported so the user can see\nwhy the drop was refused.\n\nMentored-by: Christian Couder <christian.couder@gmail.com>\nMentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>\nSigned-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>\n---\n Documentation/git-repack.adoc   |  9 ++++++\n builtin/repack.c                | 52 +++++++++++++++++++++++++++++++++\n t/t7706-repack-drop-filtered.sh | 35 ++++++++++++++++++++++\n 3 files changed, 96 insertions(+)\n\ndiff --git a/Documentation/git-repack.adoc b/Documentation/git-repack.adoc\nindex 130249a139..a1f9e64f66 100644\n--- a/Documentation/git-repack.adoc\n+++ b/Documentation/git-repack.adoc\n@@ -204,6 +204,15 @@ and with bitmap writing (`-b`/`--write-bitmap-index`), since filtering\n breaks the single-pack closure that bitmaps require. A bitmap setting\n coming from configuration is silently disabled for the duration of the\n command.\n++\n+As a convenience, since dropped objects remain recoverable by lazy fetch,\n+`--drop-filtered` refuses to run while another operation\n+(merge, rebase, am, cherry-pick, revert, or bisect) is in progress, to\n+avoid a surprising network fetch mid-operation, and refuses to drop any\n+blob that the current index references, since such a blob would only be\n+lazily re-fetched by the next command that inspects the working tree.\n+These checks are skipped in bare repositories, which have neither a\n+working tree nor an index.\n \n --dry-run::\n \tOnly meaningful with `--drop-filtered`. List the objects that\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex a5f13fdd87..c4360382c1 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -17,6 +17,8 @@\n #include \"list-objects-filter-options.h\"\n #include \"oidset.h\"\n #include \"hex.h\"\n+#include \"wt-status.h\"\n+#include \"read-cache-ll.h\"\n \n #define ALL_INTO_ONE 1\n #define LOOSEN_UNREACHABLE 2\n@@ -317,6 +319,33 @@ int cmd_repack(int argc,\n \t\tif (!repo_has_promisor_remote(repo))\n \t\t\tdie(_(\"--drop-filtered requires a promisor remote\"));\n \n+\t\t/*\n+\t\t * Refuse to run while another operation is in progress. A\n+\t\t * dropped object would just be lazily re-fetched when the\n+\t\t * operation resumes, but triggering a network fetch in the\n+\t\t * middle of a half-finished\n+\t\t * merge/rebase/cherry-pick/revert/bisect is a poor\n+\t\t * experience, so this is a UX convenience rather than a\n+\t\t * safety measure. Bare repositories have no such state, so\n+\t\t * the check is skipped there.\n+\t\t */\n+\t\tif (!is_bare_repository(repo)) {\n+\t\t\tstruct wt_status_state state = { 0 };\n+\n+\t\t\twt_status_get_state(repo, &state, 0);\n+\t\t\tif (state.merge_in_progress || state.revert_in_progress ||\n+\t\t\t    state.rebase_in_progress || state.bisect_in_progress ||\n+\t\t\t    state.cherry_pick_in_progress || state.am_in_progress ||\n+\t\t\t    state.rebase_interactive_in_progress) {\n+\t\t\t\twt_status_state_free_buffers(&state);\n+\t\t\t\tdie(_(\"--drop-filtered cannot be used while \"\n+\t\t\t\t      \"another operation (merge, rebase, am, \"\n+\t\t\t\t      \"cherry-pick, revert, or bisect) is in \"\n+\t\t\t\t      \"progress\"));\n+\t\t\t}\n+\t\t\twt_status_state_free_buffers(&state);\n+\t\t}\n+\n \t\twrite_bitmaps = 0;\n \n \t\t/*\n@@ -332,6 +361,29 @@ int cmd_repack(int argc,\n \t\tif (ret)\n \t\t\tgoto cleanup;\n \n+\t\t/*\n+\t\t * Refuse to drop blobs that the current index references.\n+\t\t * Such a blob would only be lazily re-fetched by the next\n+\t\t * command that touches the worktree, so dropping it reclaims\n+\t\t * nothing. This guard just avoids that churn. Bare\n+\t\t * repositories have no index, so the check is skipped there.\n+\t\t */\n+\t\tif (!is_bare_repository(repo) && oidset_size(&drop_oids)) {\n+\t\t\tstruct index_state *istate = repo->index;\n+\t\t\tunsigned int i;\n+\n+\t\t\tif (repo_read_index(repo) < 0)\n+\t\t\t\tdie(_(\"could not read the index\"));\n+\n+\t\t\tfor (i = 0; i < istate->cache_nr; i++) {\n+\t\t\t\tconst struct cache_entry *ce = istate->cache[i];\n+\n+\t\t\t\tif (oidset_contains(&drop_oids, &ce->oid))\n+\t\t\t\t\tdie(_(\"cannot drop '%s' (%s): it is referenced by the current index\"),\n+\t\t\t\t\t\tce->name, oid_to_hex(&ce->oid));\n+\t\t\t}\n+\t\t}\n+\n \t\tif (dry_run) {\n \t\t\tstruct oidset_iter iter;\n \t\t\tconst struct object_id *oid;\ndiff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh\nindex 80c695742f..cb36115834 100755\n--- a/t/t7706-repack-drop-filtered.sh\n+++ b/t/t7706-repack-drop-filtered.sh\n@@ -147,4 +147,39 @@ test_expect_success '--drop-filtered removes the promisor blob locally' '\n \ttest_grep \"$SMALL\" present\n '\n \n+test_expect_success '--drop-filtered refuses when a merge is in progress' '\n+\ttest_when_finished \"git -C repo merge --abort || :\" &&\n+\n+\t# Create a conflicting merge so wt_status reports it.\n+\tgit -C repo checkout -B mergebase base &&\n+\techo one >repo/conflict.txt &&\n+\tgit -C repo add conflict.txt &&\n+\tgit -C repo commit -m one &&\n+\n+\tgit -C repo checkout -B mergeother base &&\n+\techo two >repo/conflict.txt &&\n+\tgit -C repo add conflict.txt &&\n+\tgit -C repo commit -m two &&\n+\n+\ttest_must_fail git -C repo merge mergebase &&\n+\n+\ttest_must_fail git -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err &&\n+\ttest_grep \"in progress\" err\n+'\n+\n+test_expect_success '--drop-filtered refuses to drop an index-referenced blob' '\n+\t# Create a large blob, add it to the index and make it a promisor object\n+\t# so the index references it and enumeration picks it up.\n+\ttest-tool genrandom idx 4096 >repo/tracked-big.bin &&\n+\tgit -C repo add tracked-big.bin &&\n+\tOID=$(git -C repo rev-parse :tracked-big.bin) &&\n+\tprintf \"%s\\n\" \"$OID\" | pack_as_from_promisor >/dev/null &&\n+\tdelete_object repo \"$OID\" &&\n+\n+\ttest_must_fail git -C repo -c repack.writeBitmaps=false \\\n+\t\trepack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err &&\n+\ttest_grep \"referenced by the current index\" err\n+'\n+\n test_done\n-- \n2.54.0\n\n"},{"id":"550625","messageId":"CAP8UFD1esJ0fk3xPXvAmQhMK_5wrpGKZJg9YaFV0-qUAC7bf5g@mail.gmail.com","threadId":"66019","inReplyTo":"20260813200830.84348-1-r.siddharth.shrimali@gmail.com","subject":"Re: [GSoC PATCH v5 0/6] repack: add --drop-filtered to reclaim space in partial clones","fromName":"Christian Couder","fromEmail":"christian.couder@gmail.com","sentAt":"2026-08-14T17:12:39Z","receivedAt":"2026-08-14T17:12:53Z","isPatch":true,"body":"On Thu, Aug 13, 2026 at 10:08 PM Siddharth Shrimali\n<r.siddharth.shrimali@gmail.com> wrote:\n\n> Changes since v4:\n>   * list-objects-filter: fixed the object-size local from \"unsigned\n>     long\" to \"size_t\" to match object_info.sizep - caught by Junio\n>   * list-objects-filter: made the filter options parameter const and\n>     dropped the now-unnecessary cast at the call site - caught by Christian\n>   * repack-filtered: comment fixes\n>   * squashed the documentation into the patches it describes: the option\n>     description now lands with \"actually drop filtered promisor blobs\"\n>     and the guards paragraph with \"add guards for --drop-filtered\",\n>     dropping the separate documentation patch, so the series is now 6\n>     patches - Suggested by Christian\n>   * improved the \"operation in progress\" error message to name the\n>     operations (merge, rebase, am, cherry-pick, revert, or bisect)\n>   * swept test comments to be proper sentences and fixed tab or space\n>     indentation\n\nThanks. This version looks good to me.\n"},{"id":"550626","messageId":"xmqqqzk0oc4z.fsf@gitster.g","threadId":"66019","inReplyTo":"CAP8UFD1esJ0fk3xPXvAmQhMK_5wrpGKZJg9YaFV0-qUAC7bf5g@mail.gmail.com","subject":"Re: [GSoC PATCH v5 0/6] repack: add --drop-filtered to reclaim space in partial clones","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-14T17:17:00Z","receivedAt":"2026-08-14T17:17:03Z","isPatch":true,"body":"Christian Couder <christian.couder@gmail.com> writes:\n\n> On Thu, Aug 13, 2026 at 10:08 PM Siddharth Shrimali\n> <r.siddharth.shrimali@gmail.com> wrote:\n>\n>> Changes since v4:\n>>   * list-objects-filter: fixed the object-size local from \"unsigned\n>>     long\" to \"size_t\" to match object_info.sizep - caught by Junio\n>>   * list-objects-filter: made the filter options parameter const and\n>>     dropped the now-unnecessary cast at the call site - caught by Christian\n>>   * repack-filtered: comment fixes\n>>   * squashed the documentation into the patches it describes: the option\n>>     description now lands with \"actually drop filtered promisor blobs\"\n>>     and the guards paragraph with \"add guards for --drop-filtered\",\n>>     dropping the separate documentation patch, so the series is now 6\n>>     patches - Suggested by Christian\n>>   * improved the \"operation in progress\" error message to name the\n>>     operations (merge, rebase, am, cherry-pick, revert, or bisect)\n>>   * swept test comments to be proper sentences and fixed tab or space\n>>     indentation\n>\n> Thanks. This version looks good to me.\n\nThanks, both.  Shall I mark the topic for 'next', then?\n"},{"id":"550643","messageId":"CAGWgyh9EVKBEm6bqG3bRsMCYvBCReJZbj+1xHK3qE4iw2Jp3dQ@mail.gmail.com","threadId":"66019","inReplyTo":"xmqqqzk0oc4z.fsf@gitster.g","subject":"Re: [GSoC PATCH v5 0/6] repack: add --drop-filtered to reclaim space in partial clones","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-08-14T19:32:03Z","receivedAt":"2026-08-14T19:32:40Z","isPatch":true,"body":"Hi Junio,\nOn Fri, 14 Aug 2026 at 22:47, Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Christian Couder <christian.couder@gmail.com> writes:\n>\n> > Thanks. This version looks good to me.\n>\n> Thanks, both.  Shall I mark the topic for 'next', then?\n\nThanks for all the reviews. Yes, it's ready from my side.\n\nThanks,\nSiddharth Shrimali\n"},{"id":"551919","messageId":"s0vqzjavw8p.fsf@gmail.com","threadId":"66019","inReplyTo":"20260813200830.84348-7-r.siddharth.shrimali@gmail.com","subject":"Re: [GSoC PATCH v5 6/6] builtin/repack: add guards for --drop-filtered","fromName":"Samuel Bronson","fromEmail":"naesten@gmail.com","sentAt":"2026-09-03T21:52:06Z","receivedAt":"2026-09-03T21:54:52Z","isPatch":true,"body":"Siddharth Shrimali <r.siddharth.shrimali@gmail.com> writes:\n\n> @@ -332,6 +361,29 @@ int cmd_repack(int argc,\n>  \t\tif (ret)\n>  \t\t\tgoto cleanup;\n>  \n> +\t\t/*\n> +\t\t * Refuse to drop blobs that the current index references.\n> +\t\t * Such a blob would only be lazily re-fetched by the next\n> +\t\t * command that touches the worktree, so dropping it reclaims\n> +\t\t * nothing. This guard just avoids that churn. Bare\n> +\t\t * repositories have no index, so the check is skipped there.\n> +\t\t */\n> +\t\tif (!is_bare_repository(repo) && oidset_size(&drop_oids)) {\n> +\t\t\tstruct index_state *istate = repo->index;\n> +\t\t\tunsigned int i;\n> +\n> +\t\t\tif (repo_read_index(repo) < 0)\n> +\t\t\t\tdie(_(\"could not read the index\"));\n> +\n> +\t\t\tfor (i = 0; i < istate->cache_nr; i++) {\n> +\t\t\t\tconst struct cache_entry *ce = istate->cache[i];\n> +\n> +\t\t\t\tif (oidset_contains(&drop_oids, &ce->oid))\n> +\t\t\t\t\tdie(_(\"cannot drop '%s' (%s): it is referenced by the current index\"),\n> +\t\t\t\t\t\tce->name, oid_to_hex(&ce->oid));\n\nThe good news: I've tried this whole feature on a real repository and it\n*seems* to work quite well on the whole.\n\nI was able to greatly reduce the size of my llvm-project clone's object\ndatabasev not originally cloned with --filter, using it.\n\nThe bad news: dying at this time is *not* convenient, especially after\nwe've finished that *entire* enumerate_promisor_blobs(), (which is kind\nof slow for a step with no progress output, btw).\n\nWhile I do want to keep the index blobs, I do *not* want to cancel the\nwhole operation over them.\n\nThe following seems much more convenient:\n\n-- >8 --\nSubject: [RFC] builtin/repack: just don't --drop-filtered index blobs\n\nInstead of dying when we would drop a blob referenced by the index, just\n... don't drop it. (Retain the explanatory message as a warning.)\n\nThis allows `git repack -a --filter=blob:limit=0 --drop-filtered` to\nwork in non-bare repositories that have non-trivial files around.\n\nNot done:\n\n  - Fixing the tests to match\n\n  - Allowing `--filter=blob:none`\n\nSigned-off-by: Samuel Bronson <naesten@gmail.com>\n\n\n\ndiff --git a/builtin/repack.c b/builtin/repack.c\nindex c4360382c1..ab4471f740 100644\n--- a/builtin/repack.c\n+++ b/builtin/repack.c\n@@ -378,8 +378,8 @@ int cmd_repack(int argc,\n \t\t\tfor (i = 0; i < istate->cache_nr; i++) {\n \t\t\t\tconst struct cache_entry *ce = istate->cache[i];\n \n-\t\t\t\tif (oidset_contains(&drop_oids, &ce->oid))\n-\t\t\t\t\tdie(_(\"cannot drop '%s' (%s): it is referenced by the current index\"),\n+\t\t\t\tif (oidset_remove(&drop_oids, &ce->oid))\n+\t\t\t\t\twarning(_(\"cannot drop '%s' (%s): it is referenced by the current index\"),\n \t\t\t\t\t\tce->name, oid_to_hex(&ce->oid));\n \t\t\t}\n \t\t}\n"},{"id":"551953","messageId":"CAGWgyh9B=re06aofii9VFB1xOwEeTtxYE=7T14m9WFAx1ORpMg@mail.gmail.com","threadId":"66019","inReplyTo":"s0vqzjavw8p.fsf@gmail.com","subject":"Re: [GSoC PATCH v5 6/6] builtin/repack: add guards for --drop-filtered","fromName":"Siddharth Shrimali","fromEmail":"r.siddharth.shrimali@gmail.com","sentAt":"2026-09-04T10:51:33Z","receivedAt":"2026-09-04T10:52:12Z","isPatch":true,"body":"Hi Samuel,\nThanks for the review and your RFC!\n\nI have a few suggestions:\n\nOn Fri, 4 Sept 2026 at 03:24, Samuel Bronson <naesten@gmail.com> wrote:\n> > +                     for (i = 0; i < istate->cache_nr; i++) {\n> > +                             const struct cache_entry *ce = istate->cache[i];\n> > +\n> > +                             if (oidset_contains(&drop_oids, &ce->oid))\n> > +                                     die(_(\"cannot drop '%s' (%s): it is referenced by the current index\"),\n> > +                                             ce->name, oid_to_hex(&ce->oid));\n>\n> The bad news: dying at this time is *not* convenient, especially after\n> we've finished that *entire* enumerate_promisor_blobs(), (which is kind\n> of slow for a step with no progress output, btw).\n>\n\nThats actually a very good point :)\nI agree with this: aborting the whole operation because a single blob\nis referenced by the index is a poor trade-off, since it happens only\nafter the full enumerate_promisor_blobs() walk has already\nrun.\n\n> While I do want to keep the index blobs, I do *not* want to cancel the\n> whole operation over them.\n\none caveat: oidset_remove() mutates drop_oids in place, and the\n--dry-run printer iterates drop_oids afterwards. So with this change,\n--dry-run would stop listing the index-referenced blobs, when it should\nstill report them as candidates it would skip. Instead, we can collect the index\nOIDs into a separate 'skip-set' and have both the dry-run output and the\nreal drop consult that, rather than removing from drop_oids directly.\n\nAs a follow-up note, the planned drop-log work will need to account\nfor this: a blob skipped here was never dropped, so it must not be\nrecorded there.\n\n> The following seems much more convenient:\n>\n> -- >8 --\n> Subject: [RFC] builtin/repack: just don't --drop-filtered index blobs\n>\n> Instead of dying when we would drop a blob referenced by the index, just\n> ... don't drop it. (Retain the explanatory message as a warning.)\n>\n> This allows `git repack -a --filter=blob:limit=0 --drop-filtered` to\n> work in non-bare repositories that have non-trivial files around.\n>\n> Not done:\n>\n>   - Fixing the tests to match\n>\n>   - Allowing `--filter=blob:none`\n\nThanks,\nSiddharth Shrimali\n"}]}