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

jfrog / froggit-go / 12973309394

26 Jan 2025 09:17AM UTC coverage: 87.598% (-0.1%) from 87.742%
12973309394

Pull #143

github

EyalDelarea
Merge branch 'master' of https://github.com/jfrog/froggit-go into new_gh_pr_functions
Pull Request #143: Add Pull Request new APIs

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

27 existing lines in 4 files now uncovered.

4139 of 4725 relevant lines covered (87.6%)

6.77 hits per line

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

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

75
func (client *GitHubClient) runWithRateLimitRetries(handler func() (*github.Response, error)) error {
89✔
76
        client.rateLimitRetryExecutor.GitHubRateLimitExecutionHandler = handler
89✔
77
        return client.rateLimitRetryExecutor.Execute()
89✔
78
}
89✔
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) {
119✔
87
        httpClient := &http.Client{}
119✔
88
        if vcsInfo.Token != "" {
168✔
89
                httpClient = oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(&oauth2.Token{AccessToken: vcsInfo.Token}))
49✔
90
        }
49✔
91
        ghClient := github.NewClient(httpClient)
119✔
92
        if vcsInfo.APIEndpoint != "" {
204✔
93
                baseURL, err := url.Parse(strings.TrimSuffix(vcsInfo.APIEndpoint, "/") + "/")
85✔
94
                if err != nil {
85✔
95
                        return nil, err
×
96
                }
×
97
                logger.Info("Using API endpoint:", baseURL)
85✔
98
                ghClient.BaseURL = baseURL
85✔
99
        }
100
        return ghClient, nil
119✔
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
func (client *GitHubClient) ListPullRequestCommits(ctx context.Context, owner, repository string, pullRequestID int) (commitsInfo []CommitInfo, err error) {
2✔
575
        if err = validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "pullRequestID": strconv.Itoa(pullRequestID)}); err != nil {
2✔
NEW
576
                return
×
NEW
577
        }
×
578
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
579
                commits, ghResponse, err := client.ghClient.PullRequests.ListCommits(ctx, owner, repository, pullRequestID, nil)
2✔
580
                for _, commit := range commits {
3✔
581
                        commitsInfo = append(commitsInfo, mapGitHubCommitToCommitInfo(commit))
1✔
582
                }
1✔
583
                return ghResponse, err
2✔
584
        })
585
        return commitsInfo, err
2✔
586
}
587

588
func (client *GitHubClient) executeListPullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int) ([]CommentInfo, *github.Response, error) {
2✔
589
        commentsList, ghResponse, err := client.ghClient.PullRequests.ListComments(ctx, owner, repository, pullRequestID, nil)
2✔
590
        if err != nil {
3✔
591
                return []CommentInfo{}, ghResponse, err
1✔
592
        }
1✔
593
        commentsInfoList := []CommentInfo{}
1✔
594
        for _, comment := range commentsList {
2✔
595
                commentsInfoList = append(commentsInfoList, CommentInfo{
1✔
596
                        ID:      comment.GetID(),
1✔
597
                        Content: comment.GetBody(),
1✔
598
                        Created: comment.GetCreatedAt().Time,
1✔
599
                })
1✔
600
        }
1✔
601
        return commentsInfoList, ghResponse, nil
1✔
602
}
603

604
// ListPullRequestComments on GitHub
605
func (client *GitHubClient) ListPullRequestComments(ctx context.Context, owner, repository string, pullRequestID int) ([]CommentInfo, error) {
4✔
606
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
4✔
607
        if err != nil {
4✔
608
                return []CommentInfo{}, err
×
609
        }
×
610

611
        var commentsList []*github.IssueComment
4✔
612
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
8✔
613
                var ghResponse *github.Response
4✔
614
                commentsList, ghResponse, err = client.ghClient.Issues.ListComments(ctx, owner, repository, pullRequestID, &github.IssueListCommentsOptions{})
4✔
615
                return ghResponse, err
4✔
616
        })
4✔
617

