github.com/yrj2011/jx-test-infra@v0.0.0-20190529031832-7a2065ee98eb/prow/repoowners/repoowners.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package repoowners
    18  
    19  import (
    20  	"fmt"
    21  	"io/ioutil"
    22  	"os"
    23  	"path/filepath"
    24  	"regexp"
    25  	"strings"
    26  	"sync"
    27  
    28  	"github.com/ghodss/yaml"
    29  	"github.com/sirupsen/logrus"
    30  
    31  	"k8s.io/apimachinery/pkg/util/sets"
    32  	"k8s.io/test-infra/prow/git"
    33  	"k8s.io/test-infra/prow/github"
    34  
    35  	prowConf "k8s.io/test-infra/prow/config"
    36  )
    37  
    38  const (
    39  	ownersFileName  = "OWNERS"
    40  	aliasesFileName = "OWNERS_ALIASES"
    41  	// Github's api uses "" (empty) string as basedir by convention but it's clearer to use "/"
    42  	baseDirConvention = ""
    43  )
    44  
    45  var defaultDirBlacklist = sets.NewString(".git", "_output")
    46  
    47  type dirOptions struct {
    48  	NoParentOwners bool `json:"no_parent_owners,omitempty"`
    49  }
    50  
    51  // Config holds roles+usernames and labels for a directory considered as a unit of independent code
    52  type Config struct {
    53  	Approvers         []string `json:"approvers,omitempty"`
    54  	Reviewers         []string `json:"reviewers,omitempty"`
    55  	RequiredReviewers []string `json:"required_reviewers,omitempty"`
    56  	Labels            []string `json:"labels,omitempty"`
    57  }
    58  
    59  // SimpleConfig holds options and Config applied to everything under the containing directory
    60  type SimpleConfig struct {
    61  	Options dirOptions `json:"options,omitempty"`
    62  	Config  `json:",inline"`
    63  }
    64  
    65  // Empty checks if a SimpleConfig could be considered empty
    66  func (s *SimpleConfig) Empty() bool {
    67  	return len(s.Approvers) == 0 && len(s.Reviewers) == 0 && len(s.RequiredReviewers) == 0 && len(s.Labels) == 0
    68  }
    69  
    70  // FullConfig contains Filters which apply specific Config to files matching its regexp
    71  type FullConfig struct {
    72  	Options dirOptions        `json:"options,omitempty"`
    73  	Filters map[string]Config `json:"filters,omitempty"`
    74  }
    75  
    76  type githubClient interface {
    77  	ListCollaborators(org, repo string) ([]github.User, error)
    78  	GetRef(org, repo, ref string) (string, error)
    79  }
    80  
    81  type cacheEntry struct {
    82  	sha     string
    83  	aliases RepoAliases
    84  	owners  *RepoOwners
    85  }
    86  
    87  type configGetter interface {
    88  	Config() *prowConf.Config
    89  }
    90  
    91  // Interface is an interface to work with OWNERS files.
    92  type Interface interface {
    93  	LoadRepoAliases(org, repo, base string) (RepoAliases, error)
    94  	LoadRepoOwners(org, repo, base string) (RepoOwnerInterface, error)
    95  }
    96  
    97  // Client is an implementation of the Interface.
    98  var _ Interface = &Client{}
    99  
   100  // Client is the repoowners client
   101  type Client struct {
   102  	configGetter configGetter
   103  
   104  	git    *git.Client
   105  	ghc    githubClient
   106  	logger *logrus.Entry
   107  
   108  	mdYAMLEnabled     func(org, repo string) bool
   109  	skipCollaborators func(org, repo string) bool
   110  
   111  	lock  sync.Mutex
   112  	cache map[string]cacheEntry
   113  }
   114  
   115  // NewClient is the constructor for Client
   116  func NewClient(
   117  	gc *git.Client,
   118  	ghc *github.Client,
   119  	configGetter *prowConf.Agent,
   120  	mdYAMLEnabled func(org, repo string) bool,
   121  	skipCollaborators func(org, repo string) bool,
   122  ) *Client {
   123  	return &Client{
   124  		git:    gc,
   125  		ghc:    ghc,
   126  		logger: logrus.WithField("client", "repoowners"),
   127  		cache:  make(map[string]cacheEntry),
   128  
   129  		mdYAMLEnabled:     mdYAMLEnabled,
   130  		skipCollaborators: skipCollaborators,
   131  
   132  		configGetter: configGetter,
   133  	}
   134  }
   135  
   136  // RepoAliases defines groups of people to be used in OWNERS files
   137  type RepoAliases map[string]sets.String
   138  
   139  // RepoOwnerInterface is an interface to work with repoowners
   140  type RepoOwnerInterface interface {
   141  	FindApproverOwnersForFile(path string) string
   142  	FindReviewersOwnersForFile(path string) string
   143  	FindLabelsForFile(path string) sets.String
   144  	IsNoParentOwners(path string) bool
   145  	LeafApprovers(path string) sets.String
   146  	Approvers(path string) sets.String
   147  	LeafReviewers(path string) sets.String
   148  	Reviewers(path string) sets.String
   149  	RequiredReviewers(path string) sets.String
   150  }
   151  
   152  // RepoOwners implements RepoOwnerInterface
   153  var _ RepoOwnerInterface = &RepoOwners{}
   154  
   155  // RepoOwners contains the parsed OWNERS config
   156  type RepoOwners struct {
   157  	RepoAliases
   158  
   159  	approvers         map[string]map[*regexp.Regexp]sets.String
   160  	reviewers         map[string]map[*regexp.Regexp]sets.String
   161  	requiredReviewers map[string]map[*regexp.Regexp]sets.String
   162  	labels            map[string]map[*regexp.Regexp]sets.String
   163  	options           map[string]dirOptions
   164  
   165  	baseDir      string
   166  	enableMDYAML bool
   167  	dirBlacklist sets.String
   168  
   169  	log *logrus.Entry
   170  }
   171  
   172  // LoadRepoAliases returns an up-to-date RepoAliases struct for the specified repo.
   173  // If the repo does not have an aliases file then an empty alias map is returned with no error.
   174  // Note: The returned RepoAliases should be treated as read only.
   175  func (c *Client) LoadRepoAliases(org, repo, base string) (RepoAliases, error) {
   176  	log := c.logger.WithFields(logrus.Fields{"org": org, "repo": repo, "base": base})
   177  	cloneRef := fmt.Sprintf("%s/%s", org, repo)
   178  	fullName := fmt.Sprintf("%s:%s", cloneRef, base)
   179  
   180  	sha, err := c.ghc.GetRef(org, repo, fmt.Sprintf("heads/%s", base))
   181  	if err != nil {
   182  		return nil, fmt.Errorf("failed to get current SHA for %s: %v", fullName, err)
   183  	}
   184  
   185  	c.lock.Lock()
   186  	defer c.lock.Unlock()
   187  	entry, ok := c.cache[fullName]
   188  	if !ok || entry.sha != sha {
   189  		// entry is non-existent or stale.
   190  		gitRepo, err := c.git.Clone(cloneRef)
   191  		if err != nil {
   192  			return nil, fmt.Errorf("failed to clone %s: %v", cloneRef, err)
   193  		}
   194  		defer gitRepo.Clean()
   195  		if err := gitRepo.Checkout(base); err != nil {
   196  			return nil, err
   197  		}
   198  
   199  		entry.aliases = loadAliasesFrom(gitRepo.Dir, log)
   200  		entry.sha = sha
   201  		c.cache[fullName] = entry
   202  	}
   203  
   204  	return entry.aliases, nil
   205  }
   206  
   207  // LoadRepoOwners returns an up-to-date RepoOwners struct for the specified repo.
   208  // Note: The returned *RepoOwners should be treated as read only.
   209  func (c *Client) LoadRepoOwners(org, repo, base string) (RepoOwnerInterface, error) {
   210  	log := c.logger.WithFields(logrus.Fields{"org": org, "repo": repo, "base": base})
   211  	cloneRef := fmt.Sprintf("%s/%s", org, repo)
   212  	fullName := fmt.Sprintf("%s:%s", cloneRef, base)
   213  	mdYaml := c.mdYAMLEnabled(org, repo)
   214  
   215  	sha, err := c.ghc.GetRef(org, repo, fmt.Sprintf("heads/%s", base))
   216  	if err != nil {
   217  		return nil, fmt.Errorf("failed to get current SHA for %s: %v", fullName, err)
   218  	}
   219  
   220  	c.lock.Lock()
   221  	defer c.lock.Unlock()
   222  	entry, ok := c.cache[fullName]
   223  	if !ok || entry.sha != sha || entry.owners == nil || entry.owners.enableMDYAML != mdYaml {
   224  		gitRepo, err := c.git.Clone(cloneRef)
   225  		if err != nil {
   226  			return nil, fmt.Errorf("failed to clone %s: %v", cloneRef, err)
   227  		}
   228  		defer gitRepo.Clean()
   229  		if err := gitRepo.Checkout(base); err != nil {
   230  			return nil, err
   231  		}
   232  
   233  		if entry.aliases == nil || entry.sha != sha {
   234  			// aliases must be loaded
   235  			entry.aliases = loadAliasesFrom(gitRepo.Dir, log)
   236  		}
   237  
   238  		blacklistConfig := c.configGetter.Config().OwnersDirBlacklist
   239  
   240  		dirBlacklist := defaultDirBlacklist.Union(sets.NewString(blacklistConfig.Default...))
   241  		if bl, ok := blacklistConfig.Repos[org]; ok {
   242  			dirBlacklist.Insert(bl...)
   243  		}
   244  		if bl, ok := blacklistConfig.Repos[org+"/"+repo]; ok {
   245  			dirBlacklist.Insert(bl...)
   246  		}
   247  		entry.owners, err = loadOwnersFrom(gitRepo.Dir, mdYaml, entry.aliases, dirBlacklist, log)
   248  		if err != nil {
   249  			return nil, fmt.Errorf("failed to load RepoOwners for %s: %v", fullName, err)
   250  		}
   251  		entry.sha = sha
   252  		c.cache[fullName] = entry
   253  	}
   254  
   255  	if c.skipCollaborators(org, repo) {
   256  		log.Debugf("Skipping collaborator checks for %s/%s", org, repo)
   257  		return entry.owners, nil
   258  	}
   259  
   260  	var owners *RepoOwners
   261  	// Filter collaborators. We must filter the RepoOwners struct even if it came from the cache
   262  	// because the list of collaborators could have changed without the git SHA changing.
   263  	collaborators, err := c.ghc.ListCollaborators(org, repo)
   264  	if err != nil {
   265  		log.WithError(err).Errorf("Failed to list collaborators while loading RepoOwners. Skipping collaborator filtering.")
   266  		owners = entry.owners
   267  	} else {
   268  		owners = entry.owners.filterCollaborators(collaborators)
   269  	}
   270  	return owners, nil
   271  }
   272  
   273  // ExpandAlias returns members of an alias
   274  func (a RepoAliases) ExpandAlias(alias string) sets.String {
   275  	if a == nil {
   276  		return nil
   277  	}
   278  	return a[github.NormLogin(alias)]
   279  }
   280  
   281  // ExpandAliases returns members of multiple aliases, duplicates are pruned
   282  func (a RepoAliases) ExpandAliases(logins sets.String) sets.String {
   283  	if a == nil {
   284  		return logins
   285  	}
   286  	// Make logins a copy of the original set to avoid modifying the original.
   287  	logins = logins.Union(nil)
   288  	for _, login := range logins.List() {
   289  		if expanded := a.ExpandAlias(login); len(expanded) > 0 {
   290  			logins.Delete(login)
   291  			logins = logins.Union(expanded)
   292  		}
   293  	}
   294  	return logins
   295  }
   296  
   297  func loadAliasesFrom(baseDir string, log *logrus.Entry) RepoAliases {
   298  	path := filepath.Join(baseDir, aliasesFileName)
   299  	b, err := ioutil.ReadFile(path)
   300  	if os.IsNotExist(err) {
   301  		log.WithError(err).Infof("No alias file exists at %q. Using empty alias map.", path)
   302  		return nil
   303  	} else if err != nil {
   304  		log.WithError(err).Warnf("Failed to read alias file %q. Using empty alias map.", path)
   305  		return nil
   306  	}
   307  	config := &struct {
   308  		Data map[string][]string `json:"aliases,omitempty"`
   309  	}{}
   310  	if err := yaml.Unmarshal(b, config); err != nil {
   311  		log.WithError(err).Errorf("Failed to unmarshal aliases from %q. Using empty alias map.", path)
   312  		return nil
   313  	}
   314  
   315  	result := make(RepoAliases)
   316  	for alias, expanded := range config.Data {
   317  		result[github.NormLogin(alias)] = normLogins(expanded)
   318  	}
   319  	log.Infof("Loaded %d aliases from %q.", len(result), path)
   320  	return result
   321  }
   322  
   323  func loadOwnersFrom(baseDir string, mdYaml bool, aliases RepoAliases, dirBlacklist sets.String, log *logrus.Entry) (*RepoOwners, error) {
   324  	o := &RepoOwners{
   325  		RepoAliases:  aliases,
   326  		baseDir:      baseDir,
   327  		enableMDYAML: mdYaml,
   328  		log:          log,
   329  
   330  		approvers:         make(map[string]map[*regexp.Regexp]sets.String),
   331  		reviewers:         make(map[string]map[*regexp.Regexp]sets.String),
   332  		requiredReviewers: make(map[string]map[*regexp.Regexp]sets.String),
   333  		labels:            make(map[string]map[*regexp.Regexp]sets.String),
   334  		options:           make(map[string]dirOptions),
   335  
   336  		dirBlacklist: dirBlacklist,
   337  	}
   338  
   339  	return o, filepath.Walk(o.baseDir, o.walkFunc)
   340  }
   341  
   342  // by default, github's api doesn't root the project directory at "/" and instead uses the empty string for the base dir
   343  // of the project. And the built-in dir function returns "." for empty strings, so for consistency, we use this
   344  // canonicalize to get the directories of files in a consistent format with NO "/" at the root (a/b/c/ -> a/b/c)
   345  func canonicalize(path string) string {
   346  	if path == "." {
   347  		return baseDirConvention
   348  	}
   349  	return strings.TrimSuffix(path, "/")
   350  }
   351  
   352  func (o *RepoOwners) walkFunc(path string, info os.FileInfo, err error) error {
   353  	log := o.log.WithField("path", path)
   354  	if err != nil {
   355  		log.WithError(err).Error("Error while walking OWNERS files.")
   356  		return nil
   357  	}
   358  	filename := filepath.Base(path)
   359  
   360  	if info.Mode().IsDir() && o.dirBlacklist.Has(filename) {
   361  		return filepath.SkipDir
   362  	}
   363  	if !info.Mode().IsRegular() {
   364  		return nil
   365  	}
   366  
   367  	// '.md' files may contain assignees at the top of the file in a yaml header
   368  	// Note that these assignees only apply to the file itself.
   369  	if o.enableMDYAML && strings.HasSuffix(filename, ".md") {
   370  		// Parse the yaml header from the file if it exists and marshal into the config
   371  		simple := &SimpleConfig{}
   372  		if err := decodeOwnersMdConfig(path, simple); err != nil {
   373  			log.WithError(err).Error("Error decoding OWNERS config from '*.md' file.")
   374  			return nil
   375  		}
   376  
   377  		// Set owners for this file (not the directory) using the relative path if they were found
   378  		relPath, err := filepath.Rel(o.baseDir, path)
   379  		if err != nil {
   380  			log.WithError(err).Errorf("Unable to find relative path between baseDir: %q and path.", o.baseDir)
   381  			return err
   382  		}
   383  		o.applyConfigToPath(relPath, nil, &simple.Config)
   384  		o.applyOptionsToPath(relPath, simple.Options)
   385  		return nil
   386  	}
   387  
   388  	if filename != ownersFileName {
   389  		return nil
   390  	}
   391  
   392  	b, err := ioutil.ReadFile(path)
   393  	if err != nil {
   394  		log.WithError(err).Errorf("Failed to read the OWNERS file.")
   395  		return nil
   396  	}
   397  
   398  	relPath, err := filepath.Rel(o.baseDir, path)
   399  	if err != nil {
   400  		log.WithError(err).Errorf("Unable to find relative path between baseDir: %q and path.", o.baseDir)
   401  		return err
   402  	}
   403  	relPathDir := canonicalize(filepath.Dir(relPath))
   404  
   405  	simple, err := ParseSimpleConfig(b)
   406  	if err != nil || simple.Empty() {
   407  		c, err := ParseFullConfig(b)
   408  		if err != nil {
   409  			log.WithError(err).Errorf("Failed to unmarshal %s into either Simple or FullConfig.", path)
   410  		} else {
   411  			// it's a FullConfig
   412  			for pattern, config := range c.Filters {
   413  				var re *regexp.Regexp
   414  				if pattern != ".*" {
   415  					if re, err = regexp.Compile(pattern); err != nil {
   416  						log.WithError(err).Errorf("Invalid regexp %q.", pattern)
   417  						continue
   418  					}
   419  				}
   420  				o.applyConfigToPath(relPathDir, re, &config)
   421  			}
   422  			o.applyOptionsToPath(relPathDir, c.Options)
   423  		}
   424  	} else {
   425  		// it's a SimpleConfig
   426  		o.applyConfigToPath(relPathDir, nil, &simple.Config)
   427  		o.applyOptionsToPath(relPathDir, simple.Options)
   428  	}
   429  	return nil
   430  }
   431  
   432  // ParseFullConfig will unmarshal OWNERS file's content into a FullConfig
   433  // Returns an error if the content cannot be unmarshalled
   434  func ParseFullConfig(b []byte) (FullConfig, error) {
   435  	full := new(FullConfig)
   436  	err := yaml.Unmarshal(b, full)
   437  	return *full, err
   438  }
   439  
   440  // ParseSimpleConfig will unmarshal an OWNERS file's content into a SimpleConfig
   441  // Returns an error if the content cannot be unmarshalled
   442  func ParseSimpleConfig(b []byte) (SimpleConfig, error) {
   443  	simple := new(SimpleConfig)
   444  	err := yaml.Unmarshal(b, simple)
   445  	return *simple, err
   446  }
   447  
   448  var mdStructuredHeaderRegex = regexp.MustCompile("^---\n(.|\n)*\n---")
   449  
   450  // decodeOwnersMdConfig will parse the yaml header if it exists and unmarshal it into a singleOwnersConfig.
   451  // If no yaml header is found, do nothing
   452  // Returns an error if the file cannot be read or the yaml header is found but cannot be unmarshalled.
   453  func decodeOwnersMdConfig(path string, config *SimpleConfig) error {
   454  	fileBytes, err := ioutil.ReadFile(path)
   455  	if err != nil {
   456  		return err
   457  	}
   458  	// Parse the yaml header from the top of the file.  Will return an empty string if regex does not match.
   459  	meta := mdStructuredHeaderRegex.FindString(string(fileBytes))
   460  
   461  	// Unmarshal the yaml header into the config
   462  	return yaml.Unmarshal([]byte(meta), &config)
   463  }
   464  
   465  func normLogins(logins []string) sets.String {
   466  	normed := sets.NewString()
   467  	for _, login := range logins {
   468  		normed.Insert(github.NormLogin(login))
   469  	}
   470  	return normed
   471  }
   472  
   473  var defaultDirOptions = dirOptions{}
   474  
   475  func (o *RepoOwners) applyConfigToPath(path string, re *regexp.Regexp, config *Config) {
   476  	if len(config.Approvers) > 0 {
   477  		if o.approvers[path] == nil {
   478  			o.approvers[path] = make(map[*regexp.Regexp]sets.String)
   479  		}
   480  		o.approvers[path][re] = o.ExpandAliases(normLogins(config.Approvers))
   481  	}
   482  	if len(config.Reviewers) > 0 {
   483  		if o.reviewers[path] == nil {
   484  			o.reviewers[path] = make(map[*regexp.Regexp]sets.String)
   485  		}
   486  		o.reviewers[path][re] = o.ExpandAliases(normLogins(config.Reviewers))
   487  	}
   488  	if len(config.RequiredReviewers) > 0 {
   489  		if o.requiredReviewers[path] == nil {
   490  			o.requiredReviewers[path] = make(map[*regexp.Regexp]sets.String)
   491  		}
   492  		o.requiredReviewers[path][re] = o.ExpandAliases(normLogins(config.RequiredReviewers))
   493  	}
   494  	if len(config.Labels) > 0 {
   495  		if o.labels[path] == nil {
   496  			o.labels[path] = make(map[*regexp.Regexp]sets.String)
   497  		}
   498  		o.labels[path][re] = sets.NewString(config.Labels...)
   499  	}
   500  }
   501  
   502  func (o *RepoOwners) applyOptionsToPath(path string, opts dirOptions) {
   503  	if opts != defaultDirOptions {
   504  		o.options[path] = opts
   505  	}
   506  }
   507  
   508  func (o *RepoOwners) filterCollaborators(toKeep []github.User) *RepoOwners {
   509  	collabs := sets.NewString()
   510  	for _, keeper := range toKeep {
   511  		collabs.Insert(github.NormLogin(keeper.Login))
   512  	}
   513  
   514  	filter := func(ownerMap map[string]map[*regexp.Regexp]sets.String) map[string]map[*regexp.Regexp]sets.String {
   515  		filtered := make(map[string]map[*regexp.Regexp]sets.String)
   516  		for path, reMap := range ownerMap {
   517  			filtered[path] = make(map[*regexp.Regexp]sets.String)
   518  			for re, unfiltered := range reMap {
   519  				filtered[path][re] = unfiltered.Intersection(collabs)
   520  			}
   521  		}
   522  		return filtered
   523  	}
   524  
   525  	result := *o
   526  	result.approvers = filter(o.approvers)
   527  	result.reviewers = filter(o.reviewers)
   528  	return &result
   529  }
   530  
   531  // findOwnersForFile returns the OWNERS file path furthest down the tree for a specified file
   532  // using ownerMap to check for entries
   533  func findOwnersForFile(log *logrus.Entry, path string, ownerMap map[string]map[*regexp.Regexp]sets.String) string {
   534  	d := path
   535  
   536  	for ; d != baseDirConvention; d = canonicalize(filepath.Dir(d)) {
   537  		relative, err := filepath.Rel(d, path)
   538  		if err != nil {
   539  			log.WithError(err).WithField("path", path).Errorf("Unable to find relative path between %q and path.", d)
   540  			return ""
   541  		}
   542  		for re, n := range ownerMap[d] {
   543  			if re != nil && !re.MatchString(relative) {
   544  				continue
   545  			}
   546  			if len(n) != 0 {
   547  				return d
   548  			}
   549  		}
   550  	}
   551  	return ""
   552  }
   553  
   554  // FindApproverOwnersForFile returns the OWNERS file path furthest down the tree for a specified file
   555  // that contains an approvers section
   556  func (o *RepoOwners) FindApproverOwnersForFile(path string) string {
   557  	return findOwnersForFile(o.log, path, o.approvers)
   558  }
   559  
   560  // FindReviewersOwnersForFile returns the OWNERS file path furthest down the tree for a specified file
   561  // that contains a reviewers section
   562  func (o *RepoOwners) FindReviewersOwnersForFile(path string) string {
   563  	return findOwnersForFile(o.log, path, o.reviewers)
   564  }
   565  
   566  // FindLabelsForFile returns a set of labels which should be applied to PRs
   567  // modifying files under the given path.
   568  func (o *RepoOwners) FindLabelsForFile(path string) sets.String {
   569  	return o.entriesForFile(path, o.labels, false)
   570  }
   571  
   572  // IsNoParentOwners checks if an OWNERS file path refers to an OWNERS file with NoParentOwners enabled.
   573  func (o *RepoOwners) IsNoParentOwners(path string) bool {
   574  	return o.options[path].NoParentOwners
   575  }
   576  
   577  // entriesForFile returns a set of users who are assignees to the
   578  // requested file. The path variable should be a full path to a filename
   579  // and not directory as the final directory will be discounted if enableMDYAML is true
   580  // leafOnly indicates whether only the OWNERS deepest in the tree (closest to the file)
   581  // should be returned or if all OWNERS in filepath should be returned
   582  func (o *RepoOwners) entriesForFile(path string, people map[string]map[*regexp.Regexp]sets.String, leafOnly bool) sets.String {
   583  	d := path
   584  	if !o.enableMDYAML || !strings.HasSuffix(path, ".md") {
   585  		// if path is a directory, this will remove the leaf directory, and returns "." for topmost dir
   586  		d = filepath.Dir(d)
   587  		d = canonicalize(path)
   588  	}
   589  
   590  	out := sets.NewString()
   591  	for {
   592  		relative, err := filepath.Rel(d, path)
   593  		if err != nil {
   594  			o.log.WithError(err).WithField("path", path).Errorf("Unable to find relative path between %q and path.", d)
   595  			return nil
   596  		}
   597  		for re, s := range people[d] {
   598  			if re == nil || re.MatchString(relative) {
   599  				out.Insert(s.List()...)
   600  			}
   601  		}
   602  		if leafOnly && out.Len() > 0 {
   603  			break
   604  		}
   605  		if d == baseDirConvention {
   606  			break
   607  		}
   608  		if o.options[d].NoParentOwners {
   609  			break
   610  		}
   611  		d = filepath.Dir(d)
   612  		d = canonicalize(d)
   613  	}
   614  	return out
   615  }
   616  
   617  // LeafApprovers returns a set of users who are the closest approvers to the
   618  // requested file. If pkg/OWNERS has user1 and pkg/util/OWNERS has user2 this
   619  // will only return user2 for the path pkg/util/sets/file.go
   620  func (o *RepoOwners) LeafApprovers(path string) sets.String {
   621  	return o.entriesForFile(path, o.approvers, true)
   622  }
   623  
   624  // Approvers returns ALL of the users who are approvers for the
   625  // requested file (including approvers in parent dirs' OWNERS).
   626  // If pkg/OWNERS has user1 and pkg/util/OWNERS has user2 this
   627  // will return both user1 and user2 for the path pkg/util/sets/file.go
   628  func (o *RepoOwners) Approvers(path string) sets.String {
   629  	return o.entriesForFile(path, o.approvers, false)
   630  }
   631  
   632  // LeafReviewers returns a set of users who are the closest reviewers to the
   633  // requested file. If pkg/OWNERS has user1 and pkg/util/OWNERS has user2 this
   634  // will only return user2 for the path pkg/util/sets/file.go
   635  func (o *RepoOwners) LeafReviewers(path string) sets.String {
   636  	return o.entriesForFile(path, o.reviewers, true)
   637  }
   638  
   639  // Reviewers returns ALL of the users who are reviewers for the
   640  // requested file (including reviewers in parent dirs' OWNERS).
   641  // If pkg/OWNERS has user1 and pkg/util/OWNERS has user2 this
   642  // will return both user1 and user2 for the path pkg/util/sets/file.go
   643  func (o *RepoOwners) Reviewers(path string) sets.String {
   644  	return o.entriesForFile(path, o.reviewers, false)
   645  }
   646  
   647  // RequiredReviewers returns ALL of the users who are required_reviewers for the
   648  // requested file (including required_reviewers in parent dirs' OWNERS).
   649  // If pkg/OWNERS has user1 and pkg/util/OWNERS has user2 this
   650  // will return both user1 and user2 for the path pkg/util/sets/file.go
   651  func (o *RepoOwners) RequiredReviewers(path string) sets.String {
   652  	return o.entriesForFile(path, o.requiredReviewers, false)
   653  }