@@ -17,14 +17,26 @@ package githubrepo
1717import (
1818 "context"
1919 "fmt"
20+ "io"
21+ "maps"
22+ "net/http"
2023 "strings"
2124 "sync"
2225
2326 "github.com/google/go-github/v53/github"
27+ "github.com/hmarr/codeowners"
2428
2529 "github.com/ossf/scorecard/v5/clients"
2630)
2731
32+ // these are the paths where CODEOWNERS files can be found see
33+ // https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-file-location
34+ //
35+ //nolint:lll
36+ var (
37+ codeOwnerPaths []string = []string {"CODEOWNERS" , ".github/CODEOWNERS" , "docs/CODEOWNERS" }
38+ )
39+
2840type contributorsHandler struct {
2941 ghClient * github.Client
3042 once * sync.Once
@@ -42,28 +54,26 @@ func (handler *contributorsHandler) init(ctx context.Context, repourl *Repo) {
4254 handler .contributors = nil
4355}
4456
45- func (handler * contributorsHandler ) setup () error {
57+ func (handler * contributorsHandler ) setup (codeOwnerFile io.ReadCloser ) error {
58+ defer codeOwnerFile .Close ()
4659 handler .once .Do (func () {
4760 if ! strings .EqualFold (handler .repourl .commitSHA , clients .HeadSHA ) {
4861 handler .errSetup = fmt .Errorf ("%w: ListContributors only supported for HEAD queries" , clients .ErrUnsupportedFeature )
4962 return
5063 }
51- contribs , _ , err := handler .ghClient .Repositories .ListContributors (
52- handler .ctx , handler .repourl .owner , handler .repourl .repo , & github.ListContributorsOptions {})
53- if err != nil {
54- handler .errSetup = fmt .Errorf ("error during ListContributors: %w" , err )
64+
65+ contributors := make (map [string ]clients.User )
66+ mapContributors (handler , contributors )
67+ if handler .errSetup != nil {
68+ return
69+ }
70+ mapCodeOwners (handler , codeOwnerFile , contributors )
71+ if handler .errSetup != nil {
5572 return
5673 }
5774
58- for _ , contrib := range contribs {
59- if contrib .GetLogin () == "" {
60- continue
61- }
62- contributor := clients.User {
63- NumContributions : contrib .GetContributions (),
64- Login : contrib .GetLogin (),
65- }
66- orgs , _ , err := handler .ghClient .Organizations .List (handler .ctx , contrib .GetLogin (), nil )
75+ for contributor := range maps .Values (contributors ) {
76+ orgs , _ , err := handler .ghClient .Organizations .List (handler .ctx , contributor .Login , nil )
6777 // This call can fail due to token scopes. So ignore error.
6878 if err == nil {
6979 for _ , org := range orgs {
@@ -72,20 +82,94 @@ func (handler *contributorsHandler) setup() error {
7282 })
7383 }
7484 }
75- user , _ , err := handler .ghClient .Users .Get (handler .ctx , contrib . GetLogin () )
85+ user , _ , err := handler .ghClient .Users .Get (handler .ctx , contributor . Login )
7686 if err != nil {
7787 handler .errSetup = fmt .Errorf ("error during Users.Get: %w" , err )
7888 }
7989 contributor .Companies = append (contributor .Companies , user .GetCompany ())
8090 handler .contributors = append (handler .contributors , contributor )
8191 }
92+
8293 handler .errSetup = nil
8394 })
8495 return handler .errSetup
8596}
8697
87- func (handler * contributorsHandler ) getContributors () ([]clients.User , error ) {
88- if err := handler .setup (); err != nil {
98+ func mapContributors (handler * contributorsHandler , contributors map [string ]clients.User ) {
99+ // getting contributors from the github API
100+ contribs , _ , err := handler .ghClient .Repositories .ListContributors (
101+ handler .ctx , handler .repourl .owner , handler .repourl .repo , & github.ListContributorsOptions {})
102+ if err != nil {
103+ handler .errSetup = fmt .Errorf ("error during ListContributors: %w" , err )
104+ return
105+ }
106+
107+ // adding contributors to contributor map
108+ for _ , contrib := range contribs {
109+ if contrib .GetLogin () == "" {
110+ continue
111+ }
112+ contributors [contrib .GetLogin ()] = clients.User {
113+ Login : contrib .GetLogin (), NumContributions : contrib .GetContributions (),
114+ IsCodeOwner : false ,
115+ }
116+ }
117+ }
118+
119+ func mapCodeOwners (handler * contributorsHandler , codeOwnerFile io.ReadCloser , contributors map [string ]clients.User ) {
120+ ruleset , err := codeowners .ParseFile (codeOwnerFile )
121+ if err != nil {
122+ handler .errSetup = fmt .Errorf ("error during ParseFile: %w" , err )
123+ return
124+ }
125+
126+ // expanding owners
127+ owners := make ([]* clients.User , 0 )
128+ for _ , rule := range ruleset {
129+ for _ , owner := range rule .Owners {
130+ switch owner .Type {
131+ case codeowners .UsernameOwner :
132+ // if usernameOwner just add to owners list
133+ owners = append (owners , & clients.User {Login : owner .Value , NumContributions : 0 , IsCodeOwner : true })
134+ case codeowners .TeamOwner :
135+ // if teamOwner expand and add to owners list (only accessible by org members with read:org token scope)
136+ splitTeam := strings .Split (owner .Value , "/" )
137+ if len (splitTeam ) == 2 {
138+ users , response , err := handler .ghClient .Teams .ListTeamMembersBySlug (
139+ handler .ctx ,
140+ splitTeam [0 ],
141+ splitTeam [1 ],
142+ & github.TeamListTeamMembersOptions {},
143+ )
144+ if err == nil && response .StatusCode == http .StatusOK {
145+ for _ , user := range users {
146+ owners = append (owners , & clients.User {Login : user .GetLogin (), NumContributions : 0 , IsCodeOwner : true })
147+ }
148+ }
149+ }
150+ }
151+ }
152+ }
153+
154+ // adding owners to contributor map and deduping
155+ for _ , owner := range owners {
156+ if owner .Login == "" {
157+ continue
158+ }
159+ value , ok := contributors [owner .Login ]
160+ if ok {
161+ // if contributor exists already set IsCodeOwner to true
162+ value .IsCodeOwner = true
163+ contributors [owner .Login ] = value
164+ } else {
165+ // otherwise add new contributor
166+ contributors [owner .Login ] = * owner
167+ }
168+ }
169+ }
170+
171+ func (handler * contributorsHandler ) getContributors (codeOwnerFile io.ReadCloser ) ([]clients.User , error ) {
172+ if err := handler .setup (codeOwnerFile ); err != nil {
89173 return nil , fmt .Errorf ("error during contributorsHandler.setup: %w" , err )
90174 }
91175 return handler .contributors , nil
0 commit comments