• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jfrog / froggit-go / 15705256363

17 Jun 2025 10:50AM UTC coverage: 84.922% (-0.2%) from 85.167%
15705256363

Pull #156

github

web-flow
Add files via upload

List app repositories
Create pull request with response body
Pull Request #156: Add files via upload

38 of 65 new or added lines in 5 files covered. (58.46%)

3 existing lines in 3 files now uncovered.

4410 of 5193 relevant lines covered (84.92%)

6.47 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.96
/vcsclient/github.go
1
package vcsclient
2

3
import (
4
        "context"
5
        "crypto/rand"
6
        base64Utils "encoding/base64"
7
        "encoding/json"
8
        "errors"
9
        "fmt"
10
        "github.com/google/go-github/v56/github"
11
        "github.com/grokify/mogo/encoding/base64"
12
        "github.com/jfrog/froggit-go/vcsutils"
13
        "github.com/jfrog/gofrog/datastructures"
14
        "github.com/mitchellh/mapstructure"
15
        "golang.org/x/crypto/nacl/box"
16
        "golang.org/x/exp/slices"
17
        "golang.org/x/oauth2"
18
        "io"
19
        "net/http"
20
        "net/url"
21
        "path/filepath"
22
        "sort"
23
        "strconv"
24
        "strings"
25
        "time"
26
)
27

28
const (
29
        maxRetries               = 5
30
        retriesIntervalMilliSecs = 60000
31
        // https://github.com/orgs/community/discussions/27190
32
        githubPrContentSizeLimit = 65536
33
        // The maximum number of reviewers that can be added to a GitHub environment
34
        ghMaxEnvReviewers = 6
35
        regularFileCode   = "100644"
36
)
37

38
var rateLimitRetryStatuses = []int{http.StatusForbidden, http.StatusTooManyRequests}
39

40
type GitHubRateLimitExecutionHandler func() (*github.Response, error)
41

42
type GitHubRateLimitRetryExecutor struct {
43
        vcsutils.RetryExecutor
44
        GitHubRateLimitExecutionHandler
45
}
46

47
func (ghe *GitHubRateLimitRetryExecutor) Execute() error {
107✔
48
        ghe.ExecutionHandler = func() (bool, error) {
214✔
49
                ghResponse, err := ghe.GitHubRateLimitExecutionHandler()
107✔
50
                return shouldRetryIfRateLimitExceeded(ghResponse, err), err
107✔
51
        }
107✔
52
        return ghe.RetryExecutor.Execute()
107✔
53
}
54

55
// GitHubClient API version 3
56
type GitHubClient struct {
57
        vcsInfo                VcsInfo
58
        rateLimitRetryExecutor GitHubRateLimitRetryExecutor
59
        logger                 vcsutils.Log
60
        ghClient               *github.Client
61
}
62

63
// NewGitHubClient create a new GitHubClient
64
func NewGitHubClient(vcsInfo VcsInfo, logger vcsutils.Log) (*GitHubClient, error) {
141✔
65
        ghClient, err := buildGithubClient(vcsInfo, logger)
141✔
66
        if err != nil {
141✔
67
                return nil, err
×
68
        }
×
69
        return &GitHubClient{
141✔
70
                        vcsInfo:  vcsInfo,
141✔
71
                        logger:   logger,
141✔
72
                        ghClient: ghClient,
141✔
73
                        rateLimitRetryExecutor: GitHubRateLimitRetryExecutor{RetryExecutor: vcsutils.RetryExecutor{
141✔
74
                                Logger:                   logger,
141✔
75
                                MaxRetries:               maxRetries,
141✔
76
                                RetriesIntervalMilliSecs: retriesIntervalMilliSecs},
141✔
77
                        }},
141✔
78
                nil
141✔
79
}
80

81
func (client *GitHubClient) runWithRateLimitRetries(handler func() (*github.Response, error)) error {
107✔
82
        client.rateLimitRetryExecutor.GitHubRateLimitExecutionHandler = handler
107✔
83
        return client.rateLimitRetryExecutor.Execute()
107✔
84
}
107✔
85

86
// TestConnection on GitHub
87
func (client *GitHubClient) TestConnection(ctx context.Context) error {
4✔
88
        _, _, err := client.ghClient.Meta.Zen(ctx)
4✔
89
        return err
4✔
90
}
4✔
91

92
func buildGithubClient(vcsInfo VcsInfo, logger vcsutils.Log) (*github.Client, error) {
141✔
93
        httpClient := &http.Client{}
141✔
94
        if vcsInfo.Token != "" {
200✔
95
                httpClient = oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(&oauth2.Token{AccessToken: vcsInfo.Token}))
59✔
96
        }
59✔
97
        ghClient := github.NewClient(httpClient)
141✔
98
        if vcsInfo.APIEndpoint != "" {
248✔
99
                baseURL, err := url.Parse(strings.TrimSuffix(vcsInfo.APIEndpoint, "/") + "/")
107✔
100
                if err != nil {
107✔
101
                        return nil, err
×
102
                }
×
103
                logger.Info("Using API endpoint:", baseURL)
107✔
104
                ghClient.BaseURL = baseURL
107✔
105
        }
106
        return ghClient, nil
141✔
107
}
108

109
// AddSshKeyToRepository on GitHub
110
func (client *GitHubClient) AddSshKeyToRepository(ctx context.Context, owner, repository, keyName, publicKey string, permission Permission) error {
8✔
111
        err := validateParametersNotBlank(map[string]string{
8✔
112
                "owner":      owner,
8✔
113
                "repository": repository,
8✔
114
                "key name":   keyName,
8✔
115
                "public key": publicKey,
8✔
116
        })
8✔
117
        if err != nil {
13✔
118
                return err
5✔
119
        }
5✔
120

121
        readOnly := permission != ReadWrite
3✔
122
        key := github.Key{
3✔
123
                Key:      &publicKey,
3✔
124
                Title:    &keyName,
3✔
125
                ReadOnly: &readOnly,
3✔
126
        }
3✔
127

3✔
128
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
129
                _, ghResponse, err := client.ghClient.Repositories.CreateKey(ctx, owner, repository, &key)
3✔
130
                return ghResponse, err
3✔
131
        })
3✔
132
}
133

134
// ListRepositories on GitHub
135
func (client *GitHubClient) ListRepositories(ctx context.Context) (results map[string][]string, err error) {
5✔
136
        results = make(map[string][]string)
5✔
137
        for nextPage := 1; ; nextPage++ {
11✔
138
                var repositoriesInPage []*github.Repository
6✔
139
                var ghResponse *github.Response
6✔
140
                err = client.runWithRateLimitRetries(func() (*github.Response, error) {
12✔
141
                        repositoriesInPage, ghResponse, err = client.executeListRepositoriesInPage(ctx, nextPage)
6✔
142
                        return ghResponse, err
6✔
143
                })
6✔
144
                if err != nil {
8✔
145
                        return
2✔
146
                }
2✔
147

148
                for _, repo := range repositoriesInPage {
37✔
149
                        results[*repo.Owner.Login] = append(results[*repo.Owner.Login], *repo.Name)
33✔
150
                }
33✔
151
                if nextPage+1 > ghResponse.LastPage {
7✔
152
                        break
3✔
153
                }
154
        }
155
        return
3✔
156
}
157

158
func (client *GitHubClient) executeListRepositoriesInPage(ctx context.Context, page int) ([]*github.Repository, *github.Response, error) {
6✔
159
        options := &github.RepositoryListOptions{ListOptions: github.ListOptions{Page: page}}
6✔
160
        return client.ghClient.Repositories.List(ctx, "", options)
6✔
161
}
6✔
162

163
// ListAppRepositories returns a list of generic repository objects
164
func (client *GitHubClient) ListAppRepositories(ctx context.Context) (string, error) {
2✔
165
        var allRepositories []*github.Repository
2✔
166
        for nextPage := 1; ; nextPage++ {
4✔
167
                var repositoriesInPage *github.ListRepositories
2✔
168
                var ghResponse *github.Response
2✔
169
                var err error
2✔
170
                err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
171
                        repositoriesInPage, ghResponse, err = client.ghClient.Apps.ListRepos(ctx, &github.ListOptions{Page: nextPage})
2✔
172
                        return ghResponse, err
2✔
173
                })
2✔
174
                if err != nil {
3✔
175
                        return "", err
1✔
176
                }
1✔
177
                allRepositories = append(allRepositories, repositoriesInPage.Repositories...)
1✔
178
                if nextPage+1 > ghResponse.LastPage {
2✔
179
                        break
1✔
180
                }
181
        }
182

183
        json, err := json.Marshal(allRepositories)
1✔
184
        if err != nil {
1✔
NEW
185
                client.logger.Warn("Failed to marshal repositories to JSON:", err)
×
NEW
186
                return "", err
×
NEW
187
        }
×
188

189
        return string(json), nil
1✔
190
}
191

192
// ListBranches on GitHub
193
func (client *GitHubClient) ListBranches(ctx context.Context, owner, repository string) (branchList []string, err error) {
2✔
194
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
195
                var ghResponse *github.Response
2✔
196
                branchList, ghResponse, err = client.executeListBranch(ctx, owner, repository)
2✔
197
                return ghResponse, err
2✔
198
        })
2✔
199
        return
2✔
200
}
201

