diff --git a/src/libgit2/refs.c b/src/libgit2/refs.c index fd0c292ff..ac9063d59 100644 --- a/src/libgit2/refs.c +++ b/src/libgit2/refs.c @@ -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; diff --git a/tests/libgit2/refs/normalize.c b/tests/libgit2/refs/normalize.c index c2f5684ef..66f6efbe6 100644 --- a/tests/libgit2/refs/normalize.c +++ b/tests/libgit2/refs/normalize.c @@ -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"); +}