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

jfrog / froggit-go / 14219416176

02 Apr 2025 12:18PM UTC coverage: 87.377%. First build
14219416176

Pull #143

github

EyalDelarea
Fix BB Server unsupported
Pull Request #143: Add `ListPullRequestCommits`

69 of 83 new or added lines in 5 files covered. (83.13%)

4271 of 4888 relevant lines covered (87.38%)

6.71 hits per line

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

88.49
/vcsclient/gitlab.go
1
package vcsclient
2

3
import (
4
        "bytes"
5
        "context"
6
        "encoding/base64"
7
        "errors"
8
        "fmt"
9
        "github.com/jfrog/froggit-go/vcsutils"
10
        "github.com/jfrog/gofrog/datastructures"
11
        "github.com/xanzy/go-gitlab"
12
        "net/http"
13
        "sort"
14
        "strconv"
15
        "strings"
16
        "time"
17
)
18

19
// GitLabClient API version 4
20
type GitLabClient struct {
21
        glClient *gitlab.Client
22
        vcsInfo  VcsInfo
23
        logger   vcsutils.Log
24
}
25

26
// NewGitLabClient create a new GitLabClient
27
func NewGitLabClient(vcsInfo VcsInfo, logger vcsutils.Log) (*GitLabClient, error) {
83✔
28
        var client *gitlab.Client
83✔
29
        var err error
83✔
30
        if vcsInfo.APIEndpoint != "" {
132✔
31
                client, err = gitlab.NewClient(vcsInfo.Token, gitlab.WithBaseURL(vcsInfo.APIEndpoint))
49✔
32
        } else {
83✔
33
                client, err = gitlab.NewClient(vcsInfo.Token)
34✔
34
        }
34✔
35
        if err != nil {
84✔
36
                return nil, err
1✔
37
        }
1✔
38

39
        return &GitLabClient{
82✔
40
                glClient: client,
82✔
41
                vcsInfo:  vcsInfo,
82✔
42
                logger:   logger,
82✔
43
        }, nil
82✔
44
}
45

46
// TestConnection on GitLab
47
func (client *GitLabClient) TestConnection(ctx context.Context) error {
3✔
48
        _, _, err := client.glClient.Projects.ListProjects(nil, gitlab.WithContext(ctx))
3✔
49
        return err
3✔
50
}
3✔
51

52
// ListRepositories on GitLab
53
func (client *GitLabClient) ListRepositories(ctx context.Context) (map[string][]string, error) {
1✔
54
        simple := true
1✔
55
        results := make(map[string][]string)
1✔
56
        membership := true
1✔
57
        for pageID := 1; ; pageID++ {
3✔
58
                options := &gitlab.ListProjectsOptions{ListOptions: gitlab.ListOptions{Page: pageID}, Simple: &simple, Membership: &membership}
2✔
59
                projects, response, err := client.glClient.Projects.ListProjects(options, gitlab.WithContext(ctx))
2✔
60
                if err != nil {
2✔
61
                        return nil, err
×
62
                }
×
63
                for _, project := range projects {
27✔
64
                        owner := project.Namespace.Path
25✔
65
                        results[owner] = append(results[owner], project.Path)
25✔
66
                }
25✔
67
                if pageID >= response.TotalPages {
3✔
68
                        break
1✔
69
                }
70
        }
71
        return results, nil
1✔
72
}
73

74
// ListBranches on GitLab
75
func (client *GitLabClient) ListBranches(ctx context.Context, owner, repository string) ([]string, error) {
1✔
76
        branches, _, err := client.glClient.Branches.ListBranches(getProjectID(owner, repository), nil,
1✔
77
                gitlab.WithContext(ctx))
1✔
78
        if err != nil {
1✔
79
                return nil, err
×
80
        }
×
81

82
        results := make([]string, 0, len(branches))
1✔
83
        for _, branch := range branches {
3✔
84
                results = append(results, branch.Name)
2✔
85
        }
2✔
86
        return results, nil
1✔
87
}
88

89
// AddSshKeyToRepository on GitLab
90
func (client *GitLabClient) AddSshKeyToRepository(ctx context.Context, owner, repository, keyName, publicKey string, permission Permission) error {
7✔
91
        err := validateParametersNotBlank(map[string]string{
7✔
92
                "owner":      owner,
7✔
93
                "repository": repository,
7✔
94
                "key name":   keyName,
7✔
95
                "public key": publicKey,
7✔
96
        })
7✔
97
        if err != nil {
12✔
98
                return err
5✔
99
        }
5✔
100

101
        canPush := false
2✔
102
        if permission == ReadWrite {
3✔
103
                canPush = true
1✔
104
        }
1✔
105
        options := &gitlab.AddDeployKeyOptions{
2✔
106
                Title:   &keyName,
2✔
107
                Key:     &publicKey,
2✔
108
                CanPush: &canPush,
2✔
109
        }
2✔
110
        _, _, err = client.glClient.DeployKeys.AddDeployKey(getProjectID(owner, repository), options, gitlab.WithContext(ctx))
2✔
111
        return err
2✔
112
}
113

114
// CreateWebhook on GitLab
115
func (client *GitLabClient) CreateWebhook(ctx context.Context, owner, repository, branch, payloadURL string,
116
        webhookEvents ...vcsutils.WebhookEvent) (string, string, error) {
1✔
117
        token := vcsutils.CreateToken()
1✔
118
        projectHook := createProjectHook(branch, payloadURL, webhookEvents...)
1✔
119
        options := &gitlab.AddProjectHookOptions{
1✔
120
                Token:                  &token,
1✔
121
                URL:                    &projectHook.URL,
1✔
122
                MergeRequestsEvents:    &projectHook.MergeRequestsEvents,
1✔
123
                PushEvents:             &projectHook.PushEvents,
1✔
124
                PushEventsBranchFilter: &projectHook.PushEventsBranchFilter,
1✔
125
                TagPushEvents:          &projectHook.TagPushEvents,
1✔
126
        }
1✔
127
        response, _, err := client.glClient.Projects.AddProjectHook(getProjectID(owner, repository), options,
1✔
128
                gitlab.WithContext(ctx))
1✔
129
        if err != nil {
1✔
130
                return "", "", err
×
131
        }
×
132
        return strconv.Itoa(response.ID), token, nil
1✔
133
}
134

135
// UpdateWebhook on GitLab
136
func (client *GitLabClient) UpdateWebhook(ctx context.Context, owner, repository, branch, payloadURL, token,
137
        webhookID string, webhookEvents ...vcsutils.WebhookEvent) error {
1✔
138
        projectHook := createProjectHook(branch, payloadURL, webhookEvents...)
1✔
139
        options := &gitlab.EditProjectHookOptions{
1✔
140
                Token:                  &token,
1✔
141
                URL:                    &projectHook.URL,
1✔
142
                MergeRequestsEvents:    &projectHook.MergeRequestsEvents,
1✔
143
                PushEvents:             &projectHook.PushEvents,
1✔
144
                PushEventsBranchFilter: &projectHook.PushEventsBranchFilter,
1✔
145
                TagPushEvents:          &projectHook.TagPushEvents,
1✔
146
        }
1✔
147
        intWebhook, err := strconv.Atoi(webhookID)
1✔
148
        if err != nil {
1✔
149
                return err
×
150
        }
×
151
        _, _, err = client.glClient.Projects.EditProjectHook(getProjectID(owner, repository), intWebhook, options,
1✔
152
                gitlab.WithContext(ctx))
1✔
153
        return err
1✔
154
}
155

156
// DeleteWebhook on GitLab
157
func (client *GitLabClient) DeleteWebhook(ctx context.Context, owner, repository, webhookID string) error {
1✔
158
        intWebhook, err := strconv.Atoi(webhookID)
1✔
159
        if err != nil {
1✔
160
                return err
×
161
        }
×
162
        _, err = client.glClient.Projects.DeleteProjectHook(getProjectID(owner, repository), intWebhook,
1✔
163
                gitlab.WithContext(ctx))
1✔
164
        return err
1✔
165
}
166

167
// SetCommitStatus on GitLab
168
func (client *GitLabClient) SetCommitStatus(ctx context.Context, commitStatus CommitStatus, owner, repository, ref,
169
        title, description, detailsURL string) error {
1✔
170
        options := &gitlab.SetCommitStatusOptions{
1✔
171
                State:       gitlab.BuildStateValue(getGitLabCommitState(commitStatus)),
1✔
172
                Ref:         &ref,
1✔
173
                Name:        &title,
1✔
174
                Description: &description,
1✔
175
                TargetURL:   &detailsURL,
1✔
176
        }
1✔
177
        _, _, err := client.glClient.Commits.SetCommitStatus(getProjectID(owner, repository), ref, options,
1✔
178
                gitlab.WithContext(ctx))
1✔
179
        return err
1✔
180
}
1✔
181

182
// GetCommitStatuses on GitLab
183
func (client *GitLabClient) GetCommitStatuses(ctx context.Context, _, repository, ref string) (status []CommitStatusInfo, err error) {
3✔
184
        statuses, _, err := client.glClient.Commits.GetCommitStatuses(repository, ref, nil, gitlab.WithContext(ctx))
3✔
185
        if err != nil {
4✔
186
                return nil, err
1✔
187
        }
1✔
188
        results := make([]CommitStatusInfo, 0)
2✔
189
        for _, singleStatus := range statuses {
5✔
190
                results = append(results, CommitStatusInfo{
3✔
191
                        State:         commitStatusAsStringToStatus(singleStatus.Status),
3✔
192
                        Description:   singleStatus.Description,
3✔
193
                        DetailsUrl:    singleStatus.TargetURL,
3✔
194
                        Creator:       singleStatus.Author.Name,
3✔
195
                        LastUpdatedAt: extractTimeWithFallback(singleStatus.FinishedAt),
3✔
196
                        CreatedAt:     extractTimeWithFallback(singleStatus.CreatedAt),
3✔
197
                })
3✔
198
        }
3✔
199
        return results, nil
2✔
200
}
201

202
// DownloadRepository on GitLab
203
func (client *GitLabClient) DownloadRepository(ctx context.Context, owner, repository, branch, localPath string) error {
1✔
204
        format := "tar.gz"
1✔
205
        options := &gitlab.ArchiveOptions{
1✔
206
                Format: &format,
1✔
207
                SHA:    &branch,
1✔
208
        }
1✔
209
        response, _, err := client.glClient.Repositories.Archive(getProjectID(owner, repository), options,
1✔
210
                gitlab.WithContext(ctx))
1✔
211
        if err != nil {
1✔
212
                return err
×
213
        }
×
214
        client.logger.Info(repository, vcsutils.SuccessfulRepoDownload)
1✔
215
        err = vcsutils.Untar(localPath, bytes.NewReader(response), true)
1✔
216
        if err != nil {
1✔
217
                return err
×
218
        }
×
219

220
        repositoryInfo, err := client.GetRepositoryInfo(ctx, owner, repository)
1✔
221
        if err != nil {
1✔
222
                return err
×
223
        }
×
224

225
        client.logger.Info(vcsutils.SuccessfulRepoExtraction)
1✔
226
        return vcsutils.CreateDotGitFolderWithRemote(localPath, vcsutils.RemoteName, repositoryInfo.CloneInfo.HTTP)
1✔
227
}
228

229
func (client *GitLabClient) GetPullRequestCommentSizeLimit() int {
×
230
        return gitlabMergeRequestCommentSizeLimit
×
231
}
×
232

233
func (client *GitLabClient) GetPullRequestDetailsSizeLimit() int {
×
234
        return gitlabMergeRequestDetailsSizeLimit
×
235
}
×
236

