-
Notifications
You must be signed in to change notification settings - Fork 0
/
gitlab_util.go
457 lines (388 loc) · 12.1 KB
/
gitlab_util.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
// This file provides utility functions related to the Gitlab REST API.
package gitlab_util
import (
"fmt"
"hash/crc64"
"regexp"
"slices"
"strconv"
"time"
"github.com/xanzy/go-gitlab"
)
////////////////////////////////////////////////////////////////////////
// Groups
////////////////////////////////////////////////////////////////////////
// GroupFullPaths returns just the full paths for the groups.
func GroupFullPaths(groups []*gitlab.Group) []string {
result := make([]string, 0, len(groups))
for _, group := range groups {
result = append(result, group.FullPath)
}
return result
}
// FindExactGroup returns the ID of the group that exactly matches
// the search string.
func FindExactGroup(s *gitlab.GroupsService, group string) (*gitlab.Group, error) {
// If "group" is an integer, it is a group ID which requires
// different processing.
groupID, err := strconv.Atoi(group)
if err == nil {
opts := gitlab.GetGroupOptions{}
g, _, err := s.GetGroup(groupID, &opts)
if err != nil {
return nil, err
}
return g, nil
}
err = nil
// Set the group search string.
opts := gitlab.ListGroupsOptions{}
opts.Page = 1
opts.Search = gitlab.Ptr(group)
// Iterate over each page of groups.
for {
// Get a page of matching groups.
gs, resp, err := s.ListGroups(&opts)
if err != nil {
err = fmt.Errorf("FindExactGroup: %w", err)
return nil, err
}
// Check each group for an exact match.
for _, g := range gs {
if g.FullPath == group {
return g, nil
}
}
// Check if done.
if resp.NextPage == 0 {
break
}
// Move to the next page.
opts.Page = resp.NextPage
}
// Could not find a matching group.
err = fmt.Errorf(
"FindExactGroup: could not find exact match for group: %q", group)
return nil, err
}
////////////////////////////////////////////////////////////////////////
// Projects
////////////////////////////////////////////////////////////////////////
// ForEachProjectInGroup iterates over the projects in a group and
// recursively or not) calls the function f once for each project
// whose full path name matches the regular expression. An empty
// regular expression matches any string. The function f must return
// true and no error to indicate that it wants to continue being
// called with the remaining projects. If f returns an error, it will
// be forwarded to the caller as the error return value for this
// function. Prefer this function over GetAllProjects() to avoid the
// long delay to the user while waiting to collect all the projects.
func ForEachProjectInGroup(
s *gitlab.GroupsService,
group string,
expr string,
recursive bool,
f func(group *gitlab.Group, project *gitlab.Project) (bool, error),
) error {
// Find the group.
g, err := FindExactGroup(s, group)
if err != nil {
return fmt.Errorf("ForEachProjectInGroup: %w", err)
}
// Compile the regexp.
r, err := regexp.Compile(expr)
if err != nil {
return fmt.Errorf("ForEachProjectInGroup: %w", err)
}
// Set up the options for ListGroupProjects().
opts := gitlab.ListGroupProjectsOptions{}
opts.IncludeSubGroups = gitlab.Ptr(recursive)
opts.Page = 1
///opts.PerPage = 100
// Iterate over each page of groups.
for {
// Get the next page of projects.
ps, resp, err := s.ListGroupProjects(g.ID, &opts)
if err != nil {
return fmt.Errorf("ForEachProjectInGroup: %w\n", err)
}
// Invoke the callback if the full path to the project matches
// the regular expression.
for _, p := range ps {
if r.MatchString(p.PathWithNamespace) {
more, err := f(g, p)
if err != nil {
return err
}
if !more {
return nil
}
}
}
// Check if done.
if resp.NextPage == 0 {
break
}
// Move to the next page.
opts.Page = resp.NextPage
}
return nil
}
// GetAllProjects returns all the projects in a group recursively (or
// not) for each project whose full path name matches the regular
// expression. An empty regular expression matches any string.
// Prefer ForEachProjectInGroup() over this function to avoid the long
// delay while waiting to collect all the projects. The main reason
// to use this function is when deleting projects because Gitlab's
// paging gets confused because Gitlab's paging is relative to when
// you make the request for the next page, not when you made the
// request for the first page, and deleting projects necessarily
// changes the page on which some remaining projects appear. This
// function is better to use when deleting projects because it
// collects all the projects up front allowing the caller to delete
// them with impunity because there will be no next page to get.
func GetAllProjects(
s *gitlab.GroupsService,
group string,
expr string,
recursive bool,
) ([]*gitlab.Project, error) {
var result []*gitlab.Project
// Callback function used to collect all of the projects.
f := func(group *gitlab.Group, project *gitlab.Project) (bool, error) {
result = append(result, project)
return true, nil
}
// Collect all the projects.
err := ForEachProjectInGroup(s, group, expr, recursive, f)
if err != nil {
return nil, fmt.Errorf("GetAllProjects: %w", err)
}
return result, nil
}
////////////////////////////////////////////////////////////////////////
// Approval Rules
////////////////////////////////////////////////////////////////////////
// GetApprovalRuleUsernames returns the sorted list of usernames for
// the given approval rule.
func GetApprovalRuleUsernames(rule *gitlab.ProjectApprovalRule) []string {
var usernames []string
// Extract the usernames of the eligible approvers.
for _, u := range rule.Users {
usernames = append(usernames, u.Username)
}
// Sort the usernames.
slices.Sort(usernames)
return usernames
}
// ApprovalRuleToString converts the approval rule into a
// human-readable string.
func ApprovalRuleToString(rule *gitlab.ProjectApprovalRule) string {
// Get the users for the approval rule. Note that this does *not*
// include any groups that might also be part of the approval rule.
usernames := GetApprovalRuleUsernames(rule)
// Get the string representation of the list of usernames.
usernamesAsString := fmt.Sprintf("%q", usernames)
// Calculate the CRC-64 checksum of the usernames string.
cksum := crc64.Checksum(
[]byte(usernamesAsString),
crc64.MakeTable(crc64.ISO))
// Add rule ID and name.
return fmt.Sprintf("%#016x %8d %-16s %s",
cksum, rule.ID, rule.Name, usernamesAsString)
}
// UpdateApprovalRule updates the approval rule for the project to
// have the same values as before except with a new list of user IDs.
// This function is designed to be the callback for
// [ForEachApprovalRuleInProject()].
func UpdateApprovalRule(
s *gitlab.ProjectsService,
projectID int,
rule *gitlab.ProjectApprovalRule,
userIDs []int,
) (
*gitlab.ProjectApprovalRule,
error,
){
var err error
var newRule *gitlab.ProjectApprovalRule
// Extract the existing group IDs.
var groupIDs []int
for _, group := range rule.Groups {
groupIDs = append(groupIDs, group.ID)
}
// Extract the existing branch IDs.
var branchIDs []int
for _, branch := range rule.ProtectedBranches {
branchIDs = append(branchIDs, branch.ID)
}
// Set update options.
opts := gitlab.UpdateProjectLevelRuleOptions{
Name: gitlab.Ptr(rule.Name),
ApprovalsRequired: gitlab.Ptr(rule.ApprovalsRequired),
UserIDs: &userIDs,
GroupIDs: &groupIDs,
ProtectedBranchIDs: &branchIDs,
AppliesToAllProtectedBranches: gitlab.Ptr(rule.AppliesToAllProtectedBranches),
}
// Update the approval rule.
newRule, _, err = s.UpdateProjectApprovalRule(projectID, rule.ID, &opts)
return newRule, err
}
// ApprovalRulesGetter is an abstraction of GetProjectApprovalRules()
// in gitlab.ProjectsService which was added so
// ForEachApprovalRuleInProject() can be tested with requiring a paid
// Gitlab account because Gitlab CE (the free version of Gitlab) does
// not support approval rules.
type ApprovalRulesGetter interface {
GetProjectApprovalRules(
pid interface{},
opt *gitlab.GetProjectApprovalRulesListsOptions,
options ...gitlab.RequestOptionFunc,
) ([]*gitlab.ProjectApprovalRule, *gitlab.Response, error)
}
// ForEachApprovalRuleInProject iterates over the approval rules in a
// project and calls the function f once for each approval rule. The
// function f must return true and no error to indicate that it wants
// to continue being called with the remaining projects. If f returns
// an error, it will be forwarded to the caller as the error return
// value for this function.
func ForEachApprovalRuleInProject(
s ApprovalRulesGetter, /* was *gitlab.ProjectsService */
p *gitlab.Project,
f func(
approvalRule *gitlab.ProjectApprovalRule,
) (bool, error),
) error {
// Set up the options for ListGroupProjects().
opts := gitlab.GetProjectApprovalRulesListsOptions{}
opts.Page = 1
///opts.PerPage = 100
// Iterate over each page of approval rules.
for {
// Get the next page of approval rules.
rules, resp, err := s.GetProjectApprovalRules(p.ID, &opts)
if err != nil {
return fmt.Errorf("ForEachApprovalRuleInProject: %w\n", err)
}
// Invoke the callbacks.
for _, rule := range rules {
more, err := f(rule)
if err != nil {
return err
}
if !more {
return nil
}
}
// Check if done.
if resp.NextPage == 0 {
break
}
// Move to the next page.
opts.Page = resp.NextPage
}
return nil
}
////////////////////////////////////////////////////////////////////////
// Users
////////////////////////////////////////////////////////////////////////
// FindUser search for the user and returns the user that exactly
// matches the search string or all substring matches if exact is
// false. The search string can be the user ID, name, username or
// e-mail address of the user. If the search string is a user ID, the
// exact flag is ignored, and only the exact user with that ID will be
// returned.
func FindUsers(
s *gitlab.UsersService,
user string,
exact bool,
date time.Time,
) ([]*gitlab.User, error) {
var err error
var matches []*gitlab.User
var u *gitlab.User
var userID int
// If "user" is an integer, it is a user ID which requires
// different processing.
userID, err = strconv.Atoi(user)
if err == nil {
opts := gitlab.GetUsersOptions{}
u, _, err = s.GetUser(userID, opts)
if err != nil {
return nil, err
}
return []*gitlab.User{u}, nil
}
err = nil
// Iterate over all the users that match the "user" string.
err = ForEachUser(s, user, date, func(u *gitlab.User) (bool, error) {
if !exact || u.Email == user || u.Username == user || u.Name == user {
matches = append(matches, u)
}
return true, nil
})
if err != nil {
return nil, err
}
if len(matches) == 0 {
return nil, fmt.Errorf("no match found for user: %q", user)
}
if exact && len(matches) > 1 {
var usernames []string
for _, match := range matches {
usernames = append(usernames, match.Username)
}
return nil, fmt.Errorf("multiple exact matches found: %q", usernames)
}
return matches, nil
}
// ForEachUser iterates over users calling the function f once for
// each user matching the search string. An empty search string
// matches all users. The search string can be the name, username, or
// e-mail address of the user. The function f must return true and no
// error to indicate that it wants to continue being called with the
// remaining users. If f returns an error, it will be forwarded to
// the caller as the error return value for this function.
//
// Also see [FindExactUser()].
func ForEachUser(
s *gitlab.UsersService,
user string,
date time.Time,
f func(user *gitlab.User) (bool, error),
) error {
// Set up the options for ListUsers().
opts := gitlab.ListUsersOptions{}
opts.CreatedAfter = &date
if user != "" {
opts.Search = &user
}
opts.Page = 1
///opts.PerPage = 100
// Iterate over each page of users.
for {
// Get the next page of users.
users, resp, err := s.ListUsers(&opts)
if err != nil {
return fmt.Errorf("ForEachUser: %w\n", err)
}
// Invoke the callback for each user.
for _, user := range users {
more, err := f(user)
if err != nil {
return err
}
if !more {
return nil
}
}
// Check if done.
if resp.NextPage == 0 {
break
}
// Move to the next page.
opts.Page = resp.NextPage
}
return nil
}