618
        if err != nil {
7✔
619
                return []CommentInfo{}, err
3✔
620
        }
3✔
621

622
        return mapGitHubIssuesCommentToCommentInfoList(commentsList)
1✔
623
}
624

625
// DeletePullRequestReviewComments on GitHub
626
func (client *GitHubClient) DeletePullRequestReviewComments(ctx context.Context, owner, repository string, _ int, comments ...CommentInfo) error {
2✔
627
        for _, comment := range comments {
5✔
628
                commentID := comment.ID
3✔
629
                err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "commentID": strconv.FormatInt(commentID, 10)})
3✔
630
                if err != nil {
3✔
631
                        return err
×
632
                }
×
633

634
                err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
635
                        return client.executeDeletePullRequestReviewComment(ctx, owner, repository, commentID)
3✔
636
                })
3✔
637
                if err != nil {
4✔
638
                        return err
1✔
639
                }
1✔
640

641
        }
642
        return nil
1✔
643
}
644

645
func (client *GitHubClient) executeDeletePullRequestReviewComment(ctx context.Context, owner, repository string, commentID int64) (*github.Response, error) {
3✔
646
        ghResponse, err := client.ghClient.PullRequests.DeleteComment(ctx, owner, repository, commentID)
3✔
647
        if err != nil {
4✔
648
                err = fmt.Errorf("could not delete pull request review comment: %w", err)
1✔
649
        }
1✔
650
        return ghResponse, err
3✔
651
}
652

653
// DeletePullRequestComment on GitHub
654
func (client *GitHubClient) DeletePullRequestComment(ctx context.Context, owner, repository string, _, commentID int) error {
2✔
655
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
2✔
656
        if err != nil {
2✔
657
                return err
×
658
        }
×
659
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
660
                return client.executeDeletePullRequestComment(ctx, owner, repository, commentID)
2✔
661
        })
2✔
662
}
663

664
func (client *GitHubClient) executeDeletePullRequestComment(ctx context.Context, owner, repository string, commentID int) (*github.Response, error) {
2✔
665
        ghResponse, err := client.ghClient.Issues.DeleteComment(ctx, owner, repository, int64(commentID))
2✔
666
        if err != nil {
3✔
667
                return ghResponse, err
1✔
668
        }
1✔
669

670
        var statusCode int
1✔
671
        if ghResponse.Response != nil {
2✔
672
                statusCode = ghResponse.Response.StatusCode
1✔
673
        }
1✔
674
        if statusCode != http.StatusNoContent && statusCode != http.StatusOK {
1✔
675
                return ghResponse, fmt.Errorf("expected %d status code while received %d status code", http.StatusNoContent, ghResponse.Response.StatusCode)
×
676
        }
×
677

678
        return ghResponse, nil
1✔
679
}
680

681
// GetLatestCommit on GitHub
682
func (client *GitHubClient) GetLatestCommit(ctx context.Context, owner, repository, branch string) (CommitInfo, error) {
10✔
683
        commits, err := client.GetCommits(ctx, owner, repository, branch)
10✔
684
        if err != nil {
18✔
685
                return CommitInfo{}, err
8✔
686
        }
8✔
687
        latestCommit := CommitInfo{}
2✔
688
        if len(commits) > 0 {
4✔
689
                latestCommit = commits[0]
2✔
690
        }
2✔
691
        return latestCommit, nil
2✔
692
}
693

694
// GetCommits on GitHub
695
func (client *GitHubClient) GetCommits(ctx context.Context, owner, repository, branch string) ([]CommitInfo, error) {
12✔
696
        err := validateParametersNotBlank(map[string]string{
12✔
697
                "owner":      owner,
12✔
698
                "repository": repository,
12✔
699
                "branch":     branch,
12✔
700
        })
12✔
701
        if err != nil {
16✔
702
                return nil, err
4✔
703
        }
4✔
704

705
        var commitsInfo []CommitInfo
8✔
706
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
16✔
707
                var ghResponse *github.Response
8✔
708
                listOptions := &github.CommitsListOptions{
8✔
709
                        SHA: branch,
8✔
710
                        ListOptions: github.ListOptions{
8✔
711
                                Page:    1,
8✔
712
                                PerPage: vcsutils.NumberOfCommitsToFetch,
8✔
713
                        },
8✔
714
                }
8✔
715
                commitsInfo, ghResponse, err = client.executeGetCommits(ctx, owner, repository, listOptions)
8✔
716
                return ghResponse, err
8✔
717
        })
