]> git.ipfire.org Git - thirdparty/git.git/commitdiff
ref-filter: restore prefix-scoped iteration
authorTamir Duberstein <tamird@gmail.com>
Fri, 12 Jun 2026 21:27:44 +0000 (17:27 -0400)
committerJunio C Hamano <gitster@pobox.com>
Fri, 12 Jun 2026 22:54:51 +0000 (15:54 -0700)
dabecb9db2 (for-each-ref: introduce a '--start-after' option,
2025-07-15) changed branch, remote-tracking branch, and tag enumeration
from constructing an iterator with the namespace prefix to constructing
an unscoped iterator and seeking to the prefix.

Review of --start-after noted that the construction prefix and seek
position represent different state and are easy to conflate [1]. It also
noted that future branch or tag support would need to retain the
namespace prefix while moving the cursor [2].

The files backend constructs its loose-ref iterator with cache priming
enabled. cache_ref_iterator_begin() immediately applies the construction
prefix through cache_ref_iterator_set_prefix(), reading loose refs
beneath it before packed refs are opened. An empty prefix therefore
reads every loose ref, and a later seek cannot undo that I/O.

For the current single-kind filters, construct the iterator with the
namespace prefix when start_after is not set. Leave the existing
start_after path unchanged; no current command combines it with these
filters, and future support must carry the prefix separately from the
cursor.

With 10,000 unrelated loose refs in the files backend, the p6300 tests
improve as follows:

                         before   after
  branch                  2.74 s   0.11 s
  branch --remotes        2.81 s   0.12 s
  tag                     3.01 s   0.11 s

[1] https://lore.kernel.org/r/aGZidwwlToWThkn8@pks.im/
[2] https://lore.kernel.org/r/xmqqikjq7s16.fsf@gitster.g/

Fixes: dabecb9db2b2 ("for-each-ref: introduce a '--start-after' option")
Suggested-by: Karthik Nayak <karthik.188@gmail.com>
Signed-off-by: Tamir Duberstein <tamird@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
ref-filter.c
t/perf/p6300-for-each-ref.sh

index 1da4c0e60df3fa8eed3eb806e958964c90b1df3b..9b04e3af85ef1cf81a3aa01a2a5171a8001c942b 100644 (file)
@@ -3316,15 +3316,14 @@ static int do_filter_refs(struct ref_filter *filter, unsigned int type, refs_for
 
        if (prefix) {
                struct ref_iterator *iter;
+               struct ref_store *store = get_main_ref_store(the_repository);
 
-               iter = refs_ref_iterator_begin(get_main_ref_store(the_repository),
-                                              "", NULL, 0, 0);
-
-               if (filter->start_after)
+               if (filter->start_after) {
+                       iter = refs_ref_iterator_begin(store, "", NULL, 0, 0);
                        ret = start_ref_iterator_after(iter, filter->start_after);
-               else
-                       ret = ref_iterator_seek(iter, prefix,
-                                               REF_ITERATOR_SEEK_SET_PREFIX);
+               } else {
+                       iter = refs_ref_iterator_begin(store, prefix, NULL, 0, 0);
+               }
 
                if (!ret)
                        ret = do_for_each_ref_iterator(iter, fn, cb_data);
index fa7289c7522b10332daa020e2a5470ef3382327b..25ffa5e84cf8b75e05c1fb53c0d8c7336523bef3 100755 (executable)
@@ -1,6 +1,6 @@
 #!/bin/sh
 
-test_description='performance of for-each-ref'
+test_description='performance of ref-filter users'
 . ./perf-lib.sh
 
 test_perf_fresh_repo
@@ -84,4 +84,41 @@ test_expect_success 'pack refs' '
 '
 run_tests "packed"
 
+test_expect_success 'setup many unrelated refs' '
+       git init scoped &&
+       test_commit -C scoped --no-tag base &&
+       test_seq $ref_count_per_type |
+               sed "s,.*,update refs/custom/unrelated_& HEAD," |
+               git -C scoped update-ref --stdin &&
+       git -C scoped update-ref refs/remotes/origin/main HEAD &&
+       git -C scoped update-ref refs/tags/only HEAD
+'
+
+test_perf "branch (many unrelated refs)" "
+       (
+               cd scoped &&
+               for i in \$(test_seq $test_iteration_count); do
+                       git branch --format='%(refname)' >/dev/null
+               done
+       )
+"
+
+test_perf "branch --remotes (many unrelated refs)" "
+       (
+               cd scoped &&
+               for i in \$(test_seq $test_iteration_count); do
+                       git branch --remotes --format='%(refname)' >/dev/null
+               done
+       )
+"
+
+test_perf "tag (many unrelated refs)" "
+       (
+               cd scoped &&
+               for i in \$(test_seq $test_iteration_count); do
+                       git tag --format='%(refname)' >/dev/null
+               done
+       )
+"
+
 test_done