Merge branch 'ss/repack-drop-filtered' into seen

'git repack' has been taught '--drop-filtered' to delete local
promisor blobs exceeding a limit (currently 'blob:limit=') in partial
clones, reclaiming space.  Guards prevent running during other
operations or if referenced by the index.

* ss/repack-drop-filtered:
  SQUASH???
  Documentation/git-repack: document --drop-filtered and --dry-run
  builtin/repack: add guards for --drop-filtered
  builtin/repack: actually drop filtered promisor blobs
  builtin/repack: enumerate promisor blobs for --drop-filtered
  repack-promisor: allow excluding objects from the rebuilt promisor pack
  list-objects-filter: add list_objects_filter__filter_oidset()
  builtin/repack.c: add --drop-filtered and --dry-run options
This commit is contained in:
Junio C Hamano
2026-08-07 14:48:30 -07:00
9 changed files with 533 additions and 7 deletions

View File

@@ -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
-----------
@@ -187,6 +188,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

View File

@@ -14,6 +14,11 @@
#include "promisor-remote.h"
#include "repack.h"
#include "shallow.h"
#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
@@ -28,11 +33,15 @@ 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"
"[--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
};
@@ -111,6 +120,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)
{
@@ -140,6 +164,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;
@@ -194,8 +219,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 +257,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 +282,115 @@ 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 (!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"));
/*
* 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;
/*
* 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)
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;
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)
die(_("cannot delete packs in a precious-objects repo"));
@@ -365,7 +504,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);
repack_promisor_objects(repo, &po_args, &names, packtmp,
(drop_filtered && !dry_run) ? &drop_oids : NULL);
if (existing_packs_has_non_kept(&existing) &&
delete_redundant &&
@@ -556,7 +696,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,
@@ -648,6 +788,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);

View File

@@ -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;
}

View File

@@ -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 */

View File

@@ -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,79 @@ 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.
* The caller has to_drop and is responsible for clearing it.
*/
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;
}

View File

@@ -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);

View File

@@ -3,6 +3,7 @@
#include "list-objects-filter-options.h"
#include "string-list.h"
#include "oidset.h"
struct pack_objects_args {
char *window;
@@ -105,7 +106,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;
@@ -165,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,

View File

@@ -970,6 +970,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',

185
t/t7706-repack-drop-filtered.sh Executable file
View File

@@ -0,0 +1,185 @@
#!/bin/sh
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 &&
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
'
# 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_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 &&
test_grep ! "$BIG" present &&
test_grep "$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