From: Jon Simons <jon@jonsimons.org>
To: git@vger.kernel.org
Cc: Jon Simons <jon@jonsimons.org>
Subject: [PATCH 05/15] refs: stop using mkpath() in refname_match()
Date: Fri, 9 Oct 2026 15:29:43 -0400 [thread overview]
Message-ID: <20261009192953.81794-6-jon@jonsimons.org> (raw)
In-Reply-To: <20261009192953.81794-1-jon@jonsimons.org>
refname_match() formats every ref_rev_parse_rules entry through mkpath()
to then strcmp() with its given full_name. It turns out this formatting
function dominates the client-side cost of matching explicit refspecs
against a remote that advertises lots of refs.
Reuse match_parse_rule() instead to efficiently compare each rule
against its given full_name. With this change we avoid mkpath()'s
strbuf_vaddf(), and each match is now a direct prefix + suffix
comparison, length check, and memcmp of the refname at hand.
Timings from the test added in the previous commit:
Test HEAD~1 HEAD
-----------------------------------------------------------------------
5516.3: empty:refspecs:1 0.15(0.09+0.11) 0.14(0.07+0.11) -6.7%
5516.5: empty:refspecs:10 0.38(0.32+0.11) 0.17(0.11+0.11) -55.3%
5516.7: empty:refspecs:100 2.50(2.43+0.11) 0.49(0.42+0.11) -80.4%
5516.9: mirror:refspecs:1 0.21(0.15+0.11) 0.16(0.09+0.11) -23.8%
5516.11: mirror:refspecs:10 0.80(0.73+0.11) 0.26(0.19+0.11) -67.5%
5516.13: mirror:refspecs:100 6.68(6.59+0.13) 1.18(1.11+0.11) -82.3%
Notes on a change in behavior:
- mkpath() runs cleanup_path(), which strips leading "./". So,
"./refs/heads/foo" previously matched "refs/heads/foo" through the
"%.*s" rule. As of this commit, this is no longer true.
- It's considered a bug that "./"-prefixed strings previously could
have been matched with refnames in this way:
- cleanup_path() dates back to 26c8a533af (Add "mkpath()" helper
function, 2005-07-08), and is intended to be used for filepaths,
not refnames. The leading-"./" strip itself comes from f17a1b1bec
(Fix up path-cleanup in git_path() properly, 2005-07-05).
- refname_match() has used mkpath() in its matching loop since its
inception with 79803322c1 (add refname_match(), 2007-11-11).
- 6cd4a8982d (avoid using mksnpath for refs, 2017-03-28) previously
removed other cleanup_path() spots reachable from mksnpath() and
notes they were "questionable when dealing with refnames, as we
could silently canonicalize a syntactically bogus refname into a
valid one."
Tests in t5510 and t5516 that cover the aliasing behavior are toggled
to test_expect_success.
Signed-off-by: Jon Simons <jon@jonsimons.org>
---
refs.c | 88 +++++++++++++++++++++++--------------------
t/t5510-fetch.sh | 4 +-
t/t5516-fetch-push.sh | 2 +-
3 files changed, 50 insertions(+), 44 deletions(-)
diff --git a/refs.c b/refs.c
index 951db56113..9bf3bc8153 100644
--- a/refs.c
+++ b/refs.c
@@ -652,6 +652,44 @@ static const char *ref_rev_parse_rules[] = {
#define NUM_REV_PARSE_RULES (ARRAY_SIZE(ref_rev_parse_rules) - 1)
+/*
+ * Check that the string refname matches a rule of the form
+ * "{prefix}%.*s{suffix}". So "foo/bar/baz" would match the rule
+ * "foo/%.*s/baz", and return the string "bar".
+ */
+static const char *match_parse_rule(const char *refname, const char *rule,
+ size_t *len)
+{
+ /*
+ * Check that rule matches refname up to the first percent in the rule.
+ * We can bail immediately if not, but otherwise we leave "rule" at the
+ * %-placeholder, and "refname" at the start of the potential matched
+ * name.
+ */
+ while (*rule != '%') {
+ if (!*rule)
+ BUG("rev-parse rule did not have percent");
+ if (*refname++ != *rule++)
+ return NULL;
+ }
+
+ /*
+ * Check that our "%" is the expected placeholder. This assumes there
+ * are no other percents (placeholder or quoted) in the string, but
+ * that is sufficient for our rev-parse rules.
+ */
+ if (!skip_prefix(rule, "%.*s", &rule))
+ return NULL;
+
+ /*
+ * And now check that our suffix (if any) matches.
+ */
+ if (!strip_suffix(refname, rule, len))
+ return NULL;
+
+ return refname; /* len set by strip_suffix() */
+}
+
/*
* Is it possible that the caller meant full_name with abbrev_name?
* If so return a non-zero value to signal "yes"; the magnitude of
@@ -662,12 +700,18 @@ static const char *ref_rev_parse_rules[] = {
int refname_match(const char *abbrev_name, const char *full_name)
{
const char **p;
- const int abbrev_name_len = strlen(abbrev_name);
+ const size_t abbrev_name_len = strlen(abbrev_name);
const int num_rules = NUM_REV_PARSE_RULES;
- for (p = ref_rev_parse_rules; *p; p++)
- if (!strcmp(full_name, mkpath(*p, abbrev_name_len, abbrev_name)))
+ for (p = ref_rev_parse_rules; *p; p++) {
+ size_t short_name_len;
+ const char *short_name = match_parse_rule(full_name, *p,
+ &short_name_len);
+
+ if (short_name && short_name_len == abbrev_name_len &&
+ !memcmp(short_name, abbrev_name, abbrev_name_len))
return &ref_rev_parse_rules[num_rules] - p;
+ }
return 0;
}
@@ -1615,44 +1659,6 @@ int refs_update_ref(struct ref_store *refs, const char *msg,
return 0;
}
-/*
- * Check that the string refname matches a rule of the form
- * "{prefix}%.*s{suffix}". So "foo/bar/baz" would match the rule
- * "foo/%.*s/baz", and return the string "bar".
- */
-static const char *match_parse_rule(const char *refname, const char *rule,
- size_t *len)
-{
- /*
- * Check that rule matches refname up to the first percent in the rule.
- * We can bail immediately if not, but otherwise we leave "rule" at the
- * %-placeholder, and "refname" at the start of the potential matched
- * name.
- */
- while (*rule != '%') {
- if (!*rule)
- BUG("rev-parse rule did not have percent");
- if (*refname++ != *rule++)
- return NULL;
- }
-
- /*
- * Check that our "%" is the expected placeholder. This assumes there
- * are no other percents (placeholder or quoted) in the string, but
- * that is sufficient for our rev-parse rules.
- */
- if (!skip_prefix(rule, "%.*s", &rule))
- return NULL;
-
- /*
- * And now check that our suffix (if any) matches.
- */
- if (!strip_suffix(refname, rule, len))
- return NULL;
-
- return refname; /* len set by strip_suffix() */
-}
-
char *refs_shorten_unambiguous_ref(struct ref_store *refs,
const char *refname, int strict)
{
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 0303784b1f..941213f36a 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -1074,7 +1074,7 @@ test_expect_success 'LHS of refspec follows ref disambiguation rules' '
)
'
-test_expect_failure 'fetch with "./"-prefixed branch.<name>.merge does not mark any ref for merge' '
+test_expect_success 'fetch with "./"-prefixed branch.<name>.merge does not mark any ref for merge' '
mkdir dotslash-merge-default-refspec &&
(
cd dotslash-merge-default-refspec &&
@@ -1096,7 +1096,7 @@ test_expect_failure 'fetch with "./"-prefixed branch.<name>.merge does not mark
)
'
-test_expect_failure 'fetch protocol v0 with "./"-prefixed branch.<name>.merge does not match any remote ref' '
+test_expect_success 'fetch protocol v0 with "./"-prefixed branch.<name>.merge does not match any remote ref' '
mkdir dotslash-merge-fetch-protocol-v0 &&
(
cd dotslash-merge-fetch-protocol-v0 &&
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index aaeb251e2f..81ad6cd52f 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -433,7 +433,7 @@ test_expect_success 'push with onelevel ref' '
test_must_fail git push testrepo HEAD:refs/onelevel
'
-test_expect_failure 'push with "./"-prefixed src does not match any ref' '
+test_expect_success 'push with "./"-prefixed src does not match any ref' '
mk_test testrepo heads/main &&
test_must_fail git push testrepo ./refs/heads/main:refs/heads/frotz 2>err &&
test_grep "src refspec ./refs/heads/main does not match any" err
--
2.55.0
next prev parent reply other threads:[~2026-10-09 19:37 UTC|newest]
Thread overview: 18+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-09 19:29 [PATCH 00/15] push: speed up client-side refspec matching Jon Simons
2026-10-09 19:29 ` [PATCH 01/15] remote: validate --force-with-lease <refname> argument Jon Simons
2026-10-09 19:29 ` [PATCH 02/15] t5516: demonstrate push with "./"-prefixed source Jon Simons
2026-10-09 19:29 ` [PATCH 03/15] t5510: document fetch with "./"-prefixed branch.<name>.merge Jon Simons
2026-10-09 19:29 ` [PATCH 04/15] t/perf: add explicit delete refspec matching test Jon Simons
2026-10-09 19:29 ` Jon Simons [this message]
2026-10-09 19:29 ` [PATCH 06/15] remote: use strmap for check_push_refs() Jon Simons
2026-10-09 19:29 ` [PATCH 07/15] t5516: test pushing two refspecs creating the same new branch Jon Simons
2026-10-09 19:29 ` [PATCH 08/15] t5408, t5410: test duplicate updates without relying on the client Jon Simons
2026-10-09 19:29 ` [PATCH 09/15] t5408: check refspec order with distinct destinations Jon Simons
2026-10-09 19:29 ` [PATCH 10/15] t5408: expect client-side error for duplicate destinations Jon Simons
2026-10-09 19:29 ` [PATCH 11/15] remote: reject duplicate destinations on an empty remote Jon Simons
2026-10-09 19:29 ` [PATCH 12/15] remote: use strmap for match_explicit_refs() Jon Simons
2026-10-09 19:29 ` [PATCH 13/15] t/perf: measure --force-with-lease in p5516 Jon Simons
2026-10-09 19:29 ` [PATCH 14/15] remote: restructure apply_push_cas() loops Jon Simons
2026-10-09 19:29 ` [PATCH 15/15] remote: use strmap for apply_push_cas() Jon Simons
2026-10-09 20:18 ` [PATCH 00/15] push: speed up client-side refspec matching Kristoffer Haugsbakk
2026-10-11 2:35 ` Jon Simons
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261009192953.81794-6-jon@jonsimons.org \
--to=jon@jonsimons.org \
--cc=git@vger.kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox