]> git.ipfire.org Git - thirdparty/git.git/commitdiff
ref-filter: memoize --contains with generations
authorTamir Duberstein <tamird@gmail.com>
Fri, 12 Jun 2026 21:49:13 +0000 (17:49 -0400)
committerJunio C Hamano <gitster@pobox.com>
Fri, 12 Jun 2026 22:56:44 +0000 (15:56 -0700)
git branch and git for-each-ref run a separate reachability walk for
each ref considered by --contains and --no-contains. Refs with shared
history therefore traverse the same commits repeatedly.

git tag instead uses a depth-first walk that caches results across
refs. That walk can perform poorly without generation numbers: a
negative check may walk to the root instead of stopping at a nearby
divergence. Generation numbers let it stop below the oldest target.

Use the memoized walk for all ref-filter callers when generation
numbers are available. Keep git tag on its existing path without
generations. Caching still helps when many tags share deep history:
ffc4b8012d (tag: speed up --contains calculation, 2011-06-11) reduced
git tag --contains HEAD~200 in linux-2.6 from 15.417 to 5.329 seconds.

The new shared-history perf test improves from 0.72 to 0.03 seconds. In
a repository with 62,174 remote-tracking refs, running:

    git branch -r --contains c78ae85f3ce7e

improves from 104.365 seconds to 468 milliseconds.

Suggested-by: Jeff King <peff@peff.net>
Signed-off-by: Tamir Duberstein <tamird@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
commit-reach.c
t/perf/p1500-graph-walks.sh

index 58553dff05692eaa09e219186e5d4995426aed0d..349ca68a3761b0e16b28a05d5eefd88b060ea6d5 100644 (file)
@@ -854,7 +854,8 @@ static enum contains_result contains_tag_algo(struct commit *candidate,
 int commit_contains(struct ref_filter *filter, struct commit *commit,
                    struct commit_list *list, struct contains_cache *cache)
 {
-       if (filter->with_commit_tag_algo)
+       if (filter->with_commit_tag_algo ||
+           generation_numbers_enabled(the_repository))
                return contains_tag_algo(commit, list, cache) == CONTAINS_YES;
        return repo_is_descendant_of(the_repository, commit, list);
 }
index 5b23ce5db93ad32b8024011984725acf27e13afe..d167b4f7e1f2ade3c8ab46911811f3f447395151 100755 (executable)
@@ -32,7 +32,16 @@ test_expect_success 'setup' '
                echo "X:$line" >>test-tool-tags || return 1
        done &&
 
-       commit=$(git commit-tree $(git rev-parse HEAD^{tree})) &&
+       git rev-list --first-parent --max-count=8192 HEAD >contains-commits &&
+       test_file_not_empty contains-commits &&
+       git update-ref refs/contains-perf-base "$(tail -n 1 contains-commits)" &&
+       awk "{
+               printf \"update refs/contains-perf/%04d %s\\n\", NR, \$1
+       }" contains-commits |
+               git update-ref --stdin &&
+       git pack-refs --include "refs/contains-perf/*" &&
+
+       commit=$(git commit-tree HEAD^{tree}) &&
        git update-ref refs/heads/disjoint-base $commit &&
 
        git commit-graph write --reachable
@@ -62,6 +71,23 @@ test_perf 'contains: git tag --merged' '
        xargs git tag --merged=HEAD <tags
 '
 
+test_perf 'contains: git for-each-ref' '
+       git for-each-ref --contains=refs/contains-perf-base --stdin <refs
+'
+
+test_perf 'contains: git branch' '
+       xargs git branch --contains=refs/contains-perf-base <branches
+'
+
+test_perf 'contains: git tag' '
+       xargs git tag --contains=refs/contains-perf-base <tags
+'
+
+test_perf 'contains: synthetic shared history' '
+       git for-each-ref --contains=refs/contains-perf-base \
+               refs/contains-perf/ >/dev/null
+'
+
 test_perf 'is-base check: test-tool reach (refs)' '
        test-tool reach get_branch_base_for_tip <test-tool-refs
 '