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

jfrog / froggit-go / 29495493321

16 Jul 2026 09:57AM UTC coverage: 83.523% (+0.4%) from 83.089%
29495493321

Pull #194

github

shraddhan
Fix single-file commit missing SHA when updating existing GitHub content

GetContents before Contents API PUT and set blob SHA for updates so
CommitAndPushFiles no longer returns 422 "sha wasn't supplied" (froggit-go#188).
Pull Request #194: Fix single-file commit missing SHA when updating existing GitHub content

16 of 19 new or added lines in 1 file covered. (84.21%)

4851 of 5808 relevant lines covered (83.52%)

6.28 hits per line

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

87.77
/vcsclient/github.go
1
package vcsclient
2

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

165
// ListRepositoriesByOwner on GitHub returns repositories for the given organization or user.
166
// It tries listing as an org first; if that fails, falls back to listing as a user.
167
func (client *GitHubClient) ListRepositoriesByOwner(ctx context.Context, owner string) ([]string, error) {
3✔
168
        var repos []string
3✔
169
        for page := 1; ; page++ {
6✔
170
                var reposPage []*github.Repository
3✔
171
                var ghResponse *github.Response
3✔
172
                err := client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
173
                        var e error
3✔
174
                        reposPage, ghResponse, e = client.ghClient.Repositories.ListByOrg(ctx, owner,
3✔
175
                                &github.RepositoryListByOrgOptions{ListOptions: github.ListOptions{Page: page}})
3✔
176
                        return ghResponse, e
3✔
177
                })
3✔
178
                if err != nil {
5✔
179
                        // owner is a user, not an org — fall back to user repos
2✔
180
                        return client.listRepositoriesByUser(ctx, owner)
2✔
181
                }
2✔
182
                for _, r := range reposPage {
3✔
183
                        repos = append(repos, r.GetName())
2✔
184
                }
2✔
185
                if page >= ghResponse.LastPage || ghResponse.LastPage == 0 {
2✔
186
                        break
1✔
187
                }
188
        }
189
        return repos, nil
1✔
190
}
191

192
func (client *GitHubClient) listRepositoriesByUser(ctx context.Context, user string) ([]string, error) {
2✔
193
        var repos []string
2✔
194
        for page := 1; ; page++ {
4✔
195
                var reposPage []*github.Repository
2✔
196
                var ghResponse *github.Response
2✔
197
                err := client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
198
                        var e error
2✔
199
                        reposPage, ghResponse, e = client.ghClient.Repositories.ListByUser(ctx, user,
2✔
200
                                &github.RepositoryListByUserOptions{ListOptions: github.ListOptions{Page: page}})
2✔
201
                        return ghResponse, e
2✔
202
                })
2✔
203
                if err != nil {
3✔
204
                        return nil, err
1✔
205
                }
1✔
206
                for _, r := range reposPage {
2✔
207
                        repos = append(repos, r.GetName())
1✔
208
                }
1✔
209
                if page >= ghResponse.LastPage || ghResponse.LastPage == 0 {
2✔
210
                        break
1✔
211
                }
212
        }
213
        return repos, nil
1✔
214
}
215

216
// ListBranches on GitHub
217
func (client *GitHubClient) ListBranches(ctx context.Context, owner, repository string) (branchList []string, err error) {
2✔
218
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
219
                var ghResponse *github.Response
2✔
220
                branchList, ghResponse, err = client.executeListBranch(ctx, owner, repository)
2✔
221
                return ghResponse, err
2✔
222
        })
2✔
223
        return
2✔
224
}
225

226
func (client *GitHubClient) executeListBranch(ctx context.Context, owner, repository string) ([]string, *github.Response, error) {
2✔
227
        branches, ghResponse, err := client.ghClient.Repositories.ListBranches(ctx, owner, repository, nil)
2✔
228
        if err != nil {
3✔
229
                return []string{}, ghResponse, err
1✔
230
        }
1✔
231

232
        branchList := make([]string, 0, len(branches))
1✔
233
        for _, branch := range branches {
3✔
234
                branchList = append(branchList, *branch.Name)
2✔
235
        }
2✔
236
        return branchList, ghResponse, nil
1✔
237
}
238

239
// CreateWebhook on GitHub
240
func (client *GitHubClient) CreateWebhook(ctx context.Context, owner, repository, _, payloadURL string,
241
        webhookEvents ...vcsutils.WebhookEvent) (string, string, error) {
2✔
242
        token := vcsutils.CreateToken()
2✔
243
        hook := createGitHubHook(token, payloadURL, webhookEvents...)
2✔
244
        var ghResponseHook *github.Hook
2✔
245
        var err error
2✔
246
        if err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
247
                var ghResponse *github.Response
2✔
248
                ghResponseHook, ghResponse, err = client.ghClient.Repositories.CreateHook(ctx, owner, repository, hook)
2✔
249
                return ghResponse, err
2✔
250
        }); err != nil {
3✔
251
                return "", "", err
1✔
252
        }
1✔
253

254
        return strconv.FormatInt(*ghResponseHook.ID, 10), token, nil
1✔
255
}
256

257
// UpdateWebhook on GitHub
258
func (client *GitHubClient) UpdateWebhook(ctx context.Context, owner, repository, _, payloadURL, token,
259
        webhookID string, webhookEvents ...vcsutils.WebhookEvent) error {
2✔
260
        webhookIDInt64, err := strconv.ParseInt(webhookID, 10, 64)
2✔
261
        if err != nil {
2✔
262
                return err
×
263
        }
×
264

265
        hook := createGitHubHook(token, payloadURL, webhookEvents...)
2✔
266
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
267
                var ghResponse *github.Response
2✔
268
                _, ghResponse, err = client.ghClient.Repositories.EditHook(ctx, owner, repository, webhookIDInt64, hook)
2✔
269
                return ghResponse, err
2✔
270
        })
2✔
271
}
272

273
// DeleteWebhook on GitHub
274
func (client *GitHubClient) DeleteWebhook(ctx context.Context, owner, repository, webhookID string) error {
2✔
275
        webhookIDInt64, err := strconv.ParseInt(webhookID, 10, 64)
2✔
276
        if err != nil {
3✔
277
                return err
1✔
278
        }
1✔
279

280
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
2✔
281
                return client.ghClient.Repositories.DeleteHook(ctx, owner, repository, webhookIDInt64)
1✔
282
        })
1✔
283
}
284

285
// SetCommitStatus on GitHub
286
func (client *GitHubClient) SetCommitStatus(ctx context.Context, commitStatus CommitStatus, owner, repository, ref,
287
        title, description, detailsURL string) error {
2✔
288
        state := getGitHubCommitState(commitStatus)
2✔
289
        status := &github.RepoStatus{
2✔
290
                Context:     &title,
2✔
291
                TargetURL:   &detailsURL,
2✔
292
                State:       &state,
2✔
293
                Description: &description,
2✔
294
        }
2✔
295

2✔
296
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
297
                _, ghResponse, err := client.ghClient.Repositories.CreateStatus(ctx, owner, repository, ref, status)
2✔
298
                return ghResponse, err
2✔
299
        })
2✔
300
}
301

302
// GetCommitStatuses on GitHub
303
func (client *GitHubClient) GetCommitStatuses(ctx context.Context, owner, repository, ref string) (statusInfoList []CommitStatusInfo, err error) {
6✔
304
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
12✔
305
                var ghResponse *github.Response
6✔
306
                statusInfoList, ghResponse, err = client.executeGetCommitStatuses(ctx, owner, repository, ref)
6✔
307
                return ghResponse, err
6✔
308
        })
6✔
309
        return
6✔
310
}
311

312
func (client *GitHubClient) executeGetCommitStatuses(ctx context.Context, owner, repository, ref string) (statusInfoList []CommitStatusInfo, ghResponse *github.Response, err error) {
6✔
313
        statuses, ghResponse, err := client.ghClient.Repositories.GetCombinedStatus(ctx, owner, repository, ref, nil)
6✔
314
        if err != nil {
10✔
315
                return
4✔
316
        }
4✔
317

318
        for _, singleStatus := range statuses.Statuses {
6✔
319
                statusInfoList = append(statusInfoList, CommitStatusInfo{
4✔
320
                        State:         commitStatusAsStringToStatus(*singleStatus.State),
4✔
321
                        Description:   singleStatus.GetDescription(),
4✔
322
                        DetailsUrl:    singleStatus.GetTargetURL(),
4✔
323
                        Creator:       singleStatus.GetCreator().GetName(),
4✔
324
                        LastUpdatedAt: singleStatus.GetUpdatedAt().Time,
4✔
325
                        CreatedAt:     singleStatus.GetCreatedAt().Time,
4✔
326
                })
4✔
327
        }
4✔
328
        return
2✔
329
}
330

331
// DownloadRepository on GitHub
332
func (client *GitHubClient) DownloadRepository(ctx context.Context, owner, repository, branch, localPath string) (err error) {
2✔
333
        // Get the archive download link from GitHub
2✔
334
        var baseURL *url.URL
2✔
335
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
336
                var ghResponse *github.Response
2✔
337
                baseURL, ghResponse, err = client.executeGetArchiveLink(ctx, owner, repository, branch)
2✔
338
                return ghResponse, err
2✔
339
        })
2✔
340
        if err != nil {
3✔
341
                return
1✔
342
        }
1✔
343

344
        // Download the archive
345
        httpResponse, err := executeDownloadArchiveFromLink(baseURL.String())
1✔
346
        if err != nil {
1✔
347
                return
×
348
        }
×
349
        defer func() { err = errors.Join(err, httpResponse.Body.Close()) }()
2✔
350
        client.logger.Info(repository, vcsutils.SuccessfulRepoDownload)
1✔
351

1✔
352
        // Untar the archive
1✔
353
        if err = vcsutils.Untar(localPath, httpResponse.Body, true); err != nil {
1✔
354
                return
×
355
        }
×
356
        client.logger.Info(vcsutils.SuccessfulRepoExtraction)
1✔
357

1✔
358
        repositoryInfo, err := client.GetRepositoryInfo(ctx, owner, repository)
1✔
359
        if err != nil {
1✔
360
                return
×
361
        }
×
362
        // Create a .git folder in the archive with the remote repository HTTP clone url
363
        err = vcsutils.CreateDotGitFolderWithRemote(localPath, vcsutils.RemoteName, repositoryInfo.CloneInfo.HTTP)
1✔
364
        return
1✔
365
}
366

367
func (client *GitHubClient) executeGetArchiveLink(ctx context.Context, owner, repository, branch string) (baseURL *url.URL, ghResponse *github.Response, err error) {
2✔
368
        client.logger.Debug("Getting GitHub archive link to download")
2✔
369
        return client.ghClient.Repositories.GetArchiveLink(ctx, owner, repository, github.Tarball,
2✔
370
                &github.RepositoryContentGetOptions{Ref: branch}, 5)
2✔
371
}
2✔
372

373
func executeDownloadArchiveFromLink(baseURL string) (*http.Response, error) {
1✔
374
        httpClient := &http.Client{}
1✔
375
        req, err := http.NewRequest(http.MethodGet, baseURL, nil)
1✔
376
        if err != nil {
1✔
377
                return nil, err
×
378
        }
×
379
        httpResponse, err := httpClient.Do(req)
1✔
380
        if err != nil {
1✔
381
                return httpResponse, err
×
382
        }
×
383
        return httpResponse, vcsutils.CheckResponseStatusWithBody(httpResponse, http.StatusOK)
1✔
384
}
385

386
func (client *GitHubClient) GetPullRequestCommentSizeLimit() int {
×
387
        return githubPrContentSizeLimit
×
388
}
×
389

390
func (client *GitHubClient) GetPullRequestDetailsSizeLimit() int {
×
391
        return githubPrContentSizeLimit
×
392
}
×
393

394
// CreatePullRequest on GitHub
395
func (client *GitHubClient) CreatePullRequest(ctx context.Context, owner, repository, sourceBranch, targetBranch, title, description string) error {
2✔
396
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
397
                _, githubResponse, err := client.executeCreatePullRequest(ctx, owner, repository, sourceBranch, targetBranch, title, description)
2✔
398
                return githubResponse, err
2✔
399
        })
2✔
400
}
401

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

2✔
405
        err := client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
406
                pr, ghResponse, err := client.executeCreatePullRequest(ctx, owner, repository, sourceBranch, targetBranch, title, description)
2✔
407
                if err != nil {
3✔
408
                        return ghResponse, err
1✔
409
                }
1✔
410
                prInfo = mapToPullRequestInfo(pr)
1✔
411
                return ghResponse, nil
1✔
412
        })
413

414
        return prInfo, err
2✔
415
}
416

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

4✔
421
        pr, ghResponse, err := client.ghClient.PullRequests.Create(ctx, owner, repository, &github.NewPullRequest{
4✔
422
                Title: &title,
4✔
423
                Body:  &description,
4✔
424
                Head:  &head,
4✔
425
                Base:  &targetBranch,
4✔
426
        })
4✔
427
        return pr, ghResponse, err
4✔
428
}
4✔
429

430
func mapToPullRequestInfo(pr *github.PullRequest) CreatedPullRequestInfo {
1✔
431
        return CreatedPullRequestInfo{
1✔
432
                Number:      pr.GetNumber(),
1✔
433
                URL:         pr.GetHTMLURL(),
1✔
434
                StatusesUrl: pr.GetStatusesURL(),
1✔
435
        }
1✔
436
}
1✔
437

438
// UpdatePullRequest on GitHub
439
func (client *GitHubClient) UpdatePullRequest(ctx context.Context, owner, repository, title, body, targetBranchName string, id int, state vcsutils.PullRequestState) error {
3✔
440
        client.logger.Debug(vcsutils.UpdatingPullRequest, id)
3✔
441
        var baseRef *github.PullRequestBranch
3✔
442
        if targetBranchName != "" {
5✔
443
                baseRef = &github.PullRequestBranch{Ref: &targetBranchName}
2✔
444
        }
2✔
445
        pullRequest := &github.PullRequest{
3✔
446
                Body:  &body,
3✔
447
                Title: &title,
3✔
448
                State: vcsutils.MapPullRequestState(&state),
3✔
449
                Base:  baseRef,
3✔
450
        }
3✔
451

3✔
452
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
453
                _, ghResponse, err := client.ghClient.PullRequests.Edit(ctx, owner, repository, id, pullRequest)
3✔
454
                return ghResponse, err
3✔
455
        })
3✔
456
}
457

458
// ListOpenPullRequestsWithBody on GitHub
459
func (client *GitHubClient) ListOpenPullRequestsWithBody(ctx context.Context, owner, repository string) ([]PullRequestInfo, error) {
1✔
460
        return client.getOpenPullRequests(ctx, owner, repository, true)
1✔
461
}
1✔
462

463
// ListOpenPullRequests on GitHub
464
func (client *GitHubClient) ListOpenPullRequests(ctx context.Context, owner, repository string) ([]PullRequestInfo, error) {
1✔
465
        return client.getOpenPullRequests(ctx, owner, repository, false)
1✔
466
}
1✔
467

468
func (client *GitHubClient) getOpenPullRequests(ctx context.Context, owner, repository string, withBody bool) ([]PullRequestInfo, error) {
2✔
469
        var pullRequests []*github.PullRequest
2✔
470
        client.logger.Debug(vcsutils.FetchingOpenPullRequests, repository)
2✔
471
        err := client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
472
                var ghResponse *github.Response
2✔
473
                var err error
2✔
474
                pullRequests, ghResponse, err = client.ghClient.PullRequests.List(ctx, owner, repository, &github.PullRequestListOptions{State: "open"})
2✔
475
                return ghResponse, err
2✔
476
        })
2✔
477
        if err != nil {
2✔
478
                return []PullRequestInfo{}, err
×
479
        }
×
480

481
        return mapGitHubPullRequestToPullRequestInfoList(pullRequests, withBody)
2✔
482
}
483

484
func (client *GitHubClient) GetPullRequestByID(ctx context.Context, owner, repository string, pullRequestId int) (PullRequestInfo, error) {
4✔
485
        var pullRequest *github.PullRequest
4✔
486
        var ghResponse *github.Response
4✔
487
        var err error
4✔
488
        client.logger.Debug(vcsutils.FetchingPullRequestById, repository)
4✔
489
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
8✔
490
                pullRequest, ghResponse, err = client.ghClient.PullRequests.Get(ctx, owner, repository, pullRequestId)
4✔
491
                return ghResponse, err
4✔
492
        })
4✔
493
        if err != nil {
7✔
494
                return PullRequestInfo{}, err
3✔
495
        }
3✔
496

497
        if err = vcsutils.CheckResponseStatusWithBody(ghResponse.Response, http.StatusOK); err != nil {
1✔
498
                return PullRequestInfo{}, err
×
499
        }
×
500

501
        return mapGitHubPullRequestToPullRequestInfo(pullRequest, false)
1✔
502
}
503

504
func mapGitHubPullRequestToPullRequestInfo(ghPullRequest *github.PullRequest, withBody bool) (PullRequestInfo, error) {
4✔
505
        var sourceBranch, targetBranch string
4✔
506
        var err1, err2 error
4✔
507
        if ghPullRequest != nil && ghPullRequest.Head != nil && ghPullRequest.Base != nil {
8✔
508
                sourceBranch, err1 = extractBranchFromLabel(vcsutils.DefaultIfNotNil(ghPullRequest.Head.Label))
4✔
509
                targetBranch, err2 = extractBranchFromLabel(vcsutils.DefaultIfNotNil(ghPullRequest.Base.Label))
4✔
510
                err := errors.Join(err1, err2)
4✔
511
                if err != nil {
4✔
512
                        return PullRequestInfo{}, err
×
513
                }
×
514
        }
515

516
        var sourceRepoName, sourceRepoOwner string
4✔
517
        if ghPullRequest.Head.Repo == nil {
4✔
518
                return PullRequestInfo{}, errors.New("the source repository information is missing when fetching the pull request details")
×
519
        }
×
520
        if ghPullRequest.Head.Repo.Owner == nil {
4✔
521
                return PullRequestInfo{}, errors.New("the source repository owner name is missing when fetching the pull request details")
×
522
        }
×
523
        sourceRepoName = vcsutils.DefaultIfNotNil(ghPullRequest.Head.Repo.Name)
4✔
524
        sourceRepoOwner = vcsutils.DefaultIfNotNil(ghPullRequest.Head.Repo.Owner.Login)
4✔
525

4✔
526
        var targetRepoName, targetRepoOwner string
4✔
527
        if ghPullRequest.Base.Repo == nil {
4✔
528
                return PullRequestInfo{}, errors.New("the target repository information is missing when fetching the pull request details")
×
529
        }
×
530
        if ghPullRequest.Base.Repo.Owner == nil {
4✔
531
                return PullRequestInfo{}, errors.New("the target repository owner name is missing when fetching the pull request details")
×
532
        }
×
533
        targetRepoName = vcsutils.DefaultIfNotNil(ghPullRequest.Base.Repo.Name)
4✔
534
        targetRepoOwner = vcsutils.DefaultIfNotNil(ghPullRequest.Base.Repo.Owner.Login)
4✔
535

4✔
536
        var body string
4✔
537
        if withBody {
5✔
538
                body = vcsutils.DefaultIfNotNil(ghPullRequest.Body)
1✔
539
        }
1✔
540

541
        return PullRequestInfo{
4✔
542
                ID:     int64(vcsutils.DefaultIfNotNil(ghPullRequest.Number)),
4✔
543
                Title:  vcsutils.DefaultIfNotNil(ghPullRequest.Title),
4✔
544
                URL:    vcsutils.DefaultIfNotNil(ghPullRequest.HTMLURL),
4✔
545
                Body:   body,
4✔
546
                Author: vcsutils.DefaultIfNotNil(ghPullRequest.User.Login),
4✔
547
                Source: BranchInfo{
4✔
548
                        Name:       sourceBranch,
4✔
549
                        Repository: sourceRepoName,
4✔
550
                        Owner:      sourceRepoOwner,
4✔
551
                },
4✔
552
                Target: BranchInfo{
4✔
553
                        Name:       targetBranch,
4✔
554
                        Repository: targetRepoName,
4✔
555
                        Owner:      targetRepoOwner,
4✔
556
                },
4✔
557
                Status: vcsutils.DefaultIfNotNil(ghPullRequest.State),
4✔
558
        }, nil
4✔
559
}
560