8✔
718
        return commitsInfo, err
8✔
719
}
720

721
// GetCommitsWithQueryOptions on GitHub
722
func (client *GitHubClient) GetCommitsWithQueryOptions(ctx context.Context, owner, repository string, listOptions GitCommitsQueryOptions) ([]CommitInfo, error) {
2✔
723
        err := validateParametersNotBlank(map[string]string{
2✔
724
                "owner":      owner,
2✔
725
                "repository": repository,
2✔
726
        })
2✔
727
        if err != nil {
2✔
728
                return nil, err
×
729
        }
×
730
        var commitsInfo []CommitInfo
2✔
731
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
732
                var ghResponse *github.Response
2✔
733
                commitsInfo, ghResponse, err = client.executeGetCommits(ctx, owner, repository, convertToGitHubCommitsListOptions(listOptions))
2✔
734
                return ghResponse, err
2✔
735
        })
2✔
736
        return commitsInfo, err
2✔
737
}
738

739
func convertToGitHubCommitsListOptions(listOptions GitCommitsQueryOptions) *github.CommitsListOptions {
2✔
740
        return &github.CommitsListOptions{
2✔
741
                Since: listOptions.Since,
2✔
742
                Until: time.Now(),
2✔
743
                ListOptions: github.ListOptions{
2✔
744
                        Page:    listOptions.Page,
2✔
745
                        PerPage: listOptions.PerPage,
2✔
746
                },
2✔
747
        }
2✔
748
}
2✔
749

750
func (client *GitHubClient) executeGetCommits(ctx context.Context, owner, repository string, listOptions *github.CommitsListOptions) ([]CommitInfo, *github.Response, error) {
10✔
751
        commits, ghResponse, err := client.ghClient.Repositories.ListCommits(ctx, owner, repository, listOptions)
10✔
752
        if err != nil {
16✔
753
                return nil, ghResponse, err
6✔
754
        }
6✔
755

756
        var commitsInfo []CommitInfo
4✔
757
        for _, commit := range commits {
11✔
758
                commitInfo := mapGitHubCommitToCommitInfo(commit)
7✔
759
                commitsInfo = append(commitsInfo, commitInfo)
7✔
760
        }
7✔
761
        return commitsInfo, ghResponse, nil
4✔
762
}
763

764
// GetRepositoryInfo on GitHub
765
func (client *GitHubClient) GetRepositoryInfo(ctx context.Context, owner, repository string) (RepositoryInfo, error) {
6✔
766
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
6✔
767
        if err != nil {
9✔
768
                return RepositoryInfo{}, err
3✔
769
        }
3✔
770

771
        var repo *github.Repository
3✔
772
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
773
                var ghResponse *github.Response
3✔
774
                repo, ghResponse, err = client.ghClient.Repositories.Get(ctx, owner, repository)
3✔
775
                return ghResponse, err
3✔
776
        })
3✔
777
        if err != nil {
4✔
778
                return RepositoryInfo{}, err
1✔
779
        }
1✔
780

781
        return RepositoryInfo{RepositoryVisibility: getGitHubRepositoryVisibility(repo), CloneInfo: CloneInfo{HTTP: repo.GetCloneURL(), SSH: repo.GetSSHURL()}}, nil
2✔
782
}
783

784
// GetCommitBySha on GitHub
785
func (client *GitHubClient) GetCommitBySha(ctx context.Context, owner, repository, sha string) (CommitInfo, error) {
7✔
786
        err := validateParametersNotBlank(map[string]string{
7✔
787
                "owner":      owner,
7✔
788
                "repository": repository,
7✔
789
                "sha":        sha,
7✔
790
        })
7✔
791
        if err != nil {
11✔
792
                return CommitInfo{}, err
4✔
793
        }
4✔
794

795
        var commit *github.RepositoryCommit
3✔
796
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
797
                var ghResponse *github.Response
3✔
798
                commit, ghResponse, err = client.ghClient.Repositories.GetCommit(ctx, owner, repository, sha, nil)
3✔
799
                return ghResponse, err
3✔
800
        })
3✔
801
        if err != nil {
5✔
802
                return CommitInfo{}, err
2✔
803
        }
2✔
804

805
        return mapGitHubCommitToCommitInfo(commit), nil
1✔
806
}
807

808
// CreateLabel on GitHub
809
func (client *GitHubClient) CreateLabel(ctx context.Context, owner, repository string, labelInfo LabelInfo) error {
6✔
810
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "LabelInfo.name": labelInfo.Name})
6✔
811
        if err != nil {
10✔
812
                return err
4✔
813
        }
4✔
814

815
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
816
                var ghResponse *github.Response
2✔
817
                _, ghResponse, err = client.ghClient.Issues.CreateLabel(ctx, owner, repository, &github.Label{
2✔
818
                        Name:        &labelInfo.Name,
2✔
819
                        Description: &labelInfo.Description,
2✔
820
                        Color:       &labelInfo.Color,
2✔
821
                })
2✔
822
                return ghResponse, err
2✔
823
        })
2✔
824
}
825

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

833
        var labelInfo *LabelInfo
3✔
834
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
835
                var ghResponse *github.Response
3✔
836
                labelInfo, ghResponse, err = client.executeGetLabel(ctx, owner, repository, name)
3✔
837
                return ghResponse, err
3✔
838
        })
3✔
839
        return labelInfo, err
3✔
840
}
841

842
func (client *GitHubClient) executeGetLabel(ctx context.Context, owner, repository, name string) (*LabelInfo, *github.Response, error) {
3✔
843
        label, ghResponse, err := client.ghClient.Issues.GetLabel(ctx, owner, repository, name)
3✔
844
        if err != nil {
5✔
845
                if ghResponse != nil && ghResponse.Response != nil && ghResponse.Response.StatusCode == http.StatusNotFound {
3✔
846
                        return nil, ghResponse, nil
1✔
847
                }
1✔
848
                return nil, ghResponse, err
1✔
849
        }
850

851
        labelInfo := &LabelInfo{
1✔
852
                Name:        *label.Name,
1✔
853
                Description: *label.Description,
1✔
854
                Color:       *label.Color,
1✔
855
        }
1✔
856
        return labelInfo, ghResponse, nil
1✔
857
}
858

859
// ListPullRequestLabels on GitHub
860
func (client *GitHubClient) ListPullRequestLabels(ctx context.Context, owner, repository string, pullRequestID int) ([]string, error) {
5✔
861
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
5✔
862
        if err != nil {
8✔
863
                return nil, err
3✔
864
        }
3✔
865

866
        results := []string{}
2✔
867
        for nextPage := 0; ; nextPage++ {
4✔
868
                options := &github.ListOptions{Page: nextPage}
2✔
869
                var labels []*github.Label
2✔
870
                var ghResponse *github.Response
2✔
871
                err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
872
                        labels, ghResponse, err = client.ghClient.Issues.ListLabelsByIssue(ctx, owner, repository, pullRequestID, options)
2✔
873
                        return ghResponse, err
2✔
874
                })
2✔
875
                if err != nil {
3✔
876
                        return nil, err
1✔
877
                }
1✔
878
                for _, label := range labels {
2✔
879
                        results = append(results, *label.Name)
1✔
880
                }
1✔
881
                if nextPage+1 >= ghResponse.LastPage {
2✔
882
                        break
1✔
883
                }
