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

jfrog / froggit-go / 20894271609

11 Jan 2026 11:18AM UTC coverage: 83.839% (-0.3%) from 84.167%
20894271609

Pull #176

github

orto17
UploadCodeScanningWithRef
Pull Request #176: Upload Code Scanning With Ref To Github

2 of 23 new or added lines in 5 files covered. (8.7%)

225 existing lines in 3 files now uncovered.

4529 of 5402 relevant lines covered (83.84%)

6.33 hits per line

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

84.99
/vcsclient/github.go
1
package vcsclient
2

3
import (
4
        "context"
5
        "crypto/rand"
6
        base64Utils "encoding/base64"
7
        "errors"
8
        "fmt"
9
        "io"
10
        "net/http"
11
        "net/url"
12
        "path/filepath"
13
        "sort"
14
        "strconv"
15
        "strings"
16
        "time"
17

18
        "github.com/google/go-github/v74/github"
19
        "github.com/grokify/mogo/encoding/base64"
20
        "github.com/jfrog/gofrog/datastructures"
21
        "github.com/mitchellh/mapstructure"
22
        "golang.org/x/crypto/nacl/box"
23
        "golang.org/x/exp/slices"
24
        "golang.org/x/oauth2"
25

26
        "github.com/jfrog/froggit-go/vcsutils"
27
)
28

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

164
// ListBranches on GitHub
165
func (client *GitHubClient) ListBranches(ctx context.Context, owner, repository string) (branchList []string, err error) {
2✔
166
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
167
                var ghResponse *github.Response
2✔
168
                branchList, ghResponse, err = client.executeListBranch(ctx, owner, repository)
2✔
169
                return ghResponse, err
2✔
170
        })
2✔
171
        return
2✔
172
}
173

174
func (client *GitHubClient) executeListBranch(ctx context.Context, owner, repository string) ([]string, *github.Response, error) {
2✔
175
        branches, ghResponse, err := client.ghClient.Repositories.ListBranches(ctx, owner, repository, nil)
2✔
176
        if err != nil {
3✔
177
                return []string{}, ghResponse, err
1✔
178
        }
1✔
179

180
        branchList := make([]string, 0, len(branches))
1✔
181
        for _, branch := range branches {
3✔
182
                branchList = append(branchList, *branch.Name)
2✔
183
        }
2✔
184
        return branchList, ghResponse, nil
1✔
185
}
186

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

202
        return strconv.FormatInt(*ghResponseHook.ID, 10), token, nil
1✔
203
}
204

205
// UpdateWebhook on GitHub
206
func (client *GitHubClient) UpdateWebhook(ctx context.Context, owner, repository, _, payloadURL, token,
207
        webhookID string, webhookEvents ...vcsutils.WebhookEvent) error {
2✔
208
        webhookIDInt64, err := strconv.ParseInt(webhookID, 10, 64)
2✔
209
        if err != nil {
2✔
210
                return err
×
211
        }
×
212

213
        hook := createGitHubHook(token, payloadURL, webhookEvents...)
2✔
214
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
215
                var ghResponse *github.Response
2✔
216
                _, ghResponse, err = client.ghClient.Repositories.EditHook(ctx, owner, repository, webhookIDInt64, hook)
2✔
217
                return ghResponse, err
2✔
218
        })
2✔
219
}
220

221
// DeleteWebhook on GitHub
222
func (client *GitHubClient) DeleteWebhook(ctx context.Context, owner, repository, webhookID string) error {
2✔
223
        webhookIDInt64, err := strconv.ParseInt(webhookID, 10, 64)
2✔
224
        if err != nil {
3✔
225
                return err
1✔
226
        }
1✔
227

228
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
2✔
229
                return client.ghClient.Repositories.DeleteHook(ctx, owner, repository, webhookIDInt64)
1✔
230
        })
1✔
231
}
232

233
// SetCommitStatus on GitHub
234
func (client *GitHubClient) SetCommitStatus(ctx context.Context, commitStatus CommitStatus, owner, repository, ref,
235
        title, description, detailsURL string) error {
2✔
236
        state := getGitHubCommitState(commitStatus)
2✔
237
        status := &github.RepoStatus{
2✔
238
                Context:     &title,
2✔
239
                TargetURL:   &detailsURL,
2✔
240
                State:       &state,
2✔
241
                Description: &description,
2✔
242
        }
2✔
243

2✔
244
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
245
                _, ghResponse, err := client.ghClient.Repositories.CreateStatus(ctx, owner, repository, ref, status)
2✔
246
                return ghResponse, err
2✔
247
        })
2✔
248
}
249

250
// GetCommitStatuses on GitHub
251
func (client *GitHubClient) GetCommitStatuses(ctx context.Context, owner, repository, ref string) (statusInfoList []CommitStatusInfo, err error) {
6✔
252
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
12✔
253
                var ghResponse *github.Response
6✔
254
                statusInfoList, ghResponse, err = client.executeGetCommitStatuses(ctx, owner, repository, ref)
6✔
255
                return ghResponse, err
6✔
256
        })
6✔
257
        return
6✔
258
}
259

260
func (client *GitHubClient) executeGetCommitStatuses(ctx context.Context, owner, repository, ref string) (statusInfoList []CommitStatusInfo, ghResponse *github.Response, err error) {
6✔
261
        statuses, ghResponse, err := client.ghClient.Repositories.GetCombinedStatus(ctx, owner, repository, ref, nil)
6✔
262
        if err != nil {
10✔
263
                return
4✔
264
        }
4✔
265

266
        for _, singleStatus := range statuses.Statuses {
6✔
267
                statusInfoList = append(statusInfoList, CommitStatusInfo{
4✔
268
                        State:         commitStatusAsStringToStatus(*singleStatus.State),
4✔
269
                        Description:   singleStatus.GetDescription(),
4✔
270
                        DetailsUrl:    singleStatus.GetTargetURL(),
4✔
271
                        Creator:       singleStatus.GetCreator().GetName(),
4✔
272
                        LastUpdatedAt: singleStatus.GetUpdatedAt().Time,
4✔
273
                        CreatedAt:     singleStatus.GetCreatedAt().Time,
4✔
274
                })
4✔
275
        }
4✔
276
        return
2✔
277
}
278

279
// DownloadRepository on GitHub
280
func (client *GitHubClient) DownloadRepository(ctx context.Context, owner, repository, branch, localPath string) (err error) {
2✔
281
        // Get the archive download link from GitHub
2✔
282
        var baseURL *url.URL
2✔
283
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
284
                var ghResponse *github.Response
2✔
285
                baseURL, ghResponse, err = client.executeGetArchiveLink(ctx, owner, repository, branch)
2✔
286
                return ghResponse, err
2✔
287
        })
2✔
288
        if err != nil {
3✔
289
                return
1✔
290
        }
1✔
291

292
        // Download the archive
293
        httpResponse, err := executeDownloadArchiveFromLink(baseURL.String())
1✔
294
        if err != nil {
1✔
295
                return
×
296
        }
×
297
        defer func() { err = errors.Join(err, httpResponse.Body.Close()) }()
2✔
298
        client.logger.Info(repository, vcsutils.SuccessfulRepoDownload)
1✔
299

1✔
300
        // Untar the archive
1✔
301
        if err = vcsutils.Untar(localPath, httpResponse.Body, true); err != nil {
1✔
302
                return
×
303
        }
×
304
        client.logger.Info(vcsutils.SuccessfulRepoExtraction)
1✔
305

1✔
306
        repositoryInfo, err := client.GetRepositoryInfo(ctx, owner, repository)
1✔
307
        if err != nil {
1✔
308
                return
×
309
        }
×
310
        // Create a .git folder in the archive with the remote repository HTTP clone url
311
        err = vcsutils.CreateDotGitFolderWithRemote(localPath, vcsutils.RemoteName, repositoryInfo.CloneInfo.HTTP)
1✔
312
        return
1✔
313
}
314

315
func (client *GitHubClient) executeGetArchiveLink(ctx context.Context, owner, repository, branch string) (baseURL *url.URL, ghResponse *github.Response, err error) {
2✔
316
        client.logger.Debug("Getting GitHub archive link to download")
2✔
317
        return client.ghClient.Repositories.GetArchiveLink(ctx, owner, repository, github.Tarball,
2✔
318
                &github.RepositoryContentGetOptions{Ref: branch}, 5)
2✔
319
}
2✔
320

321
func executeDownloadArchiveFromLink(baseURL string) (*http.Response, error) {
1✔
322
        httpClient := &http.Client{}
1✔
323
        req, err := http.NewRequest(http.MethodGet, baseURL, nil)
1✔
324
        if err != nil {
1✔
325
                return nil, err
×
326
        }
×
327
        httpResponse, err := httpClient.Do(req)
1✔
328
        if err != nil {
1✔
329
                return httpResponse, err
×
330
        }
×
331
        return httpResponse, vcsutils.CheckResponseStatusWithBody(httpResponse, http.StatusOK)
1✔
332
}
333

334
func (client *GitHubClient) GetPullRequestCommentSizeLimit() int {
×
335
        return githubPrContentSizeLimit
×
336
}
×
337

338
func (client *GitHubClient) GetPullRequestDetailsSizeLimit() int {
×
339
        return githubPrContentSizeLimit
×
340
}
×
341

342
// CreatePullRequest on GitHub
343
func (client *GitHubClient) CreatePullRequest(ctx context.Context, owner, repository, sourceBranch, targetBranch, title, description string) error {
2✔
344
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
345
                _, githubResponse, err := client.executeCreatePullRequest(ctx, owner, repository, sourceBranch, targetBranch, title, description)
2✔
346
                return githubResponse, err
2✔
347
        })
2✔
348
}
349

