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

jfrog / froggit-go / 13133924118

04 Feb 2025 10:58AM UTC coverage: 86.623% (-1.1%) from 87.742%
13133924118

Pull #145

github

EyalDelarea
fix space
Pull Request #145: List pull requests associated with a commit

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

324 existing lines in 6 files now uncovered.

4073 of 4702 relevant lines covered (86.62%)

6.65 hits per line

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

88.58
/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

NEW
UNCOV
26
func (client *GitLabClient) ListPullRequestReviews(ctx context.Context, owner, repository string, pullRequestID int) ([]PullRequestReviewDetails, error) {
×
NEW
UNCOV
27
        // TODO implement me
×
NEW
UNCOV
28
        panic("implement me")
×
29
}
30

NEW
UNCOV
31
func (client *GitLabClient) ListPullRequestsAssociatedWithCommit(ctx context.Context, owner, repository string, commitSHA string) ([]PullRequestInfo, error) {
×
NEW
UNCOV
32
        // TODO implement me
×
NEW
UNCOV
33
        panic("implement me")
×
34
}
35

36
// NewGitLabClient create a new GitLabClient
37
func NewGitLabClient(vcsInfo VcsInfo, logger vcsutils.Log) (*GitLabClient, error) {
80✔
38
        var client *gitlab.Client
80✔
39
        var err error
80✔
40
        if vcsInfo.APIEndpoint != "" {
126✔
41
                client, err = gitlab.NewClient(vcsInfo.Token, gitlab.WithBaseURL(vcsInfo.APIEndpoint))
46✔
42
        } else {
80✔
43
                client, err = gitlab.NewClient(vcsInfo.Token)
34✔
44
        }
34✔
45
        if err != nil {
81✔
46
                return nil, err
1✔
47
        }
1✔
48

49
        return &GitLabClient{
79✔
50
                glClient: client,
79✔
51
                vcsInfo:  vcsInfo,
79✔
52
                logger:   logger,
79✔
53
        }, nil
79✔
54
}
55

56
// TestConnection on GitLab
57
func (client *GitLabClient) TestConnection(ctx context.Context) error {
3✔
58
        _, _, err := client.glClient.Projects.ListProjects(nil, gitlab.WithContext(ctx))
3✔
59
        return err
3✔
60
}
3✔
61

62
// ListRepositories on GitLab
63
func (client *GitLabClient) ListRepositories(ctx context.Context) (map[string][]string, error) {
1✔
64
        simple := true
1✔
65
        results := make(map[string][]string)
1✔
66
        membership := true
1✔
67
        for pageID := 1; ; pageID++ {
3✔
68
                options := &gitlab.ListProjectsOptions{ListOptions: gitlab.ListOptions{Page: pageID}, Simple: &simple, Membership: &membership}
2✔
69
                projects, response, err := client.glClient.Projects.ListProjects(options, gitlab.WithContext(ctx))
2✔
70
                if err != nil {
2✔
UNCOV
71
                        return nil, err
×
UNCOV
72
                }
×
73
                for _, project := range projects {
27✔
74
                        owner := project.Namespace.Path
25✔
75
                        results[owner] = append(results[owner], project.Path)
25✔
76
                }
25✔
77
                if pageID >= response.TotalPages {
3✔
78
                        break
1✔
79
                }
80
        }
81
        return results, nil
1✔
82
}
83

84
// ListBranches on GitLab
85
func (client *GitLabClient) ListBranches(ctx context.Context, owner, repository string) ([]string, error) {
1✔
86
        branches, _, err := client.glClient.Branches.ListBranches(getProjectID(owner, repository), nil,
1✔
87
                gitlab.WithContext(ctx))
1✔
88
        if err != nil {
1✔
UNCOV
89
                return nil, err
×
UNCOV
90
        }
×
91

92
        results := make([]string, 0, len(branches))
1✔
93
        for _, branch := range branches {
3✔
94
                results = append(results, branch.Name)
2✔
95
        }
2✔
96
        return results, nil
1✔
97
}
98

99
// AddSshKeyToRepository on GitLab
100
func (client *GitLabClient) AddSshKeyToRepository(ctx context.Context, owner, repository, keyName, publicKey string, permission Permission) error {
7✔
101
        err := validateParametersNotBlank(map[string]string{
7✔
102
                "owner":      owner,
7✔
103
                "repository": repository,
7✔
104
                "key name":   keyName,
7✔
105
                "public key": publicKey,
7✔
106
        })
7✔
107
        if err != nil {
12✔
108
                return err
5✔
109
        }
5✔
110

111
        canPush := false
2✔
112
        if permission == ReadWrite {
3✔
113
                canPush = true
1✔
114
        }
1✔
115
        options := &gitlab.AddDeployKeyOptions{
2✔
116
                Title:   &keyName,
2✔
117
                Key:     &publicKey,
2✔
118
                CanPush: &canPush,
2✔
119
        }
2✔
120
        _, _, err = client.glClient.DeployKeys.AddDeployKey(getProjectID(owner, repository), options, gitlab.WithContext(ctx))
2✔
121
        return err
2✔
122
}
123

124
// CreateWebhook on GitLab
125
func (client *GitLabClient) CreateWebhook(ctx context.Context, owner, repository, branch, payloadURL string,
126
        webhookEvents ...vcsutils.WebhookEvent) (string, string, error) {
1✔
127
        token := vcsutils.CreateToken()
1✔
128
        projectHook := createProjectHook(branch, payloadURL, webhookEvents...)
1✔
129
        options := &gitlab.AddProjectHookOptions{
1✔
130
                Token:                  &token,
1✔
131
                URL:                    &projectHook.URL,
1✔
132
                MergeRequestsEvents:    &projectHook.MergeRequestsEvents,
1✔
133
                PushEvents:             &projectHook.PushEvents,
1✔
134
                PushEventsBranchFilter: &projectHook.PushEventsBranchFilter,
1✔
135
                TagPushEvents:          &projectHook.TagPushEvents,
1✔
136
        }
1✔
137
        response, _, err := client.glClient.Projects.AddProjectHook(getProjectID(owner, repository), options,
1✔
138
                gitlab.WithContext(ctx))
1✔
139
        if err != nil {
1✔
UNCOV
140
                return "", "", err
×
UNCOV
141
        }
×
142
        return strconv.Itoa(response.ID), token, nil
1✔
143
}
144

145
// UpdateWebhook on GitLab
146
func (client *GitLabClient) UpdateWebhook(ctx context.Context, owner, repository, branch, payloadURL, token,
147
        webhookID string, webhookEvents ...vcsutils.WebhookEvent) error {
1✔
148
        projectHook := createProjectHook(branch, payloadURL, webhookEvents...)
1✔
149
        options := &gitlab.EditProjectHookOptions{
1✔
150
                Token:                  &token,
1✔
151
                URL:                    &projectHook.URL,
1✔
152
                MergeRequestsEvents:    &projectHook.MergeRequestsEvents,
1✔
153
                PushEvents:             &projectHook.PushEvents,
1✔
154
                PushEventsBranchFilter: &projectHook.PushEventsBranchFilter,
1✔
155
                TagPushEvents:          &projectHook.TagPushEvents,
1✔
156
        }
1✔
157
        intWebhook, err := strconv.Atoi(webhookID)
1✔
158
        if err != nil {
1✔
UNCOV
159
                return err
×
160
        }
×
161
        _, _, err = client.glClient.Projects.EditProjectHook(getProjectID(owner, repository), intWebhook, options,
1✔
162
                gitlab.WithContext(ctx))
1✔
163
        return err
1✔
164
}
165

166
// DeleteWebhook on GitLab
167
func (client *GitLabClient) DeleteWebhook(ctx context.Context, owner, repository, webhookID string) error {
1✔
168
        intWebhook, err := strconv.Atoi(webhookID)
1✔
169
        if err != nil {
1✔
UNCOV
170
                return err
×
UNCOV
171
        }
×
172
        _, err = client.glClient.Projects.DeleteProjectHook(getProjectID(owner, repository), intWebhook,
1✔
173
                gitlab.WithContext(ctx))
1✔
174
        return err
1✔
175
}
176

177
// SetCommitStatus on GitLab
178
func (client *GitLabClient) SetCommitStatus(ctx context.Context, commitStatus CommitStatus, owner, repository, ref,
179
        title, description, detailsURL string) error {
1✔
180
        options := &gitlab.SetCommitStatusOptions{
1✔
181
                State:       gitlab.BuildStateValue(getGitLabCommitState(commitStatus)),
1✔
182
                Ref:         &ref,
1✔
183
                Name:        &title,
1✔
184
                Description: &description,
1✔
185
                TargetURL:   &detailsURL,
1✔
186
        }
1✔
187
        _, _, err := client.glClient.Commits.SetCommitStatus(getProjectID(owner, repository), ref, options,
1✔
188
                gitlab.WithContext(ctx))
1✔
189
        return err
1✔
190
}
1✔
191

192
// GetCommitStatuses on GitLab
193
func (client *GitLabClient) GetCommitStatuses(ctx context.Context, _, repository, ref string) (status []CommitStatusInfo, err error) {
3✔
194
        statuses, _, err := client.glClient.Commits.GetCommitStatuses(repository, ref, nil, gitlab.WithContext(ctx))
3✔
195
        if err != nil {
4✔
196
                return nil, err
1✔
197
        }
1✔
198
        results := make([]CommitStatusInfo, 0)
2✔
199
        for _, singleStatus := range statuses {
5✔
200
                results = append(results, CommitStatusInfo{
3✔
201
                        State:         commitStatusAsStringToStatus(singleStatus.Status),
3✔
202
                        Description:   singleStatus.Description,
3✔
203
                        DetailsUrl:    singleStatus.TargetURL,
3✔
204
                        Creator:       singleStatus.Author.Name,
3✔
205
                        LastUpdatedAt: extractTimeWithFallback(singleStatus.FinishedAt),
3✔
206
                        CreatedAt:     extractTimeWithFallback(singleStatus.CreatedAt),
3✔
207
                })
3✔
208
        }
3✔
209
        return results, nil
2✔
210
}
211

212
// DownloadRepository on GitLab
213
func (client *GitLabClient) DownloadRepository(ctx context.Context, owner, repository, branch, localPath string) error {
1✔
214
        format := "tar.gz"
1✔
215
        options := &gitlab.ArchiveOptions{
1✔
216
                Format: &format,
1✔
217
                SHA:    &branch,
1✔
218
        }
1✔
219
        response, _, err := client.glClient.Repositories.Archive(getProjectID(owner, repository), options,
1✔
220
                gitlab.WithContext(ctx))
1✔
221
        if err != nil {
1✔
222
                return err
×
223
        }
×
224
        client.logger.Info(repository, vcsutils.SuccessfulRepoDownload)
1✔
225
        err = vcsutils.Untar(localPath, bytes.NewReader(response), true)
1✔
226
        if err != nil {
1✔
UNCOV
227
                return err
×
UNCOV
228
        }
×
229

230
        repositoryInfo, err := client.GetRepositoryInfo(ctx, owner, repository)
1✔
231
        if err != nil {
1✔
UNCOV
232
                return err
×
233
        }
×
234

235
        client.logger.Info(vcsutils.SuccessfulRepoExtraction)
1✔
236
        return vcsutils.CreateDotGitFolderWithRemote(localPath, vcsutils.RemoteName, repositoryInfo.CloneInfo.HTTP)
1✔
237
}
238

UNCOV
239
func (client *GitLabClient) GetPullRequestCommentSizeLimit() int {
×
UNCOV
240
        return gitlabMergeRequestCommentSizeLimit
×
UNCOV
241
}
×
242

UNCOV
243
func (client *GitLabClient) GetPullRequestDetailsSizeLimit() int {
×
UNCOV
244
        return gitlabMergeRequestDetailsSizeLimit
×
UNCOV
245
}
×
246

247
// CreatePullRequest on GitLab
248
func (client *GitLabClient) CreatePullRequest(ctx context.Context, owner, repository, sourceBranch, targetBranch,
249
        title, description string) error {
1✔
250
        options := &gitlab.CreateMergeRequestOptions{
1✔
251
                Title:        &title,
1✔
252
                Description:  &description,
1✔
253
                SourceBranch: &sourceBranch,
1✔
254
                TargetBranch: &targetBranch,
1✔
255
        }
1✔
256
        client.logger.Debug("creating new merge request:", title)
1✔
257
        _, _, err := client.glClient.MergeRequests.CreateMergeRequest(getProjectID(owner, repository), options,
1✔
258
                gitlab.WithContext(ctx))
1✔
259
        return err
1✔
260
}
1✔
261

262
// UpdatePullRequest on GitLab
263
func (client *GitLabClient) UpdatePullRequest(ctx context.Context, owner, repository, title, body, targetBranchName string, prId int, state vcsutils.PullRequestState) error {
4✔
264
        options := &gitlab.UpdateMergeRequestOptions{
4✔
265
                Title:        &title,
4✔
266
                Description:  &body,
4✔
267
                TargetBranch: &targetBranchName,
4✔
268
                StateEvent:   mapGitLabPullRequestState(&state),
4✔
269
        }
4✔
270
        client.logger.Debug("updating details of merge request ID:", prId)
4✔
271
        _, _, err := client.glClient.MergeRequests.UpdateMergeRequest(getProjectID(owner, repository), prId, options, gitlab.WithContext(ctx))
4✔
272
        return err
4✔
273
}
4✔
274

275
// ListOpenPullRequestsWithBody on GitLab
276
func (client *GitLabClient) ListOpenPullRequestsWithBody(ctx context.Context, owner, repository string) ([]PullRequestInfo, error) {
1✔
277
        return client.getOpenPullRequests(ctx, owner, repository, true)
1✔
278
}
1✔
279

280
// ListOpenPullRequests on GitLab
281
func (client *GitLabClient) ListOpenPullRequests(ctx context.Context, owner, repository string) ([]PullRequestInfo, error) {
1✔
282
        return client.getOpenPullRequests(ctx, owner, repository, false)
1✔
283
}
1✔
284

285
func (client *GitLabClient) getOpenPullRequests(ctx context.Context, owner, repository string, withBody bool) ([]PullRequestInfo, error) {
2✔
286
        openState := "opened"
2✔
287
        allScope := "all"
2✔
288
        options := &gitlab.ListProjectMergeRequestsOptions{
2✔
289
                State: &openState,
2✔
290
                Scope: &allScope,
2✔
291
        }
2✔
292
        mergeRequests, _, err := client.glClient.MergeRequests.ListProjectMergeRequests(getProjectID(owner, repository), options, gitlab.WithContext(ctx))
2✔
293
        if err != nil {
2✔
UNCOV
294
                return []PullRequestInfo{}, err
×
UNCOV
295
        }
×
296
        return client.mapGitLabMergeRequestToPullRequestInfoList(mergeRequests, owner, repository, withBody)
2✔
297
}
298

299
// GetPullRequestInfoById on GitLab
300
func (client *GitLabClient) GetPullRequestByID(_ context.Context, owner, repository string, pullRequestId int) (pullRequestInfo PullRequestInfo, err error) {
2✔
301
        client.logger.Debug("fetching merge requests by ID in", repository)
2✔
302
        mergeRequest, glResponse, err := client.glClient.MergeRequests.GetMergeRequest(getProjectID(owner, repository), pullRequestId, nil)
2✔
303
        if err != nil {
3✔
304
                return PullRequestInfo{}, err
1✔
305
        }
1✔
306
        if glResponse != nil {
2✔
307
                if err = vcsutils.CheckResponseStatusWithBody(glResponse.Response, http.StatusOK); err != nil {
1✔
UNCOV
308
                        return PullRequestInfo{}, err
×
UNCOV
309
                }
×
310
        }
311
        pullRequestInfo, err = client.mapGitLabMergeRequestToPullRequestInfo(mergeRequest, false, owner, repository)
1✔
312
        return
1✔
313
}
314

315
// AddPullRequestComment on GitLab
316
func (client *GitLabClient) AddPullRequestComment(ctx context.Context, owner, repository, content string, pullRequestID int) error {
5✔
317
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "content": content})
5✔
318
        if err != nil {
9✔
319
                return err
4✔
320
        }
