diff --git a/include/git2/refspec.h b/include/git2/refspec.h index eaf7747465c..3af335d5de2 100644 --- a/include/git2/refspec.h +++ b/include/git2/refspec.h @@ -79,7 +79,16 @@ GIT_EXTERN(int) git_refspec_force(const git_refspec *refspec); GIT_EXTERN(git_direction) git_refspec_direction(const git_refspec *spec); /** - * Check if a refspec's source descriptor matches a reference + * Check if a refspec's source descriptor matches a negative reference + * + * @param refspec the refspec + * @param refname the name of the reference to check + * @return 1 if the refspec matches, 0 otherwise + */ +GIT_EXTERN(int) git_refspec_src_matches_negative(const git_refspec *refspec, const char *refname); + +/** + * Check if a refspec's source descriptor matches a reference * * @param refspec the refspec * @param refname the name of the reference to check diff --git a/src/refs.c b/src/refs.c index 51635a9e4cd..eea0d4d6fac 100644 --- a/src/refs.c +++ b/src/refs.c @@ -813,17 +813,20 @@ static int is_valid_ref_char(char ch) } } -static int ensure_segment_validity(const char *name, char may_contain_glob) +static int ensure_segment_validity(const char *name, char may_contain_glob, bool allow_caret_prefix) { const char *current = name; + const char *start = current; char prev = '\0'; const int lock_len = (int)strlen(GIT_FILELOCK_EXTENSION); int segment_len; if (*current == '.') return -1; /* Refname starts with "." */ + if (allow_caret_prefix && *current == '^') + start++; - for (current = name; ; current++) { + for (current = start; ; current++) { if (*current == '\0' || *current == '/') break; @@ -855,7 +858,7 @@ static int ensure_segment_validity(const char *name, char may_contain_glob) return segment_len; } -static bool is_all_caps_and_underscore(const char *name, size_t len) +static bool is_valid_normalized_name(const char *name, size_t len) { size_t i; char c; @@ -865,6 +868,9 @@ static bool is_all_caps_and_underscore(const char *name, size_t len) for (i = 0; i < len; i++) { c = name[i]; + if (i == 0 && c == '^') + continue; /* The first character is allowed to be "^" for negative refspecs */ + if ((c < 'A' || c > 'Z') && c != '_') return false; } @@ -885,6 +891,7 @@ int git_reference__normalize_name( int segment_len, segments_count = 0, error = GIT_EINVALIDSPEC; unsigned int process_flags; bool normalize = (buf != NULL); + bool allow_caret_prefix = true; bool validate = (flags & GIT_REFERENCE_FORMAT__VALIDATION_DISABLE) == 0; #ifdef GIT_USE_ICONV @@ -922,7 +929,7 @@ int git_reference__normalize_name( while (true) { char may_contain_glob = process_flags & GIT_REFERENCE_FORMAT_REFSPEC_PATTERN; - segment_len = ensure_segment_validity(current, may_contain_glob); + segment_len = ensure_segment_validity(current, may_contain_glob, allow_caret_prefix); if (segment_len < 0) goto cleanup; @@ -958,6 +965,12 @@ int git_reference__normalize_name( break; current += segment_len + 1; + + /* + * A caret prefix is only allowed in the first segment to signify a + * negative refspec. + */ + allow_caret_prefix = false; } /* A refname can not be empty */ @@ -977,12 +990,12 @@ int git_reference__normalize_name( if ((segments_count == 1 ) && !(flags & GIT_REFERENCE_FORMAT_REFSPEC_SHORTHAND) && - !(is_all_caps_and_underscore(name, (size_t)segment_len) || + !(is_valid_normalized_name(name, (size_t)segment_len) || ((flags & GIT_REFERENCE_FORMAT_REFSPEC_PATTERN) && !strcmp("*", name)))) goto cleanup; if ((segments_count > 1) - && (is_all_caps_and_underscore(name, strchr(name, '/') - name))) + && (is_valid_normalized_name(name, strchr(name, '/') - name))) goto cleanup; error = 0; diff --git a/src/refspec.c b/src/refspec.c index 854240a8460..4fec5dfd1cb 100644 --- a/src/refspec.c +++ b/src/refspec.c @@ -22,6 +22,7 @@ int git_refspec__parse(git_refspec *refspec, const char *input, bool is_fetch) int is_glob = 0; const char *lhs, *rhs; int flags; + bool is_neg_refspec = false; assert(refspec && input); @@ -33,6 +34,9 @@ int git_refspec__parse(git_refspec *refspec, const char *input, bool is_fetch) refspec->force = 1; lhs++; } + if (*lhs == '^') { + is_neg_refspec = true; + } rhs = strrchr(lhs, ':'); @@ -61,7 +65,14 @@ int git_refspec__parse(git_refspec *refspec, const char *input, bool is_fetch) llen = (rhs ? (size_t)(rhs - lhs - 1) : strlen(lhs)); if (1 <= llen && memchr(lhs, '*', llen)) { - if ((rhs && !is_glob) || (!rhs && is_fetch)) + /* + * If the lefthand side contains a glob, then one of the following must be + * true, otherwise the spec is invalid + * 1) the rhs exists and also contains a glob + * 2) it is a negative refspec (i.e. no rhs) + * 3) the rhs doesn't exist and we're fetching + */ + if ((rhs && !is_glob) || (rhs && is_neg_refspec) || (!rhs && is_fetch && !is_neg_refspec)) goto invalid; is_glob = 1; } else if (rhs && is_glob) @@ -208,6 +219,14 @@ int git_refspec_force(const git_refspec *refspec) return refspec->force; } +int git_refspec_src_matches_negative(const git_refspec *refspec, const char *refname) +{ + if (refspec == NULL || refspec->src == NULL || !git_refspec_is_negative(refspec)) + return false; + + return (wildmatch(refspec->src + 1, refname, 0) == 0); +} + int git_refspec_src_matches(const git_refspec *refspec, const char *refname) { if (refspec == NULL || refspec->src == NULL) @@ -216,6 +235,14 @@ int git_refspec_src_matches(const git_refspec *refspec, const char *refname) return (wildmatch(refspec->src, refname, 0) == 0); } +int git_refspec_is_negative(const git_refspec *spec) +{ + GIT_ASSERT_ARG(spec); + GIT_ASSERT_ARG(spec->src); + + return (spec->src[0] == '^' && spec->dst == NULL); +} + int git_refspec_dst_matches(const git_refspec *refspec, const char *refname) { if (refspec == NULL || refspec->dst == NULL) diff --git a/src/refspec.h b/src/refspec.h index 2b4111f0419..b71f9c10915 100644 --- a/src/refspec.h +++ b/src/refspec.h @@ -42,6 +42,14 @@ int git_refspec__serialize(git_buf *out, const git_refspec *refspec); */ int git_refspec_is_wildcard(const git_refspec *spec); +/** + * Determines if a refspec is a negative refspec. + * + * @param spec the refspec + * @return 1 if the refspec is a negative, 0 otherwise + */ +int git_refspec_is_negative(const git_refspec *spec); + /** * DWIM `spec` with `refs` existing on the remote, append the dwim'ed * result in `out`. diff --git a/src/remote.c b/src/remote.c index d57ee3f44a2..850f3093065 100644 --- a/src/remote.c +++ b/src/remote.c @@ -2112,17 +2112,21 @@ int git_remote_is_valid_name( git_refspec *git_remote__matching_refspec(git_remote *remote, const char *refname) { git_refspec *spec; + git_refspec *match = NULL; size_t i; git_vector_foreach(&remote->active_refspecs, i, spec) { if (spec->push) continue; + if (git_refspec_is_negative(spec) && git_refspec_src_matches_negative(spec, refname)) + return NULL; + if (git_refspec_src_matches(spec, refname)) - return spec; + match = spec; } - return NULL; + return match; } git_refspec *git_remote__matching_dst_refspec(git_remote *remote, const char *refname)