350
func (client *GitHubClient) CreatePullRequestDetailed(ctx context.Context, owner, repository, sourceBranch, targetBranch, title, description string) (CreatedPullRequestInfo, error) {
2✔
351
        var prInfo CreatedPullRequestInfo
2✔
352

2✔
353
        err := client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
354
                pr, ghResponse, err := client.executeCreatePullRequest(ctx, owner, repository, sourceBranch, targetBranch, title, description)
2✔
355
                if err != nil {
3✔
356
                        return ghResponse, err
1✔
357
                }
1✔
358
                prInfo = mapToPullRequestInfo(pr)
1✔
359
                return ghResponse, nil
1✔
360
        })
361

362
        return prInfo, err
2✔
363
}
364

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

4✔
369
        pr, ghResponse, err := client.ghClient.PullRequests.Create(ctx, owner, repository, &github.NewPullRequest{
4✔
370
                Title: &title,
4✔
371
                Body:  &description,
4✔
372
                Head:  &head,
4✔
373
                Base:  &targetBranch,
4✔
374
        })
4✔
375
        return pr, ghResponse, err
4✔
376
}
4✔
377

378
func mapToPullRequestInfo(pr *github.PullRequest) CreatedPullRequestInfo {
1✔
379
        return CreatedPullRequestInfo{
1✔
380
                Number:      pr.GetNumber(),
1✔
381
                URL:         pr.GetHTMLURL(),
1✔
382
                StatusesUrl: pr.GetStatusesURL(),
1✔
383
        }
1✔
384
}
1✔
385

386
// UpdatePullRequest on GitHub
387
func (client *GitHubClient) UpdatePullRequest(ctx context.Context, owner, repository, title, body, targetBranchName string, id int, state vcsutils.PullRequestState) error {
3✔
388
        client.logger.Debug(vcsutils.UpdatingPullRequest, id)
3✔
389
        var baseRef *github.PullRequestBranch
3✔
390
        if targetBranchName != "" {
5✔
391
                baseRef = &github.PullRequestBranch{Ref: &targetBranchName}
2✔
392
        }
2✔
393
        pullRequest := &github.PullRequest{
3✔
394
                Body:  &body,
3✔
395
                Title: &title,
3✔
396
                State: vcsutils.MapPullRequestState(&state),
3✔
397
                Base:  baseRef,
3✔
398
        }
3✔
399

3✔
400
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
401
                _, ghResponse, err := client.ghClient.PullRequests.Edit(ctx, owner, repository, id, pullRequest)
3✔
402
                return ghResponse, err
3✔
403
        })
3✔
404
}
405

406
// ListOpenPullRequestsWithBody on GitHub
407
func (client *GitHubClient) ListOpenPullRequestsWithBody(ctx context.Context, owner, repository string) ([]PullRequestInfo, error) {
1✔
408
        return client.getOpenPullRequests(ctx, owner, repository, true)
1✔
409
}
1✔
410

411
// ListOpenPullRequests on GitHub
412
func (client *GitHubClient) ListOpenPullRequests(ctx context.Context, owner, repository string) ([]PullRequestInfo, error) {
1✔
413
        return client.getOpenPullRequests(ctx, owner, repository, false)
1✔
414
}
1✔
415

416
func (client *GitHubClient) getOpenPullRequests(ctx context.Context, owner, repository string, withBody bool) ([]PullRequestInfo, error) {
2✔
417
        var pullRequests []*github.PullRequest
2✔
418
        client.logger.Debug(vcsutils.FetchingOpenPullRequests, repository)
2✔
419
        err := client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
420
                var ghResponse *github.Response
2✔
421
                var err error
2✔
422
                pullRequests, ghResponse, err = client.ghClient.PullRequests.List(ctx, owner, repository, &github.PullRequestListOptions{State: "open"})
2✔
423
                return ghResponse, err
2✔
424
        })
2✔
425
        if err != nil {
2✔
426
                return []PullRequestInfo{}, err
×
427
        }
×
428

429
        return mapGitHubPullRequestToPullRequestInfoList(pullRequests, withBody)
2✔
430
}
431

432
func (client *GitHubClient) GetPullRequestByID(ctx context.Context, owner, repository string, pullRequestId int) (PullRequestInfo, error) {
4✔
433
        var pullRequest *github.PullRequest
4✔
434
        var ghResponse *github.Response
4✔
435
        var err error
4✔
436
        client.logger.Debug(vcsutils.FetchingPullRequestById, repository)
4✔
437
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
8✔
438
                pullRequest, ghResponse, err = client.ghClient.PullRequests.Get(ctx, owner, repository, pullRequestId)
4✔
439
                return ghResponse, err
4✔
440
        })
4✔
441
        if err != nil {
7✔
442
                return PullRequestInfo{}, err
3✔
443
        }
3✔
444

445
        if err = vcsutils.CheckResponseStatusWithBody(ghResponse.Response, http.StatusOK); err != nil {
1✔
446
                return PullRequestInfo{}, err
×
447
        }
×
448

449
        return mapGitHubPullRequestToPullRequestInfo(pullRequest, false)
1✔
450
}
451

452
func mapGitHubPullRequestToPullRequestInfo(ghPullRequest *github.PullRequest, withBody bool) (PullRequestInfo, error) {
4✔
453
        var sourceBranch, targetBranch string
4✔
454
        var err1, err2 error
4✔
455
        if ghPullRequest != nil && ghPullRequest.Head != nil && ghPullRequest.Base != nil {
8✔
456
                sourceBranch, err1 = extractBranchFromLabel(vcsutils.DefaultIfNotNil(ghPullRequest.Head.Label))
4✔
457
                targetBranch, err2 = extractBranchFromLabel(vcsutils.DefaultIfNotNil(ghPullRequest.Base.Label))
4✔
458
                err := errors.Join(err1, err2)
4✔
459
                if err != nil {
4✔
460
                        return PullRequestInfo{}, err
×
461
                }
×
462
        }
463

464
        var sourceRepoName, sourceRepoOwner string
4✔
465
        if ghPullRequest.Head.Repo == nil {
4✔
466
                return PullRequestInfo{}, errors.New("the source repository information is missing when fetching the pull request details")
×
467
        }
×
468
        if ghPullRequest.Head.Repo.Owner == nil {
4✔
469
                return PullRequestInfo{}, errors.New("the source repository owner name is missing when fetching the pull request details")
×
470
        }
×
471
        sourceRepoName = vcsutils.DefaultIfNotNil(ghPullRequest.Head.Repo.Name)
4✔
472
        sourceRepoOwner = vcsutils.DefaultIfNotNil(ghPullRequest.Head.Repo.Owner.Login)
4✔
473

4✔
474
        var targetRepoName, targetRepoOwner string
4✔
475
        if ghPullRequest.Base.Repo == nil {
4✔
476
                return PullRequestInfo{}, errors.New("the target repository information is missing when fetching the pull request details")
×
477
        }
×
478
        if ghPullRequest.Base.Repo.Owner == nil {
4✔
479
                return PullRequestInfo{}, errors.New("the target repository owner name is missing when fetching the pull request details")
×
480
        }
×
481
        targetRepoName = vcsutils.DefaultIfNotNil(ghPullRequest.Base.Repo.Name)
4✔
482
        targetRepoOwner = vcsutils.DefaultIfNotNil(ghPullRequest.Base.Repo.Owner.Login)
4✔
483

4✔
484
        var body string
4✔
485
        if withBody {
5✔
486
                body = vcsutils.DefaultIfNotNil(ghPullRequest.Body)
1✔
487
        }
1✔
488

489
        return PullRequestInfo{
4✔
490
                ID:     int64(vcsutils.DefaultIfNotNil(ghPullRequest.Number)),
4✔
491
                Title:  vcsutils.DefaultIfNotNil(ghPullRequest.Title),
4✔
492
                URL:    vcsutils.DefaultIfNotNil(ghPullRequest.HTMLURL),
4✔
493
                Body:   body,
4✔
494
                Author: vcsutils.DefaultIfNotNil(ghPullRequest.User.Login),
4✔
495
                Source: BranchInfo{
4✔
496
                        Name:       sourceBranch,
4✔
497
                        Repository: sourceRepoName,
4✔
498
                        Owner:      sourceRepoOwner,
4✔
499
                },
4✔
500
                Target: BranchInfo{
4✔
501
                        Name:       targetBranch,
4✔
502
                        Repository: targetRepoName,
4✔
503
                        Owner:      targetRepoOwner,
4✔
504
                },
4✔
505
                Status: vcsutils.DefaultIfNotNil(ghPullRequest.State),
4✔
506
        }, nil
4✔
507
}
508

509
// Extracts branch name from the following expected label format repo:branch
510
func extractBranchFromLabel(label string) (string, error) {
8✔
511
        split := strings.Split(label, ":")
8✔
512
        if len(split) <= 1 {
8✔
513
                return "", fmt.Errorf("bad label format %s", label)
×
514
        }
×
515
        return split[1], nil
8✔
516
}
517

518
// AddPullRequestComment on GitHub
519
func (client *GitHubClient) AddPullRequestComment(ctx context.Context, owner, repository, content string, pullRequestID int) error {
6✔
520
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "content": content})
6✔
521
        if err != nil {
10✔
522
                return err
4✔
523
        }
4✔
524

525
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
526
                var ghResponse *github.Response
2✔
527
                // We use the Issues API to add a regular comment. The PullRequests API adds a code review comment.
2✔
528
                _, ghResponse, err = client.ghClient.Issues.CreateComment(ctx, owner, repository, pullRequestID, &github.IssueComment{Body: &content})
2✔
529
                return ghResponse, err
2✔
530
        })
2✔
531
}
532

533
// AddPullRequestReviewComments on GitHub
534
func (client *GitHubClient) AddPullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int, comments ...PullRequestComment) error {
2✔
535
        prID := strconv.Itoa(pullRequestID)
2✔
536
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "pullRequestID": prID})
2✔
537
        if err != nil {
2✔
538
                return err
×
539
        }
×
540
        if len(comments) == 0 {
2✔
541
                return errors.New(vcsutils.ErrNoCommentsProvided)
×
542
        }
×
543

544
        var commits []*github.RepositoryCommit
2✔
545
        var ghResponse *github.Response
