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

jfrog / froggit-go / 13966472230

20 Mar 2025 09:49AM UTC coverage: 86.613% (-1.1%) from 87.742%
13966472230

Pull #145

github

EyalDelarea
Add gitlab
Pull Request #145: Add `ListPullRequestReviews` & `ListPullRequestsAssociatedWithCommit`

42 of 109 new or added lines in 5 files covered. (38.53%)

263 existing lines in 6 files now uncovered.

4115 of 4751 relevant lines covered (86.61%)

6.61 hits per line

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

86.58
/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 {
87✔
42
        ghe.ExecutionHandler = func() (bool, error) {
174✔
43
                ghResponse, err := ghe.GitHubRateLimitExecutionHandler()
87✔
44
                return shouldRetryIfRateLimitExceeded(ghResponse, err), err
87✔
45
        }
87✔
46
        return ghe.RetryExecutor.Execute()
87✔
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) {
117✔
59
        ghClient, err := buildGithubClient(vcsInfo, logger)
117✔
60
        if err != nil {
117✔
61
                return nil, err
×
62
        }
×
63
        return &GitHubClient{
117✔
64
                        vcsInfo:  vcsInfo,
117✔
65
                        logger:   logger,
117✔
66
                        ghClient: ghClient,
117✔
67
                        rateLimitRetryExecutor: GitHubRateLimitRetryExecutor{RetryExecutor: vcsutils.RetryExecutor{
117✔
68
                                Logger:                   logger,
117✔
69
                                MaxRetries:               maxRetries,
117✔
70
                                RetriesIntervalMilliSecs: retriesIntervalMilliSecs},
117✔
71
                        }},
117✔
72
                nil
117✔
73
}
74

75
func (client *GitHubClient) runWithRateLimitRetries(handler func() (*github.Response, error)) error {
87✔
76
        client.rateLimitRetryExecutor.GitHubRateLimitExecutionHandler = handler
87✔
77
        return client.rateLimitRetryExecutor.Execute()
87✔
78
}
87✔
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) {
117✔
87
        httpClient := &http.Client{}
117✔
88
        if vcsInfo.Token != "" {
164✔
89
                httpClient = oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(&oauth2.Token{AccessToken: vcsInfo.Token}))
47✔
90
        }
47✔
91
        ghClient := github.NewClient(httpClient)
117✔
92
        if vcsInfo.APIEndpoint != "" {
200✔
93
                baseURL, err := url.Parse(strings.TrimSuffix(vcsInfo.APIEndpoint, "/") + "/")
83✔
94
                if err != nil {
83✔
95
                        return nil, err
×
96
                }
×
97
                logger.Info("Using API endpoint:", baseURL)
83✔
98
                ghClient.BaseURL = baseURL
83✔
99
        }
100
        return ghClient, nil
117✔
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 {
6✔
411
                return PullRequestInfo{}, err
2✔
412
        }
2✔
413

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

418
        return mapGitHubPullRequestToPullRequestInfo(pullRequest, false)
2✔
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 {
5✔
429
                        return PullRequestInfo{}, err
1✔
430
                }
1✔
431
        }
432

433
        var sourceRepoName, sourceRepoOwner string
3✔
434
        if ghPullRequest.Head.Repo == nil {
3✔
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 {
3✔
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)
3✔
441
        sourceRepoOwner = vcsutils.DefaultIfNotNil(ghPullRequest.Head.Repo.Owner.Login)
3✔
442

3✔
443
        var targetRepoName, targetRepoOwner string
3✔
444
        if ghPullRequest.Base.Repo == nil {
3✔
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 {
3✔
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)
3✔
451
        targetRepoOwner = vcsutils.DefaultIfNotNil(ghPullRequest.Base.Repo.Owner.Login)
3✔
452

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

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

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

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

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

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

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

523
        latestCommitSHA := commits[len(commits)-1].GetSHA()
1✔
524

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

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

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

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

574
// ListPullRequestReviews on GitHub
NEW
575
func (client *GitHubClient) ListPullRequestReviews(ctx context.Context, owner, repository string, pullRequestID int) ([]PullRequestReviewDetails, error) {
×
NEW
576
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
×
NEW
577
        if err != nil {
×
NEW
578
                return nil, err
×
NEW
579
        }
×
580

NEW
581
        var reviews []*github.PullRequestReview
×
NEW
582
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
×
NEW
583
                var ghResponse *github.Response
×
NEW
584
                reviews, ghResponse, err = client.ghClient.PullRequests.ListReviews(ctx, owner, repository, pullRequestID, nil)
×
NEW
585
                return ghResponse, err
×
NEW
586
        })
×
NEW
587
        if err != nil {
×
NEW
588
                return nil, err
×
NEW
589
        }
×
590

NEW
591
        var reviewInfos []PullRequestReviewDetails
×
NEW
592
        for _, review := range reviews {
×
NEW
593
                reviewInfos = append(reviewInfos, PullRequestReviewDetails{
×
NEW
594
                        ID:          review.GetID(),
×
NEW
595
                        Reviewer:    review.GetUser().GetLogin(),
×
NEW
596
                        Body:        review.GetBody(),
×
NEW
597
                        State:       review.GetState(),
×
NEW
598
                        SubmittedAt: review.GetSubmittedAt().String(),
×
NEW
599
                        CommitID:    review.GetCommitID(),
×
NEW
600
                })
×
NEW
601
        }
×
602

NEW
603
        return reviewInfos, nil
×
604
}
605

606
func (client *GitHubClient) executeListPullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int) ([]CommentInfo, *github.Response, error) {
2✔
607
        commentsList, ghResponse, err := client.ghClient.PullRequests.ListComments(ctx, owner, repository, pullRequestID, nil)
2✔
608
        if err != nil {
3✔
609
                return []CommentInfo{}, ghResponse, err
1✔
610
        }
1✔
611
        commentsInfoList := []CommentInfo{}
1✔
612
        for _, comment := range commentsList {
2✔
613
                commentsInfoList = append(commentsInfoList, CommentInfo{
1✔
614
                        ID:      comment.GetID(),
1✔
615
                        Content: comment.GetBody(),
1✔
616
                        Created: comment.GetCreatedAt().Time,
1✔
617
                })
1✔
618
        }
1✔
619
        return commentsInfoList, ghResponse, nil
1✔
620
}
621

622
// ListPullRequestComments on GitHub
623
func (client *GitHubClient) ListPullRequestComments(ctx context.Context, owner, repository string, pullRequestID int) ([]CommentInfo, error) {
4✔
624
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
4✔
625
        if err != nil {
4✔
626
                return []CommentInfo{}, err
×
627
        }
×
628

629
        var commentsList []*github.IssueComment
4✔
630
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
8✔
631
                var ghResponse *github.Response
4✔
632
                commentsList, ghResponse, err = client.ghClient.Issues.ListComments(ctx, owner, repository, pullRequestID, &github.IssueListCommentsOptions{})
4✔
633
                return ghResponse, err
4✔
634
        })
4✔
635

636
        if err != nil {
7✔
637
                return []CommentInfo{}, err
3✔
638
        }
3✔
639

640
        return mapGitHubIssuesCommentToCommentInfoList(commentsList)
1✔
641
}
642

643
// DeletePullRequestReviewComments on GitHub
644
func (client *GitHubClient) DeletePullRequestReviewComments(ctx context.Context, owner, repository string, _ int, comments ...CommentInfo) error {
2✔
645
        for _, comment := range comments {
5✔
646
                commentID := comment.ID
3✔
647
                err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "commentID": strconv.FormatInt(commentID, 10)})
3✔
648
                if err != nil {
3✔
649
                        return err
×
650
                }
×
651

652
                err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
653
                        return client.executeDeletePullRequestReviewComment(ctx, owner, repository, commentID)
3✔
654
                })
3✔
655
                if err != nil {
4✔
656
                        return err
1✔
657
                }
1✔
658

659
        }
660
        return nil
1✔
661
}
662

663
func (client *GitHubClient) executeDeletePullRequestReviewComment(ctx context.Context, owner, repository string, commentID int64) (*github.Response, error) {
3✔
664
        ghResponse, err := client.ghClient.PullRequests.DeleteComment(ctx, owner, repository, commentID)
3✔
665
        if err != nil {
4✔
666
                err = fmt.Errorf("could not delete pull request review comment: %w", err)
1✔
667
        }
1✔
668
        return ghResponse, err
3✔
669
}
670

671
// DeletePullRequestComment on GitHub
672
func (client *GitHubClient) DeletePullRequestComment(ctx context.Context, owner, repository string, _, commentID int) error {
2✔
673
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
2✔
674
        if err != nil {
2✔
675
                return err
×
676
        }
×
677
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
678
                return client.executeDeletePullRequestComment(ctx, owner, repository, commentID)
2✔
679
        })
2✔
680
}
681

682
func (client *GitHubClient) executeDeletePullRequestComment(ctx context.Context, owner, repository string, commentID int) (*github.Response, error) {
2✔
683
        ghResponse, err := client.ghClient.Issues.DeleteComment(ctx, owner, repository, int64(commentID))
2✔
684
        if err != nil {
3✔
685
                return ghResponse, err
1✔
686
        }
1✔
687

688
        var statusCode int
1✔
689
        if ghResponse.Response != nil {
2✔
690
                statusCode = ghResponse.Response.StatusCode
1✔
691
        }
1✔
692
        if statusCode != http.StatusNoContent && statusCode != http.StatusOK {
1✔
693
                return ghResponse, fmt.Errorf("expected %d status code while received %d status code", http.StatusNoContent, ghResponse.Response.StatusCode)
×
694
        }
×
695

696
        return ghResponse, nil
1✔
697
}
698

699
// GetLatestCommit on GitHub
700
func (client *GitHubClient) GetLatestCommit(ctx context.Context, owner, repository, branch string) (CommitInfo, error) {
10✔
701
        commits, err := client.GetCommits(ctx, owner, repository, branch)
10✔
702
        if err != nil {
18✔
703
                return CommitInfo{}, err
8✔
704
        }
8✔
705
        latestCommit := CommitInfo{}
2✔
706
        if len(commits) > 0 {
4✔
707
                latestCommit = commits[0]
2✔
708
        }
2✔
709
        return latestCommit, nil
2✔
710
}
711

712
// GetCommits on GitHub
713
func (client *GitHubClient) GetCommits(ctx context.Context, owner, repository, branch string) ([]CommitInfo, error) {
12✔
714
        err := validateParametersNotBlank(map[string]string{
12✔
715
                "owner":      owner,
12✔
716
                "repository": repository,
12✔
717
                "branch":     branch,
12✔
718
        })
12✔
719
        if err != nil {
16✔
720
                return nil, err
4✔
721
        }
4✔
722

723
        var commitsInfo []CommitInfo
8✔
724
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
16✔
725
                var ghResponse *github.Response
8✔
726
                listOptions := &github.CommitsListOptions{
8✔
727
                        SHA: branch,
8✔
728
                        ListOptions: github.ListOptions{
8✔
729
                                Page:    1,
8✔
730
                                PerPage: vcsutils.NumberOfCommitsToFetch,
8✔
731
                        },
8✔
732
                }
8✔
733
                commitsInfo, ghResponse, err = client.executeGetCommits(ctx, owner, repository, listOptions)
8✔
734
                return ghResponse, err
8✔
735
        })
8✔
736
        return commitsInfo, err
8✔
737
}
738

739
// GetCommitsWithQueryOptions on GitHub
740
func (client *GitHubClient) GetCommitsWithQueryOptions(ctx context.Context, owner, repository string, listOptions GitCommitsQueryOptions) ([]CommitInfo, error) {
2✔
741
        err := validateParametersNotBlank(map[string]string{
2✔
742
                "owner":      owner,
2✔
743
                "repository": repository,
2✔
744
        })
2✔
745
        if err != nil {
2✔
746
                return nil, err
×
747
        }
×
748
        var commitsInfo []CommitInfo
2✔
749
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
750
                var ghResponse *github.Response
2✔
751
                commitsInfo, ghResponse, err = client.executeGetCommits(ctx, owner, repository, convertToGitHubCommitsListOptions(listOptions))
2✔
752
                return ghResponse, err
2✔
753
        })
2✔
754
        return commitsInfo, err
2✔
755
}
756

757
func convertToGitHubCommitsListOptions(listOptions GitCommitsQueryOptions) *github.CommitsListOptions {
2✔
758
        return &github.CommitsListOptions{
2✔
759
                Since: listOptions.Since,
2✔
760
                Until: time.Now(),
2✔
761
                ListOptions: github.ListOptions{
2✔
762
                        Page:    listOptions.Page,
2✔
763
                        PerPage: listOptions.PerPage,
2✔
764
                },
2✔
765
        }
2✔
766
}
2✔
767

768
func (client *GitHubClient) executeGetCommits(ctx context.Context, owner, repository string, listOptions *github.CommitsListOptions) ([]CommitInfo, *github.Response, error) {
10✔
769
        commits, ghResponse, err := client.ghClient.Repositories.ListCommits(ctx, owner, repository, listOptions)
10✔
770
        if err != nil {
16✔
771
                return nil, ghResponse, err
6✔
772
        }
6✔
773

774
        var commitsInfo []CommitInfo
4✔
775
        for _, commit := range commits {
11✔
776
                commitInfo := mapGitHubCommitToCommitInfo(commit)
7✔
777
                commitsInfo = append(commitsInfo, commitInfo)
7✔
778
        }
7✔
779
        return commitsInfo, ghResponse, nil
4✔
780
}
781