237
// CreatePullRequest on GitLab
238
func (client *GitLabClient) CreatePullRequest(ctx context.Context, owner, repository, sourceBranch, targetBranch,
239
        title, description string) error {
1✔
240
        options := &gitlab.CreateMergeRequestOptions{
1✔
241
                Title:        &title,
1✔
242
                Description:  &description,
1✔
243
                SourceBranch: &sourceBranch,
1✔
244
                TargetBranch: &targetBranch,
1✔
245
        }
1✔
246
        client.logger.Debug("creating new merge request:", title)
1✔
247
        _, _, err := client.glClient.MergeRequests.CreateMergeRequest(getProjectID(owner, repository), options,
1✔
248
                gitlab.WithContext(ctx))
1✔
249
        return err
1✔
250
}
1✔
251

252
// UpdatePullRequest on GitLab
253
func (client *GitLabClient) UpdatePullRequest(ctx context.Context, owner, repository, title, body, targetBranchName string, prId int, state vcsutils.PullRequestState) error {
4✔
254
        options := &gitlab.UpdateMergeRequestOptions{
4✔
255
                Title:        &title,
4✔
256
                Description:  &body,
4✔
257
                TargetBranch: &targetBranchName,
4✔
258
                StateEvent:   mapGitLabPullRequestState(&state),
4✔
259
        }
4✔
260
        client.logger.Debug("updating details of merge request ID:", prId)
4✔
261
        _, _, err := client.glClient.MergeRequests.UpdateMergeRequest(getProjectID(owner, repository), prId, options, gitlab.WithContext(ctx))
4✔
262
        return err
4✔
263
}
4✔
264

265
// ListOpenPullRequestsWithBody on GitLab
266
func (client *GitLabClient) ListOpenPullRequestsWithBody(ctx context.Context, owner, repository string) ([]PullRequestInfo, error) {
1✔
267
        return client.getOpenPullRequests(ctx, owner, repository, true)
1✔
268
}
1✔
269

270
// ListOpenPullRequests on GitLab
271
func (client *GitLabClient) ListOpenPullRequests(ctx context.Context, owner, repository string) ([]PullRequestInfo, error) {
1✔
272
        return client.getOpenPullRequests(ctx, owner, repository, false)
1✔
273
}
1✔
274

275
func (client *GitLabClient) getOpenPullRequests(ctx context.Context, owner, repository string, withBody bool) ([]PullRequestInfo, error) {
2✔
276
        openState := "opened"
2✔
277
        allScope := "all"
2✔
278
        options := &gitlab.ListProjectMergeRequestsOptions{
2✔
279
                State: &openState,
2✔
280
                Scope: &allScope,
2✔
281
        }
2✔
282
        mergeRequests, _, err := client.glClient.MergeRequests.ListProjectMergeRequests(getProjectID(owner, repository), options, gitlab.WithContext(ctx))
2✔
283
        if err != nil {
2✔
284
                return []PullRequestInfo{}, err
×
285
        }
×
286
        return client.mapGitLabMergeRequestToPullRequestInfoList(mergeRequests, owner, repository, withBody)
2✔
287
}
288

289
// GetPullRequestInfoById on GitLab
290
func (client *GitLabClient) GetPullRequestByID(_ context.Context, owner, repository string, pullRequestId int) (pullRequestInfo PullRequestInfo, err error) {
2✔
291
        client.logger.Debug("fetching merge requests by ID in", repository)
2✔
292
        mergeRequest, glResponse, err := client.glClient.MergeRequests.GetMergeRequest(getProjectID(owner, repository), pullRequestId, nil)
2✔
293
        if err != nil {
3✔
294
                return PullRequestInfo{}, err
1✔
295
        }
1✔
296
        if glResponse != nil {
2✔
297
                if err = vcsutils.CheckResponseStatusWithBody(glResponse.Response, http.StatusOK); err != nil {
1✔
298
                        return PullRequestInfo{}, err
×
299
                }
×
300
        }
301
        pullRequestInfo, err = client.mapGitLabMergeRequestToPullRequestInfo(mergeRequest, false, owner, repository)
1✔
302
        return
1✔
303
}
304

305
// AddPullRequestComment on GitLab
306
func (client *GitLabClient) AddPullRequestComment(ctx context.Context, owner, repository, content string, pullRequestID int) error {
5✔
307
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "content": content})
5✔
308
        if err != nil {
9✔
309
                return err
4✔
310
        }
4✔
311
        options := &gitlab.CreateMergeRequestNoteOptions{
1✔
312
                Body: &content,
1✔
313
        }
1✔
314
        _, _, err = client.glClient.Notes.CreateMergeRequestNote(getProjectID(owner, repository), pullRequestID, options,
1✔
315
                gitlab.WithContext(ctx))
1✔
316

1✔
317
        return err
1✔
318
}
319

320
// AddPullRequestReviewComments adds comments to a pull request on GitLab.
321
func (client *GitLabClient) AddPullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int, comments ...PullRequestComment) error {
2✔
322
        // Validate parameters
2✔
323
        if err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository}); err != nil {
2✔
324
                return err
×
325
        }
×
326

327
        // Check if comments are provided
328
        if len(comments) == 0 {
2✔
329
                return errors.New("could not add merge request review comments, no comments provided")
×
330
        }
×
331

332
        projectID := getProjectID(owner, repository)
2✔
333

2✔
334
        // Get merge request diff versions
2✔
335
        versions, err := client.getMergeRequestDiffVersions(ctx, projectID, pullRequestID)
2✔
336
        if err != nil {
2✔
337
                return fmt.Errorf("could not get merge request diff versions: %w", err)
×
338
        }
×
339

340
        // Get merge request details
341
        mergeRequestChanges, err := client.getMergeRequestDiff(ctx, projectID, pullRequestID)
2✔
342
        if err != nil {
2✔
343
                return fmt.Errorf("could not get merge request changes: %w", err)
×
344
        }
×
345

346
        for _, comment := range comments {
4✔
347
                if err = client.addPullRequestReviewComment(ctx, projectID, pullRequestID, comment, versions, mergeRequestChanges); err != nil {
3✔
348
                        return err
1✔
349
                }
1✔
350
        }
351

352
        return nil
1✔
353
}
354

