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

jfrog / froggit-go / 14219272602

02 Apr 2025 12:11PM UTC coverage: 87.316%. First build
14219272602

Pull #143

github

EyalDelarea
Pull master
Pull Request #143: Add `ListPullRequestCommits`

66 of 83 new or added lines in 5 files covered. (79.52%)

4268 of 4888 relevant lines covered (87.32%)

6.7 hits per line

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

89.48
/vcsclient/github.go
1
package vcsclient
2

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

25
const (
26
        maxRetries               = 5
27
        retriesIntervalMilliSecs = 60000
28
        // https://github.com/orgs/community/discussions/27190
29
        githubPrContentSizeLimit = 65536
30
)
31

32
var rateLimitRetryStatuses = []int{http.StatusForbidden, http.StatusTooManyRequests}
33

34
type GitHubRateLimitExecutionHandler func() (*github.Response, error)
35

36
type GitHubRateLimitRetryExecutor struct {
37
        vcsutils.RetryExecutor
38
        GitHubRateLimitExecutionHandler
39
}
40

41
func (ghe *GitHubRateLimitRetryExecutor) Execute() error {
93✔
42
        ghe.ExecutionHandler = func() (bool, error) {
186✔
43
                ghResponse, err := ghe.GitHubRateLimitExecutionHandler()
93✔
44
                return shouldRetryIfRateLimitExceeded(ghResponse, err), err
93✔
45
        }
93✔
46
        return ghe.RetryExecutor.Execute()
93✔
47
}
48

49
// GitHubClient API version 3
50
type GitHubClient struct {
51
        vcsInfo                VcsInfo
52
        rateLimitRetryExecutor GitHubRateLimitRetryExecutor
53
        logger                 vcsutils.Log
54
        ghClient               *github.Client
55
}
56

57
// NewGitHubClient create a new GitHubClient
58
func NewGitHubClient(vcsInfo VcsInfo, logger vcsutils.Log) (*GitHubClient, error) {
123✔
59
        ghClient, err := buildGithubClient(vcsInfo, logger)
123✔
60
        if err != nil {
123✔
61
                return nil, err
×
62
        }
×
63
        return &GitHubClient{
123✔
64
                        vcsInfo:  vcsInfo,
123✔
65
                        logger:   logger,
123✔
66
                        ghClient: ghClient,
123✔
67
                        rateLimitRetryExecutor: GitHubRateLimitRetryExecutor{RetryExecutor: vcsutils.RetryExecutor{
123✔
68
                                Logger:                   logger,
123✔
69
                                MaxRetries:               maxRetries,
123✔
70
                                RetriesIntervalMilliSecs: retriesIntervalMilliSecs},
123✔
71
                        }},
123✔
72
                nil
123✔
73
}
74

75
func (client *GitHubClient) runWithRateLimitRetries(handler func() (*github.Response, error)) error {
93✔
76
        client.rateLimitRetryExecutor.GitHubRateLimitExecutionHandler = handler
93✔
77
        return client.rateLimitRetryExecutor.Execute()
93✔
78
}
93✔
79

80
// TestConnection on GitHub
81
func (client *GitHubClient) TestConnection(ctx context.Context) error {
4✔
82
        _, _, err := client.ghClient.Meta.Zen(ctx)
4✔
83
        return err
4✔
84
}
4✔
85

86
func buildGithubClient(vcsInfo VcsInfo, logger vcsutils.Log) (*github.Client, error) {
123✔
87
        httpClient := &http.Client{}
123✔
88
        if vcsInfo.Token != "" {
174✔
89
                httpClient = oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(&oauth2.Token{AccessToken: vcsInfo.Token}))
51✔
90
        }
51✔
91
        ghClient := github.NewClient(httpClient)
123✔
92
        if vcsInfo.APIEndpoint != "" {
212✔
93
                baseURL, err := url.Parse(strings.TrimSuffix(vcsInfo.APIEndpoint, "/") + "/")
89✔
94
                if err != nil {
89✔
95
                        return nil, err
×
96
                }
×
97
                logger.Info("Using API endpoint:", baseURL)
89✔
98
                ghClient.BaseURL = baseURL
89✔
99
        }
100
        return ghClient, nil
123✔
101
}
102

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

115
        readOnly := permission != ReadWrite
3✔
116
        key := github.Key{
3✔
117
                Key:      &publicKey,
3✔
118
                Title:    &keyName,
3✔
119
                ReadOnly: &readOnly,
3✔
120
        }
3✔
121

3✔
122
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
123
                _, ghResponse, err := client.ghClient.Repositories.CreateKey(ctx, owner, repository, &key)
3✔
124
                return ghResponse, err
3✔
125
        })
3✔
126
}
127

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

142
                for _, repo := range repositoriesInPage {
37✔
143
                        results[*repo.Owner.Login] = append(results[*repo.Owner.Login], *repo.Name)
33✔
144
                }
33✔
145
                if nextPage+1 > ghResponse.LastPage {
7✔
146
                        break
3✔
147
                }
148
        }
149
        return
3✔
150
}
151

152
func (client *GitHubClient) executeListRepositoriesInPage(ctx context.Context, page int) ([]*github.Repository, *github.Response, error) {
6✔
153
        options := &github.RepositoryListOptions{ListOptions: github.ListOptions{Page: page}}
6✔
154
        return client.ghClient.Repositories.List(ctx, "", options)
6✔
155
}
6✔
156

157
// ListBranches on GitHub
158
func (client *GitHubClient) ListBranches(ctx context.Context, owner, repository string) (branchList []string, err error) {
2✔
159
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
160
                var ghResponse *github.Response
2✔
161
                branchList, ghResponse, err = client.executeListBranch(ctx, owner, repository)
2✔
162
                return ghResponse, err
2✔
163
        })
2✔
164
        return
2✔
165
}
166

167
func (client *GitHubClient) executeListBranch(ctx context.Context, owner, repository string) ([]string, *github.Response, error) {
2✔
168
        branches, ghResponse, err := client.ghClient.Repositories.ListBranches(ctx, owner, repository, nil)
2✔
169
        if err != nil {
3✔
170
                return []string{}, ghResponse, err
1✔
171
        }
1✔
172

173
        branchList := make([]string, 0, len(branches))
1✔
174
        for _, branch := range branches {
3✔
175
                branchList = append(branchList, *branch.Name)
2✔
176
        }
2✔
177
        return branchList, ghResponse, nil
1✔
178
}
179

180
// CreateWebhook on GitHub
181
func (client *GitHubClient) CreateWebhook(ctx context.Context, owner, repository, _, payloadURL string,
182
        webhookEvents ...vcsutils.WebhookEvent) (string, string, error) {
2✔
183
        token := vcsutils.CreateToken()
2✔
184
        hook := createGitHubHook(token, payloadURL, webhookEvents...)
2✔
185
        var ghResponseHook *github.Hook
2✔
186
        var err error
2✔
187
        if err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
188
                var ghResponse *github.Response
2✔
189
                ghResponseHook, ghResponse, err = client.ghClient.Repositories.CreateHook(ctx, owner, repository, hook)
2✔
190
                return ghResponse, err
2✔
191
        }); err != nil {
3✔
192
                return "", "", err
1✔
193
        }
1✔
194

195
        return strconv.FormatInt(*ghResponseHook.ID, 10), token, nil
1✔
196
}
197

198
// UpdateWebhook on GitHub
199
func (client *GitHubClient) UpdateWebhook(ctx context.Context, owner, repository, _, payloadURL, token,
200
        webhookID string, webhookEvents ...vcsutils.WebhookEvent) error {
2✔
201
        webhookIDInt64, err := strconv.ParseInt(webhookID, 10, 64)
2✔
202
        if err != nil {
2✔
203
                return err
×
204
        }
×
205

206
        hook := createGitHubHook(token, payloadURL, webhookEvents...)
2✔
207
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
208
                var ghResponse *github.Response
2✔
209
                _, ghResponse, err = client.ghClient.Repositories.EditHook(ctx, owner, repository, webhookIDInt64, hook)
2✔
210
                return ghResponse, err
2✔
211
        })
2✔
212
}
213

214
// DeleteWebhook on GitHub
215
func (client *GitHubClient) DeleteWebhook(ctx context.Context, owner, repository, webhookID string) error {
2✔
216
        webhookIDInt64, err := strconv.ParseInt(webhookID, 10, 64)
2✔
217
        if err != nil {
3✔
218
                return err
1✔
219
        }
1✔
220

221
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
2✔
222
                return client.ghClient.Repositories.DeleteHook(ctx, owner, repository, webhookIDInt64)
1✔
223
        })
1✔
224
}
225

226
// SetCommitStatus on GitHub
227
func (client *GitHubClient) SetCommitStatus(ctx context.Context, commitStatus CommitStatus, owner, repository, ref,
228
        title, description, detailsURL string) error {
2✔
229
        state := getGitHubCommitState(commitStatus)
2✔
230
        status := &github.RepoStatus{
2✔
231
                Context:     &title,
2✔
232
                TargetURL:   &detailsURL,
2✔
233
                State:       &state,
2✔
234
                Description: &description,
2✔
235
        }
2✔
236

2✔
237
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
238
                _, ghResponse, err := client.ghClient.Repositories.CreateStatus(ctx, owner, repository, ref, status)
2✔
239
                return ghResponse, err
2✔
240
        })
2✔
241
}
242

243
// GetCommitStatuses on GitHub
244
func (client *GitHubClient) GetCommitStatuses(ctx context.Context, owner, repository, ref string) (statusInfoList []CommitStatusInfo, err error) {
6✔
245
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
12✔
246
                var ghResponse *github.Response
6✔
247
                statusInfoList, ghResponse, err = client.executeGetCommitStatuses(ctx, owner, repository, ref)
6✔
248
                return ghResponse, err
6✔
249
        })
6✔
250
        return
6✔
251
}
252

253
func (client *GitHubClient) executeGetCommitStatuses(ctx context.Context, owner, repository, ref string) (statusInfoList []CommitStatusInfo, ghResponse *github.Response, err error) {
6✔
254
        statuses, ghResponse, err := client.ghClient.Repositories.GetCombinedStatus(ctx, owner, repository, ref, nil)
6✔
255
        if err != nil {
10✔
256
                return
4✔
257
        }
4✔
258

259
        for _, singleStatus := range statuses.Statuses {
6✔
260
                statusInfoList = append(statusInfoList, CommitStatusInfo{
4✔
261
                        State:         commitStatusAsStringToStatus(*singleStatus.State),
4✔
262
                        Description:   singleStatus.GetDescription(),
4✔
263
                        DetailsUrl:    singleStatus.GetTargetURL(),
4✔
264
                        Creator:       singleStatus.GetCreator().GetName(),
4✔
265
                        LastUpdatedAt: singleStatus.GetUpdatedAt().Time,
4✔
266
                        CreatedAt:     singleStatus.GetCreatedAt().Time,
4✔
267
                })
4✔
268
        }
4✔
269
        return
2✔
270
}
271

272
// DownloadRepository on GitHub
273
func (client *GitHubClient) DownloadRepository(ctx context.Context, owner, repository, branch, localPath string) (err error) {
2✔
274
        // Get the archive download link from GitHub
2✔
275
        var baseURL *url.URL
2✔
276
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
277
                var ghResponse *github.Response
2✔
278
                baseURL, ghResponse, err = client.executeGetArchiveLink(ctx, owner, repository, branch)
2✔
279
                return ghResponse, err
2✔
280
        })
2✔
281
        if err != nil {
3✔
282
                return
1✔
283
        }
1✔
284

285
        // Download the archive
286
        httpResponse, err := executeDownloadArchiveFromLink(baseURL.String())
1✔
287
        if err != nil {
1✔
288
                return
×
289
        }
×
290
        defer func() { err = errors.Join(err, httpResponse.Body.Close()) }()
2✔
291
        client.logger.Info(repository, vcsutils.SuccessfulRepoDownload)
1✔
292

1✔
293
        // Untar the archive
1✔
294
        if err = vcsutils.Untar(localPath, httpResponse.Body, true); err != nil {
1✔
295
                return
×
296
        }
×
297
        client.logger.Info(vcsutils.SuccessfulRepoExtraction)
1✔
298

1✔
299
        repositoryInfo, err := client.GetRepositoryInfo(ctx, owner, repository)
1✔
300
        if err != nil {
1✔
301
                return
×
302
        }
×
303
        // Create a .git folder in the archive with the remote repository HTTP clone url
304
        err = vcsutils.CreateDotGitFolderWithRemote(localPath, vcsutils.RemoteName, repositoryInfo.CloneInfo.HTTP)
1✔
305
        return
1✔
306
}
307

308
func (client *GitHubClient) executeGetArchiveLink(ctx context.Context, owner, repository, branch string) (baseURL *url.URL, ghResponse *github.Response, err error) {
2✔
309
        client.logger.Debug("Getting GitHub archive link to download")
2✔
310
        return client.ghClient.Repositories.GetArchiveLink(ctx, owner, repository, github.Tarball,
2✔
311
                &github.RepositoryContentGetOptions{Ref: branch}, 5)
2✔
312
}
2✔
313

314
func executeDownloadArchiveFromLink(baseURL string) (*http.Response, error) {
1✔
315
        httpClient := &http.Client{}
1✔
316
        req, err := http.NewRequest(http.MethodGet, baseURL, nil)
1✔
317
        if err != nil {
1✔
318
                return nil, err
×
319
        }
×
320
        httpResponse, err := httpClient.Do(req)
1✔
321
        if err != nil {
1✔
322
                return httpResponse, err
×
323
        }
×
324
        return httpResponse, vcsutils.CheckResponseStatusWithBody(httpResponse, http.StatusOK)
1✔
325
}
326

327
func (client *GitHubClient) GetPullRequestCommentSizeLimit() int {
×
328
        return githubPrContentSizeLimit
×
329
}
×
330

331
func (client *GitHubClient) GetPullRequestDetailsSizeLimit() int {
×
332
        return githubPrContentSizeLimit
×
333
}
×
334

335
// CreatePullRequest on GitHub
336
func (client *GitHubClient) CreatePullRequest(ctx context.Context, owner, repository, sourceBranch, targetBranch, title, description string) error {
2✔
337
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
338
                return client.executeCreatePullRequest(ctx, owner, repository, sourceBranch, targetBranch, title, description)
2✔
339
        })
2✔
340
}
341

342
func (client *GitHubClient) executeCreatePullRequest(ctx context.Context, owner, repository, sourceBranch, targetBranch, title, description string) (*github.Response, error) {
2✔
343
        head := owner + ":" + sourceBranch
2✔
344
        client.logger.Debug(vcsutils.CreatingPullRequest, title)
2✔
345

2✔
346
        _, ghResponse, err := client.ghClient.PullRequests.Create(ctx, owner, repository, &github.NewPullRequest{
2✔
347
                Title: &title,
2✔
348
                Body:  &description,
2✔
349
                Head:  &head,
2✔
350
                Base:  &targetBranch,
2✔
351
        })
2✔
352
        return ghResponse, err
2✔
353
}
2✔
354

355
// UpdatePullRequest on GitHub
356
func (client *GitHubClient) UpdatePullRequest(ctx context.Context, owner, repository, title, body, targetBranchName string, id int, state vcsutils.PullRequestState) error {
3✔
357
        client.logger.Debug(vcsutils.UpdatingPullRequest, id)
3✔
358
        var baseRef *github.PullRequestBranch
3✔
359
        if targetBranchName != "" {
5✔
360
                baseRef = &github.PullRequestBranch{Ref: &targetBranchName}
2✔
361
        }
2✔
362
        pullRequest := &github.PullRequest{
3✔
363
                Body:  &body,
3✔
364
                Title: &title,
3✔
365
                State: vcsutils.MapPullRequestState(&state),
3✔
366
                Base:  baseRef,
3✔
367
        }
3✔
368

3✔
369
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
370
                _, ghResponse, err := client.ghClient.PullRequests.Edit(ctx, owner, repository, id, pullRequest)
3✔
371
                return ghResponse, err
3✔
372
        })
3✔
373
}
374

375
// ListOpenPullRequestsWithBody on GitHub
376
func (client *GitHubClient) ListOpenPullRequestsWithBody(ctx context.Context, owner, repository string) ([]PullRequestInfo, error) {
1✔
377
        return client.getOpenPullRequests(ctx, owner, repository, true)
1✔
378
}
1✔
379

380
// ListOpenPullRequests on GitHub
381
func (client *GitHubClient) ListOpenPullRequests(ctx context.Context, owner, repository string) ([]PullRequestInfo, error) {
1✔
382
        return client.getOpenPullRequests(ctx, owner, repository, false)
1✔
383
}
1✔
384

385
func (client *GitHubClient) getOpenPullRequests(ctx context.Context, owner, repository string, withBody bool) ([]PullRequestInfo, error) {
2✔
386
        var pullRequests []*github.PullRequest
2✔
387
        client.logger.Debug(vcsutils.FetchingOpenPullRequests, repository)
2✔
388
        err := client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
389
                var ghResponse *github.Response
2✔
390
                var err error
2✔
391
                pullRequests, ghResponse, err = client.ghClient.PullRequests.List(ctx, owner, repository, &github.PullRequestListOptions{State: "open"})
2✔
392
                return ghResponse, err
2✔
393
        })
2✔
394
        if err != nil {
2✔
395
                return []PullRequestInfo{}, err
×
396
        }
×
397

398
        return mapGitHubPullRequestToPullRequestInfoList(pullRequests, withBody)
2✔
399
}
400

401
func (client *GitHubClient) GetPullRequestByID(ctx context.Context, owner, repository string, pullRequestId int) (PullRequestInfo, error) {
4✔
402
        var pullRequest *github.PullRequest
4✔
403
        var ghResponse *github.Response
4✔
404
        var err error
4✔
405
        client.logger.Debug(vcsutils.FetchingPullRequestById, repository)
4✔
406
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
8✔
407
                pullRequest, ghResponse, err = client.ghClient.PullRequests.Get(ctx, owner, repository, pullRequestId)
4✔
408
                return ghResponse, err
4✔
409
        })
4✔
410
        if err != nil {
7✔
411
                return PullRequestInfo{}, err
3✔
412
        }
3✔
413

414
        if err = vcsutils.CheckResponseStatusWithBody(ghResponse.Response, http.StatusOK); err != nil {
1✔
415
                return PullRequestInfo{}, err
×
416
        }
×
417

418
        return mapGitHubPullRequestToPullRequestInfo(pullRequest, false)
1✔
419
}
420

421
func mapGitHubPullRequestToPullRequestInfo(ghPullRequest *github.PullRequest, withBody bool) (PullRequestInfo, error) {
4✔
422
        var sourceBranch, targetBranch string
4✔
423
        var err1, err2 error
4✔
424
        if ghPullRequest != nil && ghPullRequest.Head != nil && ghPullRequest.Base != nil {
8✔
425
                sourceBranch, err1 = extractBranchFromLabel(vcsutils.DefaultIfNotNil(ghPullRequest.Head.Label))
4✔
426
                targetBranch, err2 = extractBranchFromLabel(vcsutils.DefaultIfNotNil(ghPullRequest.Base.Label))
4✔
427
                err := errors.Join(err1, err2)
4✔
428
                if err != nil {
4✔
429
                        return PullRequestInfo{}, err
×
430
                }
×
431
        }
432

433
        var sourceRepoName, sourceRepoOwner string
4✔
434
        if ghPullRequest.Head.Repo == nil {
4✔
435
                return PullRequestInfo{}, errors.New("the source repository information is missing when fetching the pull request details")
×
436
        }
×
437
        if ghPullRequest.Head.Repo.Owner == nil {
4✔
438
                return PullRequestInfo{}, errors.New("the source repository owner name is missing when fetching the pull request details")
×
439
        }
×
440
        sourceRepoName = vcsutils.DefaultIfNotNil(ghPullRequest.Head.Repo.Name)
4✔
441
        sourceRepoOwner = vcsutils.DefaultIfNotNil(ghPullRequest.Head.Repo.Owner.Login)
4✔
442

4✔
443
        var targetRepoName, targetRepoOwner string
4✔
444
        if ghPullRequest.Base.Repo == nil {
4✔
445
                return PullRequestInfo{}, errors.New("the target repository information is missing when fetching the pull request details")
×
446
        }
×
447
        if ghPullRequest.Base.Repo.Owner == nil {
4✔
448
                return PullRequestInfo{}, errors.New("the target repository owner name is missing when fetching the pull request details")
×
449
        }
×
450
        targetRepoName = vcsutils.DefaultIfNotNil(ghPullRequest.Base.Repo.Name)
4✔
451
        targetRepoOwner = vcsutils.DefaultIfNotNil(ghPullRequest.Base.Repo.Owner.Login)
4✔
452

4✔
453
        var body string
4✔
454
        if withBody {
5✔
455
                body = vcsutils.DefaultIfNotNil(ghPullRequest.Body)
1✔
456
        }
1✔
457

458
        return PullRequestInfo{
4✔
459
                ID:     int64(vcsutils.DefaultIfNotNil(ghPullRequest.Number)),
4✔
460
                Title:  vcsutils.DefaultIfNotNil(ghPullRequest.Title),
4✔
461
                URL:    vcsutils.DefaultIfNotNil(ghPullRequest.HTMLURL),
4✔
462
                Body:   body,
4✔
463
                Author: vcsutils.DefaultIfNotNil(ghPullRequest.User.Login),
4✔
464
                Source: BranchInfo{
4✔
465
                        Name:       sourceBranch,
4✔
466
                        Repository: sourceRepoName,
4✔
467
                        Owner:      sourceRepoOwner,
4✔
468
                },
4✔
469
                Target: BranchInfo{
4✔
470
                        Name:       targetBranch,
4✔
471
                        Repository: targetRepoName,
4✔
472
                        Owner:      targetRepoOwner,
4✔
473
                },
4✔
474
        }, nil
4✔
475
}
476

477
// Extracts branch name from the following expected label format repo:branch
478
func extractBranchFromLabel(label string) (string, error) {
8✔
479
        split := strings.Split(label, ":")
8✔
480
        if len(split) <= 1 {
8✔
481
                return "", fmt.Errorf("bad label format %s", label)
×
482
        }
×
483
        return split[1], nil
8✔
484
}
485

486
// AddPullRequestComment on GitHub
487
func (client *GitHubClient) AddPullRequestComment(ctx context.Context, owner, repository, content string, pullRequestID int) error {
6✔
488
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "content": content})
6✔
489
        if err != nil {
10✔
490
                return err
4✔
491
        }
4✔
492

493
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
494
                var ghResponse *github.Response
2✔
495
                // We use the Issues API to add a regular comment. The PullRequests API adds a code review comment.
2✔
496
                _, ghResponse, err = client.ghClient.Issues.CreateComment(ctx, owner, repository, pullRequestID, &github.IssueComment{Body: &content})
2✔
497
                return ghResponse, err
2✔
498
        })
2✔
499
}
500

501
// AddPullRequestReviewComments on GitHub
502
func (client *GitHubClient) AddPullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int, comments ...PullRequestComment) error {
2✔
503
        prID := strconv.Itoa(pullRequestID)
2✔
504
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "pullRequestID": prID})
2✔
505
        if err != nil {
2✔
506
                return err
×
507
        }
×
508
        if len(comments) == 0 {
2✔
509
                return errors.New(vcsutils.ErrNoCommentsProvided)
×
510
        }
×
511

512
        var commits []*github.RepositoryCommit
2✔
513
        var ghResponse *github.Response
2✔
514
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
515
                commits, ghResponse, err = client.ghClient.PullRequests.ListCommits(ctx, owner, repository, pullRequestID, nil)
2✔
516
                return ghResponse, err
2✔
517
        })
2✔
518
        if err != nil {
3✔
519
                return err
1✔
520
        }
1✔
521
        if len(commits) == 0 {
1✔
522
                return errors.New("could not fetch the commits list for pull request " + prID)
×
523
        }
×
524

525
        latestCommitSHA := commits[len(commits)-1].GetSHA()
1✔
526

1✔
527
        for _, comment := range comments {
3✔
528
                err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
529
                        ghResponse, err = client.executeCreatePullRequestReviewComment(ctx, owner, repository, latestCommitSHA, pullRequestID, comment)
2✔
530
                        return ghResponse, err
2✔
531
                })
2✔
532
                if err != nil {
2✔
533
                        return err
×
534
                }
×
535
        }
536
        return nil
1✔
537
}
538

539
func (client *GitHubClient) executeCreatePullRequestReviewComment(ctx context.Context, owner, repository, latestCommitSHA string, pullRequestID int, comment PullRequestComment) (*github.Response, error) {
2✔
540
        filePath := filepath.Clean(comment.NewFilePath)
2✔
541
        startLine := &comment.NewStartLine
2✔
542
        // GitHub API won't accept 'start_line' if it equals the end line
2✔
543
        if *startLine == comment.NewEndLine {
2✔
544
                startLine = nil
×
545
        }
×
546
        _, ghResponse, err := client.ghClient.PullRequests.CreateComment(ctx, owner, repository, pullRequestID, &github.PullRequestComment{
2✔
547
                CommitID:  &latestCommitSHA,
2✔
548
                Body:      &comment.Content,
2✔
549
                StartLine: startLine,
2✔
550
                Line:      &comment.NewEndLine,
2✔
551
                Path:      &filePath,
2✔
552
        })
2✔
553
        if err != nil {
2✔
554
                err = fmt.Errorf("could not create a code review comment for <%s/%s> in pull request %d. error received: %w",
×
555
                        owner, repository, pullRequestID, err)
×
556
        }
×
557
        return ghResponse, err
2✔
558
}
559

560
// ListPullRequestReviewComments on GitHub
561
func (client *GitHubClient) ListPullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int) ([]CommentInfo, error) {
2✔
562
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
2✔
563
        if err != nil {
2✔
564
                return nil, err
×
565
        }
×
566

567
        commentsInfoList := []CommentInfo{}
2✔
568
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
569
                var ghResponse *github.Response
2✔
570
                commentsInfoList, ghResponse, err = client.executeListPullRequestReviewComments(ctx, owner, repository, pullRequestID)
2✔
571
                return ghResponse, err
2✔
572
        })
2✔
573
        return commentsInfoList, err
2✔
574
}
575

576
func (client *GitHubClient) ListPullRequestCommits(ctx context.Context, owner, repository string, pullRequestID int) (commitsInfo []CommitInfo, err error) {
2✔
577
        if err = validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "pullRequestID": strconv.Itoa(pullRequestID)}); err != nil {
2✔
NEW
578
                return
×
NEW
579
        }
×
580
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
581
                commits, ghResponse, err := client.ghClient.PullRequests.ListCommits(ctx, owner, repository, pullRequestID, nil)
2✔
582
                for _, commit := range commits {
3✔
583
                        commitsInfo = append(commitsInfo, mapGitHubCommitToCommitInfo(commit))
1✔
584
                }
1✔
585
                return ghResponse, err
2✔
586
        })
587
        return commitsInfo, err
2✔
588
}
589

590
// ListPullRequestReviews on GitHub
591
func (client *GitHubClient) ListPullRequestReviews(ctx context.Context, owner, repository string, pullRequestID int) ([]PullRequestReviewDetails, error) {
2✔
592
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
2✔
593
        if err != nil {
2✔
594
                return nil, err
×
595
        }
×
596

597
        var reviews []*github.PullRequestReview
2✔
598
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
599
                var ghResponse *github.Response
2✔
600
                reviews, ghResponse, err = client.ghClient.PullRequests.ListReviews(ctx, owner, repository, pullRequestID, nil)
2✔
601
                return ghResponse, err
2✔
602
        })
2✔
603
        if err != nil {
3✔
604
                return nil, err
1✔
605
        }
1✔
606

607
        var reviewInfos []PullRequestReviewDetails
1✔
608
        for _, review := range reviews {
2✔
609
                reviewInfos = append(reviewInfos, PullRequestReviewDetails{
1✔
610
                        ID:          review.GetID(),
1✔
611
                        Reviewer:    review.GetUser().GetLogin(),
1✔
612
                        Body:        review.GetBody(),
1✔
613
                        State:       review.GetState(),
1✔
614
                        SubmittedAt: review.GetSubmittedAt().String(),
1✔
615
                        CommitID:    review.GetCommitID(),
1✔
616
                })
1✔
617
        }
1✔
618

619
        return reviewInfos, nil
1✔
620
}
621

622
func (client *GitHubClient) executeListPullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int) ([]CommentInfo, *github.Response, error) {
2✔
623
        commentsList, ghResponse, err := client.ghClient.PullRequests.ListComments(ctx, owner, repository, pullRequestID, nil)
2✔
624
        if err != nil {
3✔
625
                return []CommentInfo{}, ghResponse, err
1✔
626
        }
1✔
627
        commentsInfoList := []CommentInfo{}
1✔
628
        for _, comment := range commentsList {
2✔
629
                commentsInfoList = append(commentsInfoList, CommentInfo{
1✔
630
                        ID:      comment.GetID(),
1✔
631
                        Content: comment.GetBody(),
1✔
632
                        Created: comment.GetCreatedAt().Time,
1✔
633
                })
1✔
634
        }
1✔
635
        return commentsInfoList, ghResponse, nil
1✔
636
}
637

638
// ListPullRequestComments on GitHub
639
func (client *GitHubClient) ListPullRequestComments(ctx context.Context, owner, repository string, pullRequestID int) ([]CommentInfo, error) {
4✔
640
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
4✔
641
        if err != nil {
4✔
642
                return []CommentInfo{}, err
×
643
        }
×
644

645
        var commentsList []*github.IssueComment
4✔
646
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
8✔
647
                var ghResponse *github.Response
4✔
648
                commentsList, ghResponse, err = client.ghClient.Issues.ListComments(ctx, owner, repository, pullRequestID, &github.IssueListCommentsOptions{})
4✔
649
                return ghResponse, err
4✔
650
        })
4✔
651

652
        if err != nil {
7✔
653
                return []CommentInfo{}, err
3✔
654
        }
3✔
655

656
        return mapGitHubIssuesCommentToCommentInfoList(commentsList)
1✔
657
}
658

659
// DeletePullRequestReviewComments on GitHub
660
func (client *GitHubClient) DeletePullRequestReviewComments(ctx context.Context, owner, repository string, _ int, comments ...CommentInfo) error {
2✔
661
        for _, comment := range comments {
5✔
662
                commentID := comment.ID
3✔
663
                err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "commentID": strconv.FormatInt(commentID, 10)})
3✔
664
                if err != nil {
3✔
665
                        return err
×
666
                }
×
667

668
                err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
669
                        return client.executeDeletePullRequestReviewComment(ctx, owner, repository, commentID)
3✔
670
                })
3✔
671
                if err != nil {
4✔
672
                        return err
1✔
673
                }
1✔
674

675
        }
676
        return nil
1✔
677
}
678

679
func (client *GitHubClient) executeDeletePullRequestReviewComment(ctx context.Context, owner, repository string, commentID int64) (*github.Response, error) {
3✔
680
        ghResponse, err := client.ghClient.PullRequests.DeleteComment(ctx, owner, repository, commentID)
3✔
681
        if err != nil {
4✔
682
                err = fmt.Errorf("could not delete pull request review comment: %w", err)
1✔
683
        }
1✔
684
        return ghResponse, err
3✔
685
}
686

687
// DeletePullRequestComment on GitHub
688
func (client *GitHubClient) DeletePullRequestComment(ctx context.Context, owner, repository string, _, commentID int) error {
2✔
689
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
2✔
690
        if err != nil {
2✔
691
                return err
×
692
        }
×
693
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
694
                return client.executeDeletePullRequestComment(ctx, owner, repository, commentID)
2✔
695
        })
2✔
696
}
697

698
func (client *GitHubClient) executeDeletePullRequestComment(ctx context.Context, owner, repository string, commentID int) (*github.Response, error) {
2✔
699
        ghResponse, err := client.ghClient.Issues.DeleteComment(ctx, owner, repository, int64(commentID))
2✔
700
        if err != nil {
3✔
701
                return ghResponse, err
1✔
702
        }
1✔
703

704
        var statusCode int
1✔
705
        if ghResponse.Response != nil {
2✔
706
                statusCode = ghResponse.Response.StatusCode
1✔
707
        }
1✔
708
        if statusCode != http.StatusNoContent && statusCode != http.StatusOK {
1✔
709
                return ghResponse, fmt.Errorf("expected %d status code while received %d status code", http.StatusNoContent, ghResponse.Response.StatusCode)
×
710
        }
×
711

712
        return ghResponse, nil
1✔
713
}
714

715
// GetLatestCommit on GitHub
716
func (client *GitHubClient) GetLatestCommit(ctx context.Context, owner, repository, branch string) (CommitInfo, error) {
10✔
717
        commits, err := client.GetCommits(ctx, owner, repository, branch)
10✔
718
        if err != nil {
18✔
719
                return CommitInfo{}, err
8✔
720
        }
8✔
721
        latestCommit := CommitInfo{}
2✔
722
        if len(commits) > 0 {
4✔
723
                latestCommit = commits[0]
2✔
724
        }
2✔
725
        return latestCommit, nil
2✔
726
}
727

728
// GetCommits on GitHub
729
func (client *GitHubClient) GetCommits(ctx context.Context, owner, repository, branch string) ([]CommitInfo, error) {
12✔
730
        err := validateParametersNotBlank(map[string]string{
12✔
731
                "owner":      owner,
12✔
732
                "repository": repository,
12✔
733
                "branch":     branch,
12✔
734
        })
12✔
735
        if err != nil {
16✔
736
                return nil, err
4✔
737
        }
4✔
738

739
        var commitsInfo []CommitInfo
8✔
740
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
16✔
741
                var ghResponse *github.Response
8✔
742
                listOptions := &github.CommitsListOptions{
8✔
743
                        SHA: branch,
8✔
744
                        ListOptions: github.ListOptions{
8✔
745
                                Page:    1,
8✔
746
                                PerPage: vcsutils.NumberOfCommitsToFetch,
8✔
747
                        },
8✔
748
                }
8✔
749
                commitsInfo, ghResponse, err = client.executeGetCommits(ctx, owner, repository, listOptions)
8✔
750
                return ghResponse, err
8✔
751
        })
8✔
752
        return commitsInfo, err
8✔
753
}
754

755
// GetCommitsWithQueryOptions on GitHub
756
func (client *GitHubClient) GetCommitsWithQueryOptions(ctx context.Context, owner, repository string, listOptions GitCommitsQueryOptions) ([]CommitInfo, error) {
2✔
757
        err := validateParametersNotBlank(map[string]string{
2✔
758
                "owner":      owner,
2✔
759
                "repository": repository,
2✔
760
        })
2✔
761
        if err != nil {
2✔
762
                return nil, err
×
763
        }
×
764
        var commitsInfo []CommitInfo
2✔
765
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
766
                var ghResponse *github.Response
2✔
767
                commitsInfo, ghResponse, err = client.executeGetCommits(ctx, owner, repository, convertToGitHubCommitsListOptions(listOptions))
2✔
768
                return ghResponse, err
2✔
769
        })
2✔
770
        return commitsInfo, err
2✔
771
}
772

773
func convertToGitHubCommitsListOptions(listOptions GitCommitsQueryOptions) *github.CommitsListOptions {
2✔
774
        return &github.CommitsListOptions{
2✔
775
                Since: listOptions.Since,
2✔
776
                Until: time.Now(),
2✔
777
                ListOptions: github.ListOptions{
2✔
778
                        Page:    listOptions.Page,
2✔
779
                        PerPage: listOptions.PerPage,
2✔
780
                },
2✔
781
        }
2✔
782
}
2✔
783

784
func (client *GitHubClient) executeGetCommits(ctx context.Context, owner, repository string, listOptions *github.CommitsListOptions) ([]CommitInfo, *github.Response, error) {
10✔
785
        commits, ghResponse, err := client.ghClient.Repositories.ListCommits(ctx, owner, repository, listOptions)
10✔
786
        if err != nil {
16✔
787
                return nil, ghResponse, err
6✔
788
        }
6✔
789

790
        var commitsInfo []CommitInfo
4✔
791
        for _, commit := range commits {
11✔
792
                commitInfo := mapGitHubCommitToCommitInfo(commit)
7✔
793
                commitsInfo = append(commitsInfo, commitInfo)
7✔
794
        }
7✔
795
        return commitsInfo, ghResponse, nil
4✔
796
}
797

798
// GetRepositoryInfo on GitHub
799
func (client *GitHubClient) GetRepositoryInfo(ctx context.Context, owner, repository string) (RepositoryInfo, error) {
6✔
800
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
6✔
801
        if err != nil {
9✔
802
                return RepositoryInfo{}, err
3✔
803
        }
3✔
804

805
        var repo *github.Repository
3✔
806
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
807
                var ghResponse *github.Response
3✔
808
                repo, ghResponse, err = client.ghClient.Repositories.Get(ctx, owner, repository)
3✔
809
                return ghResponse, err
3✔
810
        })
3✔
811
        if err != nil {
4✔
812
                return RepositoryInfo{}, err
1✔
813
        }
1✔
814

815
        return RepositoryInfo{RepositoryVisibility: getGitHubRepositoryVisibility(repo), CloneInfo: CloneInfo{HTTP: repo.GetCloneURL(), SSH: repo.GetSSHURL()}}, nil
2✔
816
}
817

818
// GetCommitBySha on GitHub
819
func (client *GitHubClient) GetCommitBySha(ctx context.Context, owner, repository, sha string) (CommitInfo, error) {
7✔
820
        err := validateParametersNotBlank(map[string]string{
7✔
821
                "owner":      owner,
7✔
822
                "repository": repository,
7✔
823
                "sha":        sha,
7✔
824
        })
7✔
825
        if err != nil {
11✔
826
                return CommitInfo{}, err
4✔
827
        }
4✔
828

829
        var commit *github.RepositoryCommit
3✔
830
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
831
                var ghResponse *github.Response
3✔
832
                commit, ghResponse, err = client.ghClient.Repositories.GetCommit(ctx, owner, repository, sha, nil)
3✔
833
                return ghResponse, err
3✔
834
        })
3✔
835
        if err != nil {
5✔
836
                return CommitInfo{}, err
2✔
837
        }
2✔
838

839
        return mapGitHubCommitToCommitInfo(commit), nil
1✔
840
}
841

842
// CreateLabel on GitHub
843
func (client *GitHubClient) CreateLabel(ctx context.Context, owner, repository string, labelInfo LabelInfo) error {
6✔
844
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "LabelInfo.name": labelInfo.Name})
6✔
845
        if err != nil {
10✔
846
                return err
4✔
847
        }
4✔
848

849
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
850
                var ghResponse *github.Response
2✔
851
                _, ghResponse, err = client.ghClient.Issues.CreateLabel(ctx, owner, repository, &github.Label{
2✔
852
                        Name:        &labelInfo.Name,
2✔
853
                        Description: &labelInfo.Description,
2✔
854
                        Color:       &labelInfo.Color,
2✔
855
                })
2✔
856
                return ghResponse, err
2✔
857
        })
2✔
858
}
859

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

867
        var labelInfo *LabelInfo
3✔
868
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
869
                var ghResponse *github.Response
3✔
870
                labelInfo, ghResponse, err = client.executeGetLabel(ctx, owner, repository, name)
3✔
871
                return ghResponse, err
3✔
872
        })
3✔
873
        return labelInfo, err
3✔
874
}
875

876
func (client *GitHubClient) executeGetLabel(ctx context.Context, owner, repository, name string) (*LabelInfo, *github.Response, error) {
3✔
877
        label, ghResponse, err := client.ghClient.Issues.GetLabel(ctx, owner, repository, name)
3✔
878
        if err != nil {
5✔
879
                if ghResponse != nil && ghResponse.Response != nil && ghResponse.Response.StatusCode == http.StatusNotFound {
3✔
880
                        return nil, ghResponse, nil
1✔
881
                }
1✔
882
                return nil, ghResponse, err
1✔
883
        }
884

885
        labelInfo := &LabelInfo{
1✔
886
                Name:        *label.Name,
1✔
887
                Description: *label.Description,
1✔
888
                Color:       *label.Color,
1✔
889
        }
1✔
890
        return labelInfo, ghResponse, nil
1✔
891
}
892

893
// ListPullRequestLabels on GitHub
894
func (client *GitHubClient) ListPullRequestLabels(ctx context.Context, owner, repository string, pullRequestID int) ([]string, error) {
5✔
895
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
5✔
896
        if err != nil {
8✔
897
                return nil, err
3✔
898
        }
3✔
899

900
        results := []string{}
2✔
901
        for nextPage := 0; ; nextPage++ {
4✔
902
                options := &github.ListOptions{Page: nextPage}
2✔
903
                var labels []*github.Label
2✔
904
                var ghResponse *github.Response
2✔
905
                err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
906
                        labels, ghResponse, err = client.ghClient.Issues.ListLabelsByIssue(ctx, owner, repository, pullRequestID, options)
2✔
907
                        return ghResponse, err
2✔
908
                })
2✔
909
                if err != nil {
3✔
910
                        return nil, err
1✔
911
                }
1✔
912
                for _, label := range labels {
2✔
913
                        results = append(results, *label.Name)
1✔
914
                }
1✔
915
                if nextPage+1 >= ghResponse.LastPage {
2✔
916
                        break
1✔
917
                }
918
        }
919
        return results, nil
1✔
920
}
921

922
func (client *GitHubClient) ListPullRequestsAssociatedWithCommit(ctx context.Context, owner, repository string, commitSHA string) ([]PullRequestInfo, error) {
2✔
923
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
2✔
924
        if err != nil {
2✔
925
                return nil, err
×
926
        }
×
927

928
        var pulls []*github.PullRequest
2✔
929
        if err = client.runWithRateLimitRetries(func() (ghResponse *github.Response, err error) {
4✔
930
                pulls, ghResponse, err = client.ghClient.PullRequests.ListPullRequestsWithCommit(ctx, owner, repository, commitSHA, nil)
2✔
931
                return ghResponse, err
2✔
932
        }); err != nil {
3✔
933
                return nil, err
1✔
934
        }
1✔
935
        return mapGitHubPullRequestToPullRequestInfoList(pulls, false)
1✔
936
}
937

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

945
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
946
                return client.ghClient.Issues.RemoveLabelForIssue(ctx, owner, repository, pullRequestID, name)
2✔
947
        })
2✔
948
}
949

950
// UploadCodeScanning to GitHub Security tab
951
func (client *GitHubClient) UploadCodeScanning(ctx context.Context, owner, repository, branch, sarifContent string) (id string, err error) {
2✔
952
        commit, err := client.GetLatestCommit(ctx, owner, repository, branch)
2✔
953
        if err != nil {
3✔
954
                return
1✔
955
        }
1✔
956

957
        commitSHA := commit.Hash
1✔
958
        branch = vcsutils.AddBranchPrefix(branch)
1✔
959
        client.logger.Debug(vcsutils.UploadingCodeScanning, repository, "/", branch)
1✔
960

1✔
961
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
2✔
962
                var ghResponse *github.Response
1✔
963
                id, ghResponse, err = client.executeUploadCodeScanning(ctx, owner, repository, branch, commitSHA, sarifContent)
1✔
964
                return ghResponse, err
1✔
965
        })