884
        }
885
        return results, nil
1✔
886
}
887

888
// UnlabelPullRequest on GitHub
889
func (client *GitHubClient) UnlabelPullRequest(ctx context.Context, owner, repository, name string, pullRequestID int) error {
5✔
890
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
5✔
891
        if err != nil {
8✔
892
                return err
3✔
893
        }
3✔
894

895
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
896
                return client.ghClient.Issues.RemoveLabelForIssue(ctx, owner, repository, pullRequestID, name)
2✔
897
        })
2✔
898
}
899

900
// UploadCodeScanning to GitHub Security tab
901
func (client *GitHubClient) UploadCodeScanning(ctx context.Context, owner, repository, branch, sarifContent string) (id string, err error) {
2✔
902
        commit, err := client.GetLatestCommit(ctx, owner, repository, branch)
2✔
903
        if err != nil {
3✔
904
                return
1✔
905
        }
1✔
906

907
        commitSHA := commit.Hash
1✔
908
        branch = vcsutils.AddBranchPrefix(branch)
1✔
909
        client.logger.Debug(vcsutils.UploadingCodeScanning, repository, "/", branch)
1✔
910

1✔
911
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
2✔
912
                var ghResponse *github.Response
1✔
913
                id, ghResponse, err = client.executeUploadCodeScanning(ctx, owner, repository, branch, commitSHA, sarifContent)
1✔
914
                return ghResponse, err
1✔
915
        })
1✔
916
        return
1✔
917
}
918

919
func (client *GitHubClient) executeUploadCodeScanning(ctx context.Context, owner, repository, branch, commitSHA, sarifContent string) (id string, ghResponse *github.Response, err error) {
1✔
920
        encodedSarif, err := encodeScanningResult(sarifContent)
1✔
921
        if err != nil {
1✔
922
                return
×
923
        }
×
924

925
        sarifID, ghResponse, err := client.ghClient.CodeScanning.UploadSarif(ctx, owner, repository, &github.SarifAnalysis{
1✔
926
                CommitSHA: &commitSHA,
1✔
927
                Ref:       &branch,
1✔
928
                Sarif:     &encodedSarif,
1✔
929
        })
1✔
930

1✔
931
        // According to go-github API - successful ghResponse will return 202 status code
1✔
932
        // The body of the ghResponse will appear in the error, and the Sarif struct will be empty.
1✔
933
        if err != nil && ghResponse.Response.StatusCode != http.StatusAccepted {
1✔
934
                return
×
935
        }
×
936

937
        id, err = handleGitHubUploadSarifID(sarifID, err)
1✔
938
        return
1✔
939
}
940

941
func handleGitHubUploadSarifID(sarifID *github.SarifID, uploadSarifErr error) (id string, err error) {
1✔
942
        if sarifID != nil && *sarifID.ID != "" {
2✔
943
                id = *sarifID.ID
1✔
944
                return
1✔
945
        }
1✔
946
        var result map[string]string
×
947
        var ghAcceptedError *github.AcceptedError
×
948
        if errors.As(uploadSarifErr, &ghAcceptedError) {
×
949
                if err = json.Unmarshal(ghAcceptedError.Raw, &result); err != nil {
×
950
                        return
×
951
                }
×
952
                id = result["id"]
×
953
        }
954
        return
×
955
}
956

957
// DownloadFileFromRepo on GitHub
958
func (client *GitHubClient) DownloadFileFromRepo(ctx context.Context, owner, repository, branch, path string) (content []byte, statusCode int, err error) {
3✔
959
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
960
                var ghResponse *github.Response
3✔
961
                content, statusCode, ghResponse, err = client.executeDownloadFileFromRepo(ctx, owner, repository, branch, path)
3✔
962
                return ghResponse, err
3✔
963
        })
3✔
964
        return
3✔
965
}
966