355
func (client *GitLabClient) getMergeRequestDiffVersions(ctx context.Context, projectID string, pullRequestID int) ([]*gitlab.MergeRequestDiffVersion, error) {
2✔
356
        versions, _, err := client.glClient.MergeRequests.GetMergeRequestDiffVersions(projectID, pullRequestID, &gitlab.GetMergeRequestDiffVersionsOptions{}, gitlab.WithContext(ctx))
2✔
357
        return versions, err
2✔
358
}
2✔
359

360
func (client *GitLabClient) getMergeRequestDiff(ctx context.Context, projectID string, pullRequestID int) ([]*gitlab.MergeRequestDiff, error) {
2✔
361
        mergeRequestChanges, _, err := client.glClient.MergeRequests.ListMergeRequestDiffs(projectID, pullRequestID, nil, gitlab.WithContext(ctx))
2✔
362
        return mergeRequestChanges, err
2✔
363
}
2✔
364

365
func (client *GitLabClient) addPullRequestReviewComment(ctx context.Context, projectID string, pullRequestID int, comment PullRequestComment, versions []*gitlab.MergeRequestDiffVersion, mergeRequestChanges []*gitlab.MergeRequestDiff) error {
2✔
366
        // Find the corresponding change in merge request
2✔
367
        var newPath, oldPath string
2✔
368
        var newLine int
2✔
369
        var diffFound bool
2✔
370

2✔
371
        for _, diff := range mergeRequestChanges {
6✔
372
                if diff.NewPath != comment.NewFilePath {
7✔
373
                        continue
3✔
374
                }
375

376
                diffFound = true
1✔
377
                newLine = comment.NewStartLine
1✔
378
                newPath = diff.NewPath
1✔
379

1✔
380
                // New files don't have old data
1✔
381
                if !diff.NewFile {
2✔
382
                        oldPath = diff.OldPath
1✔
383
                }
1✔
384
                break
1✔
385
        }
386

387
        // If no matching change is found, return an error
388
        if !diffFound {
3✔
389
                return fmt.Errorf("could not find changes to %s in the current merge request", comment.NewFilePath)
1✔
390
        }
1✔
391

392
        // Create a NotePosition for the comment
393
        latestVersion := versions[0]
1✔
394
        diffPosition := &gitlab.PositionOptions{
1✔
395
                StartSHA:     &latestVersion.StartCommitSHA,
1✔
396
                HeadSHA:      &latestVersion.HeadCommitSHA,
1✔
397
                BaseSHA:      &latestVersion.BaseCommitSHA,
1✔
398
                PositionType: vcsutils.PointerOf("text"),
1✔
399
                NewLine:      &newLine,
1✔
400
                NewPath:      &newPath,
1✔
401
                OldLine:      &newLine,
1✔
402
                OldPath:      &oldPath,
1✔
403
        }
1✔
404

1✔
405
        // The GitLab REST API for creating a merge request discussion has strange behavior:
1✔
406
        // If the API call is not constructed precisely according to these rules, it may fail with an unclear error.
1✔
407
        // In all cases, 'new_path' and 'new_line' parameters are required.
1✔
408
        // - When commenting on a new file, do not include 'old_path' and 'old_line' parameters.
1✔
409
        // - When commenting on an existing file that has changed in the diff, omit 'old_path' and 'old_line' parameters.
1✔
410
        // - When commenting on an existing file that hasn't changed in the diff, include 'old_path' and 'old_line' parameters.
1✔
411

1✔
412
        client.logger.Debug(fmt.Sprintf("Create merge request discussion sent. newPath: %v newLine: %v oldPath: %v, oldLine: %v",
1✔
413
                newPath, newLine, oldPath, newLine))
1✔
414
        // Attempt to create a merge request discussion thread
1✔
415
        _, _, err := client.createMergeRequestDiscussion(ctx, projectID, comment.Content, pullRequestID, diffPosition)
1✔
416

1✔
417
        // Retry without oldLine and oldPath if the GitLab API call fails
1✔
418
        if err != nil {
2✔
419
                diffPosition.OldLine = nil
1✔
420
                diffPosition.OldPath = nil
1✔
421
                client.logger.Debug(fmt.Sprintf("Create merge request discussion second attempt sent. newPath: %v newLine: %v oldPath: %v, oldLine: %v",
1✔
422
                        newPath, newLine, oldPath, newLine))
1✔
423
                _, _, err = client.createMergeRequestDiscussion(ctx, projectID, comment.Content, pullRequestID, diffPosition)
1✔
424
        }
1✔
425

426
        // If the comment creation still fails, return an error
427
        if err != nil {
1✔
428
                return fmt.Errorf("could not create a merge request discussion thread: %w", err)
×
429
        }
×
430

431
        return nil
1✔
432
}
433

434
func (client *GitLabClient) createMergeRequestDiscussion(ctx context.Context, projectID, content string, pullRequestID int, position *gitlab.PositionOptions) (*gitlab.Discussion, *gitlab.Response, error) {
2✔
435
        return client.glClient.Discussions.CreateMergeRequestDiscussion(projectID, pullRequestID, &gitlab.CreateMergeRequestDiscussionOptions{
2✔
436
                Body:     &content,
2✔
437
                Position: position,
2✔
438
        }, gitlab.WithContext(ctx))
2✔
439
}
2✔
440

441
// ListPullRequestReviewComments on GitLab
442
func (client *GitLabClient) ListPullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int) ([]CommentInfo, error) {
1✔
443
        // Validate parameters
1✔
444
        if err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "pullRequestID": strconv.Itoa(pullRequestID)}); err != nil {
1✔
445
                return nil, err
×
446
        }
×
447

448
        projectID := getProjectID(owner, repository)
1✔
449

1✔
450
        discussions, _, err := client.glClient.Discussions.ListMergeRequestDiscussions(projectID, pullRequestID, &gitlab.ListMergeRequestDiscussionsOptions{}, gitlab.WithContext(ctx))
1✔
451
        if err != nil {
1✔
452
                return nil, fmt.Errorf("failed fetching the list of merge requests discussions: %w", err)
×
453
        }
×
454

