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

jfrog / froggit-go / 13050960678

30 Jan 2025 11:02AM UTC coverage: 87.328% (-0.4%) from 87.742%
13050960678

Pull #145

github

EyalDelarea
Add new GH API
Pull Request #145: List pull requests associated with a commit

0 of 22 new or added lines in 5 files covered. (0.0%)

297 existing lines in 5 files now uncovered.

4073 of 4664 relevant lines covered (87.33%)

6.7 hits per line

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

89.01
/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
func (client *GitHubClient) executeListPullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int) ([]CommentInfo, *github.Response, error) {
2✔
575
        commentsList, ghResponse, err := client.ghClient.PullRequests.ListComments(ctx, owner, repository, pullRequestID, nil)
2✔
576
        if err != nil {
3✔
577
                return []CommentInfo{}, ghResponse, err
1✔
578
        }
1✔
579
        commentsInfoList := []CommentInfo{}
1✔
580
        for _, comment := range commentsList {
2✔
581
                commentsInfoList = append(commentsInfoList, CommentInfo{
1✔
582
                        ID:      comment.GetID(),
1✔
583
                        Content: comment.GetBody(),
1✔
584
                        Created: comment.GetCreatedAt().Time,
1✔
585
                })
1✔
586
        }
1✔
587
        return commentsInfoList, ghResponse, nil
1✔
588
}
589

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

597
        var commentsList []*github.IssueComment
4✔
598
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
8✔
599
                var ghResponse *github.Response
4✔
600
                commentsList, ghResponse, err = client.ghClient.Issues.ListComments(ctx, owner, repository, pullRequestID, &github.IssueListCommentsOptions{})
4✔
601
                return ghResponse, err
4✔
602
        })
4✔
603

604
        if err != nil {
7✔
605
                return []CommentInfo{}, err
3✔
606
        }
3✔
607

608
        return mapGitHubIssuesCommentToCommentInfoList(commentsList)
1✔
609
}
610

611
// DeletePullRequestReviewComments on GitHub
612
func (client *GitHubClient) DeletePullRequestReviewComments(ctx context.Context, owner, repository string, _ int, comments ...CommentInfo) error {
2✔
613
        for _, comment := range comments {
5✔
614
                commentID := comment.ID
3✔
615
                err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "commentID": strconv.FormatInt(commentID, 10)})
3✔
616
                if err != nil {
3✔
UNCOV
617
                        return err
×
UNCOV
618
                }
×
619

620
                err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
621
                        return client.executeDeletePullRequestReviewComment(ctx, owner, repository, commentID)
3✔
622
                })
3✔
623
                if err != nil {
4✔
624
                        return err
1✔
625
                }
1✔
626

627
        }
628
        return nil
1✔
629
}
630

631
func (client *GitHubClient) executeDeletePullRequestReviewComment(ctx context.Context, owner, repository string, commentID int64) (*github.Response, error) {
3✔
632
        ghResponse, err := client.ghClient.PullRequests.DeleteComment(ctx, owner, repository, commentID)
3✔
633
        if err != nil {
4✔
634
                err = fmt.Errorf("could not delete pull request review comment: %w", err)
1✔
635
        }
1✔
636
        return ghResponse, err
3✔
637
}
638

639
// DeletePullRequestComment on GitHub
640
func (client *GitHubClient) DeletePullRequestComment(ctx context.Context, owner, repository string, _, commentID int) error {
2✔
641
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
2✔
642
        if err != nil {
2✔
UNCOV
643
                return err
×
UNCOV
644
        }
×
645
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
646
                return client.executeDeletePullRequestComment(ctx, owner, repository, commentID)
2✔
647
        })
2✔
648
}
649

650
func (client *GitHubClient) executeDeletePullRequestComment(ctx context.Context, owner, repository string, commentID int) (*github.Response, error) {
2✔
651
        ghResponse, err := client.ghClient.Issues.DeleteComment(ctx, owner, repository, int64(commentID))
2✔
652
        if err != nil {
3✔
653
                return ghResponse, err
1✔
654
        }
1✔
655

656
        var statusCode int
1✔
657
        if ghResponse.Response != nil {
2✔
658
                statusCode = ghResponse.Response.StatusCode
1✔
659
        }
1✔
660
        if statusCode != http.StatusNoContent && statusCode != http.StatusOK {
1✔
UNCOV
661
                return ghResponse, fmt.Errorf("expected %d status code while received %d status code", http.StatusNoContent, ghResponse.Response.StatusCode)
×
UNCOV
662
        }
×
663

664
        return ghResponse, nil
1✔
665
}
666