561
// Extracts branch name from the following expected label format repo:branch
562
func extractBranchFromLabel(label string) (string, error) {
8✔
563
        split := strings.Split(label, ":")
8✔
564
        if len(split) <= 1 {
8✔
565
                return "", fmt.Errorf("bad label format %s", label)
×
566
        }
×
567
        return split[1], nil
8✔
568
}
569

570
// AddPullRequestComment on GitHub
571
func (client *GitHubClient) AddPullRequestComment(ctx context.Context, owner, repository, content string, pullRequestID int) error {
6✔
572
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "content": content})
6✔
573
        if err != nil {
10✔
574
                return err
4✔
575
        }
4✔
576

577
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
578
                var ghResponse *github.Response
2✔
579
                // We use the Issues API to add a regular comment. The PullRequests API adds a code review comment.
2✔
580
                _, ghResponse, err = client.ghClient.Issues.CreateComment(ctx, owner, repository, pullRequestID, &github.IssueComment{Body: &content})
2✔
581
                return ghResponse, err
2✔
582
        })
2✔
583
}
584

585
// AddPullRequestReviewComments on GitHub
586
func (client *GitHubClient) AddPullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int, comments ...PullRequestComment) error {
2✔
587
        prID := strconv.Itoa(pullRequestID)
2✔
588
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "pullRequestID": prID})
2✔
589
        if err != nil {
2✔
590
                return err
×
591
        }
×
592
        if len(comments) == 0 {
2✔
593
                return errors.New(vcsutils.ErrNoCommentsProvided)
×
594
        }
×
595

596
        var commits []*github.RepositoryCommit
2✔
597
        var ghResponse *github.Response
2✔
598
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
599
                commits, ghResponse, err = client.ghClient.PullRequests.ListCommits(ctx, owner, repository, pullRequestID, nil)
2✔
600
                return ghResponse, err
2✔
601
        })
2✔
602
        if err != nil {
3✔
603
                return err
1✔
604
        }
1✔
605
        if len(commits) == 0 {
1✔
606
                return errors.New("could not fetch the commits list for pull request " + prID)
×
607
        }
×
608

609
        latestCommitSHA := commits[len(commits)-1].GetSHA()
1✔
610

1✔
611
        for _, comment := range comments {
3✔
612
                err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
613
                        ghResponse, err = client.executeCreatePullRequestReviewComment(ctx, owner, repository, latestCommitSHA, pullRequestID, comment)
2✔
614
                        return ghResponse, err
2✔
615
                })
2✔
616
                if err != nil {
2✔
617
                        return err
×
618
                }
×
619
        }
620
        return nil
1✔
621
}
622

623
func (client *GitHubClient) executeCreatePullRequestReviewComment(ctx context.Context, owner, repository, latestCommitSHA string, pullRequestID int, comment PullRequestComment) (*github.Response, error) {
2✔
624
        filePath := filepath.Clean(comment.NewFilePath)
2✔
625
        startLine := &comment.NewStartLine
2✔
626
        // GitHub API won't accept 'start_line' if it equals the end line
2✔
627
        if *startLine == comment.NewEndLine {
2✔
628
                startLine = nil
×
629
        }
×
630
        _, ghResponse, err := client.ghClient.PullRequests.CreateComment(ctx, owner, repository, pullRequestID, &github.PullRequestComment{
2✔
631
                CommitID:  &latestCommitSHA,
2✔
632
                Body:      &comment.Content,
2✔
633
                StartLine: startLine,
2✔
634
                Line:      &comment.NewEndLine,
2✔
635
                Path:      &filePath,
2✔
636
        })
2✔
637
        if err != nil {
2✔
638
                err = fmt.Errorf("could not create a code review comment for <%s/%s> in pull request %d. error received: %w",
×
639
                        owner, repository, pullRequestID, err)
×
640
        }
×
641
        return ghResponse, err
2✔
642
}
643

644
// ListPullRequestReviewComments on GitHub
645
func (client *GitHubClient) ListPullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int) ([]CommentInfo, error) {
2✔
646
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
2✔
647
        if err != nil {
2✔
648
                return nil, err
×
649
        }
×
650

651
        commentsInfoList := []CommentInfo{}
2✔
652
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
653
                var ghResponse *github.Response
2✔
654
                commentsInfoList, ghResponse, err = client.executeListPullRequestReviewComments(ctx, owner, repository, pullRequestID)
2✔
655
                return ghResponse, err
2✔
656
        })
2✔
657
        return commentsInfoList, err
2✔
658
}
659

660
// ListPullRequestReviews on GitHub
661
func (client *GitHubClient) ListPullRequestReviews(ctx context.Context, owner, repository string, pullRequestID int) ([]PullRequestReviewDetails, error) {
2✔
662
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
2✔
663
        if err != nil {
2✔
664
                return nil, err
×
665
        }
×
666

667
        var reviews []*github.PullRequestReview
2✔
668
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
669
                var ghResponse *github.Response
2✔
670
                reviews, ghResponse, err = client.ghClient.PullRequests.ListReviews(ctx, owner, repository, pullRequestID, nil)
2✔
671
                return ghResponse, err
2✔
672
        })
2✔
673
        if err != nil {
3✔
674
                return nil, err
1✔
675
        }
1✔
676

677
        var reviewInfos []PullRequestReviewDetails
1✔
678
        for _, review := range reviews {
2✔
679
                reviewInfos = append(reviewInfos, PullRequestReviewDetails{
1✔
680
                        ID:          review.GetID(),
1✔
681
                        Reviewer:    review.GetUser().GetLogin(),
1✔
682
                        Body:        review.GetBody(),
1✔
683
                        State:       review.GetState(),
1✔
684
                        SubmittedAt: review.GetSubmittedAt().String(),
1✔
685
                        CommitID:    review.GetCommitID(),
1✔
686
                })
1✔
687
        }
1✔
688

689
        return reviewInfos, nil
1✔
690
}
691

692
func (client *GitHubClient) executeListPullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int) ([]CommentInfo, *github.Response, error) {
2✔
693
        commentsList, ghResponse, err := client.ghClient.PullRequests.ListComments(ctx, owner, repository, pullRequestID, nil)
2✔
694
        if err != nil {
3✔
695
                return []CommentInfo{}, ghResponse, err
1✔
696
        }
1✔
697
        commentsInfoList := []CommentInfo{}
1✔
698
        for _, comment := range commentsList {
2✔
699
                commentsInfoList = append(commentsInfoList, CommentInfo{
1✔
700
                        ID:      comment.GetID(),
1✔
701
                        Content: comment.GetBody(),
1✔
702
                        Created: comment.GetCreatedAt().Time,
1✔
703
                })
1✔
704
        }
1✔
705
        return commentsInfoList, ghResponse, nil
1✔
706
}
707

708
// ListPullRequestComments on GitHub
709
func (client *GitHubClient) ListPullRequestComments(ctx context.Context, owner, repository string, pullRequestID int) ([]CommentInfo, error) {
4✔
710
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
4✔
711
        if err != nil {
4✔
712
                return []CommentInfo{}, err
×
713
        }
×
714

715
        var commentsList []*github.IssueComment
4✔
716
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
8✔
717
                var ghResponse *github.Response
4✔
718
                commentsList, ghResponse, err = client.ghClient.Issues.ListComments(ctx, owner, repository, pullRequestID, &github.IssueListCommentsOptions{})
4✔
719
                return ghResponse, err
4✔
720
        })
4✔
721

722
        if err != nil {
7✔
723
                return []CommentInfo{}, err
3✔
724
        }
3✔
725

726
        return mapGitHubIssuesCommentToCommentInfoList(commentsList)
1✔
727
}
728

729
// DeletePullRequestReviewComments on GitHub
730
func (client *GitHubClient) DeletePullRequestReviewComments(ctx context.Context, owner, repository string, _ int, comments ...CommentInfo) error {
2✔
731
        for _, comment := range comments {
5✔
732
                commentID := comment.ID
3✔
733
                err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "commentID": strconv.FormatInt(commentID, 10)})
3✔
734
                if err != nil {
3✔
735
                        return err
×
736
                }
×
737

738
                err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
739
                        return client.executeDeletePullRequestReviewComment(ctx, owner, repository, commentID)
3✔
740
                })
3✔
741
                if err != nil {
4✔
742
                        return err
1✔
743
                }
1✔
744

745
        }
746
        return nil
1✔
747
}
748

749
func (client *GitHubClient) executeDeletePullRequestReviewComment(ctx context.Context, owner, repository string, commentID int64) (*github.Response, error) {
3✔
750
        ghResponse, err := client.ghClient.PullRequests.DeleteComment(ctx, owner, repository, commentID)
3✔
751
        if err != nil {
4✔
752
                err = fmt.Errorf("could not delete pull request review comment: %w", err)
1✔
753
        }
1✔
754
        return ghResponse, err
3✔
755
}
756

757
// DeletePullRequestComment on GitHub
758
func (client *GitHubClient) DeletePullRequestComment(ctx context.Context, owner, repository string, _, commentID int) error {
2✔
759
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
2✔
760
        if err != nil {
2✔
761
                return err
×
762
        }
×
763
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
764
                return client.executeDeletePullRequestComment(ctx, owner, repository, commentID)
2✔
765
        })
2✔
766
}
767

768
func (client *GitHubClient) executeDeletePullRequestComment(ctx context.Context, owner, repository string, commentID int) (*github.Response, error) {
2✔
769
        ghResponse, err := client.ghClient.Issues.DeleteComment(ctx, owner, repository, int64(commentID))
2✔
770
        if err != nil {
3✔
771
                return ghResponse, err
1✔
772
        }
1✔
773

774
        var statusCode int
1✔
775
        if ghResponse.Response != nil {
2✔
776
                statusCode = ghResponse.StatusCode
1✔
777
        }
1✔
778
        if statusCode != http.StatusNoContent && statusCode != http.StatusOK {
1✔
779
                return ghResponse, fmt.Errorf("expected %d status code while received %d status code", http.StatusNoContent, ghResponse.StatusCode)
×
780
        }
×
781

782
        return ghResponse, nil
1✔
783
}
784