455
        var commentsInfo []CommentInfo
1✔
456
        for _, discussion := range discussions {
3✔
457
                commentsInfo = append(commentsInfo, mapGitLabNotesToCommentInfoList(discussion.Notes, discussion.ID)...)
2✔
458
        }
2✔
459

460
        return commentsInfo, nil
1✔
461
}
462

463
func (client *GitLabClient) ListPullRequestCommits(ctx context.Context, owner, repository string, pullRequestID int) ([]CommitInfo, error) {
1✔
464
        // Validate parameters
1✔
465
        err := validateParametersNotBlank(map[string]string{
1✔
466
                "owner":      owner,
1✔
467
                "repository": repository,
1✔
468
        })
1✔
469
        if err != nil {
1✔
NEW
470
                return nil, err
×
NEW
471
        }
×
472

473
        // Fetch the commits for the specified pull request
474
        commits, _, err := client.glClient.MergeRequests.GetMergeRequestCommits(getProjectID(owner, repository), pullRequestID, nil, gitlab.WithContext(ctx))
1✔
475
        if err != nil {
1✔
NEW
476
                return nil, err
×
NEW
477
        }
×
478

479
        // Map the retrieved commits to the CommitInfo structure
480
        var commitsInfo []CommitInfo
1✔
481
        for _, commit := range commits {
3✔
482
                commitInfo := mapGitLabCommitToCommitInfo(commit)
2✔
483
                commitsInfo = append(commitsInfo, commitInfo)
2✔
484
        }
2✔
485

486
        return commitsInfo, nil
1✔
487
}
488

489
// ListPullRequestComments on GitLab
490
func (client *GitLabClient) ListPullRequestComments(ctx context.Context, owner, repository string, pullRequestID int) ([]CommentInfo, error) {
1✔
491
        if err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "pullRequestID": strconv.Itoa(pullRequestID)}); err != nil {
1✔
492
                return nil, err
×
493
        }
×
494
        commentsList, _, err := client.glClient.Notes.ListMergeRequestNotes(getProjectID(owner, repository), pullRequestID, &gitlab.ListMergeRequestNotesOptions{},
1✔
495
                gitlab.WithContext(ctx))
1✔
496
        if err != nil {
1✔
497
                return []CommentInfo{}, err
×
498
        }
×
499
        return mapGitLabNotesToCommentInfoList(commentsList, ""), nil
1✔
500
}
501

502
// DeletePullRequestReviewComment on GitLab
503
func (client *GitLabClient) DeletePullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int, comments ...CommentInfo) error {
3✔
504
        if err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "pullRequestID": strconv.Itoa(pullRequestID)}); err != nil {
4✔
505
                return err
1✔
506
        }
1✔
507
        for _, comment := range comments {
5✔
508
                var commentID int64
3✔
509
                if err := validateParametersNotBlank(map[string]string{"commentID": strconv.FormatInt(commentID, 10), "discussionID": comment.ThreadID}); err != nil {
4✔
510
                        return err
1✔
511
                }
1✔
512
                if _, err := client.glClient.Discussions.DeleteMergeRequestDiscussionNote(getProjectID(owner, repository), pullRequestID, comment.ThreadID, int(commentID), gitlab.WithContext(ctx)); err != nil {
2✔
513
                        return fmt.Errorf("an error occurred while deleting pull request review comment: %w", err)
×
514
                }
×
515
        }
516
        return nil
1✔
517
}
518

519
// DeletePullRequestComment on GitLab
520
func (client *GitLabClient) DeletePullRequestComment(ctx context.Context, owner, repository string, pullRequestID, commentID int) error {
1✔
521
        if err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository}); err != nil {
1✔
522
                return err
×
523
        }
×
524
        if _, err := client.glClient.Notes.DeleteMergeRequestNote(getProjectID(owner, repository), pullRequestID, commentID, gitlab.WithContext(ctx)); err != nil {
1✔
525
                return fmt.Errorf("an error occurred while deleting pull request comment:\n%s", err.Error())
×
526
        }
×
527
        return nil
1✔
528
}
529

530
// GetLatestCommit on GitLab
531
func (client *GitLabClient) GetLatestCommit(ctx context.Context, owner, repository, branch string) (CommitInfo, error) {
7✔
532
        commits, err := client.GetCommits(ctx, owner, repository, branch)
7✔
533
        if err != nil {
12✔
534
                return CommitInfo{}, err
5✔
535
        }
5✔
536

537
        if len(commits) > 0 {
3✔
538
                return commits[0], nil
1✔
539
        }
1✔
540

541
        return CommitInfo{}, fmt.Errorf("no commits were returned for <%s/%s/%s>", owner, repository, branch)
1✔
542
}
543

544
// GetCommits on GitLab
545
func (client *GitLabClient) GetCommits(ctx context.Context, owner, repository, branch string) ([]CommitInfo, error) {
8✔
546
        err := validateParametersNotBlank(map[string]string{
8✔
547
                "owner":      owner,
8✔
548
                "repository": repository,
8✔
549
                "branch":     branch,
8✔
550
        })
8✔
551
        if err != nil {
12✔
552
                return nil, err
4✔
553
        }
4✔
554

555
        listOptions := &gitlab.ListCommitsOptions{
4✔
556
                RefName: &branch,
4✔
557
                ListOptions: gitlab.ListOptions{
4✔
558
                        Page:    1,
4✔
559
                        PerPage: vcsutils.NumberOfCommitsToFetch,
4✔
560
                },
4✔
561
        }
4✔
562
        return client.getCommitsWithQueryOptions(ctx, owner, repository, listOptions)
4✔
563
}
564

565
func (client *GitLabClient) GetCommitsWithQueryOptions(ctx context.Context, owner, repository string, listOptions GitCommitsQueryOptions) ([]CommitInfo, error) {
1✔
566
        err := validateParametersNotBlank(map[string]string{
1✔
567
                "owner":      owner,
1✔
568
                "repository": repository,
1✔
569
        })
1✔
570
        if err != nil {
1✔
571
                return nil, err
×
572
        }
×
573

574
        return client.getCommitsWithQueryOptions(ctx, owner, repository, convertToListCommitsOptions(listOptions))
1✔
575
}
576

