package main import ( "errors" "regexp" "strings" ) // ruleCovers is whether a protection rule's name covers a branch, as the forge itself decides it (Gitea 1.27, // models/git/protected_branch.go Match and modules/glob, novox/hq issue 539): a name without a glob character // covers the branch of that name in any case; any other name is a glob with `/` as its separator — `*` any // run within a segment, `**` any run across them, `?` one character of a segment, `[abc]`, `[a-z]` and `[!abc]` // one character, `{a,b}` either, `\` the next character itself — matched case-sensitively; a glob the forge // cannot compile covers its own text exactly. The TypeScript bundle's ruleCovers (guard.ts) is the same rule. func ruleCovers(rule, branch string) bool { if !strings.ContainsAny(rule, `*?\[]{}`) { return strings.EqualFold(rule, branch) } re, err := globRegexp(rule) if err != nil { return rule == branch } return re.MatchString(branch) } // globRegexp is the forge's glob as a regular expression, compiled as modules/glob compiles it with `/` as // the separator. func globRegexp(glob string) (*regexp.Regexp, error) { g := []rune(glob) var b strings.Builder pos := 0 var compile func(sub bool) error chars := func() error { b.WriteByte('[') if pos < len(g) && g[pos] == '!' { pos++ b.WriteByte('^') } for pos < len(g) { c := g[pos] pos++ switch { case c == ']': b.WriteByte(']') return nil case c == '\\': if pos >= len(g) { return errors.New("unterminated character class escape") } b.WriteByte('\\') b.WriteRune(g[pos]) pos++ default: b.WriteRune(c) } } return errors.New("unterminated character class") } compile = func(sub bool) error { for pos < len(g) { c := g[pos] pos++ if sub && c == '}' { b.WriteByte(')') return nil } switch c { case '*': if pos < len(g) && g[pos] == '*' { pos++ b.WriteString(".*") } else { b.WriteString("[^/]*") } case '?': b.WriteString("[^/]") case '+': b.WriteString(`\+`) case '[': if err := chars(); err != nil { return err } case '{': b.WriteByte('(') if err := compile(true); err != nil { return err } case ',': if sub { b.WriteByte('|') } else { b.WriteByte(',') } case '\\': if pos >= len(g) { return errors.New("no character to escape") } b.WriteByte('\\') b.WriteRune(g[pos]) pos++ case '.', '^', '$', '(', ')', '|': b.WriteByte('\\') b.WriteRune(c) default: b.WriteRune(c) } } return nil } b.WriteByte('^') if err := compile(false); err != nil { return nil, err } b.WriteByte('$') return regexp.Compile(b.String()) }