667
// GetLatestCommit on GitHub
668
func (client *GitHubClient) GetLatestCommit(ctx context.Context, owner, repository, branch string) (CommitInfo, error) {
10✔
669
        commits, err := client.GetCommits(ctx, owner, repository, branch)
10✔
670
        if err != nil {
18✔
671
                return CommitInfo{}, err
8✔
672
        }
8✔
673
        latestCommit := CommitInfo{}
2✔
674
        if len(commits) > 0 {
4✔
675
                latestCommit = commits[0]
2✔
676
        }
2✔
677
        return latestCommit, nil
2✔
678
}
679

680
// GetCommits on GitHub
681
func (client *GitHubClient) GetCommits(ctx context.Context, owner, repository, branch string) ([]CommitInfo, error) {
12✔
682
        err := validateParametersNotBlank(map[string]string{
12✔
683
                "owner":      owner,
12✔
684
                "repository": repository,
12✔
685
                "branch":     branch,
12✔
686
        })
12✔
687
        if err != nil {
16✔
688
                return nil, err
4✔
689
        }
4✔
690

691
        var commitsInfo []CommitInfo
8✔
692
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
16✔
693
                var ghResponse *github.Response
8✔
694
                listOptions := &github.CommitsListOptions{
8✔
695
                        SHA: branch,
8✔
696
                        ListOptions: github.ListOptions{
8✔
697
                                Page:    1,
8✔
698
                                PerPage: vcsutils.NumberOfCommitsToFetch,
8✔
699
                        },
8✔
700
                }
8✔
701
                commitsInfo, ghResponse, err = client.executeGetCommits(ctx, owner, repository, listOptions)
8✔
702
                return ghResponse, err
8✔
703
        })
8✔
704
        return commitsInfo, err
8✔
705
}
706

707
// GetCommitsWithQueryOptions on GitHub
708
func (client *GitHubClient) GetCommitsWithQueryOptions(ctx context.Context, owner, repository string, listOptions GitCommitsQueryOptions) ([]CommitInfo, error) {
2✔
709
        err := validateParametersNotBlank(map[string]string{
2✔
710
                "owner":      owner,
2✔
711
                "repository": repository,
2✔
712
        })
2✔
713
        if err != nil {
2✔
UNCOV
714
                return nil, err
×
UNCOV
715
        }
×
716
        var commitsInfo []CommitInfo
2✔
717
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
718
                var ghResponse *github.Response
2✔
719
                commitsInfo, ghResponse, err = client.executeGetCommits(ctx, owner, repository, convertToGitHubCommitsListOptions(listOptions))
2✔
720
                return ghResponse, err
2✔
721
        })
2✔
722
        return commitsInfo, err
2✔
723
}
724

725
func convertToGitHubCommitsListOptions(listOptions GitCommitsQueryOptions) *github.CommitsListOptions {
2✔
726
        return &github.CommitsListOptions{
2✔
727
                Since: listOptions.Since,
2✔
728
                Until: time.Now(),
2✔
729
                ListOptions: github.ListOptions{
2✔
730
                        Page:    listOptions.Page,
2✔
731
                        PerPage: listOptions.PerPage,
2✔
732
                },
2✔
733
        }
2✔
734
}
2✔
735

736
func (client *GitHubClient) executeGetCommits(ctx context.Context, owner, repository string, listOptions *github.CommitsListOptions) ([]CommitInfo, *github.Response, error) {
10✔
737
        commits, ghResponse, err := client.ghClient.Repositories.ListCommits(ctx, owner, repository, listOptions)
10✔
738
        if err != nil {
16✔
739
                return nil, ghResponse, err
6✔
740
        }
6✔
741

742
        var commitsInfo []CommitInfo
4✔
743
        for _, commit := range commits {
11✔
744
                commitInfo := mapGitHubCommitToCommitInfo(commit)
7✔
745
                commitsInfo = append(commitsInfo, commitInfo)
7✔
746
        }
7✔
747
        return commitsInfo, ghResponse, nil
4✔
748
}
749

750
// GetRepositoryInfo on GitHub
751
func (client *GitHubClient) GetRepositoryInfo(ctx context.Context, owner, repository string) (RepositoryInfo, error) {
6✔
752
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
6✔
753
        if err != nil {
9✔
754
                return RepositoryInfo{}, err
3✔
755
        }
3✔
756

757
        var repo *github.Repository
3✔
758
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
759
                var ghResponse *github.Response
3✔
760
                repo, ghResponse, err = client.ghClient.Repositories.Get(ctx, owner, repository)
3✔
761
                return ghResponse, err
3✔
762
        })
3✔
763
        if err != nil {
4✔
764
                return RepositoryInfo{}, err
1✔
765
        }
1✔
766

767
        return RepositoryInfo{RepositoryVisibility: getGitHubRepositoryVisibility(repo), CloneInfo: CloneInfo{HTTP: repo.GetCloneURL(), SSH: repo.GetSSHURL()}}, nil
2✔
768
}
769

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

781
        var commit *github.RepositoryCommit
3✔
782
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
783
                var ghResponse *github.Response