785
// GetLatestCommit on GitHub
786
func (client *GitHubClient) GetLatestCommit(ctx context.Context, owner, repository, branch string) (CommitInfo, error) {
10✔
787
        commits, err := client.GetCommits(ctx, owner, repository, branch)
10✔
788
        if err != nil {
18✔
789
                return CommitInfo{}, err
8✔
790
        }
8✔
791
        latestCommit := CommitInfo{}
2✔
792
        if len(commits) > 0 {
4✔
793
                latestCommit = commits[0]
2✔
794
        }
2✔
795
        return latestCommit, nil
2✔
796
}
797

798
// GetCommits on GitHub
799
func (client *GitHubClient) GetCommits(ctx context.Context, owner, repository, branch string) ([]CommitInfo, error) {
12✔
800
        err := validateParametersNotBlank(map[string]string{
12✔
801
                "owner":      owner,
12✔
802
                "repository": repository,
12✔
803
                "branch":     branch,
12✔
804
        })
12✔
805
        if err != nil {
16✔
806
                return nil, err
4✔
807
        }
4✔
808

809
        var commitsInfo []CommitInfo
8✔
810
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
16✔
811
                var ghResponse *github.Response
8✔
812
                listOptions := &github.CommitsListOptions{
8✔
813
                        SHA: branch,
8✔
814
                        ListOptions: github.ListOptions{
8✔
815
                                Page:    1,
8✔
816
                                PerPage: vcsutils.NumberOfCommitsToFetch,
8✔
817
                        },
8✔
818
                }
8✔
819
                commitsInfo, ghResponse, err = client.executeGetCommits(ctx, owner, repository, listOptions)
8✔
820
                return ghResponse, err
8✔
821
        })
8✔
822
        return commitsInfo, err
8✔
823
}
824

825
// GetCommitsWithQueryOptions on GitHub
826
func (client *GitHubClient) GetCommitsWithQueryOptions(ctx context.Context, owner, repository string, listOptions GitCommitsQueryOptions) ([]CommitInfo, error) {
2✔
827
        err := validateParametersNotBlank(map[string]string{
2✔
828
                "owner":      owner,
2✔
829
                "repository": repository,
2✔
830
        })
2✔
831
        if err != nil {
2✔
832
                return nil, err
×
833
        }
×
834
        var commitsInfo []CommitInfo
2✔
835
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
836
                var ghResponse *github.Response
2✔
837
                commitsInfo, ghResponse, err = client.executeGetCommits(ctx, owner, repository, convertToGitHubCommitsListOptions(listOptions))
2✔
838
                return ghResponse, err
2✔
839
        })
2✔
840
        return commitsInfo, err
2✔
841
}
842

843
func convertToGitHubCommitsListOptions(listOptions GitCommitsQueryOptions) *github.CommitsListOptions {
2✔
844
        return &github.CommitsListOptions{
2✔
845
                Since: listOptions.Since,
2✔
846
                Until: time.Now(),
2✔
847
                ListOptions: github.ListOptions{
2✔
848
                        Page:    listOptions.Page,
2✔
849
                        PerPage: listOptions.PerPage,
2✔
850
                },
2✔
851
        }
2✔
852
}
2✔
853

854
func (client *GitHubClient) executeGetCommits(ctx context.Context, owner, repository string, listOptions *github.CommitsListOptions) ([]CommitInfo, *github.Response, error) {
10✔
855
        commits, ghResponse, err := client.ghClient.Repositories.ListCommits(ctx, owner, repository, listOptions)
10✔
856
        if err != nil {
16✔
857
                return nil, ghResponse, err
6✔
858
        }
6✔
859

860
        var commitsInfo []CommitInfo
4✔
861
        for _, commit := range commits {
11✔
862
                commitInfo := mapGitHubCommitToCommitInfo(commit)
7✔
863
                commitsInfo = append(commitsInfo, commitInfo)
7✔
864
        }
7✔
865
        return commitsInfo, ghResponse, nil
4✔
866
}
867

868
// GetRepositoryInfo on GitHub
869
func (client *GitHubClient) GetRepositoryInfo(ctx context.Context, owner, repository string) (RepositoryInfo, error) {
6✔
870
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
6✔
871
        if err != nil {
9✔
872
                return RepositoryInfo{}, err
3✔
873
        }
3✔
874

875
        var repo *github.Repository
3✔
876
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
877
                var ghResponse *github.Response
3✔
878
                repo, ghResponse, err = client.ghClient.Repositories.Get(ctx, owner, repository)
3✔
879
                return ghResponse, err
3✔
880
        })
3✔
881
        if err != nil {
4✔
882
                return RepositoryInfo{}, err
1✔
883
        }
1✔
884

885
        return RepositoryInfo{RepositoryVisibility: getGitHubRepositoryVisibility(repo), CloneInfo: CloneInfo{HTTP: repo.GetCloneURL(), SSH: repo.GetSSHURL()}}, nil
2✔
886
}
887

888
// GetCommitBySha on GitHub
889
func (client *GitHubClient) GetCommitBySha(ctx context.Context, owner, repository, sha string) (CommitInfo, error) {
7✔
890
        err := validateParametersNotBlank(map[string]string{
7✔
891
                "owner":      owner,
7✔
892
                "repository": repository,
7✔
893
                "sha":        sha,
7✔
894
        })
7✔
895
        if err != nil {
11✔
896
                return CommitInfo{}, err
4✔
897
        }
4✔
898

899
        var commit *github.RepositoryCommit
3✔
900
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
901
                var ghResponse *github.Response
3✔
902
                commit, ghResponse, err = client.ghClient.Repositories.GetCommit(ctx, owner, repository, sha, nil)
3✔
903
                return ghResponse, err
3✔
904
        })
3✔
905
        if err != nil {
5✔
906
                return CommitInfo{}, err
2✔
907
        }
2✔
908

909
        return mapGitHubCommitToCommitInfo(commit), nil
1✔
910
}
911

912
// CreateLabel on GitHub
913
func (client *GitHubClient) CreateLabel(ctx context.Context, owner, repository string, labelInfo LabelInfo) error {
6✔
914
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "LabelInfo.name": labelInfo.Name})
6✔
915
        if err != nil {
10✔
916
                return err
4✔
917
        }
4✔
918

919
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
920
                var ghResponse *github.Response
2✔
921
                _, ghResponse, err = client.ghClient.Issues.CreateLabel(ctx, owner, repository, &github.Label{
2✔
922
                        Name:        &labelInfo.Name,
2✔
923
                        Description: &labelInfo.Description,
2✔
924
                        Color:       &labelInfo.Color,
2✔
925
                })
2✔
926
                return ghResponse, err
2✔
927
        })
2✔
928
}
929

930
// GetLabel on GitHub
931
func (client *GitHubClient) GetLabel(ctx context.Context, owner, repository, name string) (*LabelInfo, error) {
7✔
932
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "name": name})
7✔
933
        if err != nil {
11✔
934
                return nil, err
4✔
935
        }
4✔
936

937
        var labelInfo *LabelInfo
3✔
938
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
939
                var ghResponse *github.Response
3✔
940
                labelInfo, ghResponse, err = client.executeGetLabel(ctx, owner, repository, name)
3✔
941
                return ghResponse, err
3✔
942
        })
3✔
943
        return labelInfo, err
3✔
944
}
945

946
func (client *GitHubClient) executeGetLabel(ctx context.Context, owner, repository, name string) (*LabelInfo, *github.Response, error) {
3✔
947
        label, ghResponse, err := client.ghClient.Issues.GetLabel(ctx, owner, repository, name)
3✔
948
        if err != nil {
5✔
949
                if ghResponse != nil && ghResponse.Response != nil && ghResponse.StatusCode == http.StatusNotFound {
3✔
950
                        return nil, ghResponse, nil
1✔
951
                }
1✔
952
                return nil, ghResponse, err
1✔
953
        }
954

955
        labelInfo := &LabelInfo{
1✔
956
                Name:        *label.Name,
1✔
957
                Description: *label.Description,
1✔
958
                Color:       *label.Color,
1✔
959
        }
1✔
960
        return labelInfo, ghResponse, nil
1✔
961
}
962

963
// ListPullRequestLabels on GitHub
964
func (client *GitHubClient) ListPullRequestLabels(ctx context.Context, owner, repository string, pullRequestID int) ([]string, error) {
5✔
965
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
5✔
966
        if err != nil {
8✔
967
                return nil, err
3✔
968
        }
3✔
969

970
        results := []string{}
2✔
971
        for nextPage := 0; ; nextPage++ {
4✔
972
                options := &github.ListOptions{Page: nextPage}
2✔
973
                var labels []*github.Label
2✔
974
                var ghResponse *github.Response
2✔
975
                err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
976
                        labels, ghResponse, err = client.ghClient.Issues.ListLabelsByIssue(ctx, owner, repository, pullRequestID, options)
2✔
977
                        return ghResponse, err
2✔
978
                })
2✔
979
                if err != nil {
3✔
980
                        return nil, err
1✔
981
                }
1✔
982
                for _, label := range labels {
2✔
983
                        results = append(results, *label.Name)
1✔
984
                }
1✔
985
                if nextPage+1 >= ghResponse.LastPage {
2✔
986
                        break
1✔
987
                }
988
        }
989
        return results, nil
1✔
990
}
991

992
func (client *GitHubClient) ListPullRequestsAssociatedWithCommit(ctx context.Context, owner, repository string, commitSHA string) ([]PullRequestInfo, error) {
2✔
993
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
2✔
994
        if err != nil {
2✔
995
                return nil, err
×
996
        }
×
997

998
        var pulls []*github.PullRequest
2✔
999
        if err = client.runWithRateLimitRetries(func() (ghResponse *github.Response, err error) {
4✔
1000
                pulls, ghResponse, err = client.ghClient.PullRequests.ListPullRequestsWithCommit(ctx, owner, repository, commitSHA, nil)
2✔
1001
                return ghResponse, err
2✔
1002
        }); err != nil {
3✔
1003
                return nil, err
1✔
1004
        }
1✔
1005
        return mapGitHubPullRequestToPullRequestInfoList(pulls, false)
1✔
1006
}
1007

1008
// UnlabelPullRequest on GitHub
1009
func (client *GitHubClient) UnlabelPullRequest(ctx context.Context, owner, repository, name string, pullRequestID int) error {
5✔
1010
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
5✔
1011
        if err != nil {
8✔
1012
                return err
3✔
1013
        }
3✔
1014

1015
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1016
                return client.ghClient.Issues.RemoveLabelForIssue(ctx, owner, repository, pullRequestID, name)
2✔
1017
        })
