github.com/blend/go-sdk@v1.20220411.3/codeowners/codeowners.go (about) 1 /* 2 3 Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file. 5 6 */ 7 8 package codeowners 9 10 import ( 11 "context" 12 "fmt" 13 "io" 14 "os" 15 "path/filepath" 16 "strings" 17 ) 18 19 // New creates a new copyright engine with a given set of config options. 20 func New(options ...Option) *Codeowners { 21 var c Codeowners 22 for _, option := range options { 23 option(&c) 24 } 25 return &c 26 } 27 28 // Option mutates the Codeowners instance. 29 type Option func(*Codeowners) 30 31 // Codeowners holds the engine that generates and validates codeowners files. 32 type Codeowners struct { 33 // Config holds the configuration opitons. 34 Config 35 36 // Stdout is the writer for Verbose and Debug output. 37 // If it is unset, `os.Stdout` will be used. 38 Stdout io.Writer 39 // Stderr is the writer for Error output. 40 // If it is unset, `os.Stderr` will be used. 41 Stderr io.Writer 42 } 43 44 // GenerateFile generates the file as nominated by the config path. 45 func (c Codeowners) GenerateFile(ctx context.Context, root string) error { 46 f, err := os.Create(c.PathOrDefault()) 47 if err != nil { 48 return err 49 } 50 defer func() { _ = f.Close() }() 51 c.Debugf("codeowners path: %s", c.PathOrDefault()) 52 return c.Generate(ctx, root, f) 53 } 54 55 // Generate generates a codeowner file. 56 func (c Codeowners) Generate(ctx context.Context, root string, output io.Writer) error { 57 var codeowners File 58 err := filepath.Walk(root, func(path string, info os.FileInfo, walkErr error) error { 59 if walkErr != nil { 60 return walkErr 61 } 62 63 // skip common bogus dirs 64 if info.IsDir() { 65 if strings.HasPrefix(info.Name(), "_") { 66 return filepath.SkipDir 67 } 68 if info.Name() == "node_modules" { 69 return filepath.SkipDir 70 } 71 if strings.HasPrefix(info.Name(), ".") && info.Name() != "." { 72 return filepath.SkipDir 73 } 74 if strings.HasPrefix(path, "vendor/") { 75 return filepath.SkipDir 76 } 77 return nil 78 } 79 80 // handle go files specially 81 if strings.HasSuffix(info.Name(), ".go") { 82 owners, parseErr := ParseGoComments(root, path, OwnersGoCommentPrefix) 83 if parseErr != nil { 84 return parseErr 85 } 86 if owners != nil { 87 codeowners = append(codeowners, *owners) 88 } 89 return nil 90 } 91 92 // handle the owners file specially 93 if info.Name() == OwnersFile { 94 parsed, parseErr := ParseSource(root, path) 95 if parseErr != nil { 96 return parseErr 97 } 98 if parsed != nil { 99 codeowners = append(codeowners, *parsed) 100 } 101 return nil 102 } 103 return nil 104 }) 105 if err != nil { 106 return err 107 } 108 _, err = codeowners.WriteTo(output) 109 return err 110 } 111 112 // ValidateFile validates the file as configured in the config field. 113 func (c Codeowners) ValidateFile(ctx context.Context) error { 114 f, err := os.Open(c.PathOrDefault()) 115 if err != nil { 116 return err 117 } 118 defer func() { _ = f.Close() }() 119 c.Debugf("codeowners path: %s", c.PathOrDefault()) 120 return c.Validate(ctx, f) 121 } 122 123 // Validate validates a given codeowners file. 124 func (c Codeowners) Validate(ctx context.Context, input io.Reader) error { 125 if c.GithubToken == "" { 126 return fmt.Errorf("codeowners cannot validate; github token is empty") 127 } 128 129 codeownersFile, err := Read(input) 130 if err != nil { 131 return err 132 } 133 ghc := GithubClient{ 134 Addr: c.Config.GithubURLOrDefault(), 135 Token: c.Config.GithubToken, 136 } 137 for _, source := range codeownersFile { 138 for _, path := range source.Paths { 139 // test that the path 140 pathGlob := strings.TrimSuffix(path.PathGlob, "**") 141 pathGlob = strings.TrimSuffix(pathGlob, "*") 142 pathGlob = strings.TrimPrefix(pathGlob, "/") 143 pathGlob = filepath.Join("./", pathGlob) 144 if _, err := os.Stat(pathGlob); err != nil { 145 return fmt.Errorf("codeowners path glob doesn't exist: %q", pathGlob) 146 } 147 148 // test that the owner(s) exist in github 149 for _, owner := range path.Owners { 150 c.Verbosef("codeowners source: %s; checking if owner exists: %s", source.Source, owner) 151 if strings.Contains(owner, "/") { 152 if err := ghc.TeamExists(ctx, owner); err != nil { 153 return fmt.Errorf("github team not found: %q", owner) 154 } 155 } else { 156 if err := ghc.UserExists(ctx, owner); err != nil { 157 return fmt.Errorf("github user not found: %q", owner) 158 } 159 } 160 } 161 } 162 } 163 return nil 164 } 165 166 // GetStdout returns standard out. 167 func (c Codeowners) GetStdout() io.Writer { 168 if c.QuietOrDefault() { 169 return io.Discard 170 } 171 if c.Stdout != nil { 172 return c.Stdout 173 } 174 return os.Stdout 175 } 176 177 // GetStderr returns standard error. 178 func (c Codeowners) GetStderr() io.Writer { 179 if c.QuietOrDefault() { 180 return io.Discard 181 } 182 if c.Stderr != nil { 183 return c.Stderr 184 } 185 return os.Stderr 186 } 187 188 // Verbosef writes to stdout if the `Verbose` flag is true. 189 func (c Codeowners) Verbosef(format string, args ...interface{}) { 190 if !c.VerboseOrDefault() { 191 return 192 } 193 fmt.Fprintf(c.GetStdout(), format+"\n", args...) 194 } 195 196 // Debugf writes to stdout if the `Debug` flag is true. 197 func (c Codeowners) Debugf(format string, args ...interface{}) { 198 if !c.DebugOrDefault() { 199 return 200 } 201 fmt.Fprintf(c.GetStdout(), format+"\n", args...) 202 }