202
func (client *GitHubClient) executeListBranch(ctx context.Context, owner, repository string) ([]string, *github.Response, error) {
2✔
203
        branches, ghResponse, err := client.ghClient.Repositories.ListBranches(ctx, owner, repository, nil)
2✔
204
        if err != nil {
3✔
205
                return []string{}, ghResponse, err
1✔
206
        }
1✔
207

208
        branchList := make([]string, 0, len(branches))
1✔
209
        for _, branch := range branches {
3✔
210
                branchList = append(branchList, *branch.Name)
2✔
211
        }
2✔
212
        return branchList, ghResponse, nil
1✔
213
}
214

215
// CreateWebhook on GitHub
216
func (client *GitHubClient) CreateWebhook(ctx context.Context, owner, repository, _, payloadURL string,
217
        webhookEvents ...vcsutils.WebhookEvent) (string, string, error) {
2✔
218
        token := vcsutils.CreateToken()
2✔
219
        hook := createGitHubHook(token, payloadURL, webhookEvents...)
2✔
220
        var ghResponseHook *github.Hook
2✔
221
        var err error
2✔
222
        if err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
223
                var ghResponse *github.Response
2✔
224
                ghResponseHook, ghResponse, err = client.ghClient.Repositories.CreateHook(ctx, owner, repository, hook)
2✔
225
                return ghResponse, err
2✔
226
        }); err != nil {
3✔
227
                return "", "", err
1✔
228
        }
1✔
229

230
        return strconv.FormatInt(*ghResponseHook.ID, 10), token, nil
1✔
231
}
232

233
// UpdateWebhook on GitHub
234
func (client *GitHubClient) UpdateWebhook(ctx context.Context, owner, repository, _, payloadURL, token,
235
        webhookID string, webhookEvents ...vcsutils.WebhookEvent) error {
2✔
236
        webhookIDInt64, err := strconv.ParseInt(webhookID, 10, 64)
2✔
237
        if err != nil {
2✔
238
                return err
×
239
        }
×
240

241
        hook := createGitHubHook(token, payloadURL, webhookEvents...)
2✔
242
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
243
                var ghResponse *github.Response
2✔
244
                _, ghResponse, err = client.ghClient.Repositories.EditHook(ctx, owner, repository, webhookIDInt64, hook)
2✔
245
                return ghResponse, err
2✔
246
        })
2✔
247
}
248

249
// DeleteWebhook on GitHub
250
func (client *GitHubClient) DeleteWebhook(ctx context.Context, owner, repository, webhookID string) error {
2✔
251
        webhookIDInt64, err := strconv.ParseInt(webhookID, 10, 64)
2✔
252
        if err != nil {
3✔
253
                return err
1✔
254
        }
1✔
255

256
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
2✔
257
                return client.ghClient.Repositories.DeleteHook(ctx, owner, repository, webhookIDInt64)
1✔
258
        })
1✔
259
}
260

261
// SetCommitStatus on GitHub
262
func (client *GitHubClient) SetCommitStatus(ctx context.Context, commitStatus CommitStatus, owner, repository, ref,
263
        title, description, detailsURL string) error {
2✔
264
        state := getGitHubCommitState(commitStatus)
2✔
265
        status := &github.RepoStatus{
2✔
266
                Context:     &title,
2✔
267
                TargetURL:   &detailsURL,
2✔
268
                State:       &state,
2✔
269
                Description: &description,
2✔
270
        }
2✔
271

2✔
272
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
273
                _, ghResponse, err := client.ghClient.Repositories.CreateStatus(ctx, owner, repository, ref, status)
2✔
274
                return ghResponse, err
2✔
275
        })
2✔
276
}
277

278
// GetCommitStatuses on GitHub
279
func (client *GitHubClient) GetCommitStatuses(ctx context.Context, owner, repository, ref string) (statusInfoList []CommitStatusInfo, err error) {
6✔
280
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
12✔
281
                var ghResponse *github.Response
6✔
282
                statusInfoList, ghResponse, err = client.executeGetCommitStatuses(ctx, owner, repository, ref)
6✔
283
                return ghResponse, err
6✔
284
        })
6✔
285
        return
6✔
286
}
287

288
func (client *GitHubClient) executeGetCommitStatuses(ctx context.Context, owner, repository, ref string) (statusInfoList []CommitStatusInfo, ghResponse *github.Response, err error) {
6✔
289
        statuses, ghResponse, err := client.ghClient.Repositories.GetCombinedStatus(ctx, owner, repository, ref, nil)
6✔
290
        if err != nil {
10✔
291
                return
4✔
292
        }
4✔
293

294
        for _, singleStatus := range statuses.Statuses {
6✔
295
                statusInfoList = append(statusInfoList, CommitStatusInfo{
4✔
296
                        State:         commitStatusAsStringToStatus(*singleStatus.State),
4✔
297
                        Description:   singleStatus.GetDescription(),
4✔
298
                        DetailsUrl:    singleStatus.GetTargetURL(),
4✔
299
                        Creator:       singleStatus.GetCreator().GetName(),
4✔
300
                        LastUpdatedAt: singleStatus.GetUpdatedAt().Time,
4✔
301
                        CreatedAt:     singleStatus.GetCreatedAt().Time,
4✔
302
                })
4✔
303
        }
4✔
304
        return
2✔
305
}
306

307
// DownloadRepository on GitHub
308
func (client *GitHubClient) DownloadRepository(ctx context.Context, owner, repository, branch, localPath string) (err error) {
2✔
309
        // Get the archive download link from GitHub
2✔
310
        var baseURL *url.URL
2✔
311
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
312
                var ghResponse *github.Response
2✔
313
                baseURL, ghResponse, err = client.executeGetArchiveLink(ctx, owner, repository, branch)
2✔
314
                return ghResponse, err
2✔
315
        })
2✔
316
        if err != nil {
3✔
317
                return
1✔
318
        }
1✔
319

320
        // Download the archive
321
        httpResponse, err := executeDownloadArchiveFromLink(baseURL.String())
1✔
322
        if err != nil {
1✔
323
                return
×
324
        }
×
325
        defer func() { err = errors.Join(err, httpResponse.Body.Close()) }()
2✔
326
        client.logger.Info(repository, vcsutils.SuccessfulRepoDownload)
1✔
327

1✔
328
        // Untar the archive
1✔
329
        if err = vcsutils.Untar(localPath, httpResponse.Body, true); err != nil {
1✔
330
                return
×
331
        }
×
332
        client.logger.Info(vcsutils.SuccessfulRepoExtraction)
1✔
333

1✔
334
        repositoryInfo, err := client.GetRepositoryInfo(ctx, owner, repository)
1✔
335
        if err != nil {
1✔
336
                return
×
337
        }
×
338
        // Create a .git folder in the archive with the remote repository HTTP clone url
339
        err = vcsutils.CreateDotGitFolderWithRemote(localPath, vcsutils.RemoteName, repositoryInfo.CloneInfo.HTTP)
1✔
340
        return
1✔
341
}
342

343
func (client *GitHubClient) executeGetArchiveLink(ctx context.Context, owner, repository, branch string) (baseURL *url.URL, ghResponse *github.Response, err error) {
2✔
344
        client.logger.Debug("Getting GitHub archive link to download")
2✔
345
        return client.ghClient.Repositories.GetArchiveLink(ctx, owner, repository, github.Tarball,
2✔
346
                &github.RepositoryContentGetOptions{Ref: branch}, 5)
2✔
347
}
2✔
348

349
func executeDownloadArchiveFromLink(baseURL string) (*http.Response, error) {
1✔
350
        httpClient := &http.Client{}
1✔
351
        req, err := http.NewRequest(http.MethodGet, baseURL, nil)
1✔
352
        if err != nil {
1✔
353
                return nil, err
×
354
        }
×
355
        httpResponse, err := httpClient.Do(req)
1✔
356
        if err != nil {
1✔
357
                return httpResponse, err
×
358
        }
×
359
        return httpResponse, vcsutils.CheckResponseStatusWithBody(httpResponse, http.StatusOK)
1✔
360
}
361

362
func (client *GitHubClient) GetPullRequestCommentSizeLimit() int {
×
363
        return githubPrContentSizeLimit
×
364
}
×
365

366
func (client *GitHubClient) GetPullRequestDetailsSizeLimit() int {
×
367
        return githubPrContentSizeLimit
×
368
}
×
369

370
// CreatePullRequest on GitHub
371
func (client *GitHubClient) CreatePullRequest(ctx context.Context, owner, repository, sourceBranch, targetBranch, title, description string) error {
2✔
372
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
373
                _, response, err := client.executeCreatePullRequest(ctx, owner, repository, sourceBranch, targetBranch, title, description)
2✔
374
                return response, err
2✔
375
        })
2✔
376
}
377

378
func (client *GitHubClient) CreatePullRequestWithResponse(ctx context.Context, owner, repository, sourceBranch, targetBranch, title, description string) (string, error) {
2✔
379
        var pr *github.PullRequest
2✔
380
        var ghResponse *github.Response
2✔
381
        var err error
2✔
382
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
383
                pr, ghResponse, err = client.executeCreatePullRequest(ctx, owner, repository, sourceBranch, targetBranch, title, description)
2✔
384
                return ghResponse, err
2✔
385
        })
2✔
386
        if err != nil {
3✔
387
                return "", err
1✔
388
        }
1✔
389
        json, err := json.Marshal(pr)
1✔
390
        if err != nil {
1✔
NEW
391
                client.logger.Warn("Failed to marshal pull request to JSON:", err)
×
NEW
392
                return "", err
×
NEW
393
        }
×
394

395
        return string(json), nil
1✔
396
}
397