2✔
1018
}
1019

1020
// UploadCodeScanning to GitHub Security tab
1021
func (client *GitHubClient) UploadCodeScanning(ctx context.Context, owner, repository, branch, sarifContent string) (id string, err error) {
2✔
1022
        commit, err := client.GetLatestCommit(ctx, owner, repository, branch)
2✔
1023
        if err != nil {
3✔
1024
                return
1✔
1025
        }
1✔
1026

1027
        commitSHA := commit.Hash
1✔
1028
        branch = vcsutils.AddBranchPrefix(branch)
1✔
1029
        client.logger.Debug(vcsutils.UploadingCodeScanning, repository, "/", branch)
1✔
1030

1✔
1031
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
2✔
1032
                var ghResponse *github.Response
1✔
1033
                id, ghResponse, err = client.executeUploadCodeScanning(ctx, owner, repository, branch, commitSHA, sarifContent)
1✔
1034
                return ghResponse, err
1✔
1035
        })
1✔
1036
        return
1✔
1037
}
1038

1039
func (client *GitHubClient) UploadCodeScanningWithRef(ctx context.Context, owner, repository, ref, commitSHA, sarifContent string) (id string, err error) {
2✔
1040
        client.logger.Debug(vcsutils.UploadingCodeScanning, repository, "/", ref)
2✔
1041

2✔
1042
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1043
                var ghResponse *github.Response
2✔
1044
                id, ghResponse, err = client.executeUploadCodeScanning(ctx, owner, repository, ref, commitSHA, sarifContent)
2✔
1045
                return ghResponse, err
2✔
1046
        })
2✔
1047
        return
2✔
1048
}
1049

1050
func (client *GitHubClient) executeUploadCodeScanning(ctx context.Context, owner, repository, ref, commitSHA, sarifContent string) (id string, ghResponse *github.Response, err error) {
3✔
1051
        encodedSarif, err := encodeScanningResult(sarifContent)
3✔
1052
        if err != nil {
3✔
1053
                return
×
1054
        }
×
1055

1056
        sarifID, ghResponse, err := client.ghClient.CodeScanning.UploadSarif(ctx, owner, repository, &github.SarifAnalysis{
3✔
1057
                CommitSHA: &commitSHA,
3✔
1058
                Ref:       &ref,
3✔
1059
                Sarif:     &encodedSarif,
3✔
1060
        })
3✔
1061

3✔
1062
        // According to go-github API - successful ghResponse will return 202 status code
3✔
1063
        // The body of the ghResponse will appear in the error, and the Sarif struct will be empty.
3✔
1064
        if err != nil && (ghResponse == nil || ghResponse.Response == nil || ghResponse.StatusCode != http.StatusAccepted) {
4✔
1065
                return
1✔
1066
        }
1✔
1067

1068
        id = extractIdFronSarifIDIfExists(sarifID)
2✔
1069
        return
2✔
1070
}
1071

1072
func extractIdFronSarifIDIfExists(sarifID *github.SarifID) string {
2✔
1073
        if sarifID != nil && *sarifID.ID != "" {
2✔
1074
                return *sarifID.ID
×
1075
        }
×
1076
        return ""
2✔
1077
}
1078

1079
// DownloadFileFromRepo on GitHub
1080
func (client *GitHubClient) DownloadFileFromRepo(ctx context.Context, owner, repository, branch, path string) (content []byte, statusCode int, err error) {
3✔
1081
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
6✔
1082
                var ghResponse *github.Response
3✔
1083
                content, statusCode, ghResponse, err = client.executeDownloadFileFromRepo(ctx, owner, repository, branch, path)
3✔
1084
                return ghResponse, err
3✔
1085
        })
3✔
1086

1087
        return
3✔
1088
}
1089

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

1096
        statusCode = ghResponse.StatusCode
2✔
1097
        if err != nil {
3✔
1098
                if statusCode != http.StatusOK {
2✔
1099
                        err = fmt.Errorf("expected %d status code while received %d status code with error:\n%s", http.StatusOK, ghResponse.StatusCode, err)
1✔
1100
                }
1✔
1101
                return
1✔
1102
        }
1103

1104
        if fileContent != nil {
2✔
1105
                var contentStr string
1✔
1106
                contentStr, err = fileContent.GetContent()
1✔
1107
                if err != nil {
1✔
1108
                        return
×
1109
                }
×
1110
                content = []byte(contentStr)
1✔
1111
        }
1112
        return
1✔
1113
}
1114

1115
// GetRepositoryEnvironmentInfo on GitHub
1116
func (client *GitHubClient) GetRepositoryEnvironmentInfo(ctx context.Context, owner, repository, name string) (RepositoryEnvironmentInfo, error) {
2✔
1117
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "name": name})
2✔
1118
        if err != nil {
2✔
1119
                return RepositoryEnvironmentInfo{}, err
×
1120
        }
×
1121

1122
        var repositoryEnvInfo *RepositoryEnvironmentInfo
2✔
1123
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1124
                var ghResponse *github.Response
2✔
1125
                repositoryEnvInfo, ghResponse, err = client.executeGetRepositoryEnvironmentInfo(ctx, owner, repository, name)
2✔
1126
                return ghResponse, err
2✔
1127
        })
2✔
1128
        return *repositoryEnvInfo, err
2✔
1129
}
1130

1131
func (client *GitHubClient) CreateBranch(ctx context.Context, owner, repository, sourceBranch, newBranch string) error {
2✔
1132
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "sourceBranch": sourceBranch, "newBranch": newBranch})
2✔
1133
        if err != nil {
2✔
1134
                return err
×
1135
        }
×
1136

1137
        var sourceBranchRef *github.Reference
2✔
1138
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1139
                sourceBranch = vcsutils.AddBranchPrefix(sourceBranch)
2✔
1140
                sourceBranchRef, _, err = client.ghClient.Git.GetRef(ctx, owner, repository, sourceBranch)
2✔
1141
                if err != nil {
3✔
1142
                        return nil, err
1✔
1143
                }
1✔
1144
                return nil, nil
1✔
1145
        })
1146
        if err != nil {
3✔
1147
                return err
1✔
1148
        }
1✔
1149

1150
        if sourceBranchRef == nil {
1✔
1151
                return fmt.Errorf("failed to get reference for source branch %s", sourceBranch)
×
1152
        }
×
1153
        if sourceBranchRef.Object == nil {
1✔
1154
                return fmt.Errorf("source branch %s reference object is nil", sourceBranch)
×
1155
        }
×
1156

1157
        latestCommitSHA := sourceBranchRef.Object.SHA
1✔
1158
        newBranch = vcsutils.AddBranchPrefix(newBranch)
1✔
1159
        ref := &github.Reference{
1✔
1160
                Ref:    github.Ptr(newBranch),
1✔
1161
                Object: &github.GitObject{SHA: latestCommitSHA},
1✔
1162
        }
1✔
1163

1✔
1164
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
2✔
1165
                _, _, err = client.ghClient.Git.CreateRef(ctx, owner, repository, ref)
1✔
1166
                if err != nil {
1✔
1167
                        return nil, err
×
1168
                }
×
1169
                return nil, nil
1✔
1170
        })
1171
}
1172

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

1179
        publicKey, _, err := client.ghClient.Actions.GetOrgPublicKey(ctx, owner)
2✔
1180
        if err != nil {
3✔
1181
                return err
1✔
1182
        }
1✔
1183

1184
        encryptedValue, err := encryptSecret(publicKey, secretValue)
1✔
1185
        if err != nil {
1✔
1186
                return err
×
1187
        }
×
1188

1189
        secret := &github.EncryptedSecret{
1✔
1190
                Name:           secretName,
1✔
1191
                KeyID:          publicKey.GetKeyID(),
1✔
1192
                EncryptedValue: encryptedValue,
1✔
1193
                Visibility:     "all",
1✔
1194
        }
1✔
1195

1✔
1196
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
2✔
1197
                _, err = client.ghClient.Actions.CreateOrUpdateOrgSecret(ctx, owner, secret)
1✔
1198
                return nil, err
1✔
1199
        })
1✔
1200
        return err
1✔
1201
}
1202

1203
func (client *GitHubClient) CreateOrgVariable(ctx context.Context, owner, variableName, variableValue string) error {
2✔
1204
        err := validateParametersNotBlank(map[string]string{"owner": owner, "variableName": variableName, "variableValue": variableValue})
2✔
1205
        if err != nil {
2✔
1206
                return err
×
1207
        }
×
1208

1209
        variable := &github.ActionsVariable{
2✔
1210
                Name:       variableName,
2✔
1211
                Value:      variableValue,
2✔
1212
                Visibility: github.Ptr("all"),
2✔
1213
        }
2✔
1214

2✔
1215
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1216
                _, err = client.ghClient.Actions.CreateOrgVariable(ctx, owner, variable)
2✔
1217
                return nil, err
2✔
1218
        })
2✔
1219
        return err
2✔
1220
}
1221

1222
func (client *GitHubClient) AllowWorkflows(ctx context.Context, owner string) error {
2✔
1223
        err := validateParametersNotBlank(map[string]string{"owner": owner})
2✔
1224
        if err != nil {
2✔
1225
                return err
×
1226
        }
×
1227

1228
        requestBody := &github.ActionsPermissions{
2✔
1229
                AllowedActions:      github.Ptr("all"),
2✔
1230
                EnabledRepositories: github.Ptr("all"),
2✔
1231
        }
2✔
1232

2✔
1233
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1234
                _, _, err = client.ghClient.Actions.EditActionsPermissions(ctx, owner, *requestBody)
2✔
1235
                return nil, err
2✔
1236
        })
2✔
1237
        return err
2✔
1238
}
1239

1240
func (client *GitHubClient) TriggerWorkflow(ctx context.Context, owner, repo, eventType string, payload map[string]interface{}) error {
2✔
1241
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repo": repo, "eventType": eventType})
2✔
1242
        if err != nil {
2✔
1243
                return err
×
1244
        }
×
1245

1246
        rawPayload, err := json.Marshal(payload)
2✔
1247
        if err != nil {
2✔
1248
                return fmt.Errorf("failed to marshal workflow payload: %w", err)
×
1249
        }
×
1250
        raw := json.RawMessage(rawPayload)
2✔
1251

2✔
1252
        return client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1253
                _, resp, err := client.ghClient.Repositories.Dispatch(ctx, owner, repo, github.DispatchRequestOptions{
2✔
1254
                        EventType:     eventType,
2✔
1255
                        ClientPayload: &raw,
2✔
1256
                })
2✔
1257
                return resp, err
2✔
1258
        })
