refs: honor REFSPEC_SHORTHAND for multi-segment refs

GIT_REFERENCE_FORMAT_REFSPEC_SHORTHAND is documented to "interpret the
name as part of a refspec in shorthand form so the ONELEVEL naming rules
aren't enforced and 'master' becomes a valid name."

However, the multi-segment pseudoref check was not respecting this flag,
rejecting valid refspecs like "A/b" and "HEAD/feature" even when
SHORTHAND was set.

The single-segment check at line 1015 already honors this flag. This
change makes the multi-segment check at line 1021 consistent with that
behavior and with the documented intent.

Git itself accepts these refspec patterns without issue.
This commit is contained in:
Robert Hensing
2025-10-15 17:53:22 +02:00
parent 58d9363f02
commit 76314a893a
2 changed files with 54 additions and 0 deletions

View File

@@ -1018,6 +1018,7 @@ int git_reference__normalize_name(
goto cleanup;
if ((segments_count > 1)
&& !(flags & GIT_REFERENCE_FORMAT_REFSPEC_SHORTHAND)
&& (is_valid_normalized_name(name, strchr(name, '/') - name)))
goto cleanup;

View File

@@ -407,3 +407,56 @@ void test_refs_normalize__negative_refspec_pattern(void)
ensure_refname_invalid(
GIT_REFERENCE_FORMAT_REFSPEC_PATTERN, "foo/^bar");
}
void test_refs_normalize__refspec_shorthand_allows_uppercase_segments(void)
{
/* Without REFSPEC_SHORTHAND, multi-level refs starting with ALL_CAPS (pseudorefs) should be rejected */
ensure_refname_invalid(
GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL, "A/b");
ensure_refname_invalid(
GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL, "FOO/BAR/BAZ");
ensure_refname_invalid(
GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL, "HEAD_TRACKER/foo");
ensure_refname_invalid(
GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL, "HEAD/feature");
ensure_refname_invalid(
GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL, "FETCH_HEAD/branch");
ensure_refname_invalid(
GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL, "MERGE_HEAD/test");
/* With REFSPEC_SHORTHAND, they should be allowed (used in refspecs like "A/b:refs/heads/A/b") */
ensure_refname_normalized(
GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL | GIT_REFERENCE_FORMAT_REFSPEC_SHORTHAND,
"A/b", "A/b");
ensure_refname_normalized(
GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL | GIT_REFERENCE_FORMAT_REFSPEC_SHORTHAND,
"FOO/BAR/BAZ", "FOO/BAR/BAZ");
ensure_refname_normalized(
GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL | GIT_REFERENCE_FORMAT_REFSPEC_SHORTHAND,
"HEAD_TRACKER/foo", "HEAD_TRACKER/foo");
ensure_refname_normalized(
GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL | GIT_REFERENCE_FORMAT_REFSPEC_SHORTHAND,
"HEAD/feature", "HEAD/feature");
ensure_refname_normalized(
GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL | GIT_REFERENCE_FORMAT_REFSPEC_SHORTHAND,
"FETCH_HEAD/branch", "FETCH_HEAD/branch");
ensure_refname_normalized(
GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL | GIT_REFERENCE_FORMAT_REFSPEC_SHORTHAND,
"MERGE_HEAD/test", "MERGE_HEAD/test");
/* Mixed case first segments don't look like pseudorefs, so they're allowed even without SHORTHAND */
ensure_refname_normalized(
GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL, "AaA/b", "AaA/b");
ensure_refname_normalized(
GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL, "Head/feature", "Head/feature");
ensure_refname_normalized(
GIT_REFERENCE_FORMAT_ALLOW_ONELEVEL, "merge_head/test", "merge_head/test");
/* Fully qualified refs starting with uppercase should always be allowed */
ensure_refname_normalized(
GIT_REFERENCE_FORMAT_NORMAL, "refs/heads/A/b", "refs/heads/A/b");
ensure_refname_normalized(
GIT_REFERENCE_FORMAT_NORMAL, "refs/heads/FOO/BAR", "refs/heads/FOO/BAR");
ensure_refname_normalized(
GIT_REFERENCE_FORMAT_NORMAL, "refs/heads/HEAD/feature", "refs/heads/HEAD/feature");
}