internal/policy/codeowners.go

babb6e8da233aba1c5246ad4ec56802c2f7553bd
gitbay/internal/policy/codeowners.go history · blame · raw

79 lines · 2296 bytes

 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}