398
func (client *GitHubClient) executeCreatePullRequest(ctx context.Context, owner, repository, sourceBranch, targetBranch, title, description string) (*github.PullRequest, *github.Response, error) {
4✔
399
        head := owner + ":" + sourceBranch
4✔
400
        client.logger.Debug(vcsutils.CreatingPullRequest, title)
4✔
401

4✔
402
        ghPullRequest, ghResponse, err := client.ghClient.PullRequests.Create(ctx, owner, repository, &github.NewPullRequest{
4✔
403
                Title: &title,
4✔
404
                Body:  &description,
4✔
405
                Head:  &head,
4✔
406
                Base:  &targetBranch,
4✔
407
        })
4✔
408
        return ghPullRequest, ghResponse, err
4✔
409
}
4✔
410

411
// UpdatePullRequest on GitHub
412
func (client *GitHubClient) UpdatePullRequest(ctx context.Context, owner, repository, title, body, targetBranchName string, id int, state vcsutils.PullRequestState) error {
3✔
413
        client.logger.Debug(vcsutils.UpdatingPullRequest, id)
3✔
414
        var baseRef *github.PullRequestBranch
3✔
415
        if targetBranchName != "" {
5✔
416
                baseRef = &github.PullRequestBranch{Ref: &targetBranchName}
2✔
417
        }
2✔
418
        pullRequest := &github.PullRequest{
3✔
419
                Body:  &body,
3✔
420
                Title: &title,
3✔
421
                State: vcsutils.MapPullRequestState(&state),
3✔
422
                Base:  baseRef,
3✔
423
        }
3✔
424

3✔
425
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
426
                _, ghResponse, err := client.ghClient.PullRequests.Edit(ctx, owner, repository, id, pullRequest)
3✔
427
                return ghResponse, err
3✔
428
        })
3✔
429
}
430

431
// ListOpenPullRequestsWithBody on GitHub
432
func (client *GitHubClient) ListOpenPullRequestsWithBody(ctx context.Context, owner, repository string) ([]PullRequestInfo, error) {
1✔
433
        return client.getOpenPullRequests(ctx, owner, repository, true)
1✔
434
}
1✔
435

436
// ListOpenPullRequests on GitHub
437
func (client *GitHubClient) ListOpenPullRequests(ctx context.Context, owner, repository string) ([]PullRequestInfo, error) {
1✔
438
        return client.getOpenPullRequests(ctx, owner, repository, false)
1✔
439
}
1✔
440

441
func (client *GitHubClient) getOpenPullRequests(ctx context.Context, owner, repository string, withBody bool) ([]PullRequestInfo, error) {
2✔
442
        var pullRequests []*github.PullRequest
2✔
443
        client.logger.Debug(vcsutils.FetchingOpenPullRequests, repository)
2✔
444
        err := client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
445
                var ghResponse *github.Response
2✔
446
                var err error
2✔
447
                pullRequests, ghResponse, err = client.ghClient.PullRequests.List(ctx, owner, repository, &github.PullRequestListOptions{State: "open"})
2✔
448
                return ghResponse, err
2✔
449
        })
2✔
450
        if err != nil {
2✔
451
                return []PullRequestInfo{}, err
×
452
        }
×
453

454
        return mapGitHubPullRequestToPullRequestInfoList(pullRequests, withBody)
2✔
455
}
456

457
func (client *GitHubClient) GetPullRequestByID(ctx context.Context, owner, repository string, pullRequestId int) (PullRequestInfo, error) {
4✔
458
        var pullRequest *github.PullRequest
4✔
459
        var ghResponse *github.Response
4✔
460
        var err error
4✔
461
        client.logger.Debug(vcsutils.FetchingPullRequestById, repository)
4✔
462
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
8✔
463
                pullRequest, ghResponse, err = client.ghClient.PullRequests.Get(ctx, owner, repository, pullRequestId)
4✔
464
                return ghResponse, err
4✔
465
        })
4✔
466
        if err != nil {
7✔
467
                return PullRequestInfo{}, err
3✔
468
        }
3✔
469

470
        if err = vcsutils.CheckResponseStatusWithBody(ghResponse.Response, http.StatusOK); err != nil {
1✔
471
                return PullRequestInfo{}, err
×
472
        }
×
473

474
        return mapGitHubPullRequestToPullRequestInfo(pullRequest, false)
1✔
475
}
476

477
func mapGitHubPullRequestToPullRequestInfo(ghPullRequest *github.PullRequest, withBody bool) (PullRequestInfo, error) {
4✔
478
        var sourceBranch, targetBranch string
4✔
479
        var err1, err2 error
4✔
480
        if ghPullRequest != nil && ghPullRequest.Head != nil && ghPullRequest.Base != nil {
8✔
481
                sourceBranch, err1 = extractBranchFromLabel(vcsutils.DefaultIfNotNil(ghPullRequest.Head.Label))
4✔
482
                targetBranch, err2 = extractBranchFromLabel(vcsutils.DefaultIfNotNil(ghPullRequest.Base.Label))
4✔
483
                err := errors.Join(err1, err2)
4✔
484
                if err != nil {
4✔
485
                        return PullRequestInfo{}, err
×
486
                }
×
487
        }
488

489
        var sourceRepoName, sourceRepoOwner string
4✔
490
        if ghPullRequest.Head.Repo == nil {
4✔
491
                return PullRequestInfo{}, errors.New("the source repository information is missing when fetching the pull request details")
×
492
        }
×
493
        if ghPullRequest.Head.Repo.Owner == nil {
4✔
494
                return PullRequestInfo{}, errors.New("the source repository owner name is missing when fetching the pull request details")
×
495
        }
×
496
        sourceRepoName = vcsutils.DefaultIfNotNil(ghPullRequest.Head.Repo.Name)
4✔
497
        sourceRepoOwner = vcsutils.DefaultIfNotNil(ghPullRequest.Head.Repo.Owner.Login)
4✔
498

4✔
499
        var targetRepoName, targetRepoOwner string
4✔
500
        if ghPullRequest.Base.Repo == nil {
4✔
501
                return PullRequestInfo{}, errors.New("the target repository information is missing when fetching the pull request details")
×
502
        }
×
503
        if ghPullRequest.Base.Repo.Owner == nil {
4✔
504
                return PullRequestInfo{}, errors.New("the target repository owner name is missing when fetching the pull request details")
×
505
        }
×
506
        targetRepoName = vcsutils.DefaultIfNotNil(ghPullRequest.Base.Repo.Name)
4✔
507
        targetRepoOwner = vcsutils.DefaultIfNotNil(ghPullRequest.Base.Repo.Owner.Login)
4✔
508

4✔
509
        var body string
4✔
510
        if withBody {
5✔
511
                body = vcsutils.DefaultIfNotNil(ghPullRequest.Body)
1✔
512
        }
1✔
513

514
        return PullRequestInfo{
4✔
515
                ID:     int64(vcsutils.DefaultIfNotNil(ghPullRequest.Number)),
4✔
516
                Title:  vcsutils.DefaultIfNotNil(ghPullRequest.Title),
4✔
517
                URL:    vcsutils.DefaultIfNotNil(ghPullRequest.HTMLURL),
4✔
518
                Body:   body,
4✔
519
                Author: vcsutils.DefaultIfNotNil(ghPullRequest.User.Login),
4✔
520
                Source: BranchInfo{
4✔
521
                        Name:       sourceBranch,
4✔
522
                        Repository: sourceRepoName,
4✔
523
                        Owner:      sourceRepoOwner,
4✔
524
                },
4✔
525
                Target: BranchInfo{
4✔
526
                        Name:       targetBranch,
4✔
527
                        Repository: targetRepoName,
4✔
528
                        Owner:      targetRepoOwner,
4✔
529
                },
4✔
530
        }, nil
4✔
531
}
532

533
// Extracts branch name from the following expected label format repo:branch
534
func extractBranchFromLabel(label string) (string, error) {
8✔
535
        split := strings.Split(label, ":")
8✔
536
        if len(split) <= 1 {
8✔
537
                return "", fmt.Errorf("bad label format %s", label)
×
538
        }
×
539
        return split[1], nil
8✔
540
}
541

542
// AddPullRequestComment on GitHub
543
func (client *GitHubClient) AddPullRequestComment(ctx context.Context, owner, repository, content string, pullRequestID int) error {
6✔
544
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "content": content})
6✔
545
        if err != nil {
10✔
546
                return err
4✔
547
        }
4✔
548

549
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
550
                var ghResponse *github.Response
2✔
551
                // We use the Issues API to add a regular comment. The PullRequests API adds a code review comment.
2✔
552
                _, ghResponse, err = client.ghClient.Issues.CreateComment(ctx, owner, repository, pullRequestID, &github.IssueComment{Body: &content})
2✔
553
                return ghResponse, err
2✔
554
        })
2✔
555
}
556

557
// AddPullRequestReviewComments on GitHub
558
func (client *GitHubClient) AddPullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int, comments ...PullRequestComment) error {
2✔
559
        prID := strconv.Itoa(pullRequestID)
2✔
560
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "pullRequestID": prID})
2✔
561
        if err != nil {
2✔
562
                return err
×
563
        }
×
564
        if len(comments) == 0 {
2✔
565
                return errors.New(vcsutils.ErrNoCommentsProvided)
×
566
        }
×
567

568
        var commits []*github.RepositoryCommit
2✔
569
        var ghResponse *github.Response
2✔
570
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
571
                commits, ghResponse, err = client.ghClient.PullRequests.ListCommits(ctx, owner, repository, pullRequestID, nil)
2✔
572
                return ghResponse, err
2✔
573
        })
2✔
574
        if err != nil {
3✔
575
                return err
1✔
576
        }
1✔
577
        if len(commits) == 0 {
1✔
578
                return errors.New("could not fetch the commits list for pull request " + prID)
×
579
        }