577
func convertToListCommitsOptions(options GitCommitsQueryOptions) *gitlab.ListCommitsOptions {
1✔
578
        t := time.Now()
1✔
579
        return &gitlab.ListCommitsOptions{
1✔
580
                ListOptions: gitlab.ListOptions{
1✔
581
                        Page:    options.Page,
1✔
582
                        PerPage: options.PerPage,
1✔
583
                },
1✔
584
                Since: &options.Since,
1✔
585
                Until: &t,
1✔
586
        }
1✔
587
}
1✔
588

589
func (client *GitLabClient) getCommitsWithQueryOptions(ctx context.Context, owner, repository string, options *gitlab.ListCommitsOptions) ([]CommitInfo, error) {
5✔
590
        commits, _, err := client.glClient.Commits.ListCommits(getProjectID(owner, repository), options, gitlab.WithContext(ctx))
5✔
591
        if err != nil {
6✔
592
                return nil, err
1✔
593
        }
1✔
594

595
        var commitsInfo []CommitInfo
4✔
596
        for _, commit := range commits {
10✔
597
                commitInfo := mapGitLabCommitToCommitInfo(commit)
6✔
598
                commitsInfo = append(commitsInfo, commitInfo)
6✔
599
        }
6✔
600
        return commitsInfo, nil
4✔
601
}
602

603
// GetRepositoryInfo on GitLab
604
func (client *GitLabClient) GetRepositoryInfo(ctx context.Context, owner, repository string) (RepositoryInfo, error) {
5✔
605
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
5✔
606
        if err != nil {
8✔
607
                return RepositoryInfo{}, err
3✔
608
        }
3✔
609

610
        project, _, err := client.glClient.Projects.GetProject(getProjectID(owner, repository), nil, gitlab.WithContext(ctx))
2✔
611
        if err != nil {
2✔
612
                return RepositoryInfo{}, err
×
613
        }
×
614

615
        return RepositoryInfo{RepositoryVisibility: getGitLabProjectVisibility(project), CloneInfo: CloneInfo{HTTP: project.HTTPURLToRepo, SSH: project.SSHURLToRepo}}, nil
2✔
616
}
617

618
// GetCommitBySha on GitLab
619
func (client *GitLabClient) GetCommitBySha(ctx context.Context, owner, repository, sha string) (CommitInfo, error) {
6✔
620
        err := validateParametersNotBlank(map[string]string{
6✔
621
                "owner":      owner,
6✔
622
                "repository": repository,
6✔
623
                "sha":        sha,
6✔
624
        })
6✔
625
        if err != nil {
10✔
626
                return CommitInfo{}, err
4✔
627
        }
4✔
628

629
        commit, _, err := client.glClient.Commits.GetCommit(getProjectID(owner, repository), sha, nil, gitlab.WithContext(ctx))
2✔
630
        if err != nil {
3✔
631
                return CommitInfo{}, err
1✔
632
        }
1✔
633
        return mapGitLabCommitToCommitInfo(commit), nil
1✔
634
}
635

636
// CreateLabel on GitLab
637
func (client *GitLabClient) CreateLabel(ctx context.Context, owner, repository string, labelInfo LabelInfo) error {
5✔
638
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "LabelInfo.name": labelInfo.Name})
5✔
639
        if err != nil {
9✔
640
                return err
4✔
641
        }
4✔
642

643
        _, _, err = client.glClient.Labels.CreateLabel(getProjectID(owner, repository), &gitlab.CreateLabelOptions{
1✔
644
                Name:        &labelInfo.Name,
1✔
645
                Description: &labelInfo.Description,
1✔
646
                Color:       &labelInfo.Color,
1✔
647
        }, gitlab.WithContext(ctx))
1✔
648

1✔
649
        return err
1✔
650
}
651

652
// GetLabel on GitLub
653
func (client *GitLabClient) GetLabel(ctx context.Context, owner, repository, name string) (*LabelInfo, error) {
6✔
654
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "name": name})
6✔
655
        if err != nil {
10✔
656
                return nil, err
4✔
657
        }
4✔
658

659
        labels, _, err := client.glClient.Labels.ListLabels(getProjectID(owner, repository), &gitlab.ListLabelsOptions{}, gitlab.WithContext(ctx))
2✔
660
        if err != nil {
2✔
661
                return nil, err
×
662
        }
×
663

664
        for _, label := range labels {
4✔
665
                if label.Name == name {
3✔
666
                        return &LabelInfo{
1✔
667
                                Name:        label.Name,
1✔
668
                                Description: label.Description,
1✔
669
                                Color:       strings.TrimPrefix(label.Color, "#"),
1✔
670
                        }, err
1✔
671
                }
1✔
672
        }
673

674
        return nil, nil
1✔
675
}
676

677
// ListPullRequestLabels on GitLab
678
func (client *GitLabClient) ListPullRequestLabels(ctx context.Context, owner, repository string, pullRequestID int) ([]string, error) {
4✔
679
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
4✔
680
        if err != nil {
7✔
681
                return []string{}, err
3✔
682
        }
3✔
683
        mergeRequest, _, err := client.glClient.MergeRequests.GetMergeRequest(getProjectID(owner, repository), pullRequestID,
1✔
684
                &gitlab.GetMergeRequestsOptions{}, gitlab.WithContext(ctx))
1✔
685
        if err != nil {
1✔
686
                return []string{}, err
×
687
        }
×
688

689
        return mergeRequest.Labels, nil
1✔
690
}
691

692
// UnlabelPullRequest on GitLab
693
func (client *GitLabClient) UnlabelPullRequest(ctx context.Context, owner, repository, label string, pullRequestID int) error {
4✔
694
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
4✔
695
        if err != nil {
7✔
696
                return err
3✔
697
        }
3✔
698
        labels := gitlab.LabelOptions{label}
1✔
699
        _, _, err = client.glClient.MergeRequests.UpdateMergeRequest(getProjectID(owner, repository), pullRequestID, &gitlab.UpdateMergeRequestOptions{
1✔
700
                RemoveLabels: &labels,
1✔
701
        }, gitlab.WithContext(ctx))
1✔
702
        return err
1✔
703
}
704