782
// GetRepositoryInfo on GitHub
783
func (client *GitHubClient) GetRepositoryInfo(ctx context.Context, owner, repository string) (RepositoryInfo, error) {
6✔
784
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
6✔
785
        if err != nil {
9✔
786
                return RepositoryInfo{}, err
3✔
787
        }
3✔
788

789
        var repo *github.Repository
3✔
790
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
791
                var ghResponse *github.Response
3✔
792
                repo, ghResponse, err = client.ghClient.Repositories.Get(ctx, owner, repository)
3✔
793
                return ghResponse, err
3✔
794
        })
3✔
795
        if err != nil {
4✔
796
                return RepositoryInfo{}, err
1✔
797
        }
1✔
798

799
        return RepositoryInfo{RepositoryVisibility: getGitHubRepositoryVisibility(repo), CloneInfo: CloneInfo{HTTP: repo.GetCloneURL(), SSH: repo.GetSSHURL()}}, nil
2✔
800
}
801

802
// GetCommitBySha on GitHub
803
func (client *GitHubClient) GetCommitBySha(ctx context.Context, owner, repository, sha string) (CommitInfo, error) {
7✔
804
        err := validateParametersNotBlank(map[string]string{
7✔
805
                "owner":      owner,
7✔
806
                "repository": repository,
7✔
807
                "sha":        sha,
7✔
808
        })
7✔
809
        if err != nil {
11✔
810
                return CommitInfo{}, err
4✔
811
        }
4✔
812

813
        var commit *github.RepositoryCommit
3✔
814
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
815
                var ghResponse *github.Response
3✔
816
                commit, ghResponse, err = client.ghClient.Repositories.GetCommit(ctx, owner, repository, sha, nil)
3✔
817
                return ghResponse, err
3✔
818
        })
3✔
819
        if err != nil {
5✔
820
                return CommitInfo{}, err
2✔
821
        }
2✔
822

823
        return mapGitHubCommitToCommitInfo(commit), nil
1✔
824
}
825

826
// CreateLabel on GitHub
827
func (client *GitHubClient) CreateLabel(ctx context.Context, owner, repository string, labelInfo LabelInfo) error {
6✔
828
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "LabelInfo.name": labelInfo.Name})
6✔
829
        if err != nil {
10✔
830
                return err
4✔
831
        }
4✔
832

833
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
834
                var ghResponse *github.Response
2✔
835
                _, ghResponse, err = client.ghClient.Issues.CreateLabel(ctx, owner, repository, &github.Label{
2✔
836
                        Name:        &labelInfo.Name,
2✔
837
                        Description: &labelInfo.Description,
2✔
838
                        Color:       &labelInfo.Color,
2✔
839
                })
2✔
840
                return ghResponse, err
2✔
841
        })
2✔
842
}
843

844
// GetLabel on GitHub
845
func (client *GitHubClient) GetLabel(ctx context.Context, owner, repository, name string) (*LabelInfo, error) {
7✔
846
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "name": name})
7✔
847
        if err != nil {
11✔
848
                return nil, err
4✔
849
        }
4✔
850

851
        var labelInfo *LabelInfo
3✔
852
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
853
                var ghResponse *github.Response
3✔
854
                labelInfo, ghResponse, err = client.executeGetLabel(ctx, owner, repository, name)
3✔
855
                return ghResponse, err
3✔
856
        })
3✔
857
        return labelInfo, err
3✔
858
}
859

860
func (client *GitHubClient) executeGetLabel(ctx context.Context, owner, repository, name string) (*LabelInfo, *github.Response, error) {
3✔
861
        label, ghResponse, err := client.ghClient.Issues.GetLabel(ctx, owner, repository, name)
3✔
862
        if err != nil {
5✔
863
                if ghResponse != nil && ghResponse.Response != nil && ghResponse.Response.StatusCode == http.StatusNotFound {
3✔
864
                        return nil, ghResponse, nil
1✔
865
                }
1✔
866
                return nil, ghResponse, err
1✔
867
        }
868

869
        labelInfo := &LabelInfo{
1✔
870
                Name:        *label.Name,
1✔
871
                Description: *label.Description,
1✔
872
                Color:       *label.Color,
1✔
873
        }
1✔
874
        return labelInfo, ghResponse, nil
1✔
875
}
876

