builtin/repack: enumerate promisor blobs for --drop-filtered

Add enumeration logic for --drop-filtered. In --dry-run mode, print
the OIDs of locally-held promisor blobs that exceed the filter
threshold, as candidates for removal.

Reading from write_filtered_pack() cannot work for partial clones.
git repack routes promisor objects through a separate path:
repack_promisor_objects() repacks them first, and the main
pack-objects run uses --exclude-promisor-objects. By the time
write_filtered_pack() runs, the promisor blobs are already consumed by
the main pack. The filtered pack is always empty on a partial clone.

Instead, walk promisor objects directly via odb_for_each_object() with
ODB_FOR_EACH_OBJECT_PROMISOR_ONLY, collecting all promisor blobs into
an oidset. The blobs exceeding the filter threshold are then selected
using list_objects_filter__filter_oidset().

Every object enumerated this way is a promisor object, so it is
recoverable from the promisor remote in the same sense as the rest of a
partial clone, as long as the remote still has it. This holds without a
separate is_promisor_object() check. A future implementation can verify
availability against the remote directly once a client-side
remote-object-info query exists.

OBJECT_INFO_SKIP_FETCH_OBJECT is passed to every object info query so
enumeration never triggers a lazy fetch.

The enumeration collects candidates into a caller-provided oidset and
--dry-run prints them. Actually removing the objects, together with the
required promisor-remote verification, is written in a later commit.

Mentored-by: Christian Couder <christian.couder@gmail.com>
Mentored-by: Siddharth Asthana <siddharthasthana31@gmail.com>
Signed-off-by: Siddharth Shrimali <r.siddharth.shrimali@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Siddharth Shrimali
2026-08-06 16:51:59 +05:30
committed by Junio C Hamano
parent 948c800418
commit 0bed9fda6c
4 changed files with 187 additions and 2 deletions

View File

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

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

View File

@@ -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,

View File

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