2✔
1259
}
1260

1261
func (client *GitHubClient) GetRepoCollaborators(ctx context.Context, owner, repo, affiliation, permission string) ([]string, error) {
2✔
1262
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repo": repo, "affiliation": affiliation, "permission": permission})
2✔
1263
        if err != nil {
2✔
1264
                return nil, err
×
1265
        }
×
1266

1267
        var collaborators []*github.User
2✔
1268
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1269
                var ghResponse *github.Response
2✔
1270
                var err error
2✔
1271
                collaborators, ghResponse, err = client.ghClient.Repositories.ListCollaborators(ctx, owner, repo, &github.ListCollaboratorsOptions{
2✔
1272
                        Affiliation: affiliation,
2✔
1273
                        Permission:  permission,
2✔
1274
                })
2✔
1275
                return ghResponse, err
2✔
1276
        })
2✔
1277
        if err != nil {
3✔
1278
                return nil, err
1✔
1279
        }
1✔
1280

1281
        var names []string
1✔
1282
        for _, collab := range collaborators {
2✔
1283
                names = append(names, collab.GetLogin())
1✔
1284
        }
1✔
1285
        return names, nil
1✔
1286
}
1287

1288
func (client *GitHubClient) GetRepoTeamsByPermissions(ctx context.Context, owner, repo string, permissions []string) ([]int64, error) {
2✔
1289
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repo": repo, "permissions": strings.Join(permissions, ",")})
2✔
1290
        if err != nil {
2✔
1291
                return nil, err
×
1292
        }
×
1293

1294
        var allTeams []*github.Team
2✔
1295
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1296
                var resp *github.Response
2✔
1297
                var err error
2✔
1298
                allTeams, resp, err = client.ghClient.Repositories.ListTeams(ctx, owner, repo, nil)
2✔
1299
                return resp, err
2✔
1300
        })
2✔
1301
        if err != nil {
3✔
1302
                return nil, err
1✔
1303
        }
1✔
1304

1305
        permMap := make(map[string]bool)
1✔
1306
        for _, p := range permissions {
2✔
1307
                permMap[strings.ToLower(p)] = true
1✔
1308
        }
1✔
1309

1310
        var matchedTeams []int64
1✔
1311
        for _, team := range allTeams {
2✔
1312
                if permMap[strings.ToLower(team.GetPermission())] {
2✔
1313
                        matchedTeams = append(matchedTeams, team.GetID())
1✔
1314
                }
1✔
1315
        }
1316

1317
        return matchedTeams, nil
1✔
1318
}
1319

1320
func (client *GitHubClient) CreateOrUpdateEnvironment(ctx context.Context, owner, repo, envName string, teams []int64, users []string) error {
2✔
1321
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repo": repo, "envName": envName})
2✔
1322
        if err != nil {
2✔
1323
                return err
×
1324
        }
×
1325

1326
        var envReviewers []*github.EnvReviewers
2✔
1327
        for _, team := range teams {
4✔
1328
                envReviewers = append(envReviewers, &github.EnvReviewers{
2✔
1329
                        Type: github.Ptr("Team"),
2✔
1330
                        ID:   &team,
2✔
1331
                })
2✔
1332
        }
2✔
1333

1334
        if len(envReviewers) >= ghMaxEnvReviewers {
2✔
1335
                envReviewers = envReviewers[:ghMaxEnvReviewers]
×
1336
                _, _, err := client.ghClient.Repositories.CreateUpdateEnvironment(ctx, owner, repo, envName, &github.CreateUpdateEnvironment{
×
1337
                        Reviewers: envReviewers,
×
1338
                })
×
1339
                return err
×
1340
        }
×
1341

1342
        for _, userName := range users {
2✔
1343
                user, _, err := client.ghClient.Users.Get(ctx, userName)
×
1344

×
1345
                if err != nil {
×
1346
                        return err
×
1347
                }
×
1348
                userId := user.GetID()
×
1349
                envReviewers = append(envReviewers, &github.EnvReviewers{
×
1350
                        Type: github.Ptr("User"),
×
1351
                        ID:   github.Ptr(userId),
×
1352
                })
×
1353
        }
1354

1355
        if len(envReviewers) >= ghMaxEnvReviewers {
2✔
1356
                envReviewers = envReviewers[:ghMaxEnvReviewers]
×
1357
                _, _, err := client.ghClient.Repositories.CreateUpdateEnvironment(ctx, owner, repo, envName, &github.CreateUpdateEnvironment{
×
1358
                        Reviewers: envReviewers,
×
1359
                })
×
1360
                return err
×
1361
        }
×
1362

1363
        _, _, err = client.ghClient.Repositories.CreateUpdateEnvironment(ctx, owner, repo, envName, &github.CreateUpdateEnvironment{
2✔
1364
                Reviewers: envReviewers,
2✔
1365
        })
2✔
1366
        return err
2✔
1367
}
1368

1369
func (client *GitHubClient) CommitAndPushFiles(
1370
        ctx context.Context,
1371
        owner, repo, sourceBranch, commitMessage, authorName, authorEmail string,
1372
        files []FileToCommit,
1373
) error {
4✔
1374
        if len(files) == 0 {
4✔
1375
                return errors.New("no files provided to commit")
×
1376
        }
×
1377

1378
        if len(files) == 1 {
6✔
1379
                client.logger.Debug("Using Contents API for single file commit")
2✔
1380
                return client.commitSingleFile(ctx, owner, repo, sourceBranch, files[0], commitMessage, authorName, authorEmail)
2✔
1381
        }
2✔
1382

1383
        client.logger.Debug("Using Git API for ", len(files), " file commit")
2✔
1384
        return client.commitMultipleFiles(ctx, owner, repo, sourceBranch, files, commitMessage, authorName, authorEmail)
2✔
1385
}
1386

1387
func (client *GitHubClient) commitSingleFile(
1388
        ctx context.Context,
1389
        owner, repo, branch string,
1390
        file FileToCommit,
1391
        commitMessage, authorName, authorEmail string,
1392
) error {
2✔
1393

2✔
1394
        fileOptions := &github.RepositoryContentFileOptions{
2✔
1395
                Message: &commitMessage,
2✔
1396
                Content: []byte(file.Content),
2✔
1397
                Branch:  &branch,
2✔
1398
                Author: &github.CommitAuthor{
2✔
1399
                        Name:  &authorName,
2✔
1400
                        Email: &authorEmail,
2✔
1401
                },
2✔
1402
                Committer: &github.CommitAuthor{
2✔
1403
                        Name:  &authorName,
2✔
1404
                        Email: &authorEmail,
2✔
1405
                },
2✔
1406
        }
2✔
1407

2✔
1408
        // Contents API requires the current blob SHA when updating an existing file.
2✔
1409
        var existing *github.RepositoryContent
2✔
1410
        err := client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1411
                var ghResponse *github.Response
2✔
1412
                var getErr error
2✔
1413
                existing, _, ghResponse, getErr = client.ghClient.Repositories.GetContents(
2✔
1414
                        ctx, owner, repo, file.Path, &github.RepositoryContentGetOptions{Ref: branch})
2✔
1415
                if getErr != nil {
3✔
1416
                        if ghResponse != nil && ghResponse.StatusCode == http.StatusNotFound {
2✔
1417
                                return ghResponse, nil
1✔
1418
                        }
1✔
NEW
1419
                        return ghResponse, getErr
×
1420
                }
1421
                return ghResponse, nil
1✔
1422
        })
1423
        if err != nil {
2✔
NEW
1424
                return fmt.Errorf("failed to commit single file %s: %w", file.Path, err)
×
NEW
1425
        }
×
1426
        if existing != nil && existing.SHA != nil {
3✔
1427
                fileOptions.SHA = existing.SHA
1✔
1428
        }
1✔
1429

1430
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1431
                _, ghResponse, err := client.ghClient.Repositories.CreateFile(ctx, owner, repo, file.Path, fileOptions)
2✔
1432
                return ghResponse, err
2✔
1433
        })
2✔
1434

1435
        if err != nil {
2✔
1436
                return fmt.Errorf("failed to commit single file %s: %w", file.Path, err)
×
1437
        }
×
1438
        return nil
2✔
1439
}
1440

1441
func (client *GitHubClient) commitMultipleFiles(
1442
        ctx context.Context,
1443
        owner, repo, sourceBranch string,
1444
        files []FileToCommit,
1445
        commitMessage, authorName, authorEmail string,
1446
) error {
2✔
1447
        ref, _, err := client.ghClient.Git.GetRef(ctx, owner, repo, "refs/heads/"+sourceBranch)
2✔
1448
        if err != nil {
3✔
1449
                return fmt.Errorf("failed to get branch ref: %w", err)
1✔
1450
        }
1✔
1451

1452
        parentCommit, _, err := client.ghClient.Git.GetCommit(ctx, owner, repo, *ref.Object.SHA)
1✔
1453
        if err != nil {
1✔
1454
                return fmt.Errorf("failed to get parent commit: %w", err)
×
1455
        }
×
1456

1457
        treeEntries, err := client.createBlobs(ctx, owner, repo, files)
1✔
1458
        if err != nil {
1✔
1459
                return err
×
1460
        }
×
1461

1462
        tree, _, err := client.ghClient.Git.CreateTree(ctx, owner, repo, *parentCommit.Tree.SHA, treeEntries)
1✔
1463
        if err != nil {
1✔
1464
                return fmt.Errorf("failed to create tree: %w", err)
×
1465
        }
×
1466

1467
        commit := &github.Commit{
1✔
1468
                Message: github.Ptr(commitMessage),
1✔
1469
                Tree:    tree,
1✔
1470
                Parents: []*github.Commit{{SHA: parentCommit.SHA}},
1✔
1471
                Author: &github.CommitAuthor{
1✔
1472
                        Name:  github.Ptr(authorName),
1✔
1473
                        Email: github.Ptr(authorEmail),
1✔
1474
                        Date:  &github.Timestamp{Time: time.Now()},
1✔
1475
                },
1✔
1476
        }
1✔
1477

1✔
1478
        newCommit, _, err := client.ghClient.Git.CreateCommit(ctx, owner, repo, commit, nil)
1✔
1479
        if err != nil {
1✔
1480
                return fmt.Errorf("failed to create commit: %w", err)
×
1481
        }
×
1482

1483
        ref.Object.SHA = newCommit.SHA
1✔
1484
        _, _, err = client.ghClient.Git.UpdateRef(ctx, owner, repo, ref, false)
1✔
1485
        if err != nil {
1✔
1486
                return fmt.Errorf("failed to update branch ref: %w", err)
×
1487
        }
×
1488
        return nil
1✔
1489
}
1490