×
580

581
        latestCommitSHA := commits[len(commits)-1].GetSHA()
1✔
582

1✔
583
        for _, comment := range comments {
3✔
584
                err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
585
                        ghResponse, err = client.executeCreatePullRequestReviewComment(ctx, owner, repository, latestCommitSHA, pullRequestID, comment)
2✔
586
                        return ghResponse, err
2✔
587
                })
2✔
588
                if err != nil {
2✔
589
                        return err
×
590
                }
×
591
        }
592
        return nil
1✔
593
}
594

595
func (client *GitHubClient) executeCreatePullRequestReviewComment(ctx context.Context, owner, repository, latestCommitSHA string, pullRequestID int, comment PullRequestComment) (*github.Response, error) {
2✔
596
        filePath := filepath.Clean(comment.NewFilePath)
2✔
597
        startLine := &comment.NewStartLine
2✔
598
        // GitHub API won't accept 'start_line' if it equals the end line
2✔
599
        if *startLine == comment.NewEndLine {
2✔
600
                startLine = nil
×
601
        }
×
602
        _, ghResponse, err := client.ghClient.PullRequests.CreateComment(ctx, owner, repository, pullRequestID, &github.PullRequestComment{
2✔
603
                CommitID:  &latestCommitSHA,
2✔
604
                Body:      &comment.Content,
2✔
605
                StartLine: startLine,
2✔
606
                Line:      &comment.NewEndLine,
2✔
607
                Path:      &filePath,
2✔
608
        })
2✔
609
        if err != nil {
2✔
610
                err = fmt.Errorf("could not create a code review comment for <%s/%s> in pull request %d. error received: %w",
×
611
                        owner, repository, pullRequestID, err)
×
612
        }
×
613
        return ghResponse, err
2✔
614
}
615

616
// ListPullRequestReviewComments on GitHub
617
func (client *GitHubClient) ListPullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int) ([]CommentInfo, error) {
2✔
618
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
2✔
619
        if err != nil {
2✔
620
                return nil, err
×
621
        }
×
622

623
        commentsInfoList := []CommentInfo{}
2✔
624
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
625
                var ghResponse *github.Response
2✔
626
                commentsInfoList, ghResponse, err = client.executeListPullRequestReviewComments(ctx, owner, repository, pullRequestID)
2✔
627
                return ghResponse, err
2✔
628
        })
2✔
629
        return commentsInfoList, err
2✔
630
}
631

632
// ListPullRequestReviews on GitHub
633
func (client *GitHubClient) ListPullRequestReviews(ctx context.Context, owner, repository string, pullRequestID int) ([]PullRequestReviewDetails, error) {
2✔
634
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
2✔
635
        if err != nil {
2✔
636
                return nil, err
×
637
        }
×
638

639
        var reviews []*github.PullRequestReview
2✔
640
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
641
                var ghResponse *github.Response
2✔
642
                reviews, ghResponse, err = client.ghClient.PullRequests.ListReviews(ctx, owner, repository, pullRequestID, nil)
2✔
643
                return ghResponse, err
2✔
644
        })
2✔
645
        if err != nil {
3✔
646
                return nil, err
1✔
647
        }
1✔
648

649
        var reviewInfos []PullRequestReviewDetails
1✔
650
        for _, review := range reviews {
2✔
651
                reviewInfos = append(reviewInfos, PullRequestReviewDetails{
1✔
652
                        ID:          review.GetID(),
1✔
653
                        Reviewer:    review.GetUser().GetLogin(),
1✔
654
                        Body:        review.GetBody(),
1✔
655
                        State:       review.GetState(),
1✔
656
                        SubmittedAt: review.GetSubmittedAt().String(),
1✔
657
                        CommitID:    review.GetCommitID(),
1✔
658
                })
1✔
659
        }
1✔
660

661
        return reviewInfos, nil
1✔
662
}
663

664
func (client *GitHubClient) executeListPullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int) ([]CommentInfo, *github.Response, error) {
2✔
665
        commentsList, ghResponse, err := client.ghClient.PullRequests.ListComments(ctx, owner, repository, pullRequestID, nil)
2✔
666
        if err != nil {
3✔
667
                return []CommentInfo{}, ghResponse, err
1✔
668
        }
1✔
669
        commentsInfoList := []CommentInfo{}
1✔
670
        for _, comment := range commentsList {
2✔
671
                commentsInfoList = append(commentsInfoList, CommentInfo{
1✔
672
                        ID:      comment.GetID(),
1✔
673
                        Content: comment.GetBody(),
1✔
674
                        Created: comment.GetCreatedAt().Time,
1✔
675
                })
1✔
676
        }
1✔
677
        return commentsInfoList, ghResponse, nil
1✔
678
}
679

680
// ListPullRequestComments on GitHub
681
func (client *GitHubClient) ListPullRequestComments(ctx context.Context, owner, repository string, pullRequestID int) ([]CommentInfo, error) {
4✔
682
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
4✔
683
        if err != nil {
4✔
684
                return []CommentInfo{}, err
×
685
        }
×
686

687
        var commentsList []*github.IssueComment
4✔
688
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
8✔
689
                var ghResponse *github.Response
4✔
690
                commentsList, ghResponse, err = client.ghClient.Issues.ListComments(ctx, owner, repository, pullRequestID, &github.IssueListCommentsOptions{})
4✔
691
                return ghResponse, err
4✔
692
        })
4✔
693

694
        if err != nil {
7✔
695
                return []CommentInfo{}, err
3✔
696
        }
3✔
697

698
        return mapGitHubIssuesCommentToCommentInfoList(commentsList)
1✔
699
}
700

701
// DeletePullRequestReviewComments on GitHub
702
func (client *GitHubClient) DeletePullRequestReviewComments(ctx context.Context, owner, repository string, _ int, comments ...CommentInfo) error {
2✔
703
        for _, comment := range comments {
5✔
704
                commentID := comment.ID
3✔
705
                err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "commentID": strconv.FormatInt(commentID, 10)})
3✔
706
                if err != nil {
3✔
707
                        return err
×
708
                }
×
709

710
                err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
711
                        return client.executeDeletePullRequestReviewComment(ctx, owner, repository, commentID)
3✔
712
                })
3✔
713
                if err != nil {
4✔
714
                        return err
1✔
715
                }
1✔
716

717
        }
718
        return nil
1✔
719
}
720

721
func (client *GitHubClient) executeDeletePullRequestReviewComment(ctx context.Context, owner, repository string, commentID int64) (*github.Response, error) {
3✔
722
        ghResponse, err := client.ghClient.PullRequests.DeleteComment(ctx, owner, repository, commentID)
3✔
723
        if err != nil {
4✔
724
                err = fmt.Errorf("could not delete pull request review comment: %w", err)
1✔
725
        }
1✔
726
        return ghResponse, err
3✔
727
}
728

729
// DeletePullRequestComment on GitHub
730
func (client *GitHubClient) DeletePullRequestComment(ctx context.Context, owner, repository string, _, commentID int) error {
2✔
731
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
2✔
732
        if err != nil {
2✔
733
                return err
×
734
        }
×
735
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
736
                return client.executeDeletePullRequestComment(ctx, owner, repository, commentID)
2✔
737
        })
2✔
738
}
739

740
func (client *GitHubClient) executeDeletePullRequestComment(ctx context.Context, owner, repository string, commentID int) (*github.Response, error) {
2✔
741
        ghResponse, err := client.ghClient.Issues.DeleteComment(ctx, owner, repository, int64(commentID))
2✔
742
        if err != nil {
3✔
743
                return ghResponse, err
1✔
744
        }
1✔
745

746
        var statusCode int
1✔
747
        if ghResponse.Response != nil {
2✔
748
                statusCode = ghResponse.Response.StatusCode
1✔
749
        }
1✔
750
        if statusCode != http.StatusNoContent && statusCode != http.StatusOK {
1✔
751
                return ghResponse, fmt.Errorf("expected %d status code while received %d status code", http.StatusNoContent, ghResponse.Response.StatusCode)
×
752
        }
×
753

754
        return ghResponse, nil
1✔
755
}
756

757
// GetLatestCommit on GitHub
758
func (client *GitHubClient) GetLatestCommit(ctx context.Context, owner, repository, branch string) (CommitInfo, error) {
10✔
759
        commits, err := client.GetCommits(ctx, owner, repository, branch)
10✔
760
        if err != nil {
18✔
761
                return CommitInfo{}, err
8✔
762
        }
8✔
763
        latestCommit := CommitInfo{}
2✔
764
        if len(commits) > 0 {
4✔
765
                latestCommit = commits[0]
2✔
766
        }
2✔
767
        return latestCommit, nil
2✔
768
}
769

770
// GetCommits on GitHub
771
func (client *GitHubClient) GetCommits(ctx context.Context, owner, repository, branch string) ([]CommitInfo, error) {
12✔
772
        err := validateParametersNotBlank(map[string]string{
12✔
773
                "owner":      owner,
12✔
774
                "repository": repository,
12✔
775
                "branch":     branch,
12✔
776
        })
12✔
777
        if err != nil {
16✔
778
                return nil, err
4✔
779
        }
4✔
780

781
        var commitsInfo []CommitInfo
8✔
782
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
16✔
783
                var ghResponse *github.Response
8✔
784
                listOptions := &github.CommitsListOptions{
8✔
785
                        SHA: branch,
8✔
786
                        ListOptions: github.ListOptions{
8✔
787
                                Page:    1,
8✔
788
                                PerPage: vcsutils.NumberOfCommitsToFetch,
8✔
789
                        },
8✔
790
                }
8✔
791
                commitsInfo, ghResponse, err = client.executeGetCommits(ctx, owner, repository, listOptions)
8✔
792
                return ghResponse, err
8✔
793
        })
