{"thread":{"id":"65775","subject":"[PATCH v2] ref-filter: restore prefix-scoped iteration","startedAt":"2026-06-09T02:35:02Z","lastAt":"2026-06-18T15:54:28Z","messageCount":9,"participants":["Tamir Duberstein","Karthik Nayak","Patrick Steinhardt","Junio C Hamano"],"isPatch":true,"patchVersion":2,"patchTotal":null},"messages":[{"id":"544997","messageId":"20260608-fix-git-branch-regression-v2-1-fd82075a8520@gmail.com","threadId":"65775","inReplyTo":null,"subject":"[PATCH v2] ref-filter: restore prefix-scoped iteration","fromName":"Tamir Duberstein","fromEmail":"tamird@gmail.com","sentAt":"2026-06-09T02:34:57Z","receivedAt":"2026-06-09T02:35:02Z","isPatch":true,"body":"Commit dabecb9db2 (for-each-ref: introduce a '--start-after' option,\n2025-07-15) changed single-kind branch, remote-tracking branch, and tag\nenumeration in do_filter_refs() from constructing an iterator with the\nnamespace prefix to constructing an unscoped iterator and applying the\nprefix with ref_iterator_seek().\n\nBefore that change, refs_for_each_fullref_in() passed the namespace\nprefix during iterator construction. That helper has since been\nreplaced by refs_for_each_ref_ext().\n\nThe files backend primes its loose-ref cache for the construction\nprefix before it opens packed refs. An empty construction prefix\ntherefore reads every loose ref, and a later seek cannot undo that I/O.\nConsequently, git branch, git branch --remotes, and git tag scale with\nunrelated loose refs.\n\nPatrick Steinhardt observed during review that iterator construction\nand seeking accepted similar strings but assigned them different state\nsemantics. Junio C Hamano then pointed out that no current command can\ncombine start_after with this single-kind path, but future branch or\ntag support would need to keep the namespace while moving the cursor.\n\nKeep the existing start_after path unchanged. The iterator API cannot\ncurrently seek to one string while retaining another as its prefix:\nan unflagged seek clears the prefix, while REF_ITERATOR_SEEK_SET_PREFIX\nreplaces it with the seek string.\n\nFor the commands affected by this regression, which do not set\nstart_after, pass the namespace prefix during iterator construction so\nthat loose refs are scoped before the packed-refs snapshot is opened.\nThis fixes the current regression without deleting the ref-filter state\ndiscussed during review or changing its dormant behavior.\n\nAdd REFFILES-gated performance cases with one branch, one\nremote-tracking branch, one tag, and 10,000 unrelated loose refs. The\nbenchmarks were run with:\n\n    GIT_PERF_REPEAT_COUNT=5 GIT_PERF_MAKE_OPTS=-j8 \\\n        t/perf/run a89346e34a . -- p6300-for-each-ref.sh\n\nThe following are the best of five runs, with each run invoking the\ncommand ten times. Times are elapsed seconds with user and system CPU\nseconds in parentheses:\n\n                                  a89346e34a       this commit\n  branch                       2.74(0.13+2.56)   0.11(0.04+0.04)\n  branch --remotes             2.81(0.13+2.62)   0.12(0.04+0.04)\n  tag                          3.01(0.14+2.82)   0.11(0.04+0.04)\n\nBoth revisions used the default -O2 build flags and a config.mak\ncontaining only \"NO_REGEX = NeedsStartEnd\". They were built with Apple\nclang 21.0.0 on macOS 26.5. The machine was a MacBook Pro (Mac16,6)\nwith a 16-core Apple M4 Max (12 performance and four efficiency cores)\nand 128 GB RAM.\n\nLink: https://lore.kernel.org/git/aGZidwwlToWThkn8@pks.im/\nLink: https://lore.kernel.org/git/xmqqikjq7s16.fsf@gitster.g/\nFixes: dabecb9db2b2 (\"for-each-ref: introduce a '--start-after' option\")\nAssisted-by: Codex gpt-5.5\nSigned-off-by: Tamir Duberstein <tamird@gmail.com>\n---\nThe series is based on a89346e34a (maint) because the regression has\nbeen present in released versions since Git 2.51.0.\n---\nChanges in v2:\n- Extract local variable `store`.\n- Link to v1: https://patch.msgid.link/20260605-fix-git-branch-regression-v1-1-02f40ad40929@gmail.com\n---\n ref-filter.c                 | 28 +++++++++++++++++++---------\n t/perf/p6300-for-each-ref.sh | 39 ++++++++++++++++++++++++++++++++++++++-\n 2 files changed, 57 insertions(+), 10 deletions(-)\n\ndiff --git a/ref-filter.c b/ref-filter.c\nindex 1da4c0e60d..5cbc007d64 100644\n--- a/ref-filter.c\n+++ b/ref-filter.c\n@@ -3315,19 +3315,29 @@ static int do_filter_refs(struct ref_filter *filter, unsigned int type, refs_for\n \t\tprefix = \"refs/tags/\";\n \n \tif (prefix) {\n-\t\tstruct ref_iterator *iter;\n+\t\tstruct ref_store *store = get_main_ref_store(the_repository);\n \n-\t\titer = refs_ref_iterator_begin(get_main_ref_store(the_repository),\n-\t\t\t\t\t       \"\", NULL, 0, 0);\n+\t\tif (filter->start_after) {\n+\t\t\tstruct ref_iterator *iter;\n+\n+\t\t\titer = refs_ref_iterator_begin(store, \"\", NULL, 0, 0);\n \n-\t\tif (filter->start_after)\n \t\t\tret = start_ref_iterator_after(iter, filter->start_after);\n-\t\telse\n-\t\t\tret = ref_iterator_seek(iter, prefix,\n-\t\t\t\t\t\tREF_ITERATOR_SEEK_SET_PREFIX);\n+\t\t\tif (!ret)\n+\t\t\t\tret = do_for_each_ref_iterator(iter, fn,\n+\t\t\t\t\t\t\t       cb_data);\n+\t\t} else {\n+\t\t\t/*\n+\t\t\t * Pass the prefix during construction because the files\n+\t\t\t * backend primes loose refs before a later seek can\n+\t\t\t * narrow the iterator.\n+\t\t\t */\n+\t\t\tstruct refs_for_each_ref_options opts = {\n+\t\t\t\t.prefix = prefix,\n+\t\t\t};\n \n-\t\tif (!ret)\n-\t\t\tret = do_for_each_ref_iterator(iter, fn, cb_data);\n+\t\t\tret = refs_for_each_ref_ext(store, fn, cb_data, &opts);\n+\t\t}\n \t} else if (filter->kind & FILTER_REFS_REGULAR) {\n \t\tret = for_each_fullref_in_pattern(filter, fn, cb_data);\n \t}\ndiff --git a/t/perf/p6300-for-each-ref.sh b/t/perf/p6300-for-each-ref.sh\nindex fa7289c752..ed9c1c6a19 100755\n--- a/t/perf/p6300-for-each-ref.sh\n+++ b/t/perf/p6300-for-each-ref.sh\n@@ -1,6 +1,6 @@\n #!/bin/sh\n \n-test_description='performance of for-each-ref'\n+test_description='performance of ref-filter users'\n . ./perf-lib.sh\n \n test_perf_fresh_repo\n@@ -84,4 +84,41 @@ test_expect_success 'pack refs' '\n '\n run_tests \"packed\"\n \n+test_expect_success REFFILES 'setup many unrelated loose refs' '\n+\tgit init scoped &&\n+\ttest_commit -C scoped --no-tag base &&\n+\ttest_seq $ref_count_per_type |\n+\t\tsed \"s,.*,update refs/custom/unrelated_& HEAD,\" |\n+\t\tgit -C scoped update-ref --stdin &&\n+\tgit -C scoped update-ref refs/remotes/origin/main HEAD &&\n+\tgit -C scoped update-ref refs/tags/only HEAD\n+'\n+\n+test_perf \"branch (many unrelated loose refs)\" --prereq REFFILES \"\n+\t(\n+\t\tcd scoped &&\n+\t\tfor i in \\$(test_seq $test_iteration_count); do\n+\t\t\tgit branch --format='%(refname)' >/dev/null\n+\t\tdone\n+\t)\n+\"\n+\n+test_perf \"branch --remotes (many unrelated loose refs)\" --prereq REFFILES \"\n+\t(\n+\t\tcd scoped &&\n+\t\tfor i in \\$(test_seq $test_iteration_count); do\n+\t\t\tgit branch --remotes --format='%(refname)' >/dev/null\n+\t\tdone\n+\t)\n+\"\n+\n+test_perf \"tag (many unrelated loose refs)\" --prereq REFFILES \"\n+\t(\n+\t\tcd scoped &&\n+\t\tfor i in \\$(test_seq $test_iteration_count); do\n+\t\t\tgit tag --format='%(refname)' >/dev/null\n+\t\tdone\n+\t)\n+\"\n+\n test_done\n\n---\nbase-commit: a89346e34a937f001e5d397ee62224e3e9852040\nchange-id: 20260605-fix-git-branch-regression-9e4236f18091\n\nBest regards,\n--  \nTamir Duberstein <tamird@gmail.com>\n\n"},{"id":"545127","messageId":"CAOLa=ZRHKNNymXGk31YgECjUmF9nZ8GsPUdQb7aKBH5DKMz7=w@mail.gmail.com","threadId":"65775","inReplyTo":"20260608-fix-git-branch-regression-v2-1-fd82075a8520@gmail.com","subject":"Re: [PATCH v2] ref-filter: restore prefix-scoped iteration","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-06-10T10:50:16Z","receivedAt":"2026-06-10T10:50:18Z","isPatch":true,"body":"Tamir Duberstein <tamird@gmail.com> writes:\n\n> Commit dabecb9db2 (for-each-ref: introduce a '--start-after' option,\n> 2025-07-15) changed single-kind branch, remote-tracking branch, and tag\n> enumeration in do_filter_refs() from constructing an iterator with the\n> namespace prefix to constructing an unscoped iterator and applying the\n> prefix with ref_iterator_seek().\n>\n> Before that change, refs_for_each_fullref_in() passed the namespace\n> prefix during iterator construction. That helper has since been\n> replaced by refs_for_each_ref_ext().\n>\n> The files backend primes its loose-ref cache for the construction\n> prefix before it opens packed refs. An empty construction prefix\n> therefore reads every loose ref, and a later seek cannot undo that I/O.\n> Consequently, git branch, git branch --remotes, and git tag scale with\n> unrelated loose refs.\n>\n\nAnd this is the crux of the issue. Currently we do\n\n- refs_ref_iterator_begin()\n  - ref_iterator_seek()\n\nAnd between the two `cache_ref_iterator_set_prefix()` is already called\nwhich caches all the loose refs. This is the IO intensive operation this\npatch tries to avoid.\n\nI think it would be worthwhile to add this information in the commit\nmessage.\n\n>\n> Patrick Steinhardt observed during review that iterator construction\n> and seeking accepted similar strings but assigned them different state\n> semantics. Junio C Hamano then pointed out that no current command can\n> combine start_after with this single-kind path, but future branch or\n> tag support would need to keep the namespace while moving the cursor.\n>\n> Keep the existing start_after path unchanged. The iterator API cannot\n> currently seek to one string while retaining another as its prefix:\n> an unflagged seek clears the prefix, while REF_ITERATOR_SEEK_SET_PREFIX\n> replaces it with the seek string.\n>\n> For the commands affected by this regression, which do not set\n> start_after, pass the namespace prefix during iterator construction so\n> that loose refs are scoped before the packed-refs snapshot is opened.\n> This fixes the current regression without deleting the ref-filter state\n> discussed during review or changing its dormant behavior.\n>\n> Add REFFILES-gated performance cases with one branch, one\n> remote-tracking branch, one tag, and 10,000 unrelated loose refs. The\n> benchmarks were run with:\n>\n>     GIT_PERF_REPEAT_COUNT=5 GIT_PERF_MAKE_OPTS=-j8 \\\n>         t/perf/run a89346e34a . -- p6300-for-each-ref.sh\n>\n> The following are the best of five runs, with each run invoking the\n> command ten times. Times are elapsed seconds with user and system CPU\n> seconds in parentheses:\n>\n>                                   a89346e34a       this commit\n>   branch                       2.74(0.13+2.56)   0.11(0.04+0.04)\n>   branch --remotes             2.81(0.13+2.62)   0.12(0.04+0.04)\n>   tag                          3.01(0.14+2.82)   0.11(0.04+0.04)\n>\n> Both revisions used the default -O2 build flags and a config.mak\n> containing only \"NO_REGEX = NeedsStartEnd\". They were built with Apple\n> clang 21.0.0 on macOS 26.5. The machine was a MacBook Pro (Mac16,6)\n> with a 16-core Apple M4 Max (12 performance and four efficiency cores)\n> and 128 GB RAM.\n>\n> Link: https://lore.kernel.org/git/aGZidwwlToWThkn8@pks.im/\n> Link: https://lore.kernel.org/git/xmqqikjq7s16.fsf@gitster.g/\n> Fixes: dabecb9db2b2 (\"for-each-ref: introduce a '--start-after' option\")\n> Assisted-by: Codex gpt-5.5\n> Signed-off-by: Tamir Duberstein <tamird@gmail.com>\n> ---\n> The series is based on a89346e34a (maint) because the regression has\n> been present in released versions since Git 2.51.0.\n> ---\n> Changes in v2:\n> - Extract local variable `store`.\n> - Link to v1: https://patch.msgid.link/20260605-fix-git-branch-regression-v1-1-02f40ad40929@gmail.com\n> ---\n>  ref-filter.c                 | 28 +++++++++++++++++++---------\n>  t/perf/p6300-for-each-ref.sh | 39 ++++++++++++++++++++++++++++++++++++++-\n>  2 files changed, 57 insertions(+), 10 deletions(-)\n>\n> diff --git a/ref-filter.c b/ref-filter.c\n> index 1da4c0e60d..5cbc007d64 100644\n> --- a/ref-filter.c\n> +++ b/ref-filter.c\n> @@ -3315,19 +3315,29 @@ static int do_filter_refs(struct ref_filter *filter, unsigned int type, refs_for\n>  \t\tprefix = \"refs/tags/\";\n>\n>  \tif (prefix) {\n> -\t\tstruct ref_iterator *iter;\n> +\t\tstruct ref_store *store = get_main_ref_store(the_repository);\n>\n> -\t\titer = refs_ref_iterator_begin(get_main_ref_store(the_repository),\n> -\t\t\t\t\t       \"\", NULL, 0, 0);\n> +\t\tif (filter->start_after) {\n> +\t\t\tstruct ref_iterator *iter;\n> +\n> +\t\t\titer = refs_ref_iterator_begin(store, \"\", NULL, 0, 0);\n>\n> -\t\tif (filter->start_after)\n>  \t\t\tret = start_ref_iterator_after(iter, filter->start_after);\n> -\t\telse\n> -\t\t\tret = ref_iterator_seek(iter, prefix,\n> -\t\t\t\t\t\tREF_ITERATOR_SEEK_SET_PREFIX);\n> +\t\t\tif (!ret)\n> +\t\t\t\tret = do_for_each_ref_iterator(iter, fn,\n> +\t\t\t\t\t\t\t       cb_data);\n> +\t\t} else {\n> +\t\t\t/*\n> +\t\t\t * Pass the prefix during construction because the files\n> +\t\t\t * backend primes loose refs before a later seek can\n> +\t\t\t * narrow the iterator.\n> +\t\t\t */\n> +\t\t\tstruct refs_for_each_ref_options opts = {\n> +\t\t\t\t.prefix = prefix,\n> +\t\t\t};\n>\n> -\t\tif (!ret)\n> -\t\t\tret = do_for_each_ref_iterator(iter, fn, cb_data);\n> +\t\t\tret = refs_for_each_ref_ext(store, fn, cb_data, &opts);\n> +\t\t}\n\nThis would work, as now we separate out the regular path to use\n`do_for_each_ref_iterator()` instead.\n\nBut this causes a bit of confusion, why do we need to use\n`do_for_each_ref_iterator()` and why not simply provide the prefix to\n`refs_ref_iterator_begin()`, like before?\n\nOn top of master, the below diff seems to fix the issue and works with\nthe benchmarks provided in this patch. (I haven't tested it with out\ntest suite though).\n\nmodified   ref-filter.c\n@@ -3316,15 +3316,16 @@ static int do_filter_refs(struct ref_filter\n*filter, unsigned int type, refs_for\n\n \tif (prefix) {\n \t\tstruct ref_iterator *iter;\n+\t\tstruct ref_store *store;\n\n-\t\titer = refs_ref_iterator_begin(get_main_ref_store(the_repository),\n-\t\t\t\t\t       \"\", NULL, 0, 0);\n+\t\tstore = get_main_ref_store(the_repository);\n\n-\t\tif (filter->start_after)\n+\t\tif (filter->start_after) {\n+\t\t\titer = refs_ref_iterator_begin(store, \"\", NULL, 0, 0);\n \t\t\tret = start_ref_iterator_after(iter, filter->start_after);\n-\t\telse\n-\t\t\tret = ref_iterator_seek(iter, prefix,\n-\t\t\t\t\t\tREF_ITERATOR_SEEK_SET_PREFIX);\n+\t\t} else {\n+\t\t\titer = refs_ref_iterator_begin(store, prefix, NULL, 0, 0);\n+\t\t}\n\n \t\tif (!ret)\n \t\t\tret = do_for_each_ref_iterator(iter, fn, cb_data);\n\n\nI would say something like this would make more sense, since it still\nkeeps the current structure without introducing a new command.\n\n>  \t} else if (filter->kind & FILTER_REFS_REGULAR) {\n>  \t\tret = for_each_fullref_in_pattern(filter, fn, cb_data);\n>  \t}\n> diff --git a/t/perf/p6300-for-each-ref.sh b/t/perf/p6300-for-each-ref.sh\n> index fa7289c752..ed9c1c6a19 100755\n> --- a/t/perf/p6300-for-each-ref.sh\n> +++ b/t/perf/p6300-for-each-ref.sh\n> @@ -1,6 +1,6 @@\n>  #!/bin/sh\n>\n> -test_description='performance of for-each-ref'\n> +test_description='performance of ref-filter users'\n>  . ./perf-lib.sh\n>\n>  test_perf_fresh_repo\n> @@ -84,4 +84,41 @@ test_expect_success 'pack refs' '\n>  '\n>  run_tests \"packed\"\n>\n> +test_expect_success REFFILES 'setup many unrelated loose refs' '\n> +\tgit init scoped &&\n> +\ttest_commit -C scoped --no-tag base &&\n> +\ttest_seq $ref_count_per_type |\n> +\t\tsed \"s,.*,update refs/custom/unrelated_& HEAD,\" |\n> +\t\tgit -C scoped update-ref --stdin &&\n> +\tgit -C scoped update-ref refs/remotes/origin/main HEAD &&\n> +\tgit -C scoped update-ref refs/tags/only HEAD\n> +'\n> +\n> +test_perf \"branch (many unrelated loose refs)\" --prereq REFFILES \"\n> +\t(\n> +\t\tcd scoped &&\n> +\t\tfor i in \\$(test_seq $test_iteration_count); do\n> +\t\t\tgit branch --format='%(refname)' >/dev/null\n> +\t\tdone\n> +\t)\n> +\"\n> +\n> +test_perf \"branch --remotes (many unrelated loose refs)\" --prereq REFFILES \"\n> +\t(\n> +\t\tcd scoped &&\n> +\t\tfor i in \\$(test_seq $test_iteration_count); do\n> +\t\t\tgit branch --remotes --format='%(refname)' >/dev/null\n> +\t\tdone\n> +\t)\n> +\"\n> +\n> +test_perf \"tag (many unrelated loose refs)\" --prereq REFFILES \"\n> +\t(\n> +\t\tcd scoped &&\n> +\t\tfor i in \\$(test_seq $test_iteration_count); do\n> +\t\t\tgit tag --format='%(refname)' >/dev/null\n> +\t\tdone\n> +\t)\n> +\"\n> +\n>  test_done\n>\n> ---\n> base-commit: a89346e34a937f001e5d397ee62224e3e9852040\n> change-id: 20260605-fix-git-branch-regression-9e4236f18091\n>\n> Best regards,\n> --\n> Tamir Duberstein <tamird@gmail.com>\n\nThanks for the patch, this is indeed a regression we must fix and the\nbenchmarks are a clear indication of it.\n"},{"id":"545134","messageId":"CAJ-ks9n=27u+Ujz0CBWRS+9ePNpqiiP+jkDfUrk4viMPR8qDww@mail.gmail.com","threadId":"65775","inReplyTo":"CAOLa=ZRHKNNymXGk31YgECjUmF9nZ8GsPUdQb7aKBH5DKMz7=w@mail.gmail.com","subject":"Re: [PATCH v2] ref-filter: restore prefix-scoped iteration","fromName":"Tamir Duberstein","fromEmail":"tamird@gmail.com","sentAt":"2026-06-10T12:25:03Z","receivedAt":"2026-06-10T12:25:41Z","isPatch":true,"body":"On Wed, Jun 10, 2026 at 3:50 AM Karthik Nayak <karthik.188@gmail.com> wrote:\n>\n> Tamir Duberstein <tamird@gmail.com> writes:\n>\n> > Commit dabecb9db2 (for-each-ref: introduce a '--start-after' option,\n> > 2025-07-15) changed single-kind branch, remote-tracking branch, and tag\n> > enumeration in do_filter_refs() from constructing an iterator with the\n> > namespace prefix to constructing an unscoped iterator and applying the\n> > prefix with ref_iterator_seek().\n> >\n> > Before that change, refs_for_each_fullref_in() passed the namespace\n> > prefix during iterator construction. That helper has since been\n> > replaced by refs_for_each_ref_ext().\n> >\n> > The files backend primes its loose-ref cache for the construction\n> > prefix before it opens packed refs. An empty construction prefix\n> > therefore reads every loose ref, and a later seek cannot undo that I/O.\n> > Consequently, git branch, git branch --remotes, and git tag scale with\n> > unrelated loose refs.\n> >\n>\n> And this is the crux of the issue. Currently we do\n>\n> - refs_ref_iterator_begin()\n>   - ref_iterator_seek()\n>\n> And between the two `cache_ref_iterator_set_prefix()` is already called\n> which caches all the loose refs. This is the IO intensive operation this\n> patch tries to avoid.\n>\n> I think it would be worthwhile to add this information in the commit\n> message.\n\nAgreed. I will explain that `cache_ref_iterator_set_prefix()` primes\nthe loose-ref cache during iterator construction, before the later\nseek can narrow it.\n\n>\n> >\n> > Patrick Steinhardt observed during review that iterator construction\n> > and seeking accepted similar strings but assigned them different state\n> > semantics. Junio C Hamano then pointed out that no current command can\n> > combine start_after with this single-kind path, but future branch or\n> > tag support would need to keep the namespace while moving the cursor.\n> >\n> > Keep the existing start_after path unchanged. The iterator API cannot\n> > currently seek to one string while retaining another as its prefix:\n> > an unflagged seek clears the prefix, while REF_ITERATOR_SEEK_SET_PREFIX\n> > replaces it with the seek string.\n> >\n> > For the commands affected by this regression, which do not set\n> > start_after, pass the namespace prefix during iterator construction so\n> > that loose refs are scoped before the packed-refs snapshot is opened.\n> > This fixes the current regression without deleting the ref-filter state\n> > discussed during review or changing its dormant behavior.\n> >\n> > Add REFFILES-gated performance cases with one branch, one\n> > remote-tracking branch, one tag, and 10,000 unrelated loose refs. The\n> > benchmarks were run with:\n> >\n> >     GIT_PERF_REPEAT_COUNT=5 GIT_PERF_MAKE_OPTS=-j8 \\\n> >         t/perf/run a89346e34a . -- p6300-for-each-ref.sh\n> >\n> > The following are the best of five runs, with each run invoking the\n> > command ten times. Times are elapsed seconds with user and system CPU\n> > seconds in parentheses:\n> >\n> >                                   a89346e34a       this commit\n> >   branch                       2.74(0.13+2.56)   0.11(0.04+0.04)\n> >   branch --remotes             2.81(0.13+2.62)   0.12(0.04+0.04)\n> >   tag                          3.01(0.14+2.82)   0.11(0.04+0.04)\n> >\n> > Both revisions used the default -O2 build flags and a config.mak\n> > containing only \"NO_REGEX = NeedsStartEnd\". They were built with Apple\n> > clang 21.0.0 on macOS 26.5. The machine was a MacBook Pro (Mac16,6)\n> > with a 16-core Apple M4 Max (12 performance and four efficiency cores)\n> > and 128 GB RAM.\n> >\n> > Link: https://lore.kernel.org/git/aGZidwwlToWThkn8@pks.im/\n> > Link: https://lore.kernel.org/git/xmqqikjq7s16.fsf@gitster.g/\n> > Fixes: dabecb9db2b2 (\"for-each-ref: introduce a '--start-after' option\")\n> > Assisted-by: Codex gpt-5.5\n> > Signed-off-by: Tamir Duberstein <tamird@gmail.com>\n> > ---\n> > The series is based on a89346e34a (maint) because the regression has\n> > been present in released versions since Git 2.51.0.\n> > ---\n> > Changes in v2:\n> > - Extract local variable `store`.\n> > - Link to v1: https://patch.msgid.link/20260605-fix-git-branch-regression-v1-1-02f40ad40929@gmail.com\n> > ---\n> >  ref-filter.c                 | 28 +++++++++++++++++++---------\n> >  t/perf/p6300-for-each-ref.sh | 39 ++++++++++++++++++++++++++++++++++++++-\n> >  2 files changed, 57 insertions(+), 10 deletions(-)\n> >\n> > diff --git a/ref-filter.c b/ref-filter.c\n> > index 1da4c0e60d..5cbc007d64 100644\n> > --- a/ref-filter.c\n> > +++ b/ref-filter.c\n> > @@ -3315,19 +3315,29 @@ static int do_filter_refs(struct ref_filter *filter, unsigned int type, refs_for\n> >               prefix = \"refs/tags/\";\n> >\n> >       if (prefix) {\n> > -             struct ref_iterator *iter;\n> > +             struct ref_store *store = get_main_ref_store(the_repository);\n> >\n> > -             iter = refs_ref_iterator_begin(get_main_ref_store(the_repository),\n> > -                                            \"\", NULL, 0, 0);\n> > +             if (filter->start_after) {\n> > +                     struct ref_iterator *iter;\n> > +\n> > +                     iter = refs_ref_iterator_begin(store, \"\", NULL, 0, 0);\n> >\n> > -             if (filter->start_after)\n> >                       ret = start_ref_iterator_after(iter, filter->start_after);\n> > -             else\n> > -                     ret = ref_iterator_seek(iter, prefix,\n> > -                                             REF_ITERATOR_SEEK_SET_PREFIX);\n> > +                     if (!ret)\n> > +                             ret = do_for_each_ref_iterator(iter, fn,\n> > +                                                            cb_data);\n> > +             } else {\n> > +                     /*\n> > +                      * Pass the prefix during construction because the files\n> > +                      * backend primes loose refs before a later seek can\n> > +                      * narrow the iterator.\n> > +                      */\n> > +                     struct refs_for_each_ref_options opts = {\n> > +                             .prefix = prefix,\n> > +                     };\n> >\n> > -             if (!ret)\n> > -                     ret = do_for_each_ref_iterator(iter, fn, cb_data);\n> > +                     ret = refs_for_each_ref_ext(store, fn, cb_data, &opts);\n> > +             }\n>\n> This would work, as now we separate out the regular path to use\n> `do_for_each_ref_iterator()` instead.\n>\n> But this causes a bit of confusion, why do we need to use\n> `do_for_each_ref_iterator()` and why not simply provide the prefix to\n> `refs_ref_iterator_begin()`, like before?\n\nWe do not. Your version is simpler and preserves the existing iterator\nflow. I have adopted it for v3. Thanks!\n\n> [...]\n>\n> Thanks for the patch, this is indeed a regression we must fix and the\n> benchmarks are a clear indication of it.\n\nThank you! I'll try not to break threading on the next roll.\n"},{"id":"545135","messageId":"20260610-fix-git-branch-regression-v3-1-6fd48fad7a53@gmail.com","threadId":"65775","inReplyTo":"20260608-fix-git-branch-regression-v2-1-fd82075a8520@gmail.com","subject":"[PATCH v3] ref-filter: restore prefix-scoped iteration","fromName":"Tamir Duberstein","fromEmail":"tamird@gmail.com","sentAt":"2026-06-10T12:29:49Z","receivedAt":"2026-06-10T12:30:01Z","isPatch":true,"body":"dabecb9db2 (for-each-ref: introduce a '--start-after' option,\n2025-07-15) changed branch, remote-tracking branch, and tag enumeration\nfrom constructing an iterator with the namespace prefix to constructing\nan unscoped iterator and seeking to the prefix.\n\nThe files backend constructs its loose-ref iterator with cache priming\nenabled. cache_ref_iterator_begin() immediately applies the construction\nprefix through cache_ref_iterator_set_prefix(), reading loose refs\nbeneath it before packed refs are opened. An empty prefix therefore\nreads every loose ref, and a later seek cannot undo that I/O.\n\nFor these single-kind filters, construct the iterator with the namespace\nprefix when start_after is not set. Keep the existing unscoped\nconstruction for start_after, whose seek position may differ from the\nnamespace prefix.\n\nWith 10,000 unrelated loose refs, the p6300 tests improve as follows:\n\n                         before   after\n  branch                  2.74 s   0.11 s\n  branch --remotes        2.81 s   0.12 s\n  tag                     3.01 s   0.11 s\n\nLink: https://lore.kernel.org/git/aGZidwwlToWThkn8@pks.im/\nLink: https://lore.kernel.org/git/xmqqikjq7s16.fsf@gitster.g/\nLink: https://lore.kernel.org/r/CAOLa=ZRHKNNymXGk31YgECjUmF9nZ8GsPUdQb7aKBH5DKMz7=w@mail.gmail.com\nFixes: dabecb9db2b2 (\"for-each-ref: introduce a '--start-after' option\")\nSuggested-by: Karthik Nayak <karthik.188@gmail.com>\nAssisted-by: Codex gpt-5.5\nSigned-off-by: Tamir Duberstein <tamird@gmail.com>\n---\nThe series is based on a89346e34a (maint) because the regression has\nbeen present in released versions since Git 2.51.0.\n---\nChanges in v3:\n- Construct the iterator directly with the namespace prefix.\n- Explain when the files backend primes its loose-ref cache.\n- Condense the commit message and performance results.\n- Link to v2: https://patch.msgid.link/20260608-fix-git-branch-regression-v2-1-fd82075a8520@gmail.com\n\nChanges in v2:\n- Extract local variable `store`.\n- Link to v1: https://patch.msgid.link/20260605-fix-git-branch-regression-v1-1-02f40ad40929@gmail.com\n---\n ref-filter.c                 | 13 ++++++-------\n t/perf/p6300-for-each-ref.sh | 39 ++++++++++++++++++++++++++++++++++++++-\n 2 files changed, 44 insertions(+), 8 deletions(-)\n\ndiff --git a/ref-filter.c b/ref-filter.c\nindex 1da4c0e60d..9b04e3af85 100644\n--- a/ref-filter.c\n+++ b/ref-filter.c\n@@ -3316,15 +3316,14 @@ static int do_filter_refs(struct ref_filter *filter, unsigned int type, refs_for\n \n \tif (prefix) {\n \t\tstruct ref_iterator *iter;\n+\t\tstruct ref_store *store = get_main_ref_store(the_repository);\n \n-\t\titer = refs_ref_iterator_begin(get_main_ref_store(the_repository),\n-\t\t\t\t\t       \"\", NULL, 0, 0);\n-\n-\t\tif (filter->start_after)\n+\t\tif (filter->start_after) {\n+\t\t\titer = refs_ref_iterator_begin(store, \"\", NULL, 0, 0);\n \t\t\tret = start_ref_iterator_after(iter, filter->start_after);\n-\t\telse\n-\t\t\tret = ref_iterator_seek(iter, prefix,\n-\t\t\t\t\t\tREF_ITERATOR_SEEK_SET_PREFIX);\n+\t\t} else {\n+\t\t\titer = refs_ref_iterator_begin(store, prefix, NULL, 0, 0);\n+\t\t}\n \n \t\tif (!ret)\n \t\t\tret = do_for_each_ref_iterator(iter, fn, cb_data);\ndiff --git a/t/perf/p6300-for-each-ref.sh b/t/perf/p6300-for-each-ref.sh\nindex fa7289c752..ed9c1c6a19 100755\n--- a/t/perf/p6300-for-each-ref.sh\n+++ b/t/perf/p6300-for-each-ref.sh\n@@ -1,6 +1,6 @@\n #!/bin/sh\n \n-test_description='performance of for-each-ref'\n+test_description='performance of ref-filter users'\n . ./perf-lib.sh\n \n test_perf_fresh_repo\n@@ -84,4 +84,41 @@ test_expect_success 'pack refs' '\n '\n run_tests \"packed\"\n \n+test_expect_success REFFILES 'setup many unrelated loose refs' '\n+\tgit init scoped &&\n+\ttest_commit -C scoped --no-tag base &&\n+\ttest_seq $ref_count_per_type |\n+\t\tsed \"s,.*,update refs/custom/unrelated_& HEAD,\" |\n+\t\tgit -C scoped update-ref --stdin &&\n+\tgit -C scoped update-ref refs/remotes/origin/main HEAD &&\n+\tgit -C scoped update-ref refs/tags/only HEAD\n+'\n+\n+test_perf \"branch (many unrelated loose refs)\" --prereq REFFILES \"\n+\t(\n+\t\tcd scoped &&\n+\t\tfor i in \\$(test_seq $test_iteration_count); do\n+\t\t\tgit branch --format='%(refname)' >/dev/null\n+\t\tdone\n+\t)\n+\"\n+\n+test_perf \"branch --remotes (many unrelated loose refs)\" --prereq REFFILES \"\n+\t(\n+\t\tcd scoped &&\n+\t\tfor i in \\$(test_seq $test_iteration_count); do\n+\t\t\tgit branch --remotes --format='%(refname)' >/dev/null\n+\t\tdone\n+\t)\n+\"\n+\n+test_perf \"tag (many unrelated loose refs)\" --prereq REFFILES \"\n+\t(\n+\t\tcd scoped &&\n+\t\tfor i in \\$(test_seq $test_iteration_count); do\n+\t\t\tgit tag --format='%(refname)' >/dev/null\n+\t\tdone\n+\t)\n+\"\n+\n test_done\n\n---\nbase-commit: a89346e34a937f001e5d397ee62224e3e9852040\nchange-id: 20260605-fix-git-branch-regression-9e4236f18091\n\nBest regards,\n--  \nTamir Duberstein <tamird@gmail.com>\n\n"},{"id":"545369","messageId":"aivx-7VOKE_TC50R@pks.im","threadId":"65775","inReplyTo":"20260610-fix-git-branch-regression-v3-1-6fd48fad7a53@gmail.com","subject":"Re: [PATCH v3] ref-filter: restore prefix-scoped iteration","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-06-12T11:48:11Z","receivedAt":"2026-06-12T11:48:18Z","isPatch":true,"body":"On Wed, Jun 10, 2026 at 05:29:49AM -0700, Tamir Duberstein wrote:\n> dabecb9db2 (for-each-ref: introduce a '--start-after' option,\n> 2025-07-15) changed branch, remote-tracking branch, and tag enumeration\n> from constructing an iterator with the namespace prefix to constructing\n> an unscoped iterator and seeking to the prefix.\n> \n> The files backend constructs its loose-ref iterator with cache priming\n> enabled. cache_ref_iterator_begin() immediately applies the construction\n> prefix through cache_ref_iterator_set_prefix(), reading loose refs\n> beneath it before packed refs are opened. An empty prefix therefore\n> reads every loose ref, and a later seek cannot undo that I/O.\n> \n> For these single-kind filters, construct the iterator with the namespace\n> prefix when start_after is not set. Keep the existing unscoped\n> construction for start_after, whose seek position may differ from the\n> namespace prefix.\n> \n> With 10,000 unrelated loose refs, the p6300 tests improve as follows:\n> \n>                          before   after\n>   branch                  2.74 s   0.11 s\n>   branch --remotes        2.81 s   0.12 s\n>   tag                     3.01 s   0.11 s\n> \n> Link: https://lore.kernel.org/git/aGZidwwlToWThkn8@pks.im/\n> Link: https://lore.kernel.org/git/xmqqikjq7s16.fsf@gitster.g/\n> Link: https://lore.kernel.org/r/CAOLa=ZRHKNNymXGk31YgECjUmF9nZ8GsPUdQb7aKBH5DKMz7=w@mail.gmail.com\n\nI honestly have no idea what you want to say with these links, as they\nseem to just link to random reviews mails when the above mentioned\ncommit was reviewed. In general, we typically try to embed references\nlike this into the explanation, like:\n\n    In [1], we discussed... and this is relevant because of ...\n\n    [1]: https://lore.kernel.org/git/aGZidwwlToWThkn8@pks.im/\n\nJust dropping the links as-is without much of an explanation isn't\nhelpful.\n\n> diff --git a/ref-filter.c b/ref-filter.c\n> index 1da4c0e60d..9b04e3af85 100644\n> --- a/ref-filter.c\n> +++ b/ref-filter.c\n> @@ -3316,15 +3316,14 @@ static int do_filter_refs(struct ref_filter *filter, unsigned int type, refs_for\n>  \n>  \tif (prefix) {\n>  \t\tstruct ref_iterator *iter;\n> +\t\tstruct ref_store *store = get_main_ref_store(the_repository);\n>  \n> -\t\titer = refs_ref_iterator_begin(get_main_ref_store(the_repository),\n> -\t\t\t\t\t       \"\", NULL, 0, 0);\n> -\n> -\t\tif (filter->start_after)\n> +\t\tif (filter->start_after) {\n> +\t\t\titer = refs_ref_iterator_begin(store, \"\", NULL, 0, 0);\n>  \t\t\tret = start_ref_iterator_after(iter, filter->start_after);\n> -\t\telse\n> -\t\t\tret = ref_iterator_seek(iter, prefix,\n> -\t\t\t\t\t\tREF_ITERATOR_SEEK_SET_PREFIX);\n> +\t\t} else {\n> +\t\t\titer = refs_ref_iterator_begin(store, prefix, NULL, 0, 0);\n> +\t\t}\n>  \n>  \t\tif (!ret)\n>  \t\t\tret = do_for_each_ref_iterator(iter, fn, cb_data);\n\nThe patch itself seems sensible to me.\n\n> diff --git a/t/perf/p6300-for-each-ref.sh b/t/perf/p6300-for-each-ref.sh\n> index fa7289c752..ed9c1c6a19 100755\n> --- a/t/perf/p6300-for-each-ref.sh\n> +++ b/t/perf/p6300-for-each-ref.sh\n> @@ -1,6 +1,6 @@\n>  #!/bin/sh\n>  \n> -test_description='performance of for-each-ref'\n> +test_description='performance of ref-filter users'\n>  . ./perf-lib.sh\n>  \n>  test_perf_fresh_repo\n> @@ -84,4 +84,41 @@ test_expect_success 'pack refs' '\n>  '\n>  run_tests \"packed\"\n>  \n> +test_expect_success REFFILES 'setup many unrelated loose refs' '\n> +\tgit init scoped &&\n> +\ttest_commit -C scoped --no-tag base &&\n> +\ttest_seq $ref_count_per_type |\n> +\t\tsed \"s,.*,update refs/custom/unrelated_& HEAD,\" |\n> +\t\tgit -C scoped update-ref --stdin &&\n> +\tgit -C scoped update-ref refs/remotes/origin/main HEAD &&\n> +\tgit -C scoped update-ref refs/tags/only HEAD\n> +'\n\nI've already called this out before on other patches, but the REFFILES\nprerequisite just doesn't make any sense here as this test logic is\ngeneric.\n\nPatrick\n"},{"id":"545420","messageId":"CAJ-ks9mZWnx49WXnmY3=on-n=33iLBULP7qqvh=TN2kYwJK+TQ@mail.gmail.com","threadId":"65775","inReplyTo":"aivx-7VOKE_TC50R@pks.im","subject":"Re: [PATCH v3] ref-filter: restore prefix-scoped iteration","fromName":"Tamir Duberstein","fromEmail":"tamird@gmail.com","sentAt":"2026-06-12T21:24:49Z","receivedAt":"2026-06-12T21:25:30Z","isPatch":true,"body":"On Fri, Jun 12, 2026 at 7:48 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Wed, Jun 10, 2026 at 05:29:49AM -0700, Tamir Duberstein wrote:\n> > dabecb9db2 (for-each-ref: introduce a '--start-after' option,\n> > 2025-07-15) changed branch, remote-tracking branch, and tag enumeration\n> > from constructing an iterator with the namespace prefix to constructing\n> > an unscoped iterator and seeking to the prefix.\n> >\n> > The files backend constructs its loose-ref iterator with cache priming\n> > enabled. cache_ref_iterator_begin() immediately applies the construction\n> > prefix through cache_ref_iterator_set_prefix(), reading loose refs\n> > beneath it before packed refs are opened. An empty prefix therefore\n> > reads every loose ref, and a later seek cannot undo that I/O.\n> >\n> > For these single-kind filters, construct the iterator with the namespace\n> > prefix when start_after is not set. Keep the existing unscoped\n> > construction for start_after, whose seek position may differ from the\n> > namespace prefix.\n> >\n> > With 10,000 unrelated loose refs, the p6300 tests improve as follows:\n> >\n> >                          before   after\n> >   branch                  2.74 s   0.11 s\n> >   branch --remotes        2.81 s   0.12 s\n> >   tag                     3.01 s   0.11 s\n> >\n> > Link: https://lore.kernel.org/git/aGZidwwlToWThkn8@pks.im/\n> > Link: https://lore.kernel.org/git/xmqqikjq7s16.fsf@gitster.g/\n> > Link: https://lore.kernel.org/r/CAOLa=ZRHKNNymXGk31YgECjUmF9nZ8GsPUdQb7aKBH5DKMz7=w@mail.gmail.com\n>\n> I honestly have no idea what you want to say with these links, as they\n> seem to just link to random reviews mails when the above mentioned\n> commit was reviewed. In general, we typically try to embed references\n> like this into the explanation, like:\n>\n>     In [1], we discussed... and this is relevant because of ...\n>\n>     [1]: https://lore.kernel.org/git/aGZidwwlToWThkn8@pks.im/\n>\n> Just dropping the links as-is without much of an explanation isn't\n> helpful.\n\nWill be numbered references in next spin.\n\n>\n> > diff --git a/ref-filter.c b/ref-filter.c\n> > index 1da4c0e60d..9b04e3af85 100644\n> > --- a/ref-filter.c\n> > +++ b/ref-filter.c\n> > @@ -3316,15 +3316,14 @@ static int do_filter_refs(struct ref_filter *filter, unsigned int type, refs_for\n> >\n> >       if (prefix) {\n> >               struct ref_iterator *iter;\n> > +             struct ref_store *store = get_main_ref_store(the_repository);\n> >\n> > -             iter = refs_ref_iterator_begin(get_main_ref_store(the_repository),\n> > -                                            \"\", NULL, 0, 0);\n> > -\n> > -             if (filter->start_after)\n> > +             if (filter->start_after) {\n> > +                     iter = refs_ref_iterator_begin(store, \"\", NULL, 0, 0);\n> >                       ret = start_ref_iterator_after(iter, filter->start_after);\n> > -             else\n> > -                     ret = ref_iterator_seek(iter, prefix,\n> > -                                             REF_ITERATOR_SEEK_SET_PREFIX);\n> > +             } else {\n> > +                     iter = refs_ref_iterator_begin(store, prefix, NULL, 0, 0);\n> > +             }\n> >\n> >               if (!ret)\n> >                       ret = do_for_each_ref_iterator(iter, fn, cb_data);\n>\n> The patch itself seems sensible to me.\n>\n> > diff --git a/t/perf/p6300-for-each-ref.sh b/t/perf/p6300-for-each-ref.sh\n> > index fa7289c752..ed9c1c6a19 100755\n> > --- a/t/perf/p6300-for-each-ref.sh\n> > +++ b/t/perf/p6300-for-each-ref.sh\n> > @@ -1,6 +1,6 @@\n> >  #!/bin/sh\n> >\n> > -test_description='performance of for-each-ref'\n> > +test_description='performance of ref-filter users'\n> >  . ./perf-lib.sh\n> >\n> >  test_perf_fresh_repo\n> > @@ -84,4 +84,41 @@ test_expect_success 'pack refs' '\n> >  '\n> >  run_tests \"packed\"\n> >\n> > +test_expect_success REFFILES 'setup many unrelated loose refs' '\n> > +     git init scoped &&\n> > +     test_commit -C scoped --no-tag base &&\n> > +     test_seq $ref_count_per_type |\n> > +             sed \"s,.*,update refs/custom/unrelated_& HEAD,\" |\n> > +             git -C scoped update-ref --stdin &&\n> > +     git -C scoped update-ref refs/remotes/origin/main HEAD &&\n> > +     git -C scoped update-ref refs/tags/only HEAD\n> > +'\n>\n> I've already called this out before on other patches, but the REFFILES\n> prerequisite just doesn't make any sense here as this test logic is\n> generic.\n\nYou're right. Removed in v4.\n"},{"id":"545422","messageId":"20260612-fix-git-branch-regression-v4-1-f150038c02f4@gmail.com","threadId":"65775","inReplyTo":"20260610-fix-git-branch-regression-v3-1-6fd48fad7a53@gmail.com","subject":"[PATCH v4] ref-filter: restore prefix-scoped iteration","fromName":"Tamir Duberstein","fromEmail":"tamird@gmail.com","sentAt":"2026-06-12T21:27:44Z","receivedAt":"2026-06-12T21:27:54Z","isPatch":true,"body":"dabecb9db2 (for-each-ref: introduce a '--start-after' option,\n2025-07-15) changed branch, remote-tracking branch, and tag enumeration\nfrom constructing an iterator with the namespace prefix to constructing\nan unscoped iterator and seeking to the prefix.\n\nReview of --start-after noted that the construction prefix and seek\nposition represent different state and are easy to conflate [1]. It also\nnoted that future branch or tag support would need to retain the\nnamespace prefix while moving the cursor [2].\n\nThe files backend constructs its loose-ref iterator with cache priming\nenabled. cache_ref_iterator_begin() immediately applies the construction\nprefix through cache_ref_iterator_set_prefix(), reading loose refs\nbeneath it before packed refs are opened. An empty prefix therefore\nreads every loose ref, and a later seek cannot undo that I/O.\n\nFor the current single-kind filters, construct the iterator with the\nnamespace prefix when start_after is not set. Leave the existing\nstart_after path unchanged; no current command combines it with these\nfilters, and future support must carry the prefix separately from the\ncursor.\n\nWith 10,000 unrelated loose refs in the files backend, the p6300 tests\nimprove as follows:\n\n                         before   after\n  branch                  2.74 s   0.11 s\n  branch --remotes        2.81 s   0.12 s\n  tag                     3.01 s   0.11 s\n\n[1] https://lore.kernel.org/r/aGZidwwlToWThkn8@pks.im/\n[2] https://lore.kernel.org/r/xmqqikjq7s16.fsf@gitster.g/\n\nFixes: dabecb9db2b2 (\"for-each-ref: introduce a '--start-after' option\")\nSuggested-by: Karthik Nayak <karthik.188@gmail.com>\nSigned-off-by: Tamir Duberstein <tamird@gmail.com>\n---\nThe series is based on a89346e34a (maint) because the regression has\nbeen present in released versions since Git 2.51.0.\n---\nChanges in v4:\n- Explain the historical references in the commit message.\n- Run the new performance cases with both ref backends.\n- Drop the Assisted-by trailer.\n- Link to v3: https://patch.msgid.link/20260610-fix-git-branch-regression-v3-1-6fd48fad7a53@gmail.com\n\nChanges in v3:\n- Construct the iterator directly with the namespace prefix.\n- Explain when the files backend primes its loose-ref cache.\n- Condense the commit message and performance results.\n- Link to v2: https://patch.msgid.link/20260608-fix-git-branch-regression-v2-1-fd82075a8520@gmail.com\n\nChanges in v2:\n- Extract local variable `store`.\n- Link to v1: https://patch.msgid.link/20260605-fix-git-branch-regression-v1-1-02f40ad40929@gmail.com\n---\n ref-filter.c                 | 13 ++++++-------\n t/perf/p6300-for-each-ref.sh | 39 ++++++++++++++++++++++++++++++++++++++-\n 2 files changed, 44 insertions(+), 8 deletions(-)\n\ndiff --git a/ref-filter.c b/ref-filter.c\nindex 1da4c0e60d..9b04e3af85 100644\n--- a/ref-filter.c\n+++ b/ref-filter.c\n@@ -3316,15 +3316,14 @@ static int do_filter_refs(struct ref_filter *filter, unsigned int type, refs_for\n \n \tif (prefix) {\n \t\tstruct ref_iterator *iter;\n+\t\tstruct ref_store *store = get_main_ref_store(the_repository);\n \n-\t\titer = refs_ref_iterator_begin(get_main_ref_store(the_repository),\n-\t\t\t\t\t       \"\", NULL, 0, 0);\n-\n-\t\tif (filter->start_after)\n+\t\tif (filter->start_after) {\n+\t\t\titer = refs_ref_iterator_begin(store, \"\", NULL, 0, 0);\n \t\t\tret = start_ref_iterator_after(iter, filter->start_after);\n-\t\telse\n-\t\t\tret = ref_iterator_seek(iter, prefix,\n-\t\t\t\t\t\tREF_ITERATOR_SEEK_SET_PREFIX);\n+\t\t} else {\n+\t\t\titer = refs_ref_iterator_begin(store, prefix, NULL, 0, 0);\n+\t\t}\n \n \t\tif (!ret)\n \t\t\tret = do_for_each_ref_iterator(iter, fn, cb_data);\ndiff --git a/t/perf/p6300-for-each-ref.sh b/t/perf/p6300-for-each-ref.sh\nindex fa7289c752..25ffa5e84c 100755\n--- a/t/perf/p6300-for-each-ref.sh\n+++ b/t/perf/p6300-for-each-ref.sh\n@@ -1,6 +1,6 @@\n #!/bin/sh\n \n-test_description='performance of for-each-ref'\n+test_description='performance of ref-filter users'\n . ./perf-lib.sh\n \n test_perf_fresh_repo\n@@ -84,4 +84,41 @@ test_expect_success 'pack refs' '\n '\n run_tests \"packed\"\n \n+test_expect_success 'setup many unrelated refs' '\n+\tgit init scoped &&\n+\ttest_commit -C scoped --no-tag base &&\n+\ttest_seq $ref_count_per_type |\n+\t\tsed \"s,.*,update refs/custom/unrelated_& HEAD,\" |\n+\t\tgit -C scoped update-ref --stdin &&\n+\tgit -C scoped update-ref refs/remotes/origin/main HEAD &&\n+\tgit -C scoped update-ref refs/tags/only HEAD\n+'\n+\n+test_perf \"branch (many unrelated refs)\" \"\n+\t(\n+\t\tcd scoped &&\n+\t\tfor i in \\$(test_seq $test_iteration_count); do\n+\t\t\tgit branch --format='%(refname)' >/dev/null\n+\t\tdone\n+\t)\n+\"\n+\n+test_perf \"branch --remotes (many unrelated refs)\" \"\n+\t(\n+\t\tcd scoped &&\n+\t\tfor i in \\$(test_seq $test_iteration_count); do\n+\t\t\tgit branch --remotes --format='%(refname)' >/dev/null\n+\t\tdone\n+\t)\n+\"\n+\n+test_perf \"tag (many unrelated refs)\" \"\n+\t(\n+\t\tcd scoped &&\n+\t\tfor i in \\$(test_seq $test_iteration_count); do\n+\t\t\tgit tag --format='%(refname)' >/dev/null\n+\t\tdone\n+\t)\n+\"\n+\n test_done\n\n---\nbase-commit: a89346e34a937f001e5d397ee62224e3e9852040\nchange-id: 20260605-fix-git-branch-regression-9e4236f18091\n\nBest regards,\n--  \nTamir Duberstein <tamird@gmail.com>\n\n"},{"id":"545611","messageId":"CAJ-ks9=Qa0956vCmE=RyReu4jfdXzjiSO4rRGD9nGbNNrMvOfA@mail.gmail.com","threadId":"65775","inReplyTo":"20260612-fix-git-branch-regression-v4-1-f150038c02f4@gmail.com","subject":"Re: [PATCH v4] ref-filter: restore prefix-scoped iteration","fromName":"Tamir Duberstein","fromEmail":"tamird@gmail.com","sentAt":"2026-06-15T20:47:14Z","receivedAt":"2026-06-15T20:47:52Z","isPatch":true,"body":"On Fri, Jun 12, 2026 at 5:27 PM Tamir Duberstein <tamird@gmail.com> wrote:\n>\n> dabecb9db2 (for-each-ref: introduce a '--start-after' option,\n> 2025-07-15) changed branch, remote-tracking branch, and tag enumeration\n> from constructing an iterator with the namespace prefix to constructing\n> an unscoped iterator and seeking to the prefix.\n>\n> Review of --start-after noted that the construction prefix and seek\n> position represent different state and are easy to conflate [1]. It also\n> noted that future branch or tag support would need to retain the\n> namespace prefix while moving the cursor [2].\n>\n> The files backend constructs its loose-ref iterator with cache priming\n> enabled. cache_ref_iterator_begin() immediately applies the construction\n> prefix through cache_ref_iterator_set_prefix(), reading loose refs\n> beneath it before packed refs are opened. An empty prefix therefore\n> reads every loose ref, and a later seek cannot undo that I/O.\n>\n> For the current single-kind filters, construct the iterator with the\n> namespace prefix when start_after is not set. Leave the existing\n> start_after path unchanged; no current command combines it with these\n> filters, and future support must carry the prefix separately from the\n> cursor.\n>\n> With 10,000 unrelated loose refs in the files backend, the p6300 tests\n> improve as follows:\n>\n>                          before   after\n>   branch                  2.74 s   0.11 s\n>   branch --remotes        2.81 s   0.12 s\n>   tag                     3.01 s   0.11 s\n>\n> [1] https://lore.kernel.org/r/aGZidwwlToWThkn8@pks.im/\n> [2] https://lore.kernel.org/r/xmqqikjq7s16.fsf@gitster.g/\n>\n> Fixes: dabecb9db2b2 (\"for-each-ref: introduce a '--start-after' option\")\n> Suggested-by: Karthik Nayak <karthik.188@gmail.com>\n> Signed-off-by: Tamir Duberstein <tamird@gmail.com>\n> ---\n> The series is based on a89346e34a (maint) because the regression has\n> been present in released versions since Git 2.51.0.\n> ---\n> Changes in v4:\n> - Explain the historical references in the commit message.\n> - Run the new performance cases with both ref backends.\n> - Drop the Assisted-by trailer.\n> - Link to v3: https://patch.msgid.link/20260610-fix-git-branch-regression-v3-1-6fd48fad7a53@gmail.com\n>\n> Changes in v3:\n> - Construct the iterator directly with the namespace prefix.\n> - Explain when the files backend primes its loose-ref cache.\n> - Condense the commit message and performance results.\n> - Link to v2: https://patch.msgid.link/20260608-fix-git-branch-regression-v2-1-fd82075a8520@gmail.com\n>\n> Changes in v2:\n> - Extract local variable `store`.\n> - Link to v1: https://patch.msgid.link/20260605-fix-git-branch-regression-v1-1-02f40ad40929@gmail.com\n> ---\n\nHi folks, does this look reasonable?\n"},{"id":"545863","messageId":"xmqqik7fsv2m.fsf@gitster.g","threadId":"65775","inReplyTo":"20260612-fix-git-branch-regression-v4-1-f150038c02f4@gmail.com","subject":"Re: [PATCH v4] ref-filter: restore prefix-scoped iteration","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-18T15:54:25Z","receivedAt":"2026-06-18T15:54:28Z","isPatch":true,"body":"Tamir Duberstein <tamird@gmail.com> writes:\n\n> Changes in v4:\n> - Explain the historical references in the commit message.\n> - Run the new performance cases with both ref backends.\n> - Drop the Assisted-by trailer.\n> - Link to v3: https://patch.msgid.link/20260610-fix-git-branch-regression-v3-1-6fd48fad7a53@gmail.com\n\nThis seems to fully address comments by Patrick in\nhttps://lore.kernel.org/git/aivx-7VOKE_TC50R@pks.im/\n\nLet me mark the topic for 'next'.  Thanks all who discussed this patch.\n"}]}