2✔
546
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
547
                commits, ghResponse, err = client.ghClient.PullRequests.ListCommits(ctx, owner, repository, pullRequestID, nil)
2✔
548
                return ghResponse, err
2✔
549
        })
2✔
550
        if err != nil {
3✔
551
                return err
1✔
552
        }
1✔
553
        if len(commits) == 0 {
1✔
554
                return errors.New("could not fetch the commits list for pull request " + prID)
×
555
        }
×
556

557
        latestCommitSHA := commits[len(commits)-1].GetSHA()
1✔
558

1✔
559
        for _, comment := range comments {
3✔
560
                err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
561
                        ghResponse, err = client.executeCreatePullRequestReviewComment(ctx, owner, repository, latestCommitSHA, pullRequestID, comment)
2✔
562
                        return ghResponse, err
2✔
563
                })
2✔
564
                if err != nil {
2✔
565
                        return err
×
566
                }
×
567
        }
568
        return nil
1✔
569
}
570

571
func (client *GitHubClient) executeCreatePullRequestReviewComment(ctx context.Context, owner, repository, latestCommitSHA string, pullRequestID int, comment PullRequestComment) (*github.Response, error) {
2✔
572
        filePath := filepath.Clean(comment.NewFilePath)
2✔
573
        startLine := &comment.NewStartLine
2✔
574
        // GitHub API won't accept 'start_line' if it equals the end line
2✔
575
        if *startLine == comment.NewEndLine {
2✔
576
                startLine = nil
×
577
        }
×
578
        _, ghResponse, err := client.ghClient.PullRequests.CreateComment(ctx, owner, repository, pullRequestID, &github.PullRequestComment{
2✔
579
                CommitID:  &latestCommitSHA,
2✔
580
                Body:      &comment.Content,
2✔
581
                StartLine: startLine,
2✔
582
                Line:      &comment.NewEndLine,
2✔
583
                Path:      &filePath,
2✔
584
        })
2✔
585
        if err != nil {
2✔
586
                err = fmt.Errorf("could not create a code review comment for <%s/%s> in pull request %d. error received: %w",
×
587
                        owner, repository, pullRequestID, err)
×
588
        }
×
589
        return ghResponse, err
2✔
590
}
591

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

599
        commentsInfoList := []CommentInfo{}
2✔
600
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
601
                var ghResponse *github.Response
2✔
602
                commentsInfoList, ghResponse, err = client.executeListPullRequestReviewComments(ctx, owner, repository, pullRequestID)
2✔
603
                return ghResponse, err
2✔
604
        })
2✔
605
        return commentsInfoList, err
2✔
606
}
607

608
// ListPullRequestReviews on GitHub
609
func (client *GitHubClient) ListPullRequestReviews(ctx context.Context, owner, repository string, pullRequestID int) ([]PullRequestReviewDetails, error) {
2✔
610
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
2✔
611
        if err != nil {
2✔
612
                return nil, err
×
613
        }
×
614

615
        var reviews []*github.PullRequestReview
2✔
616
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
617
                var ghResponse *github.Response
2✔
618
                reviews, ghResponse, err = client.ghClient.PullRequests.ListReviews(ctx, owner, repository, pullRequestID, nil)
2✔
619
                return ghResponse, err
2✔
620
        })
2✔
621
        if err != nil {
3✔
622
                return nil, err
1✔
623
        }
1✔
624

625
        var reviewInfos []PullRequestReviewDetails
1✔
626
        for _, review := range reviews {
2✔
627
                reviewInfos = append(reviewInfos, PullRequestReviewDetails{
1✔
628
                        ID:          review.GetID(),
1✔
629
                        Reviewer:    review.GetUser().GetLogin(),
1✔
630
                        Body:        review.GetBody(),
1✔
631
                        State:       review.GetState(),
1✔
632
                        SubmittedAt: review.GetSubmittedAt().String(),
1✔
633
                        CommitID:    review.GetCommitID(),
1✔
634
                })
1✔
635
        }
1✔
636

637
        return reviewInfos, nil
1✔
638
}
639

640
func (client *GitHubClient) executeListPullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int) ([]CommentInfo, *github.Response, error) {
2✔
641
        commentsList, ghResponse, err := client.ghClient.PullRequests.ListComments(ctx, owner, repository, pullRequestID, nil)
2✔
642
        if err != nil {
3✔
643
                return []CommentInfo{}, ghResponse, err
1✔
644
        }
1✔
645
        commentsInfoList := []CommentInfo{}
1✔
646
        for _, comment := range commentsList {
2✔
647
                commentsInfoList = append(commentsInfoList, CommentInfo{
1✔
648
                        ID:      comment.GetID(),
1✔
649
                        Content: comment.GetBody(),
1✔
650
                        Created: comment.GetCreatedAt().Time,
1✔
651
                })
1✔
652
        }
1✔
653
        return commentsInfoList, ghResponse, nil
1✔
654
}
655

656
// ListPullRequestComments on GitHub
657
func (client *GitHubClient) ListPullRequestComments(ctx context.Context, owner, repository string, pullRequestID int) ([]CommentInfo, error) {
4✔
658
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
4✔
659
        if err != nil {
4✔
660
                return []CommentInfo{}, err
×
661
        }
×
662

663
        var commentsList []*github.IssueComment
4✔
664
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
8✔
665
                var ghResponse *github.Response
4✔
666
                commentsList, ghResponse, err = client.ghClient.Issues.ListComments(ctx, owner, repository, pullRequestID, &github.IssueListCommentsOptions{})
4✔
667
                return ghResponse, err
4✔
668
        })
4✔
669

670
        if err != nil {
7✔
671
                return []CommentInfo{}, err
3✔
672
        }
3✔
673

674
        return mapGitHubIssuesCommentToCommentInfoList(commentsList)
1✔
675
}
676

677
// DeletePullRequestReviewComments on GitHub
678
func (client *GitHubClient) DeletePullRequestReviewComments(ctx context.Context, owner, repository string, _ int, comments ...CommentInfo) error {
2✔
679
        for _, comment := range comments {
5✔
680
                commentID := comment.ID
3✔
681
                err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "commentID": strconv.FormatInt(commentID, 10)})
3✔
682
                if err != nil {
3✔
683
                        return err
×
684
                }
×
685

686
                err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
687
                        return client.executeDeletePullRequestReviewComment(ctx, owner, repository, commentID)
3✔
688
                })
3✔
689
                if err != nil {
4✔
690
                        return err
1✔
691
                }
1✔
692

693
        }
694
        return nil
1✔
695
}
696

697
func (client *GitHubClient) executeDeletePullRequestReviewComment(ctx context.Context, owner, repository string, commentID int64) (*github.Response, error) {
3✔
698
        ghResponse, err := client.ghClient.PullRequests.DeleteComment(ctx, owner, repository, commentID)
3✔
699
        if err != nil {
4✔
700
                err = fmt.Errorf("could not delete pull request review comment: %w", err)
1✔
701
        }
1✔
702
        return ghResponse, err
3✔
703
}
704

705
// DeletePullRequestComment on GitHub
706
func (client *GitHubClient) DeletePullRequestComment(ctx context.Context, owner, repository string, _, commentID int) error {
2✔
707
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
2✔
708
        if err != nil {
2✔
709
                return err
×
710
        }
×
711
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
712
                return client.executeDeletePullRequestComment(ctx, owner, repository, commentID)
2✔
713
        })
2✔
714
}
715

716
func (client *GitHubClient) executeDeletePullRequestComment(ctx context.Context, owner, repository string, commentID int) (*github.Response, error) {
2✔
717
        ghResponse, err := client.ghClient.Issues.DeleteComment(ctx, owner, repository, int64(commentID))
2✔
718
        if err != nil {
3✔
719
                return ghResponse, err
1✔
720
        }
1✔
721

722
        var statusCode int
1✔
723
        if ghResponse.Response != nil {
2✔
724
                statusCode = ghResponse.Response.StatusCode
1✔
725
        }
1✔
726
        if statusCode != http.StatusNoContent && statusCode != http.StatusOK {
1✔
727
                return ghResponse, fmt.Errorf("expected %d status code while received %d status code", http.StatusNoContent, ghResponse.Response.StatusCode)
×
728
        }
×
729

730
        return ghResponse, nil
1✔
731
}
732

733
// GetLatestCommit on GitHub
734
func (client *GitHubClient) GetLatestCommit(ctx context.Context, owner, repository, branch string) (CommitInfo, error) {
10✔
735
        commits, err := client.GetCommits(ctx, owner, repository, branch)
10✔
736
        if err != nil {
18✔
737
                return CommitInfo{}, err
8✔
738
        }
8✔
739
        latestCommit := CommitInfo{}
2✔
740
        if len(commits) > 0 {
4✔
741
                latestCommit = commits[0]
2✔
742
        }
2✔
743
        return latestCommit, nil
2✔
744
}
745

746
// GetCommits on GitHub
747
func (client *GitHubClient) GetCommits(ctx context.Context, owner, repository, branch string) ([]CommitInfo, error) {
12✔
748
        err := validateParametersNotBlank(map[string]string{
12✔
749
                "owner":      owner,
12✔
750
                "repository": repository,
12✔
751
                "branch":     branch,
12✔
752
        })
12✔
753
        if err != nil {
16✔
754
                return nil, err
4✔
755
        }
4✔
756

757
        var commitsInfo []CommitInfo
8✔
758
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
16✔
759
                var ghResponse *github.Response
8✔
760
                listOptions := &github.CommitsListOptions{
8✔
761
                        SHA: branch,
8✔
762
                        ListOptions: github.ListOptions{
8✔
763
                                Page:    1,
8✔
764
                                PerPage: vcsutils.NumberOfCommitsToFetch,
8✔
765
                        },
8✔
766
                }
8✔
767
                commitsInfo, ghResponse, err = client.executeGetCommits(ctx, owner, repository, listOptions)
8✔
768
                return ghResponse, err
8✔
769
        })
8✔
770
        return commitsInfo, err
8✔
771
}
772

