git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH 06/15] remote: use strmap for check_push_refs()

From
Jon Simons <jon@jonsimons.org>
Date
Oct 9, 2026, 19:29 UTC
Message-ID
<20261009192953.81794-7-jon@jonsimons.org>
In-Reply-To
<20261009192953.81794-1-jon@jonsimons.org>

Optimize check_push_refs() by replacing a linear traversal of all local refs with strmap lookups.

Before this change, matching R explicit refspecs against N local refs in check_push_refs() entails O(R * N) calls to refname_match().

After this change, we build a strmap of local refs O(N) and use it for O(R * rules) lookups of the refspecs.

The new count_refspec_match_in_map() is equivalent to the previous count_refspec_match():

 - count_refspec_match() for 'pattern' iterates every local ref,
   adding a match for each 'refname_match(pattern, refname)',
   which searches against the six ref_rev_parse_rules.
 - count_refspec_match_in_map() for 'pattern' generates the six
   possible matches with expand_ref_prefix(), and then issues
   one strmap lookup for each one.

match_explicit() still works on the ref list, so temporarily introduce match_explicit_lhs_map() alongside match_explicit_lhs(). These two functions are recombined in a subsequent commit that converts match_explicit().

Timings show benefit for the case where the client has many local refs and specifies multiple refspecs:

  Test                           HEAD~1            HEAD
  -----------------------------------------------------------------------
  5516.3: empty:refspecs:1       0.14(0.07+0.11)   0.13(0.07+0.10) -7.1%
  5516.5: empty:refspecs:10      0.16(0.10+0.10)   0.16(0.10+0.10) +0.0%
  5516.7: empty:refspecs:100     0.47(0.41+0.10)   0.47(0.41+0.10) +0.0%
  5516.9: mirror:refspecs:1      0.16(0.09+0.11)   0.16(0.10+0.11) +0.0%
  5516.11: mirror:refspecs:10    0.26(0.19+0.11)   0.22(0.16+0.11) -15.4%
  5516.13: mirror:refspecs:100   1.19(1.13+0.10)   0.82(0.75+0.11) -31.1%
Signed-off-by: Jon Simons <jon@jonsimons.org>
---
 remote.c | 186 ++++++++++++++++++++++++++++++++++++++-----------------
 1 file changed, 129 insertions(+), 57 deletions(-)
diff --git a/remote.c b/remote.c
index 114d4d983c..91d35b37fe 100644
--- a/remote.c
+++ b/remote.c
@@ -21,6 +21,7 @@
 #include "dir.h"
 #include "setup.h"
 #include "string-list.h"
+#include "strmap.h"
 #include "strvec.h"
 #include "commit-reach.h"
 #include "advice.h"
@@ -1056,60 +1057,95 @@ void free_refs(struct ref *ref)
 	}
 }
 
