diff --git a/travis/module-span/multi-module-span.go b/travis/module-span/multi-module-span.go new file mode 100644 index 000000000..1bb92c436 --- /dev/null +++ b/travis/module-span/multi-module-span.go @@ -0,0 +1,100 @@ +// multi-module-span script: +// A script which can detect when a pull request would make changes which +// span a series of restricted directories (ex: multiple go modules or projects) +// +// When a pull request includes files which span two modules the script will +// exit with a non-zero exit code. +// +// Running: +// go run multi-module-span.go -owner=kubernetes-sigs -repo=kustomize -pr=2997 cmd/config api/ kustomize/ kyaml/ + +package main + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + + "github.com/google/go-github/github" +) + +func main() { + owner := flag.String("owner", "", "the github repository owner name") + repo := flag.String("repo", "", "the github repository name") + pullrequest := flag.Int("pr", -1, "the pull request number") + flag.Parse() + // Treat all following arguments as restricted directories + restrictedPaths := flag.Args() + + // Short circuit check if restricted paths is less than 2 + // Conflicts won't exist in this scenario so we don't need to call the API + if len(restrictedPaths) <= 1 { + fmt.Println("Check not run. Add at least two restricted paths and run again.") + os.Exit(0) + } + + client := github.NewClient(nil) + + files, err := ListAllPullRequestFiles(client, owner, pullrequest, repo) + + if err != nil { + fmt.Println("Unable to retrieve pull request details:", err.Error()) + os.Exit(2) + } + + contributedRestrictedPaths := CountRestrictedPathUses(files, restrictedPaths) + modifiedRestrictedDirectories := CountModifiedRestrictedDirectories(contributedRestrictedPaths) + + // Exit with error if two or more restricted directories where modified + if modifiedRestrictedDirectories > 1 { + fmt.Println("Modifications to multiple restricted directories occurred.") + os.Exit(1) + } +} + +// ListAllPullRequestFiles retrieves as many files as possible for the +// target pull request. +// +// NOTE: GitHub API limits ListFiles to a maximum of 3000 files. Very large +// changes which exceed this limit may pass this check even if they +// do contain spanning changes. +// see: https://developer.github.com/v3/pulls/#list-pull-requests-files +func ListAllPullRequestFiles(client *github.Client, owner *string, pullrequest *int, repo *string) ([]*github.CommitFile, error) { + // GitHub returns the first 30 files by default, increase this value + options := &github.ListOptions{PerPage: 3000} + files, _, err := client.PullRequests.ListFiles(context.Background(), *owner, *repo, *pullrequest, options) + return files, err +} + +// CountModifiedRestrictedDirectories Accepts a map of paths and the number of +// occurances and returns the count of the paths which had a non-zero value. +func CountModifiedRestrictedDirectories(contributedRestrictedPaths map[string]int) int { + modifiedRestrictedDirectories := 0 + for _, occurance := range contributedRestrictedPaths { + if occurance != 0 { + modifiedRestrictedDirectories++ + } + } + return modifiedRestrictedDirectories +} + +// CountRestrictedPathUses Constructs a map that contains the number of +// references keyed to each restricted path. This provides details about how +// many files in the list are associated with each restricted path. +func CountRestrictedPathUses(files []*github.CommitFile, restrictedPaths []string) map[string]int { + contributedRestrictedPaths := make(map[string]int) + for _, path := range restrictedPaths { + contributedRestrictedPaths[path] = 0 + } + + for _, file := range files { + for path := range contributedRestrictedPaths { + if strings.HasPrefix(*file.Filename, path) { + contributedRestrictedPaths[path]++ + } + } + } + return contributedRestrictedPaths +}