773
// GetCommitsWithQueryOptions on GitHub
774
func (client *GitHubClient) GetCommitsWithQueryOptions(ctx context.Context, owner, repository string, listOptions GitCommitsQueryOptions) ([]CommitInfo, error) {
2✔
775
        err := validateParametersNotBlank(map[string]string{
2✔
776
                "owner":      owner,
2✔
777
                "repository": repository,
2✔
778
        })
2✔
779
        if err != nil {
2✔
780
                return nil, err
×
781
        }
×
782
        var commitsInfo []CommitInfo
2✔
783
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
784
                var ghResponse *github.Response
2✔
785
                commitsInfo, ghResponse, err = client.executeGetCommits(ctx, owner, repository, convertToGitHubCommitsListOptions(listOptions))
2✔
786
                return ghResponse, err
2✔
787
        })
2✔
788
        return commitsInfo, err
2✔
789
}
790

791
func convertToGitHubCommitsListOptions(listOptions GitCommitsQueryOptions) *github.CommitsListOptions {
2✔
792
        return &github.CommitsListOptions{
2✔
793
                Since: listOptions.Since,
2✔
794
                Until: time.Now(),
2✔
795
                ListOptions: github.ListOptions{
2✔
796
                        Page:    listOptions.Page,
2✔
797
                        PerPage: listOptions.PerPage,
2✔
798
                },
2✔
799
        }
2✔
800
}
2✔
801

802
func (client *GitHubClient) executeGetCommits(ctx context.Context, owner, repository string, listOptions *github.CommitsListOptions) ([]CommitInfo, *github.Response, error) {
10✔
803
        commits, ghResponse, err := client.ghClient.Repositories.ListCommits(ctx, owner, repository, listOptions)
10✔
804
        if err != nil {
16✔
805
                return nil, ghResponse, err
6✔
806
        }
6✔
807

808
        var commitsInfo []CommitInfo
4✔
809
        for _, commit := range commits {
11✔
810
                commitInfo := mapGitHubCommitToCommitInfo(commit)
7✔
811
                commitsInfo = append(commitsInfo, commitInfo)
7✔
812
        }
7✔
813
        return commitsInfo, ghResponse, nil
4✔
814
}
815

816
// GetRepositoryInfo on GitHub
817
func (client *GitHubClient) GetRepositoryInfo(ctx context.Context, owner, repository string) (RepositoryInfo, error) {
6✔
818
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
6✔
819
        if err != nil {
9✔
820
                return RepositoryInfo{}, err
3✔
821
        }
3✔
822

823
        var repo *github.Repository
3✔
824
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
825
                var ghResponse *github.Response
3✔
826
                repo, ghResponse, err = client.ghClient.Repositories.Get(ctx, owner, repository)
3✔
827
                return ghResponse, err
3✔
828
        })
3✔
829
        if err != nil {
4✔
830
                return RepositoryInfo{}, err
1✔
831
        }
1✔
832

833
        return RepositoryInfo{RepositoryVisibility: getGitHubRepositoryVisibility(repo), CloneInfo: CloneInfo{HTTP: repo.GetCloneURL(), SSH: repo.GetSSHURL()}}, nil
2✔
834
}
835

836
// GetCommitBySha on GitHub
837
func (client *GitHubClient) GetCommitBySha(ctx context.Context, owner, repository, sha string) (CommitInfo, error) {
7✔
838
        err := validateParametersNotBlank(map[string]string{
7✔
839
                "owner":      owner,
7✔
840
                "repository": repository,
7✔
841
                "sha":        sha,
7✔
842
        })
7✔
843
        if err != nil {
11✔
844
                return CommitInfo{}, err
4✔
845
        }
4✔
846

847
        var commit *github.RepositoryCommit
3✔
848
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
849
                var ghResponse *github.Response
3✔
850
                commit, ghResponse, err = client.ghClient.Repositories.GetCommit(ctx, owner, repository, sha, nil)
3✔
851
                return ghResponse, err
3✔
852
        })
3✔
853
        if err != nil {
5✔
854
                return CommitInfo{}, err
2✔
855
        }
2✔
856

857
        return mapGitHubCommitToCommitInfo(commit), nil
1✔
858
}
859

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

867
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
868
                var ghResponse *github.Response
2✔
869
                _, ghResponse, err = client.ghClient.Issues.CreateLabel(ctx, owner, repository, &github.Label{
2✔
870
                        Name:        &labelInfo.Name,
2✔
871
                        Description: &labelInfo.Description,
2✔
872
                        Color:       &labelInfo.Color,
2✔
873
                })
2✔
874
                return ghResponse, err
2✔
875
        })
2✔
876
}
877

878
// GetLabel on GitHub
879
func (client *GitHubClient) GetLabel(ctx context.Context, owner, repository, name string) (*LabelInfo, error) {
7✔
880
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "name": name})
7✔
881
        if err != nil {
11✔
882
                return nil, err
4✔
883
        }
4✔
884

885
        var labelInfo *LabelInfo
3✔
886
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
887
                var ghResponse *github.Response
3✔
888
                labelInfo, ghResponse, err = client.executeGetLabel(ctx, owner, repository, name)
3✔
889
                return ghResponse, err
3✔
890
        })
3✔
891
        return labelInfo, err
3✔
892
}
893

894
func (client *GitHubClient) executeGetLabel(ctx context.Context, owner, repository, name string) (*LabelInfo, *github.Response, error) {
3✔
895
        label, ghResponse, err := client.ghClient.Issues.GetLabel(ctx, owner, repository, name)
3✔
896
        if err != nil {
5✔
897
                if ghResponse != nil && ghResponse.Response != nil && ghResponse.Response.StatusCode == http.StatusNotFound {
3✔
898
                        return nil, ghResponse, nil
1✔
899
                }
1✔
900
                return nil, ghResponse, err
1✔
901
        }
902

903
        labelInfo := &LabelInfo{
1✔
904
                Name:        *label.Name,
1✔
905
                Description: *label.Description,
1✔
906
                Color:       *label.Color,
1✔
907
        }
1✔
908
        return labelInfo, ghResponse, nil
1✔
909
}
910

911
// ListPullRequestLabels on GitHub
912
func (client *GitHubClient) ListPullRequestLabels(ctx context.Context, owner, repository string, pullRequestID int) ([]string, error) {
5✔
913
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
5✔
914
        if err != nil {
8✔
915
                return nil, err
3✔
916
        }
3✔
917

918
        results := []string{}
2✔
919
        for nextPage := 0; ; nextPage++ {
4✔
920
                options := &github.ListOptions{Page: nextPage}
2✔
921
                var labels []*github.Label
2✔
922
                var ghResponse *github.Response
2✔
923
                err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
924
                        labels, ghResponse, err = client.ghClient.Issues.ListLabelsByIssue(ctx, owner, repository, pullRequestID, options)
2✔
925
                        return ghResponse, err
2✔
926
                })
2✔
927
                if err != nil {
3✔
928
                        return nil, err
1✔
929
                }
1✔
930
                for _, label := range labels {
2✔
931
                        results = append(results, *label.Name)
1✔
932
                }
1✔
933
                if nextPage+1 >= ghResponse.LastPage {
2✔
934
                        break
1✔
935
                }
936
        }
937
        return results, nil
1✔
938
}
939

940
func (client *GitHubClient) ListPullRequestsAssociatedWithCommit(ctx context.Context, owner, repository string, commitSHA string) ([]PullRequestInfo, error) {
2✔
941
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
2✔
942
        if err != nil {
2✔
943
                return nil, err
×
944
        }
×
945

946
        var pulls []*github.PullRequest
2✔
947
        if err = client.runWithRateLimitRetries(func() (ghResponse *github.Response, err error) {
4✔
948
                pulls, ghResponse, err = client.ghClient.PullRequests.ListPullRequestsWithCommit(ctx, owner, repository, commitSHA, nil)
2✔
949
                return ghResponse, err
2✔
950
        }); err != nil {
3✔
951
                return nil, err
1✔
952
        }
1✔
953
        return mapGitHubPullRequestToPullRequestInfoList(pulls, false)
1✔
954
}
955

956
// UnlabelPullRequest on GitHub
957
func (client *GitHubClient) UnlabelPullRequest(ctx context.Context, owner, repository, name string, pullRequestID int) error {
5✔
958
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
5✔
959
        if err != nil {
8✔
960
                return err
3✔
961
        }
3✔
962

963
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
964
                return client.ghClient.Issues.RemoveLabelForIssue(ctx, owner, repository, pullRequestID, name)
2✔
965
        })
2✔
966
}
967

968
// UploadCodeScanning to GitHub Security tab
969
func (client *GitHubClient) UploadCodeScanning(ctx context.Context, owner, repository, branch, sarifContent string) (id string, err error) {
2✔
970
        commit, err := client.GetLatestCommit(ctx, owner, repository, branch)
2✔
971
        if err != nil {
3✔
972
                return
1✔
973
        }
1✔
974

975
        commitSHA := commit.Hash
1✔
976
        branch = vcsutils.AddBranchPrefix(branch)
1✔
977
        client.logger.Debug(vcsutils.UploadingCodeScanning, repository, "/", branch)
1✔
978

1✔
979
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
2✔
980
                var ghResponse *github.Response
1✔
981
                id, ghResponse, err = client.executeUploadCodeScanning(ctx, owner, repository, branch, commitSHA, sarifContent)
1✔
982
                return ghResponse, err
1✔
983
        })
1✔
984
        return
1✔
985
}
986

987
// UploadCodeScanningWithRef uploads SARIF to GitHub Code Scanning with a specific ref and commit SHA
988
// This is useful for PR uploads where the ref should be refs/pull/<number>/head
NEW
989
func (client *GitHubClient) UploadCodeScanningWithRef(ctx context.Context, owner, repository, ref, commitSHA, sarifContent string) (id string, err error) {
×
NEW
990
        client.logger.Debug(vcsutils.UploadingCodeScanning, repository, "/", ref)
×
NEW
991

×
NEW
992
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
×
NEW
993
                var ghResponse *github.Response
×
NEW
994
                id, ghResponse, err = client.executeUploadCodeScanning(ctx, owner, repository, ref, commitSHA, sarifContent)
×
NEW
995
                return ghResponse, err
×
NEW
996
        })
