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  }