705
// UploadCodeScanning on GitLab
706
func (client *GitLabClient) UploadCodeScanning(_ context.Context, _ string, _ string, _ string, _ string) (string, error) {
1✔
707
        return "", errGitLabCodeScanningNotSupported
1✔
708
}
1✔
709

710
// GetRepositoryEnvironmentInfo on GitLab
711
func (client *GitLabClient) GetRepositoryEnvironmentInfo(_ context.Context, _, _, _ string) (RepositoryEnvironmentInfo, error) {
1✔
712
        return RepositoryEnvironmentInfo{}, errGitLabGetRepoEnvironmentInfoNotSupported
1✔
713
}
1✔
714

715
// DownloadFileFromRepo on GitLab
716
func (client *GitLabClient) DownloadFileFromRepo(_ context.Context, owner, repository, branch, path string) ([]byte, int, error) {
1✔
717
        file, glResponse, err := client.glClient.RepositoryFiles.GetFile(getProjectID(owner, repository), path, &gitlab.GetFileOptions{Ref: &branch})
1✔
718
        var statusCode int
1✔
719
        if glResponse != nil && glResponse.Response != nil {
2✔
720
                statusCode = glResponse.Response.StatusCode
1✔
721
        }
1✔
722
        if err != nil {
1✔
723
                return nil, statusCode, err
×
724
        }
×
725
        if statusCode != http.StatusOK {
1✔
726
                return nil, statusCode, fmt.Errorf("expected %d status code while received %d status code", http.StatusOK, glResponse.StatusCode)
×
727
        }
×
728
        var content []byte
1✔
729
        if file != nil {
2✔
730
                content, err = base64.StdEncoding.DecodeString(file.Content)
1✔
731
        }
1✔
732
        return content, statusCode, err
1✔
733
}
734

735
func (client *GitLabClient) GetModifiedFiles(_ context.Context, owner, repository, refBefore, refAfter string) ([]string, error) {
6✔
736
        if err := validateParametersNotBlank(map[string]string{
6✔
737
                "owner":      owner,
6✔
738
                "repository": repository,
6✔
739
                "refBefore":  refBefore,
6✔
740
                "refAfter":   refAfter,
6✔
741
        }); err != nil {
10✔
742
                return nil, err
4✔
743
        }
4✔
744

745
        // No pagination is needed according to the official documentation at
746
        // https://docs.gitlab.com/ce/api/repositories.html#compare-branches-tags-or-commits
747
        compare, _, err := client.glClient.Repositories.Compare(
2✔
748
                getProjectID(owner, repository),
2✔
749
                &gitlab.CompareOptions{From: &refBefore, To: &refAfter},
2✔
750
        )
2✔
751
        if err != nil {
3✔
752
                return nil, err
1✔
753
        }
1✔
754

755
        fileNamesSet := datastructures.MakeSet[string]()
1✔
756
        for _, diff := range compare.Diffs {
4✔
757
                fileNamesSet.Add(diff.NewPath)
3✔
758
                fileNamesSet.Add(diff.OldPath)
3✔
759
        }
3✔
760
        _ = fileNamesSet.Remove("") // Make sure there are no blank filepath.
1✔
761
        fileNamesList := fileNamesSet.ToSlice()
1✔
762
        sort.Strings(fileNamesList)
1✔
763
        return fileNamesList, nil
1✔
764
}
765

766
func (client *GitLabClient) ListPullRequestReviews(ctx context.Context, owner, repository string, pullRequestID int) ([]PullRequestReviewDetails, error) {
1✔
767
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
1✔
768
        if err != nil {
1✔
769
                return nil, err
×
770
        }
×
771

772
        var prNotes []*gitlab.Note
1✔
773
        prNotes, _, err = client.glClient.Notes.ListMergeRequestNotes(owner+"/"+repository, pullRequestID, nil)
1✔
774
        if err != nil {
1✔
775
                return nil, err
×
776
        }
×
777

778
        var reviewInfos []PullRequestReviewDetails
1✔
779
        for _, review := range prNotes {
3✔
780
                reviewInfos = append(reviewInfos, PullRequestReviewDetails{
2✔
781
                        ID:          int64(review.ID),
2✔
782
                        Reviewer:    review.Author.Username,
2✔
783
                        Body:        review.Body,
2✔
784
                        SubmittedAt: review.CreatedAt.Format(time.RFC3339),
2✔
785
                        CommitID:    review.CommitID,
2✔
786
                })
2✔
787
        }
2✔
788

789
        return reviewInfos, nil
1✔
790
}
791

792
func (client *GitLabClient) ListPullRequestsAssociatedWithCommit(ctx context.Context, owner, repository string, commitSHA string) ([]PullRequestInfo, error) {
1✔
793
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
1✔
794
        if err != nil {
1✔
795
                return nil, err
×
796
        }
×
797

798
        var mergeRequests []*gitlab.MergeRequest
1✔
799
        mergeRequests, _, err = client.glClient.Commits.ListMergeRequestsByCommit(owner+"/"+repository, commitSHA, nil)
1✔
800
        if err != nil {
1✔
801
                return nil, err
×
802
        }
×
803
        return client.mapGitLabMergeRequestToPullRequestInfoList(mergeRequests, owner, repository, true)
1✔
804
}
805

806
func getProjectID(owner, project string) string {
43✔
807
        return fmt.Sprintf("%s/%s", owner, project)
43✔
808
}
43✔
809

