Git development
 help / color / mirror / Atom feed
From: "SZEDER Gábor" <szeder.dev@gmail.com>
To: Jon Simons <jon@jonsimons.org>
Cc: git@vger.kernel.org
Subject: Re: [PATCH v2 03/12] refs: stop using mkpath() in refname_match()
Date: Wed, 28 Oct 2026 22:19:51 +0100	[thread overview]
Message-ID: <auJm92xzDos9MF+8@szeder.dev> (raw)
In-Reply-To: <20261014031347.50433-4-jon@jonsimons.org>

On Tue, Oct 13, 2026 at 11:13:38PM -0400, Jon Simons wrote:
> 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.

I was wondering whether we should worry about breaking something out
there that relies on "./refs/heads/foo" working as a fully qualified
ref, according to Hyrum's law...

>  - 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."

But apprently we didn't worry about it back then with 6cd4a8982d, so I
don't think we should worry about it now, either.

> Tests are introduced for the observable behavior change:
> 
>  - t5516 now asserts that pushing a "./"-prefixed source fails
> 
>  - t5510 now asserts that fetch does not match a "./"-prefixed
>    branch.<name>.merge value, either via the default fetch refspec
>    or against the remote's advertised refs.
> 
> Signed-off-by: Jon Simons <jon@jonsimons.org>
> ---
>  refs.c                | 88 +++++++++++++++++++++++--------------------
>  t/t5510-fetch.sh      | 44 ++++++++++++++++++++++
>  t/t5516-fetch-push.sh |  6 +++
>  3 files changed, 97 insertions(+), 41 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() */
> +}
> +

"--color-moved" shows that this function was moved here without any
modification.  Good.

>  /*
>   * 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;
> +	}

Looks good.

>  	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 300bd5396d..941213f36a 100755
> --- a/t/t5510-fetch.sh
> +++ b/t/t5510-fetch.sh
> @@ -1074,6 +1074,50 @@ test_expect_success 'LHS of refspec follows ref disambiguation rules' '
>  	)
>  '
>  
> +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 &&
> +		git init -b main server &&
> +		test_commit -C server one &&
> +		git -C server branch other &&
> +		test_commit -C server two &&
> +
> +		# The bogus ./refs/heads/main should not match the remote refs/heads/main.
> +		git clone server client &&
> +		git -C client config branch.main.merge ./refs/heads/main &&
> +		git -C client fetch &&
> +		{
> +			echo "$(git -C server rev-parse main)	not-for-merge" &&
> +			echo "$(git -C server rev-parse other)	not-for-merge"
> +		} >expect &&
> +		cut -f -2 client/.git/FETCH_HEAD >actual &&
> +		test_cmp expect actual
> +	)
> +'
> +
> +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 &&
> +		git init -b main server &&
> +		test_commit -C server one &&
> +		git -C server branch other &&
> +		test_commit -C server two &&
> +
> +		# Omit refs/heads/main from the fetch refspec so that the merge
> +		# source is instead looked up among the refs the remote advertised.
> +		git clone server client-v0 &&
> +		git -C client-v0 config remote.origin.fetch \
> +			+refs/heads/other:refs/remotes/origin/other &&
> +		git -C client-v0 config branch.main.merge ./refs/heads/main &&
> +		git -C client-v0 -c protocol.version=0 fetch &&
> +		echo "$(git -C server rev-parse other)	not-for-merge" >expect &&
> +		cut -f -2 client-v0/.git/FETCH_HEAD >actual &&
> +		test_cmp expect actual
> +	)
> +'
> +

Testing the "./"-prefixed "branch.<name>.merge" config is a nice
touch.

>  test_expect_success 'fetch.writeCommitGraph' '
>  	git clone three write &&
>  	(
> diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
> index b982b209bf..81ad6cd52f 100755
> --- a/t/t5516-fetch-push.sh
> +++ b/t/t5516-fetch-push.sh
> @@ -433,6 +433,12 @@ test_expect_success 'push with onelevel ref' '
>  	test_must_fail git push testrepo HEAD:refs/onelevel
>  '
>  
> +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
> +'
> +
>  test_expect_success 'push with colon-less refspec (1)' '
>  	mk_test testrepo heads/frotz tags/frotz &&
>  	git branch -f frotz main &&
> -- 
> 2.55.0
> 

  reply	other threads:[~2026-10-28 21:19 UTC|newest]

Thread overview: 38+ 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 ` [PATCH 05/15] refs: stop using mkpath() in refname_match() Jon Simons
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
2026-10-14  3:13 ` [PATCH v2 00/12] " Jon Simons
2026-10-14  3:13   ` [PATCH v2 01/12] remote: validate --force-with-lease <refname> argument Jon Simons
2026-11-05 23:03     ` Junio C Hamano
2026-10-14  3:13   ` [PATCH v2 02/12] t/perf: add explicit delete refspec matching test Jon Simons
2026-10-14  3:13   ` [PATCH v2 03/12] refs: stop using mkpath() in refname_match() Jon Simons
2026-10-28 21:19     ` SZEDER Gábor [this message]
2026-11-05 22:26     ` Junio C Hamano
2026-10-14  3:13   ` [PATCH v2 04/12] remote: use strmap for check_push_refs() Jon Simons
2026-10-14  3:13   ` [PATCH v2 05/12] send-pack: plug leak upon match_push_refs failure Jon Simons
2026-11-05 22:28     ` Junio C Hamano
2026-10-14  3:13   ` [PATCH v2 06/12] t5408, t5410: test duplicate updates without relying on the client Jon Simons
2026-10-14  3:13   ` [PATCH v2 07/12] t5408: check refspec order with distinct destinations Jon Simons
2026-10-14  3:13   ` [PATCH v2 08/12] remote: reject duplicate destinations on an empty remote Jon Simons
2026-11-05 22:32     ` Junio C Hamano
2026-10-14  3:13   ` [PATCH v2 09/12] remote: use strmap for match_explicit_refs() Jon Simons
2026-11-05 22:41     ` Junio C Hamano
2026-10-14  3:13   ` [PATCH v2 10/12] t/perf: measure --force-with-lease in p5516 Jon Simons
2026-10-14  3:13   ` [PATCH v2 11/12] remote: restructure apply_push_cas() loops Jon Simons
2026-10-14  3:13   ` [PATCH v2 12/12] remote: use strmap for apply_push_cas() Jon Simons
2026-11-05 22:49     ` Junio C Hamano

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=auJm92xzDos9MF+8@szeder.dev \
    --to=szeder.dev@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=jon@jonsimons.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