ref-filter: restore prefix-scoped iteration

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>

Tamir Duberstein committed Jun 12, 2026 at 17:27 UTC ad2e20d8aab0d4d43fbac407736f05a94e65c49b
2 files changed +44 -8
ref-filter.c
+6 -7
@@ -3316,15 +3316,14 @@ static int do_filter_refs(struct ref_filter *filter, unsigned int type, refs_for
3316
3317 if (prefix) {
3318 struct ref_iterator *iter;
3319 + struct ref_store *store = get_main_ref_store(the_repository);
3320
3320 - iter = refs_ref_iterator_begin(get_main_ref_store(the_repository),
3321 - "", NULL, 0, 0);
3322 -
3323 - if (filter->start_after)
3321 + if (filter->start_after) {
3322 + iter = refs_ref_iterator_begin(store, "", NULL, 0, 0);
3323 ret = start_ref_iterator_after(iter, filter->start_after);
3325 - else
3326 - ret = ref_iterator_seek(iter, prefix,
3327 - REF_ITERATOR_SEEK_SET_PREFIX);
3324 + } else {
3325 + iter = refs_ref_iterator_begin(store, prefix, NULL, 0, 0);
3326 + }
3327
3328 if (!ret)
3329 ret = do_for_each_ref_iterator(iter, fn, cb_data);
t/perf/p6300-for-each-ref.sh
+38 -1
@@ -1,6 +1,6 @@
1 #!/bin/sh
2
3 -test_description='performance of for-each-ref'
3 +test_description='performance of ref-filter users'
4 . ./perf-lib.sh
5
6 test_perf_fresh_repo
@@ -84,4 +84,41 @@ test_expect_success 'pack refs' '
84 '
85 run_tests "packed"
86
87 +test_expect_success 'setup many unrelated refs' '
88 + git init scoped &&
89 + test_commit -C scoped --no-tag base &&
90 + test_seq $ref_count_per_type |
91 + sed "s,.*,update refs/custom/unrelated_& HEAD," |
92 + git -C scoped update-ref --stdin &&
93 + git -C scoped update-ref refs/remotes/origin/main HEAD &&
94 + git -C scoped update-ref refs/tags/only HEAD
95 +'
96 +
97 +test_perf "branch (many unrelated refs)" "
98 + (
99 + cd scoped &&
100 + for i in \$(test_seq $test_iteration_count); do
101 + git branch --format='%(refname)' >/dev/null
102 + done
103 + )
104 +"
105 +
106 +test_perf "branch --remotes (many unrelated refs)" "
107 + (
108 + cd scoped &&
109 + for i in \$(test_seq $test_iteration_count); do
110 + git branch --remotes --format='%(refname)' >/dev/null
111 + done
112 + )
113 +"
114 +
115 +test_perf "tag (many unrelated refs)" "
116 + (
117 + cd scoped &&
118 + for i in \$(test_seq $test_iteration_count); do
119 + git tag --format='%(refname)' >/dev/null
120 + done
121 + )
122 +"
123 +
124 test_done