8✔
794
        return commitsInfo, err
8✔
795
}
796

797
// GetCommitsWithQueryOptions on GitHub
798
func (client *GitHubClient) GetCommitsWithQueryOptions(ctx context.Context, owner, repository string, listOptions GitCommitsQueryOptions) ([]CommitInfo, error) {
2✔
799
        err := validateParametersNotBlank(map[string]string{
2✔
800
                "owner":      owner,
2✔
801
                "repository": repository,
2✔
802
        })
2✔
803
        if err != nil {
2✔
804
                return nil, err
×
805
        }
×
806
        var commitsInfo []CommitInfo
2✔
807
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
808
                var ghResponse *github.Response
2✔
809
                commitsInfo, ghResponse, err = client.executeGetCommits(ctx, owner, repository, convertToGitHubCommitsListOptions(listOptions))
2✔
810
                return ghResponse, err
2✔
811
        })
2✔
812
        return commitsInfo, err
2✔
813
}
814

815
func convertToGitHubCommitsListOptions(listOptions GitCommitsQueryOptions) *github.CommitsListOptions {
2✔
816
        return &github.CommitsListOptions{
2✔
817
                Since: listOptions.Since,
2✔
818
                Until: time.Now(),
2✔
819
                ListOptions: github.ListOptions{
2✔
820
                        Page:    listOptions.Page,
2✔
821
                        PerPage: listOptions.PerPage,
2✔
822
                },
2✔
823
        }
2✔
824
}
2✔
825

826
func (client *GitHubClient) executeGetCommits(ctx context.Context, owner, repository string, listOptions *github.CommitsListOptions) ([]CommitInfo, *github.Response, error) {
10✔
827
        commits, ghResponse, err := client.ghClient.Repositories.ListCommits(ctx, owner, repository, listOptions)
10✔
828
        if err != nil {
16✔
829
                return nil, ghResponse, err
6✔
830
        }
6✔
831

832
        var commitsInfo []CommitInfo
4✔
833
        for _, commit := range commits {
11✔
834
                commitInfo := mapGitHubCommitToCommitInfo(commit)
7✔
835
                commitsInfo = append(commitsInfo, commitInfo)
7✔
836
        }
7✔
837
        return commitsInfo, ghResponse, nil
4✔
838
}
839

840
// GetRepositoryInfo on GitHub
841
func (client *GitHubClient) GetRepositoryInfo(ctx context.Context, owner, repository string) (RepositoryInfo, error) {
6✔
842
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
6✔
843
        if err != nil {
9✔
844
                return RepositoryInfo{}, err
3✔
845
        }
3✔
846

847
        var repo *github.Repository
3✔
848
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
849
                var ghResponse *github.Response
3✔
850
                repo, ghResponse, err = client.ghClient.Repositories.Get(ctx, owner, repository)
3✔
851
                return ghResponse, err
3✔
852
        })
3✔
853
        if err != nil {
4✔
854
                return RepositoryInfo{}, err
1✔
855
        }
1✔
856

857
        return RepositoryInfo{RepositoryVisibility: getGitHubRepositoryVisibility(repo), CloneInfo: CloneInfo{HTTP: repo.GetCloneURL(), SSH: repo.GetSSHURL()}}, nil
2✔
858
}
859

860
// GetCommitBySha on GitHub
861
func (client *GitHubClient) GetCommitBySha(ctx context.Context, owner, repository, sha string) (CommitInfo, error) {
7✔
862
        err := validateParametersNotBlank(map[string]string{
7✔
863
                "owner":      owner,
7✔
864
                "repository": repository,
7✔
865
                "sha":        sha,
7✔
866
        })
7✔
867
        if err != nil {
11✔
868
                return CommitInfo{}, err
4✔
869
        }
4✔
870

871
        var commit *github.RepositoryCommit
3✔
872
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
873
                var ghResponse *github.Response
3✔
874
                commit, ghResponse, err = client.ghClient.Repositories.GetCommit(ctx, owner, repository, sha, nil)
3✔
875
                return ghResponse, err
3✔
876
        })
3✔
877
        if err != nil {
5✔
878
                return CommitInfo{}, err
2✔
879
        }
2✔
880

881
        return mapGitHubCommitToCommitInfo(commit), nil
1✔
882
}
883

884
// CreateLabel on GitHub
885
func (client *GitHubClient) CreateLabel(ctx context.Context, owner, repository string, labelInfo LabelInfo) error {
6✔
886
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "LabelInfo.name": labelInfo.Name})
6✔
887
        if err != nil {
10✔
888
                return err
4✔
889
        }
4✔
890

891
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
892
                var ghResponse *github.Response
2✔
893
                _, ghResponse, err = client.ghClient.Issues.CreateLabel(ctx, owner, repository, &github.Label{
2✔
894
                        Name:        &labelInfo.Name,
2✔
895
                        Description: &labelInfo.Description,
2✔
896
                        Color:       &labelInfo.Color,
2✔
897
                })
2✔
898
                return ghResponse, err
2✔
899
        })
2✔
900
}
901

902
// GetLabel on GitHub
903
func (client *GitHubClient) GetLabel(ctx context.Context, owner, repository, name string) (*LabelInfo, error) {
7✔
904
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "name": name})
7✔
905
        if err != nil {
11✔
906
                return nil, err
4✔
907
        }
4✔
908

909
        var labelInfo *LabelInfo
3✔
910
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
911
                var ghResponse *github.Response
3✔
912
                labelInfo, ghResponse, err = client.executeGetLabel(ctx, owner, repository, name)
3✔
913
                return ghResponse, err
3✔
914
        })
3✔
915
        return labelInfo, err
3✔
916
}
917

918
func (client *GitHubClient) executeGetLabel(ctx context.Context, owner, repository, name string) (*LabelInfo, *github.Response, error) {
3✔
919
        label, ghResponse, err := client.ghClient.Issues.GetLabel(ctx, owner, repository, name)
3✔
920
        if err != nil {
5✔
921
                if ghResponse != nil && ghResponse.Response != nil && ghResponse.Response.StatusCode == http.StatusNotFound {
3✔
922
                        return nil, ghResponse, nil
1✔
923
                }
1✔
924
                return nil, ghResponse, err
1✔
925
        }
926

927
        labelInfo := &LabelInfo{
1✔
928
                Name:        *label.Name,
1✔
929
                Description: *label.Description,
1✔
930
                Color:       *label.Color,
1✔
931
        }
1✔
932
        return labelInfo, ghResponse, nil
1✔
933
}
934

935
// ListPullRequestLabels on GitHub
936
func (client *GitHubClient) ListPullRequestLabels(ctx context.Context, owner, repository string, pullRequestID int) ([]string, error) {
5✔
937
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
5✔
938
        if err != nil {
8✔
939
                return nil, err
3✔
940
        }
3✔
941

942
        results := []string{}
2✔
943
        for nextPage := 0; ; nextPage++ {
4✔
944
                options := &github.ListOptions{Page: nextPage}
2✔
945
                var labels []*github.Label
2✔
946
                var ghResponse *github.Response
2✔
947
                err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
948
                        labels, ghResponse, err = client.ghClient.Issues.ListLabelsByIssue(ctx, owner, repository, pullRequestID, options)
2✔
949
                        return ghResponse, err
2✔
950
                })
2✔
951
                if err != nil {
3✔
952
                        return nil, err
1✔
953
                }
1✔
954
                for _, label := range labels {
2✔
955
                        results = append(results, *label.Name)
1✔
956
                }
1✔
957
                if nextPage+1 >= ghResponse.LastPage {
2✔
958
                        break
1✔
959
                }
960
        }
961
        return results, nil
1✔
962
}
963

964
func (client *GitHubClient) ListPullRequestsAssociatedWithCommit(ctx context.Context, owner, repository string, commitSHA string) ([]PullRequestInfo, error) {
2✔
965
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
2✔
966
        if err != nil {
2✔
967
                return nil, err
×
968
        }
×
969

970
        var pulls []*github.PullRequest
2✔
971
        if err = client.runWithRateLimitRetries(func() (ghResponse *github.Response, err error) {
4✔
972
                pulls, ghResponse, err = client.ghClient.PullRequests.ListPullRequestsWithCommit(ctx, owner, repository, commitSHA, nil)
2✔
973
                return ghResponse, err
2✔
974
        }); err != nil {
3✔
975
                return nil, err
1✔
976
        }
1✔
977
        return mapGitHubPullRequestToPullRequestInfoList(pulls, false)
1✔
978
}
979

980
// UnlabelPullRequest on GitHub
981
func (client *GitHubClient) UnlabelPullRequest(ctx context.Context, owner, repository, name string, pullRequestID int) error {
5✔
982
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
5✔
983
        if err != nil {
8✔
984
                return err
3✔
985
        }
3✔
986

987
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
988
                return client.ghClient.Issues.RemoveLabelForIssue(ctx, owner, repository, pullRequestID, name)
2✔
989
        })
2✔
990
}
991