967
func (client *GitHubClient) executeDownloadFileFromRepo(ctx context.Context, owner, repository, branch, path string) (content []byte, statusCode int, ghResponse *github.Response, err error) {
3✔
968
        body, ghResponse, err := client.ghClient.Repositories.DownloadContents(ctx, owner, repository, path, &github.RepositoryContentGetOptions{Ref: branch})
3✔
969
        defer func() {
6✔
970
                if body != nil {
4✔
971
                        err = errors.Join(err, body.Close())
1✔
972
                }
1✔
973
        }()
974

975
        if ghResponse == nil || ghResponse.Response == nil {
4✔
976
                return
1✔
977
        }
1✔
978

979
        statusCode = ghResponse.StatusCode
2✔
980
        if err != nil && statusCode != http.StatusOK {
2✔
981
                err = fmt.Errorf("expected %d status code while received %d status code with error:\n%s", http.StatusOK, ghResponse.StatusCode, err)
×
982
                return
×
983
        }
×
984

985
        if body != nil {
3✔
986
                content, err = io.ReadAll(body)
1✔
987
        }
1✔
988
        return
2✔
989
}
990

991
// GetRepositoryEnvironmentInfo on GitHub
992
func (client *GitHubClient) GetRepositoryEnvironmentInfo(ctx context.Context, owner, repository, name string) (RepositoryEnvironmentInfo, error) {
2✔
993
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "name": name})
2✔
994
        if err != nil {
2✔
995
                return RepositoryEnvironmentInfo{}, err
×
996
        }
×
997

998
        var repositoryEnvInfo *RepositoryEnvironmentInfo
2✔
999
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1000
                var ghResponse *github.Response
2✔
1001
                repositoryEnvInfo, ghResponse, err = client.executeGetRepositoryEnvironmentInfo(ctx, owner, repository, name)
2✔
1002
                return ghResponse, err
2✔
1003
        })
2✔
1004
        return *repositoryEnvInfo, err
2✔
1005
}
1006

1007
func (client *GitHubClient) executeGetRepositoryEnvironmentInfo(ctx context.Context, owner, repository, name string) (*RepositoryEnvironmentInfo, *github.Response, error) {
2✔
1008
        environment, ghResponse, err := client.ghClient.Repositories.GetEnvironment(ctx, owner, repository, name)
2✔
1009
        if err != nil {
3✔
1010
                return &RepositoryEnvironmentInfo{}, ghResponse, err
1✔
1011
        }
1✔
1012

1013
        if err = vcsutils.CheckResponseStatusWithBody(ghResponse.Response, http.StatusOK); err != nil {
1✔
1014
                return &RepositoryEnvironmentInfo{}, ghResponse, err
×
1015
        }
×
1016

1017
        reviewers, err := extractGitHubEnvironmentReviewers(environment)
1✔
1018
        if err != nil {
1✔
1019
                return &RepositoryEnvironmentInfo{}, ghResponse, err
×
1020
        }
×
1021

1022
        return &RepositoryEnvironmentInfo{
1✔
1023
                        Name:      environment.GetName(),
1✔
1024
                        Url:       environment.GetURL(),
1✔
1025
                        Reviewers: reviewers,
1✔
1026
                },
1✔
1027
                ghResponse,
1✔
1028
                nil
1✔
1029
}
1030

1031
func (client *GitHubClient) GetModifiedFiles(ctx context.Context, owner, repository, refBefore, refAfter string) ([]string, error) {
6✔
1032
        err := validateParametersNotBlank(map[string]string{
6✔
1033
                "owner":      owner,
6✔
1034
                "repository": repository,
6✔
1035
                "refBefore":  refBefore,
6✔
1036
                "refAfter":   refAfter,
6✔
1037
        })
6✔
1038
        if err != nil {
10✔
1039
                return nil, err
4✔
1040
        }
4✔
1041

1042
        var fileNamesList []string
2✔
1043
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1044
                var ghResponse *github.Response
2✔
1045
                fileNamesList, ghResponse, err = client.executeGetModifiedFiles(ctx, owner, repository, refBefore, refAfter)
2✔
1046
                return ghResponse, err
2✔
1047
        })