4✔
321
        options := &gitlab.CreateMergeRequestNoteOptions{
1✔
322
                Body: &content,
1✔
323
        }
1✔
324
        _, _, err = client.glClient.Notes.CreateMergeRequestNote(getProjectID(owner, repository), pullRequestID, options,
1✔
325
                gitlab.WithContext(ctx))
1✔
326

1✔
327
        return err
1✔
328
}
329

330
// AddPullRequestReviewComments adds comments to a pull request on GitLab.
331
func (client *GitLabClient) AddPullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int, comments ...PullRequestComment) error {
2✔
332
        // Validate parameters
2✔
333
        if err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository}); err != nil {
2✔
UNCOV
334
                return err
×
UNCOV
335
        }
×
336

337
        // Check if comments are provided
338
        if len(comments) == 0 {
2✔
UNCOV
339
                return errors.New("could not add merge request review comments, no comments provided")
×
UNCOV
340
        }
×
341

342
        projectID := getProjectID(owner, repository)
2✔
343

2✔
344
        // Get merge request diff versions
2✔
345
        versions, err := client.getMergeRequestDiffVersions(ctx, projectID, pullRequestID)
2✔
346
        if err != nil {
2✔
UNCOV
347
                return fmt.Errorf("could not get merge request diff versions: %w", err)
×
UNCOV
348
        }
×
349

350
        // Get merge request details
351
        mergeRequestChanges, err := client.getMergeRequestDiff(ctx, projectID, pullRequestID)
2✔
352
        if err != nil {
2✔
UNCOV
353
                return fmt.Errorf("could not get merge request changes: %w", err)
×
UNCOV
354
        }
×
355

356
        for _, comment := range comments {
4✔
357
                if err = client.addPullRequestReviewComment(ctx, projectID, pullRequestID, comment, versions, mergeRequestChanges); err != nil {
3✔
358
                        return err
1✔
359
                }
1✔
360
        }
361

362
        return nil
1✔
363
}
364

365
func (client *GitLabClient) getMergeRequestDiffVersions(ctx context.Context, projectID string, pullRequestID int) ([]*gitlab.MergeRequestDiffVersion, error) {
2✔
366
        versions, _, err := client.glClient.MergeRequests.GetMergeRequestDiffVersions(projectID, pullRequestID, &gitlab.GetMergeRequestDiffVersionsOptions{}, gitlab.WithContext(ctx))
2✔
367
        return versions, err
2✔
368
}
2✔
369

370
func (client *GitLabClient) getMergeRequestDiff(ctx context.Context, projectID string, pullRequestID int) ([]*gitlab.MergeRequestDiff, error) {
2✔
371
        mergeRequestChanges, _, err := client.glClient.MergeRequests.ListMergeRequestDiffs(projectID, pullRequestID, nil, gitlab.WithContext(ctx))
2✔
372
        return mergeRequestChanges, err
2✔
373
}
2✔
374

375
func (client *GitLabClient) addPullRequestReviewComment(ctx context.Context, projectID string, pullRequestID int, comment PullRequestComment, versions []*gitlab.MergeRequestDiffVersion, mergeRequestChanges []*gitlab.MergeRequestDiff) error {
2✔
376
        // Find the corresponding change in merge request
2✔
377
        var newPath, oldPath string
2✔
378
        var newLine int
2✔
379
        var diffFound bool
2✔
380

2✔
381
        for _, diff := range mergeRequestChanges {
6✔
382
                if diff.NewPath != comment.NewFilePath {
7✔
383
                        continue
3✔
384
                }
385

386
                diffFound = true
1✔
387
                newLine = comment.NewStartLine
1✔
388
                newPath = diff.NewPath
1✔
389

1✔
390
                // New files don't have old data
1✔
391
                if !diff.NewFile {
2✔
392
                        oldPath = diff.OldPath
1✔
393
                }
1✔
394
                break
1✔
395
        }
396

397
        // If no matching change is found, return an error
398
        if !diffFound {
3✔
399
                return fmt.Errorf("could not find changes to %s in the current merge request", comment.NewFilePath)
1✔
400
        }
1✔
401

402
        // Create a NotePosition for the comment
403
        latestVersion := versions[0]
1✔
404
        diffPosition := &gitlab.PositionOptions{
1✔
405
                StartSHA:     &latestVersion.StartCommitSHA,
1✔
406
                HeadSHA:      &latestVersion.HeadCommitSHA,
1✔
407
                BaseSHA:      &latestVersion.BaseCommitSHA,
1✔
408
                PositionType: vcsutils.PointerOf("text"),
1✔
409
                NewLine:      &newLine,
1✔
410
                NewPath:      &newPath,
1✔
411
                OldLine:      &newLine,
1✔
412
                OldPath:      &oldPath,
1✔
413
        }
1✔
414

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

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

1✔
427
        // Retry without oldLine and oldPath if the GitLab API call fails
1✔
428
        if err != nil {
2✔
429
                diffPosition.OldLine = nil
1✔
430
                diffPosition.OldPath = nil
1✔
431
                client.logger.Debug(fmt.Sprintf("Create merge request discussion second attempt sent. newPath: %v newLine: %v oldPath: %v, oldLine: %v",
1✔
432
                        newPath, newLine, oldPath, newLine))
1✔
433
                _, _, err = client.createMergeRequestDiscussion(ctx, projectID, comment.Content, pullRequestID, diffPosition)
1✔
434
        }
1✔
435

436
        // If the comment creation still fails, return an error
437
        if err != nil {
1✔
UNCOV
438
                return fmt.Errorf("could not create a merge request discussion thread: %w", err)
×
UNCOV
439
        }
×
440

441
        return nil
1✔
442
}
443