1✔
966
        return
1✔
967
}
968

969
func (client *GitHubClient) executeUploadCodeScanning(ctx context.Context, owner, repository, branch, commitSHA, sarifContent string) (id string, ghResponse *github.Response, err error) {
1✔
970
        encodedSarif, err := encodeScanningResult(sarifContent)
1✔
971
        if err != nil {
1✔
972
                return
×
973
        }
×
974

975
        sarifID, ghResponse, err := client.ghClient.CodeScanning.UploadSarif(ctx, owner, repository, &github.SarifAnalysis{
1✔
976
                CommitSHA: &commitSHA,
1✔
977
                Ref:       &branch,
1✔
978
                Sarif:     &encodedSarif,
1✔
979
        })
1✔
980

1✔
981
        // According to go-github API - successful ghResponse will return 202 status code
1✔
982
        // The body of the ghResponse will appear in the error, and the Sarif struct will be empty.
1✔
983
        if err != nil && ghResponse.Response.StatusCode != http.StatusAccepted {
1✔
984
                return
×
985
        }
×
986

987
        id, err = handleGitHubUploadSarifID(sarifID, err)
1✔
988
        return
1✔
989
}
990

991
func handleGitHubUploadSarifID(sarifID *github.SarifID, uploadSarifErr error) (id string, err error) {
1✔
992
        if sarifID != nil && *sarifID.ID != "" {
2✔
993
                id = *sarifID.ID
1✔
994
                return
1✔
995
        }
1✔
996
        var result map[string]string
×
997
        var ghAcceptedError *github.AcceptedError
×
998
        if errors.As(uploadSarifErr, &ghAcceptedError) {
×
999
                if err = json.Unmarshal(ghAcceptedError.Raw, &result); err != nil {
×
1000
                        return
×
1001
                }
×
1002
                id = result["id"]
×
1003
        }
1004
        return
×
1005
}
1006

1007
// DownloadFileFromRepo on GitHub
1008
func (client *GitHubClient) DownloadFileFromRepo(ctx context.Context, owner, repository, branch, path string) (content []byte, statusCode int, err error) {
3✔
1009
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
1010
                var ghResponse *github.Response
3✔
1011
                content, statusCode, ghResponse, err = client.executeDownloadFileFromRepo(ctx, owner, repository, branch, path)
3✔
1012
                return ghResponse, err
3✔
1013
        })
3✔
1014
        return
3✔
1015
}
1016

1017
func (client *GitHubClient) executeDownloadFileFromRepo(ctx context.Context, owner, repository, branch, path string) (content []byte, statusCode int, ghResponse *github.Response, err error) {
3✔
1018
        body, ghResponse, err := client.ghClient.Repositories.DownloadContents(ctx, owner, repository, path, &github.RepositoryContentGetOptions{Ref: branch})
3✔
1019
        defer func() {
6✔
1020
                if body != nil {
4✔
1021
                        err = errors.Join(err, body.Close())
1✔
1022
                }
1✔
1023
        }()
1024

1025
        if ghResponse == nil || ghResponse.Response == nil {
4✔
1026
                return
1✔
1027
        }
1✔
1028

1029
        statusCode = ghResponse.StatusCode
2✔
1030
        if err != nil && statusCode != http.StatusOK {
2✔
1031
                err = fmt.Errorf("expected %d status code while received %d status code with error:\n%s", http.StatusOK, ghResponse.StatusCode, err)
×
1032
                return
×
1033
        }
×
1034

1035
        if body != nil {
3✔
1036
                content, err = io.ReadAll(body)
1✔
1037
        }
1✔
1038
        return
2✔
1039
}
1040

1041
// GetRepositoryEnvironmentInfo on GitHub
1042
func (client *GitHubClient) GetRepositoryEnvironmentInfo(ctx context.Context, owner, repository, name string) (RepositoryEnvironmentInfo, error) {
2✔
1043
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "name": name})
2✔
1044
        if err != nil {
2✔
1045
                return RepositoryEnvironmentInfo{}, err
×
1046
        }
×
1047

1048
        var repositoryEnvInfo *RepositoryEnvironmentInfo
2✔
1049
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1050
                var ghResponse *github.Response
2✔
1051
                repositoryEnvInfo, ghResponse, err = client.executeGetRepositoryEnvironmentInfo(ctx, owner, repository, name)
2✔
1052
                return ghResponse, err
2✔
1053
        })
2✔
1054
        return *repositoryEnvInfo, err
2✔
1055
}
1056

1057
func (client *GitHubClient) executeGetRepositoryEnvironmentInfo(ctx context.Context, owner, repository, name string) (*RepositoryEnvironmentInfo, *github.Response, error) {
2✔
1058
        environment, ghResponse, err := client.ghClient.Repositories.GetEnvironment(ctx, owner, repository, name)
2✔
1059
        if err != nil {
3✔
1060
                return &RepositoryEnvironmentInfo{}, ghResponse, err
1✔
1061
        }
1✔
1062

1063
        if err = vcsutils.CheckResponseStatusWithBody(ghResponse.Response, http.StatusOK); err != nil {
1✔
1064
                return &RepositoryEnvironmentInfo{}, ghResponse, err
×
1065
        }
×
1066

1067
        reviewers, err := extractGitHubEnvironmentReviewers(environment)
1✔
1068
        if err != nil {
1✔
1069
                return &RepositoryEnvironmentInfo{}, ghResponse, err
×
1070
        }
×
1071

1072
        return &RepositoryEnvironmentInfo{
1✔
1073
                        Name:      environment.GetName(),
1✔
1074
                        Url:       environment.GetURL(),
1✔
1075
                        Reviewers: reviewers,
1✔
1076
                },
1✔
1077
                ghResponse,
1✔
1078
                nil
1✔
1079
}
1080

1081
func (client *GitHubClient) GetModifiedFiles(ctx context.Context, owner, repository, refBefore, refAfter string) ([]string, error) {
6✔
1082
        err := validateParametersNotBlank(map[string]string{
6✔
1083
                "owner":      owner,
6✔
1084
                "repository": repository,
6✔
1085
                "refBefore":  refBefore,
6✔
1086
                "refAfter":   refAfter,
6✔
1087
        })
6✔
1088
        if err != nil {
10✔
1089
                return nil, err
4✔
1090
        }
4✔
1091

1092
        var fileNamesList []string
2✔
1093
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1094
                var ghResponse *github.Response
2✔
1095
                fileNamesList, ghResponse, err = client.executeGetModifiedFiles(ctx, owner, repository, refBefore, refAfter)
2✔
1096
                return ghResponse, err
2✔
1097
        })
2✔
1098
        return fileNamesList, err
2✔
1099
}
1100

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

2✔
1108
        comparison, ghResponse, err := client.ghClient.Repositories.CompareCommits(ctx, owner, repository, refBefore, refAfter, listOptions)
2✔
1109
        if err != nil {
3✔
1110
                return nil, ghResponse, err
1✔
1111
        }
1✔
1112

1113
        if err = vcsutils.CheckResponseStatusWithBody(ghResponse.Response, http.StatusOK); err != nil {
1✔
1114
                return nil, ghResponse, err
×
1115
        }
×
1116