3✔
784
                commit, ghResponse, err = client.ghClient.Repositories.GetCommit(ctx, owner, repository, sha, nil)
3✔
785
                return ghResponse, err
3✔
786
        })
3✔
787
        if err != nil {
5✔
788
                return CommitInfo{}, err
2✔
789
        }
2✔
790

791
        return mapGitHubCommitToCommitInfo(commit), nil
1✔
792
}
793

794
// CreateLabel on GitHub
795
func (client *GitHubClient) CreateLabel(ctx context.Context, owner, repository string, labelInfo LabelInfo) error {
6✔
796
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "LabelInfo.name": labelInfo.Name})
6✔
797
        if err != nil {
10✔
798
                return err
4✔
799
        }
4✔
800

801
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
802
                var ghResponse *github.Response
2✔
803
                _, ghResponse, err = client.ghClient.Issues.CreateLabel(ctx, owner, repository, &github.Label{
2✔
804
                        Name:        &labelInfo.Name,
2✔
805
                        Description: &labelInfo.Description,
2✔
806
                        Color:       &labelInfo.Color,
2✔
807
                })
2✔
808
                return ghResponse, err
2✔
809
        })
2✔
810
}
811

812
// GetLabel on GitHub
813
func (client *GitHubClient) GetLabel(ctx context.Context, owner, repository, name string) (*LabelInfo, error) {
7✔
814
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "name": name})
7✔
815
        if err != nil {
11✔
816
                return nil, err
4✔
817
        }
4✔
818

819
        var labelInfo *LabelInfo
3✔
820
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
821
                var ghResponse *github.Response
3✔
822
                labelInfo, ghResponse, err = client.executeGetLabel(ctx, owner, repository, name)
3✔
823
                return ghResponse, err
3✔
824
        })
3✔
825
        return labelInfo, err
3✔
826
}
827

828
func (client *GitHubClient) executeGetLabel(ctx context.Context, owner, repository, name string) (*LabelInfo, *github.Response, error) {
3✔
829
        label, ghResponse, err := client.ghClient.Issues.GetLabel(ctx, owner, repository, name)
3✔
830
        if err != nil {
5✔
831
                if ghResponse != nil && ghResponse.Response != nil && ghResponse.Response.StatusCode == http.StatusNotFound {
3✔
832
                        return nil, ghResponse, nil
1✔
833
                }
1✔
834
                return nil, ghResponse, err
1✔
835
        }
836

837
        labelInfo := &LabelInfo{
1✔
838
                Name:        *label.Name,
1✔
839
                Description: *label.Description,
1✔
840
                Color:       *label.Color,
1✔
841
        }
1✔
842
        return labelInfo, ghResponse, nil
1✔
843
}
844

845
// ListPullRequestLabels on GitHub
846
func (client *GitHubClient) ListPullRequestLabels(ctx context.Context, owner, repository string, pullRequestID int) ([]string, error) {
5✔
847
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
5✔
848
        if err != nil {
8✔
849
                return nil, err
3✔
850
        }
3✔
851

852
        results := []string{}
2✔
853
        for nextPage := 0; ; nextPage++ {
4✔
854
                options := &github.ListOptions{Page: nextPage}
2✔
855
                var labels []*github.Label
2✔
856
                var ghResponse *github.Response
2✔
857
                err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
858
                        labels, ghResponse, err = client.ghClient.Issues.ListLabelsByIssue(ctx, owner, repository, pullRequestID, options)
2✔
859
                        return ghResponse, err
2✔
860
                })
2✔
861
                if err != nil {
3✔
862
                        return nil, err
1✔
863
                }
1✔
864
                for _, label := range labels {
2✔
865
                        results = append(results, *label.Name)
1✔
866
                }
1✔
867
                if nextPage+1 >= ghResponse.LastPage {
2✔
868
                        break
1✔
869
                }
870
        }
871
        return results, nil
1✔
872
}
873

NEW
UNCOV
874
func (client *GitHubClient) ListPullRequestsAssociatedWithCommit(ctx context.Context, owner, repository string, commitSHA string) ([]PullRequestInfo, error) {
×
NEW
UNCOV
875
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
×
NEW
UNCOV
876
        if err != nil {
×
NEW
UNCOV
877
                return nil, err
×
NEW
UNCOV
878
        }
×
879

NEW
UNCOV
880
        pulls, err := client.ListPullRequestsAssociatedWithCommit(ctx, owner, repository, commitSHA)
×
NEW
UNCOV
881
        if err != nil {
×
NEW
UNCOV
882
                return nil, err
×
NEW
UNCOV
883
        }
×
NEW
UNCOV
884
        return pulls, nil
×
885
}
886

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1195
        return compressedScan, err
1✔
1196
}
1197

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

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

1207
        if !slices.Contains(rateLimitRetryStatuses, ghResponse.StatusCode) {
110✔
1208
                return false
54✔
1209
        }
54✔
1210

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

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

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