2✔
1048
        return fileNamesList, err
2✔
1049
}
1050

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

2✔
1058
        comparison, ghResponse, err := client.ghClient.Repositories.CompareCommits(ctx, owner, repository, refBefore, refAfter, listOptions)
2✔
1059
        if err != nil {
3✔
1060
                return nil, ghResponse, err
1✔
1061
        }
1✔
1062

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

1067
        fileNamesSet := datastructures.MakeSet[string]()
1✔
1068
        for _, file := range comparison.Files {
18✔
1069
                fileNamesSet.Add(vcsutils.DefaultIfNotNil(file.Filename))
17✔
1070
                fileNamesSet.Add(vcsutils.DefaultIfNotNil(file.PreviousFilename))
17✔
1071
        }
17✔
1072

1073
        _ = fileNamesSet.Remove("") // Make sure there are no blank filepath.
1✔
1074
        fileNamesList := fileNamesSet.ToSlice()
1✔
1075
        sort.Strings(fileNamesList)
1✔
1076

1✔
1077
        return fileNamesList, ghResponse, nil
1✔
1078
}
1079

1080
// Extract code reviewers from environment
1081
func extractGitHubEnvironmentReviewers(environment *github.Environment) ([]string, error) {
2✔
1082
        var reviewers []string
2✔
1083
        protectionRules := environment.ProtectionRules
2✔
1084
        if protectionRules == nil {
2✔
1085
                return reviewers, nil
×
1086
        }
×
1087
        reviewerStruct := repositoryEnvironmentReviewer{}
2✔
1088
        for _, rule := range protectionRules {
4✔
1089
                for _, reviewer := range rule.Reviewers {
5✔
1090
                        if err := mapstructure.Decode(reviewer.Reviewer, &reviewerStruct); err != nil {
3✔
1091
                                return []string{}, err
×
1092
                        }
×
1093
                        reviewers = append(reviewers, reviewerStruct.Login)
3✔
1094
                }
1095
        }
1096
        return reviewers, nil
2✔
1097
}
1098

1099
func createGitHubHook(token, payloadURL string, webhookEvents ...vcsutils.WebhookEvent) *github.Hook {
4✔
1100
        return &github.Hook{
4✔
1101
                Events: getGitHubWebhookEvents(webhookEvents...),
4✔
1102
                Config: map[string]interface{}{
4✔
1103
                        "url":          payloadURL,
4✔
1104
                        "content_type": "json",
4✔
1105
                        "secret":       token,
4✔
1106
                },
4✔
1107
        }
4✔
1108
}
4✔
1109

1110
// Get varargs of webhook events and return a slice of GitHub webhook events
1111
func getGitHubWebhookEvents(webhookEvents ...vcsutils.WebhookEvent) []string {
4✔
1112
        events := datastructures.MakeSet[string]()
4✔
1113
        for _, event := range webhookEvents {
16✔
1114
                switch event {
12✔
1115
                case vcsutils.PrOpened, vcsutils.PrEdited, vcsutils.PrMerged, vcsutils.PrRejected:
8✔
1116
                        events.Add("pull_request")
8✔
1117
                case vcsutils.Push, vcsutils.TagPushed, vcsutils.TagRemoved:
4✔
1118
                        events.Add("push")
4✔
1119
                }
1120
        }
1121
        return events.ToSlice()
4✔
1122
}
1123

1124
func getGitHubRepositoryVisibility(repo *github.Repository) RepositoryVisibility {
5✔
1125
        switch *repo.Visibility {
5✔
1126
        case "public":
3✔
1127
                return Public
3✔
1128
        case "internal":
1✔
1129
                return Internal
1✔
1130
        default:
1✔
1131
                return Private
1✔
1132
        }
1133
}
1134