444
func (client *GitLabClient) createMergeRequestDiscussion(ctx context.Context, projectID, content string, pullRequestID int, position *gitlab.PositionOptions) (*gitlab.Discussion, *gitlab.Response, error) {
2✔
445
        return client.glClient.Discussions.CreateMergeRequestDiscussion(projectID, pullRequestID, &gitlab.CreateMergeRequestDiscussionOptions{
2✔
446
                Body:     &content,
2✔
447
                Position: position,
2✔
448
        }, gitlab.WithContext(ctx))
2✔
449
}
2✔
450

451
// ListPullRequestReviewComments on GitLab
452
func (client *GitLabClient) ListPullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int) ([]CommentInfo, error) {
1✔
453
        // Validate parameters
1✔
454
        if err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "pullRequestID": strconv.Itoa(pullRequestID)}); err != nil {
1✔
UNCOV
455
                return nil, err
×
UNCOV
456
        }
×
457

458
        projectID := getProjectID(owner, repository)
1✔
459

1✔
460
        discussions, _, err := client.glClient.Discussions.ListMergeRequestDiscussions(projectID, pullRequestID, &gitlab.ListMergeRequestDiscussionsOptions{}, gitlab.WithContext(ctx))
1✔
461
        if err != nil {
1✔
UNCOV
462
                return nil, fmt.Errorf("failed fetching the list of merge requests discussions: %w", err)
×
UNCOV
463
        }
×
464

465
        var commentsInfo []CommentInfo
1✔
466
        for _, discussion := range discussions {
3✔
467
                commentsInfo = append(commentsInfo, mapGitLabNotesToCommentInfoList(discussion.Notes, discussion.ID)...)
2✔
468
        }
2✔
469

470
        return commentsInfo, nil
1✔
471
}
472

473
// ListPullRequestComments on GitLab
474
func (client *GitLabClient) ListPullRequestComments(ctx context.Context, owner, repository string, pullRequestID int) ([]CommentInfo, error) {
1✔
475
        if err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "pullRequestID": strconv.Itoa(pullRequestID)}); err != nil {
1✔
UNCOV
476
                return nil, err
×
UNCOV
477
        }
×
478
        commentsList, _, err := client.glClient.Notes.ListMergeRequestNotes(getProjectID(owner, repository), pullRequestID, &gitlab.ListMergeRequestNotesOptions{},
1✔
479
                gitlab.WithContext(ctx))
1✔
480
        if err != nil {
1✔
UNCOV
481
                return []CommentInfo{}, err
×
UNCOV
482
        }
×
483
        return mapGitLabNotesToCommentInfoList(commentsList, ""), nil
1✔
484
}
485

486
// DeletePullRequestReviewComment on GitLab
487
func (client *GitLabClient) DeletePullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int, comments ...CommentInfo) error {
3✔
488
        if err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "pullRequestID": strconv.Itoa(pullRequestID)}); err != nil {
4✔
489
                return err
1✔
490
        }
1✔
491
        for _, comment := range comments {
5✔
492
                var commentID int64
3✔
493
                if err := validateParametersNotBlank(map[string]string{"commentID": strconv.FormatInt(commentID, 10), "discussionID": comment.ThreadID}); err != nil {
4✔
494
                        return err
1✔
495
                }
1✔
496
                if _, err := client.glClient.Discussions.DeleteMergeRequestDiscussionNote(getProjectID(owner, repository), pullRequestID, comment.ThreadID, int(commentID), gitlab.WithContext(ctx)); err != nil {
2✔
497
                        return fmt.Errorf("an error occurred while deleting pull request review comment: %w", err)
×
UNCOV
498
                }
×
499
        }
500
        return nil
1✔
501
}
502

503
// DeletePullRequestComment on GitLab
504
func (client *GitLabClient) DeletePullRequestComment(ctx context.Context, owner, repository string, pullRequestID, commentID int) error {
1✔
505
        if err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository}); err != nil {
1✔
UNCOV
506
                return err
×
UNCOV
507
        }
×
508
        if _, err := client.glClient.Notes.DeleteMergeRequestNote(getProjectID(owner, repository), pullRequestID, commentID, gitlab.WithContext(ctx)); err != nil {
1✔
UNCOV
509
                return fmt.Errorf("an error occurred while deleting pull request comment:\n%s", err.Error())
×
UNCOV
510
        }
×
511
        return nil
1✔
512
}
513