877
// ListPullRequestLabels on GitHub
878
func (client *GitHubClient) ListPullRequestLabels(ctx context.Context, owner, repository string, pullRequestID int) ([]string, error) {
5✔
879
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
5✔
880
        if err != nil {
8✔
881
                return nil, err
3✔
882
        }
3✔
883

884
        results := []string{}
2✔
885
        for nextPage := 0; ; nextPage++ {
4✔
886
                options := &github.ListOptions{Page: nextPage}
2✔
887
                var labels []*github.Label
2✔
888
                var ghResponse *github.Response
2✔
889
                err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
890
                        labels, ghResponse, err = client.ghClient.Issues.ListLabelsByIssue(ctx, owner, repository, pullRequestID, options)
2✔
891
                        return ghResponse, err
2✔
892
                })
2✔
893
                if err != nil {
3✔
894
                        return nil, err
1✔
895
                }
1✔
896
                for _, label := range labels {
2✔
897
                        results = append(results, *label.Name)
1✔
898
                }
1✔
899
                if nextPage+1 >= ghResponse.LastPage {
2✔
900
                        break
1✔
901
                }
902
        }
903
        return results, nil
1✔
904
}
905

NEW
UNCOV
906
func (client *GitHubClient) ListPullRequestsAssociatedWithCommit(ctx context.Context, owner, repository string, commitSHA string) ([]PullRequestInfo, error) {
×
NEW
907
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
×
NEW
908
        if err != nil {
×
NEW
909
                return nil, err
×
NEW
910
        }
×
911

NEW
912
        pulls, _, err := client.ghClient.PullRequests.ListPullRequestsWithCommit(ctx, owner, repository, commitSHA, nil)
×
NEW
913
        if err != nil {
×
NEW
914
                return nil, err
×
NEW
915
        }
×
NEW
916
        return mapGitHubPullRequestToPullRequestInfoList(pulls, false)
×
917
}
918

919
// UnlabelPullRequest on GitHub
920
func (client *GitHubClient) UnlabelPullRequest(ctx context.Context, owner, repository, name string, pullRequestID int) error {
5✔
921
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
5✔
922
        if err != nil {
8✔
923
                return err
3✔
924
        }
3✔
925

926
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
927
                return client.ghClient.Issues.RemoveLabelForIssue(ctx, owner, repository, pullRequestID, name)
2✔
928
        })
2✔
929
}
930

931
// UploadCodeScanning to GitHub Security tab
932
func (client *GitHubClient) UploadCodeScanning(ctx context.Context, owner, repository, branch, sarifContent string) (id string, err error) {
2✔
933
        commit, err := client.GetLatestCommit(ctx, owner, repository, branch)
2✔
934
        if err != nil {
3✔
935
                return
1✔
936
        }
1✔
937

938
        commitSHA := commit.Hash
1✔
939
        branch = vcsutils.AddBranchPrefix(branch)
1✔
940
        client.logger.Debug(vcsutils.UploadingCodeScanning, repository, "/", branch)
1✔
941

1✔
942
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
2✔
943
                var ghResponse *github.Response
1✔
944
                id, ghResponse, err = client.executeUploadCodeScanning(ctx, owner, repository, branch, commitSHA, sarifContent)
1✔
945
                return ghResponse, err
1✔
946
        })
1✔
947
        return
1✔
948
}
949

950
func (client *GitHubClient) executeUploadCodeScanning(ctx context.Context, owner, repository, branch, commitSHA, sarifContent string) (id string, ghResponse *github.Response, err error) {
1✔
951
        encodedSarif, err := encodeScanningResult(sarifContent)
1✔
952
        if err != nil {
1✔
UNCOV
953
                return
×
UNCOV
954
        }
×
955

956
        sarifID, ghResponse, err := client.ghClient.CodeScanning.UploadSarif(ctx, owner, repository, &github.SarifAnalysis{
1✔
957
                CommitSHA: &commitSHA,
1✔
958
                Ref:       &branch,
1✔
959
                Sarif:     &encodedSarif,
1✔
960
        })
1✔
961

1✔
962
        // According to go-github API - successful ghResponse will return 202 status code
1✔
963
        // The body of the ghResponse will appear in the error, and the Sarif struct will be empty.
1✔
964
        if err != nil && ghResponse.Response.StatusCode != http.StatusAccepted {
1✔
UNCOV
965
                return
×
UNCOV
966
        }
×
967

968
        id, err = handleGitHubUploadSarifID(sarifID, err)
1✔
969
        return
1✔
970
}
971