1117
        fileNamesSet := datastructures.MakeSet[string]()
1✔
1118
        for _, file := range comparison.Files {
18✔
1119
                fileNamesSet.Add(vcsutils.DefaultIfNotNil(file.Filename))
17✔
1120
                fileNamesSet.Add(vcsutils.DefaultIfNotNil(file.PreviousFilename))
17✔
1121
        }
17✔
1122

1123
        _ = fileNamesSet.Remove("") // Make sure there are no blank filepath.
1✔
1124
        fileNamesList := fileNamesSet.ToSlice()
1✔
1125
        sort.Strings(fileNamesList)
1✔
1126

1✔
1127
        return fileNamesList, ghResponse, nil
1✔
1128
}
1129

1130
// Extract code reviewers from environment
1131
func extractGitHubEnvironmentReviewers(environment *github.Environment) ([]string, error) {
2✔
1132
        var reviewers []string
2✔
1133
        protectionRules := environment.ProtectionRules
2✔
1134
        if protectionRules == nil {
2✔
1135
                return reviewers, nil
×
1136
        }
×
1137
        reviewerStruct := repositoryEnvironmentReviewer{}
2✔
1138
        for _, rule := range protectionRules {
4✔
1139
                for _, reviewer := range rule.Reviewers {
5✔
1140
                        if err := mapstructure.Decode(reviewer.Reviewer, &reviewerStruct); err != nil {
3✔
1141
                                return []string{}, err
×
1142
                        }
×
1143
                        reviewers = append(reviewers, reviewerStruct.Login)
3✔
1144
                }
1145
        }
1146
        return reviewers, nil
2✔
1147
}
1148

1149
func createGitHubHook(token, payloadURL string, webhookEvents ...vcsutils.WebhookEvent) *github.Hook {
4✔
1150
        return &github.Hook{
4✔
1151
                Events: getGitHubWebhookEvents(webhookEvents...),
4✔
1152
                Config: map[string]interface{}{
4✔
1153
                        "url":          payloadURL,
4✔
1154
                        "content_type": "json",
4✔
1155
                        "secret":       token,
4✔
1156
                },
4✔
1157
        }
4✔
1158
}
4✔
1159

1160
// Get varargs of webhook events and return a slice of GitHub webhook events
1161
func getGitHubWebhookEvents(webhookEvents ...vcsutils.WebhookEvent) []string {
4✔
1162
        events := datastructures.MakeSet[string]()
4✔
1163
        for _, event := range webhookEvents {
16✔
1164
                switch event {
12✔
1165
                case vcsutils.PrOpened, vcsutils.PrEdited, vcsutils.PrMerged, vcsutils.PrRejected:
8✔
1166
                        events.Add("pull_request")
8✔
1167
                case vcsutils.Push, vcsutils.TagPushed, vcsutils.TagRemoved:
4✔
1168
                        events.Add("push")
4✔
1169
                }
1170
        }
1171
        return events.ToSlice()
4✔
1172
}
1173

1174
func getGitHubRepositoryVisibility(repo *github.Repository) RepositoryVisibility {
5✔
1175
        switch *repo.Visibility {
5✔
1176
        case "public":
3✔
1177
                return Public
3✔
1178
        case "internal":
1✔
1179
                return Internal
1✔
1180
        default:
1✔
1181
                return Private
1✔
1182
        }
1183
}
1184

1185
func getGitHubCommitState(commitState CommitStatus) string {
7✔
1186
        switch commitState {
7✔
1187
        case Pass:
1✔
1188
                return "success"
1✔
1189
        case Fail:
1✔
1190
                return "failure"
1✔
1191
        case Error:
3✔
1192
                return "error"
3✔
1193
        case InProgress:
1✔
1194
                return "pending"
1✔
1195
        }
1196
        return ""
1✔
1197
}
1198

1199
func mapGitHubCommitToCommitInfo(commit *github.RepositoryCommit) CommitInfo {
9✔
1200
        parents := make([]string, len(commit.Parents))
9✔
1201
        for i, c := range commit.Parents {
17✔
1202
                parents[i] = c.GetSHA()
8✔
1203
        }
8✔
1204
        details := commit.GetCommit()
9✔
1205
        return CommitInfo{
9✔
1206
                Hash:          commit.GetSHA(),
9✔
1207
                AuthorName:    details.GetAuthor().GetName(),
9✔
1208
                CommitterName: details.GetCommitter().GetName(),
9✔
1209
                Url:           commit.GetURL(),
9✔
1210
                Timestamp:     details.GetCommitter().GetDate().UTC().Unix(),
9✔
1211
                Message:       details.GetMessage(),
9✔
1212
                ParentHashes:  parents,
9✔
1213
                AuthorEmail:   details.GetAuthor().GetEmail(),
9✔
1214
        }
9✔
1215
}
1216

1217
func mapGitHubIssuesCommentToCommentInfoList(commentsList []*github.IssueComment) (res []CommentInfo, err error) {
1✔
1218
        for _, comment := range commentsList {
3✔
1219
                res = append(res, CommentInfo{
2✔
1220
                        ID:      comment.GetID(),
2✔
1221
                        Content: comment.GetBody(),
2✔
1222
                        Created: comment.GetCreatedAt().Time,
2✔
1223
                })
2✔
1224
        }
2✔
1225
        return
1✔
1226
}
1227

1228
func mapGitHubPullRequestToPullRequestInfoList(pullRequestList []*github.PullRequest, withBody bool) (res []PullRequestInfo, err error) {
3✔
1229
        var mappedPullRequest PullRequestInfo
3✔
1230
        for _, pullRequest := range pullRequestList {
6✔
1231
                mappedPullRequest, err = mapGitHubPullRequestToPullRequestInfo(pullRequest, withBody)
3✔
1232
                if err != nil {
3✔
1233
                        return
×
1234
                }
×
1235
                res = append(res, mappedPullRequest)
3✔
1236
        }
1237
        return
3✔
1238
}
1239

1240
func encodeScanningResult(data string) (string, error) {
1✔
1241
        compressedScan, err := base64.EncodeGzip([]byte(data), 6)
1✔
1242
        if err != nil {
1✔
1243
                return "", err
×
1244
        }
×
1245

1246
        return compressedScan, err
1✔
1247
}
1248

1249
type repositoryEnvironmentReviewer struct {
1250
        Login string `mapstructure:"login"`
1251
}
1252

1253
func shouldRetryIfRateLimitExceeded(ghResponse *github.Response, requestError error) bool {
97✔
1254
        if ghResponse == nil || ghResponse.Response == nil {
134✔
1255
                return false
37✔
1256
        }
37✔
1257

1258
        if !slices.Contains(rateLimitRetryStatuses, ghResponse.StatusCode) {
118✔
1259
                return false
58✔
1260
        }
58✔
1261

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

1268
        body, err := io.ReadAll(ghResponse.Body)
1✔
1269
        if err != nil {
1✔
1270
                return false
×
1271
        }
×
1272
        return strings.Contains(string(body), "rate limit")
1✔
1273
}
1274

1275
func isRateLimitAbuseError(requestError error) bool {
4✔
1276
        var abuseRateLimitError *github.AbuseRateLimitError
4✔
1277
        var rateLimitError *github.RateLimitError
4✔
1278
        return errors.As(requestError, &abuseRateLimitError) || errors.As(requestError, &rateLimitError)
4✔
1279
}
4✔
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