810
func createProjectHook(branch string, payloadURL string, webhookEvents ...vcsutils.WebhookEvent) *gitlab.ProjectHook {
2✔
811
        options := &gitlab.ProjectHook{URL: payloadURL}
2✔
812
        for _, webhookEvent := range webhookEvents {
11✔
813
                switch webhookEvent {
9✔
814
                case vcsutils.PrOpened, vcsutils.PrEdited, vcsutils.PrRejected, vcsutils.PrMerged:
6✔
815
                        options.MergeRequestsEvents = true
6✔
816
                case vcsutils.Push:
1✔
817
                        options.PushEvents = true
1✔
818
                        options.PushEventsBranchFilter = branch
1✔
819
                case vcsutils.TagPushed, vcsutils.TagRemoved:
2✔
820
                        options.TagPushEvents = true
2✔
821
                }
822
        }
823
        return options
2✔
824
}
825

826
func getGitLabProjectVisibility(project *gitlab.Project) RepositoryVisibility {
5✔
827
        switch project.Visibility {
5✔
828
        case gitlab.PublicVisibility:
1✔
829
                return Public
1✔
830
        case gitlab.InternalVisibility:
1✔
831
                return Internal
1✔
832
        default:
3✔
833
                return Private
3✔
834
        }
835
}
836

837
func getGitLabCommitState(commitState CommitStatus) string {
6✔
838
        switch commitState {
6✔
839
        case Pass:
1✔
840
                return "success"
1✔
841
        case Fail:
1✔
842
                return "failed"
1✔
843
        case Error:
1✔
844
                return "failed"
1✔
845
        case InProgress:
2✔
846
                return "running"
2✔
847
        }
848
        return ""
1✔
849
}
850

851
func mapGitLabCommitToCommitInfo(commit *gitlab.Commit) CommitInfo {
9✔
852
        return CommitInfo{
9✔
853
                Hash:          commit.ID,
9✔
854
                AuthorName:    commit.AuthorName,
9✔
855
                CommitterName: commit.CommitterName,
9✔
856
                Url:           commit.WebURL,
9✔
857
                Timestamp:     commit.CommittedDate.UTC().Unix(),
9✔
858
                Message:       commit.Message,
9✔
859
                ParentHashes:  commit.ParentIDs,
9✔
860
                AuthorEmail:   commit.AuthorEmail,
9✔
861
        }
9✔
862
}
9✔
863

864
func mapGitLabNotesToCommentInfoList(notes []*gitlab.Note, discussionId string) (res []CommentInfo) {
3✔
865
        for _, note := range notes {
8✔
866
                res = append(res, CommentInfo{
5✔
867
                        ID:       int64(note.ID),
5✔
868
                        ThreadID: discussionId,
5✔
869
                        Content:  note.Body,
5✔
870
                        Created:  *note.CreatedAt,
5✔
871
                })
5✔
872
        }
5✔
873
        return
3✔
874
}
875

876
func (client *GitLabClient) mapGitLabMergeRequestToPullRequestInfoList(mergeRequests []*gitlab.MergeRequest, owner, repository string, withBody bool) (res []PullRequestInfo, err error) {
3✔
877
        for _, mergeRequest := range mergeRequests {
6✔
878
                var mergeRequestInfo PullRequestInfo
3✔
879
                if mergeRequestInfo, err = client.mapGitLabMergeRequestToPullRequestInfo(mergeRequest, withBody, owner, repository); err != nil {
3✔
880
                        return
×
881
                }
×
882
                res = append(res, mergeRequestInfo)
3✔
883
        }
884
        return
3✔
885
}
886

887
func (client *GitLabClient) mapGitLabMergeRequestToPullRequestInfo(mergeRequest *gitlab.MergeRequest, withBody bool, owner, repository string) (PullRequestInfo, error) {
4✔
888
        var body string
4✔
889
        if withBody {
6✔
890
                body = mergeRequest.Description
2✔
891
        }
2✔
892
        sourceOwner := owner
4✔
893
        var err error
4✔
894
        if mergeRequest.SourceProjectID != mergeRequest.TargetProjectID {
4✔
895
                if sourceOwner, err = client.getProjectOwnerByID(mergeRequest.SourceProjectID); err != nil {
×
896
                        return PullRequestInfo{}, err
×
897
                }
×
898
        }
899

900
        return PullRequestInfo{
4✔
901
                ID:     int64(mergeRequest.IID),
4✔
902
                Title:  mergeRequest.Title,
4✔
903
                Body:   body,
4✔
904
                Author: mergeRequest.Author.Username,
4✔
905
                Source: BranchInfo{
4✔
906
                        Name:       mergeRequest.SourceBranch,
4✔
907
                        Repository: repository,
4✔
908
                        Owner:      sourceOwner,
4✔
909
                },
4✔
910
                URL: mergeRequest.WebURL,
4✔
911
                Target: BranchInfo{
4✔
912
                        Name:       mergeRequest.TargetBranch,
4✔
913
                        Repository: repository,
4✔
914
                        Owner:      owner,
4✔
915
                },
4✔
916
        }, nil
4✔
917
}
918

919
func (client *GitLabClient) getProjectOwnerByID(projectID int) (string, error) {
2✔
920
        project, glResponse, err := client.glClient.Projects.GetProject(projectID, &gitlab.GetProjectOptions{})
2✔
921
        if err != nil {
2✔
922
                return "", err
×
923
        }
×
924
        if glResponse != nil {
4✔
925
                if err = vcsutils.CheckResponseStatusWithBody(glResponse.Response, http.StatusOK); err != nil {
2✔
926
                        return "", err
×
927
                }
×
928
        }
929
        if project.Namespace == nil {
3✔
930
                return "", fmt.Errorf("could not fetch the name of the project owner. Project ID: %d", projectID)
1✔
931
        }
1✔
932
        return project.Namespace.Name, nil
1✔
933
}
934

935
func mapGitLabPullRequestState(state *vcsutils.PullRequestState) *string {
4✔
936
        var stateStringValue string
4✔
937
        switch *state {
4✔
938
        case vcsutils.Open:
2✔
939
                stateStringValue = "reopen"
2✔
940
        case vcsutils.Closed:
1✔
941
                stateStringValue = "close"
1✔
942
        default:
1✔
943
                return nil
1✔
944
        }
945
        return &stateStringValue
3✔
946
}
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