2023-05-30 10:18:05 -05:00
package main
import (
"errors"
"flag"
"fmt"
"io"
"io/fs"
"log"
"strings"
2023-07-18 12:42:09 -05:00
"os"
2023-05-30 10:18:05 -05:00
"golang.org/x/mod/modfile"
)
type Module struct {
Name string
Owners [ ] string
Indirect bool
}
func parseModule ( mod * modfile . Require ) Module {
m := Module { Name : mod . Mod . String ( ) }
// For each require, access the comment.
for _ , comment := range mod . Syntax . Comments . Suffix {
owners := strings . Fields ( comment . Token )
// For each comment, determine if it contains owner(s).
for _ , owner := range owners {
if strings . Contains ( owner , "indirect" ) {
m . Indirect = true
}
// If there is an owner, add to owners list.
if strings . Contains ( owner , "@" ) {
m . Owners = append ( m . Owners , owner )
}
}
}
return m
}
func parseGoMod ( fileSystem fs . FS , name string ) ( [ ] Module , error ) {
file , err := fileSystem . Open ( name )
if err != nil {
return nil , err
}
defer file . Close ( )
// Turn modfile into array of bytes.
data , err := io . ReadAll ( file )
if err != nil {
return nil , err
}
// Parse modfile.
modFile , err := modfile . Parse ( name , data , nil )
if err != nil {
return nil , err
}
modules := [ ] Module { }
// Iterate through requires in modfile.
for _ , mod := range modFile . Require {
m := parseModule ( mod )
modules = append ( modules , m )
}
return modules , nil
}
// Validate that each module has an owner.
2023-07-18 12:42:09 -05:00
// An example CLI command is `go run scripts/modowners/modowners.go check go.mod`
2023-05-30 10:18:05 -05:00
func check ( fileSystem fs . FS , logger * log . Logger , args [ ] string ) error {
m , err := parseGoMod ( fileSystem , args [ 0 ] )
if err != nil {
return err
}
fail := false
for _ , mod := range m {
if ! mod . Indirect && len ( mod . Owners ) == 0 {
logger . Println ( mod . Name )
fail = true
}
}
if fail {
2023-07-18 12:42:09 -05:00
return errors . New ( "one or more newly added dependencies do not have an assigned owner - please assign a team as an owner" )
2023-05-30 10:18:05 -05:00
}
return nil
}
2023-07-18 12:42:09 -05:00
// Print owner(s) for a given dependency.
// An example CLI command to get a list of all owners in go.mod with a count of the number of dependencies they own is `go run scripts/modowners/modowners.go owners -a -c go.mod`
// An example CLI command to get the owner for a specific dependency is `go run scripts/modowners/modowners.go owners -d cloud.google.com/go/storage@v1.30.1 go.mod`. You must use `dependency@version`, not `dependency version`.
2023-05-30 10:18:05 -05:00
func owners ( fileSystem fs . FS , logger * log . Logger , args [ ] string ) error {
fs := flag . NewFlagSet ( "owners" , flag . ExitOnError )
2023-07-18 12:42:09 -05:00
allOwners := fs . Bool ( "a" , false , "print all owners in specified file" )
2023-05-30 10:18:05 -05:00
count := fs . Bool ( "c" , false , "print count of dependencies per owner" )
2023-07-18 12:42:09 -05:00
dep := fs . String ( "d" , "" , "name of dependency" )
2023-05-30 10:18:05 -05:00
fs . Parse ( args )
m , err := parseGoMod ( fileSystem , fs . Arg ( 0 ) )
if err != nil {
return err
}
owners := map [ string ] int { }
for _ , mod := range m {
2023-07-18 12:42:09 -05:00
if len ( * dep ) > 0 && mod . Name == * dep {
for _ , owner := range mod . Owners {
logger . Println ( owner )
break
}
}
2023-05-30 10:18:05 -05:00
if mod . Indirect == false {
for _ , owner := range mod . Owners {
owners [ owner ] ++
}
}
}
2023-07-18 12:42:09 -05:00
if * allOwners {
for owner , n := range owners {
if * count {
logger . Println ( owner , n )
} else {
logger . Println ( owner )
}
2023-05-30 10:18:05 -05:00
}
}
return nil
}
2023-07-18 12:42:09 -05:00
// Print dependencies for a given owner. Can specify one or more owners.
2024-02-15 03:00:30 -06:00
// An example CLI command to list all direct dependencies owned by Delivery and Authnz `go run scripts/modowners/modowners.go modules -o @grafana/grafana-release-guild,@grafana/identity-access-team go.mod`
2023-05-30 10:18:05 -05:00
func modules ( fileSystem fs . FS , logger * log . Logger , args [ ] string ) error {
fs := flag . NewFlagSet ( "modules" , flag . ExitOnError )
2023-07-18 12:42:09 -05:00
indirect := fs . Bool ( "i" , false , "print indirect dependencies" )
2023-05-30 10:18:05 -05:00
owner := fs . String ( "o" , "" , "one or more owners" )
fs . Parse ( args )
2023-07-18 12:42:09 -05:00
m , err := parseGoMod ( fileSystem , fs . Arg ( 0 ) )
2023-05-30 10:18:05 -05:00
if err != nil {
return err
}
ownerFlags := strings . Split ( * owner , "," )
for _ , mod := range m {
2023-07-18 12:42:09 -05:00
if len ( * owner ) == 0 || hasCommonElement ( mod . Owners , ownerFlags ) {
if * indirect || ! mod . Indirect {
logger . Println ( mod . Name )
}
2023-05-30 10:18:05 -05:00
}
}
return nil
}
func hasCommonElement ( a [ ] string , b [ ] string ) bool {
for _ , u := range a {
for _ , v := range b {
if u == v {
return true
}
}
}
return false
}
func main ( ) {
2023-07-18 12:42:09 -05:00
log . SetFlags ( 0 )
log . SetOutput ( os . Stdout )
2023-05-30 10:18:05 -05:00
if len ( os . Args ) < 2 {
fmt . Println ( "usage: modowners subcommand go.mod..." )
os . Exit ( 1 )
}
type CmdFunc func ( fs . FS , * log . Logger , [ ] string ) error
cmds := map [ string ] CmdFunc { "check" : check , "owners" : owners , "modules" : modules }
2023-07-18 12:42:09 -05:00
if f , ok := cmds [ os . Args [ 1 ] ] ; ! ok {
2023-05-30 10:18:05 -05:00
log . Fatal ( "invalid command" )
} else if err := f ( os . DirFS ( "." ) , log . Default ( ) , os . Args [ 2 : ] ) ; err != nil {
log . Fatal ( err )
}
}