972
func handleGitHubUploadSarifID(sarifID *github.SarifID, uploadSarifErr error) (id string, err error) {
1✔
973
        if sarifID != nil && *sarifID.ID != "" {
2✔
974
                id = *sarifID.ID
1✔
975
                return
1✔
976
        }
1✔
UNCOV
977
        var result map[string]string
×
UNCOV
978
        var ghAcceptedError *github.AcceptedError
×
UNCOV
979
        if errors.As(uploadSarifErr, &ghAcceptedError) {
×
980
                if err = json.Unmarshal(ghAcceptedError.Raw, &result); err != nil {
×
981
                        return
×
982
                }
×
983
                id = result["id"]
×
984
        }
985
        return
×
986
}
987

988
// DownloadFileFromRepo on GitHub
989
func (client *GitHubClient) DownloadFileFromRepo(ctx context.Context, owner, repository, branch, path string) (content []byte, statusCode int, err error) {
3✔
990
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
991
                var ghResponse *github.Response
3✔
992
                content, statusCode, ghResponse, err = client.executeDownloadFileFromRepo(ctx, owner, repository, branch, path)
3✔
993
                return ghResponse, err
3✔
994
        })
3✔
995
        return
3✔
996
}
997

998
func (client *GitHubClient) executeDownloadFileFromRepo(ctx context.Context, owner, repository, branch, path string) (content []byte, statusCode int, ghResponse *github.Response, err error) {
3✔
999
        body, ghResponse, err := client.ghClient.Repositories.DownloadContents(ctx, owner, repository, path, &github.RepositoryContentGetOptions{Ref: branch})
3✔
1000
        defer func() {
6✔
1001
                if body != nil {
4✔
1002
                        err = errors.Join(err, body.Close())
1✔
1003
                }
1✔
1004
        }()
1005

1006
        if ghResponse == nil || ghResponse.Response == nil {
4✔
1007
                return
1✔
1008
        }
1✔
1009

1010
        statusCode = ghResponse.StatusCode
2✔
1011
        if err != nil && statusCode != http.StatusOK {
2✔
UNCOV
1012
                err = fmt.Errorf("expected %d status code while received %d status code with error:\n%s", http.StatusOK, ghResponse.StatusCode, err)
×
UNCOV
1013
                return
×
UNCOV
1014
        }
×
1015

1016
        if body != nil {
3✔
1017
                content, err = io.ReadAll(body)
1✔
1018
        }
1✔
1019
        return
2✔
1020
}
1021

1022
// GetRepositoryEnvironmentInfo on GitHub
1023
func (client *GitHubClient) GetRepositoryEnvironmentInfo(ctx context.Context, owner, repository, name string) (RepositoryEnvironmentInfo, error) {
2✔
1024
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "name": name})
2✔
1025
        if err != nil {
2✔
UNCOV
1026
                return RepositoryEnvironmentInfo{}, err
×
UNCOV
1027
        }
×
1028

1029
        var repositoryEnvInfo *RepositoryEnvironmentInfo
2✔
1030
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1031
                var ghResponse *github.Response
2✔
1032
                repositoryEnvInfo, ghResponse, err = client.executeGetRepositoryEnvironmentInfo(ctx, owner, repository, name)
2✔
1033
                return ghResponse, err
2✔
1034
        })
2✔
1035
        return *repositoryEnvInfo, err
2✔
1036
}
1037

1038
func (client *GitHubClient) executeGetRepositoryEnvironmentInfo(ctx context.Context, owner, repository, name string) (*RepositoryEnvironmentInfo, *github.Response, error) {
2✔
1039
        environment, ghResponse, err := client.ghClient.Repositories.GetEnvironment(ctx, owner, repository, name)
2✔
1040
        if err != nil {
3✔
1041
                return &RepositoryEnvironmentInfo{}, ghResponse, err
1✔
1042
        }
1✔
1043

1044
        if err = vcsutils.CheckResponseStatusWithBody(ghResponse.Response, http.StatusOK); err != nil {
1✔
UNCOV
1045
                return &RepositoryEnvironmentInfo{}, ghResponse, err
×
UNCOV
1046
        }
×
1047

1048
        reviewers, err := extractGitHubEnvironmentReviewers(environment)
1✔
1049
        if err != nil {
1✔
UNCOV
1050
                return &RepositoryEnvironmentInfo{}, ghResponse, err
×
UNCOV
1051
        }
×
1052

1053
        return &RepositoryEnvironmentInfo{
1✔
1054
                        Name:      environment.GetName(),
1✔
1055
                        Url:       environment.GetURL(),
1✔
1056
                        Reviewers: reviewers,
1✔
1057
                },
1✔
1058
                ghResponse,
1✔
1059
                nil
1✔
1060
}
1061