992
// UploadCodeScanning to GitHub Security tab
993
func (client *GitHubClient) UploadCodeScanning(ctx context.Context, owner, repository, branch, sarifContent string) (id string, err error) {
2✔
994
        commit, err := client.GetLatestCommit(ctx, owner, repository, branch)
2✔
995
        if err != nil {
3✔
996
                return
1✔
997
        }
1✔
998

999
        commitSHA := commit.Hash
1✔
1000
        branch = vcsutils.AddBranchPrefix(branch)
1✔
1001
        client.logger.Debug(vcsutils.UploadingCodeScanning, repository, "/", branch)
1✔
1002

1✔
1003
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
2✔
1004
                var ghResponse *github.Response
1✔
1005
                id, ghResponse, err = client.executeUploadCodeScanning(ctx, owner, repository, branch, commitSHA, sarifContent)
1✔
1006
                return ghResponse, err
1✔
1007
        })
1✔
1008
        return
1✔
1009
}
1010

1011
func (client *GitHubClient) executeUploadCodeScanning(ctx context.Context, owner, repository, branch, commitSHA, sarifContent string) (id string, ghResponse *github.Response, err error) {
1✔
1012
        encodedSarif, err := encodeScanningResult(sarifContent)
1✔
1013
        if err != nil {
1✔
1014
                return
×
1015
        }
×
1016

1017
        sarifID, ghResponse, err := client.ghClient.CodeScanning.UploadSarif(ctx, owner, repository, &github.SarifAnalysis{
1✔
1018
                CommitSHA: &commitSHA,
1✔
1019
                Ref:       &branch,
1✔
1020
                Sarif:     &encodedSarif,
1✔
1021
        })
1✔
1022

1✔
1023
        // According to go-github API - successful ghResponse will return 202 status code
1✔
1024
        // The body of the ghResponse will appear in the error, and the Sarif struct will be empty.
1✔
1025
        if err != nil && ghResponse.Response.StatusCode != http.StatusAccepted {
1✔
1026
                return
×
1027
        }
×
1028

1029
        id, err = handleGitHubUploadSarifID(sarifID, err)
1✔
1030
        return
1✔
1031
}
1032

1033
func handleGitHubUploadSarifID(sarifID *github.SarifID, uploadSarifErr error) (id string, err error) {
1✔
1034
        if sarifID != nil && *sarifID.ID != "" {
2✔
1035
                id = *sarifID.ID
1✔
1036
                return
1✔
1037
        }
1✔
1038
        var result map[string]string
×
1039
        var ghAcceptedError *github.AcceptedError
×
1040
        if errors.As(uploadSarifErr, &ghAcceptedError) {
×
1041
                if err = json.Unmarshal(ghAcceptedError.Raw, &result); err != nil {
×
1042
                        return
×
1043
                }
×
1044
                id = result["id"]
×
1045
        }
1046
        return
×
1047
}
1048

1049
// DownloadFileFromRepo on GitHub
1050
func (client *GitHubClient) DownloadFileFromRepo(ctx context.Context, owner, repository, branch, path string) (content []byte, statusCode int, err error) {
3✔
1051
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
1052
                var ghResponse *github.Response
3✔
1053
                content, statusCode, ghResponse, err = client.executeDownloadFileFromRepo(ctx, owner, repository, branch, path)
3✔
1054
                return ghResponse, err
3✔
1055
        })
3✔
1056
        return
3✔
1057
}
1058

1059
func (client *GitHubClient) executeDownloadFileFromRepo(ctx context.Context, owner, repository, branch, path string) (content []byte, statusCode int, ghResponse *github.Response, err error) {
3✔
1060
        body, ghResponse, err := client.ghClient.Repositories.DownloadContents(ctx, owner, repository, path, &github.RepositoryContentGetOptions{Ref: branch})
3✔
1061
        defer func() {
6✔
1062
                if body != nil {
4✔
1063
                        err = errors.Join(err, body.Close())
1✔
1064
                }
1✔
1065
        }()
1066

1067
        if ghResponse == nil || ghResponse.Response == nil {
4✔
1068
                return
1✔
1069
        }
1✔
1070

1071
        statusCode = ghResponse.StatusCode
2✔
1072
        if err != nil && statusCode != http.StatusOK {
2✔
1073
                err = fmt.Errorf("expected %d status code while received %d status code with error:\n%s", http.StatusOK, ghResponse.StatusCode, err)
×
1074
                return
×
1075
        }
×
1076

1077
        if body != nil {
3✔
1078
                content, err = io.ReadAll(body)
1✔
1079
        }
1✔
1080
        return
2✔
1081
}
1082

1083
// GetRepositoryEnvironmentInfo on GitHub
1084
func (client *GitHubClient) GetRepositoryEnvironmentInfo(ctx context.Context, owner, repository, name string) (RepositoryEnvironmentInfo, error) {
2✔
1085
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "name": name})
2✔
1086
        if err != nil {
2✔
1087
                return RepositoryEnvironmentInfo{}, err
×
1088
        }
×
1089

1090
        var repositoryEnvInfo *RepositoryEnvironmentInfo
2✔
1091
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1092
                var ghResponse *github.Response
2✔
1093
                repositoryEnvInfo, ghResponse, err = client.executeGetRepositoryEnvironmentInfo(ctx, owner, repository, name)
2✔
1094
                return ghResponse, err
2✔
1095
        })
2✔
1096
        return *repositoryEnvInfo, err
2✔
1097
}
1098

1099
func (client *GitHubClient) CreateBranch(ctx context.Context, owner, repository, sourceBranch, newBranch string) error {
2✔
1100
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "sourceBranch": sourceBranch, "newBranch": newBranch})
2✔
1101
        if err != nil {
2✔
1102
                return err
×
1103
        }
×
1104

1105
        var sourceBranchRef *github.Branch
2✔
1106
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1107
                sourceBranchRef, _, err = client.ghClient.Repositories.GetBranch(ctx, owner, repository, sourceBranch, 3)
2✔
1108
                if err != nil {
3✔
1109
                        return nil, err
1✔
1110
                }
1✔
1111
                return nil, nil
1✔
1112
        })
1113
        if err != nil {
3✔
1114
                return err
1✔
1115
        }
1✔
1116

1117
        latestCommitSHA := sourceBranchRef.Commit.SHA
1✔
1118
        ref := &github.Reference{
1✔
1119
                Ref:    github.String("refs/heads/" + newBranch),
1✔
1120
                Object: &github.GitObject{SHA: latestCommitSHA},
1✔
1121
        }
1✔
1122

1✔
1123
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
2✔
1124
                _, _, err = client.ghClient.Git.CreateRef(ctx, owner, repository, ref)
1✔
1125
                if err != nil {
1✔
1126
                        return nil, err
×
1127
                }
×
1128
                return nil, nil
1✔
1129
        })
1130
}
1131

1132
func (client *GitHubClient) AddOrganizationSecret(ctx context.Context, owner, secretName, secretValue string) error {
2✔
1133
        err := validateParametersNotBlank(map[string]string{"secretName": secretName, "owner": owner, "secretValue": secretValue})
2✔
1134
        if err != nil {
2✔
1135
                return err
×
1136
        }
×
1137

1138
        publicKey, _, err := client.ghClient.Actions.GetOrgPublicKey(ctx, owner)
2✔
1139
        if err != nil {
3✔
1140
                return err
1✔
1141
        }
1✔
1142

1143
        encryptedValue, err := encryptSecret(publicKey, secretValue)
1✔
1144
        if err != nil {
1✔
1145
                return err
×
1146
        }
×
1147

1148
        secret := &github.EncryptedSecret{
1✔
1149
                Name:           secretName,
1✔
1150
                KeyID:          publicKey.GetKeyID(),
1✔
1151
                EncryptedValue: encryptedValue,
1✔
1152
                Visibility:     "all",
1✔
1153
        }
1✔
1154

1✔
1155
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
2✔
1156
                _, err = client.ghClient.Actions.CreateOrUpdateOrgSecret(ctx, owner, secret)
1✔
1157
                return nil, err
1✔
1158
        })
1✔
1159
        return err
1✔
1160
}
1161

1162
func (client *GitHubClient) AllowWorkflows(ctx context.Context, owner string) error {
2✔
1163
        err := validateParametersNotBlank(map[string]string{"owner": owner})
2✔
1164
        if err != nil {
2✔
1165
                return err
×
1166
        }
×
1167

1168
        requestBody := &github.ActionsPermissions{
2✔
1169
                AllowedActions:      github.String("all"),
2✔
1170
                EnabledRepositories: github.String("all"),
2✔
1171
        }
2✔
1172

2✔
1173
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1174
                _, _, err = client.ghClient.Actions.EditActionsPermissions(ctx, owner, *requestBody)
2✔
1175
                return nil, err
2✔
1176
        })
2✔
1177
        return err
2✔
1178
}
1179

1180
func (client *GitHubClient) GetRepoCollaborators(ctx context.Context, owner, repo, affiliation, permission string) ([]string, error) {
2✔
1181
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repo": repo, "affiliation": affiliation, "permission": permission})
2✔
1182
        if err != nil {
2✔
1183
                return nil, err
×
1184
        }
×
1185

1186
        var collaborators []*github.User
2✔
1187
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1188
                var ghResponse *github.Response
2✔
1189
                var err error
2✔
1190
                collaborators, ghResponse, err = client.ghClient.Repositories.ListCollaborators(ctx, owner, repo, &github.ListCollaboratorsOptions{
2✔
1191
                        Affiliation: affiliation,
2✔
1192
                        Permission:  permission,
2✔
1193
                })
2✔
1194
                return ghResponse, err
2✔
1195
        })
2✔
1196
        if err != nil {
3✔
1197
                return nil, err
1✔
1198
        }
1✔
1199

1200
        var names []string
1✔
1201
        for _, collab := range collaborators {
2✔
1202
                names = append(names, collab.GetLogin())
1✔
1203
        }
1✔
1204
        return names, nil
1✔
1205
}
1206