514
// GetLatestCommit on GitLab
515
func (client *GitLabClient) GetLatestCommit(ctx context.Context, owner, repository, branch string) (CommitInfo, error) {
7✔
516
        commits, err := client.GetCommits(ctx, owner, repository, branch)
7✔
517
        if err != nil {
12✔
518
                return CommitInfo{}, err
5✔
519
        }
5✔
520

521
        if len(commits) > 0 {
3✔
522
                return commits[0], nil
1✔
523
        }
1✔
524

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

528
// GetCommits on GitLab
529
func (client *GitLabClient) GetCommits(ctx context.Context, owner, repository, branch string) ([]CommitInfo, error) {
8✔
530
        err := validateParametersNotBlank(map[string]string{
8✔
531
                "owner":      owner,
8✔
532
                "repository": repository,
8✔
533
                "branch":     branch,
8✔
534
        })
8✔
535
        if err != nil {
12✔
536
                return nil, err
4✔
537
        }
4✔
538

539
        listOptions := &gitlab.ListCommitsOptions{
4✔
540
                RefName: &branch,
4✔
541
                ListOptions: gitlab.ListOptions{
4✔
542
                        Page:    1,
4✔
543
                        PerPage: vcsutils.NumberOfCommitsToFetch,
4✔
544
                },
4✔
545
        }
4✔
546
        return client.getCommitsWithQueryOptions(ctx, owner, repository, listOptions)
4✔
547
}
548

549
func (client *GitLabClient) GetCommitsWithQueryOptions(ctx context.Context, owner, repository string, listOptions GitCommitsQueryOptions) ([]CommitInfo, error) {
1✔
550
        err := validateParametersNotBlank(map[string]string{
1✔
551
                "owner":      owner,
1✔
552
                "repository": repository,
1✔
553
        })
1✔
554
        if err != nil {
1✔
UNCOV
555
                return nil, err
×
UNCOV
556
        }
×
557

558
        return client.getCommitsWithQueryOptions(ctx, owner, repository, convertToListCommitsOptions(listOptions))
1✔
559
}
560

561
func convertToListCommitsOptions(options GitCommitsQueryOptions) *gitlab.ListCommitsOptions {
1✔
562
        t := time.Now()
1✔
563
        return &gitlab.ListCommitsOptions{
1✔
564
                ListOptions: gitlab.ListOptions{
1✔
565
                        Page:    options.Page,
1✔
566
                        PerPage: options.PerPage,
1✔
567
                },
1✔
568
                Since: &options.Since,
1✔
569
                Until: &t,
1✔
570
        }
1✔
571
}
1✔
572

573
func (client *GitLabClient) getCommitsWithQueryOptions(ctx context.Context, owner, repository string, options *gitlab.ListCommitsOptions) ([]CommitInfo, error) {
5✔
574
        commits, _, err := client.glClient.Commits.ListCommits(getProjectID(owner, repository), options, gitlab.WithContext(ctx))
5✔
575
        if err != nil {
6✔
576
                return nil, err
1✔
577
        }
1✔
578

579
        var commitsInfo []CommitInfo
4✔
580
        for _, commit := range commits {
10✔
581
                commitInfo := mapGitLabCommitToCommitInfo(commit)
6✔
582
                commitsInfo = append(commitsInfo, commitInfo)
6✔
583
        }
6✔
584
        return commitsInfo, nil
4✔
585
}
586

587
// GetRepositoryInfo on GitLab
588
func (client *GitLabClient) GetRepositoryInfo(ctx context.Context, owner, repository string) (RepositoryInfo, error) {
5✔
589
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
5✔
590
        if err != nil {
8✔
591
                return RepositoryInfo{}, err
3✔
592
        }
3✔
593

594
        project, _, err := client.glClient.Projects.GetProject(getProjectID(owner, repository), nil, gitlab.WithContext(ctx))
2✔
595
        if err != nil {
2✔
UNCOV
596
                return RepositoryInfo{}, err
×
UNCOV
597
        }
×
598

599
        return RepositoryInfo{RepositoryVisibility: getGitLabProjectVisibility(project), CloneInfo: CloneInfo{HTTP: project.HTTPURLToRepo, SSH: project.SSHURLToRepo}}, nil
2✔
600
}
601

602
// GetCommitBySha on GitLab
603
func (client *GitLabClient) GetCommitBySha(ctx context.Context, owner, repository, sha string) (CommitInfo, error) {
6✔
604
        err := validateParametersNotBlank(map[string]string{
6✔
605
                "owner":      owner,
6✔
606
                "repository": repository,
6✔
607
                "sha":        sha,
6✔
608
        })
6✔
609
        if err != nil {
10✔
610
                return CommitInfo{}, err
4✔
611
        }
4✔
612

613
        commit, _, err := client.glClient.Commits.GetCommit(getProjectID(owner, repository), sha, nil, gitlab.WithContext(ctx))
2✔
614
        if err != nil {
3✔
615
                return CommitInfo{}, err
1✔
616
        }
1✔
617
        return mapGitLabCommitToCommitInfo(commit), nil
1✔
618
}
619

620
// CreateLabel on GitLab
621
func (client *GitLabClient) CreateLabel(ctx context.Context, owner, repository string, labelInfo LabelInfo) error {
5✔
622
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "LabelInfo.name": labelInfo.Name})
5✔
623
        if err != nil {
9✔
624
                return err
4✔
625
        }
4✔
626

627
        _, _, err = client.glClient.Labels.CreateLabel(getProjectID(owner, repository), &gitlab.CreateLabelOptions{
1✔
628
                Name:        &labelInfo.Name,
1✔
629
                Description: &labelInfo.Description,
1✔
630
                Color:       &labelInfo.Color,
1✔
631
        }, gitlab.WithContext(ctx))
1✔
632

1✔
633
        return err
1✔
634
}
635

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

643
        labels, _, err := client.glClient.Labels.ListLabels(getProjectID(owner, repository), &gitlab.ListLabelsOptions{}, gitlab.WithContext(ctx))
2✔
644
        if err != nil {
2✔
UNCOV
645
                return nil, err
×
UNCOV
646
        }
×
647

