pax_global_header00006660000000000000000000000064146372630710014523gustar00rootroot0000000000000052 comment=ece93bb36f11efd76dcde307128d6c172c994511 go-codeowners-0.5.0/000077500000000000000000000000001463726307100143005ustar00rootroot00000000000000go-codeowners-0.5.0/.github/000077500000000000000000000000001463726307100156405ustar00rootroot00000000000000go-codeowners-0.5.0/.github/CODEOWNERS000066400000000000000000000000241463726307100172270ustar00rootroot00000000000000# * @hairyhenderson go-codeowners-0.5.0/.github/dependabot.yml000066400000000000000000000005671463726307100205000ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: gomod directory: "/" schedule: interval: weekly day: saturday time: "02:00" timezone: Canada/Eastern open-pull-requests-limit: 10 - package-ecosystem: github-actions directory: "/" schedule: interval: weekly day: saturday time: "02:00" timezone: Canada/Eastern open-pull-requests-limit: 10 go-codeowners-0.5.0/.github/workflows/000077500000000000000000000000001463726307100176755ustar00rootroot00000000000000go-codeowners-0.5.0/.github/workflows/build.yml000066400000000000000000000015661463726307100215270ustar00rootroot00000000000000name: Build on: push: branches: - main pull_request: permissions: contents: read jobs: test: runs-on: ubuntu-latest container: image: ghcr.io/hairyhenderson/gomplate-ci-build:latest steps: - uses: actions/checkout@v4 - run: go test -coverprofile=c.out -v -race ./... windows-test: runs-on: windows-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version-file: 'go.mod' - run: go test -v ./... lint: name: lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version-file: 'go.mod' - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: version: latest args: --verbose --max-same-issues=0 --max-issues-per-linter=0 go-codeowners-0.5.0/.github/workflows/stale.yml000066400000000000000000000033021463726307100215260ustar00rootroot00000000000000name: 'Stale issue handler' on: workflow_dispatch: issue_comment: schedule: - cron: '42 04 * * *' permissions: issues: write pull-requests: write jobs: stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v9 # See https://github.com/actions/stale#all-options with: exempt-all-milestones: true days-before-stale: 60 days-before-close: 14 stale-issue-message: | This issue is stale because it has been open for 60 days with no activity. If it is no longer relevant or necessary, please close it. Given no action, it will be closed in 14 days. If it's still relevant, one of the following will remove the stale marking: - A maintainer can add this issue to a milestone to indicate that it's been accepted and will be worked on - A maintainer can remove the `stale` label - Anyone can post an update or other comment stale-pr-message: | This pull request is stale because it has been open for 60 days with no activity. If it is no longer relevant or necessary, please close it. Given no action, it will be closed in 14 days. If it's still relevant, one of the following will remove the stale marking: - A maintainer can add this pull request to a milestone to indicate that it's been accepted and will be worked on - A maintainer can remove the `stale` label - Anyone can post an update or other comment - Anyone with write access can push a commit to the pull request branch go-codeowners-0.5.0/.golangci.yml000066400000000000000000000023771463726307100166750ustar00rootroot00000000000000linters-settings: govet: check-shadowing: true enable: - fieldalignment golint: min-confidence: 0 gocyclo: min-complexity: 10 dupl: threshold: 100 goconst: min-len: 2 min-occurrences: 4 lll: line-length: 140 nolintlint: allow-unused: false # report any unused nolint directives require-explanation: false # don't require an explanation for nolint directives require-specific: false # don't require nolint directives to be specific about which linter is being skipped linters: disable-all: true enable: - asciicheck - bodyclose # - dogsled - dupl - errcheck - exhaustive - exportloopref - funlen - gci - gochecknoglobals - gochecknoinits - gocognit - goconst - gocritic - gocyclo - godox - gofmt # - gofumpt - goheader - goimports # - gomnd - gomodguard - goprintffuncname - gosec - gosimple - govet - ineffassign - lll - misspell - nakedret - nestif # - nlreturn - noctx - nolintlint - prealloc - revive - rowserrcheck - sqlclosecheck - staticcheck - stylecheck - typecheck - unconvert - unparam - unused - whitespace # - wsl go-codeowners-0.5.0/LICENSE000066400000000000000000000021061463726307100153040ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2018-2024 Dave Henderson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. go-codeowners-0.5.0/README.md000066400000000000000000000025121463726307100155570ustar00rootroot00000000000000[![Go Reference][doc-image]][docs] [![Build][build-image]][build-url] # go-codeowners A package that finds and parses [`CODEOWNERS`](https://help.github.com/articles/about-codeowners/) files. Features: - operates on local repos - doesn't require a cloned repo (i.e. doesn't need a `.git` directory to be present at the repo's root) - can be called from within a repo (doesn't have to be at the root) - will find `CODEOWNERS` files in all documented locations: the repo's root, `docs/`, and `.github/` (or `.gitlab/` for GitLab repos) ## Usage ```console go get -u github.com/hairyhenderson/go-codeowners ``` To find the owner of the README.md file: ```go import "github.com/hairyhenderson/go-codeowners" func main() { c, _ := FromFile(cwd()) owners := c.Owners("README.md") for i, o := range owners { fmt.Printf("Owner #%d is %s\n", i, o) } } ``` See the [docs][] for more information. ## License [The MIT License](http://opensource.org/licenses/MIT) Copyright (c) 2018-2023 Dave Henderson [docs]: https://pkg.go.dev/github.com/hairyhenderson/go-codeowners [doc-image]: https://pkg.go.dev/badge/github.com/hairyhenderson/go-codeowners.svg [build-image]: https://github.com/hairyhenderson/go-codeowners/actions/workflows/build.yml/badge.svg [build-url]: https://github.com/hairyhenderson/go-codeowners/actions/workflows/build.yml go-codeowners-0.5.0/codeowners.go000066400000000000000000000150071463726307100170020ustar00rootroot00000000000000package codeowners import ( "bufio" "errors" "fmt" "io" "io/fs" "os" "path" "path/filepath" "regexp" "strings" ) // 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, ", ")) } func dirExists(fsys fs.FS, path string) (bool, error) { fi, err := fs.Stat(fsys, path) if err == nil && fi.IsDir() { return true, nil } if errors.Is(err, fs.ErrNotExist) { return false, nil } return false, err } // findCodeownersFile - find a CODEOWNERS file somewhere within or below // the working directory (wd), and open it. func findCodeownersFile(fsys fs.FS, wd string) (io.Reader, string, error) { dir := wd for { for _, p := range []string{".", "docs", ".github", ".gitlab"} { pth := path.Join(dir, p) exists, err := dirExists(fsys, pth) if err != nil { return nil, "", err } if exists { f := path.Join(pth, "CODEOWNERS") _, err := fs.Stat(fsys, f) if err != nil { if errors.Is(err, fs.ErrNotExist) { continue } return nil, "", err } r, err := fsys.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] instead. func NewCodeowners(path string) (*Codeowners, error) { return FromFile(path) } // FromFile creates a Codeowners from the path to a local file. Consider using // [FromFileWithFS] instead. func FromFile(path string) (*Codeowners, error) { base := "/" if filepath.IsAbs(path) && filepath.VolumeName(path) != "" { base = path[:len(filepath.VolumeName(path))+1] } path = path[len(base):] return FromFileWithFS(os.DirFS(base), path) } // FromFileWithFS creates a Codeowners from the path to a file relative to the // given filesystem. func FromFileWithFS(fsys fs.FS, path string) (*Codeowners, error) { r, root, err := findCodeownersFile(fsys, 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, } patterns, err := parseCodeowners(r) if err != nil { return nil, err } co.Patterns = patterns return co, nil } func isSection(line string) bool { return strings.HasPrefix(line, "^") || strings.HasPrefix(line, "[") } // parseCodeowners parses a list of Codeowners from a Reader func parseCodeowners(r io.Reader) ([]Codeowner, error) { co := []Codeowner{} s := bufio.NewScanner(r) var defaultOwners []string for s.Scan() { line := s.Text() if isSection(line) { defaultOwners = parseDefaultOwners(line) continue } fields := strings.Fields(line) if len(fields) > 0 && strings.HasPrefix(fields[0], "#") { continue } if len(fields) > 1 { fields = combineEscapedSpaces(fields) c, err := NewCodeowner(fields[0], fields[1:]) if err != nil { return nil, err } co = append(co, c) } else if len(fields) == 1 && defaultOwners != nil { c, err := NewCodeowner(fields[0], defaultOwners) if err != nil { return nil, err } co = append(co, c) } } return co, nil } func parseDefaultOwners(line string) []string { index := strings.LastIndex(line, "]") if index != -1 && len(line) > index+1 { return strings.Fields(strings.TrimSpace(line[index+1:])) } return nil } // 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, err := getPattern(pattern) if err != nil { return Codeowner{}, err } 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, error) { // 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.ReplaceAll(line, "?", `\?`) line = strings.ReplaceAll(line, magicStar, "*") // Temporary regex expr := "" switch { case strings.HasSuffix(line, "/"): expr = line + "(|.*)$" case strings.HasSuffix(line, "/([^/]*)"): expr = line + "$" default: expr = line + "($|/.*$)" } if strings.HasPrefix(expr, "/") { expr = "^(|/)" + expr[1:] } else { expr = "^(|.*/)" + expr } return regexp.Compile(expr) } go-codeowners-0.5.0/codeowners_test.go000066400000000000000000000230101463726307100200320ustar00rootroot00000000000000package codeowners import ( "bytes" "fmt" "io" "os" "path" "runtime" "strings" "testing" "testing/fstest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) //nolint:gochecknoglobals var ( sample = `# comment * @everyone foobar/ someone@else.com docs/** @org/docteam @joe` sample2 = `* @hairyhenderson` sample3 = `baz/* @baz @qux` sample4 = `[test] * @everyone [test2][2] */foo @everyoneelse` // based on https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax // with a few unimportant modifications fullSample = `# This is a comment. # Each line is a file pattern followed by one or more owners. # These owners will be the default owners for everything in # the repo. Unless a later match takes precedence, # @global-owner1 and @global-owner2 will be requested for # review when someone opens a pull request. * @global-owner1 @global-owner2 # Order is important; the last matching pattern takes the most # precedence. When someone opens a pull request that only # modifies JS files, only @js-owner and not the global # owner(s) will be requested for a review. *.js @js-owner # You can also use email addresses if you prefer. They'll be # used to look up users just like we do for commit author # emails. *.go docs@example.com # In this example, @doctocat owns any files in the build/logs # directory at the root of the repository and any of its # subdirectories. /build/logs/ @doctocat # In this example, @fooowner owns any files in the /cells/foo # directory at the root of the repository and any of its # subdirectories and files. /cells/foo @fooowner # The 'docs/*' pattern will match files like # 'docs/getting-started.md' but not further nested files like # 'docs/build-app/troubleshooting.md'. docs/* docs@example.com # In this example, @octocat owns any file in an apps directory # anywhere in your repository. apps/ @octocat # In this example, @doctocat owns any file in the '/docs' # directory in the root of your repository. /docs/ @doctocat foobar/ @fooowner \#foo/ @hashowner docs/*.md @mdowner # this example tests an escaped space in the path space/test\ space/ @spaceowner # In this example, @infra owns any file and directory in the # '/terraform' directory in the root of your repository. /terraform @infra ` gitlabSections = `# This is a GitLab section with default owners. [Team 1][1] @default1 @default2 *.js @js-owner *.txt # This is another section with new defaults. [Team 2] @default3 @default4 *.java @java-owner * # This is an optional sections without defaults. ^[Team 3] *.go @team-1 ` codeowners []Codeowner ) func TestParseGitLabSectionsWithDefaults(t *testing.T) { t.Parallel() r := bytes.NewBufferString(gitlabSections) c, _ := parseCodeowners(r) expected := []Codeowner{ co("*.js", []string{"@js-owner"}), co("*.txt", []string{"@default1", "@default2"}), co("*.java", []string{"@java-owner"}), co("*", []string{"@default3", "@default4"}), co("*.go", []string{"@team-1"}), } assert.Equal(t, expected, c) } func TestParseCodeowners(t *testing.T) { t.Parallel() r := bytes.NewBufferString(sample) c, _ := parseCodeowners(r) expected := []Codeowner{ co("*", []string{"@everyone"}), co("foobar/", []string{"someone@else.com"}), co("docs/**", []string{"@org/docteam", "@joe"}), } assert.Equal(t, expected, c) } func TestParseCodeownersSections(t *testing.T) { t.Parallel() r := bytes.NewBufferString(sample4) c, _ := parseCodeowners(r) expected := []Codeowner{ co("*", []string{"@everyone"}), co("*/foo", []string{"@everyoneelse"}), } assert.Equal(t, expected, c) } func BenchmarkParseCodeowners(b *testing.B) { r := bytes.NewBufferString(sample) var c []Codeowner for n := 0; n < b.N; n++ { c, _ = parseCodeowners(r) } codeowners = c } func TestFindCodeownersFile(t *testing.T) { fsys := fstest.MapFS{ "src/.github/CODEOWNERS": &fstest.MapFile{Data: []byte(sample)}, "src/foo/CODEOWNERS": &fstest.MapFile{Data: []byte(sample2)}, "src/foo/qux/docs/CODEOWNERS": &fstest.MapFile{Data: []byte(sample3)}, } r, root, err := findCodeownersFile(fsys, "src") require.NoError(t, err) assert.NotNil(t, r) assert.Equal(t, "src", root) b, _ := io.ReadAll(r) assert.Equal(t, sample, string(b)) r, root, err = findCodeownersFile(fsys, "src/foo/bar") require.NoError(t, err) assert.NotNil(t, r) assert.Equal(t, "src/foo", root) b, _ = io.ReadAll(r) assert.Equal(t, sample2, string(b)) r, root, err = findCodeownersFile(fsys, "src/foo/qux/quux") require.NoError(t, err) assert.NotNil(t, r) assert.Equal(t, "src/foo/qux", root) b, _ = io.ReadAll(r) assert.Equal(t, sample3, string(b)) r, _, err = findCodeownersFile(fsys, ".") require.NoError(t, err) assert.Nil(t, r) } func co(pattern string, owners []string) Codeowner { c, err := NewCodeowner(pattern, owners) if err != nil { panic(err) } return c } func TestFullParseCodeowners(t *testing.T) { t.Parallel() c, _ := parseCodeowners(strings.NewReader(fullSample)) codeowners := &Codeowners{ repoRoot: "/build", Patterns: c, } // these tests were ported from https://github.com/softprops/codeowners data := []struct { path string owners []string }{ {"#foo/bar.go", []string{"@hashowner"}}, {"foobar/baz.go", []string{"@fooowner"}}, {"/docs/README.md", []string{"@mdowner"}}, // XXX: uncertain about this one {"blah/docs/README.md", []string{"docs@example.com"}}, {"foo.txt", []string{"@global-owner1", "@global-owner2"}}, {"foo/bar.txt", []string{"@global-owner1", "@global-owner2"}}, {"foo.js", []string{"@js-owner"}}, {"foo/bar.js", []string{"@js-owner"}}, {"foo.go", []string{"docs@example.com"}}, {"foo/bar.go", []string{"docs@example.com"}}, // relative to root {"build/logs/foo.go", []string{"@doctocat"}}, {"build/logs/foo/bar.go", []string{"@doctocat"}}, // not relative to root {"foo/build/logs/foo.go", []string{"docs@example.com"}}, // docs anywhere {"foo/docs/foo.js", []string{"docs@example.com"}}, {"foo/bar/docs/foo.js", []string{"docs@example.com"}}, // but not nested {"foo/bar/docs/foo/foo.js", []string{"@js-owner"}}, {"foo/apps/foo.js", []string{"@octocat"}}, {"docs/foo.js", []string{"@doctocat"}}, {"/docs/foo.js", []string{"@doctocat"}}, {"/space/test space/doc1.txt", []string{"@spaceowner"}}, {"/terraform/kubernetes", []string{"@infra"}}, {"/cells/foo", []string{"@fooowner"}}, {"/cells/foo/", []string{"@fooowner"}}, {"/cells/foo/bar", []string{"@fooowner"}}, {"/cells/foo/bar/quux", []string{"@fooowner"}}, } for _, d := range data { t.Run(fmt.Sprintf("%q==%#v", d.path, d.owners), func(t *testing.T) { assert.EqualValues(t, d.owners, codeowners.Owners(d.path)) }) } } func TestOwners(t *testing.T) { foo := []string{"@foo"} bar := []string{"@bar"} baz := []string{"@baz"} data := []struct { patterns []Codeowner path string expected []string }{ {[]Codeowner{co("a/*", foo)}, "c/b", nil}, {[]Codeowner{co("**", foo)}, "a/b", foo}, {[]Codeowner{co("**", foo), co("a/b/*", bar)}, "a/b/c", bar}, {[]Codeowner{co("**", foo), co("a/b/*", bar), co("a/b/c", baz)}, "a/b/c", baz}, {[]Codeowner{co("**", foo), co("a/*/c", bar), co("a/b/*", baz)}, "a/b/c", baz}, {[]Codeowner{co("**", foo), co("a/b/*", bar), co("a/b/", baz)}, "a/b/bar", baz}, {[]Codeowner{co("**", foo), co("a/b/*", bar), co("a/b/", baz)}, "/someroot/a/b/bar", baz}, {[]Codeowner{ co("*", foo), co("/a/*", bar), co("/b/**", baz)}, "/a/aa/file", foo}, {[]Codeowner{ co("*", foo), co("/a/**", bar)}, "/a/bb/file", bar}, {[]Codeowner{ co("*", []string{"@foo", "@bar"}), co("/bar/", bar)}, "/bar/quux", bar}, {[]Codeowner{ co("*", []string{"@foo", "@bar"}), co("/bar", bar)}, "/bar", bar}, {[]Codeowner{ co("*", []string{"@foo", "@bar"}), co("/bar", bar)}, "/bar/quux", bar}, } for _, d := range data { t.Run(fmt.Sprintf("%s==%s", d.path, d.expected), func(t *testing.T) { c := &Codeowners{Patterns: d.patterns, repoRoot: "/someroot"} owners := c.Owners(d.path) assert.Equal(t, d.expected, owners) }) } } func TestCombineEscapedSpaces(t *testing.T) { data := []struct { fields []string expected []string }{ {[]string{"docs/", "@owner"}, []string{"docs/", "@owner"}}, {[]string{"docs/bob/**", "@owner"}, []string{"docs/bob/**", "@owner"}}, {[]string{"docs/bob\\", "test/", "@owner"}, []string{"docs/bob test/", "@owner"}}, {[]string{"docs/bob\\", "test/sub/final\\", "space/", "@owner"}, []string{"docs/bob test/sub/final space/", "@owner"}}, {[]string{"docs/bob\\", "test/another\\", "test/**", "@owner"}, []string{"docs/bob test/another test/**", "@owner"}}, } for _, d := range data { t.Run(fmt.Sprintf("%s==%s", d.fields, d.expected), func(t *testing.T) { assert.Equal(t, d.expected, combineEscapedSpaces(d.fields)) }) } } func cwd() string { _, filename, _, _ := runtime.Caller(0) cwd := path.Dir(filename) return cwd } func ExampleFromFile() { c, _ := FromFile(cwd()) fmt.Println(c.Patterns[0]) // Output: // * @hairyhenderson } func ExampleFromFileWithFS() { // open filesystem rooted at current working directory fsys := os.DirFS(cwd()) c, _ := FromFileWithFS(fsys, ".") fmt.Println(c.Patterns[0]) // Output: // * @hairyhenderson } func ExampleFromReader() { reader := strings.NewReader(sample2) c, _ := FromReader(reader, "") fmt.Println(c.Patterns[0]) // Output: // * @hairyhenderson } func ExampleCodeowners_Owners() { c, _ := FromFile(cwd()) owners := c.Owners("README.md") for i, o := range owners { fmt.Printf("Owner #%d is %s\n", i, o) } // Output: // Owner #0 is @hairyhenderson } go-codeowners-0.5.0/go.mod000066400000000000000000000005441463726307100154110ustar00rootroot00000000000000module github.com/hairyhenderson/go-codeowners go 1.21.8 require github.com/stretchr/testify v1.9.0 require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) go-codeowners-0.5.0/go.sum000066400000000000000000000036021463726307100154340ustar00rootroot00000000000000github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=