×
NEW
997
        return
×
998
}
999

1000
func (client *GitHubClient) executeUploadCodeScanning(ctx context.Context, owner, repository, ref, commitSHA, sarifContent string) (id string, ghResponse *github.Response, err error) {
1✔
1001
        encodedSarif, err := encodeScanningResult(sarifContent)
1✔
1002
        if err != nil {
1✔
UNCOV
1003
                return
×
UNCOV
1004
        }
×
1005

1006
        sarifID, ghResponse, err := client.ghClient.CodeScanning.UploadSarif(ctx, owner, repository, &github.SarifAnalysis{
1✔
1007
                CommitSHA: &commitSHA,
1✔
1008
                Ref:       &ref,
1✔
1009
                Sarif:     &encodedSarif,
1✔
1010
        })
1✔
1011

1✔
1012
        // According to go-github API - successful ghResponse will return 202 status code
1✔
1013
        // The body of the ghResponse will appear in the error, and the Sarif struct will be empty.
1✔
1014
        if err != nil && ghResponse.Response.StatusCode != http.StatusAccepted {
1✔
UNCOV
1015
                return
×
UNCOV
1016
        }
×
1017

1018
        id = extractIdFronSarifIDIfExists(sarifID)
1✔
1019
        return
1✔
1020
}
1021

1022
func extractIdFronSarifIDIfExists(sarifID *github.SarifID) string {
1✔
1023
        if sarifID != nil && *sarifID.ID != "" {
1✔
UNCOV
1024
                return *sarifID.ID
×
UNCOV
1025
        }
×
1026
        return ""
1✔
1027
}
1028

1029
// DownloadFileFromRepo on GitHub
1030
func (client *GitHubClient) DownloadFileFromRepo(ctx context.Context, owner, repository, branch, path string) (content []byte, statusCode int, err error) {
3✔
1031
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
1032
                var ghResponse *github.Response
3✔
1033
                content, statusCode, ghResponse, err = client.executeDownloadFileFromRepo(ctx, owner, repository, branch, path)
3✔
1034
                return ghResponse, err
3✔
1035
        })
3✔
1036

1037
        return
3✔
1038
}
1039

1040
func (client *GitHubClient) executeDownloadFileFromRepo(ctx context.Context, owner, repository, branch, path string) (content []byte, statusCode int, ghResponse *github.Response, err error) {
3✔
1041
        fileContent, _, ghResponse, err := client.ghClient.Repositories.GetContents(ctx, owner, repository, path, &github.RepositoryContentGetOptions{Ref: branch})
3✔
1042
        if ghResponse == nil || ghResponse.Response == nil {
4✔
1043
                return
1✔
1044
        }
1✔
1045

1046
        statusCode = ghResponse.StatusCode
2✔
1047
        if err != nil {
3✔
1048
                if statusCode != http.StatusOK {
2✔
1049
                        err = fmt.Errorf("expected %d status code while received %d status code with error:\n%s", http.StatusOK, ghResponse.StatusCode, err)
1✔
1050
                }
1✔
1051
                return
1✔
1052
        }
1053

1054
        if fileContent != nil {
2✔
1055
                var contentStr string
1✔
1056
                contentStr, err = fileContent.GetContent()
1✔
1057
                if err != nil {
1✔
UNCOV
1058
                        return
×
UNCOV
1059
                }
×
1060
                content = []byte(contentStr)
1✔
1061
        }
1062
        return
1✔
1063
}
1064

1065
// GetRepositoryEnvironmentInfo on GitHub
1066
func (client *GitHubClient) GetRepositoryEnvironmentInfo(ctx context.Context, owner, repository, name string) (RepositoryEnvironmentInfo, error) {
2✔
1067
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "name": name})
2✔
1068
        if err != nil {
2✔
UNCOV
1069
                return RepositoryEnvironmentInfo{}, err
×
UNCOV
1070
        }
×
1071

1072
        var repositoryEnvInfo *RepositoryEnvironmentInfo
2✔
1073
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1074
                var ghResponse *github.Response
2✔
1075
                repositoryEnvInfo, ghResponse, err = client.executeGetRepositoryEnvironmentInfo(ctx, owner, repository, name)
2✔
1076
                return ghResponse, err
2✔
1077
        })
2✔
1078
        return *repositoryEnvInfo, err
2✔
1079
}
1080

1081
func (client *GitHubClient) CreateBranch(ctx context.Context, owner, repository, sourceBranch, newBranch string) error {
2✔
1082
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "sourceBranch": sourceBranch, "newBranch": newBranch})
2✔
1083
        if err != nil {
2✔
UNCOV
1084
                return err
×
UNCOV
1085
        }
×
1086

1087
        var sourceBranchRef *github.Reference
2✔
1088
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1089
                sourceBranch = vcsutils.AddBranchPrefix(sourceBranch)
2✔
1090
                sourceBranchRef, _, err = client.ghClient.Git.GetRef(ctx, owner, repository, sourceBranch)
2✔
1091
                if err != nil {
3✔
1092
                        return nil, err
1✔
1093
                }
1✔
1094
                return nil, nil
1✔
1095
        })
1096
        if err != nil {
3✔
1097
                return err
1✔
1098
        }
1✔
1099

1100
        if sourceBranchRef == nil {
1✔
UNCOV
1101
                return fmt.Errorf("failed to get reference for source branch %s", sourceBranch)
×
1102
        }
×
1103
        if sourceBranchRef.Object == nil {
1✔
UNCOV
1104
                return fmt.Errorf("source branch %s reference object is nil", sourceBranch)
×
UNCOV
1105
        }
×
1106

1107
        latestCommitSHA := sourceBranchRef.Object.SHA
1✔
1108
        newBranch = vcsutils.AddBranchPrefix(newBranch)
1✔
1109
        ref := &github.Reference{
1✔
1110
                Ref:    github.Ptr(newBranch),
1✔
1111
                Object: &github.GitObject{SHA: latestCommitSHA},
1✔
1112
        }
1✔
1113

1✔
1114
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
2✔
1115
                _, _, err = client.ghClient.Git.CreateRef(ctx, owner, repository, ref)
1✔
1116
                if err != nil {
1✔
UNCOV
1117
                        return nil, err
×
UNCOV
1118
                }
×
1119
                return nil, nil
1✔
1120
        })
1121
}
1122

1123
func (client *GitHubClient) AddOrganizationSecret(ctx context.Context, owner, secretName, secretValue string) error {
2✔
1124
        err := validateParametersNotBlank(map[string]string{"secretName": secretName, "owner": owner, "secretValue": secretValue})
2✔
1125
        if err != nil {
2✔
UNCOV
1126
                return err
×
UNCOV
1127
        }
×
1128

1129
        publicKey, _, err := client.ghClient.Actions.GetOrgPublicKey(ctx, owner)
2✔
1130
        if err != nil {
3✔
1131
                return err
1✔
1132
        }
1✔
1133

1134
        encryptedValue, err := encryptSecret(publicKey, secretValue)
1✔
1135
        if err != nil {
1✔
UNCOV
1136
                return err
×
UNCOV
1137
        }
×
1138

1139
        secret := &github.EncryptedSecret{
1✔
1140
                Name:           secretName,
1✔
1141
                KeyID:          publicKey.GetKeyID(),
1✔
1142
                EncryptedValue: encryptedValue,
1✔
1143
                Visibility:     "all",
1✔
1144
        }
1✔
1145

1✔
1146
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
2✔
1147
                _, err = client.ghClient.Actions.CreateOrUpdateOrgSecret(ctx, owner, secret)
1✔
1148
                return nil, err
1✔
1149
        })
1✔
1150
        return err
1✔
1151
}
1152

1153
func (client *GitHubClient) CreateOrgVariable(ctx context.Context, owner, variableName, variableValue string) error {
2✔
1154
        err := validateParametersNotBlank(map[string]string{"owner": owner, "variableName": variableName, "variableValue": variableValue})
2✔
1155
        if err != nil {
2✔
UNCOV
1156
                return err
×
UNCOV
1157
        }
×
1158

1159
        variable := &github.ActionsVariable{
2✔
1160
                Name:       variableName,
2✔
1161
                Value:      variableValue,
2✔
1162
                Visibility: github.Ptr("all"),
2✔
1163
        }
2✔
1164

2✔
1165
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1166
                _, err = client.ghClient.Actions.CreateOrgVariable(ctx, owner, variable)
2✔
1167
                return nil, err
2✔
1168
        })
2✔
1169
        return err
2✔
1170
}
1171

1172
func (client *GitHubClient) AllowWorkflows(ctx context.Context, owner string) error {
2✔
1173
        err := validateParametersNotBlank(map[string]string{"owner": owner})
2✔
1174
        if err != nil {
2✔
UNCOV
1175
                return err
×
UNCOV
1176
        }
×
1177

1178
        requestBody := &github.ActionsPermissions{
2✔
1179
                AllowedActions:      github.Ptr("all"),
2✔
1180
                EnabledRepositories: github.Ptr("all"),
2✔
1181
        }
2✔
1182

2✔
1183
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1184
                _, _, err = client.ghClient.Actions.EditActionsPermissions(ctx, owner, *requestBody)
2✔
1185
                return nil, err
2✔
1186
        })
2✔
1187
        return err
2✔
1188
}
1189

1190
func (client *GitHubClient) GetRepoCollaborators(ctx context.Context, owner, repo, affiliation, permission string) ([]string, error) {
2✔
1191
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repo": repo, "affiliation": affiliation, "permission": permission})
2✔
1192
        if err != nil {
2✔
UNCOV
1193
                return nil, err
×
UNCOV
1194
        }
×
1195

1196
        var collaborators []*github.User
2✔
1197
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1198
                var ghResponse *github.Response
2✔
1199
                var err error
2✔
1200
                collaborators, ghResponse, err = client.ghClient.Repositories.ListCollaborators(ctx, owner, repo, &github.ListCollaboratorsOptions{
2✔
1201
                        Affiliation: affiliation,
2✔
1202
                        Permission:  permission,
2✔
1203
                })
2✔
1204
                return ghResponse, err
2✔
1205
        })