1062
func (client *GitHubClient) GetModifiedFiles(ctx context.Context, owner, repository, refBefore, refAfter string) ([]string, error) {
6✔
1063
        err := validateParametersNotBlank(map[string]string{
6✔
1064
                "owner":      owner,
6✔
1065
                "repository": repository,
6✔
1066
                "refBefore":  refBefore,
6✔
1067
                "refAfter":   refAfter,
6✔
1068
        })
6✔
1069
        if err != nil {
10✔
1070
                return nil, err
4✔
1071
        }
4✔
1072

1073
        var fileNamesList []string
2✔
1074
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1075
                var ghResponse *github.Response
2✔
1076
                fileNamesList, ghResponse, err = client.executeGetModifiedFiles(ctx, owner, repository, refBefore, refAfter)
2✔
1077
                return ghResponse, err
2✔
1078
        })
2✔
1079
        return fileNamesList, err
2✔
1080
}
1081

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

2✔
1089
        comparison, ghResponse, err := client.ghClient.Repositories.CompareCommits(ctx, owner, repository, refBefore, refAfter, listOptions)
2✔
1090
        if err != nil {
3✔
1091
                return nil, ghResponse, err
1✔
1092
        }
1✔
1093

1094
        if err = vcsutils.CheckResponseStatusWithBody(ghResponse.Response, http.StatusOK); err != nil {
1✔
UNCOV
1095
                return nil, ghResponse, err
×
UNCOV
1096
        }
×
1097

1098
        fileNamesSet := datastructures.MakeSet[string]()
1✔
1099
        for _, file := range comparison.Files {
18✔
1100
                fileNamesSet.Add(vcsutils.DefaultIfNotNil(file.Filename))
17✔
1101
                fileNamesSet.Add(vcsutils.DefaultIfNotNil(file.PreviousFilename))
17✔
1102
        }
17✔
1103

1104
        _ = fileNamesSet.Remove("") // Make sure there are no blank filepath.
1✔
1105
        fileNamesList := fileNamesSet.ToSlice()
1✔
1106
        sort.Strings(fileNamesList)
1✔
1107

1✔
1108
        return fileNamesList, ghResponse, nil
1✔
1109
}
1110

1111
// Extract code reviewers from environment
1112
func extractGitHubEnvironmentReviewers(environment *github.Environment) ([]string, error) {
2✔
1113
        var reviewers []string
2✔
1114
        protectionRules := environment.ProtectionRules
2✔
1115
        if protectionRules == nil {
2✔
UNCOV
1116
                return reviewers, nil
×
UNCOV
1117
        }
×
1118
        reviewerStruct := repositoryEnvironmentReviewer{}
2✔
1119
        for _, rule := range protectionRules {
4✔
1120
                for _, reviewer := range rule.Reviewers {
5✔
1121
                        if err := mapstructure.Decode(reviewer.Reviewer, &reviewerStruct); err != nil {
3✔
UNCOV
1122
                                return []string{}, err
×
UNCOV
1123
                        }
×
1124
                        reviewers = append(reviewers, reviewerStruct.Login)
3✔
1125
                }
1126
        }
1127
        return reviewers, nil
2✔
1128
}
1129

1130
func createGitHubHook(token, payloadURL string, webhookEvents ...vcsutils.WebhookEvent) *github.Hook {
4✔
1131
        return &github.Hook{
4✔
1132
                Events: getGitHubWebhookEvents(webhookEvents...),
4✔
1133
                Config: map[string]interface{}{
4✔
1134
                        "url":          payloadURL,
4✔
1135
                        "content_type": "json",
4✔
1136
                        "secret":       token,
4✔
1137
                },
4✔
1138
        }
4✔
1139
}
4✔
1140

1141
// Get varargs of webhook events and return a slice of GitHub webhook events
1142
func getGitHubWebhookEvents(webhookEvents ...vcsutils.WebhookEvent) []string {
4✔
1143
        events := datastructures.MakeSet[string]()
4✔
1144
        for _, event := range webhookEvents {
16✔
1145
                switch event {
12✔
1146
                case vcsutils.PrOpened, vcsutils.PrEdited, vcsutils.PrMerged, vcsutils.PrRejected:
8✔
1147
                        events.Add("pull_request")
8✔
1148
                case vcsutils.Push, vcsutils.TagPushed, vcsutils.TagRemoved:
4✔
1149
                        events.Add("push")
4✔
1150
                }
1151
        }
1152
        return events.ToSlice()
4✔
1153
}
1154