648
        for _, label := range labels {
4✔
649
                if label.Name == name {
3✔
650
                        return &LabelInfo{
1✔
651
                                Name:        label.Name,
1✔
652
                                Description: label.Description,
1✔
653
                                Color:       strings.TrimPrefix(label.Color, "#"),
1✔
654
                        }, err
1✔
655
                }
1✔
656
        }
657

658
        return nil, nil
1✔
659
}
660

661
// ListPullRequestLabels on GitLab
662
func (client *GitLabClient) ListPullRequestLabels(ctx context.Context, owner, repository string, pullRequestID int) ([]string, error) {
4✔
663
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
4✔
664
        if err != nil {
7✔
665
                return []string{}, err
3✔
666
        }
3✔
667
        mergeRequest, _, err := client.glClient.MergeRequests.GetMergeRequest(getProjectID(owner, repository), pullRequestID,
1✔
668
                &gitlab.GetMergeRequestsOptions{}, gitlab.WithContext(ctx))
1✔
669
        if err != nil {
1✔
UNCOV
670
                return []string{}, err
×
UNCOV
671
        }
×
672

673
        return mergeRequest.Labels, nil
1✔
674
}
675

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

689
// UploadCodeScanning on GitLab
690
func (client *GitLabClient) UploadCodeScanning(_ context.Context, _ string, _ string, _ string, _ string) (string, error) {
1✔
691
        return "", errGitLabCodeScanningNotSupported
1✔
692
}
1✔
693

694
// GetRepositoryEnvironmentInfo on GitLab
695
func (client *GitLabClient) GetRepositoryEnvironmentInfo(_ context.Context, _, _, _ string) (RepositoryEnvironmentInfo, error) {
1✔
696
        return RepositoryEnvironmentInfo{}, errGitLabGetRepoEnvironmentInfoNotSupported
1✔
697
}
1✔
698

699
// DownloadFileFromRepo on GitLab
700
func (client *GitLabClient) DownloadFileFromRepo(_ context.Context, owner, repository, branch, path string) ([]byte, int, error) {
1✔
701
        file, glResponse, err := client.glClient.RepositoryFiles.GetFile(getProjectID(owner, repository), path, &gitlab.GetFileOptions{Ref: &branch})
1✔
702
        var statusCode int
1✔
703
        if glResponse != nil && glResponse.Response != nil {
2✔
704
                statusCode = glResponse.Response.StatusCode
1✔
705
        }
1✔
706
        if err != nil {
1✔
UNCOV
707
                return nil, statusCode, err
×
UNCOV
708
        }
×
709
        if statusCode != http.StatusOK {
1✔
UNCOV
710
                return nil, statusCode, fmt.Errorf("expected %d status code while received %d status code", http.StatusOK, glResponse.StatusCode)
×
UNCOV
711
        }
×
712
        var content []byte
1✔
713
        if file != nil {
2✔
714
                content, err = base64.StdEncoding.DecodeString(file.Content)
1✔
715
        }
1✔
716
        return content, statusCode, err
1✔
717
}
718

719
func (client *GitLabClient) GetModifiedFiles(_ context.Context, owner, repository, refBefore, refAfter string) ([]string, error) {
6✔
720
        if err := validateParametersNotBlank(map[string]string{
6✔
721
                "owner":      owner,
6✔
722
                "repository": repository,
6✔
723
                "refBefore":  refBefore,
6✔
724
                "refAfter":   refAfter,
6✔
725
        }); err != nil {
10✔
726
                return nil, err
4✔
727
        }
4✔
728

729
        // No pagination is needed according to the official documentation at
730
        // https://docs.gitlab.com/ce/api/repositories.html#compare-branches-tags-or-commits
731
        compare, _, err := client.glClient.Repositories.Compare(
2✔
732
                getProjectID(owner, repository),
2✔
733
                &gitlab.CompareOptions{From: &refBefore, To: &refAfter},
2✔
734
        )
2✔
735
        if err != nil {
3✔
736
                return nil, err
1✔
737
        }
1✔
738

739
        fileNamesSet := datastructures.MakeSet[string]()
1✔
740
        for _, diff := range compare.Diffs {
4✔
741
                fileNamesSet.Add(diff.NewPath)
3✔
742
                fileNamesSet.Add(diff.OldPath)
3✔
743
        }
3✔
744
        _ = fileNamesSet.Remove("") // Make sure there are no blank filepath.
1✔
745
        fileNamesList := fileNamesSet.ToSlice()
1✔
746
        sort.Strings(fileNamesList)
1✔
747
        return fileNamesList, nil
1✔
748
}
749

750
func getProjectID(owner, project string) string {
42✔
751
        return fmt.Sprintf("%s/%s", owner, project)
42✔
752
}
42✔
753

754
func createProjectHook(branch string, payloadURL string, webhookEvents ...vcsutils.WebhookEvent) *gitlab.ProjectHook {
2✔
755
        options := &gitlab.ProjectHook{URL: payloadURL}
2✔
756
        for _, webhookEvent := range webhookEvents {
11✔
757
                switch webhookEvent {
9✔
758
                case vcsutils.PrOpened, vcsutils.PrEdited, vcsutils.PrRejected, vcsutils.PrMerged:
6✔
759
                        options.MergeRequestsEvents = true
6✔
760
                case vcsutils.Push:
1✔
761
                        options.PushEvents = true
1✔
762
                        options.PushEventsBranchFilter = branch
1✔
763
                case vcsutils.TagPushed, vcsutils.TagRemoved:
2✔
764
                        options.TagPushEvents = true
2✔
765
                }
766
        }
767
        return options
2✔
768
}
769