2✔
1206
        if err != nil {
3✔
1207
                return nil, err
1✔
1208
        }
1✔
1209

1210
        var names []string
1✔
1211
        for _, collab := range collaborators {
2✔
1212
                names = append(names, collab.GetLogin())
1✔
1213
        }
1✔
1214
        return names, nil
1✔
1215
}
1216

1217
func (client *GitHubClient) GetRepoTeamsByPermissions(ctx context.Context, owner, repo string, permissions []string) ([]int64, error) {
2✔
1218
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repo": repo, "permissions": strings.Join(permissions, ",")})
2✔
1219
        if err != nil {
2✔
UNCOV
1220
                return nil, err
×
UNCOV
1221
        }
×
1222

1223
        var allTeams []*github.Team
2✔
1224
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1225
                var resp *github.Response
2✔
1226
                var err error
2✔
1227
                allTeams, resp, err = client.ghClient.Repositories.ListTeams(ctx, owner, repo, nil)
2✔
1228
                return resp, err
2✔
1229
        })
2✔
1230
        if err != nil {
3✔
1231
                return nil, err
1✔
1232
        }
1✔
1233

1234
        permMap := make(map[string]bool)
1✔
1235
        for _, p := range permissions {
2✔
1236
                permMap[strings.ToLower(p)] = true
1✔
1237
        }
1✔
1238

1239
        var matchedTeams []int64
1✔
1240
        for _, team := range allTeams {
2✔
1241
                if permMap[strings.ToLower(team.GetPermission())] {
2✔
1242
                        matchedTeams = append(matchedTeams, team.GetID())
1✔
1243
                }
1✔
1244
        }
1245

1246
        return matchedTeams, nil
1✔
1247
}
1248

1249
func (client *GitHubClient) CreateOrUpdateEnvironment(ctx context.Context, owner, repo, envName string, teams []int64, users []string) error {
2✔
1250
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repo": repo, "envName": envName})
2✔
1251
        if err != nil {
2✔
UNCOV
1252
                return err
×
UNCOV
1253
        }
×
1254

1255
        var envReviewers []*github.EnvReviewers
2✔
1256
        for _, team := range teams {
4✔
1257
                envReviewers = append(envReviewers, &github.EnvReviewers{
2✔
1258
                        Type: github.Ptr("Team"),
2✔
1259
                        ID:   &team,
2✔
1260
                })
2✔
1261
        }
2✔
1262

1263
        if len(envReviewers) >= ghMaxEnvReviewers {
2✔
1264
                envReviewers = envReviewers[:ghMaxEnvReviewers]
×
1265
                _, _, err := client.ghClient.Repositories.CreateUpdateEnvironment(ctx, owner, repo, envName, &github.CreateUpdateEnvironment{
×
1266
                        Reviewers: envReviewers,
×
1267
                })
×
UNCOV
1268
                return err
×
UNCOV
1269
        }
×
1270

1271
        for _, userName := range users {
2✔
1272
                user, _, err := client.ghClient.Users.Get(ctx, userName)
×
1273

×
1274
                if err != nil {
×
1275
                        return err
×
1276
                }
×
1277
                userId := user.GetID()
×
1278
                envReviewers = append(envReviewers, &github.EnvReviewers{
×
1279
                        Type: github.Ptr("User"),
×
UNCOV
1280
                        ID:   github.Ptr(userId),
×
UNCOV
1281
                })
×
1282
        }
1283

1284
        if len(envReviewers) >= ghMaxEnvReviewers {
2✔
1285
                envReviewers = envReviewers[:ghMaxEnvReviewers]
×
1286
                _, _, err := client.ghClient.Repositories.CreateUpdateEnvironment(ctx, owner, repo, envName, &github.CreateUpdateEnvironment{
×
1287
                        Reviewers: envReviewers,
×
1288
                })
×
UNCOV
1289
                return err
×
UNCOV
1290
        }
×
1291

1292
        _, _, err = client.ghClient.Repositories.CreateUpdateEnvironment(ctx, owner, repo, envName, &github.CreateUpdateEnvironment{
2✔
1293
                Reviewers: envReviewers,
2✔
1294
        })
2✔
1295
        return err
2✔
1296
}
1297

1298
func (client *GitHubClient) CommitAndPushFiles(
1299
        ctx context.Context,
1300
        owner, repo, sourceBranch, commitMessage, authorName, authorEmail string,
1301
        files []FileToCommit,
1302
) error {
2✔
1303
        if len(files) == 0 {
2✔
UNCOV
1304
                return errors.New("no files provided to commit")
×
UNCOV
1305
        }
×
1306

1307
        if len(files) == 1 {
2✔
1308
                client.logger.Debug("Using Contents API for single file commit")
×
UNCOV
1309
                return client.commitSingleFile(ctx, owner, repo, sourceBranch, files[0], commitMessage, authorName, authorEmail)
×
UNCOV
1310
        }
×
1311

1312
        client.logger.Debug("Using Git API for ", len(files), " file commit")
2✔
1313
        return client.commitMultipleFiles(ctx, owner, repo, sourceBranch, files, commitMessage, authorName, authorEmail)
2✔
1314
}
1315

1316
func (client *GitHubClient) commitSingleFile(
1317
        ctx context.Context,
1318
        owner, repo, branch string,
1319
        file FileToCommit,
1320
        commitMessage, authorName, authorEmail string,
1321
) error {
×
1322

×
1323
        fileOptions := &github.RepositoryContentFileOptions{
×
1324
                Message: &commitMessage,
×
1325
                Content: []byte(file.Content),
×
1326
                Branch:  &branch,
×
1327
                Author: &github.CommitAuthor{
×
1328
                        Name:  &authorName,
×
1329
                        Email: &authorEmail,
×
1330
                },
×
1331
                Committer: &github.CommitAuthor{
×
1332
                        Name:  &authorName,
×
1333
                        Email: &authorEmail,
×
1334
                },
×
1335
        }
×
1336

×
1337
        err := client.runWithRateLimitRetries(func() (*github.Response, error) {
×
1338
                _, ghResponse, err := client.ghClient.Repositories.CreateFile(ctx, owner, repo, file.Path, fileOptions)
×
UNCOV
1339
                return ghResponse, err
×
1340
        })
×
1341

1342
        if err != nil {
×
1343
                return fmt.Errorf("failed to commit single file %s: %w", file.Path, err)
×
UNCOV
1344
        }
×
UNCOV
1345
        return nil
×
1346
}
1347

1348
func (client *GitHubClient) commitMultipleFiles(
1349
        ctx context.Context,
1350
        owner, repo, sourceBranch string,
1351
        files []FileToCommit,
1352
        commitMessage, authorName, authorEmail string,
1353
) error {
2✔
1354
        ref, _, err := client.ghClient.Git.GetRef(ctx, owner, repo, "refs/heads/"+sourceBranch)
2✔
1355
        if err != nil {
3✔
1356
                return fmt.Errorf("failed to get branch ref: %w", err)
1✔
1357
        }
1✔
1358

1359
        parentCommit, _, err := client.ghClient.Git.GetCommit(ctx, owner, repo, *ref.Object.SHA)
1✔
1360
        if err != nil {
1✔
UNCOV
1361
                return fmt.Errorf("failed to get parent commit: %w", err)
×
UNCOV
1362
        }
×
1363

1364
        treeEntries, err := client.createBlobs(ctx, owner, repo, files)
1✔
1365
        if err != nil {
1✔
UNCOV
1366
                return err
×
UNCOV
1367
        }
×
1368

1369
        tree, _, err := client.ghClient.Git.CreateTree(ctx, owner, repo, *parentCommit.Tree.SHA, treeEntries)
1✔
1370
        if err != nil {
1✔
UNCOV
1371
                return fmt.Errorf("failed to create tree: %w", err)
×
UNCOV
1372
        }
×
1373

1374
        commit := &github.Commit{
1✔
1375
                Message: github.Ptr(commitMessage),
1✔
1376
                Tree:    tree,
1✔
1377
                Parents: []*github.Commit{{SHA: parentCommit.SHA}},
1✔
1378
                Author: &github.CommitAuthor{
1✔
1379
                        Name:  github.Ptr(authorName),
1✔
1380
                        Email: github.Ptr(authorEmail),
1✔
1381
                        Date:  &github.Timestamp{Time: time.Now()},
1✔
1382
                },
1✔
1383
        }
1✔
1384

1✔
1385
        newCommit, _, err := client.ghClient.Git.CreateCommit(ctx, owner, repo, commit, nil)
1✔
1386
        if err != nil {
1✔
UNCOV
1387
                return fmt.Errorf("failed to create commit: %w", err)
×
UNCOV
1388
        }
×
1389

1390
        ref.Object.SHA = newCommit.SHA
1✔
1391
        _, _, err = client.ghClient.Git.UpdateRef(ctx, owner, repo, ref, false)
1✔
1392
        if err != nil {
1✔
UNCOV
1393
                return fmt.Errorf("failed to update branch ref: %w", err)
×
UNCOV
1394
        }
×
1395
        return nil
1✔
1396
}
1397

1398
func (client *GitHubClient) createBlobs(ctx context.Context, owner, repo string, files []FileToCommit) ([]*github.TreeEntry, error) {
1✔
1399
        var treeEntries []*github.TreeEntry
1✔
1400
        for _, file := range files {
3✔
1401
                var blob *github.Blob
2✔
1402
                err := client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1403
                        var ghResponse *github.Response
2✔
1404
                        var err error
2✔
1405
                        blob, ghResponse, err = client.ghClient.Git.CreateBlob(ctx, owner, repo, &github.Blob{
2✔
1406
                                Content:  github.Ptr(file.Content),
2✔
1407
                                Encoding: github.Ptr("utf-8"),
2✔
1408
                        })
2✔
1409
                        return ghResponse, err
2✔
1410
                })