+struct refspec_match {
+	struct ref *matched_weak;
+	struct ref *matched;
+	int weak_match;
+	int match;
+};
+
+static void add_refspec_match(struct refspec_match *m, const char *pattern,
+			      struct ref *ref)
+{
+	size_t patlen = strlen(pattern);
+	size_t namelen = strlen(ref->name);
+
+	/* A match is "weak" if it is with refs outside
+	 * heads or tags, and did not specify the pattern
+	 * in full (e.g. "refs/remotes/origin/master") or at
+	 * least from the toplevel (e.g. "remotes/origin/master");
+	 * otherwise "git push $URL master" would result in
+	 * ambiguity between remotes/origin/master and heads/master
+	 * at the remote site.
+	 */
+	if (namelen != patlen &&
+	    patlen != namelen - 5 &&
+	    !starts_with(ref->name, "refs/heads/") &&
+	    !starts_with(ref->name, "refs/tags/")) {
+		/* We want to catch the case where only weak
+		 * matches are found and there are multiple
+		 * matches, and where more than one strong
+		 * matches are found, as ambiguous.  One
+		 * strong match with zero or more weak matches
+		 * are acceptable as a unique match.
+		 */
+		m->matched_weak = ref;
+		m->weak_match++;
+	} else {
+		m->matched = ref;
+		m->match++;
+	}
+}
+
+static int finish_refspec_match(const struct refspec_match *m,
+				struct ref **matched_ref)
+{
+	if (!m->matched) {
+		if (matched_ref)
+			*matched_ref = m->matched_weak;
+		return m->weak_match;
+	}
+	if (matched_ref)
+		*matched_ref = m->matched;
+	return m->match;
+}
+
 int count_refspec_match(const char *pattern,
 			struct ref *refs,
 			struct ref **matched_ref)
 {
-	int patlen = strlen(pattern);
-	struct ref *matched_weak = NULL;
-	struct ref *matched = NULL;
-	int weak_match = 0;
-	int match = 0;
+	struct refspec_match m = { 0 };
 
-	for (weak_match = match = 0; refs; refs = refs->next) {
-		char *name = refs->name;
-		int namelen = strlen(name);
+	for (; refs; refs = refs->next) {
+		if (refname_match(pattern, refs->name))
+			add_refspec_match(&m, pattern, refs);
+	}
+	return finish_refspec_match(&m, matched_ref);
+}
 
-		if (!refname_match(pattern, name))
-			continue;
+static void ref_map_init(struct strmap *map, struct ref *refs)
+{
+	strmap_init_with_options(map, NULL, 0);
+	for (; refs; refs = refs->next)
+		strmap_put(map, refs->name, refs);
+}
 
-		/* A match is "weak" if it is with refs outside
-		 * heads or tags, and did not specify the pattern
-		 * in full (e.g. "refs/remotes/origin/master") or at
-		 * least from the toplevel (e.g. "remotes/origin/master");
-		 * otherwise "git push $URL master" would result in
-		 * ambiguity between remotes/origin/master and heads/master
-		 * at the remote site.
-		 */
-		if (namelen != patlen &&
-		    patlen != namelen - 5 &&
-		    !starts_with(name, "refs/heads/") &&
-		    !starts_with(name, "refs/tags/")) {
-			/* We want to catch the case where only weak
-			 * matches are found and there are multiple
-			 * matches, and where more than one strong
-			 * matches are found, as ambiguous.  One
-			 * strong match with zero or more weak matches
-			 * are acceptable as a unique match.
-			 */
-			matched_weak = refs;
-			weak_match++;
-		}
-		else {
-			matched = refs;
-			match++;
-		}
-	}
-	if (!matched) {
-		if (matched_ref)
-			*matched_ref = matched_weak;
-		return weak_match;
-	}
-	else {
-		if (matched_ref)
-			*matched_ref = matched;
-		return match;
+static int count_refspec_match_in_map(const char *pattern,
+				      struct strmap *refs,
+				      struct ref **matched_ref)
+{
+	struct refspec_match m = { 0 };
+	struct strvec names = STRVEC_INIT;
+	size_t i;
+
+	expand_ref_prefix(&names, pattern);
+	for (i = 0; i < names.nr; i++) {
+		struct ref *ref = strmap_get(refs, names.v[i]);
+		if (ref)
+			add_refspec_match(&m, pattern, ref);
 	}
+	strvec_clear(&names);
+	return finish_refspec_match(&m, matched_ref);
 }
 
 void tail_link_ref(struct ref *ref, struct ref ***tail)
@@ -1178,12 +1214,12 @@ static char *guess_ref(const char *name, struct ref *peer)
 	return strbuf_detach(&buf, NULL);
 }
 