770
func getGitLabProjectVisibility(project *gitlab.Project) RepositoryVisibility {
5✔
771
        switch project.Visibility {
5✔
772
        case gitlab.PublicVisibility:
1✔
773
                return Public
1✔
774
        case gitlab.InternalVisibility:
1✔
775
                return Internal
1✔
776
        default:
3✔
777
                return Private
3✔
778
        }
779
}
780

781
func getGitLabCommitState(commitState CommitStatus) string {
6✔
782
        switch commitState {
6✔
783
        case Pass:
1✔
784
                return "success"
1✔
785
        case Fail:
1✔
786
                return "failed"
1✔
787
        case Error:
1✔
788
                return "failed"
1✔
789
        case InProgress:
2✔
790
                return "running"
2✔
791
        }
792
        return ""
1✔
793
}
794

795
func mapGitLabCommitToCommitInfo(commit *gitlab.Commit) CommitInfo {
7✔
796
        return CommitInfo{
7✔
797
                Hash:          commit.ID,
7✔
798
                AuthorName:    commit.AuthorName,
7✔
799
                CommitterName: commit.CommitterName,
7✔
800
                Url:           commit.WebURL,
7✔
801
                Timestamp:     commit.CommittedDate.UTC().Unix(),
7✔
802
                Message:       commit.Message,
7✔
803
                ParentHashes:  commit.ParentIDs,
7✔
804
                AuthorEmail:   commit.AuthorEmail,
7✔
805
        }
7✔
806
}
7✔
807

808
func mapGitLabNotesToCommentInfoList(notes []*gitlab.Note, discussionId string) (res []CommentInfo) {
3✔
809
        for _, note := range notes {
8✔
810
                res = append(res, CommentInfo{
5✔
811
                        ID:       int64(note.ID),
5✔
812
                        ThreadID: discussionId,
5✔
813
                        Content:  note.Body,
5✔
814
                        Created:  *note.CreatedAt,
5✔
815
                })
5✔
816
        }
5✔
817
        return
3✔
818
}
819

820
func (client *GitLabClient) mapGitLabMergeRequestToPullRequestInfoList(mergeRequests []*gitlab.MergeRequest, owner, repository string, withBody bool) (res []PullRequestInfo, err error) {
2✔
821
        for _, mergeRequest := range mergeRequests {
4✔
822
                var mergeRequestInfo PullRequestInfo
2✔
823
                if mergeRequestInfo, err = client.mapGitLabMergeRequestToPullRequestInfo(mergeRequest, withBody, owner, repository); err != nil {
2✔
UNCOV
824
                        return
×
UNCOV
825
                }
×
826
                res = append(res, mergeRequestInfo)
2✔
827
        }
828
        return
2✔
829
}
830

831
func (client *GitLabClient) mapGitLabMergeRequestToPullRequestInfo(mergeRequest *gitlab.MergeRequest, withBody bool, owner, repository string) (PullRequestInfo, error) {
3✔
832
        var body string
3✔
833
        if withBody {
4✔
834
                body = mergeRequest.Description
1✔
835
        }
1✔
836
        sourceOwner := owner
3✔
837
        var err error
3✔
838
        if mergeRequest.SourceProjectID != mergeRequest.TargetProjectID {
3✔
UNCOV
839
                if sourceOwner, err = client.getProjectOwnerByID(mergeRequest.SourceProjectID); err != nil {
×
UNCOV
840
                        return PullRequestInfo{}, err
×
UNCOV
841
                }
×
842
        }
843

844
        return PullRequestInfo{
3✔
845
                ID:   int64(mergeRequest.IID),
3✔
846
                Body: body,
3✔
847
                Source: BranchInfo{
3✔
848
                        Name:       mergeRequest.SourceBranch,
3✔
849
                        Repository: repository,
3✔
850
                        Owner:      sourceOwner,
3✔
851
                },
3✔
852
                URL: mergeRequest.WebURL,
3✔
853
                Target: BranchInfo{
3✔
854
                        Name:       mergeRequest.TargetBranch,
3✔
855
                        Repository: repository,
3✔
856
                        Owner:      owner,
3✔
857
                },
3✔
858
        }, nil
3✔
859
}
860

861
func (client *GitLabClient) getProjectOwnerByID(projectID int) (string, error) {
2✔
862
        project, glResponse, err := client.glClient.Projects.GetProject(projectID, &gitlab.GetProjectOptions{})
2✔
863
        if err != nil {
2✔
UNCOV
864
                return "", err
×
UNCOV
865
        }
×
866
        if glResponse != nil {
4✔
867
                if err = vcsutils.CheckResponseStatusWithBody(glResponse.Response, http.StatusOK); err != nil {
2✔
UNCOV
868
                        return "", err
×
869
                }
×
870
        }
871
        if project.Namespace == nil {
3✔
872
                return "", fmt.Errorf("could not fetch the name of the project owner. Project ID: %d", projectID)
1✔
873
        }
1✔
874
        return project.Namespace.Name, nil
1✔
875
}
876

877
func mapGitLabPullRequestState(state *vcsutils.PullRequestState) *string {
4✔
878
        var stateStringValue string
4✔
879
        switch *state {
4✔
880
        case vcsutils.Open:
2✔
881
                stateStringValue = "reopen"
2✔
882
        case vcsutils.Closed:
1✔
883
                stateStringValue = "close"
1✔
884
        default:
1✔
885
                return nil
1✔
886
        }
887
        return &stateStringValue
3✔
888
}
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