1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package reviewer
16
17 import (
18 "context"
19 "fmt"
20 "math/rand"
21 "sort"
22
23 "github.com/pkg/errors"
24 "github.com/rs/zerolog"
25
26 "edge-infra.dev/pkg/f8n/devinfra/repo/owners/policybot/pull"
27
28 "edge-infra.dev/pkg/f8n/devinfra/repo/owners/policybot/policy/common"
29 )
30
31 const (
32 LogKeyLeafNode = "leaf_node"
33 )
34
35 type Selection struct {
36 Users []string
37 Teams []string
38 }
39
40
41
42
43 func (s Selection) Difference(reviewers []*pull.Reviewer) Selection {
44 users := make(map[string]bool)
45 teams := make(map[string]bool)
46 for _, r := range reviewers {
47 switch r.Type {
48 case pull.ReviewerUser:
49 users[r.Name] = true
50 case pull.ReviewerTeam:
51 teams[r.Name] = true
52 }
53 }
54
55 var newUsers []string
56 for _, u := range s.Users {
57 if !users[u] {
58 newUsers = append(newUsers, u)
59 }
60 }
61
62 var newTeams []string
63 for _, t := range s.Teams {
64 if !teams[t] {
65 newTeams = append(newTeams, t)
66 }
67 }
68
69 return Selection{
70 Users: newUsers,
71 Teams: newTeams,
72 }
73 }
74
75
76 func (s Selection) IsEmpty() bool {
77 return len(s.Users) == 0 && len(s.Teams) == 0
78 }
79
80
81 func FindRequests(result *common.Result) []*common.Result {
82 if result.Status != common.StatusPending {
83 return nil
84 }
85
86 var reqs []*common.Result
87 for _, c := range result.Children {
88 reqs = append(reqs, FindRequests(c)...)
89 }
90 if len(result.Children) == 0 && result.ReviewRequestRule != nil && result.Error == nil {
91 reqs = append(reqs, result)
92 }
93 return reqs
94 }
95
96
97 func selectRandomUsers(n int, users []string, r *rand.Rand) []string {
98 var selections []string
99 if n == 0 {
100 return selections
101 }
102 if n >= len(users) {
103 return users
104 }
105
106 selected := make(map[int]bool)
107 for i := 0; i < n; i++ {
108 j := 0
109 for {
110
111 if j > n*5 {
112
113 panic(fmt.Sprintf("failed to select random value for %d %d", n, len(users)))
114 }
115 m := r.Intn(len(users))
116 if !selected[m] {
117 selected[m] = true
118 selections = append(selections, users[m])
119 break
120 }
121 j++
122 }
123 }
124 return selections
125 }
126
127 func selectTeamMembers(prctx pull.Context, allTeams []string) (map[string][]string, error) {
128 var allTeamsMembers = make(map[string][]string)
129 for _, team := range allTeams {
130 teamMembers, err := prctx.TeamMembers(team)
131 if err != nil {
132 return nil, errors.Wrapf(err, "failed to get member listing for team %s", team)
133 }
134 allTeamsMembers[team] = teamMembers
135 }
136
137 return allTeamsMembers, nil
138 }
139
140 func selectOrgMembers(prctx pull.Context, allOrgs []string) ([]string, error) {
141 var allOrgsMembers []string
142 for _, org := range allOrgs {
143 orgMembers, err := prctx.OrganizationMembers(org)
144 if err != nil {
145 return nil, errors.Wrapf(err, "failed to get member listing for org %s", org)
146 }
147 allOrgsMembers = append(allOrgsMembers, orgMembers...)
148 }
149
150 return allOrgsMembers, nil
151 }
152
153 func getPossibleReviewers(prctx pull.Context, users map[string]struct{}, collaborators []*pull.Collaborator) []string {
154 var possibleReviewers []string
155 for _, c := range collaborators {
156 _, exists := users[c.Name]
157 if c.Name != prctx.Author() && exists {
158 possibleReviewers = append(possibleReviewers, c.Name)
159 }
160 }
161
162
163
164 sort.Strings(possibleReviewers)
165 return possibleReviewers
166 }
167
168 func SelectReviewers(ctx context.Context, prctx pull.Context, results []*common.Result, r *rand.Rand) (Selection, error) {
169 selection := Selection{}
170
171 for _, child := range results {
172 logger := zerolog.Ctx(ctx).With().Str(LogKeyLeafNode, child.Name).Logger()
173 childCtx := logger.WithContext(ctx)
174
175 switch child.ReviewRequestRule.Mode {
176 case common.RequestModeTeams:
177 if err := selectTeamReviewers(childCtx, prctx, &selection, child); err != nil {
178 return selection, err
179 }
180 case common.RequestModeAllUsers, common.RequestModeRandomUsers:
181 if err := selectUserReviewers(childCtx, prctx, &selection, child, r); err != nil {
182 return selection, err
183 }
184 default:
185 return selection, fmt.Errorf("unknown reviewer selection mode: %s", child.ReviewRequestRule.Mode)
186 }
187 }
188 return selection, nil
189 }
190
191 func selectTeamReviewers(ctx context.Context, prctx pull.Context, selection *Selection, result *common.Result) error {
192 logger := zerolog.Ctx(ctx)
193
194 eligibleTeams, err := prctx.Teams()
195 if err != nil {
196 return err
197 }
198
199 var teams []string
200 for team, perm := range eligibleTeams {
201 switch {
202 case requestsTeam(result, prctx.RepositoryOwner()+"/"+team):
203 teams = append(teams, team)
204 case requestsPermission(result, perm):
205 teams = append(teams, team)
206 }
207 }
208
209 logger.Debug().Msgf("Requesting %d teams for review", len(teams))
210 selection.Teams = append(selection.Teams, teams...)
211 return nil
212 }
213
214 func selectUserReviewers(ctx context.Context, prctx pull.Context, selection *Selection, result *common.Result, r *rand.Rand) error {
215 logger := zerolog.Ctx(ctx)
216
217 allUsers := make(map[string]struct{})
218 for _, user := range result.ReviewRequestRule.Users {
219 allUsers[user] = struct{}{}
220 }
221
222 if len(result.ReviewRequestRule.Teams) > 0 {
223 logger.Debug().Msg("Selecting from teams for review")
224 teamsToUsers, err := selectTeamMembers(prctx, result.ReviewRequestRule.Teams)
225 if err != nil {
226 logger.Warn().Err(err).Msgf("failed to get member listing for teams, skipping team member selection")
227 }
228 for _, users := range teamsToUsers {
229 for _, user := range users {
230 allUsers[user] = struct{}{}
231 }
232 }
233 }
234
235 if len(result.ReviewRequestRule.Organizations) > 0 {
236 logger.Debug().Msg("Selecting from organizations for review")
237 orgMembers, err := selectOrgMembers(prctx, result.ReviewRequestRule.Organizations)
238 if err != nil {
239 logger.Warn().Err(err).Msg("failed to get member listing for org, skipping org member selection")
240 }
241 for _, user := range orgMembers {
242 allUsers[user] = struct{}{}
243 }
244 }
245
246 collaborators, err := prctx.RepositoryCollaborators()
247 if err != nil {
248 return errors.Wrap(err, "failed to list repository collaborators")
249 }
250
251 if len(result.ReviewRequestRule.Permissions) > 0 {
252 logger.Debug().Msg("Selecting from collaborators by permission for review")
253 for _, c := range collaborators {
254 for _, cp := range c.Permissions {
255 if cp.ViaRepo && requestsPermission(result, cp.Permission) {
256 allUsers[c.Name] = struct{}{}
257 }
258 }
259 }
260 }
261
262 possibleReviewers := getPossibleReviewers(prctx, allUsers, collaborators)
263 if len(possibleReviewers) == 0 {
264 logger.Debug().Msg("Found 0 eligible reviewers; skipping review request")
265 return nil
266 }
267
268 switch result.ReviewRequestRule.Mode {
269 case common.RequestModeAllUsers:
270 logger.Debug().Msgf("Found %d eligible reviewers; selecting all", len(possibleReviewers))
271 selection.Users = append(selection.Users, possibleReviewers...)
272
273 case common.RequestModeRandomUsers:
274 count := result.ReviewRequestRule.RequiredCount
275 selectedUsers := selectRandomUsers(count, possibleReviewers, r)
276
277 logger.Debug().Msgf("Found %d eligible reviewers; randomly selecting %d", len(possibleReviewers), count)
278 selection.Users = append(selection.Users, selectedUsers...)
279 }
280 return nil
281 }
282
283 func requestsTeam(r *common.Result, team string) bool {
284 for _, t := range r.ReviewRequestRule.Teams {
285 if t == team {
286 return true
287 }
288 }
289 return false
290 }
291
292 func requestsPermission(r *common.Result, perm pull.Permission) bool {
293 for _, p := range r.ReviewRequestRule.Permissions {
294 if p == perm {
295 return true
296 }
297 }
298 return false
299 }
300
View as plain text