// Copyright (C) 2024 Umorpha Systems // SPDX-License-Identifier: AGPL-3.0-or-later package gitcache import ( "path" "strings" ) // ValidateRef validates that a ref name is of the correct syntax; // much like git-check-ref-format(1). // // There are a few differences from git-check-ref-format(1): // // - We accept the special ref name "HEAD" (even though it does not // contain a "/") // // - We require that Ref names must not just contain a "/", but must // start with "refs/". // // - If the `glob` argument is true, then a few restrictions are // relaxed: // // The bans on the characters '*' and '?' are removed. // // The ban on the character '[' is relaxed; it is allowed when it // opens a valid character-class specification in the [path.Match] // syntax, with the exception that '!' is the negation character // rather than '^'. // // If the ref name is just "*", then it does not need to have the // "refs/" prefix. func ValidateRef(ref string, glob bool) bool { if ref == "HEAD" { return true } if glob { if ref == "*" { return true } if _, err := path.Match(strings.ReplaceAll(ref, "[!", "[^"), ""); err != nil { return false } } // git-check-ref-format(1) documents 10 rules: // 2, 9. must contain a slash, must not be "@" (we are stricter). if !strings.HasPrefix(ref, "refs/") { return false } // 1, 6. parts for _, part := range strings.Split(ref, "/") { if part == "" || strings.HasPrefix(part, ".") || strings.HasSuffix(part, ".lock") { return false } } // 3. Can't contain ".." if strings.Contains(ref, "..") { return false } // 4(a), ASCII Control characters for i := 0; i < len(ref); i++ { c := ref[i] if c < 040 || c == 0177 { return false } } // 4(b), 5, 10. space, ~, ^, :, ?, *, [, \ if strings.ContainsAny(ref, " ~^:\\") { return false } if !glob && strings.ContainsAny(ref, "?*[") { return false } // 7. end with dot if strings.HasSuffix(ref, ".") { return false } // 8. "@{" if strings.Contains(ref, "@{") { return false } return true } func isLiteral(glob string) bool { return !strings.ContainsAny(glob, "?*[") } // MatchRef returns whether the `refglob` glob (which must be in // ValidateRef(refglob, true) syntax) matches the `refname` string // (which must be in ValidateRef(refname, false) syntax. If either // argument has invalid syntax, then false is returned. func MatchRef(refglob, refname string) bool { if !ValidateRef(refglob, true) || !ValidateRef(refname, false) { return false } if isLiteral(refglob) { // fast-path return refname == refglob } matched, _ := path.Match(strings.ReplaceAll(refglob, "[!", "[^"), refname) return matched } // ParseRev splits a rev, as specified by gitrevisions(7), into its // "ref" part and its "suffix" part. // // This accepts a subset of the syntax doumented in gitrevisions(7): // // - We don't accept raw hashs, things must be in terms of a ref. // - We don't accept `git describe` output. // - We don't accept abbreviated ref names, they must be the full ref // name. // - Of the special "ref/"-les ref names, we only accept "HEAD", not // "FETCH_HEAD" or any of the others. // - We don't accept the ":/" syntax for searching commit // messages. // - We don't accept the ":[:]" syntax for referring to the // index. // // As an extension to gitrevisions(7), if the `glob` argument is true, // it then in place of a ref name, it accepts a glob that would match // a ref name; see [ValidateRef] for details. // // BUG(lukeshu): ParseRev does no validation of the suffix. func ParseRev(rev string, glob bool) (ref, suffix string, ok bool) { if rev == "@" { return "HEAD", "", true } // - "@{" : reflog // - "^" : peel // - "~" : peel // - ":" : object var idx int for start := 0; ; { idx = strings.IndexAny(rev[start:], "@^~:") if idx >= 0 { idx += start if rev[idx] == '@' && !strings.HasPrefix(rev[idx:], "@{") { start = idx + 1 continue } } break } if idx < 0 { idx = len(rev) } ref = rev[:idx] suffix = rev[idx:] if ref == "" && strings.HasPrefix(suffix, "@") { ref = "HEAD" } if !ValidateRef(ref, glob) { return "", "", false } return ref, suffix, true }