1491
func (client *GitHubClient) createBlobs(ctx context.Context, owner, repo string, files []FileToCommit) ([]*github.TreeEntry, error) {
1✔
1492
        var treeEntries []*github.TreeEntry
1✔
1493
        for _, file := range files {
3✔
1494
                var blob *github.Blob
2✔
1495
                err := client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1496
                        var ghResponse *github.Response
2✔
1497
                        var err error
2✔
1498
                        blob, ghResponse, err = client.ghClient.Git.CreateBlob(ctx, owner, repo, &github.Blob{
2✔
1499
                                Content:  github.Ptr(file.Content),
2✔
1500
                                Encoding: github.Ptr("utf-8"),
2✔
1501
                        })
2✔
1502
                        return ghResponse, err
2✔
1503
                })
2✔
1504
                if err != nil {
2✔
1505
                        return nil, fmt.Errorf("failed to create blob for %s: %w", file.Path, err)
×
1506
                }
×
1507

1508
                treeEntries = append(treeEntries, &github.TreeEntry{
2✔
1509
                        Path: github.Ptr(file.Path),
2✔
1510
                        Mode: github.Ptr(regularFileCode),
2✔
1511
                        Type: github.Ptr("blob"),
2✔
1512
                        SHA:  blob.SHA,
2✔
1513
                })
2✔
1514
        }
1515

1516
        return treeEntries, nil
1✔
1517
}
1518

1519
func (client *GitHubClient) MergePullRequest(ctx context.Context, owner, repo string, prNumber int, commitMessage string) error {
2✔
1520
        err := client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1521
                _, resp, err := client.ghClient.PullRequests.Merge(ctx, owner, repo, prNumber, commitMessage, nil)
2✔
1522
                return resp, err
2✔
1523
        })
2✔
1524
        return err
2✔
1525
}
1526

1527
func (client *GitHubClient) executeGetRepositoryEnvironmentInfo(ctx context.Context, owner, repository, name string) (*RepositoryEnvironmentInfo, *github.Response, error) {
2✔
1528
        environment, ghResponse, err := client.ghClient.Repositories.GetEnvironment(ctx, owner, repository, name)
2✔
1529
        if err != nil {
3✔
1530
                return &RepositoryEnvironmentInfo{}, ghResponse, err
1✔
1531
        }
1✔
1532

1533
        if err = vcsutils.CheckResponseStatusWithBody(ghResponse.Response, http.StatusOK); err != nil {
1✔
1534
                return &RepositoryEnvironmentInfo{}, ghResponse, err
×
1535
        }
×
1536

1537
        reviewers, err := extractGitHubEnvironmentReviewers(environment)
1✔
1538
        if err != nil {
1✔
1539
                return &RepositoryEnvironmentInfo{}, ghResponse, err
×
1540
        }
×
1541

1542
        return &RepositoryEnvironmentInfo{
1✔
1543
                        Name:      environment.GetName(),
1✔
1544
                        Url:       environment.GetURL(),
1✔
1545
                        Reviewers: reviewers,
1✔
1546
                },
1✔
1547
                ghResponse,
1✔
1548
                nil
1✔
1549
}
1550

1551
func (client *GitHubClient) GetModifiedFiles(ctx context.Context, owner, repository, refBefore, refAfter string) ([]string, error) {
6✔
1552
        err := validateParametersNotBlank(map[string]string{
6✔
1553
                "owner":      owner,
6✔
1554
                "repository": repository,
6✔
1555
                "refBefore":  refBefore,
6✔
1556
                "refAfter":   refAfter,
6✔
1557
        })
6✔
1558
        if err != nil {
10✔
1559
                return nil, err
4✔
1560
        }
4✔
1561

1562
        var fileNamesList []string
2✔
1563
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1564
                var ghResponse *github.Response
2✔
1565
                fileNamesList, ghResponse, err = client.executeGetModifiedFiles(ctx, owner, repository, refBefore, refAfter)
2✔
1566
                return ghResponse, err
2✔
1567
        })
2✔
1568
        return fileNamesList, err
2✔
1569
}
1570

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

2✔
1578
        comparison, ghResponse, err := client.ghClient.Repositories.CompareCommits(ctx, owner, repository, refBefore, refAfter, listOptions)
2✔
1579
        if err != nil {
3✔
1580
                return nil, ghResponse, err
1✔
1581
        }
1✔
1582

1583
        if err = vcsutils.CheckResponseStatusWithBody(ghResponse.Response, http.StatusOK); err != nil {
1✔
1584
                return nil, ghResponse, err
×
1585
        }
×
1586

1587
        fileNamesSet := datastructures.MakeSet[string]()
1✔
1588
        for _, file := range comparison.Files {
18✔
1589
                fileNamesSet.Add(vcsutils.DefaultIfNotNil(file.Filename))
17✔
1590
                fileNamesSet.Add(vcsutils.DefaultIfNotNil(file.PreviousFilename))
17✔
1591
        }
17✔
1592

1593
        _ = fileNamesSet.Remove("") // Make sure there are no blank filepath.
1✔
1594
        fileNamesList := fileNamesSet.ToSlice()
1✔
1595
        sort.Strings(fileNamesList)
1✔
1596

1✔
1597
        return fileNamesList, ghResponse, nil
1✔
1598
}
1599

1600
// Extract code reviewers from environment
1601
func extractGitHubEnvironmentReviewers(environment *github.Environment) ([]string, error) {
2✔
1602
        var reviewers []string
2✔
1603
        protectionRules := environment.ProtectionRules
2✔
1604
        if protectionRules == nil {
2✔
1605
                return reviewers, nil
×
1606
        }
×
1607
        reviewerStruct := repositoryEnvironmentReviewer{}
2✔
1608
        for _, rule := range protectionRules {
4✔
1609
                for _, reviewer := range rule.Reviewers {
5✔
1610
                        if err := mapstructure.Decode(reviewer.Reviewer, &reviewerStruct); err != nil {
3✔
1611
                                return []string{}, err
×
1612
                        }
×
1613
                        reviewers = append(reviewers, reviewerStruct.Login)
3✔
1614
                }
1615
        }
1616
        return reviewers, nil
2✔
1617
}
1618

1619
func createGitHubHook(token, payloadURL string, webhookEvents ...vcsutils.WebhookEvent) *github.Hook {
4✔
1620
        contentType := "json"
4✔
1621
        return &github.Hook{
4✔
1622
                Events: getGitHubWebhookEvents(webhookEvents...),
4✔
1623
                Config: &github.HookConfig{
4✔
1624
                        ContentType: &contentType,
4✔
1625
                        URL:         &payloadURL,
4✔
1626
                        Secret:      &token,
4✔
1627
                },
4✔
1628
        }
4✔
1629
}
4✔
1630

1631
// Get varargs of webhook events and return a slice of GitHub webhook events
1632
func getGitHubWebhookEvents(webhookEvents ...vcsutils.WebhookEvent) []string {
4✔
1633
        events := datastructures.MakeSet[string]()
4✔
1634
        for _, event := range webhookEvents {
16✔
1635
                switch event {
12✔
1636
                case vcsutils.PrOpened, vcsutils.PrEdited, vcsutils.PrMerged, vcsutils.PrRejected:
8✔
1637
                        events.Add("pull_request")
8✔
1638
                case vcsutils.Push, vcsutils.TagPushed, vcsutils.TagRemoved:
4✔
1639
                        events.Add("push")
4✔
1640
                }
1641
        }
1642
        return events.ToSlice()
4✔
1643
}
1644

1645
func getGitHubRepositoryVisibility(repo *github.Repository) RepositoryVisibility {
5✔
1646
        switch *repo.Visibility {
5✔
1647
        case "public":
3✔
1648
                return Public
3✔
1649
        case "internal":
1✔
1650
                return Internal
1✔
1651
        default:
1✔
1652
                return Private
1✔
1653
        }
1654
}
1655

1656
func getGitHubCommitState(commitState CommitStatus) string {
7✔
1657
        switch commitState {
7✔
1658
        case Pass:
1✔
1659
                return "success"
1✔
1660
        case Fail:
1✔
1661
                return "failure"
1✔
1662
        case Error:
3✔
1663
                return "error"
3✔
1664
        case InProgress:
1✔
1665
                return "pending"
1✔
1666
        }
1667
        return ""
1✔
1668
}
1669

1670
func mapGitHubCommitToCommitInfo(commit *github.RepositoryCommit) CommitInfo {
8✔
1671
        parents := make([]string, len(commit.Parents))
8✔
1672
        for i, c := range commit.Parents {
15✔
1673
                parents[i] = c.GetSHA()
7✔
1674
        }
7✔
1675
        details := commit.GetCommit()
8✔
1676
        return CommitInfo{
8✔
1677
                Hash:          commit.GetSHA(),
8✔
1678
                AuthorName:    details.GetAuthor().GetName(),
8✔
1679
                CommitterName: details.GetCommitter().GetName(),
8✔
1680
                Url:           commit.GetURL(),
8✔
1681
                Timestamp:     details.GetCommitter().GetDate().UTC().Unix(),
8✔
1682
                Message:       details.GetMessage(),
8✔
1683
                ParentHashes:  parents,
8✔
1684
                AuthorEmail:   details.GetAuthor().GetEmail(),
8✔
1685
        }
8✔
1686
}
1687

1688
func mapGitHubIssuesCommentToCommentInfoList(commentsList []*github.IssueComment) (res []CommentInfo, err error) {
1✔
1689
        for _, comment := range commentsList {
3✔
1690
                res = append(res, CommentInfo{
2✔
1691
                        ID:      comment.GetID(),
2✔
1692
                        Content: comment.GetBody(),
2✔
1693
                        Created: comment.GetCreatedAt().Time,
2✔
1694
                })
2✔
1695
        }
2✔
1696
        return
1✔
1697
}
1698