1155
func getGitHubRepositoryVisibility(repo *github.Repository) RepositoryVisibility {
5✔
1156
        switch *repo.Visibility {
5✔
1157
        case "public":
3✔
1158
                return Public
3✔
1159
        case "internal":
1✔
1160
                return Internal
1✔
1161
        default:
1✔
1162
                return Private
1✔
1163
        }
1164
}
1165

1166
func getGitHubCommitState(commitState CommitStatus) string {
7✔
1167
        switch commitState {
7✔
1168
        case Pass:
1✔
1169
                return "success"
1✔
1170
        case Fail:
1✔
1171
                return "failure"
1✔
1172
        case Error:
3✔
1173
                return "error"
3✔
1174
        case InProgress:
1✔
1175
                return "pending"
1✔
1176
        }
1177
        return ""
1✔
1178
}
1179

1180
func mapGitHubCommitToCommitInfo(commit *github.RepositoryCommit) CommitInfo {
8✔
1181
        parents := make([]string, len(commit.Parents))
8✔
1182
        for i, c := range commit.Parents {
15✔
1183
                parents[i] = c.GetSHA()
7✔
1184
        }
7✔
1185
        details := commit.GetCommit()
8✔
1186
        return CommitInfo{
8✔
1187
                Hash:          commit.GetSHA(),
8✔
1188
                AuthorName:    details.GetAuthor().GetName(),
8✔
1189
                CommitterName: details.GetCommitter().GetName(),
8✔
1190
                Url:           commit.GetURL(),
8✔
1191
                Timestamp:     details.GetCommitter().GetDate().UTC().Unix(),
8✔
1192
                Message:       details.GetMessage(),
8✔
1193
                ParentHashes:  parents,
8✔
1194
                AuthorEmail:   details.GetAuthor().GetEmail(),
8✔
1195
        }
8✔
1196
}
1197

1198
func mapGitHubIssuesCommentToCommentInfoList(commentsList []*github.IssueComment) (res []CommentInfo, err error) {
1✔
1199
        for _, comment := range commentsList {
3✔
1200
                res = append(res, CommentInfo{
2✔
1201
                        ID:      comment.GetID(),
2✔
1202
                        Content: comment.GetBody(),
2✔
1203
                        Created: comment.GetCreatedAt().Time,
2✔
1204
                })
2✔
1205
        }
2✔
1206
        return
1✔
1207
}
1208

1209
func mapGitHubPullRequestToPullRequestInfoList(pullRequestList []*github.PullRequest, withBody bool) (res []PullRequestInfo, err error) {
2✔
1210
        var mappedPullRequest PullRequestInfo
2✔
1211
        for _, pullRequest := range pullRequestList {
4✔
1212
                mappedPullRequest, err = mapGitHubPullRequestToPullRequestInfo(pullRequest, withBody)
2✔
1213
                if err != nil {
2✔
UNCOV
1214
                        return
×
UNCOV
1215
                }
×
1216
                res = append(res, mappedPullRequest)
2✔
1217
        }
1218
        return
2✔
1219
}
1220

1221
func encodeScanningResult(data string) (string, error) {
1✔
1222
        compressedScan, err := base64.EncodeGzip([]byte(data), 6)
1✔
1223
        if err != nil {
1✔
UNCOV
1224
                return "", err
×
UNCOV
1225
        }
×
1226

1227
        return compressedScan, err
1✔
1228
}
1229

1230
type repositoryEnvironmentReviewer struct {
1231
        Login string `mapstructure:"login"`
1232
}
1233

1234
func shouldRetryIfRateLimitExceeded(ghResponse *github.Response, requestError error) bool {
91✔
1235
        if ghResponse == nil || ghResponse.Response == nil {
126✔
1236
                return false
35✔
1237
        }
35✔
1238

1239
        if !slices.Contains(rateLimitRetryStatuses, ghResponse.StatusCode) {
110✔
1240
                return false
54✔
1241
        }
54✔
1242

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

1249
        body, err := io.ReadAll(ghResponse.Body)
1✔
1250
        if err != nil {
1✔
UNCOV
1251
                return false
×
UNCOV
1252
        }
×
1253
        return strings.Contains(string(body), "rate limit")
1✔
1254
}
1255

1256
func isRateLimitAbuseError(requestError error) bool {
4✔
1257
        var abuseRateLimitError *github.AbuseRateLimitError
4✔
1258
        var rateLimitError *github.RateLimitError
4✔
1259
        return errors.As(requestError, &abuseRateLimitError) || errors.As(requestError, &rateLimitError)
4✔
1260
}
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