internal/policy/codeowners.go
79 lines · 2296 bytes
4 symbols in this file
1package policy
2
3import (
4 "path"
5 "strings"
6)
7
8// CodeownersRule is one line of a CODEOWNERS file: a pattern and the users
9// who own paths matching it.
10type CodeownersRule struct {
11 Pattern string
12 Owners []string // usernames, @ stripped
13}
14
15// ParseCodeowners reads CODEOWNERS content: one rule per line, gitignore-
16// style pattern followed by @user owners; #-comments and blanks ignored.
17func ParseCodeowners(content string) []CodeownersRule {
18 var rules []CodeownersRule
19 for _, line := range strings.Split(content, "\n") {
20 line = strings.TrimSpace(line)
21 if line == "" || strings.HasPrefix(line, "#") {
22 continue
23 }
24 fields := strings.Fields(line)
25 if len(fields) < 2 {
26 continue
27 }
28 var owners []string
29 for _, f := range fields[1:] {
30 owners = append(owners, strings.TrimPrefix(f, "@"))
31 }
32 rules = append(rules, CodeownersRule{Pattern: fields[0], Owners: owners})
33 }
34 return rules
35}
36
37// OwnersFor returns the owners of a path: the last matching rule wins,
38// CODEOWNERS convention. nil means unowned.
39func OwnersFor(rules []CodeownersRule, filePath string) []string {
40 var owners []string
41 for _, r := range rules {
42 if codeownersMatch(r.Pattern, filePath) {
43 owners = r.Owners
44 }
45 }
46 return owners
47}
48
49// codeownersMatch implements the pattern subset gitbay supports:
50// - "*" everything
51// - "*.go" extension match on the basename, anywhere
52// - "docs/" directory prefix (anywhere unless anchored with /)
53// - "/cmd/x.go" exact or glob path anchored at the root
54// - "internal/*" glob against the full path (one segment per *)
55func codeownersMatch(pattern, filePath string) bool {
56 anchored := strings.HasPrefix(pattern, "/")
57 pattern = strings.TrimPrefix(pattern, "/")
58 if pattern == "*" {
59 return true
60 }
61 // Directory rule: everything under it.
62 if strings.HasSuffix(pattern, "/") {
63 dir := strings.TrimSuffix(pattern, "/")
64 if strings.HasPrefix(filePath, dir+"/") {
65 return true
66 }
67 if !anchored && strings.Contains(filePath, "/"+dir+"/") {
68 return true
69 }
70 return false
71 }
72 // Bare pattern without a slash: match the basename anywhere.
73 if !strings.Contains(pattern, "/") && !anchored {
74 ok, _ := path.Match(pattern, path.Base(filePath))
75 return ok
76 }
77 ok, _ := path.Match(pattern, filePath)
78 return ok
79}