1699
func mapGitHubPullRequestToPullRequestInfoList(pullRequestList []*github.PullRequest, withBody bool) (res []PullRequestInfo, err error) {
3✔
1700
        var mappedPullRequest PullRequestInfo
3✔
1701
        for _, pullRequest := range pullRequestList {
6✔
1702
                mappedPullRequest, err = mapGitHubPullRequestToPullRequestInfo(pullRequest, withBody)
3✔
1703
                if err != nil {
3✔
1704
                        return
×
1705
                }
×
1706
                res = append(res, mappedPullRequest)
3✔
1707
        }
1708
        return
3✔
1709
}
1710

1711
func encodeScanningResult(data string) (string, error) {
3✔
1712
        compressedScan, err := base64.EncodeGzip([]byte(data), 6)
3✔
1713
        if err != nil {
3✔
1714
                return "", err
×
1715
        }
×
1716

1717
        return compressedScan, err
3✔
1718
}
1719

1720
type repositoryEnvironmentReviewer struct {
1721
        Login string `mapstructure:"login"`
1722
}
1723

1724
func shouldRetryIfRateLimitExceeded(ghResponse *github.Response, requestError error) bool {
130✔
1725
        if ghResponse == nil || ghResponse.Response == nil {
185✔
1726
                return false
55✔
1727
        }
55✔
1728

1729
        if !slices.Contains(rateLimitRetryStatuses, ghResponse.StatusCode) {
148✔
1730
                return false
73✔
1731
        }
73✔
1732

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

1739
        body, err := io.ReadAll(ghResponse.Body)
1✔
1740
        if err != nil {
1✔
1741
                return false
×
1742
        }
×
1743
        return strings.Contains(string(body), "rate limit")
1✔
1744
}
1745

1746
func isRateLimitAbuseError(requestError error) bool {
4✔
1747
        var abuseRateLimitError *github.AbuseRateLimitError
4✔
1748
        var rateLimitError *github.RateLimitError
4✔
1749
        return errors.As(requestError, &abuseRateLimitError) || errors.As(requestError, &rateLimitError)
4✔
1750
}
4✔
1751

1752
func encryptSecret(publicKey *github.PublicKey, secretValue string) (string, error) {
1✔
1753
        publicKeyBytes, err := base64Utils.StdEncoding.DecodeString(publicKey.GetKey())
1✔
1754
        if err != nil {
1✔
1755
                return "", err
×
1756
        }
×
1757

1758
        var publicKeyDecoded [32]byte
1✔
1759
        copy(publicKeyDecoded[:], publicKeyBytes)
1✔
1760

1✔
1761
        encrypted, err := box.SealAnonymous(nil, []byte(secretValue), &publicKeyDecoded, rand.Reader)
1✔
1762
        if err != nil {
1✔
1763
                return "", err
×
1764
        }
×
1765

1766
        encryptedBase64 := base64Utils.StdEncoding.EncodeToString(encrypted)
1✔
1767
        return encryptedBase64, nil
1✔
1768
}
1769

1770
func (client *GitHubClient) ListAppRepositories(ctx context.Context) ([]AppRepositoryInfo, error) {
2✔
1771
        var results []AppRepositoryInfo
2✔
1772

2✔
1773
        var allRepositories []*github.Repository
2✔
1774
        for nextPage := 1; ; nextPage++ {
4✔
1775
                var repositoriesInPage *github.ListRepositories
2✔
1776
                var ghResponse *github.Response
2✔
1777
                var err error
2✔
1778
                err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1779
                        repositoriesInPage, ghResponse, err = client.ghClient.Apps.ListRepos(ctx, &github.ListOptions{Page: nextPage})
2✔
1780
                        return ghResponse, err
2✔
1781
                })
2✔
1782
                if err != nil {
3✔
1783
                        return nil, err
1✔
1784
                }
1✔
1785
                allRepositories = append(allRepositories, repositoriesInPage.Repositories...)
1✔
1786
                if nextPage+1 > ghResponse.LastPage {
2✔
1787
                        break
1✔
1788
                }
1789
        }
1790

1791
        for _, repo := range allRepositories {
2✔
1792
                if repo == nil || repo.Owner == nil || repo.Owner.Login == nil || repo.Name == nil {
1✔
1793
                        continue
×
1794
                }
1795
                repoInfo := AppRepositoryInfo{
1✔
1796
                        ID:            repo.GetID(),
1✔
1797
                        Name:          vcsutils.DefaultIfNotNil(repo.Name),
1✔
1798
                        FullName:      vcsutils.DefaultIfNotNil(repo.FullName),
1✔
1799
                        Owner:         vcsutils.DefaultIfNotNil(repo.Owner.Login),
1✔
1800
                        Private:       repo.GetPrivate(),
1✔
1801
                        Description:   vcsutils.DefaultIfNotNil(repo.Description),
1✔
1802
                        URL:           vcsutils.DefaultIfNotNil(repo.HTMLURL),
1✔
1803
                        CloneURL:      vcsutils.DefaultIfNotNil(repo.CloneURL),
1✔
1804
                        SSHURL:        vcsutils.DefaultIfNotNil(repo.SSHURL),
1✔
1805
                        DefaultBranch: vcsutils.DefaultIfNotNil(repo.DefaultBranch),
1✔
1806
                }
1✔
1807
                results = append(results, repoInfo)
1✔
1808
        }
1809

1810
        return results, nil
1✔
1811
}
1812
func (client *GitHubClient) UploadSnapshotToDependencyGraph(ctx context.Context, owner, repo string, snapshot *SbomSnapshot) error {
2✔
1813
        if snapshot == nil {
2✔
1814
                return fmt.Errorf("provided snapshot is nil")
×
1815
        }
×
1816

1817
        ghSnapshot, err := convertToGitHubSnapshot(snapshot)
2✔
1818
        if err != nil {
2✔
1819
                return fmt.Errorf("failed to convert snapshot to GitHub format: %w", err)
×
1820
        }
×
1821

1822
        var ghResponse *github.Response
2✔
1823
        err = client.runWithRateLimitRetries(func() (*github.Response, error) {
4✔
1824
                _, ghResponse, err = client.ghClient.DependencyGraph.CreateSnapshot(ctx, owner, repo, ghSnapshot)
2✔
1825
                return ghResponse, err
2✔
1826
        })
2✔
1827

1828
        if err != nil {
3✔
1829
                return fmt.Errorf("failed to upload snapshot to dependency graph: %w", err)
1✔
1830
        }
1✔
1831

1832
        if ghResponse == nil || ghResponse.Response == nil || ghResponse.StatusCode != http.StatusCreated {
1✔
1833
                return fmt.Errorf("dependency submission call finished with unexpected status code: %d", ghResponse.StatusCode)
×
1834
        }
×
1835

1836
        client.logger.Info(vcsutils.SuccessfulSnapshotUpload, ghResponse.StatusCode)
1✔
1837
        return nil
1✔
1838
}
1839

1840
func convertToGitHubSnapshot(snapshot *SbomSnapshot) (*github.DependencyGraphSnapshot, error) {
2✔
1841
        ghSnapshot := &github.DependencyGraphSnapshot{
2✔
1842
                Version: snapshot.Version,
2✔
1843
                Sha:     &snapshot.Sha,
2✔
1844
                Ref:     &snapshot.Ref,
2✔
1845
                Scanned: &github.Timestamp{Time: snapshot.Scanned}, // Use current time if not provided
2✔
1846
        }
2✔
1847

2✔
1848
        if snapshot.Job == nil {
2✔
1849
                return nil, fmt.Errorf("job information is required in the snapshot")
×
1850
        }
×
1851
        ghSnapshot.Job = &github.DependencyGraphSnapshotJob{
2✔
1852
                Correlator: &snapshot.Job.Correlator,
2✔
1853
                ID:         &snapshot.Job.ID,
2✔
1854
        }
2✔
1855

2✔
1856
        if snapshot.Detector == nil {
2✔
1857
                return nil, fmt.Errorf("detector information is required in the snapshot")
×
1858
        }
×
1859
        ghSnapshot.Detector = &github.DependencyGraphSnapshotDetector{
2✔
1860
                Name:    &snapshot.Detector.Name,
2✔
1861
                Version: &snapshot.Detector.Version,
2✔
1862
                URL:     &snapshot.Detector.Url,
2✔
1863
        }
2✔
1864

2✔
1865
        if len(snapshot.Manifests) == 0 {
2✔
1866
                return nil, fmt.Errorf("at least one manifest is required in the snapshot")
×
1867
        }
×
1868
        ghSnapshot.Manifests = make(map[string]*github.DependencyGraphSnapshotManifest)
2✔
1869
        for manifestName, manifest := range snapshot.Manifests {
4✔
1870
                ghManifest := &github.DependencyGraphSnapshotManifest{
2✔
1871
                        Name: &manifest.Name,
2✔
1872
                }
2✔
1873

2✔
1874
                if manifest.File == nil {
2✔
1875
                        return nil, fmt.Errorf("manifest '%s' is missing file information", manifestName)
×
1876
                }
×
1877
                ghManifest.File = &github.DependencyGraphSnapshotManifestFile{SourceLocation: &manifest.File.SourceLocation}
2✔
1878

2✔
1879
                if len(manifest.Resolved) == 0 {
2✔
1880
                        return nil, fmt.Errorf("manifest '%s' must have at least one resolved dependency", manifestName)
×
1881
                }
×
1882
                ghManifest.Resolved = make(map[string]*github.DependencyGraphSnapshotResolvedDependency)
2✔
1883
                for depName, dep := range manifest.Resolved {
8✔
1884
                        ghDep := &github.DependencyGraphSnapshotResolvedDependency{
6✔
1885
                                PackageURL:   &dep.PackageURL,
6✔
1886
                                Dependencies: dep.Dependencies,
6✔
1887
                                Relationship: &dep.Relationship,
6✔
1888
                        }
6✔
1889
                        ghManifest.Resolved[depName] = ghDep
6✔
1890
                }
6✔
1891

1892
                ghSnapshot.Manifests[manifestName] = ghManifest
2✔
1893
        }
1894
        return ghSnapshot, nil
2✔
1895
}
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