1135
func getGitHubCommitState(commitState CommitStatus) string {
7✔
1136
        switch commitState {
7✔
1137
        case Pass:
1✔
1138
                return "success"
1✔
1139
        case Fail:
1✔
1140
                return "failure"
1✔
1141
        case Error:
3✔
1142
                return "error"
3✔
1143
        case InProgress:
1✔
1144
                return "pending"
1✔
1145
        }
1146
        return ""
1✔
1147
}
1148

1149
func mapGitHubCommitToCommitInfo(commit *github.RepositoryCommit) CommitInfo {
9✔
1150
        parents := make([]string, len(commit.Parents))
9✔
1151
        for i, c := range commit.Parents {
17✔
1152
                parents[i] = c.GetSHA()
8✔
1153
        }
8✔
1154
        details := commit.GetCommit()
9✔
1155
        return CommitInfo{
9✔
1156
                Hash:          commit.GetSHA(),
9✔
1157
                AuthorName:    details.GetAuthor().GetName(),
9✔
1158
                CommitterName: details.GetCommitter().GetName(),
9✔
1159
                Url:           commit.GetURL(),
9✔
1160
                Timestamp:     details.GetCommitter().GetDate().UTC().Unix(),
9✔
1161
                Message:       details.GetMessage(),
9✔
1162
                ParentHashes:  parents,
9✔
1163
                AuthorEmail:   details.GetAuthor().GetEmail(),
9✔
1164
        }
9✔
1165
}
1166

1167
func mapGitHubIssuesCommentToCommentInfoList(commentsList []*github.IssueComment) (res []CommentInfo, err error) {
1✔
1168
        for _, comment := range commentsList {
3✔
1169
                res = append(res, CommentInfo{
2✔
1170
                        ID:      comment.GetID(),
2✔
1171
                        Content: comment.GetBody(),
2✔
1172
                        Created: comment.GetCreatedAt().Time,
2✔
1173
                })
2✔
1174
        }
2✔
1175
        return
1✔
1176
}
1177

1178
func mapGitHubPullRequestToPullRequestInfoList(pullRequestList []*github.PullRequest, withBody bool) (res []PullRequestInfo, err error) {
2✔
1179
        var mappedPullRequest PullRequestInfo
2✔
1180
        for _, pullRequest := range pullRequestList {
4✔
1181
                mappedPullRequest, err = mapGitHubPullRequestToPullRequestInfo(pullRequest, withBody)
2✔
1182
                if err != nil {
2✔
1183
                        return
×
1184
                }
×
1185
                res = append(res, mappedPullRequest)
2✔
1186
        }
1187
        return
2✔
1188
}
1189

1190
func encodeScanningResult(data string) (string, error) {
1✔
1191
        compressedScan, err := base64.EncodeGzip([]byte(data), 6)
1✔
1192
        if err != nil {
1✔
1193
                return "", err
×
1194
        }
×
1195

1196
        return compressedScan, err
1✔
1197
}
1198

1199
type repositoryEnvironmentReviewer struct {
1200
        Login string `mapstructure:"login"`
1201
}
1202

1203
func shouldRetryIfRateLimitExceeded(ghResponse *github.Response, requestError error) bool {
93✔
1204
        if ghResponse == nil || ghResponse.Response == nil {
128✔
1205
                return false
35✔
1206
        }
35✔
1207

1208
        if !slices.Contains(rateLimitRetryStatuses, ghResponse.StatusCode) {
114✔
1209
                return false
56✔
1210
        }
56✔
1211

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

1218
        body, err := io.ReadAll(ghResponse.Body)
1✔
1219
        if err != nil {
1✔
1220
                return false
×
1221
        }
×
1222
        return strings.Contains(string(body), "rate limit")
1✔
1223
}
1224

1225
func isRateLimitAbuseError(requestError error) bool {
4✔
1226
        var abuseRateLimitError *github.AbuseRateLimitError
4✔
1227
        var rateLimitError *github.RateLimitError
4✔
1228
        return errors.As(requestError, &abuseRateLimitError) || errors.As(requestError, &rateLimitError)
4✔
1229
}
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