2✔
1411
                if err != nil {
2✔
UNCOV
1412
                        return nil, fmt.Errorf("failed to create blob for %s: %w", file.Path, err)
×
UNCOV
1413
                }
×
1414

1415
                treeEntries = append(treeEntries, &github.TreeEntry{
2✔
1416
                        Path: github.Ptr(file.Path),
2✔
1417
                        Mode: github.Ptr(regularFileCode),
2✔
1418
                        Type: github.Ptr("blob"),
2✔
1419
                        SHA:  blob.SHA,
2✔
1420
                })
2✔
1421
        }
1422

1423
        return treeEntries, nil
1✔
1424
}
1425

1426
func (client *GitHubClient) MergePullRequest(ctx context.Context, owner, repo string, prNumber int, commitMessage string) error {
2✔
1427
        err := client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1428
                _, resp, err := client.ghClient.PullRequests.Merge(ctx, owner, repo, prNumber, commitMessage, nil)
2✔
1429
                return resp, err
2✔
1430
        })
2✔
1431
        return err
2✔
1432
}
1433

1434
func (client *GitHubClient) executeGetRepositoryEnvironmentInfo(ctx context.Context, owner, repository, name string) (*RepositoryEnvironmentInfo, *github.Response, error) {
2✔
1435
        environment, ghResponse, err := client.ghClient.Repositories.GetEnvironment(ctx, owner, repository, name)
2✔
1436
        if err != nil {
3✔
1437
                return &RepositoryEnvironmentInfo{}, ghResponse, err
1✔
1438
        }
1✔
1439

1440
        if err = vcsutils.CheckResponseStatusWithBody(ghResponse.Response, http.StatusOK); err != nil {
1✔
UNCOV
1441
                return &RepositoryEnvironmentInfo{}, ghResponse, err
×
UNCOV
1442
        }
×
1443

1444
        reviewers, err := extractGitHubEnvironmentReviewers(environment)
1✔
1445
        if err != nil {
1✔
UNCOV
1446
                return &RepositoryEnvironmentInfo{}, ghResponse, err
×
UNCOV
1447
        }
×
1448

1449
        return &RepositoryEnvironmentInfo{
1✔
1450
                        Name:      environment.GetName(),
1✔
1451
                        Url:       environment.GetURL(),
1✔
1452
                        Reviewers: reviewers,
1✔
1453
                },
1✔
1454
                ghResponse,
1✔
1455
                nil
1✔
1456
}
1457

1458
func (client *GitHubClient) GetModifiedFiles(ctx context.Context, owner, repository, refBefore, refAfter string) ([]string, error) {
6✔
1459
        err := validateParametersNotBlank(map[string]string{
6✔
1460
                "owner":      owner,
6✔
1461
                "repository": repository,
6✔
1462
                "refBefore":  refBefore,
6✔
1463
                "refAfter":   refAfter,
6✔
1464
        })
6✔
1465
        if err != nil {
10✔
1466
                return nil, err
4✔
1467
        }
4✔
1468

1469
        var fileNamesList []string
2✔
1470
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1471
                var ghResponse *github.Response
2✔
1472
                fileNamesList, ghResponse, err = client.executeGetModifiedFiles(ctx, owner, repository, refBefore, refAfter)
2✔
1473
                return ghResponse, err
2✔
1474
        })
2✔
1475
        return fileNamesList, err
2✔
1476
}
1477

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

2✔
1485
        comparison, ghResponse, err := client.ghClient.Repositories.CompareCommits(ctx, owner, repository, refBefore, refAfter, listOptions)
2✔
1486
        if err != nil {
3✔
1487
                return nil, ghResponse, err
1✔
1488
        }
1✔
1489

1490
        if err = vcsutils.CheckResponseStatusWithBody(ghResponse.Response, http.StatusOK); err != nil {
1✔
UNCOV
1491
                return nil, ghResponse, err
×
UNCOV
1492
        }
×
1493

1494
        fileNamesSet := datastructures.MakeSet[string]()
1✔
1495
        for _, file := range comparison.Files {
18✔
1496
                fileNamesSet.Add(vcsutils.DefaultIfNotNil(file.Filename))
17✔
1497
                fileNamesSet.Add(vcsutils.DefaultIfNotNil(file.PreviousFilename))
17✔
1498
        }
17✔
1499

1500
        _ = fileNamesSet.Remove("") // Make sure there are no blank filepath.
1✔
1501
        fileNamesList := fileNamesSet.ToSlice()
1✔
1502
        sort.Strings(fileNamesList)
1✔
1503

1✔
1504
        return fileNamesList, ghResponse, nil
1✔
1505
}
1506

1507
// Extract code reviewers from environment
1508
func extractGitHubEnvironmentReviewers(environment *github.Environment) ([]string, error) {
2✔
1509
        var reviewers []string
2✔
1510
        protectionRules := environment.ProtectionRules
2✔
1511
        if protectionRules == nil {
2✔
UNCOV
1512
                return reviewers, nil
×
UNCOV
1513
        }
×
1514
        reviewerStruct := repositoryEnvironmentReviewer{}
2✔
1515
        for _, rule := range protectionRules {
4✔
1516
                for _, reviewer := range rule.Reviewers {
5✔
1517
                        if err := mapstructure.Decode(reviewer.Reviewer, &reviewerStruct); err != nil {
3✔
UNCOV
1518
                                return []string{}, err
×
UNCOV
1519
                        }
×
1520
                        reviewers = append(reviewers, reviewerStruct.Login)
3✔
1521
                }
1522
        }
1523
        return reviewers, nil
2✔
1524
}
1525

1526
func createGitHubHook(token, payloadURL string, webhookEvents ...vcsutils.WebhookEvent) *github.Hook {
4✔
1527
        contentType := "json"
4✔
1528
        return &github.Hook{
4✔
1529
                Events: getGitHubWebhookEvents(webhookEvents...),
4✔
1530
                Config: &github.HookConfig{
4✔
1531
                        ContentType: &contentType,
4✔
1532
                        URL:         &payloadURL,
4✔
1533
                        Secret:      &token,
4✔
1534
                },
4✔
1535
        }
4✔
1536
}
4✔
1537

1538
// Get varargs of webhook events and return a slice of GitHub webhook events
1539
func getGitHubWebhookEvents(webhookEvents ...vcsutils.WebhookEvent) []string {
4✔
1540
        events := datastructures.MakeSet[string]()
4✔
1541
        for _, event := range webhookEvents {
16✔
1542
                switch event {
12✔
1543
                case vcsutils.PrOpened, vcsutils.PrEdited, vcsutils.PrMerged, vcsutils.PrRejected:
8✔
1544
                        events.Add("pull_request")
8✔
1545
                case vcsutils.Push, vcsutils.TagPushed, vcsutils.TagRemoved:
4✔
1546
                        events.Add("push")
4✔
1547
                }
1548
        }
1549
        return events.ToSlice()
4✔
1550
}
1551

1552
func getGitHubRepositoryVisibility(repo *github.Repository) RepositoryVisibility {
5✔
1553
        switch *repo.Visibility {
5✔
1554
        case "public":
3✔
1555
                return Public
3✔
1556
        case "internal":
1✔
1557
                return Internal
1✔
1558
        default:
1✔
1559
                return Private
1✔
1560
        }
1561
}
1562

1563
func getGitHubCommitState(commitState CommitStatus) string {
7✔
1564
        switch commitState {
7✔
1565
        case Pass:
1✔
1566
                return "success"
1✔
1567
        case Fail:
1✔
1568
                return "failure"
1✔
1569
        case Error:
3✔
1570
                return "error"
3✔
1571
        case InProgress:
1✔
1572
                return "pending"
1✔
1573
        }
1574
        return ""
1✔
1575
}
1576

1577
func mapGitHubCommitToCommitInfo(commit *github.RepositoryCommit) CommitInfo {
8✔
1578
        parents := make([]string, len(commit.Parents))
8✔
1579
        for i, c := range commit.Parents {
15✔
1580
                parents[i] = c.GetSHA()
7✔
1581
        }
7✔
1582
        details := commit.GetCommit()
8✔
1583
        return CommitInfo{
8✔
1584
                Hash:          commit.GetSHA(),
8✔
1585
                AuthorName:    details.GetAuthor().GetName(),
8✔
1586
                CommitterName: details.GetCommitter().GetName(),
8✔
1587
                Url:           commit.GetURL(),
8✔
1588
                Timestamp:     details.GetCommitter().GetDate().UTC().Unix(),
8✔
1589
                Message:       details.GetMessage(),
8✔
1590
                ParentHashes:  parents,
8✔
1591
                AuthorEmail:   details.GetAuthor().GetEmail(),
8✔
1592
        }
8✔
1593
}
1594

1595
func mapGitHubIssuesCommentToCommentInfoList(commentsList []*github.IssueComment) (res []CommentInfo, err error) {
1✔
1596
        for _, comment := range commentsList {
3✔
1597
                res = append(res, CommentInfo{
2✔
1598
                        ID:      comment.GetID(),
2✔
1599
                        Content: comment.GetBody(),
2✔
1600
                        Created: comment.GetCreatedAt().Time,
2✔
1601
                })
2✔
1602
        }
2✔
1603
        return
1✔
1604
}
1605

1606
func mapGitHubPullRequestToPullRequestInfoList(pullRequestList []*github.PullRequest, withBody bool) (res []PullRequestInfo, err error) {
3✔
1607
        var mappedPullRequest PullRequestInfo
3✔
1608
        for _, pullRequest := range pullRequestList {
6✔
1609
                mappedPullRequest, err = mapGitHubPullRequestToPullRequestInfo(pullRequest, withBody)
3✔
1610
                if err != nil {
3✔
UNCOV
1611
                        return
×
UNCOV
1612
                }
×
1613
                res = append(res, mappedPullRequest)
3✔
1614
        }
1615
        return
3✔
1616
}
1617

