214 lines
5.1 KiB
Go
214 lines
5.1 KiB
Go
package codeowners
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/spf13/afero"
|
|
)
|
|
|
|
// Codeowners - patterns/owners mappings for the given repo
|
|
type Codeowners struct {
|
|
repoRoot string
|
|
Patterns []Codeowner
|
|
}
|
|
|
|
// Codeowner - owners for a given pattern
|
|
type Codeowner struct {
|
|
Pattern string
|
|
re *regexp.Regexp
|
|
Owners []string
|
|
}
|
|
|
|
func (c Codeowner) String() string {
|
|
return fmt.Sprintf("%s\t%v", c.Pattern, strings.Join(c.Owners, ", "))
|
|
}
|
|
|
|
var fs = afero.NewOsFs()
|
|
|
|
// findCodeownersFile - find a CODEOWNERS file somewhere within or below
|
|
// the working directory (wd), and open it.
|
|
func findCodeownersFile(wd string) (io.Reader, string, error) {
|
|
dir := wd
|
|
for {
|
|
for _, p := range []string{".", "docs", ".github", ".gitlab"} {
|
|
pth := path.Join(dir, p)
|
|
exists, err := afero.DirExists(fs, pth)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if exists {
|
|
f := path.Join(pth, "CODEOWNERS")
|
|
_, err := fs.Stat(f)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
return nil, "", err
|
|
}
|
|
r, err := fs.Open(f)
|
|
return r, dir, err
|
|
}
|
|
}
|
|
odir := dir
|
|
dir = path.Dir(odir)
|
|
// if we can't go up any further...
|
|
if odir == dir {
|
|
break
|
|
}
|
|
// if we're heading above the volume name (relevant on Windows)...
|
|
if len(dir) < len(filepath.VolumeName(odir)) {
|
|
break
|
|
}
|
|
}
|
|
return nil, "", nil
|
|
}
|
|
|
|
// Deprecated: Use FromFile(path) instead.
|
|
func NewCodeowners(path string) (*Codeowners, error) {
|
|
return FromFile(path)
|
|
}
|
|
|
|
// FromFile creates a Codeowners from the path to a local file.
|
|
func FromFile(path string) (*Codeowners, error) {
|
|
r, root, err := findCodeownersFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if r == nil {
|
|
return nil, fmt.Errorf("No CODEOWNERS found in %s", path)
|
|
}
|
|
return FromReader(r, root)
|
|
}
|
|
|
|
// FromReader creates a Codeowners from a given Reader instance and root path.
|
|
func FromReader(r io.Reader, repoRoot string) (*Codeowners, error) {
|
|
co := &Codeowners{
|
|
repoRoot: repoRoot,
|
|
}
|
|
co.Patterns = parseCodeowners(r)
|
|
return co, nil
|
|
}
|
|
|
|
// parseCodeowners parses a list of Codeowners from a Reader
|
|
func parseCodeowners(r io.Reader) []Codeowner {
|
|
co := []Codeowner{}
|
|
s := bufio.NewScanner(r)
|
|
for s.Scan() {
|
|
fields := strings.Fields(s.Text())
|
|
if len(fields) > 0 && strings.HasPrefix(fields[0], "#") {
|
|
continue
|
|
}
|
|
if len(fields) > 1 {
|
|
fields = combineEscapedSpaces(fields)
|
|
c, _ := NewCodeowner(fields[0], fields[1:])
|
|
co = append(co, c)
|
|
}
|
|
}
|
|
return co
|
|
}
|
|
|
|
// if any of the elements ends with a \, it was an escaped space
|
|
// put it back together properly so it's not treated as separate fields
|
|
func combineEscapedSpaces(fields []string) []string {
|
|
outFields := make([]string, 0)
|
|
escape := `\`
|
|
for i := 0; i < len(fields); i++ {
|
|
outField := fields[i]
|
|
for strings.HasSuffix(fields[i], escape) && i+1 < len(fields) {
|
|
outField = strings.Join([]string{strings.TrimRight(outField, escape), fields[i+1]}, " ")
|
|
i++
|
|
}
|
|
outFields = append(outFields, outField)
|
|
}
|
|
|
|
return outFields
|
|
}
|
|
|
|
// NewCodeowner -
|
|
func NewCodeowner(pattern string, owners []string) (Codeowner, error) {
|
|
re := getPattern(pattern)
|
|
c := Codeowner{
|
|
Pattern: pattern,
|
|
re: re,
|
|
Owners: owners,
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// Owners - return the list of code owners for the given path
|
|
// (within the repo root)
|
|
func (c *Codeowners) Owners(path string) []string {
|
|
if strings.HasPrefix(path, c.repoRoot) {
|
|
path = strings.Replace(path, c.repoRoot, "", 1)
|
|
}
|
|
|
|
// Order is important; the last matching pattern takes the most precedence.
|
|
for i := len(c.Patterns) - 1; i >= 0; i-- {
|
|
p := c.Patterns[i]
|
|
|
|
if p.re.MatchString(path) {
|
|
return p.Owners
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// based on github.com/sabhiram/go-gitignore
|
|
// but modified so that 'dir/*' only matches files in 'dir/'
|
|
func getPattern(line string) *regexp.Regexp {
|
|
// when # or ! is escaped with a \
|
|
if regexp.MustCompile(`^(\\#|\\!)`).MatchString(line) {
|
|
line = line[1:]
|
|
}
|
|
|
|
// If we encounter a foo/*.blah in a folder, prepend the / char
|
|
if regexp.MustCompile(`([^\/+])/.*\*\.`).MatchString(line) && line[0] != '/' {
|
|
line = "/" + line
|
|
}
|
|
|
|
// Handle escaping the "." char
|
|
line = regexp.MustCompile(`\.`).ReplaceAllString(line, `\.`)
|
|
|
|
magicStar := "#$~"
|
|
|
|
// Handle "/**/" usage
|
|
if strings.HasPrefix(line, "/**/") {
|
|
line = line[1:]
|
|
}
|
|
line = regexp.MustCompile(`/\*\*/`).ReplaceAllString(line, `(/|/.+/)`)
|
|
line = regexp.MustCompile(`\*\*/`).ReplaceAllString(line, `(|.`+magicStar+`/)`)
|
|
line = regexp.MustCompile(`/\*\*`).ReplaceAllString(line, `(|/.`+magicStar+`)`)
|
|
|
|
// Handle escaping the "*" char
|
|
line = regexp.MustCompile(`\\\*`).ReplaceAllString(line, `\`+magicStar)
|
|
line = regexp.MustCompile(`\*`).ReplaceAllString(line, `([^/]*)`)
|
|
|
|
// Handle escaping the "?" char
|
|
line = strings.Replace(line, "?", `\?`, -1)
|
|
|
|
line = strings.Replace(line, magicStar, "*", -1)
|
|
|
|
// Temporary regex
|
|
var expr = ""
|
|
if strings.HasSuffix(line, "/") {
|
|
expr = line + "(|.*)$"
|
|
} else {
|
|
expr = line + "$"
|
|
}
|
|
if strings.HasPrefix(expr, "/") {
|
|
expr = "^(|/)" + expr[1:]
|
|
} else {
|
|
expr = "^(|.*/)" + expr
|
|
}
|
|
pattern, _ := regexp.Compile(expr)
|
|
|
|
return pattern
|
|
}
|