last-modified: keep per-path Bloom filters for wildcard pathspecs

The last-modified builtin expands the pathspec to a set of literal paths
and builds a Bloom key for each. During the walk it looks those keys up
in the commit's filter to decide whether the commit is worth diffing.
These lookups need `bloom_filter_settings` for the key hashing.

prepare_revision_walk() runs prepare_to_use_bloom_filter() to build the
pathspec key vectors. For a pathspec that cannot be turned into a Bloom
key, such as a top-level wildcard like "*.c", that function gives up and
clears `bloom_filter_settings`.

Restore `bloom_filter_settings` after prepare_revision_walk() so the
per-path check keeps working for wildcard pathspecs.

This change isn't having any effect on the output, but only has an
impact on performance. Add a "bloom_queries" trace2 counter that records
how often the per-path Bloom check runs, and a test that asserts the
count increments as appropriate for a top-level wildcard pathspec.

Signed-off-by: Toon Claes <toon@iotcl.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Toon Claes 2026-08-31 17:18:46 +02:00 committed by Junio C Hamano
parent 287e852391
commit 8948156775
2 changed files with 42 additions and 0 deletions

View File

@ -18,6 +18,7 @@
#include "quote.h"
#include "repository.h"
#include "revision.h"
#include "trace2.h"

/* Remember to update object flag allocation in object.h */
#define PARENT1 (1u<<16) /* used instead of SEEN */
@ -63,6 +64,8 @@ struct last_modified {

/* 'scratch' to avoid allocating a bitmap every process_parent() */
struct bitmap *scratch;

unsigned int count_bloom_filter_queries;
};

static struct bitmap *active_paths_for(struct last_modified *lm, struct commit *c)
@ -272,6 +275,8 @@ static bool maybe_changed_path(struct last_modified *lm,
if (!filter)
return true;

lm->count_bloom_filter_queries++;

/*
* With --show-trees we also track the tree entries containing the
* paths, so a change to any of those parent directories matters too.
@ -369,6 +374,14 @@ static int last_modified_run(struct last_modified *lm)

prepare_revision_walk(&lm->rev);

/*
* prepare_revision_walk() clears bloom_filter_settings for pathspecs
* without a Bloom key. Restore it so the per-path check keeps working.
*/
if (!lm->rev.bloom_filter_settings)
lm->rev.bloom_filter_settings =
get_bloom_filter_settings(lm->rev.repo);

max_count = lm->rev.max_count;

init_active_paths_for_commit(&lm->active_paths);
@ -476,6 +489,9 @@ cleanup:
if (hashmap_get_size(&lm->paths))
BUG("paths remaining beyond boundary in last-modified");

trace2_data_intmax("last-modified", lm->rev.repo, "bloom_queries",
lm->count_bloom_filter_queries);

clear_prio_queue(&not_queue);
clear_prio_queue(&queue);
clear_active_paths_for_commit(&lm->active_paths);

View File

@ -290,6 +290,32 @@ test_expect_success 'last-modified with Bloom filters and --show-trees' '
)
'

test_expect_success 'last-modified with Bloom filters and top-level wildcard' '
test_when_finished rm -rf wildcard &&
git init wildcard &&
(
cd wildcard &&
test_commit base-c a.c &&
test_commit base-h a.h &&
test_commit touch-c a.c &&
mkdir d &&
test_commit sub-c d/b.c &&

git commit-graph write --reachable --changed-paths &&
GIT_TRACE2_PERF="$(pwd)/off.perf" \
git -c core.commitGraph=false last-modified -r HEAD \
-- "*.c" >expect &&
test_grep "data .* bloom_queries:0$" off.perf &&

GIT_TRACE2_PERF="$(pwd)/on.perf" \
git -c core.commitGraph=true last-modified -r HEAD \
-- "*.c" >actual &&
test_grep "data .* bloom_queries:2$" on.perf &&

test_cmp expect actual
)
'

test_expect_success 'cannot run last-modified on two commits' '
test_must_fail git last-modified HEAD HEAD~1 2>err &&
test_grep "last-modified can only operate on one commit at a time" err