1207
func (client *GitHubClient) GetRepoTeamsByPermissions(ctx context.Context, owner, repo string, permissions []string) ([]int64, error) {
2✔
1208
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repo": repo, "permissions": strings.Join(permissions, ",")})
2✔
1209
        if err != nil {
2✔
1210
                return nil, err
×
1211
        }
×
1212

1213
        var allTeams []*github.Team
2✔
1214
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1215
                var resp *github.Response
2✔
1216
                var err error
2✔
1217
                allTeams, resp, err = client.ghClient.Repositories.ListTeams(ctx, owner, repo, nil)
2✔
1218
                return resp, err
2✔
1219
        })
2✔
1220
        if err != nil {
3✔
1221
                return nil, err
1✔
1222
        }
1✔
1223

1224
        permMap := make(map[string]bool)
1✔
1225
        for _, p := range permissions {
2✔
1226
                permMap[strings.ToLower(p)] = true
1✔
1227
        }
1✔
1228

1229
        var matchedTeams []int64
1✔
1230
        for _, team := range allTeams {
2✔
1231
                if permMap[strings.ToLower(team.GetPermission())] {
2✔
1232
                        matchedTeams = append(matchedTeams, team.GetID())
1✔
1233
                }
1✔
1234
        }
1235

1236
        return matchedTeams, nil
1✔
1237
}
1238

1239
func (client *GitHubClient) CreateOrUpdateEnvironment(ctx context.Context, owner, repo, envName string, teams []int64, users []string) error {
2✔
1240
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repo": repo, "envName": envName})
2✔
1241
        if err != nil {
2✔
1242
                return err
×
1243
        }
×
1244

1245
        var envReviewers []*github.EnvReviewers
2✔
1246
        for _, team := range teams {
4✔
1247
                envReviewers = append(envReviewers, &github.EnvReviewers{
2✔
1248
                        Type: github.String("Team"),
2✔
1249
                        ID:   &team,
2✔
1250
                })
2✔
1251
        }
2✔
1252

1253
        if len(envReviewers) >= ghMaxEnvReviewers {
2✔
1254
                envReviewers = envReviewers[:ghMaxEnvReviewers]
×
1255
                _, _, err := client.ghClient.Repositories.CreateUpdateEnvironment(ctx, owner, repo, envName, &github.CreateUpdateEnvironment{
×
1256
                        Reviewers: envReviewers,
×
1257
                })
×
1258
                return err
×
1259
        }
×
1260

1261
        for _, userName := range users {
2✔
1262
                user, _, err := client.ghClient.Users.Get(ctx, userName)
×
1263

×
1264
                if err != nil {
×
1265
                        return err
×
1266
                }
×
1267
                userId := user.GetID()
×
1268
                envReviewers = append(envReviewers, &github.EnvReviewers{
×
1269
                        Type: github.String("User"),
×
1270
                        ID:   github.Int64(userId),
×
1271
                })
×
1272
        }
1273

1274
        if len(envReviewers) >= ghMaxEnvReviewers {
2✔
1275
                envReviewers = envReviewers[:ghMaxEnvReviewers]
×
1276
                _, _, err := client.ghClient.Repositories.CreateUpdateEnvironment(ctx, owner, repo, envName, &github.CreateUpdateEnvironment{
×
1277
                        Reviewers: envReviewers,
×
1278
                })
×
1279
                return err
×
1280
        }
×
1281

1282
        _, _, err = client.ghClient.Repositories.CreateUpdateEnvironment(ctx, owner, repo, envName, &github.CreateUpdateEnvironment{
2✔
1283
                Reviewers: envReviewers,
2✔
1284
        })
2✔
1285
        return err
2✔
1286
}
1287

1288
func (client *GitHubClient) CommitAndPushFiles(
1289
        ctx context.Context,
1290
        owner, repo, sourceBranch, commitMessage, authorName, authorEmail string,
1291
        files []FileToCommit,
1292
) error {
2✔
1293
        if len(files) == 0 {
2✔
1294
                return errors.New("no files provided to commit")
×
1295
        }
×
1296

1297
        ref, _, err := client.ghClient.Git.GetRef(ctx, owner, repo, "refs/heads/"+sourceBranch)
2✔
1298
        if err != nil {
3✔
1299
                return fmt.Errorf("failed to get branch ref: %w", err)
1✔
1300
        }
1✔
1301

1302
        parentCommit, _, err := client.ghClient.Git.GetCommit(ctx, owner, repo, *ref.Object.SHA)
1✔
1303
        if err != nil {
1✔
1304
                return fmt.Errorf("failed to get parent commit: %w", err)
×
1305
        }
×
1306

1307
        var treeEntries []*github.TreeEntry
1✔
1308
        for _, file := range files {
2✔
1309
                blob, _, err := client.ghClient.Git.CreateBlob(ctx, owner, repo, &github.Blob{
1✔
1310
                        Content:  github.String(file.Content),
1✔
1311
                        Encoding: github.String("utf-8"),
1✔
1312
                })
1✔
1313
                if err != nil {
1✔
1314
                        return fmt.Errorf("failed to create blob for %s: %w", file.Path, err)
×
1315
                }
×
1316

1317
                // Add each file to the treeEntries
1318
                treeEntries = append(treeEntries, &github.TreeEntry{
1✔
1319
                        Path: github.String(file.Path),
1✔
1320
                        Mode: github.String(regularFileCode),
1✔
1321
                        Type: github.String("blob"),
1✔
1322
                        SHA:  blob.SHA,
1✔
1323
                })
1✔
1324
        }
1325

1326
        tree, _, err := client.ghClient.Git.CreateTree(ctx, owner, repo, *parentCommit.Tree.SHA, treeEntries)
1✔
1327
        if err != nil {
1✔
1328
                return fmt.Errorf("failed to create tree: %w", err)
×
1329
        }
×
1330

1331
        commit := &github.Commit{
1✔
1332
                Message: github.String(commitMessage),
1✔
1333
                Tree:    tree,
1✔
1334
                Parents: []*github.Commit{{SHA: parentCommit.SHA}},
1✔
1335
                Author: &github.CommitAuthor{
1✔
1336
                        Name:  github.String(authorName),
1✔
1337
                        Email: github.String(authorEmail),
1✔
1338
                        Date:  &github.Timestamp{Time: time.Now()},
1✔
1339
                },
1✔
1340
        }
1✔
1341

1✔
1342
        newCommit, _, err := client.ghClient.Git.CreateCommit(ctx, owner, repo, commit, nil)
1✔
1343
        if err != nil {
1✔
1344
                return fmt.Errorf("failed to create commit: %w", err)
×
1345
        }
×
1346

1347
        ref.Object.SHA = newCommit.SHA
1✔
1348
        _, _, err = client.ghClient.Git.UpdateRef(ctx, owner, repo, ref, false)
1✔
1349
        if err != nil {
1✔
1350
                return fmt.Errorf("failed to update branch ref: %w", err)
×
1351
        }
×
1352
        return nil
1✔
1353
}
1354

1355
func (client *GitHubClient) MergePullRequest(ctx context.Context, owner, repo string, prNumber int, commitMessage string) error {
2✔
1356
        err := client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1357
                _, resp, err := client.ghClient.PullRequests.Merge(ctx, owner, repo, prNumber, commitMessage, nil)
2✔
1358
                return resp, err
2✔
1359
        })
2✔
1360
        return err
2✔
1361
}
1362

1363
func (client *GitHubClient) executeGetRepositoryEnvironmentInfo(ctx context.Context, owner, repository, name string) (*RepositoryEnvironmentInfo, *github.Response, error) {
2✔
1364
        environment, ghResponse, err := client.ghClient.Repositories.GetEnvironment(ctx, owner, repository, name)
2✔
1365
        if err != nil {
3✔
1366
                return &RepositoryEnvironmentInfo{}, ghResponse, err
1✔
1367
        }
1✔
1368

1369
        if err = vcsutils.CheckResponseStatusWithBody(ghResponse.Response, http.StatusOK); err != nil {
1✔
1370
                return &RepositoryEnvironmentInfo{}, ghResponse, err
×
1371
        }
×
1372

1373
        reviewers, err := extractGitHubEnvironmentReviewers(environment)
1✔
1374
        if err != nil {
1✔
1375
                return &RepositoryEnvironmentInfo{}, ghResponse, err
×
1376
        }
×
1377

1378
        return &RepositoryEnvironmentInfo{
1✔
1379
                        Name:      environment.GetName(),
1✔
1380
                        Url:       environment.GetURL(),
1✔
1381
                        Reviewers: reviewers,
1✔
1382
                },
1✔
1383
                ghResponse,
1✔
1384
                nil
1✔
1385
}
1386

1387
func (client *GitHubClient) GetModifiedFiles(ctx context.Context, owner, repository, refBefore, refAfter string) ([]string, error) {
6✔
1388
        err := validateParametersNotBlank(map[string]string{
6✔
1389
                "owner":      owner,
6✔
1390
                "repository": repository,
6✔
1391
                "refBefore":  refBefore,
6✔
1392
                "refAfter":   refAfter,
6✔
1393
        })
6✔
1394
        if err != nil {
10✔
1395
                return nil, err
4✔
1396
        }
4✔
1397

1398
        var fileNamesList []string
2✔
1399
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1400
                var ghResponse *github.Response
2✔
1401
                fileNamesList, ghResponse, err = client.executeGetModifiedFiles(ctx, owner, repository, refBefore, refAfter)
2✔
1402
                return ghResponse, err
2✔
1403
        })
2✔
1404
        return fileNamesList, err
2✔
1405
}
1406

