* [GSoC PATCH v2 1/7] builtin/repack.c: add --drop-filtered and --dry-run options
2026-07-30 17:41 ` [GSoC PATCH v2 " Siddharth Shrimali
@ 2026-07-30 17:41 ` Siddharth Shrimali
2026-08-04 21:13 ` Siddharth Asthana
2026-07-30 17:41 ` [GSoC PATCH v2 2/7] list-objects-filter: add list_objects_filter__filter_oidset() Siddharth Shrimali
` (8 subsequent siblings)
9 siblings, 1 reply; 46+ messages in thread
From: Siddharth Shrimali @ 2026-07-30 17:41 UTC (permalink / raw)
To: git
Cc: gitster, christian.couder, siddharthasthana31, me, ps,
johannes.schindelin, l.s.r, r.siddharth.shrimali
Add two new command-line options to 'git-repack':
--drop-filtered: intended to eventually delete objects that match
the filter specification. Requires --filter and -a,
and is incompatible with --filter-to.
--dry-run: show which objects would be dropped without making any
changes. Only meaningful with --drop-filtered.
Keep --dry-run as a separate option rather than folding it into
--drop-filtered (e.g --drop-filtered=dry-run), to stay consistent with
the --dry-run option other Git commands already provide and to leave
room for it to describe other repack behavior later. A
--drop-filtered=<mode> form can still be added later if more
drop-specific modes are needed.
--drop-filtered also requires a promisor remote to be configured, since
dropping objects without a remote to fetch them back from would be
permanent data loss.
--drop-filtered is incompatible with bitmap writing: filtering breaks
the "all objects in one pack" closure that bitmaps require. Snapshot
the bitmap setting after config but before option parsing so an
explicit -b/--write-bitmap-index on the command line can be told apart
from a repack.writeBitmaps configuration value. An explicit -b is
reported as a conflict, while a config-provided default is silently
disabled for the duration of the command.
These options currently only perform validation. The actual enumeration
and deletion will be added in follow-up commits.
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>
Signed-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>
---
builtin/repack.c | 63 +++++++++++++++++++++++++++++++++
t/meson.build | 1 +
t/t7706-repack-drop-filtered.sh | 49 +++++++++++++++++++++++++
3 files changed, 113 insertions(+)
create mode 100755 t/t7706-repack-drop-filtered.sh
diff --git a/builtin/repack.c b/builtin/repack.c
index db504d673f..322b01cb3e 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -14,6 +14,7 @@
#include "promisor-remote.h"
#include "repack.h"
#include "shallow.h"
+#include "list-objects-filter-options.h"
#define ALL_INTO_ONE 1
#define LOOSEN_UNREACHABLE 2
@@ -28,6 +29,8 @@ static int use_delta_islands;
static int run_update_server_info = 1;
static char *packdir, *packtmp_name, *packtmp;
static int midx_must_contain_cruft = 1;
+static int drop_filtered;
+static int dry_run;
static const char *const git_repack_usage[] = {
N_("git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\n"
@@ -148,6 +151,7 @@ int cmd_repack(int argc,
/* variables to be filled by option parsing */
struct repack_config_ctx config_ctx;
int delete_redundant = 0;
+ int write_bitmaps_before_parse;
const char *unpack_unreachable = NULL;
int keep_unreachable = 0;
struct string_list keep_pack_list = STRING_LIST_INIT_NODUP;
@@ -231,6 +235,10 @@ int cmd_repack(int argc,
N_("pack prefix to store a pack containing pruned objects")),
OPT_STRING(0, "filter-to", &filter_to, N_("dir"),
N_("pack prefix to store a pack containing filtered out objects")),
+ OPT_BOOL(0, "drop-filtered", &drop_filtered,
+ N_("delete filtered out objects (requires --filter)")),
+ OPT_BOOL(0, "dry-run", &dry_run,
+ N_("only show which objects would be dropped")),
OPT_END()
};
@@ -244,6 +252,13 @@ int cmd_repack(int argc,
repo_config(repo, repack_config, &config_ctx);
+ /*
+ * update the bitmap setting after config but before command line
+ * parsing, so we can later tell whether -b/--write-bitmap-index was
+ * given explicitly on the command line or not
+ */
+ write_bitmaps_before_parse = write_bitmaps;
+
argc = parse_options(argc, argv, prefix, builtin_repack_options,
git_repack_usage, 0);
@@ -252,6 +267,54 @@ int cmd_repack(int argc,
po_args.depth = xstrdup_or_null(opt_depth);
po_args.threads = xstrdup_or_null(opt_threads);
+ die_for_incompatible_opt2(drop_filtered, "--drop-filtered",
+ !!filter_to, "--filter-to");
+
+ die_for_incompatible_opt2(drop_filtered, "--drop-filtered",
+ write_bitmaps > 0, "--write-bitmap-index");
+
+ if (dry_run && !drop_filtered)
+ die(_("--dry-run only takes effect with --drop-filtered"));
+
+ if (drop_filtered) {
+ int bitmaps_from_cmdline = (write_bitmaps != write_bitmaps_before_parse);
+
+ if (!dry_run)
+ die(_("--drop-filtered doesn't work without --dry-run yet"));
+
+ if (!po_args.filter_options.choice)
+ die(_("--drop-filtered requires --filter"));
+
+ if (!(pack_everything & ALL_INTO_ONE))
+ die(_("--drop-filtered requires -a"));
+
+ /*
+ * Only blob:limit=<n> is supported for now. Reject other
+ * filter choices early, before walking the object database.
+ */
+ if (po_args.filter_options.choice != LOFC_BLOB_LIMIT)
+ die(_("--drop-filtered only supports --filter=blob:limit=<n> for now"));
+
+ /*
+ * an explicit -b on the command line is a conflict we have to
+ * report, a bitmap setting from config is silently overridden
+ * for the duration of the command
+ */
+ if (bitmaps_from_cmdline && write_bitmaps > 0)
+ die(_("options '%s' and '%s' cannot be used together"),
+ "--drop-filtered", "--write-bitmap-index");
+
+ /*
+ * Without a promisor remote there is nowhere to re-fetch the
+ * dropped objects from, so dropping them would be permanent
+ * data loss.
+ */
+ if (!repo_has_promisor_remote(repo))
+ die(_("--drop-filtered requires a promisor remote"));
+
+ write_bitmaps = 0;
+ }
+
if (delete_redundant && repo->repository_format_precious_objects)
die(_("cannot delete packs in a precious-objects repo"));
diff --git a/t/meson.build b/t/meson.build
index d8161c368b..c2bf60d129 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -963,6 +963,7 @@ integration_tests = [
't7703-repack-geometric.sh',
't7704-repack-cruft.sh',
't7705-repack-incremental-midx.sh',
+ 't7706-repack-drop-filtered.sh',
't7800-difftool.sh',
't7810-grep.sh',
't7811-grep-open.sh',
diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh
new file mode 100755
index 0000000000..65be756e33
--- /dev/null
+++ b/t/t7706-repack-drop-filtered.sh
@@ -0,0 +1,49 @@
+#!/bin/sh
+
+test_description='git repack --drop-filtered option validation'
+
+. ./test-lib.sh
+
+# checks for options validations before any promisor walk
+test_expect_success 'setup plain repo for validation' '
+ git init plain &&
+ test_commit -C plain initial &&
+ git clone --bare plain plain.git &&
+ git -C plain.git repack -a -d
+'
+
+test_expect_success '--drop-filtered requires --filter' '
+ test_must_fail git -C plain.git repack --drop-filtered --dry-run -a 2>err &&
+ test_grep "drop-filtered requires --filter" err
+'
+
+test_expect_success '--drop-filtered cannot be used with --filter-to' '
+ test_must_fail git -C plain.git repack --drop-filtered \
+ --filter=blob:limit=1k --filter-to=./filter-out 2>err &&
+ test_grep "options .--drop-filtered. and .--filter-to. cannot be used together" err
+'
+
+test_expect_success '--dry-run only takes effect with --drop-filtered' '
+ test_must_fail git -C plain.git repack --dry-run 2>err &&
+ test_grep "dry-run only takes effect with --drop-filtered" err
+'
+
+test_expect_success '--drop-filtered requires -a' '
+ test_must_fail git -C plain.git repack --drop-filtered \
+ --filter=blob:limit=1k --dry-run 2>err &&
+ test_grep "drop-filtered requires -a" err
+'
+
+test_expect_success '--drop-filtered fails with --write-bitmap-index' '
+ test_must_fail git -C plain.git repack --drop-filtered \
+ --filter=blob:limit=1k --dry-run -a -b 2>err &&
+ test_grep "options .--drop-filtered. and .--write-bitmap-index. cannot be used together" err
+'
+
+test_expect_success '--drop-filtered fails without a promisor remote' '
+ test_must_fail git -C plain.git repack --drop-filtered \
+ --filter=blob:limit=1k --dry-run -a 2>err &&
+ test_grep "drop-filtered requires a promisor remote" err
+'
+
+test_done
--
2.54.0
^ permalink raw reply related [flat|nested] 46+ messages in thread* Re: [GSoC PATCH v2 1/7] builtin/repack.c: add --drop-filtered and --dry-run options
2026-07-30 17:41 ` [GSoC PATCH v2 1/7] builtin/repack.c: add --drop-filtered and --dry-run options Siddharth Shrimali
@ 2026-08-04 21:13 ` Siddharth Asthana
0 siblings, 0 replies; 46+ messages in thread
From: Siddharth Asthana @ 2026-08-04 21:13 UTC (permalink / raw)
To: Siddharth Shrimali, git
Cc: gitster, christian.couder, me, ps, johannes.schindelin, l.s.r
On 30/07/26 23:11, Siddharth Shrimali wrote:
> Add two new command-line options to 'git-repack':
>
> --drop-filtered: intended to eventually delete objects that match
> the filter specification. Requires --filter and -a,
> and is incompatible with --filter-to.
> --dry-run: show which objects would be dropped without making any
> changes. Only meaningful with --drop-filtered.
>
> Keep --dry-run as a separate option rather than folding it into
> --drop-filtered (e.g --drop-filtered=dry-run), to stay consistent with
> the --dry-run option other Git commands already provide and to leave
> room for it to describe other repack behavior later. A
> --drop-filtered=<mode> form can still be added later if more
> drop-specific modes are needed.
>
> --drop-filtered also requires a promisor remote to be configured, since
> dropping objects without a remote to fetch them back from would be
> permanent data loss.
>
> --drop-filtered is incompatible with bitmap writing: filtering breaks
> the "all objects in one pack" closure that bitmaps require. Snapshot
> the bitmap setting after config but before option parsing so an
> explicit -b/--write-bitmap-index on the command line can be told apart
> from a repack.writeBitmaps configuration value. An explicit -b is
> reported as a conflict, while a config-provided default is silently
> disabled for the duration of the command.
>
> These options currently only perform validation. The actual enumeration
> and deletion will be added in follow-up commits.
>
> Mentored-by: Christian Couder <christian.couder@gmail.com>
> Mentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>
> Signed-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>
> ---
> builtin/repack.c | 63 +++++++++++++++++++++++++++++++++
> t/meson.build | 1 +
> t/t7706-repack-drop-filtered.sh | 49 +++++++++++++++++++++++++
> 3 files changed, 113 insertions(+)
> create mode 100755 t/t7706-repack-drop-filtered.sh
>
> diff --git a/builtin/repack.c b/builtin/repack.c
> index db504d673f..322b01cb3e 100644
> --- a/builtin/repack.c
> +++ b/builtin/repack.c
> @@ -14,6 +14,7 @@
> #include "promisor-remote.h"
> #include "repack.h"
> #include "shallow.h"
> +#include "list-objects-filter-options.h"
>
> #define ALL_INTO_ONE 1
> #define LOOSEN_UNREACHABLE 2
> @@ -28,6 +29,8 @@ static int use_delta_islands;
> static int run_update_server_info = 1;
> static char *packdir, *packtmp_name, *packtmp;
> static int midx_must_contain_cruft = 1;
> +static int drop_filtered;
> +static int dry_run;
>
> static const char *const git_repack_usage[] = {
> N_("git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\n"
> @@ -148,6 +151,7 @@ int cmd_repack(int argc,
> /* variables to be filled by option parsing */
> struct repack_config_ctx config_ctx;
> int delete_redundant = 0;
> + int write_bitmaps_before_parse;
> const char *unpack_unreachable = NULL;
> int keep_unreachable = 0;
> struct string_list keep_pack_list = STRING_LIST_INIT_NODUP;
> @@ -231,6 +235,10 @@ int cmd_repack(int argc,
> N_("pack prefix to store a pack containing pruned objects")),
> OPT_STRING(0, "filter-to", &filter_to, N_("dir"),
> N_("pack prefix to store a pack containing filtered out objects")),
> + OPT_BOOL(0, "drop-filtered", &drop_filtered,
> + N_("delete filtered out objects (requires --filter)")),
> + OPT_BOOL(0, "dry-run", &dry_run,
> + N_("only show which objects would be dropped")),
> OPT_END()
> };
>
> @@ -244,6 +252,13 @@ int cmd_repack(int argc,
>
> repo_config(repo, repack_config, &config_ctx);
>
> + /*
> + * update the bitmap setting after config but before command line
> + * parsing, so we can later tell whether -b/--write-bitmap-index was
> + * given explicitly on the command line or not
> + */
> + write_bitmaps_before_parse = write_bitmaps;
> +
> argc = parse_options(argc, argv, prefix, builtin_repack_options,
> git_repack_usage, 0);
>
> @@ -252,6 +267,54 @@ int cmd_repack(int argc,
> po_args.depth = xstrdup_or_null(opt_depth);
> po_args.threads = xstrdup_or_null(opt_threads);
>
> + die_for_incompatible_opt2(drop_filtered, "--drop-filtered",
> + !!filter_to, "--filter-to");
> +
> + die_for_incompatible_opt2(drop_filtered, "--drop-filtered",
> + write_bitmaps > 0, "--write-bitmap-index");
> +
> + if (dry_run && !drop_filtered)
> + die(_("--dry-run only takes effect with --drop-filtered"));
> +
> + if (drop_filtered) {
> + int bitmaps_from_cmdline = (write_bitmaps != write_bitmaps_before_parse);
> +
> + if (!dry_run)
> + die(_("--drop-filtered doesn't work without --dry-run yet"));
> +
> + if (!po_args.filter_options.choice)
> + die(_("--drop-filtered requires --filter"));
> +
> + if (!(pack_everything & ALL_INTO_ONE))
> + die(_("--drop-filtered requires -a"));
> +
> + /*
> + * Only blob:limit=<n> is supported for now. Reject other
> + * filter choices early, before walking the object database.
> + */
> + if (po_args.filter_options.choice != LOFC_BLOB_LIMIT)
> + die(_("--drop-filtered only supports --filter=blob:limit=<n> for now"));
> +
> + /*
> + * an explicit -b on the command line is a conflict we have to
> + * report, a bitmap setting from config is silently overridden
> + * for the duration of the command
> + */
> + if (bitmaps_from_cmdline && write_bitmaps > 0)
> + die(_("options '%s' and '%s' cannot be used together"),
> + "--drop-filtered", "--write-bitmap-index");
Thanks for tackling the bitmap CLI vs config split.
One case still looks off: if repack.writeBitmaps is already true and
the user also passes -b, write_bitmaps is 1 before and after parse, so
bitmaps_from_cmdline stays false and we never error. I think explicit
-b should still be rejected there.
Thanks.
Siddharth
> +
> + /*
> + * Without a promisor remote there is nowhere to re-fetch the
> + * dropped objects from, so dropping them would be permanent
> + * data loss.
> + */
> + if (!repo_has_promisor_remote(repo))
> + die(_("--drop-filtered requires a promisor remote"));
> +
> + write_bitmaps = 0;
> + }
> +
> if (delete_redundant && repo->repository_format_precious_objects)
> die(_("cannot delete packs in a precious-objects repo"));
>
> diff --git a/t/meson.build b/t/meson.build
> index d8161c368b..c2bf60d129 100644
> --- a/t/meson.build
> +++ b/t/meson.build
> @@ -963,6 +963,7 @@ integration_tests = [
> 't7703-repack-geometric.sh',
> 't7704-repack-cruft.sh',
> 't7705-repack-incremental-midx.sh',
> + 't7706-repack-drop-filtered.sh',
> 't7800-difftool.sh',
> 't7810-grep.sh',
> 't7811-grep-open.sh',
> diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh
> new file mode 100755
> index 0000000000..65be756e33
> --- /dev/null
> +++ b/t/t7706-repack-drop-filtered.sh
> @@ -0,0 +1,49 @@
> +#!/bin/sh
> +
> +test_description='git repack --drop-filtered option validation'
> +
> +. ./test-lib.sh
> +
> +# checks for options validations before any promisor walk
> +test_expect_success 'setup plain repo for validation' '
> + git init plain &&
> + test_commit -C plain initial &&
> + git clone --bare plain plain.git &&
> + git -C plain.git repack -a -d
> +'
> +
> +test_expect_success '--drop-filtered requires --filter' '
> + test_must_fail git -C plain.git repack --drop-filtered --dry-run -a 2>err &&
> + test_grep "drop-filtered requires --filter" err
> +'
> +
> +test_expect_success '--drop-filtered cannot be used with --filter-to' '
> + test_must_fail git -C plain.git repack --drop-filtered \
> + --filter=blob:limit=1k --filter-to=./filter-out 2>err &&
> + test_grep "options .--drop-filtered. and .--filter-to. cannot be used together" err
> +'
> +
> +test_expect_success '--dry-run only takes effect with --drop-filtered' '
> + test_must_fail git -C plain.git repack --dry-run 2>err &&
> + test_grep "dry-run only takes effect with --drop-filtered" err
> +'
> +
> +test_expect_success '--drop-filtered requires -a' '
> + test_must_fail git -C plain.git repack --drop-filtered \
> + --filter=blob:limit=1k --dry-run 2>err &&
> + test_grep "drop-filtered requires -a" err
> +'
> +
> +test_expect_success '--drop-filtered fails with --write-bitmap-index' '
> + test_must_fail git -C plain.git repack --drop-filtered \
> + --filter=blob:limit=1k --dry-run -a -b 2>err &&
> + test_grep "options .--drop-filtered. and .--write-bitmap-index. cannot be used together" err
> +'
> +
> +test_expect_success '--drop-filtered fails without a promisor remote' '
> + test_must_fail git -C plain.git repack --drop-filtered \
> + --filter=blob:limit=1k --dry-run -a 2>err &&
> + test_grep "drop-filtered requires a promisor remote" err
> +'
> +
> +test_done
^ permalink raw reply [flat|nested] 46+ messages in thread
* [GSoC PATCH v2 2/7] list-objects-filter: add list_objects_filter__filter_oidset()
2026-07-30 17:41 ` [GSoC PATCH v2 " Siddharth Shrimali
2026-07-30 17:41 ` [GSoC PATCH v2 1/7] builtin/repack.c: add --drop-filtered and --dry-run options Siddharth Shrimali
@ 2026-07-30 17:41 ` Siddharth Shrimali
2026-07-30 17:41 ` [GSoC PATCH v2 3/7] repack-promisor: allow excluding objects from the rebuilt promisor pack Siddharth Shrimali
` (7 subsequent siblings)
9 siblings, 0 replies; 46+ messages in thread
From: Siddharth Shrimali @ 2026-07-30 17:41 UTC (permalink / raw)
To: git
Cc: gitster, christian.couder, siddharthasthana31, me, ps,
johannes.schindelin, l.s.r, r.siddharth.shrimali
The existing filter entry point, list_objects_filter__filter_object(),
is built around the object-walk path: it expects traversal context and
provisional omit sets, and is meant to be called as objects are
visited during a walk. A caller that already has a set of OIDs in hand
and only wants to know which ones a filter would select has no usable
entry point into the filter API.
--drop-filtered is exactly such a caller: it collects promisor blobs
into an oidset and needs to know which of them exceed the filter
threshold, without performing an object walk.
Add a helper, list_objects_filter__filter_oidset(), that takes a set
of OIDs and populates an "omitted" set with those that would be
filtered out by the given filter options. Only blob:limit=N filters
are supported for now.
This helper does not actually reuse the existing filter machinery.
It reimplements the blob:limit size check directly. That machinery
is tied to the object-walk path and cannot easily be driven
from a plain oidset. A NEEDSWORK comment marks this so the helper can
later be refactored to reuse the real filter logic instead of
duplicating it.
OBJECT_INFO_SKIP_FETCH_OBJECT is passed when reading object info so
the helper never triggers a lazy fetch.
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>
Signed-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>
---
list-objects-filter.c | 45 +++++++++++++++++++++++++++++++++++++++++++
list-objects-filter.h | 16 +++++++++++++++
2 files changed, 61 insertions(+)
diff --git a/list-objects-filter.c b/list-objects-filter.c
index c912ff3079..6a2e9d5b24 100644
--- a/list-objects-filter.c
+++ b/list-objects-filter.c
@@ -828,3 +828,48 @@ void list_objects_filter__free(struct filter *filter)
filter->free_fn(filter->filter_data);
free(filter);
}
+
+/*
+ * NEEDSWORK: this reimplements the blob:limit size check rather than
+ * reusing the existing filter machinery in
+ * list_objects_filter__filter_object(). That machinery is currently
+ * tied to the object-walk path and cannot easily be driven from a
+ * plain oidset. It would be nice to refactor the filter code so this
+ * helper can reuse it instead of duplicating the size check.
+ */
+int list_objects_filter__filter_oidset(struct repository *r,
+ struct list_objects_filter_options *opts,
+ const struct oidset *in,
+ struct oidset *omitted)
+{
+ struct oidset_iter iter;
+ const struct object_id *oid;
+
+ if (opts->choice != LOFC_BLOB_LIMIT)
+ return error(_("filter_oidset: only blob:limit filters are supported"));
+
+ oidset_iter_init(in, &iter);
+ while ((oid = oidset_iter_next(&iter))) {
+ struct object_info info = OBJECT_INFO_INIT;
+ enum object_type type;
+ unsigned long size;
+
+ info.typep = &type;
+ info.sizep = &size;
+
+ /*
+ * Use OBJECT_INFO_SKIP_FETCH_OBJECT to avoid triggering
+ * a lazy fetch while inspecting candidates for removal.
+ */
+ if (odb_read_object_info_extended(r->objects, oid, &info,
+ OBJECT_INFO_SKIP_FETCH_OBJECT) < 0)
+ continue;
+
+ if (type != OBJ_BLOB)
+ continue;
+
+ if (size >= opts->blob_limit_value)
+ oidset_insert(omitted, oid);
+ }
+ return 0;
+}
diff --git a/list-objects-filter.h b/list-objects-filter.h
index 9e98814111..56a2d87aa0 100644
--- a/list-objects-filter.h
+++ b/list-objects-filter.h
@@ -94,4 +94,20 @@ enum list_objects_filter_result list_objects_filter__filter_object(
*/
void list_objects_filter__free(struct filter *filter);
+/*
+ * Given a set of OIDs in 'in', populate 'omitted' with those that
+ * would be filtered by 'opts'. Currently only blob:limit=N is
+ * supported. Objects that cannot be read are silently skipped.
+ *
+ * NEEDSWORK: this reimplements the blob:limit size check rather than
+ * reusing the existing filter machinery. See the matching comment in
+ * list-objects-filter.c.
+ *
+ * Return 0 on success, -1 if the filter is not supported.
+ */
+int list_objects_filter__filter_oidset(struct repository *r,
+ struct list_objects_filter_options *opts,
+ const struct oidset *in,
+ struct oidset *omitted);
+
#endif /* LIST_OBJECTS_FILTER_H */
--
2.54.0
^ permalink raw reply related [flat|nested] 46+ messages in thread* [GSoC PATCH v2 3/7] repack-promisor: allow excluding objects from the rebuilt promisor pack
2026-07-30 17:41 ` [GSoC PATCH v2 " Siddharth Shrimali
2026-07-30 17:41 ` [GSoC PATCH v2 1/7] builtin/repack.c: add --drop-filtered and --dry-run options Siddharth Shrimali
2026-07-30 17:41 ` [GSoC PATCH v2 2/7] list-objects-filter: add list_objects_filter__filter_oidset() Siddharth Shrimali
@ 2026-07-30 17:41 ` Siddharth Shrimali
2026-07-30 17:41 ` [GSoC PATCH v2 4/7] builtin/repack: enumerate promisor blobs for --drop-filtered Siddharth Shrimali
` (6 subsequent siblings)
9 siblings, 0 replies; 46+ messages in thread
From: Siddharth Shrimali @ 2026-07-30 17:41 UTC (permalink / raw)
To: git
Cc: gitster, christian.couder, siddharthasthana31, me, ps,
johannes.schindelin, l.s.r, r.siddharth.shrimali
Add a to_drop oidset parameter to repack_promisor_objects(). When it is
non-NULL, write_oid() omits those objects from the rebuilt promisor
pack. This is the mechanism --drop-filtered will use to remove promisor
blobs, i.e. rebuild the promisor pack without them.
All existing callers pass NULL, so behavior is unchanged.
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>
Signed-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>
---
builtin/repack.c | 2 +-
repack-promisor.c | 15 ++++++++++++++-
repack.h | 4 +++-
3 files changed, 18 insertions(+), 3 deletions(-)
diff --git a/builtin/repack.c b/builtin/repack.c
index 322b01cb3e..f25d189b07 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -425,7 +425,7 @@ int cmd_repack(int argc,
strvec_push(&cmd.args, "--delta-islands");
if (pack_everything & ALL_INTO_ONE) {
- repack_promisor_objects(repo, &po_args, &names, packtmp);
+ repack_promisor_objects(repo, &po_args, &names, packtmp, NULL);
if (existing_packs_has_non_kept(&existing) &&
delete_redundant &&
diff --git a/repack-promisor.c b/repack-promisor.c
index 90318ce150..fabfdc168a 100644
--- a/repack-promisor.c
+++ b/repack-promisor.c
@@ -6,10 +6,12 @@
#include "path.h"
#include "repository.h"
#include "run-command.h"
+#include "oidset.h"
struct write_oid_context {
struct child_process *cmd;
const struct git_hash_algo *algop;
+ const struct oidset *to_drop;
};
/*
@@ -23,6 +25,15 @@ static int write_oid(const struct object_id *oid,
struct write_oid_context *ctx = data;
struct child_process *cmd = ctx->cmd;
+ /*
+ * Objects in to_drop are being removed from the repository, so
+ * omit them from the rebuilt promisor pack. Each such object is a
+ * promisor object and therefore remains recoverable from the
+ * promisor remote.
+ */
+ if (ctx->to_drop && oidset_contains(ctx->to_drop, oid))
+ return 0;
+
if (cmd->in == -1) {
if (start_command(cmd))
die(_("could not start pack-objects to repack promisor objects"));
@@ -81,7 +92,8 @@ static void finish_repacking_promisor_objects(struct repository *repo,
void repack_promisor_objects(struct repository *repo,
const struct pack_objects_args *args,
- struct string_list *names, const char *packtmp)
+ struct string_list *names, const char *packtmp,
+ const struct oidset *to_drop)
{
struct write_oid_context ctx;
struct child_process cmd = CHILD_PROCESS_INIT;
@@ -98,6 +110,7 @@ void repack_promisor_objects(struct repository *repo,
*/
ctx.cmd = &cmd;
ctx.algop = repo->hash_algo;
+ ctx.to_drop = to_drop;
odb_for_each_object(repo->objects, NULL, write_oid, &ctx,
ODB_FOR_EACH_OBJECT_PROMISOR_ONLY);
diff --git a/repack.h b/repack.h
index f9fbc895f0..a5a3f7c6ba 100644
--- a/repack.h
+++ b/repack.h
@@ -3,6 +3,7 @@
#include "list-objects-filter-options.h"
#include "string-list.h"
+#include "oidset.h"
struct pack_objects_args {
char *window;
@@ -100,7 +101,8 @@ void generated_pack_install(struct generated_pack *pack, const char *name,
void repack_promisor_objects(struct repository *repo,
const struct pack_objects_args *args,
- struct string_list *names, const char *packtmp);
+ struct string_list *names, const char *packtmp,
+ const struct oidset *to_drop);
struct pack_geometry {
struct packed_git **pack;
--
2.54.0
^ permalink raw reply related [flat|nested] 46+ messages in thread* [GSoC PATCH v2 4/7] builtin/repack: enumerate promisor blobs for --drop-filtered
2026-07-30 17:41 ` [GSoC PATCH v2 " Siddharth Shrimali
` (2 preceding siblings ...)
2026-07-30 17:41 ` [GSoC PATCH v2 3/7] repack-promisor: allow excluding objects from the rebuilt promisor pack Siddharth Shrimali
@ 2026-07-30 17:41 ` Siddharth Shrimali
2026-07-30 17:41 ` [GSoC PATCH v2 5/7] builtin/repack: actually drop filtered promisor blobs Siddharth Shrimali
` (5 subsequent siblings)
9 siblings, 0 replies; 46+ messages in thread
From: Siddharth Shrimali @ 2026-07-30 17:41 UTC (permalink / raw)
To: git
Cc: gitster, christian.couder, siddharthasthana31, me, ps,
johannes.schindelin, l.s.r, r.siddharth.shrimali
Add enumeration logic for --drop-filtered. In --dry-run mode, print
the OIDs of locally-held promisor blobs that exceed the filter
threshold, as candidates for removal.
Reading from write_filtered_pack() cannot work for partial clones.
git repack routes promisor objects through a separate path:
repack_promisor_objects() repacks them first, and the main
pack-objects run uses --exclude-promisor-objects. By the time
write_filtered_pack() runs, the promisor blobs are already consumed by
the main pack. The filtered pack is always empty on a partial clone.
Instead, walk promisor objects directly via odb_for_each_object() with
ODB_FOR_EACH_OBJECT_PROMISOR_ONLY, collecting all promisor blobs into
an oidset. The blobs exceeding the filter threshold are then selected
using list_objects_filter__filter_oidset().
Every object enumerated this way is a promisor object by construction,
so it is guaranteed to be recoverable from the promisor remote and is
safe to drop. No separate is_promisor_object() check is needed.
OBJECT_INFO_SKIP_FETCH_OBJECT is passed to every object info query so
enumeration never triggers a lazy fetch.
The enumeration collects candidates into a caller-provided oidset and
--dry-run prints them. Actually removing the objects, together with the
required promisor-remote verification, is written in a later commit.
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>
Signed-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>
---
builtin/repack.c | 20 +++++++-
repack-filtered.c | 80 +++++++++++++++++++++++++++++++
repack.h | 4 ++
t/t7706-repack-drop-filtered.sh | 84 ++++++++++++++++++++++++++++++++-
4 files changed, 186 insertions(+), 2 deletions(-)
diff --git a/builtin/repack.c b/builtin/repack.c
index f25d189b07..8cb92d1a62 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -15,6 +15,8 @@
#include "repack.h"
#include "shallow.h"
#include "list-objects-filter-options.h"
+#include "oidset.h"
+#include "hex.h"
#define ALL_INTO_ONE 1
#define LOOSEN_UNREACHABLE 2
@@ -143,6 +145,7 @@ int cmd_repack(int argc,
struct string_list_item *item;
struct string_list names = STRING_LIST_INIT_DUP;
struct existing_packs existing = EXISTING_PACKS_INIT;
+ struct oidset drop_oids = OIDSET_INIT;
struct pack_geometry geometry = { 0 };
struct tempfile *refs_snapshot = NULL;
int i, ret;
@@ -313,6 +316,20 @@ int cmd_repack(int argc,
die(_("--drop-filtered requires a promisor remote"));
write_bitmaps = 0;
+
+ ret = enumerate_promisor_blobs(repo, &po_args.filter_options, &drop_oids);
+
+ if (ret)
+ goto cleanup;
+
+ if (dry_run) {
+ struct oidset_iter iter;
+ const struct object_id *oid;
+
+ oidset_iter_init(&drop_oids, &iter);
+ while ((oid = oidset_iter_next(&iter)))
+ printf("%s\n", oid_to_hex(oid));
+ }
}
if (delete_redundant && repo->repository_format_precious_objects)
@@ -608,7 +625,7 @@ int cmd_repack(int argc,
}
}
- if (po_args.filter_options.choice) {
+ if (po_args.filter_options.choice && !drop_filtered) {
struct write_pack_opts opts = {
.po_args = &po_args,
.destination = filter_to,
@@ -701,6 +718,7 @@ int cmd_repack(int argc,
cleanup:
string_list_clear(&keep_pack_list, 0);
string_list_clear(&names, 1);
+ oidset_clear(&drop_oids);
existing_packs_release(&existing);
pack_geometry_release(&geometry);
pack_objects_args_release(&po_args);
diff --git a/repack-filtered.c b/repack-filtered.c
index edcf7667c5..217fc54d7b 100644
--- a/repack-filtered.c
+++ b/repack-filtered.c
@@ -3,6 +3,12 @@
#include "repository.h"
#include "run-command.h"
#include "string-list.h"
+#include "hex.h"
+#include "packfile.h"
+#include "list-objects-filter-options.h"
+#include "list-objects-filter.h"
+#include "odb.h"
+#include "promisor-remote.h"
int write_filtered_pack(const struct write_pack_opts *opts,
struct existing_packs *existing,
@@ -49,3 +55,77 @@ int write_filtered_pack(const struct write_pack_opts *opts,
return finish_pack_objects_cmd(existing->repo->hash_algo, opts, &cmd,
names);
}
+
+struct collect_cb_data {
+ struct repository *repo;
+ struct oidset *set;
+};
+
+static int collect_promisor_blob(const struct object_id *oid,
+ struct object_info *oi UNUSED,
+ void *cb_data)
+{
+ struct collect_cb_data *data = cb_data;
+ struct object_info info = OBJECT_INFO_INIT;
+ enum object_type type;
+
+ info.typep = &type;
+
+ /*
+ * Use OBJECT_INFO_SKIP_FETCH_OBJECT to avoid triggering a
+ * lazy fetch while collecting promisor blobs.
+ */
+ if (odb_read_object_info_extended(data->repo->objects, oid, &info,
+ OBJECT_INFO_SKIP_FETCH_OBJECT) < 0)
+ return 0;
+
+ if (type == OBJ_BLOB)
+ oidset_insert(data->set, oid);
+
+ return 0;
+}
+
+int enumerate_promisor_blobs(struct repository *repo,
+ const struct list_objects_filter_options *filter,
+ struct oidset *to_drop)
+{
+ struct oidset all_promisor_blobs = OIDSET_INIT;
+ struct collect_cb_data cb = {
+ .repo = repo,
+ .set = &all_promisor_blobs
+ };
+ int ret = 0;
+
+ /*
+ * The caller (cmd_repack) is responsible for validating that a
+ * blob:limit filter and a promisor remote are present before
+ * calling this function.
+ *
+ * Walk only promisor objects. Every object visited here is
+ * guaranteed to be recoverable from the promisor remote, so
+ * it is safe to drop.
+ *
+ * We do not use write_filtered_pack() here because git repack
+ * routes promisor objects through repack_promisor_objects()
+ * before the filter machinery runs, so the filtered pack never
+ * contains promisor blobs. Direct enumeration via
+ * ODB_FOR_EACH_OBJECT_PROMISOR_ONLY is the correct approach.
+ */
+ ret = odb_for_each_object(repo->objects, NULL,
+ collect_promisor_blob, &cb,
+ ODB_FOR_EACH_OBJECT_PROMISOR_ONLY);
+ if (ret)
+ goto cleanup;
+
+ /*
+ * Apply the filter to find which blobs exceed the threshold.
+ */
+ ret = list_objects_filter__filter_oidset(repo,
+ (struct list_objects_filter_options *)filter,
+ &all_promisor_blobs,
+ to_drop);
+
+cleanup:
+ oidset_clear(&all_promisor_blobs);
+ return ret;
+}
diff --git a/repack.h b/repack.h
index a5a3f7c6ba..61e554e4ed 100644
--- a/repack.h
+++ b/repack.h
@@ -167,6 +167,10 @@ int write_filtered_pack(const struct write_pack_opts *opts,
struct existing_packs *existing,
struct string_list *names);
+int enumerate_promisor_blobs(struct repository *repo,
+ const struct list_objects_filter_options *filter,
+ struct oidset *to_drop);
+
int write_cruft_pack(const struct write_pack_opts *opts,
const char *cruft_expiration,
unsigned long combine_cruft_below_size,
diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh
index 65be756e33..cbdb580702 100755
--- a/t/t7706-repack-drop-filtered.sh
+++ b/t/t7706-repack-drop-filtered.sh
@@ -1,9 +1,36 @@
#!/bin/sh
-test_description='git repack --drop-filtered option validation'
+test_description='git repack --drop-filtered enumerates filtered promisor blobs'
. ./test-lib.sh
+delete_object () {
+ local repo="$1" &&
+ local obj="$2" &&
+ local path="$repo/.git/objects/$(test_oid_to_path "$obj")" &&
+ rm "$path"
+}
+
+# pack the objects into a promisor pack inside "repo". it is a pack
+# accompanied by an empty ".promisor" marker file. objects
+# in such a pack are treated as recoverable from the promisor remote.
+pack_as_from_promisor () {
+ HASH=$(git -C repo pack-objects .git/objects/pack/pack) &&
+ >repo/.git/objects/pack/pack-$HASH.promisor &&
+ echo $HASH
+}
+
+# write a blob of $1 bytes into "repo", record it as coming from the
+# promisor remote, and remove the loose copy so the object is only
+# present in the promisor pack
+promisor_blob () {
+ test-tool genrandom "$1" "$2" >blob_content &&
+ OID=$(git -C repo hash-object -w --stdin <blob_content) &&
+ printf "%s\n" "$OID" | pack_as_from_promisor >/dev/null &&
+ delete_object repo "$OID" &&
+ echo "$OID"
+}
+
# checks for options validations before any promisor walk
test_expect_success 'setup plain repo for validation' '
git init plain &&
@@ -46,4 +73,59 @@ test_expect_success '--drop-filtered fails without a promisor remote' '
test_grep "drop-filtered requires a promisor remote" err
'
+# enumeration tests using promisor pack
+test_expect_success 'setup repo with a promisor remote' '
+ rm -rf repo &&
+ test_create_repo repo &&
+ test_commit -C repo base &&
+
+ # mark the repo as a partial clone with a promisor remote so the
+ # promisor walk and the safety guard are satisfied
+ git -C repo config core.repositoryformatversion 1 &&
+ git -C repo config extensions.partialclone origin &&
+ git -C repo config remote.origin.promisor true &&
+ git -C repo config remote.origin.url "." &&
+
+ BIG=$(promisor_blob big 3072) &&
+ SMALL=$(promisor_blob small 512) &&
+ echo "$BIG" >big_oid &&
+ echo "$SMALL" >small_oid
+'
+
+test_expect_success 'promisor blob over the threshold is listed' '
+ BIG=$(cat big_oid) &&
+ SMALL=$(cat small_oid) &&
+
+ git -C repo -c repack.writeBitmaps=false \
+ repack --drop-filtered --filter=blob:limit=1k --dry-run -a >out &&
+
+ test_grep "$BIG" out &&
+ test_grep ! "$SMALL" out
+'
+
+test_expect_success 'locally created blob is never listed' '
+ BIG=$(cat big_oid) &&
+
+ # large blob that exists only locally must never be a drop candidate.
+ # dropping it would be unrecoverable
+ test-tool genrandom local 4096 >local_content &&
+ LOCAL=$(git -C repo hash-object -w --stdin <local_content) &&
+
+ git -C repo -c repack.writeBitmaps=false \
+ repack --drop-filtered --filter=blob:limit=1k --dry-run -a >out &&
+
+ test_grep "$BIG" out &&
+ test_grep ! "$LOCAL" out
+'
+
+test_expect_success '--dry-run does not remove the filtered objects' '
+ BIG=$(cat big_oid) &&
+
+ git -C repo -c repack.writeBitmaps=false \
+ repack --drop-filtered --filter=blob:limit=1k --dry-run -a >out &&
+
+ # candidate blob must still be present after a dry run
+ git -C repo cat-file -e "$BIG"
+'
+
test_done
--
2.54.0
^ permalink raw reply related [flat|nested] 46+ messages in thread* [GSoC PATCH v2 5/7] builtin/repack: actually drop filtered promisor blobs
2026-07-30 17:41 ` [GSoC PATCH v2 " Siddharth Shrimali
` (3 preceding siblings ...)
2026-07-30 17:41 ` [GSoC PATCH v2 4/7] builtin/repack: enumerate promisor blobs for --drop-filtered Siddharth Shrimali
@ 2026-07-30 17:41 ` Siddharth Shrimali
2026-07-30 17:41 ` [GSoC PATCH v2 6/7] builtin/repack: add safety guards for --drop-filtered Siddharth Shrimali
` (4 subsequent siblings)
9 siblings, 0 replies; 46+ messages in thread
From: Siddharth Shrimali @ 2026-07-30 17:41 UTC (permalink / raw)
To: git
Cc: gitster, christian.couder, siddharthasthana31, me, ps,
johannes.schindelin, l.s.r, r.siddharth.shrimali
Make --drop-filtered remove the enumerated promisor blobs instead of
only listing them.
The drop set is computed before repack_promisor_objects() runs, and on
a real run it is passed in so the rebuilt promisor pack omits those
blobs. --drop-filtered implies -d so the old promisor packs, which
still contain the dropped blobs, are removed. Without this the blobs
would survive in the redundant packs. The existing repack machinery
performs the write-before-delete and fsync, so the drop is crash-safe.
The dropped blobs become absent locally but remain recoverable from the
promisor remote, so a later access lazy-fetches them back
transparently. --dry-run keeps its previous behavior, i.e. it lists the
candidates and changes nothing.
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>
Signed-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>
---
builtin/repack.c | 17 ++++++++++-------
repack-filtered.c | 1 +
t/t7706-repack-drop-filtered.sh | 12 ++++++++++++
3 files changed, 23 insertions(+), 7 deletions(-)
diff --git a/builtin/repack.c b/builtin/repack.c
index 8cb92d1a62..9a15ab1f2a 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -273,18 +273,12 @@ int cmd_repack(int argc,
die_for_incompatible_opt2(drop_filtered, "--drop-filtered",
!!filter_to, "--filter-to");
- die_for_incompatible_opt2(drop_filtered, "--drop-filtered",
- write_bitmaps > 0, "--write-bitmap-index");
-
if (dry_run && !drop_filtered)
die(_("--dry-run only takes effect with --drop-filtered"));
if (drop_filtered) {
int bitmaps_from_cmdline = (write_bitmaps != write_bitmaps_before_parse);
- if (!dry_run)
- die(_("--drop-filtered doesn't work without --dry-run yet"));
-
if (!po_args.filter_options.choice)
die(_("--drop-filtered requires --filter"));
@@ -317,6 +311,14 @@ int cmd_repack(int argc,
write_bitmaps = 0;
+ /*
+ * Dropping objects means rebuilding the promisor packs
+ * without them and then removing the old packs, so the
+ * redundant packs must be deleted. Imply -d on a real run.
+ */
+ if (!dry_run)
+ delete_redundant = 1;
+
ret = enumerate_promisor_blobs(repo, &po_args.filter_options, &drop_oids);
if (ret)
@@ -442,7 +444,8 @@ int cmd_repack(int argc,
strvec_push(&cmd.args, "--delta-islands");
if (pack_everything & ALL_INTO_ONE) {
- repack_promisor_objects(repo, &po_args, &names, packtmp, NULL);
+ repack_promisor_objects(repo, &po_args, &names, packtmp,
+ (drop_filtered && !dry_run) ? &drop_oids : NULL);
if (existing_packs_has_non_kept(&existing) &&
delete_redundant &&
diff --git a/repack-filtered.c b/repack-filtered.c
index 217fc54d7b..08796818d8 100644
--- a/repack-filtered.c
+++ b/repack-filtered.c
@@ -119,6 +119,7 @@ int enumerate_promisor_blobs(struct repository *repo,
/*
* Apply the filter to find which blobs exceed the threshold.
+ * The caller has to_drop and is responsible for clearing it.
*/
ret = list_objects_filter__filter_oidset(repo,
(struct list_objects_filter_options *)filter,
diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh
index cbdb580702..b3e493e851 100755
--- a/t/t7706-repack-drop-filtered.sh
+++ b/t/t7706-repack-drop-filtered.sh
@@ -128,4 +128,16 @@ test_expect_success '--dry-run does not remove the filtered objects' '
git -C repo cat-file -e "$BIG"
'
+test_expect_success '--drop-filtered removes the promisor blob locally' '
+ BIG=$(cat big_oid) &&
+ SMALL=$(cat small_oid) &&
+
+ git -C repo -c repack.writeBitmaps=false \
+ repack --drop-filtered --filter=blob:limit=1k -a &&
+
+ git -C repo cat-file --batch-all-objects --batch-check="%(objectname)" >present &&
+ ! grep -q "$BIG" present &&
+ grep -q "$SMALL" present
+'
+
test_done
--
2.54.0
^ permalink raw reply related [flat|nested] 46+ messages in thread* [GSoC PATCH v2 6/7] builtin/repack: add safety guards for --drop-filtered
2026-07-30 17:41 ` [GSoC PATCH v2 " Siddharth Shrimali
` (4 preceding siblings ...)
2026-07-30 17:41 ` [GSoC PATCH v2 5/7] builtin/repack: actually drop filtered promisor blobs Siddharth Shrimali
@ 2026-07-30 17:41 ` Siddharth Shrimali
2026-08-04 21:13 ` Siddharth Asthana
2026-07-30 17:41 ` [GSoC PATCH v2 7/7] Documentation/git-repack: document --drop-filtered and --dry-run Siddharth Shrimali
` (3 subsequent siblings)
9 siblings, 1 reply; 46+ messages in thread
From: Siddharth Shrimali @ 2026-07-30 17:41 UTC (permalink / raw)
To: git
Cc: gitster, christian.couder, siddharthasthana31, me, ps,
johannes.schindelin, l.s.r, r.siddharth.shrimali
--drop-filtered removes local promisor blobs. That is only safe when the
repository is not mid-operation and when the blobs are not actively in
use, so add two guards, both skipped for bare repositories which have
neither a worktree nor an index.
First, refuse to run while a merge, rebase, am, cherry-pick, revert, or
bisect is in progress. During these operations the working tree and
index are in an intermediate state, and rewriting packs and deleting
objects underneath a half-finished operation is unsafe.
Second, refuse to drop a blob that the current index references. Such a
blob is needed by the working tree, so dropping it would only cause the
next command that touches the worktree to lazy-fetch it straight back,
reclaiming nothing. The offending path is reported so the user can see
why the drop was refused.
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>
Signed-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>
---
builtin/repack.c | 47 +++++++++++++++++++++++++++++++++
t/t7706-repack-drop-filtered.sh | 36 +++++++++++++++++++++++++
2 files changed, 83 insertions(+)
diff --git a/builtin/repack.c b/builtin/repack.c
index 9a15ab1f2a..2339bcaac4 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -17,6 +17,8 @@
#include "list-objects-filter-options.h"
#include "oidset.h"
#include "hex.h"
+#include "wt-status.h"
+#include "read-cache-ll.h"
#define ALL_INTO_ONE 1
#define LOOSEN_UNREACHABLE 2
@@ -309,6 +311,28 @@ int cmd_repack(int argc,
if (!repo_has_promisor_remote(repo))
die(_("--drop-filtered requires a promisor remote"));
+ /*
+ * refuse to drop objects while another operation is in
+ * progress. the working tree and index are in an
+ * intermediate state, and rewriting packs in a half-finished
+ * merge/rebase/cherry-pick/revert/bisect is unsafe
+ * bare repositories have no such state, so the check
+ * is skipped there
+ */
+ if (!is_bare_repository(repo)) {
+ struct wt_status_state state = { 0 };
+
+ wt_status_get_state(repo, &state, 0);
+ if (state.merge_in_progress || state.revert_in_progress ||
+ state.rebase_in_progress ||state.bisect_in_progress ||
+ state.cherry_pick_in_progress ||state.am_in_progress||
+ state.rebase_interactive_in_progress) {
+ wt_status_state_free_buffers(&state);
+ die(_("--drop-filtered cannot be used while another operation is in progress"));
+ }
+ wt_status_state_free_buffers(&state);
+ }
+
write_bitmaps = 0;
/*
@@ -324,6 +348,29 @@ int cmd_repack(int argc,
if (ret)
goto cleanup;
+ /*
+ * refuse to drop blobs that the current index references.
+ * dropping such a blob would cause the very next command
+ * that touches the worktree to lazy-fetch it straight back, so
+ * the drop would reclaim nothing. bare repositories have no
+ * index, so the check is skipped there.
+ */
+ if (!is_bare_repository(repo) && oidset_size(&drop_oids)) {
+ struct index_state *istate = repo->index;
+ unsigned int i;
+
+ if (repo_read_index(repo) < 0)
+ die(_("could not read the index"));
+
+ for (i = 0; i < istate->cache_nr; i++) {
+ const struct cache_entry *ce = istate->cache[i];
+
+ if (oidset_contains(&drop_oids, &ce->oid))
+ die(_("cannot drop '%s' (%s): it is referenced by the current index"),
+ ce->name, oid_to_hex(&ce->oid));
+ }
+ }
+
if (dry_run) {
struct oidset_iter iter;
const struct object_id *oid;
diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh
index b3e493e851..dabed97541 100755
--- a/t/t7706-repack-drop-filtered.sh
+++ b/t/t7706-repack-drop-filtered.sh
@@ -140,4 +140,40 @@ test_expect_success '--drop-filtered removes the promisor blob locally' '
grep -q "$SMALL" present
'
+test_expect_success '--drop-filtered refuses when a merge is in progress' '
+ test_when_finished "git -C repo merge --abort || :" &&
+
+ # creat a conflicting merge so wt_status reports it
+ git -C repo checkout -B mergebase base &&
+ echo one >repo/conflict.txt &&
+ git -C repo add conflict.txt &&
+ git -C repo commit -m one &&
+
+ git -C repo checkout -B mergeother base &&
+ echo two >repo/conflict.txt &&
+ git -C repo add conflict.txt &&
+ git -C repo commit -m two &&
+
+ test_must_fail git -C repo merge mergebase &&
+
+ test_must_fail git -C repo -c repack.writeBitmaps=false \
+ repack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err &&
+ test_grep "in progress" err
+'
+
+
+test_expect_success '--drop-filtered refuses to drop an index-referenced blob' '
+ # create a large blob, add it to the index and make it a promisor object
+ # so the index references it and enumeration picks it up
+ test-tool genrandom idx 4096 >repo/tracked-big.bin &&
+ git -C repo add tracked-big.bin &&
+ OID=$(git -C repo rev-parse :tracked-big.bin) &&
+ printf "%s\n" "$OID" | pack_as_from_promisor >/dev/null &&
+ delete_object repo "$OID" &&
+
+ test_must_fail git -C repo -c repack.writeBitmaps=false \
+ repack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err &&
+ test_grep "referenced by the current index" err
+'
+
test_done
--
2.54.0
^ permalink raw reply related [flat|nested] 46+ messages in thread* Re: [GSoC PATCH v2 6/7] builtin/repack: add safety guards for --drop-filtered
2026-07-30 17:41 ` [GSoC PATCH v2 6/7] builtin/repack: add safety guards for --drop-filtered Siddharth Shrimali
@ 2026-08-04 21:13 ` Siddharth Asthana
0 siblings, 0 replies; 46+ messages in thread
From: Siddharth Asthana @ 2026-08-04 21:13 UTC (permalink / raw)
To: Siddharth Shrimali, git
Cc: gitster, christian.couder, me, ps, johannes.schindelin, l.s.r
On 30/07/26 23:11, Siddharth Shrimali wrote:
> --drop-filtered removes local promisor blobs. That is only safe when the
> repository is not mid-operation and when the blobs are not actively in
> use, so add two guards, both skipped for bare repositories which have
> neither a worktree nor an index.
>
> First, refuse to run while a merge, rebase, am, cherry-pick, revert, or
> bisect is in progress. During these operations the working tree and
> index are in an intermediate state, and rewriting packs and deleting
> objects underneath a half-finished operation is unsafe.
>
> Second, refuse to drop a blob that the current index references. Such a
Index guard looks good to me. Same idea as on the RFC.
Thanks.
Siddharth
> blob is needed by the working tree, so dropping it would only cause the
> next command that touches the worktree to lazy-fetch it straight back,
> reclaiming nothing. The offending path is reported so the user can see
> why the drop was refused.
>
> Mentored-by: Christian Couder <christian.couder@gmail.com>
> Mentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>
> Signed-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>
> ---
> builtin/repack.c | 47 +++++++++++++++++++++++++++++++++
> t/t7706-repack-drop-filtered.sh | 36 +++++++++++++++++++++++++
> 2 files changed, 83 insertions(+)
>
> diff --git a/builtin/repack.c b/builtin/repack.c
> index 9a15ab1f2a..2339bcaac4 100644
> --- a/builtin/repack.c
> +++ b/builtin/repack.c
> @@ -17,6 +17,8 @@
> #include "list-objects-filter-options.h"
> #include "oidset.h"
> #include "hex.h"
> +#include "wt-status.h"
> +#include "read-cache-ll.h"
>
> #define ALL_INTO_ONE 1
> #define LOOSEN_UNREACHABLE 2
> @@ -309,6 +311,28 @@ int cmd_repack(int argc,
> if (!repo_has_promisor_remote(repo))
> die(_("--drop-filtered requires a promisor remote"));
>
> + /*
> + * refuse to drop objects while another operation is in
> + * progress. the working tree and index are in an
> + * intermediate state, and rewriting packs in a half-finished
> + * merge/rebase/cherry-pick/revert/bisect is unsafe
> + * bare repositories have no such state, so the check
> + * is skipped there
> + */
> + if (!is_bare_repository(repo)) {
> + struct wt_status_state state = { 0 };
> +
> + wt_status_get_state(repo, &state, 0);
> + if (state.merge_in_progress || state.revert_in_progress ||
> + state.rebase_in_progress ||state.bisect_in_progress ||
> + state.cherry_pick_in_progress ||state.am_in_progress||
> + state.rebase_interactive_in_progress) {
> + wt_status_state_free_buffers(&state);
> + die(_("--drop-filtered cannot be used while another operation is in progress"));
> + }
> + wt_status_state_free_buffers(&state);
> + }
> +
> write_bitmaps = 0;
>
> /*
> @@ -324,6 +348,29 @@ int cmd_repack(int argc,
> if (ret)
> goto cleanup;
>
> + /*
> + * refuse to drop blobs that the current index references.
> + * dropping such a blob would cause the very next command
> + * that touches the worktree to lazy-fetch it straight back, so
> + * the drop would reclaim nothing. bare repositories have no
> + * index, so the check is skipped there.
> + */
> + if (!is_bare_repository(repo) && oidset_size(&drop_oids)) {
> + struct index_state *istate = repo->index;
> + unsigned int i;
> +
> + if (repo_read_index(repo) < 0)
> + die(_("could not read the index"));
> +
> + for (i = 0; i < istate->cache_nr; i++) {
> + const struct cache_entry *ce = istate->cache[i];
> +
> + if (oidset_contains(&drop_oids, &ce->oid))
> + die(_("cannot drop '%s' (%s): it is referenced by the current index"),
> + ce->name, oid_to_hex(&ce->oid));
> + }
> + }
> +
> if (dry_run) {
> struct oidset_iter iter;
> const struct object_id *oid;
> diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh
> index b3e493e851..dabed97541 100755
> --- a/t/t7706-repack-drop-filtered.sh
> +++ b/t/t7706-repack-drop-filtered.sh
> @@ -140,4 +140,40 @@ test_expect_success '--drop-filtered removes the promisor blob locally' '
> grep -q "$SMALL" present
> '
>
> +test_expect_success '--drop-filtered refuses when a merge is in progress' '
> + test_when_finished "git -C repo merge --abort || :" &&
> +
> + # creat a conflicting merge so wt_status reports it
> + git -C repo checkout -B mergebase base &&
> + echo one >repo/conflict.txt &&
> + git -C repo add conflict.txt &&
> + git -C repo commit -m one &&
> +
> + git -C repo checkout -B mergeother base &&
> + echo two >repo/conflict.txt &&
> + git -C repo add conflict.txt &&
> + git -C repo commit -m two &&
> +
> + test_must_fail git -C repo merge mergebase &&
> +
> + test_must_fail git -C repo -c repack.writeBitmaps=false \
> + repack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err &&
> + test_grep "in progress" err
> +'
> +
> +
> +test_expect_success '--drop-filtered refuses to drop an index-referenced blob' '
> + # create a large blob, add it to the index and make it a promisor object
> + # so the index references it and enumeration picks it up
> + test-tool genrandom idx 4096 >repo/tracked-big.bin &&
> + git -C repo add tracked-big.bin &&
> + OID=$(git -C repo rev-parse :tracked-big.bin) &&
> + printf "%s\n" "$OID" | pack_as_from_promisor >/dev/null &&
> + delete_object repo "$OID" &&
> +
> + test_must_fail git -C repo -c repack.writeBitmaps=false \
> + repack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err &&
> + test_grep "referenced by the current index" err
> +'
> +
> test_done
^ permalink raw reply [flat|nested] 46+ messages in thread
* [GSoC PATCH v2 7/7] Documentation/git-repack: document --drop-filtered and --dry-run
2026-07-30 17:41 ` [GSoC PATCH v2 " Siddharth Shrimali
` (5 preceding siblings ...)
2026-07-30 17:41 ` [GSoC PATCH v2 6/7] builtin/repack: add safety guards for --drop-filtered Siddharth Shrimali
@ 2026-07-30 17:41 ` Siddharth Shrimali
2026-07-31 15:33 ` [GSoC PATCH v2 0/7] repack: add --drop-filtered to reclaim space in partial clones Junio C Hamano
` (2 subsequent siblings)
9 siblings, 0 replies; 46+ messages in thread
From: Siddharth Shrimali @ 2026-07-30 17:41 UTC (permalink / raw)
To: git
Cc: gitster, christian.couder, siddharthasthana31, me, ps,
johannes.schindelin, l.s.r, r.siddharth.shrimali
Describe the new --drop-filtered and --dry-run options: what they do,
only blob:limit filters are supported for now, a promisor remote is
required, --drop-filtered requires -a and implies -d so the redundant
packs are actually removed, its incompatibilities with --filter-to and
bitmap writing, and the safety guards that refuse to run mid-operation
or to drop index-referenced blobs.
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>
Signed-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>
---
Documentation/git-repack.adoc | 35 +++++++++++++++++++++++++++++++++++
1 file changed, 35 insertions(+)
diff --git a/Documentation/git-repack.adoc b/Documentation/git-repack.adoc
index 72c42015e2..9efff838f2 100644
--- a/Documentation/git-repack.adoc
+++ b/Documentation/git-repack.adoc
@@ -12,6 +12,7 @@ SYNOPSIS
'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]
[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]
[--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]
+ [--filter=<filter-spec>] [--drop-filtered [--dry-run]]]
DESCRIPTION
-----------
@@ -182,6 +183,40 @@ depth is 4095.
`objects` and `objects/info/alternates` sections of
linkgit:gitrepository-layout[5].
+--drop-filtered::
+ Delete the local objects that match the `--filter` specification
+ instead of keeping them in a separate packfile, reclaiming the
+ disk space they occupy. This is intended for partial clones,
+ where the filtered objects are promisor objects that remain
+ recoverable from the promisor remote and are lazily re-fetched
+ on demand when they are next needed.
++
+Only large blobs are supported for now, so `--filter=blob:limit=<n>`
+is currently the only accepted filter. Because dropped objects must be
+recoverable, this option requires a promisor remote to be configured
+and refuses to run otherwise.
++
+This option requires `-a`, and implies `-d`: the objects are dropped by
+rebuilding the promisor pack without them and then removing the now
+redundant old packs, so the redundant packs must be deleted for the
+space to actually be reclaimed. It is incompatible with `--filter-to`
+and with bitmap writing (`-b`/`--write-bitmap-index`), since filtering
+breaks the single-pack closure that bitmaps require. A bitmap setting
+coming from configuration is silently disabled for the duration of the
+command.
++
+As a safety measure, `--drop-filtered` refuses to run while another
+operation (merge, rebase, am, cherry-pick, revert, or bisect) is in
+progress, and refuses to drop any blob that the current index
+references, since such a blob would only be lazily re-fetched by the
+next command that inspects the working tree. These checks are skipped
+in bare repositories, which have neither a working tree nor an index.
+
+--dry-run::
+ Only meaningful with `--drop-filtered`. List the objects that
+ would be dropped, one object ID per line, without rebuilding any
+ pack or deleting anything.
+
-b::
--write-bitmap-index::
Write a reachability bitmap index as part of the repack. This
--
2.54.0
^ permalink raw reply related [flat|nested] 46+ messages in thread* Re: [GSoC PATCH v2 0/7] repack: add --drop-filtered to reclaim space in partial clones
2026-07-30 17:41 ` [GSoC PATCH v2 " Siddharth Shrimali
` (6 preceding siblings ...)
2026-07-30 17:41 ` [GSoC PATCH v2 7/7] Documentation/git-repack: document --drop-filtered and --dry-run Siddharth Shrimali
@ 2026-07-31 15:33 ` Junio C Hamano
2026-08-01 18:19 ` Siddharth Shrimali
2026-08-04 21:12 ` Siddharth Asthana
2026-08-06 11:21 ` [GSoC PATCH v3 " Siddharth Shrimali
9 siblings, 1 reply; 46+ messages in thread
From: Junio C Hamano @ 2026-07-31 15:33 UTC (permalink / raw)
To: Siddharth Shrimali
Cc: git, christian.couder, siddharthasthana31, me, ps,
johannes.schindelin, l.s.r
Siddharth Shrimali <r.siddharth.shrimali@gmail.com> writes:
> How it works:
> * Enumerate promisor objects directly (ODB_FOR_EACH_OBJECT_PROMISOR_ONLY)
> and select the blobs exceeding the filter threshold. Every enumerated
> object is a promisor object by construction, so it is guaranteed
> recoverable and locally-created objects are never candidates.
By 'by construction', do you mean 'It is guaranteed recoverable, as
long as ODB_FOR_EACH_OBJECT_PROMISOR_ONLY is working correctly'?
Since I do not use it, I do not personally trust promisor-based
traversal all that much, and it would be great if we could hear from
other practitioners that this really works well.
> * Rebuild the promisor pack without the selected blobs, reusing the
> existing repack machinery, so the drop is crash-safe (write, fsync,
> install, then delete the old pack).
This is sensible, as long as this repacking is done only with
locally available data, without dynamically pulling in lazy objects
from the promisor (which would defeat the whole point ;-)).
Presumably, this rebuilding is done without an extra traversal,
driven instead by the list of enumerated promisor objects we
constructed above (excluding the unwanted ones)?
> * --dry-run lists the candidates and changes nothing.
I wonder whether size is the only criterion we would want to use
when choosing what to discard among objects we know the promisor can
give us on-demand. It is, of course, perfectly fine to make it the
only condition in this first effort, but it would help to imagine
what other criteria we might want in the future and how they would
fit into the framework you establish with this series. Ensuring
that the framework is easily extensible with a future set of rules
will keep us from painting ourselves into a corner.
> Safety guards refuse to run while a merge, rebase, am, cherry-pick,
> revert, or bisect is in progress, and refuse to drop a blob referenced
> by the current index (it would only be lazily re-fetched by the next
> worktree command). Both are skipped for bare repositories.
I assume you do not mean a race where an operation wants to write a
blob, finds that an identical one that came from the promisor remote
already exists locally, refrains from writing another copy, and the
drop-filtered operation removes the blob at the right moment.
Rather, you likely have in mind an operation that stops, gives
control back to the user, and, while the user ponders the situation,
the drop-filtered operation kicks in and removes the blobs involved
in the operation in progress. Am I reading you correctly?
Even in either of these situations, I do not quite see why the
safeguards are necessary. The operation completes, or stays stopped
in the middle. The user's next move (whether they issue a new
command after completion or resume the interrupted operation) will
automatically lazy-refetch what the drop-filtered operation
discarded as needed, will it not?
^ permalink raw reply [flat|nested] 46+ messages in thread* Re: [GSoC PATCH v2 0/7] repack: add --drop-filtered to reclaim space in partial clones
2026-07-31 15:33 ` [GSoC PATCH v2 0/7] repack: add --drop-filtered to reclaim space in partial clones Junio C Hamano
@ 2026-08-01 18:19 ` Siddharth Shrimali
2026-08-02 2:18 ` Junio C Hamano
0 siblings, 1 reply; 46+ messages in thread
From: Siddharth Shrimali @ 2026-08-01 18:19 UTC (permalink / raw)
To: Junio C Hamano, christian.couder, siddharthasthana31
Cc: git, me, ps, johannes.schindelin, l.s.r, ttaylorr
Hi,
thanks for the review Junio!
On Fri, 31 Jul 2026 at 21:04, Junio C Hamano <gitster@pobox.com> wrote:
> By 'by construction', do you mean 'It is guaranteed recoverable, as
> long as ODB_FOR_EACH_OBJECT_PROMISOR_ONLY is working correctly'?
yes, that is what I meant. the object is recoverable because it came from a
promisor pack (due to a .promisor file), so the remote has promised to
give it back.
"by construction" means "recoverable as long as the promisor-only walk correctly
picks out promisor objects".
> Since I do not use it, I do not personally trust promisor-based
> traversal all that much, and it would be great if we could hear from
> other practitioners that this really works well.
i'll also be glad to hear from people who actually use partial clone
about whether
leaning on the promisor-only walk here is a good idea for now, until the
remote-object-info side of the cat-file protocol lands, which would let us
verify against the remote directly.
> This is sensible, as long as this repacking is done only with
> locally available data, without dynamically pulling in lazy objects
> from the promisor (which would defeat the whole point ;-)).
right, i made sure of that :)
enumeration passes OBJECT_INFO_SKIP_FETCH_OBJECT on
every object-info lookup, so it never triggers a lazy fetch.
The rebuild is local too: it only repacks promisor objects that are
already present.
I confirmed this by tracing a real drop and by moving the promisor remote away
entirely before a drop, it still completed, so it clearly did not need
the remote
> Presumably, this rebuilding is done without an extra traversal,
> driven instead by the list of enumerated promisor objects we
> constructed above (excluding the unwanted ones)?
not quite, there are two walks right now. First,
enumerate_promisor_blobs() walks the
promisor objects to figure out what to drop. Then
repack_promisor_objects() does its own
promisor-only walk to rebuild the pack, skipping anything in that drop set.
so the rebuild does use the drop set, but through a second walk, not by directly
reusing the first list.
> I wonder whether size is the only criterion we would want to use
> when choosing what to discard among objects we know the promisor can
> give us on-demand. It is, of course, perfectly fine to make it the
> only condition in this first effort, but it would help to imagine
> what other criteria we might want in the future and how they would
> fit into the framework you establish with this series. Ensuring
> that the framework is easily extensible with a future set of rules
> will keep us from painting ourselves into a corner.
true, i agree..
size (blob:limit) is the only rule for now, but its easy to imagine others:
how old an object is or when it was last used, its path, its type, or whether
its still reachable from the current branch. The design should handle
those without
much trouble. Enumeration builds a set of promisor objects, and then one step
narrows that set down to what we actually drop.
Right now that step is just the blob:limit filter. A new rule would
plug in at the same spot,
narrowing the same set, so the overall "list them, then pick what to drop" shape
would not change
> I assume you do not mean a race where an operation wants to write a
> blob, finds that an identical one that came from the promisor remote
> already exists locally, refrains from writing another copy, and the
> drop-filtered operation removes the blob at the right moment.
> Rather, you likely have in mind an operation that stops, gives
> control back to the user, and, while the user ponders the situation,
> the drop-filtered operation kicks in and removes the blobs involved
> in the operation in progress. Am I reading you correctly?
um yes, the case i had in mind is the second one: an operation stops halfway,
hands control back to the user, and drop-filtered runs in that gap and
removes blobs the paused operation was using
> Even in either of these situations, I do not quite see why the
> safeguards are necessary. The operation completes, or stays stopped
> in the middle. The user's next move (whether they issue a new
> command after completion or resume the interrupted operation) will
> automatically lazy-refetch what the drop-filtered operation
> discarded as needed, will it not?
yes, you got that right. Since the objects are promised, whatever gets dropped
will just be lazy-refetched when the user runs the next command or resumes the
operation.
the guards avoid immediately re-downloading something we just dropped,
(which we can call as some wasted work : )), and a network fetch in
the middle of,
say, resolving a merge.
The index guard is the same story- the blob it protects would just be
re-fetched by
the next command anyway.
So they are a convenience to avoid pointless re-fetching, not a
correctness measure.
I am happy to drop them or keep them clearly documented as just that,
whichever the list prefers.
Thanks,
Siddharth Shrimali
^ permalink raw reply [flat|nested] 46+ messages in thread
* Re: [GSoC PATCH v2 0/7] repack: add --drop-filtered to reclaim space in partial clones
2026-08-01 18:19 ` Siddharth Shrimali
@ 2026-08-02 2:18 ` Junio C Hamano
2026-08-02 11:28 ` Siddharth Shrimali
0 siblings, 1 reply; 46+ messages in thread
From: Junio C Hamano @ 2026-08-02 2:18 UTC (permalink / raw)
To: Siddharth Shrimali
Cc: christian.couder, siddharthasthana31, git, me, ps,
johannes.schindelin, l.s.r, ttaylorr
Siddharth Shrimali <r.siddharth.shrimali@gmail.com> writes:
> So they are a convenience to avoid pointless re-fetching, not a
> correctness measure.
> I am happy to drop them or keep them clearly documented as just that,
> whichever the list prefers.
Doesn't it suggest that the "cull anything refetchable" feature can
gain a bit more smart? Given an object you know you fetched from a
promisor remote, are there cheap ways to determine how long you had
it in your repository? "This large blob can be refetched if we
wanted to, but we downloaded it just 20 minutes ago, so let's not
cull it just yet", or something like that, perhaps?
^ permalink raw reply [flat|nested] 46+ messages in thread
* Re: [GSoC PATCH v2 0/7] repack: add --drop-filtered to reclaim space in partial clones
2026-08-02 2:18 ` Junio C Hamano
@ 2026-08-02 11:28 ` Siddharth Shrimali
0 siblings, 0 replies; 46+ messages in thread
From: Siddharth Shrimali @ 2026-08-02 11:28 UTC (permalink / raw)
To: Junio C Hamano
Cc: christian.couder, siddharthasthana31, git, me, ps,
johannes.schindelin, l.s.r, ttaylorr
On Sun, 2 Aug 2026 at 07:48, Junio C Hamano <gitster@pobox.com> wrote:
>
> Siddharth Shrimali <r.siddharth.shrimali@gmail.com> writes:
> Doesn't it suggest that the "cull anything refetchable" feature can
> gain a bit more smart? Given an object you know you fetched from a
> promisor remote, are there cheap ways to determine how long you had
> it in your repository?
agreed,
I like this a lot, recency is what the index guard was reaching for: dont
drop something you are likely to want again right away.
the tricky part is that droppable objects are always in packs (a lazy fetch
produces a promisor pack, never a loose object, in every config i
tried), and packed objects dont carry a per-object timestamp on their
own.
The cheap signal available is the promisor packs own mtime: since
each lazy fetch writes its own pack, early on that mtime is a decent
proxy for "when did this object arrive". The catch is that once a repack
consolidates packs, that per-fetch granularity is lost and you only know
the age of the combined pack.
Git does already track per-object mtimes for cruft packs (via the
.mtimes file used for --cruft-expiration), so there is precedent for
age-based culling. whether something similar is worth doing for promisor
objects, so age survives repacking, would be a larger discussion
either way a "dont cull objects younger than <time>" rule fits the same
enumerate-then-select framework as just another predicate narrowing the
candidate set, so I'll note it as a promising follow-up criterion
Thanks,
Siddharth Shrimali
> "This large blob can be refetched if we
> wanted to, but we downloaded it just 20 minutes ago, so let's not
> cull it just yet", or something like that, perhaps?
^ permalink raw reply [flat|nested] 46+ messages in thread
* Re: [GSoC PATCH v2 0/7] repack: add --drop-filtered to reclaim space in partial clones
2026-07-30 17:41 ` [GSoC PATCH v2 " Siddharth Shrimali
` (7 preceding siblings ...)
2026-07-31 15:33 ` [GSoC PATCH v2 0/7] repack: add --drop-filtered to reclaim space in partial clones Junio C Hamano
@ 2026-08-04 21:12 ` Siddharth Asthana
2026-08-06 11:21 ` [GSoC PATCH v3 " Siddharth Shrimali
9 siblings, 0 replies; 46+ messages in thread
From: Siddharth Asthana @ 2026-08-04 21:12 UTC (permalink / raw)
To: Siddharth Shrimali, git
Cc: gitster, christian.couder, me, ps, johannes.schindelin, l.s.r
On 30/07/26 23:11, Siddharth Shrimali wrote:
> This is v2 of the series adding "git repack --drop-filtered" to reclaim
> disk space in partial clones by dropping large, locally-held promisor
> blobs that remain recoverable from the promisor remote. v1 was sent as
> an RFC [1].
>
> Partial clones let you work with large repositories without downloading
> every blob up front. Mising blobs are lazily fetched from the promisor
> remote on demand. Over time these accumulate locally and there is
> currently no safe, built-in way to reclaim that space short of
> re-cloning. This series adds that reverse direction: enumerate promisor
> blobs over a size threshold, drop them locally, and rely on the existing
> lazy-fetch machinery to bring them back transparently when needed.
>
> How it works:
> * Enumerate promisor objects directly (ODB_FOR_EACH_OBJECT_PROMISOR_ONLY)
> and select the blobs exceeding the filter threshold. Every enumerated
> object is a promisor object by construction, so it is guaranteed
> recoverable and locally-created objects are never candidates.
Thanks for v2. Guards, docs, and dropping the drop-log all match what we
have talked about on the RFC.
On "guaranteed": I would soften that a bit in next round. It is
recoverable in the same sense as the rest of partial clone, as long as
the promisor remote still has it. Fine for now, just a bit strong
without a remote check.
For the promisor-only walk: that matches how we already treat those
objects, so using it here looks right to me.
On the guards you already covered Junio's point well. I still like the
index one so we do not drop something and fetch it straight back. Mid-op
is more UX. Docs/cover can just say that clearly.
Thanks.
Siddharth
> * Rebuild the promisor pack without the selected blobs, reusing the
> existing repack machinery, so the drop is crash-safe (write, fsync,
> install, then delete the old pack).
> * --dry-run lists the candidates and changes nothing.
>
> Safety guards refuse to run while a merge, rebase, am, cherry-pick,
> revert, or bisect is in progress, and refuse to drop a blob referenced
> by the current index (it would only be lazily re-fetched by the next
> worktree command). Both are skipped for bare repositories.
>
> Changes since v1:
> * distinguish an explicit -b/--write-bitmap-index on the command line
> (reported as a conflict) from a repack.writeBitmaps config value
> (silently disabled for the command). This addresses Junio's review
> that the previous check could not tell the two apart
> * documented the choice to keep --dry-run as a separate option rather
> than --drop-filtered=<mode>
> * implemented the safety guards
> * Added git-repack documentation for --drop-filtered and --dry-run
> * Reorganised so enumerate_promisor_blobs() is introduced in its final
> signature
> * Distributed the tests into the commits that introduce each behavior,
> instead of a single standalone test commit.
> * Dropped the drop-log commit from this series
>
> To do:
> * Remote verification: verifying against the remote awaits the "remote-object-info"
> cat-file protocol command.
> * Drop log: introduce with the error-path change that reads it.
> * --verbose: space-reclaimed reporting.
>
> [1] https://lore.kernel.org/git/20260716132848.95982-1-r.siddharth.shrimali@gmail.com/
>
> Siddharth Shrimali (7):
> builtin/repack.c: add --drop-filtered and --dry-run options
> list-objects-filter: add list_objects_filter__filter_oidset()
> repack-promisor: allow excluding objects from the rebuilt promisor
> pack
> builtin/repack: enumerate promisor blobs for --drop-filtered
> builtin/repack: actually drop filtered promisor blobs
> builtin/repack: add safety guards for --drop-filtered
> Documentation/git-repack: document --drop-filtered and --dry-run
>
> Documentation/git-repack.adoc | 35 +++++++
> builtin/repack.c | 135 +++++++++++++++++++++++-
> list-objects-filter.c | 45 ++++++++
> list-objects-filter.h | 16 +++
> repack-filtered.c | 81 +++++++++++++++
> repack-promisor.c | 15 ++-
> repack.h | 8 +-
> t/meson.build | 1 +
> t/t7706-repack-drop-filtered.sh | 179 ++++++++++++++++++++++++++++++++
> 9 files changed, 511 insertions(+), 4 deletions(-)
> create mode 100755 t/t7706-repack-drop-filtered.sh
>
^ permalink raw reply [flat|nested] 46+ messages in thread* [GSoC PATCH v3 0/7] repack: add --drop-filtered to reclaim space in partial clones
2026-07-30 17:41 ` [GSoC PATCH v2 " Siddharth Shrimali
` (8 preceding siblings ...)
2026-08-04 21:12 ` Siddharth Asthana
@ 2026-08-06 11:21 ` Siddharth Shrimali
2026-08-06 11:21 ` [GSoC PATCH v3 1/7] builtin/repack.c: add --drop-filtered and --dry-run options Siddharth Shrimali
` (8 more replies)
9 siblings, 9 replies; 46+ messages in thread
From: Siddharth Shrimali @ 2026-08-06 11:21 UTC (permalink / raw)
To: git
Cc: gitster, christian.couder, siddharthasthana31, ttaylorr, me, ps,
johannes.schindelin, l.s.r, r.siddharth.shrimali
This is v3 of the series adding "git repack --drop-filtered" to reclaim
disk space in partial clones by dropping large, locally-held promisor
blobs that remain recoverable from the promisor remote. v2 was at [1].
Partial clones let you work with large repositories without downloading
every blob up front. Missing blobs are lazily fetched from the promisor
remote on demand. over time these accumulate locally and there is
currently no safe, built-in way to reclaim that space short of
re-cloning. This series adds that reverse direction: enumerate promisor
blobs over a size threshold, drop them locally, and rely on the existing
lazy-fetch machinery to bring them back transparently when needed.
How it works:
* enumerate promisor objects directly (ODB_FOR_EACH_OBJECT_PROMISOR_ONLY)
and select the blobs exceeding the filter threshold. Every enumerated
object is a promisor object, so it is recoverable from the promisor
remote as long as the remote still has it, the same assumption the
rest of partial clone relies on
* rebuild the promisor pack without the selected blobs, reusing the
existing repack machinery, so the drop is crash-safe (write, fsync,
install, then delete the old pack)
* --dry-run lists the candidates and changes nothing
the guards refuse to run while a merge, rebase, am, cherry-pick, revert,
or bisect is in progress, and refuse to drop a blob referenced by the
current index (it would only be lazily re-fetched by the next worktree
command). Dropped objects stay recoverable via lazy fetch, so these are
a convenience (avoid pointless re-fetch and a surprising mid-operation
fetch) rather than a correctness measure. Both are skipped for bare
repositories
Changes since v2:
* bitmap config: detect an explicit -b/--write-bitmap-index
with an option callback instead of a before/after snapshot, so an
explicit -b is always reported as a conflict, even when
repack.writeBitmaps is already true in config
* softened "guaranteed recoverable" to "recoverable as long as the
remote still has it"
* reframed the guards in the commit message and docs as a convenience
rather than a safety measure
To do:
* Remote verification: verifying against the remote awaits the "remote-object-info"
cat-file protocol command
* Recency: a "don't cull recently-fetched objects" rule as another
selection criterion alongside size
* Drop log: introduce with the error-path change that reads it.
[1] https://lore.kernel.org/git/20260730174153.9949-1-r.siddharth.shrimali@gmail.com/
Siddharth Shrimali (7):
builtin/repack.c: add --drop-filtered and --dry-run options
list-objects-filter: add list_objects_filter__filter_oidset()
repack-promisor: allow excluding objects from the rebuilt promisor
pack
builtin/repack: enumerate promisor blobs for --drop-filtered
builtin/repack: actually drop filtered promisor blobs
builtin/repack: add guards for --drop-filtered
Documentation/git-repack: document --drop-filtered and --dry-run
Documentation/git-repack.adoc | 37 +++++++
builtin/repack.c | 148 ++++++++++++++++++++++++-
list-objects-filter.c | 45 ++++++++
list-objects-filter.h | 16 +++
repack-filtered.c | 82 ++++++++++++++
repack-promisor.c | 15 ++-
repack.h | 8 +-
t/meson.build | 1 +
t/t7706-repack-drop-filtered.sh | 185 ++++++++++++++++++++++++++++++++
9 files changed, 531 insertions(+), 6 deletions(-)
create mode 100755 t/t7706-repack-drop-filtered.sh
--
2.54.0
^ permalink raw reply [flat|nested] 46+ messages in thread* [GSoC PATCH v3 1/7] builtin/repack.c: add --drop-filtered and --dry-run options
2026-08-06 11:21 ` [GSoC PATCH v3 " Siddharth Shrimali
@ 2026-08-06 11:21 ` Siddharth Shrimali
2026-08-06 11:21 ` [GSoC PATCH v3 2/7] list-objects-filter: add list_objects_filter__filter_oidset() Siddharth Shrimali
` (7 subsequent siblings)
8 siblings, 0 replies; 46+ messages in thread
From: Siddharth Shrimali @ 2026-08-06 11:21 UTC (permalink / raw)
To: git
Cc: gitster, christian.couder, siddharthasthana31, ttaylorr, me, ps,
johannes.schindelin, l.s.r, r.siddharth.shrimali
Add two new command-line options to 'git-repack':
--drop-filtered: intended to eventually delete objects that match
the filter specification. Requires --filter and -a,
and is incompatible with --filter-to.
--dry-run: show which objects would be dropped without making any
changes. Only meaningful with --drop-filtered.
Keep --dry-run as a separate option rather than folding it into
--drop-filtered (e.g --drop-filtered=dry-run), to stay consistent with
the --dry-run option other Git commands already provide and to leave
room for it to describe other repack behavior later. A
--drop-filtered=<mode> form can still be added later if more
drop-specific modes are needed.
--drop-filtered also requires a promisor remote to be configured, since
dropping objects without a remote to fetch them back from would be
permanent data loss.
--drop-filtered is incompatible with bitmap writing: filtering breaks
the "all objects in one pack" closure that bitmaps require. Detect an
explicit -b/--write-bitmap-index on the command line with a dedicated
option callback that sets a "write_bitmaps_given" flag, so it can be
distinguished from a repack.writeBitmaps configuration value even when
config already enables bitmaps. An explicit -b is reported as a conflict,
while a config-provided default is silently disabled for the duration
of the command.
These options currently only perform validation. The actual enumeration
and deletion will be added in follow-up commits.
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>
Signed-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>
---
builtin/repack.c | 71 ++++++++++++++++++++++++++++++++-
t/meson.build | 1 +
t/t7706-repack-drop-filtered.sh | 55 +++++++++++++++++++++++++
3 files changed, 125 insertions(+), 2 deletions(-)
create mode 100755 t/t7706-repack-drop-filtered.sh
diff --git a/builtin/repack.c b/builtin/repack.c
index db504d673f..2e8b7ea45c 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -14,6 +14,7 @@
#include "promisor-remote.h"
#include "repack.h"
#include "shallow.h"
+#include "list-objects-filter-options.h"
#define ALL_INTO_ONE 1
#define LOOSEN_UNREACHABLE 2
@@ -28,6 +29,9 @@ static int use_delta_islands;
static int run_update_server_info = 1;
static char *packdir, *packtmp_name, *packtmp;
static int midx_must_contain_cruft = 1;
+static int drop_filtered;
+static int dry_run;
+static int write_bitmaps_given;
static const char *const git_repack_usage[] = {
N_("git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\n"
@@ -111,6 +115,21 @@ static int repack_config(const char *var, const char *value,
return git_default_config(var, value, ctx, cb);
}
+static int option_parse_write_bitmaps(const struct option *opt, const char *arg,
+ int unset)
+{
+ int *value = opt->value;
+
+ BUG_ON_OPT_ARG(arg);
+ if (unset)
+ *value = 0;
+ else
+ *value = 1;
+
+ write_bitmaps_given = 1;
+ return 0;
+}
+
static int option_parse_write_midx(const struct option *opt, const char *arg,
int unset)
{
@@ -194,8 +213,9 @@ int cmd_repack(int argc,
OPT__QUIET(&po_args.quiet, N_("be quiet")),
OPT_BOOL('l', "local", &po_args.local,
N_("pass --local to git-pack-objects")),
- OPT_BOOL('b', "write-bitmap-index", &write_bitmaps,
- N_("write bitmap index")),
+ OPT_CALLBACK_F('b', "write-bitmap-index", &write_bitmaps, NULL,
+ N_("write bitmap index"),
+ PARSE_OPT_NOARG, option_parse_write_bitmaps),
OPT_BOOL('i', "delta-islands", &use_delta_islands,
N_("pass --delta-islands to git-pack-objects")),
OPT_STRING(0, "unpack-unreachable", &unpack_unreachable, N_("approxidate"),
@@ -231,6 +251,10 @@ int cmd_repack(int argc,
N_("pack prefix to store a pack containing pruned objects")),
OPT_STRING(0, "filter-to", &filter_to, N_("dir"),
N_("pack prefix to store a pack containing filtered out objects")),
+ OPT_BOOL(0, "drop-filtered", &drop_filtered,
+ N_("delete filtered out objects (requires --filter)")),
+ OPT_BOOL(0, "dry-run", &dry_run,
+ N_("only show which objects would be dropped")),
OPT_END()
};
@@ -252,6 +276,49 @@ int cmd_repack(int argc,
po_args.depth = xstrdup_or_null(opt_depth);
po_args.threads = xstrdup_or_null(opt_threads);
+ die_for_incompatible_opt2(drop_filtered, "--drop-filtered",
+ !!filter_to, "--filter-to");
+
+ if (dry_run && !drop_filtered)
+ die(_("--dry-run only takes effect with --drop-filtered"));
+
+ if (drop_filtered) {
+ if (!dry_run)
+ die(_("--drop-filtered doesn't work without --dry-run yet"));
+
+ if (!po_args.filter_options.choice)
+ die(_("--drop-filtered requires --filter"));
+
+ if (!(pack_everything & ALL_INTO_ONE))
+ die(_("--drop-filtered requires -a"));
+
+ /*
+ * Only blob:limit=<n> is supported for now. Reject other
+ * filter choices early, before walking the object database.
+ */
+ if (po_args.filter_options.choice != LOFC_BLOB_LIMIT)
+ die(_("--drop-filtered only supports --filter=blob:limit=<n> for now"));
+
+ /*
+ * an explicit -b on the command line is a conflict we have to
+ * report, a bitmap setting from config is silently overridden
+ * for the duration of the command
+ */
+ if (write_bitmaps_given && write_bitmaps > 0)
+ die(_("options '%s' and '%s' cannot be used together"),
+ "--drop-filtered", "--write-bitmap-index");
+
+ /*
+ * Without a promisor remote there is nowhere to re-fetch the
+ * dropped objects from, so dropping them would be permanent
+ * data loss.
+ */
+ if (!repo_has_promisor_remote(repo))
+ die(_("--drop-filtered requires a promisor remote"));
+
+ write_bitmaps = 0;
+ }
+
if (delete_redundant && repo->repository_format_precious_objects)
die(_("cannot delete packs in a precious-objects repo"));
diff --git a/t/meson.build b/t/meson.build
index a25f37d2f5..92352e43c4 100644
--- a/t/meson.build
+++ b/t/meson.build
@@ -964,6 +964,7 @@ integration_tests = [
't7703-repack-geometric.sh',
't7704-repack-cruft.sh',
't7705-repack-incremental-midx.sh',
+ 't7706-repack-drop-filtered.sh',
't7800-difftool.sh',
't7810-grep.sh',
't7811-grep-open.sh',
diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh
new file mode 100755
index 0000000000..f27b09a30e
--- /dev/null
+++ b/t/t7706-repack-drop-filtered.sh
@@ -0,0 +1,55 @@
+#!/bin/sh
+
+test_description='git repack --drop-filtered option validation'
+
+. ./test-lib.sh
+
+# checks for options validations before any promisor walk
+test_expect_success 'setup plain repo for validation' '
+ git init plain &&
+ test_commit -C plain initial &&
+ git clone --bare plain plain.git &&
+ git -C plain.git repack -a -d
+'
+
+test_expect_success '--drop-filtered requires --filter' '
+ test_must_fail git -C plain.git repack --drop-filtered --dry-run -a 2>err &&
+ test_grep "drop-filtered requires --filter" err
+'
+
+test_expect_success '--drop-filtered cannot be used with --filter-to' '
+ test_must_fail git -C plain.git repack --drop-filtered \
+ --filter=blob:limit=1k --filter-to=./filter-out 2>err &&
+ test_grep "options .--drop-filtered. and .--filter-to. cannot be used together" err
+'
+
+test_expect_success '--dry-run only takes effect with --drop-filtered' '
+ test_must_fail git -C plain.git repack --dry-run 2>err &&
+ test_grep "dry-run only takes effect with --drop-filtered" err
+'
+
+test_expect_success '--drop-filtered requires -a' '
+ test_must_fail git -C plain.git repack --drop-filtered \
+ --filter=blob:limit=1k --dry-run 2>err &&
+ test_grep "drop-filtered requires -a" err
+'
+
+test_expect_success '--drop-filtered fails with --write-bitmap-index' '
+ test_must_fail git -C plain.git repack --drop-filtered \
+ --filter=blob:limit=1k --dry-run -a -b 2>err &&
+ test_grep "options .--drop-filtered. and .--write-bitmap-index. cannot be used together" err
+'
+
+test_expect_success '--drop-filtered rejects explicit -b even when repack.writeBitmaps=true' '
+ test_must_fail git -C plain.git -c repack.writeBitmaps=true \
+ repack --drop-filtered --filter=blob:limit=1k --dry-run -a -b 2>err &&
+ test_grep "options .--drop-filtered. and .--write-bitmap-index. cannot be used together" err
+'
+
+test_expect_success '--drop-filtered fails without a promisor remote' '
+ test_must_fail git -C plain.git repack --drop-filtered \
+ --filter=blob:limit=1k --dry-run -a 2>err &&
+ test_grep "drop-filtered requires a promisor remote" err
+'
+
+test_done
--
2.54.0
^ permalink raw reply related [flat|nested] 46+ messages in thread* [GSoC PATCH v3 2/7] list-objects-filter: add list_objects_filter__filter_oidset()
2026-08-06 11:21 ` [GSoC PATCH v3 " Siddharth Shrimali
2026-08-06 11:21 ` [GSoC PATCH v3 1/7] builtin/repack.c: add --drop-filtered and --dry-run options Siddharth Shrimali
@ 2026-08-06 11:21 ` Siddharth Shrimali
2026-08-06 11:21 ` [GSoC PATCH v3 3/7] repack-promisor: allow excluding objects from the rebuilt promisor pack Siddharth Shrimali
` (6 subsequent siblings)
8 siblings, 0 replies; 46+ messages in thread
From: Siddharth Shrimali @ 2026-08-06 11:21 UTC (permalink / raw)
To: git
Cc: gitster, christian.couder, siddharthasthana31, ttaylorr, me, ps,
johannes.schindelin, l.s.r, r.siddharth.shrimali
The existing filter entry point, list_objects_filter__filter_object(),
is built around the object-walk path: it expects traversal context and
provisional omit sets, and is meant to be called as objects are
visited during a walk. A caller that already has a set of OIDs in hand
and only wants to know which ones a filter would select has no usable
entry point into the filter API.
--drop-filtered is exactly such a caller: it collects promisor blobs
into an oidset and needs to know which of them exceed the filter
threshold, without performing an object walk.
Add a helper, list_objects_filter__filter_oidset(), that takes a set
of OIDs and populates an "omitted" set with those that would be
filtered out by the given filter options. Only blob:limit=N filters
are supported for now.
This helper does not actually reuse the existing filter machinery.
It reimplements the blob:limit size check directly. That machinery
is tied to the object-walk path and cannot easily be driven
from a plain oidset. A NEEDSWORK comment marks this so the helper can
later be refactored to reuse the real filter logic instead of
duplicating it.
OBJECT_INFO_SKIP_FETCH_OBJECT is passed when reading object info so
the helper never triggers a lazy fetch.
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>
Signed-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>
---
list-objects-filter.c | 45 +++++++++++++++++++++++++++++++++++++++++++
list-objects-filter.h | 16 +++++++++++++++
2 files changed, 61 insertions(+)
diff --git a/list-objects-filter.c b/list-objects-filter.c
index c912ff3079..6a2e9d5b24 100644
--- a/list-objects-filter.c
+++ b/list-objects-filter.c
@@ -828,3 +828,48 @@ void list_objects_filter__free(struct filter *filter)
filter->free_fn(filter->filter_data);
free(filter);
}
+
+/*
+ * NEEDSWORK: this reimplements the blob:limit size check rather than
+ * reusing the existing filter machinery in
+ * list_objects_filter__filter_object(). That machinery is currently
+ * tied to the object-walk path and cannot easily be driven from a
+ * plain oidset. It would be nice to refactor the filter code so this
+ * helper can reuse it instead of duplicating the size check.
+ */
+int list_objects_filter__filter_oidset(struct repository *r,
+ struct list_objects_filter_options *opts,
+ const struct oidset *in,
+ struct oidset *omitted)
+{
+ struct oidset_iter iter;
+ const struct object_id *oid;
+
+ if (opts->choice != LOFC_BLOB_LIMIT)
+ return error(_("filter_oidset: only blob:limit filters are supported"));
+
+ oidset_iter_init(in, &iter);
+ while ((oid = oidset_iter_next(&iter))) {
+ struct object_info info = OBJECT_INFO_INIT;
+ enum object_type type;
+ unsigned long size;
+
+ info.typep = &type;
+ info.sizep = &size;
+
+ /*
+ * Use OBJECT_INFO_SKIP_FETCH_OBJECT to avoid triggering
+ * a lazy fetch while inspecting candidates for removal.
+ */
+ if (odb_read_object_info_extended(r->objects, oid, &info,
+ OBJECT_INFO_SKIP_FETCH_OBJECT) < 0)
+ continue;
+
+ if (type != OBJ_BLOB)
+ continue;
+
+ if (size >= opts->blob_limit_value)
+ oidset_insert(omitted, oid);
+ }
+ return 0;
+}
diff --git a/list-objects-filter.h b/list-objects-filter.h
index 9e98814111..56a2d87aa0 100644
--- a/list-objects-filter.h
+++ b/list-objects-filter.h
@@ -94,4 +94,20 @@ enum list_objects_filter_result list_objects_filter__filter_object(
*/
void list_objects_filter__free(struct filter *filter);
+/*
+ * Given a set of OIDs in 'in', populate 'omitted' with those that
+ * would be filtered by 'opts'. Currently only blob:limit=N is
+ * supported. Objects that cannot be read are silently skipped.
+ *
+ * NEEDSWORK: this reimplements the blob:limit size check rather than
+ * reusing the existing filter machinery. See the matching comment in
+ * list-objects-filter.c.
+ *
+ * Return 0 on success, -1 if the filter is not supported.
+ */
+int list_objects_filter__filter_oidset(struct repository *r,
+ struct list_objects_filter_options *opts,
+ const struct oidset *in,
+ struct oidset *omitted);
+
#endif /* LIST_OBJECTS_FILTER_H */
--
2.54.0
^ permalink raw reply related [flat|nested] 46+ messages in thread* [GSoC PATCH v3 3/7] repack-promisor: allow excluding objects from the rebuilt promisor pack
2026-08-06 11:21 ` [GSoC PATCH v3 " Siddharth Shrimali
2026-08-06 11:21 ` [GSoC PATCH v3 1/7] builtin/repack.c: add --drop-filtered and --dry-run options Siddharth Shrimali
2026-08-06 11:21 ` [GSoC PATCH v3 2/7] list-objects-filter: add list_objects_filter__filter_oidset() Siddharth Shrimali
@ 2026-08-06 11:21 ` Siddharth Shrimali
2026-08-06 11:21 ` [GSoC PATCH v3 4/7] builtin/repack: enumerate promisor blobs for --drop-filtered Siddharth Shrimali
` (5 subsequent siblings)
8 siblings, 0 replies; 46+ messages in thread
From: Siddharth Shrimali @ 2026-08-06 11:21 UTC (permalink / raw)
To: git
Cc: gitster, christian.couder, siddharthasthana31, ttaylorr, me, ps,
johannes.schindelin, l.s.r, r.siddharth.shrimali
Add a to_drop oidset parameter to repack_promisor_objects(). When it is
non-NULL, write_oid() omits those objects from the rebuilt promisor
pack. This is the mechanism --drop-filtered will use to remove promisor
blobs, i.e. rebuild the promisor pack without them.
All existing callers pass NULL, so behavior is unchanged.
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>
Signed-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>
---
builtin/repack.c | 2 +-
repack-promisor.c | 15 ++++++++++++++-
repack.h | 4 +++-
3 files changed, 18 insertions(+), 3 deletions(-)
diff --git a/builtin/repack.c b/builtin/repack.c
index 2e8b7ea45c..0a4dadb896 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -429,7 +429,7 @@ int cmd_repack(int argc,
strvec_push(&cmd.args, "--delta-islands");
if (pack_everything & ALL_INTO_ONE) {
- repack_promisor_objects(repo, &po_args, &names, packtmp);
+ repack_promisor_objects(repo, &po_args, &names, packtmp, NULL);
if (existing_packs_has_non_kept(&existing) &&
delete_redundant &&
diff --git a/repack-promisor.c b/repack-promisor.c
index 90318ce150..fabfdc168a 100644
--- a/repack-promisor.c
+++ b/repack-promisor.c
@@ -6,10 +6,12 @@
#include "path.h"
#include "repository.h"
#include "run-command.h"
+#include "oidset.h"
struct write_oid_context {
struct child_process *cmd;
const struct git_hash_algo *algop;
+ const struct oidset *to_drop;
};
/*
@@ -23,6 +25,15 @@ static int write_oid(const struct object_id *oid,
struct write_oid_context *ctx = data;
struct child_process *cmd = ctx->cmd;
+ /*
+ * Objects in to_drop are being removed from the repository, so
+ * omit them from the rebuilt promisor pack. Each such object is a
+ * promisor object and therefore remains recoverable from the
+ * promisor remote.
+ */
+ if (ctx->to_drop && oidset_contains(ctx->to_drop, oid))
+ return 0;
+
if (cmd->in == -1) {
if (start_command(cmd))
die(_("could not start pack-objects to repack promisor objects"));
@@ -81,7 +92,8 @@ static void finish_repacking_promisor_objects(struct repository *repo,
void repack_promisor_objects(struct repository *repo,
const struct pack_objects_args *args,
- struct string_list *names, const char *packtmp)
+ struct string_list *names, const char *packtmp,
+ const struct oidset *to_drop)
{
struct write_oid_context ctx;
struct child_process cmd = CHILD_PROCESS_INIT;
@@ -98,6 +110,7 @@ void repack_promisor_objects(struct repository *repo,
*/
ctx.cmd = &cmd;
ctx.algop = repo->hash_algo;
+ ctx.to_drop = to_drop;
odb_for_each_object(repo->objects, NULL, write_oid, &ctx,
ODB_FOR_EACH_OBJECT_PROMISOR_ONLY);
diff --git a/repack.h b/repack.h
index f9fbc895f0..a5a3f7c6ba 100644
--- a/repack.h
+++ b/repack.h
@@ -3,6 +3,7 @@
#include "list-objects-filter-options.h"
#include "string-list.h"
+#include "oidset.h"
struct pack_objects_args {
char *window;
@@ -100,7 +101,8 @@ void generated_pack_install(struct generated_pack *pack, const char *name,
void repack_promisor_objects(struct repository *repo,
const struct pack_objects_args *args,
- struct string_list *names, const char *packtmp);
+ struct string_list *names, const char *packtmp,
+ const struct oidset *to_drop);
struct pack_geometry {
struct packed_git **pack;
--
2.54.0
^ permalink raw reply related [flat|nested] 46+ messages in thread* [GSoC PATCH v3 4/7] builtin/repack: enumerate promisor blobs for --drop-filtered
2026-08-06 11:21 ` [GSoC PATCH v3 " Siddharth Shrimali
` (2 preceding siblings ...)
2026-08-06 11:21 ` [GSoC PATCH v3 3/7] repack-promisor: allow excluding objects from the rebuilt promisor pack Siddharth Shrimali
@ 2026-08-06 11:21 ` Siddharth Shrimali
2026-08-06 11:22 ` [GSoC PATCH v3 5/7] builtin/repack: actually drop filtered promisor blobs Siddharth Shrimali
` (4 subsequent siblings)
8 siblings, 0 replies; 46+ messages in thread
From: Siddharth Shrimali @ 2026-08-06 11:21 UTC (permalink / raw)
To: git
Cc: gitster, christian.couder, siddharthasthana31, ttaylorr, me, ps,
johannes.schindelin, l.s.r, r.siddharth.shrimali
Add enumeration logic for --drop-filtered. In --dry-run mode, print
the OIDs of locally-held promisor blobs that exceed the filter
threshold, as candidates for removal.
Reading from write_filtered_pack() cannot work for partial clones.
git repack routes promisor objects through a separate path:
repack_promisor_objects() repacks them first, and the main
pack-objects run uses --exclude-promisor-objects. By the time
write_filtered_pack() runs, the promisor blobs are already consumed by
the main pack. The filtered pack is always empty on a partial clone.
Instead, walk promisor objects directly via odb_for_each_object() with
ODB_FOR_EACH_OBJECT_PROMISOR_ONLY, collecting all promisor blobs into
an oidset. The blobs exceeding the filter threshold are then selected
using list_objects_filter__filter_oidset().
Every object enumerated this way is a promisor object, so it is
recoverable from the promisor remote in the same sense as the rest of a
partial clone, as long as the remote still has it. This holds without a
separate is_promisor_object() check. A future implementation can verify
availability against the remote directly once a client-side
remote-object-info query exists.
OBJECT_INFO_SKIP_FETCH_OBJECT is passed to every object info query so
enumeration never triggers a lazy fetch.
The enumeration collects candidates into a caller-provided oidset and
--dry-run prints them. Actually removing the objects, together with the
required promisor-remote verification, is written in a later commit.
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>
Signed-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>
---
builtin/repack.c | 20 +++++++-
repack-filtered.c | 81 +++++++++++++++++++++++++++++++
repack.h | 4 ++
t/t7706-repack-drop-filtered.sh | 84 ++++++++++++++++++++++++++++++++-
4 files changed, 187 insertions(+), 2 deletions(-)
diff --git a/builtin/repack.c b/builtin/repack.c
index 0a4dadb896..c5f39cef00 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -15,6 +15,8 @@
#include "repack.h"
#include "shallow.h"
#include "list-objects-filter-options.h"
+#include "oidset.h"
+#include "hex.h"
#define ALL_INTO_ONE 1
#define LOOSEN_UNREACHABLE 2
@@ -159,6 +161,7 @@ int cmd_repack(int argc,
struct string_list_item *item;
struct string_list names = STRING_LIST_INIT_DUP;
struct existing_packs existing = EXISTING_PACKS_INIT;
+ struct oidset drop_oids = OIDSET_INIT;
struct pack_geometry geometry = { 0 };
struct tempfile *refs_snapshot = NULL;
int i, ret;
@@ -317,6 +320,20 @@ int cmd_repack(int argc,
die(_("--drop-filtered requires a promisor remote"));
write_bitmaps = 0;
+
+ ret = enumerate_promisor_blobs(repo, &po_args.filter_options, &drop_oids);
+
+ if (ret)
+ goto cleanup;
+
+ if (dry_run) {
+ struct oidset_iter iter;
+ const struct object_id *oid;
+
+ oidset_iter_init(&drop_oids, &iter);
+ while ((oid = oidset_iter_next(&iter)))
+ printf("%s\n", oid_to_hex(oid));
+ }
}
if (delete_redundant && repo->repository_format_precious_objects)
@@ -612,7 +629,7 @@ int cmd_repack(int argc,
}
}
- if (po_args.filter_options.choice) {
+ if (po_args.filter_options.choice && !drop_filtered) {
struct write_pack_opts opts = {
.po_args = &po_args,
.destination = filter_to,
@@ -705,6 +722,7 @@ int cmd_repack(int argc,
cleanup:
string_list_clear(&keep_pack_list, 0);
string_list_clear(&names, 1);
+ oidset_clear(&drop_oids);
existing_packs_release(&existing);
pack_geometry_release(&geometry);
pack_objects_args_release(&po_args);
diff --git a/repack-filtered.c b/repack-filtered.c
index edcf7667c5..79ba6d90aa 100644
--- a/repack-filtered.c
+++ b/repack-filtered.c
@@ -3,6 +3,12 @@
#include "repository.h"
#include "run-command.h"
#include "string-list.h"
+#include "hex.h"
+#include "packfile.h"
+#include "list-objects-filter-options.h"
+#include "list-objects-filter.h"
+#include "odb.h"
+#include "promisor-remote.h"
int write_filtered_pack(const struct write_pack_opts *opts,
struct existing_packs *existing,
@@ -49,3 +55,78 @@ int write_filtered_pack(const struct write_pack_opts *opts,
return finish_pack_objects_cmd(existing->repo->hash_algo, opts, &cmd,
names);
}
+
+struct collect_cb_data {
+ struct repository *repo;
+ struct oidset *set;
+};
+
+static int collect_promisor_blob(const struct object_id *oid,
+ struct object_info *oi UNUSED,
+ void *cb_data)
+{
+ struct collect_cb_data *data = cb_data;
+ struct object_info info = OBJECT_INFO_INIT;
+ enum object_type type;
+
+ info.typep = &type;
+
+ /*
+ * Use OBJECT_INFO_SKIP_FETCH_OBJECT to avoid triggering a
+ * lazy fetch while collecting promisor blobs.
+ */
+ if (odb_read_object_info_extended(data->repo->objects, oid, &info,
+ OBJECT_INFO_SKIP_FETCH_OBJECT) < 0)
+ return 0;
+
+ if (type == OBJ_BLOB)
+ oidset_insert(data->set, oid);
+
+ return 0;
+}
+
+int enumerate_promisor_blobs(struct repository *repo,
+ const struct list_objects_filter_options *filter,
+ struct oidset *to_drop)
+{
+ struct oidset all_promisor_blobs = OIDSET_INIT;
+ struct collect_cb_data cb = {
+ .repo = repo,
+ .set = &all_promisor_blobs
+ };
+ int ret = 0;
+
+ /*
+ * The caller (cmd_repack) is responsible for validating that a
+ * blob:limit filter and a promisor remote are present before
+ * calling this function.
+ *
+ * Walk only promisor objects. every object visited here is a
+ * promisor object, so it is recoverable from the promisor remote
+ * as long as the remote still has it, the same assumption the rest
+ * of partial clone relies on
+
+ * We do not use write_filtered_pack() here because git repack
+ * routes promisor objects through repack_promisor_objects()
+ * before the filter machinery runs, so the filtered pack never
+ * contains promisor blobs. Direct enumeration via
+ * ODB_FOR_EACH_OBJECT_PROMISOR_ONLY is the correct approach.
+ */
+ ret = odb_for_each_object(repo->objects, NULL,
+ collect_promisor_blob, &cb,
+ ODB_FOR_EACH_OBJECT_PROMISOR_ONLY);
+ if (ret)
+ goto cleanup;
+
+ /*
+ * Apply the filter to find which blobs exceed the threshold.
+ */
+ ret = list_objects_filter__filter_oidset(repo,
+ (struct list_objects_filter_options *)filter,
+ &all_promisor_blobs,
+ to_drop);
+
+cleanup:
+ oidset_clear(&all_promisor_blobs);
+ return ret;
+}
diff --git a/repack.h b/repack.h
index a5a3f7c6ba..61e554e4ed 100644
--- a/repack.h
+++ b/repack.h
@@ -167,6 +167,10 @@ int write_filtered_pack(const struct write_pack_opts *opts,
struct existing_packs *existing,
struct string_list *names);
+int enumerate_promisor_blobs(struct repository *repo,
+ const struct list_objects_filter_options *filter,
+ struct oidset *to_drop);
+
int write_cruft_pack(const struct write_pack_opts *opts,
const char *cruft_expiration,
unsigned long combine_cruft_below_size,
diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh
index f27b09a30e..453053cc18 100755
--- a/t/t7706-repack-drop-filtered.sh
+++ b/t/t7706-repack-drop-filtered.sh
@@ -1,9 +1,36 @@
#!/bin/sh
-test_description='git repack --drop-filtered option validation'
+test_description='git repack --drop-filtered enumerates filtered promisor blobs'
. ./test-lib.sh
+delete_object () {
+ local repo="$1" &&
+ local obj="$2" &&
+ local path="$repo/.git/objects/$(test_oid_to_path "$obj")" &&
+ rm "$path"
+}
+
+# pack the objects into a promisor pack inside "repo". it is a pack
+# accompanied by an empty ".promisor" marker file. objects
+# in such a pack are treated as recoverable from the promisor remote.
+pack_as_from_promisor () {
+ HASH=$(git -C repo pack-objects .git/objects/pack/pack) &&
+ >repo/.git/objects/pack/pack-$HASH.promisor &&
+ echo $HASH
+}
+
+# write a blob of $1 bytes into "repo", record it as coming from the
+# promisor remote, and remove the loose copy so the object is only
+# present in the promisor pack
+promisor_blob () {
+ test-tool genrandom "$1" "$2" >blob_content &&
+ OID=$(git -C repo hash-object -w --stdin <blob_content) &&
+ printf "%s\n" "$OID" | pack_as_from_promisor >/dev/null &&
+ delete_object repo "$OID" &&
+ echo "$OID"
+}
+
# checks for options validations before any promisor walk
test_expect_success 'setup plain repo for validation' '
git init plain &&
@@ -52,4 +79,59 @@ test_expect_success '--drop-filtered fails without a promisor remote' '
test_grep "drop-filtered requires a promisor remote" err
'
+# enumeration tests using promisor pack
+test_expect_success 'setup repo with a promisor remote' '
+ rm -rf repo &&
+ test_create_repo repo &&
+ test_commit -C repo base &&
+
+ # mark the repo as a partial clone with a promisor remote so the
+ # promisor walk and the safety guard are satisfied
+ git -C repo config core.repositoryformatversion 1 &&
+ git -C repo config extensions.partialclone origin &&
+ git -C repo config remote.origin.promisor true &&
+ git -C repo config remote.origin.url "." &&
+
+ BIG=$(promisor_blob big 3072) &&
+ SMALL=$(promisor_blob small 512) &&
+ echo "$BIG" >big_oid &&
+ echo "$SMALL" >small_oid
+'
+
+test_expect_success 'promisor blob over the threshold is listed' '
+ BIG=$(cat big_oid) &&
+ SMALL=$(cat small_oid) &&
+
+ git -C repo -c repack.writeBitmaps=false \
+ repack --drop-filtered --filter=blob:limit=1k --dry-run -a >out &&
+
+ test_grep "$BIG" out &&
+ test_grep ! "$SMALL" out
+'
+
+test_expect_success 'locally created blob is never listed' '
+ BIG=$(cat big_oid) &&
+
+ # large blob that exists only locally must never be a drop candidate.
+ # dropping it would be unrecoverable
+ test-tool genrandom local 4096 >local_content &&
+ LOCAL=$(git -C repo hash-object -w --stdin <local_content) &&
+
+ git -C repo -c repack.writeBitmaps=false \
+ repack --drop-filtered --filter=blob:limit=1k --dry-run -a >out &&
+
+ test_grep "$BIG" out &&
+ test_grep ! "$LOCAL" out
+'
+
+test_expect_success '--dry-run does not remove the filtered objects' '
+ BIG=$(cat big_oid) &&
+
+ git -C repo -c repack.writeBitmaps=false \
+ repack --drop-filtered --filter=blob:limit=1k --dry-run -a >out &&
+
+ # candidate blob must still be present after a dry run
+ git -C repo cat-file -e "$BIG"
+'
+
test_done
--
2.54.0
^ permalink raw reply related [flat|nested] 46+ messages in thread* [GSoC PATCH v3 5/7] builtin/repack: actually drop filtered promisor blobs
2026-08-06 11:21 ` [GSoC PATCH v3 " Siddharth Shrimali
` (3 preceding siblings ...)
2026-08-06 11:21 ` [GSoC PATCH v3 4/7] builtin/repack: enumerate promisor blobs for --drop-filtered Siddharth Shrimali
@ 2026-08-06 11:22 ` Siddharth Shrimali
2026-08-06 11:22 ` [GSoC PATCH v3 6/7] builtin/repack: add guards for --drop-filtered Siddharth Shrimali
` (3 subsequent siblings)
8 siblings, 0 replies; 46+ messages in thread
From: Siddharth Shrimali @ 2026-08-06 11:22 UTC (permalink / raw)
To: git
Cc: gitster, christian.couder, siddharthasthana31, ttaylorr, me, ps,
johannes.schindelin, l.s.r, r.siddharth.shrimali
Make --drop-filtered remove the enumerated promisor blobs instead of
only listing them.
The drop set is computed before repack_promisor_objects() runs, and on
a real run it is passed in so the rebuilt promisor pack omits those
blobs. --drop-filtered implies -d so the old promisor packs, which
still contain the dropped blobs, are removed. Without this the blobs
would survive in the redundant packs. The existing repack machinery
performs the write-before-delete and fsync, so the drop is crash-safe.
The dropped blobs become absent locally but remain recoverable from the
promisor remote, so a later access lazy-fetches them back
transparently. --dry-run keeps its previous behavior, i.e. it lists the
candidates and changes nothing.
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>
Signed-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>
---
builtin/repack.c | 14 ++++++++++----
repack-filtered.c | 1 +
t/t7706-repack-drop-filtered.sh | 12 ++++++++++++
3 files changed, 23 insertions(+), 4 deletions(-)
diff --git a/builtin/repack.c b/builtin/repack.c
index c5f39cef00..a20589a7ae 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -286,9 +286,6 @@ int cmd_repack(int argc,
die(_("--dry-run only takes effect with --drop-filtered"));
if (drop_filtered) {
- if (!dry_run)
- die(_("--drop-filtered doesn't work without --dry-run yet"));
-
if (!po_args.filter_options.choice)
die(_("--drop-filtered requires --filter"));
@@ -321,6 +318,14 @@ int cmd_repack(int argc,
write_bitmaps = 0;
+ /*
+ * Dropping objects means rebuilding the promisor packs
+ * without them and then removing the old packs, so the
+ * redundant packs must be deleted. Imply -d on a real run.
+ */
+ if (!dry_run)
+ delete_redundant = 1;
+
ret = enumerate_promisor_blobs(repo, &po_args.filter_options, &drop_oids);
if (ret)
@@ -446,7 +451,8 @@ int cmd_repack(int argc,
strvec_push(&cmd.args, "--delta-islands");
if (pack_everything & ALL_INTO_ONE) {
- repack_promisor_objects(repo, &po_args, &names, packtmp, NULL);
+ repack_promisor_objects(repo, &po_args, &names, packtmp,
+ (drop_filtered && !dry_run) ? &drop_oids : NULL);
if (existing_packs_has_non_kept(&existing) &&
delete_redundant &&
diff --git a/repack-filtered.c b/repack-filtered.c
index 79ba6d90aa..e6c35c23de 100644
--- a/repack-filtered.c
+++ b/repack-filtered.c
@@ -120,6 +120,7 @@ int enumerate_promisor_blobs(struct repository *repo,
/*
* Apply the filter to find which blobs exceed the threshold.
+ * The caller has to_drop and is responsible for clearing it.
*/
ret = list_objects_filter__filter_oidset(repo,
(struct list_objects_filter_options *)filter,
diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh
index 453053cc18..88c2bb0857 100755
--- a/t/t7706-repack-drop-filtered.sh
+++ b/t/t7706-repack-drop-filtered.sh
@@ -134,4 +134,16 @@ test_expect_success '--dry-run does not remove the filtered objects' '
git -C repo cat-file -e "$BIG"
'
+test_expect_success '--drop-filtered removes the promisor blob locally' '
+ BIG=$(cat big_oid) &&
+ SMALL=$(cat small_oid) &&
+
+ git -C repo -c repack.writeBitmaps=false \
+ repack --drop-filtered --filter=blob:limit=1k -a &&
+
+ git -C repo cat-file --batch-all-objects --batch-check="%(objectname)" >present &&
+ ! grep -q "$BIG" present &&
+ grep -q "$SMALL" present
+'
+
test_done
--
2.54.0
^ permalink raw reply related [flat|nested] 46+ messages in thread* [GSoC PATCH v3 6/7] builtin/repack: add guards for --drop-filtered
2026-08-06 11:21 ` [GSoC PATCH v3 " Siddharth Shrimali
` (4 preceding siblings ...)
2026-08-06 11:22 ` [GSoC PATCH v3 5/7] builtin/repack: actually drop filtered promisor blobs Siddharth Shrimali
@ 2026-08-06 11:22 ` Siddharth Shrimali
2026-08-06 11:22 ` [GSoC PATCH v3 7/7] Documentation/git-repack: document --drop-filtered and --dry-run Siddharth Shrimali
` (2 subsequent siblings)
8 siblings, 0 replies; 46+ messages in thread
From: Siddharth Shrimali @ 2026-08-06 11:22 UTC (permalink / raw)
To: git
Cc: gitster, christian.couder, siddharthasthana31, ttaylorr, me, ps,
johannes.schindelin, l.s.r, r.siddharth.shrimali
--drop-filtered removes local promisor blobs. That is only safe when the
repository is not mid-operation and when the blobs are not actively in
use, so add two guards, both skipped for bare repositories which have
neither a worktree nor an index.
First, refuse to run while a merge, rebase, am, cherry-pick, revert, or
bisect is in progress. During these operations the working tree and
index are in an intermediate state, and rewriting packs and deleting
objects underneath a half-finished operation is unsafe.
Second, refuse to drop a blob that the current index references. Such a
blob is needed by the working tree, so dropping it would only cause the
next command that touches the worktree to lazy-fetch it straight back,
reclaiming nothing. The offending path is reported so the user can see
why the drop was refused.
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>
Signed-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>
---
builtin/repack.c | 49 +++++++++++++++++++++++++++++++++
t/t7706-repack-drop-filtered.sh | 36 ++++++++++++++++++++++++
2 files changed, 85 insertions(+)
diff --git a/builtin/repack.c b/builtin/repack.c
index a20589a7ae..170e94f8bd 100644
--- a/builtin/repack.c
+++ b/builtin/repack.c
@@ -17,6 +17,8 @@
#include "list-objects-filter-options.h"
#include "oidset.h"
#include "hex.h"
+#include "wt-status.h"
+#include "read-cache-ll.h"
#define ALL_INTO_ONE 1
#define LOOSEN_UNREACHABLE 2
@@ -316,6 +318,30 @@ int cmd_repack(int argc,
if (!repo_has_promisor_remote(repo))
die(_("--drop-filtered requires a promisor remote"));
+ /*
+ * refuse to run while another operation is in progress. A
+ * dropped object would just be lazily re-fetched when the
+ * operation resumes, but triggering a network fetch in the
+ * middle of a half-finished
+ * merge/rebase/cherry-pick/revert/bisect is a poor
+ * experience, so this is a UX convenience rather than a
+ * safety measure. Bare repositories have no such state, so
+ * the check is skipped there.
+ */
+ if (!is_bare_repository(repo)) {
+ struct wt_status_state state = { 0 };
+
+ wt_status_get_state(repo, &state, 0);
+ if (state.merge_in_progress || state.revert_in_progress ||
+ state.rebase_in_progress ||state.bisect_in_progress ||
+ state.cherry_pick_in_progress ||state.am_in_progress||
+ state.rebase_interactive_in_progress) {
+ wt_status_state_free_buffers(&state);
+ die(_("--drop-filtered cannot be used while another operation is in progress"));
+ }
+ wt_status_state_free_buffers(&state);
+ }
+
write_bitmaps = 0;
/*
@@ -331,6 +357,29 @@ int cmd_repack(int argc,
if (ret)
goto cleanup;
+ /*
+ * refuse to drop blobs that the current index references.
+ * such a blob would only be lazily re-fetched by the next
+ * command that touches the worktree, so dropping it reclaims
+ * nothing. This guard just avoids that churn. bare
+ * repositories have no index, so the check is skipped there.
+ */
+ if (!is_bare_repository(repo) && oidset_size(&drop_oids)) {
+ struct index_state *istate = repo->index;
+ unsigned int i;
+
+ if (repo_read_index(repo) < 0)
+ die(_("could not read the index"));
+
+ for (i = 0; i < istate->cache_nr; i++) {
+ const struct cache_entry *ce = istate->cache[i];
+
+ if (oidset_contains(&drop_oids, &ce->oid))
+ die(_("cannot drop '%s' (%s): it is referenced by the current index"),
+ ce->name, oid_to_hex(&ce->oid));
+ }
+ }
+
if (dry_run) {
struct oidset_iter iter;
const struct object_id *oid;
diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh
index 88c2bb0857..6774886f1e 100755
--- a/t/t7706-repack-drop-filtered.sh
+++ b/t/t7706-repack-drop-filtered.sh
@@ -146,4 +146,40 @@ test_expect_success '--drop-filtered removes the promisor blob locally' '
grep -q "$SMALL" present
'
+test_expect_success '--drop-filtered refuses when a merge is in progress' '
+ test_when_finished "git -C repo merge --abort || :" &&
+
+ # creat a conflicting merge so wt_status reports it
+ git -C repo checkout -B mergebase base &&
+ echo one >repo/conflict.txt &&
+ git -C repo add conflict.txt &&
+ git -C repo commit -m one &&
+
+ git -C repo checkout -B mergeother base &&
+ echo two >repo/conflict.txt &&
+ git -C repo add conflict.txt &&
+ git -C repo commit -m two &&
+
+ test_must_fail git -C repo merge mergebase &&
+
+ test_must_fail git -C repo -c repack.writeBitmaps=false \
+ repack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err &&
+ test_grep "in progress" err
+'
+
+
+test_expect_success '--drop-filtered refuses to drop an index-referenced blob' '
+ # create a large blob, add it to the index and make it a promisor object
+ # so the index references it and enumeration picks it up
+ test-tool genrandom idx 4096 >repo/tracked-big.bin &&
+ git -C repo add tracked-big.bin &&
+ OID=$(git -C repo rev-parse :tracked-big.bin) &&
+ printf "%s\n" "$OID" | pack_as_from_promisor >/dev/null &&
+ delete_object repo "$OID" &&
+
+ test_must_fail git -C repo -c repack.writeBitmaps=false \
+ repack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err &&
+ test_grep "referenced by the current index" err
+'
+
test_done
--
2.54.0
^ permalink raw reply related [flat|nested] 46+ messages in thread* [GSoC PATCH v3 7/7] Documentation/git-repack: document --drop-filtered and --dry-run
2026-08-06 11:21 ` [GSoC PATCH v3 " Siddharth Shrimali
` (5 preceding siblings ...)
2026-08-06 11:22 ` [GSoC PATCH v3 6/7] builtin/repack: add guards for --drop-filtered Siddharth Shrimali
@ 2026-08-06 11:22 ` Siddharth Shrimali
2026-08-06 21:34 ` [GSoC PATCH v3 0/7] repack: add --drop-filtered to reclaim space in partial clones Junio C Hamano
2026-08-06 22:19 ` Junio C Hamano
8 siblings, 0 replies; 46+ messages in thread
From: Siddharth Shrimali @ 2026-08-06 11:22 UTC (permalink / raw)
To: git
Cc: gitster, christian.couder, siddharthasthana31, ttaylorr, me, ps,
johannes.schindelin, l.s.r, r.siddharth.shrimali
Describe the new --drop-filtered and --dry-run options: what they do,
only blob:limit filters are supported for now, a promisor remote is
required, --drop-filtered requires -a and implies -d so the redundant
packs are actually removed, its incompatibilities with --filter-to and
bitmap writing, and the safety guards that refuse to run mid-operation
or to drop index-referenced blobs.
Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>
Signed-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>
---
Documentation/git-repack.adoc | 37 +++++++++++++++++++++++++++++++++++
1 file changed, 37 insertions(+)
diff --git a/Documentation/git-repack.adoc b/Documentation/git-repack.adoc
index 72c42015e2..4c6aa3bc18 100644
--- a/Documentation/git-repack.adoc
+++ b/Documentation/git-repack.adoc
@@ -12,6 +12,7 @@ SYNOPSIS
'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]
[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]
[--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]
+ [--filter=<filter-spec>] [--drop-filtered [--dry-run]]]
DESCRIPTION
-----------
@@ -182,6 +183,42 @@ depth is 4095.
`objects` and `objects/info/alternates` sections of
linkgit:gitrepository-layout[5].
+--drop-filtered::
+ Delete the local objects that match the `--filter` specification
+ instead of keeping them in a separate packfile, reclaiming the
+ disk space they occupy. This is intended for partial clones,
+ where the filtered objects are promisor objects that remain
+ recoverable from the promisor remote and are lazily re-fetched
+ on demand when they are next needed.
++
+Only large blobs are supported for now, so `--filter=blob:limit=<n>`
+is currently the only accepted filter. Because dropped objects must be
+recoverable, this option requires a promisor remote to be configured
+and refuses to run otherwise.
++
+This option requires `-a`, and implies `-d`: the objects are dropped by
+rebuilding the promisor pack without them and then removing the now
+redundant old packs, so the redundant packs must be deleted for the
+space to actually be reclaimed. It is incompatible with `--filter-to`
+and with bitmap writing (`-b`/`--write-bitmap-index`), since filtering
+breaks the single-pack closure that bitmaps require. A bitmap setting
+coming from configuration is silently disabled for the duration of the
+command.
++
+As a convenience since dropped objects remain recoverable by lazy fetch,
+`--drop-filtered` refuses to run while another operation
+(merge, rebase, am, cherry-pick, revert, or bisect) is in progress, to
+avoid a surprising network fetch mid-operation, and refuses to drop any
+blob that the current index references, since such a blob would only be
+lazily re-fetched by the next command that inspects the working tree.
+These checks are skipped in bare repositories, which have neither a
+working tree nor an index.
+
+--dry-run::
+ Only meaningful with `--drop-filtered`. List the objects that
+ would be dropped, one object ID per line, without rebuilding any
+ pack or deleting anything.
+
-b::
--write-bitmap-index::
Write a reachability bitmap index as part of the repack. This
--
2.54.0
^ permalink raw reply related [flat|nested] 46+ messages in thread* Re: [GSoC PATCH v3 0/7] repack: add --drop-filtered to reclaim space in partial clones
2026-08-06 11:21 ` [GSoC PATCH v3 " Siddharth Shrimali
` (6 preceding siblings ...)
2026-08-06 11:22 ` [GSoC PATCH v3 7/7] Documentation/git-repack: document --drop-filtered and --dry-run Siddharth Shrimali
@ 2026-08-06 21:34 ` Junio C Hamano
2026-08-06 22:19 ` Junio C Hamano
8 siblings, 0 replies; 46+ messages in thread
From: Junio C Hamano @ 2026-08-06 21:34 UTC (permalink / raw)
To: Siddharth Shrimali
Cc: git, christian.couder, siddharthasthana31, ttaylorr, me, ps,
johannes.schindelin, l.s.r
You would need something like the attached patch.
Didn't you get these when you ran "make test"?
t7706-repack-drop-filtered.sh:145: error: bare grep outside pipeline (use test_grep)
t7706-repack-drop-filtered.sh:146: error: bare grep outside pipeline (use test_grep)
t/t7706-repack-drop-filtered.sh | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git i/t/t7706-repack-drop-filtered.sh w/t/t7706-repack-drop-filtered.sh
index 6774886f1e..05d58fa456 100755
--- i/t/t7706-repack-drop-filtered.sh
+++ w/t/t7706-repack-drop-filtered.sh
@@ -142,8 +142,8 @@ test_expect_success '--drop-filtered removes the promisor blob locally' '
repack --drop-filtered --filter=blob:limit=1k -a &&
git -C repo cat-file --batch-all-objects --batch-check="%(objectname)" >present &&
- ! grep -q "$BIG" present &&
- grep -q "$SMALL" present
+ test_grep ! "$BIG" present &&
+ test_grep "$SMALL" present
'
test_expect_success '--drop-filtered refuses when a merge is in progress' '
^ permalink raw reply related [flat|nested] 46+ messages in thread* Re: [GSoC PATCH v3 0/7] repack: add --drop-filtered to reclaim space in partial clones
2026-08-06 11:21 ` [GSoC PATCH v3 " Siddharth Shrimali
` (7 preceding siblings ...)
2026-08-06 21:34 ` [GSoC PATCH v3 0/7] repack: add --drop-filtered to reclaim space in partial clones Junio C Hamano
@ 2026-08-06 22:19 ` Junio C Hamano
2026-08-07 9:06 ` Siddharth Shrimali
8 siblings, 1 reply; 46+ messages in thread
From: Junio C Hamano @ 2026-08-06 22:19 UTC (permalink / raw)
To: Siddharth Shrimali
Cc: git, christian.couder, siddharthasthana31, ttaylorr, me, ps,
johannes.schindelin, l.s.r
Siddharth Shrimali <r.siddharth.shrimali@gmail.com> writes:
> This is v3 of the series adding "git repack --drop-filtered" to reclaim
> disk space in partial clones by dropping large, locally-held promisor
> blobs that remain recoverable from the promisor remote. v2 was at [1].
Also I am getting a failure from t0450.
--- adoc 2026-08-06 22:05:39.038464944 +0000
+++ help 2026-08-06 22:05:39.046464970 +0000
@@ -1,4 +1,3 @@
git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]
[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]
[--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]
- [--filter=<filter-spec>] [--drop-filtered [--dry-run]]]
not ok 650 - repack -h output and SYNOPSIS agree
#
# t2s="$(adoc_to_synopsis "$builtin")" &&
# if test "$builtin" = "merge-tree"
# then
# test_when_finished "rm -f t2s.new" &&
# sed -e 's/ (deprecated)$//g' <"$t2s" >t2s.new
# t2s=t2s.new
# fi &&
# h2s="$(help_to_synopsis "$builtin")" &&
#
# # The *.adoc and -h use different spacing for the
# # alignment of continued usage output, normalize it.
# align_after_nl "$builtin" <"$t2s" >adoc &&
# align_after_nl "$builtin" <"$h2s" >help &&
# test_cmp adoc help
#
1..650
Have these patches been reviewed and tested? Is this a new breakage
in v3?
I think the accumulated fixes so far I have are as follows, but I
suspect they need to be split and squashed into multiple patches (I
didn't check).
Documentation/git-repack.adoc | 2 +-
builtin/repack.c | 3 ++-
t/t7706-repack-drop-filtered.sh | 4 ++--
3 files changed, 5 insertions(+), 4 deletions(-)
diff --git i/Documentation/git-repack.adoc w/Documentation/git-repack.adoc
index 1364d6cd49..1775fb7645 100644
--- i/Documentation/git-repack.adoc
+++ w/Documentation/git-repack.adoc
@@ -12,7 +12,7 @@ SYNOPSIS
'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]
[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]
[--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]
- [--filter=<filter-spec>] [--drop-filtered [--dry-run]]]
+ [--filter=<filter-spec>] [--drop-filtered [--dry-run]]
DESCRIPTION
-----------
diff --git i/builtin/repack.c w/builtin/repack.c
index 9473342843..81ec093808 100644
--- i/builtin/repack.c
+++ w/builtin/repack.c
@@ -40,7 +40,8 @@ static int write_bitmaps_given;
static const char *const git_repack_usage[] = {
N_("git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\n"
"[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]\n"
- "[--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]"),
+ "[--write-midx[=<mode>]] [--name-hash-version=<n>] [--path-walk]\n"
+ "[--filter=<filter-spec>] [--drop-filtered [--dry-run]]"),
NULL
};
diff --git i/t/t7706-repack-drop-filtered.sh w/t/t7706-repack-drop-filtered.sh
index 6774886f1e..05d58fa456 100755
--- i/t/t7706-repack-drop-filtered.sh
+++ w/t/t7706-repack-drop-filtered.sh
@@ -142,8 +142,8 @@ test_expect_success '--drop-filtered removes the promisor blob locally' '
repack --drop-filtered --filter=blob:limit=1k -a &&
git -C repo cat-file --batch-all-objects --batch-check="%(objectname)" >present &&
- ! grep -q "$BIG" present &&
- grep -q "$SMALL" present
+ test_grep ! "$BIG" present &&
+ test_grep "$SMALL" present
'
test_expect_success '--drop-filtered refuses when a merge is in progress' '
^ permalink raw reply related [flat|nested] 46+ messages in thread* Re: [GSoC PATCH v3 0/7] repack: add --drop-filtered to reclaim space in partial clones
2026-08-06 22:19 ` Junio C Hamano
@ 2026-08-07 9:06 ` Siddharth Shrimali
2026-08-07 20:52 ` Junio C Hamano
0 siblings, 1 reply; 46+ messages in thread
From: Siddharth Shrimali @ 2026-08-07 9:06 UTC (permalink / raw)
To: Junio C Hamano
Cc: git, christian.couder, siddharthasthana31, ttaylorr, me, ps,
johannes.schindelin, l.s.r
On Fri, 7 Aug 2026 at 03:49, Junio C Hamano <gitster@pobox.com> wrote:
> Have these patches been reviewed and tested? Is this a new breakage
> in v3?
>
> I think the accumulated fixes so far I have are as follows, but I
> suspect they need to be split and squashed into multiple patches (I
> didn't check).
sorry for the trouble, these are my mistakes: i ran t7706 but not the full
test suite, so i missed the t0450 SYNOPSIS check and the lint errors.
The -h usage string wasnt updated to match the new .adoc synopsis,
and i left a bare grep in the test. I'll fix all three, before sending a v4
thanks for catching these, and for the fixups.
Thanks,
Siddharth Shrimali
^ permalink raw reply [flat|nested] 46+ messages in thread
* Re: [GSoC PATCH v3 0/7] repack: add --drop-filtered to reclaim space in partial clones
2026-08-07 9:06 ` Siddharth Shrimali
@ 2026-08-07 20:52 ` Junio C Hamano
0 siblings, 0 replies; 46+ messages in thread
From: Junio C Hamano @ 2026-08-07 20:52 UTC (permalink / raw)
To: Siddharth Shrimali
Cc: git, christian.couder, siddharthasthana31, ttaylorr, me, ps,
johannes.schindelin, l.s.r
Siddharth Shrimali <r.siddharth.shrimali@gmail.com> writes:
> On Fri, 7 Aug 2026 at 03:49, Junio C Hamano <gitster@pobox.com> wrote:
>> Have these patches been reviewed and tested? Is this a new breakage
>> in v3?
>>
>> I think the accumulated fixes so far I have are as follows, but I
>> suspect they need to be split and squashed into multiple patches (I
>> didn't check).
> sorry for the trouble, these are my mistakes: i ran t7706 but not the full
> test suite, so i missed the t0450 SYNOPSIS check and the lint errors.
> The -h usage string wasnt updated to match the new .adoc synopsis,
> and i left a bare grep in the test. I'll fix all three, before sending a v4
Please do not limit yourself to "all three". Do not expect
reviewers to be exhaustive. You are expected to be.
IOW, do not just run a selected few tests. Run the full testsuite,
and then some more, like making a trial merge to 'next' and to
'seen' and run full testsuite on the results.
Thanks.
^ permalink raw reply [flat|nested] 46+ messages in thread