1618
func encodeScanningResult(data string) (string, error) {
1✔
1619
        compressedScan, err := base64.EncodeGzip([]byte(data), 6)
1✔
1620
        if err != nil {
1✔
UNCOV
1621
                return "", err
×
UNCOV
1622
        }
×
1623

1624
        return compressedScan, err
1✔
1625
}
1626

1627
type repositoryEnvironmentReviewer struct {
1628
        Login string `mapstructure:"login"`
1629
}
1630

1631
func shouldRetryIfRateLimitExceeded(ghResponse *github.Response, requestError error) bool {
117✔
1632
        if ghResponse == nil || ghResponse.Response == nil {
168✔
1633
                return false
51✔
1634
        }
51✔
1635

1636
        if !slices.Contains(rateLimitRetryStatuses, ghResponse.StatusCode) {
130✔
1637
                return false
64✔
1638
        }
64✔
1639

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

1646
        body, err := io.ReadAll(ghResponse.Body)
1✔
1647
        if err != nil {
1✔
UNCOV
1648
                return false
×
UNCOV
1649
        }
×
1650
        return strings.Contains(string(body), "rate limit")
1✔
1651
}
1652

1653
func isRateLimitAbuseError(requestError error) bool {
4✔
1654
        var abuseRateLimitError *github.AbuseRateLimitError
4✔
1655
        var rateLimitError *github.RateLimitError
4✔
1656
        return errors.As(requestError, &abuseRateLimitError) || errors.As(requestError, &rateLimitError)
4✔
1657
}
4✔
1658

1659
func encryptSecret(publicKey *github.PublicKey, secretValue string) (string, error) {
1✔
1660
        publicKeyBytes, err := base64Utils.StdEncoding.DecodeString(publicKey.GetKey())
1✔
1661
        if err != nil {
1✔
UNCOV
1662
                return "", err
×
UNCOV
1663
        }
×
1664

1665
        var publicKeyDecoded [32]byte
1✔
1666
        copy(publicKeyDecoded[:], publicKeyBytes)
1✔
1667

1✔
1668
        encrypted, err := box.SealAnonymous(nil, []byte(secretValue), &publicKeyDecoded, rand.Reader)
1✔
1669
        if err != nil {
1✔
UNCOV
1670
                return "", err
×
UNCOV
1671
        }
×
1672

1673
        encryptedBase64 := base64Utils.StdEncoding.EncodeToString(encrypted)
1✔
1674
        return encryptedBase64, nil
1✔
1675
}
1676

1677
func (client *GitHubClient) ListAppRepositories(ctx context.Context) ([]AppRepositoryInfo, error) {
2✔
1678
        var results []AppRepositoryInfo
2✔
1679

2✔
1680
        var allRepositories []*github.Repository
2✔
1681
        for nextPage := 1; ; nextPage++ {
4✔
1682
                var repositoriesInPage *github.ListRepositories
2✔
1683
                var ghResponse *github.Response
2✔
1684
                var err error
2✔
1685
                err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1686
                        repositoriesInPage, ghResponse, err = client.ghClient.Apps.ListRepos(ctx, &github.ListOptions{Page: nextPage})
2✔
1687
                        return ghResponse, err
2✔
1688
                })
2✔
1689
                if err != nil {
3✔
1690
                        return nil, err
1✔
1691
                }
1✔
1692
                allRepositories = append(allRepositories, repositoriesInPage.Repositories...)
1✔
1693
                if nextPage+1 > ghResponse.LastPage {
2✔
1694
                        break
1✔
1695
                }
1696
        }
1697

1698
        for _, repo := range allRepositories {
2✔
1699
                if repo == nil || repo.Owner == nil || repo.Owner.Login == nil || repo.Name == nil {
1✔
UNCOV
1700
                        continue
×
1701
                }
1702
                repoInfo := AppRepositoryInfo{
1✔
1703
                        ID:            repo.GetID(),
1✔
1704
                        Name:          vcsutils.DefaultIfNotNil(repo.Name),
1✔
1705
                        FullName:      vcsutils.DefaultIfNotNil(repo.FullName),
1✔
1706
                        Owner:         vcsutils.DefaultIfNotNil(repo.Owner.Login),
1✔
1707
                        Private:       repo.GetPrivate(),
1✔
1708
                        Description:   vcsutils.DefaultIfNotNil(repo.Description),
1✔
1709
                        URL:           vcsutils.DefaultIfNotNil(repo.HTMLURL),
1✔
1710
                        CloneURL:      vcsutils.DefaultIfNotNil(repo.CloneURL),
1✔
1711
                        SSHURL:        vcsutils.DefaultIfNotNil(repo.SSHURL),
1✔
1712
                        DefaultBranch: vcsutils.DefaultIfNotNil(repo.DefaultBranch),
1✔
1713
                }
1✔
1714
                results = append(results, repoInfo)
1✔
1715
        }
1716

1717
        return results, nil
1✔
1718
}
1719
func (client *GitHubClient) UploadSnapshotToDependencyGraph(ctx context.Context, owner, repo string, snapshot *SbomSnapshot) error {
2✔
1720
        if snapshot == nil {
2✔
UNCOV
1721
                return fmt.Errorf("provided snapshot is nil")
×
UNCOV
1722
        }
×
1723

1724
        ghSnapshot, err := convertToGitHubSnapshot(snapshot)
2✔
1725
        if err != nil {
2✔
UNCOV
1726
                return fmt.Errorf("failed to convert snapshot to GitHub format: %w", err)
×
UNCOV
1727
        }
×
1728

1729
        var ghResponse *github.Response
2✔
1730
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1731
                _, ghResponse, err = client.ghClient.DependencyGraph.CreateSnapshot(ctx, owner, repo, ghSnapshot)
2✔
1732
                return ghResponse, err
2✔
1733
        })
2✔
1734

1735
        if err != nil {
3✔
1736
                return fmt.Errorf("failed to upload snapshot to dependency graph: %w", err)
1✔
1737
        }
1✔
1738

1739
        if ghResponse == nil || ghResponse.Response == nil || ghResponse.Response.StatusCode != http.StatusCreated {
1✔
UNCOV
1740
                return fmt.Errorf("dependency submission call finished with unexpected status code: %d", ghResponse.Response.StatusCode)
×
UNCOV
1741
        }
×
1742

1743
        client.logger.Info(vcsutils.SuccessfulSnapshotUpload, ghResponse.StatusCode)
1✔
1744
        return nil
1✔
1745
}
1746

1747
func convertToGitHubSnapshot(snapshot *SbomSnapshot) (*github.DependencyGraphSnapshot, error) {
2✔
1748
        ghSnapshot := &github.DependencyGraphSnapshot{
2✔
1749
                Version: snapshot.Version,
2✔
1750
                Sha:     &snapshot.Sha,
2✔
1751
                Ref:     &snapshot.Ref,
2✔
1752
                Scanned: &github.Timestamp{Time: snapshot.Scanned}, // Use current time if not provided
2✔
1753
        }
2✔
1754

2✔
1755
        if snapshot.Job == nil {
2✔
UNCOV
1756
                return nil, fmt.Errorf("job information is required in the snapshot")
×
UNCOV
1757
        }
×
1758
        ghSnapshot.Job = &github.DependencyGraphSnapshotJob{
2✔
1759
                Correlator: &snapshot.Job.Correlator,
2✔
1760
                ID:         &snapshot.Job.ID,
2✔
1761
        }
2✔
1762

2✔
1763
        if snapshot.Detector == nil {
2✔
UNCOV
1764
                return nil, fmt.Errorf("detector information is required in the snapshot")
×
UNCOV
1765
        }
×
1766
        ghSnapshot.Detector = &github.DependencyGraphSnapshotDetector{
2✔
1767
                Name:    &snapshot.Detector.Name,
2✔
1768
                Version: &snapshot.Detector.Version,
2✔
1769
                URL:     &snapshot.Detector.Url,
2✔
1770
        }
2✔
1771

2✔
1772
        if len(snapshot.Manifests) == 0 {
2✔
UNCOV
1773
                return nil, fmt.Errorf("at least one manifest is required in the snapshot")
×
UNCOV
1774
        }
×
1775
        ghSnapshot.Manifests = make(map[string]*github.DependencyGraphSnapshotManifest)
2✔
1776
        for manifestName, manifest := range snapshot.Manifests {
4✔
1777
                ghManifest := &github.DependencyGraphSnapshotManifest{
2✔
1778
                        Name: &manifest.Name,
2✔
1779
                }
2✔
1780

2✔
1781
                if manifest.File == nil {
2✔
UNCOV
1782
                        return nil, fmt.Errorf("manifest '%s' is missing file information", manifestName)
×
UNCOV
1783
                }
×
1784
                ghManifest.File = &github.DependencyGraphSnapshotManifestFile{SourceLocation: &manifest.File.SourceLocation}
2✔
1785

2✔
1786
                if len(manifest.Resolved) == 0 {
2✔
UNCOV
1787
                        return nil, fmt.Errorf("manifest '%s' must have at least one resolved dependency", manifestName)
×
UNCOV
1788
                }
×
1789
                ghManifest.Resolved = make(map[string]*github.DependencyGraphSnapshotResolvedDependency)
2✔
1790
                for depName, dep := range manifest.Resolved {
8✔
1791
                        ghDep := &github.DependencyGraphSnapshotResolvedDependency{
6✔
1792
                                PackageURL:   &dep.PackageURL,
6✔
1793
                                Dependencies: dep.Dependencies,
6✔
1794
                                Relationship: &dep.Relationship,
6✔
1795
                        }
6✔
1796
                        ghManifest.Resolved[depName] = ghDep
6✔
1797
                }
6✔
1798

1799
                ghSnapshot.Manifests[manifestName] = ghManifest
2✔
1800
        }
1801
        return ghSnapshot, nil
2✔
1802
}
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