-static int match_explicit_lhs(struct ref *src,
-			      struct refspec_item *rs,
-			      struct ref **match,
-			      int *allocated_match)
+static int match_explicit_lhs_count(const int count,
+				    struct refspec_item *rs,
+				    struct ref **match,
+				    int *allocated_match)
 {
-	switch (count_refspec_match(rs->src, src, match)) {
+	switch (count) {
 	case 1:
 		if (allocated_match)
 			*allocated_match = 0;
@@ -1203,6 +1239,24 @@ static int match_explicit_lhs(struct ref *src,
 	}
 }
 
+static int match_explicit_lhs(struct ref *src,
+			      struct refspec_item *rs,
+			      struct ref **match,
+			      int *allocated_match)
+{
+	return match_explicit_lhs_count(count_refspec_match(rs->src, src, match),
+					rs, match, allocated_match);
+}
+
+static int match_explicit_lhs_map(struct strmap *src,
+				  struct refspec_item *rs,
+				  struct ref **match,
+				  int *allocated_match)
+{
+	return match_explicit_lhs_count(count_refspec_match_in_map(rs->src, src, match),
+					rs, match, allocated_match);
+}
+
 static void show_push_unqualified_ref_name_error(const char *dst_value,
 						 const char *matched_src_name)
 {
@@ -1265,6 +1319,20 @@ static void show_push_unqualified_ref_name_error(const char *dst_value,
 	}
 }
 
+static bool refspec_item_is_explicit(const struct refspec_item *item)
+{
+	return !item->pattern && !item->matching && !item->negative;
+}
+
+static bool any_refspec_item_is_explicit(const struct refspec *rs)
+{
+	for (int i = 0; i < rs->nr; i++) {
+		if (refspec_item_is_explicit(&rs->items[i]))
+			return true;
+	}
+	return false;
+}
+
 static int match_explicit(struct ref *src, struct ref *dst,
 			  struct ref ***dst_tail,
 			  struct refspec_item *rs)
@@ -1275,7 +1343,7 @@ static int match_explicit(struct ref *src, struct ref *dst,
 	const char *dst_value = rs->dst;
 	char *dst_guess;
 
-	if (rs->pattern || rs->matching || rs->negative) {
+	if (!refspec_item_is_explicit(rs)) {
 		ret = 0;
 		goto out;
 	}
@@ -1564,17 +1632,21 @@ static void prepare_ref_index(struct string_list *ref_index, struct ref *ref)
  */
 int check_push_refs(struct ref *src, struct refspec *rs)
 {
+	struct strmap src_map;
 	int ret = 0;
-	int i;
 
-	for (i = 0; i < rs->nr; i++) {
-		struct refspec_item *item = &rs->items[i];
+	if (!any_refspec_item_is_explicit(rs))
+		return 0;
 
-		if (item->pattern || item->matching || item->negative)
+	ref_map_init(&src_map, src);
+	for (int i = 0; i < rs->nr; i++) {
+		struct refspec_item *item = &rs->items[i];
+		if (!refspec_item_is_explicit(item))
 			continue;
 
-		ret |= match_explicit_lhs(src, item, NULL, NULL);
+		ret |= match_explicit_lhs_map(&src_map, item, NULL, NULL);
 	}
+	strmap_clear(&src_map, 0);
 
 	return ret;
 }
-- 
2.55.0
Previous: Jon SimonsNext: Jon Simons
Message 7 of 18 in “push: speed up client-side refspec matching”
  1. 00/15 push: speed up client-side refspec matchingJon Simons, Oct 9, 2026
  2. 01/15 remote: validate --force-with-lease <refname> argumentJon Simons, Oct 9, 2026
  3. 02/15 t5516: demonstrate push with "./"-prefixed sourceJon Simons, Oct 9, 2026
  4. 03/15 t5510: document fetch with "./"-prefixed branch.<name>.mergeJon Simons, Oct 9, 2026
  5. 04/15 t/perf: add explicit delete refspec matching testJon Simons, Oct 9, 2026
  6. 05/15 refs: stop using mkpath() in refname_match()Jon Simons, Oct 9, 2026
  7. 06/15 remote: use strmap for check_push_refs()Jon Simons, Oct 9, 2026
  8. 07/15 t5516: test pushing two refspecs creating the same new branchJon Simons, Oct 9, 2026
  9. 08/15 t5408, t5410: test duplicate updates without relying on the clientJon Simons, Oct 9, 2026
  10. 09/15 t5408: check refspec order with distinct destinationsJon Simons, Oct 9, 2026
  11. 10/15 t5408: expect client-side error for duplicate destinationsJon Simons, Oct 9, 2026
  12. 11/15 remote: reject duplicate destinations on an empty remoteJon Simons, Oct 9, 2026
  13. 12/15 remote: use strmap for match_explicit_refs()Jon Simons, Oct 9, 2026
  14. 13/15 t/perf: measure --force-with-lease in p5516Jon Simons, Oct 9, 2026
  15. 14/15 remote: restructure apply_push_cas() loopsJon Simons, Oct 9, 2026
  16. 15/15 remote: use strmap for apply_push_cas()Jon Simons, Oct 9, 2026
  17. Kristoffer HaugsbakkOct 9, 2026
  18. Jon SimonsOct 11, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.