1407
func (client *GitHubClient) executeGetModifiedFiles(ctx context.Context, owner, repository, refBefore, refAfter string) ([]string, *github.Response, error) {
2✔
1408
        // According to the https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#compare-two-commits
2✔
1409
        // the list of changed files is always returned with the first page fully,
2✔
1410
        // so we don't need to iterate over other pages to get additional info about the files.
2✔
1411
        // And we also do not need info about the change that is why we can limit only to a single entity.
2✔
1412
        listOptions := &github.ListOptions{PerPage: 1}
2✔
1413

2✔
1414
        comparison, ghResponse, err := client.ghClient.Repositories.CompareCommits(ctx, owner, repository, refBefore, refAfter, listOptions)
2✔
1415
        if err != nil {
3✔
1416
                return nil, ghResponse, err
1✔
1417
        }
1✔
1418

1419
        if err = vcsutils.CheckResponseStatusWithBody(ghResponse.Response, http.StatusOK); err != nil {
1✔
1420
                return nil, ghResponse, err
×
1421
        }
×
1422

1423
        fileNamesSet := datastructures.MakeSet[string]()
1✔
1424
        for _, file := range comparison.Files {
18✔
1425
                fileNamesSet.Add(vcsutils.DefaultIfNotNil(file.Filename))
17✔
1426
                fileNamesSet.Add(vcsutils.DefaultIfNotNil(file.PreviousFilename))
17✔
1427
        }
17✔
1428

1429
        _ = fileNamesSet.Remove("") // Make sure there are no blank filepath.
1✔
1430
        fileNamesList := fileNamesSet.ToSlice()
1✔
1431
        sort.Strings(fileNamesList)
1✔
1432

1✔
1433
        return fileNamesList, ghResponse, nil
1✔
1434
}
1435

1436
// Extract code reviewers from environment
1437
func extractGitHubEnvironmentReviewers(environment *github.Environment) ([]string, error) {
2✔
1438
        var reviewers []string
2✔
1439
        protectionRules := environment.ProtectionRules
2✔
1440
        if protectionRules == nil {
2✔
1441
                return reviewers, nil
×
1442
        }
×
1443
        reviewerStruct := repositoryEnvironmentReviewer{}
2✔
1444
        for _, rule := range protectionRules {
4✔
1445
                for _, reviewer := range rule.Reviewers {
5✔
1446
                        if err := mapstructure.Decode(reviewer.Reviewer, &reviewerStruct); err != nil {
3✔
1447
                                return []string{}, err
×
1448
                        }
×
1449
                        reviewers = append(reviewers, reviewerStruct.Login)
3✔
1450
                }
1451
        }
1452
        return reviewers, nil
2✔
1453
}
1454

1455
func createGitHubHook(token, payloadURL string, webhookEvents ...vcsutils.WebhookEvent) *github.Hook {
4✔
1456
        return &github.Hook{
4✔
1457
                Events: getGitHubWebhookEvents(webhookEvents...),
4✔
1458
                Config: map[string]interface{}{
4✔
1459
                        "url":          payloadURL,
4✔
1460
                        "content_type": "json",
4✔
1461
                        "secret":       token,
4✔
1462
                },
4✔
1463
        }
4✔
1464
}
4✔
1465

1466
// Get varargs of webhook events and return a slice of GitHub webhook events
1467
func getGitHubWebhookEvents(webhookEvents ...vcsutils.WebhookEvent) []string {
4✔
1468
        events := datastructures.MakeSet[string]()
4✔
1469
        for _, event := range webhookEvents {
16✔
1470
                switch event {
12✔
1471
                case vcsutils.PrOpened, vcsutils.PrEdited, vcsutils.PrMerged, vcsutils.PrRejected:
8✔
1472
                        events.Add("pull_request")
8✔
1473
                case vcsutils.Push, vcsutils.TagPushed, vcsutils.TagRemoved:
4✔
1474
                        events.Add("push")
4✔
1475
                }
1476
        }
1477
        return events.ToSlice()
4✔
1478
}
1479

1480
func getGitHubRepositoryVisibility(repo *github.Repository) RepositoryVisibility {
5✔
1481
        switch *repo.Visibility {
5✔
1482
        case "public":
3✔
1483
                return Public
3✔
1484
        case "internal":
1✔
1485
                return Internal
1✔
1486
        default:
1✔
1487
                return Private
1✔
1488
        }
1489
}
1490

1491
func getGitHubCommitState(commitState CommitStatus) string {
7✔
1492
        switch commitState {
7✔
1493
        case Pass:
1✔
1494
                return "success"
1✔
1495
        case Fail:
1✔
1496
                return "failure"
1✔
1497
        case Error:
3✔
1498
                return "error"
3✔
1499
        case InProgress:
1✔
1500
                return "pending"
1✔
1501
        }
1502
        return ""
1✔
1503
}
1504

1505
func mapGitHubCommitToCommitInfo(commit *github.RepositoryCommit) CommitInfo {
8✔
1506
        parents := make([]string, len(commit.Parents))
8✔
1507
        for i, c := range commit.Parents {
15✔
1508
                parents[i] = c.GetSHA()
7✔
1509
        }
7✔
1510
        details := commit.GetCommit()
8✔
1511
        return CommitInfo{
8✔
1512
                Hash:          commit.GetSHA(),
8✔
1513
                AuthorName:    details.GetAuthor().GetName(),
8✔
1514
                CommitterName: details.GetCommitter().GetName(),
8✔
1515
                Url:           commit.GetURL(),
8✔
1516
                Timestamp:     details.GetCommitter().GetDate().UTC().Unix(),
8✔
1517
                Message:       details.GetMessage(),
8✔
1518
                ParentHashes:  parents,
8✔
1519
                AuthorEmail:   details.GetAuthor().GetEmail(),
8✔
1520
        }
8✔
1521
}
1522

1523
func mapGitHubIssuesCommentToCommentInfoList(commentsList []*github.IssueComment) (res []CommentInfo, err error) {
1✔
1524
        for _, comment := range commentsList {
3✔
1525
                res = append(res, CommentInfo{
2✔
1526
                        ID:      comment.GetID(),
2✔
1527
                        Content: comment.GetBody(),
2✔
1528
                        Created: comment.GetCreatedAt().Time,
2✔
1529
                })
2✔
1530
        }
2✔
1531
        return
1✔
1532
}
1533

1534
func mapGitHubPullRequestToPullRequestInfoList(pullRequestList []*github.PullRequest, withBody bool) (res []PullRequestInfo, err error) {
3✔
1535
        var mappedPullRequest PullRequestInfo
3✔
1536
        for _, pullRequest := range pullRequestList {
6✔
1537
                mappedPullRequest, err = mapGitHubPullRequestToPullRequestInfo(pullRequest, withBody)
3✔
1538
                if err != nil {
3✔
1539
                        return
×
1540
                }
×
1541
                res = append(res, mappedPullRequest)
3✔
1542
        }
1543
        return
3✔
1544
}
1545

1546
func encodeScanningResult(data string) (string, error) {
1✔
1547
        compressedScan, err := base64.EncodeGzip([]byte(data), 6)
1✔
1548
        if err != nil {
1✔
1549
                return "", err
×
1550
        }
×
1551

1552
        return compressedScan, err
1✔
1553
}
1554

1555
type repositoryEnvironmentReviewer struct {
1556
        Login string `mapstructure:"login"`
1557
}
1558

1559
func shouldRetryIfRateLimitExceeded(ghResponse *github.Response, requestError error) bool {
111✔
1560
        if ghResponse == nil || ghResponse.Response == nil {
159✔
1561
                return false
48✔
1562
        }
48✔
1563

1564
        if !slices.Contains(rateLimitRetryStatuses, ghResponse.StatusCode) {
124✔
1565
                return false
61✔
1566
        }
61✔
1567

1568
        // In case of encountering a rate limit abuse, it's advisable to observe a considerate delay before attempting a retry.
1569
        // This prevents immediate retries within the current sequence, allowing a respectful interval before reattempting the request.
1570
        if requestError != nil && isRateLimitAbuseError(requestError) {
3✔
1571
                return false
1✔
1572
        }
1✔
1573

1574
        body, err := io.ReadAll(ghResponse.Body)
1✔
1575
        if err != nil {
1✔
1576
                return false
×
1577
        }
×
1578
        return strings.Contains(string(body), "rate limit")
1✔
1579
}
1580

1581
func isRateLimitAbuseError(requestError error) bool {
4✔
1582
        var abuseRateLimitError *github.AbuseRateLimitError
4✔
1583
        var rateLimitError *github.RateLimitError
4✔
1584
        return errors.As(requestError, &abuseRateLimitError) || errors.As(requestError, &rateLimitError)
4✔
1585
}
4✔
1586

1587
func encryptSecret(publicKey *github.PublicKey, secretValue string) (string, error) {
1✔
1588
        publicKeyBytes, err := base64Utils.StdEncoding.DecodeString(publicKey.GetKey())
1✔
1589
        if err != nil {
1✔
1590
                return "", err
×
1591
        }
×
1592

1593
        var publicKeyDecoded [32]byte
1✔
1594
        copy(publicKeyDecoded[:], publicKeyBytes)
1✔
1595

1✔
1596
        encrypted, err := box.SealAnonymous(nil, []byte(secretValue), &publicKeyDecoded, rand.Reader)
1✔
1597
        if err != nil {
1✔
1598
                return "", err
×
1599
        }
×
1600

1601
        encryptedBase64 := base64Utils.StdEncoding.EncodeToString(encrypted)
1✔
1602
        return encryptedBase64, nil
1✔
1603
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc