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

jfrog / froggit-go / 15140282759

20 May 2025 02:29PM UTC coverage: 85.324%. First build
15140282759

Pull #153

github

eyalk007
added merge pull request
Pull Request #153: Added support for all needed functions of global installation of Frogbot

177 of 327 new or added lines in 5 files covered. (54.13%)

4378 of 5131 relevant lines covered (85.32%)

6.49 hits per line

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

85.81
/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) {
82✔
28
        var client *gitlab.Client
82✔
29
        var err error
82✔
30
        if vcsInfo.APIEndpoint != "" {
130✔
31
                client, err = gitlab.NewClient(vcsInfo.Token, gitlab.WithBaseURL(vcsInfo.APIEndpoint))
48✔
32
        } else {
82✔
33
                client, err = gitlab.NewClient(vcsInfo.Token)
34✔
34
        }
34✔
35
        if err != nil {
83✔
36
                return nil, err
1✔
37
        }
1✔
38

39
        return &GitLabClient{
81✔
40
                glClient: client,
81✔
41
                vcsInfo:  vcsInfo,
81✔
42
                logger:   logger,
81✔
43
        }, nil
81✔
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
// ListPullRequestComments on GitLab
464
func (client *GitLabClient) ListPullRequestComments(ctx context.Context, owner, repository string, pullRequestID int) ([]CommentInfo, error) {
1✔
465
        if err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "pullRequestID": strconv.Itoa(pullRequestID)}); err != nil {
1✔
466
                return nil, err
×
467
        }
×
468
        commentsList, _, err := client.glClient.Notes.ListMergeRequestNotes(getProjectID(owner, repository), pullRequestID, &gitlab.ListMergeRequestNotesOptions{},
1✔
469
                gitlab.WithContext(ctx))
1✔
470
        if err != nil {
1✔
471
                return []CommentInfo{}, err
×
472
        }
×
473
        return mapGitLabNotesToCommentInfoList(commentsList, ""), nil
1✔
474
}
475

476
// DeletePullRequestReviewComment on GitLab
477
func (client *GitLabClient) DeletePullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int, comments ...CommentInfo) error {
3✔
478
        if err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "pullRequestID": strconv.Itoa(pullRequestID)}); err != nil {
4✔
479
                return err
1✔
480
        }
1✔
481
        for _, comment := range comments {
5✔
482
                var commentID int64
3✔
483
                if err := validateParametersNotBlank(map[string]string{"commentID": strconv.FormatInt(commentID, 10), "discussionID": comment.ThreadID}); err != nil {
4✔
484
                        return err
1✔
485
                }
1✔
486
                if _, err := client.glClient.Discussions.DeleteMergeRequestDiscussionNote(getProjectID(owner, repository), pullRequestID, comment.ThreadID, int(commentID), gitlab.WithContext(ctx)); err != nil {
2✔
487
                        return fmt.Errorf("an error occurred while deleting pull request review comment: %w", err)
×
488
                }
×
489
        }
490
        return nil
1✔
491
}
492

493
// DeletePullRequestComment on GitLab
494
func (client *GitLabClient) DeletePullRequestComment(ctx context.Context, owner, repository string, pullRequestID, commentID int) error {
1✔
495
        if err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository}); err != nil {
1✔
496
                return err
×
497
        }
×
498
        if _, err := client.glClient.Notes.DeleteMergeRequestNote(getProjectID(owner, repository), pullRequestID, commentID, gitlab.WithContext(ctx)); err != nil {
1✔
499
                return fmt.Errorf("an error occurred while deleting pull request comment:\n%s", err.Error())
×
500
        }
×
501
        return nil
1✔
502
}
503

504
// GetLatestCommit on GitLab
505
func (client *GitLabClient) GetLatestCommit(ctx context.Context, owner, repository, branch string) (CommitInfo, error) {
7✔
506
        commits, err := client.GetCommits(ctx, owner, repository, branch)
7✔
507
        if err != nil {
12✔
508
                return CommitInfo{}, err
5✔
509
        }
5✔
510

511
        if len(commits) > 0 {
3✔
512
                return commits[0], nil
1✔
513
        }
1✔
514

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

518
// GetCommits on GitLab
519
func (client *GitLabClient) GetCommits(ctx context.Context, owner, repository, branch string) ([]CommitInfo, error) {
8✔
520
        err := validateParametersNotBlank(map[string]string{
8✔
521
                "owner":      owner,
8✔
522
                "repository": repository,
8✔
523
                "branch":     branch,
8✔
524
        })
8✔
525
        if err != nil {
12✔
526
                return nil, err
4✔
527
        }
4✔
528

529
        listOptions := &gitlab.ListCommitsOptions{
4✔
530
                RefName: &branch,
4✔
531
                ListOptions: gitlab.ListOptions{
4✔
532
                        Page:    1,
4✔
533
                        PerPage: vcsutils.NumberOfCommitsToFetch,
4✔
534
                },
4✔
535
        }
4✔
536
        return client.getCommitsWithQueryOptions(ctx, owner, repository, listOptions)
4✔
537
}
538

539
func (client *GitLabClient) GetCommitsWithQueryOptions(ctx context.Context, owner, repository string, listOptions GitCommitsQueryOptions) ([]CommitInfo, error) {
1✔
540
        err := validateParametersNotBlank(map[string]string{
1✔
541
                "owner":      owner,
1✔
542
                "repository": repository,
1✔
543
        })
1✔
544
        if err != nil {
1✔
545
                return nil, err
×
546
        }
×
547

548
        return client.getCommitsWithQueryOptions(ctx, owner, repository, convertToListCommitsOptions(listOptions))
1✔
549
}
550

551
func convertToListCommitsOptions(options GitCommitsQueryOptions) *gitlab.ListCommitsOptions {
1✔
552
        t := time.Now()
1✔
553
        return &gitlab.ListCommitsOptions{
1✔
554
                ListOptions: gitlab.ListOptions{
1✔
555
                        Page:    options.Page,
1✔
556
                        PerPage: options.PerPage,
1✔
557
                },
1✔
558
                Since: &options.Since,
1✔
559
                Until: &t,
1✔
560
        }
1✔
561
}
1✔
562

563
func (client *GitLabClient) getCommitsWithQueryOptions(ctx context.Context, owner, repository string, options *gitlab.ListCommitsOptions) ([]CommitInfo, error) {
5✔
564
        commits, _, err := client.glClient.Commits.ListCommits(getProjectID(owner, repository), options, gitlab.WithContext(ctx))
5✔
565
        if err != nil {
6✔
566
                return nil, err
1✔
567
        }
1✔
568

569
        var commitsInfo []CommitInfo
4✔
570
        for _, commit := range commits {
10✔
571
                commitInfo := mapGitLabCommitToCommitInfo(commit)
6✔
572
                commitsInfo = append(commitsInfo, commitInfo)
6✔
573
        }
6✔
574
        return commitsInfo, nil
4✔
575
}
576

577
// GetRepositoryInfo on GitLab
578
func (client *GitLabClient) GetRepositoryInfo(ctx context.Context, owner, repository string) (RepositoryInfo, error) {
5✔
579
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
5✔
580
        if err != nil {
8✔
581
                return RepositoryInfo{}, err
3✔
582
        }
3✔
583

584
        project, _, err := client.glClient.Projects.GetProject(getProjectID(owner, repository), nil, gitlab.WithContext(ctx))
2✔
585
        if err != nil {
2✔
586
                return RepositoryInfo{}, err
×
587
        }
×
588

589
        return RepositoryInfo{RepositoryVisibility: getGitLabProjectVisibility(project), CloneInfo: CloneInfo{HTTP: project.HTTPURLToRepo, SSH: project.SSHURLToRepo}}, nil
2✔
590
}
591

592
// GetCommitBySha on GitLab
593
func (client *GitLabClient) GetCommitBySha(ctx context.Context, owner, repository, sha string) (CommitInfo, error) {
6✔
594
        err := validateParametersNotBlank(map[string]string{
6✔
595
                "owner":      owner,
6✔
596
                "repository": repository,
6✔
597
                "sha":        sha,
6✔
598
        })
6✔
599
        if err != nil {
10✔
600
                return CommitInfo{}, err
4✔
601
        }
4✔
602

603
        commit, _, err := client.glClient.Commits.GetCommit(getProjectID(owner, repository), sha, nil, gitlab.WithContext(ctx))
2✔
604
        if err != nil {
3✔
605
                return CommitInfo{}, err
1✔
606
        }
1✔
607
        return mapGitLabCommitToCommitInfo(commit), nil
1✔
608
}
609

610
// CreateLabel on GitLab
611
func (client *GitLabClient) CreateLabel(ctx context.Context, owner, repository string, labelInfo LabelInfo) error {
5✔
612
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "LabelInfo.name": labelInfo.Name})
5✔
613
        if err != nil {
9✔
614
                return err
4✔
615
        }
4✔
616

617
        _, _, err = client.glClient.Labels.CreateLabel(getProjectID(owner, repository), &gitlab.CreateLabelOptions{
1✔
618
                Name:        &labelInfo.Name,
1✔
619
                Description: &labelInfo.Description,
1✔
620
                Color:       &labelInfo.Color,
1✔
621
        }, gitlab.WithContext(ctx))
1✔
622

1✔
623
        return err
1✔
624
}
625

626
// GetLabel on GitLub
627
func (client *GitLabClient) GetLabel(ctx context.Context, owner, repository, name string) (*LabelInfo, error) {
6✔
628
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "name": name})
6✔
629
        if err != nil {
10✔
630
                return nil, err
4✔
631
        }
4✔
632

633
        labels, _, err := client.glClient.Labels.ListLabels(getProjectID(owner, repository), &gitlab.ListLabelsOptions{}, gitlab.WithContext(ctx))
2✔
634
        if err != nil {
2✔
635
                return nil, err
×
636
        }
×
637

638
        for _, label := range labels {
4✔
639
                if label.Name == name {
3✔
640
                        return &LabelInfo{
1✔
641
                                Name:        label.Name,
1✔
642
                                Description: label.Description,
1✔
643
                                Color:       strings.TrimPrefix(label.Color, "#"),
1✔
644
                        }, err
1✔
645
                }
1✔
646
        }
647

648
        return nil, nil
1✔
649
}
650

651
// ListPullRequestLabels on GitLab
652
func (client *GitLabClient) ListPullRequestLabels(ctx context.Context, owner, repository string, pullRequestID int) ([]string, error) {
4✔
653
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
4✔
654
        if err != nil {
7✔
655
                return []string{}, err
3✔
656
        }
3✔
657
        mergeRequest, _, err := client.glClient.MergeRequests.GetMergeRequest(getProjectID(owner, repository), pullRequestID,
1✔
658
                &gitlab.GetMergeRequestsOptions{}, gitlab.WithContext(ctx))
1✔
659
        if err != nil {
1✔
660
                return []string{}, err
×
661
        }
×
662

663
        return mergeRequest.Labels, nil
1✔
664
}
665

666
// UnlabelPullRequest on GitLab
667
func (client *GitLabClient) UnlabelPullRequest(ctx context.Context, owner, repository, label string, pullRequestID int) error {
4✔
668
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
4✔
669
        if err != nil {
7✔
670
                return err
3✔
671
        }
3✔
672
        labels := gitlab.LabelOptions{label}
1✔
673
        _, _, err = client.glClient.MergeRequests.UpdateMergeRequest(getProjectID(owner, repository), pullRequestID, &gitlab.UpdateMergeRequestOptions{
1✔
674
                RemoveLabels: &labels,
1✔
675
        }, gitlab.WithContext(ctx))
1✔
676
        return err
1✔
677
}
678

679
// UploadCodeScanning on GitLab
680
func (client *GitLabClient) UploadCodeScanning(_ context.Context, _ string, _ string, _ string, _ string) (string, error) {
1✔
681
        return "", errGitLabCodeScanningNotSupported
1✔
682
}
1✔
683

684
// GetRepositoryEnvironmentInfo on GitLab
685
func (client *GitLabClient) GetRepositoryEnvironmentInfo(_ context.Context, _, _, _ string) (RepositoryEnvironmentInfo, error) {
1✔
686
        return RepositoryEnvironmentInfo{}, errGitLabGetRepoEnvironmentInfoNotSupported
1✔
687
}
1✔
688

689
// DownloadFileFromRepo on GitLab
690
func (client *GitLabClient) DownloadFileFromRepo(_ context.Context, owner, repository, branch, path string) ([]byte, int, error) {
1✔
691
        file, glResponse, err := client.glClient.RepositoryFiles.GetFile(getProjectID(owner, repository), path, &gitlab.GetFileOptions{Ref: &branch})
1✔
692
        var statusCode int
1✔
693
        if glResponse != nil && glResponse.Response != nil {
2✔
694
                statusCode = glResponse.Response.StatusCode
1✔
695
        }
1✔
696
        if err != nil {
1✔
697
                return nil, statusCode, err
×
698
        }
×
699
        if statusCode != http.StatusOK {
1✔
700
                return nil, statusCode, fmt.Errorf("expected %d status code while received %d status code", http.StatusOK, glResponse.StatusCode)
×
701
        }
×
702
        var content []byte
1✔
703
        if file != nil {
2✔
704
                content, err = base64.StdEncoding.DecodeString(file.Content)
1✔
705
        }
1✔
706
        return content, statusCode, err
1✔
707
}
708

709
func (client *GitLabClient) GetModifiedFiles(_ context.Context, owner, repository, refBefore, refAfter string) ([]string, error) {
6✔
710
        if err := validateParametersNotBlank(map[string]string{
6✔
711
                "owner":      owner,
6✔
712
                "repository": repository,
6✔
713
                "refBefore":  refBefore,
6✔
714
                "refAfter":   refAfter,
6✔
715
        }); err != nil {
10✔
716
                return nil, err
4✔
717
        }
4✔
718

719
        // No pagination is needed according to the official documentation at
720
        // https://docs.gitlab.com/ce/api/repositories.html#compare-branches-tags-or-commits
721
        compare, _, err := client.glClient.Repositories.Compare(
2✔
722
                getProjectID(owner, repository),
2✔
723
                &gitlab.CompareOptions{From: &refBefore, To: &refAfter},
2✔
724
        )
2✔
725
        if err != nil {
3✔
726
                return nil, err
1✔
727
        }
1✔
728

729
        fileNamesSet := datastructures.MakeSet[string]()
1✔
730
        for _, diff := range compare.Diffs {
4✔
731
                fileNamesSet.Add(diff.NewPath)
3✔
732
                fileNamesSet.Add(diff.OldPath)
3✔
733
        }
3✔
734
        _ = fileNamesSet.Remove("") // Make sure there are no blank filepath.
1✔
735
        fileNamesList := fileNamesSet.ToSlice()
1✔
736
        sort.Strings(fileNamesList)
1✔
737
        return fileNamesList, nil
1✔
738
}
739

740
func (client *GitLabClient) ListPullRequestReviews(ctx context.Context, owner, repository string, pullRequestID int) ([]PullRequestReviewDetails, error) {
1✔
741
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
1✔
742
        if err != nil {
1✔
743
                return nil, err
×
744
        }
×
745

746
        var prNotes []*gitlab.Note
1✔
747
        prNotes, _, err = client.glClient.Notes.ListMergeRequestNotes(owner+"/"+repository, pullRequestID, nil)
1✔
748
        if err != nil {
1✔
749
                return nil, err
×
750
        }
×
751

752
        var reviewInfos []PullRequestReviewDetails
1✔
753
        for _, review := range prNotes {
3✔
754
                reviewInfos = append(reviewInfos, PullRequestReviewDetails{
2✔
755
                        ID:          int64(review.ID),
2✔
756
                        Reviewer:    review.Author.Username,
2✔
757
                        Body:        review.Body,
2✔
758
                        SubmittedAt: review.CreatedAt.Format(time.RFC3339),
2✔
759
                        CommitID:    review.CommitID,
2✔
760
                })
2✔
761
        }
2✔
762

763
        return reviewInfos, nil
1✔
764
}
765

766
func (client *GitLabClient) ListPullRequestsAssociatedWithCommit(ctx context.Context, owner, repository string, commitSHA string) ([]PullRequestInfo, 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 mergeRequests []*gitlab.MergeRequest
1✔
773
        mergeRequests, _, err = client.glClient.Commits.ListMergeRequestsByCommit(owner+"/"+repository, commitSHA, nil)
1✔
774
        if err != nil {
1✔
775
                return nil, err
×
776
        }
×
777
        return client.mapGitLabMergeRequestToPullRequestInfoList(mergeRequests, owner, repository, true)
1✔
778
}
779

NEW
780
func (client *GitLabClient) CreateBranch(ctx context.Context, owner, repository, sourceBranch, newBranch string) error {
×
NEW
781
        return errGitLabCreateBranchNotSupported
×
NEW
782
}
×
783

NEW
784
func (client *GitLabClient) AllowWorkflows(ctx context.Context, owner string) error {
×
NEW
785
        return errGitLabAllowWorkflowsNotSupported
×
NEW
786
}
×
787

NEW
788
func (client *GitLabClient) AddOrganizationSecret(ctx context.Context, owner, secretName, secretValue string) error {
×
NEW
789
        return errGitLLabAddOrganizationSecretNotSupported
×
NEW
790
}
×
791

NEW
792
func (client *GitLabClient) CommitAndPushFiles(ctx context.Context, owner, repo, sourceBranch, commitMessage, authorName, authorEmail string, files []FileToCommit) error {
×
NEW
793
        return errGitLabCommitAndPushFilesNotSupported
×
NEW
794
}
×
795

NEW
796
func (client *GitLabClient) GetRepoCollaborators(ctx context.Context, owner, repo, affiliation, permission string) ([]string, error) {
×
NEW
797
        return nil, errGitLabGetCollaboratorsNotSupported
×
NEW
798
}
×
799

NEW
800
func (client *GitLabClient) GetRepoTeamsByPermissions(ctx context.Context, owner, repo string, permissions []string) ([]int64, error) {
×
NEW
801
        return nil, errGitLabGetRepoTeamsByPermissionsNotSupported
×
NEW
802
}
×
803

NEW
804
func (client *GitLabClient) CreateOrUpdateEnvironment(ctx context.Context, owner, repo, envName string, teams []int64, users []string) error {
×
NEW
805
        return errGitLabCreateOrUpdateEnvironmentNotSupported
×
NEW
806
}
×
807

NEW
808
func (client *GitLabClient) MergePullRequest(ctx context.Context, owner, repo string, prNumber int, commitMessage string) error {
×
NEW
809
        return errGitLabMergePullRequestNotSupported
×
NEW
810
}
×
811

812
func getProjectID(owner, project string) string {
42✔
813
        return fmt.Sprintf("%s/%s", owner, project)
42✔
814
}
42✔
815

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

832
func getGitLabProjectVisibility(project *gitlab.Project) RepositoryVisibility {
5✔
833
        switch project.Visibility {
5✔
834
        case gitlab.PublicVisibility:
1✔
835
                return Public
1✔
836
        case gitlab.InternalVisibility:
1✔
837
                return Internal
1✔
838
        default:
3✔
839
                return Private
3✔
840
        }
841
}
842

843
func getGitLabCommitState(commitState CommitStatus) string {
6✔
844
        switch commitState {
6✔
845
        case Pass:
1✔
846
                return "success"
1✔
847
        case Fail:
1✔
848
                return "failed"
1✔
849
        case Error:
1✔
850
                return "failed"
1✔
851
        case InProgress:
2✔
852
                return "running"
2✔
853
        }
854
        return ""
1✔
855
}
856

857
func mapGitLabCommitToCommitInfo(commit *gitlab.Commit) CommitInfo {
7✔
858
        return CommitInfo{
7✔
859
                Hash:          commit.ID,
7✔
860
                AuthorName:    commit.AuthorName,
7✔
861
                CommitterName: commit.CommitterName,
7✔
862
                Url:           commit.WebURL,
7✔
863
                Timestamp:     commit.CommittedDate.UTC().Unix(),
7✔
864
                Message:       commit.Message,
7✔
865
                ParentHashes:  commit.ParentIDs,
7✔
866
                AuthorEmail:   commit.AuthorEmail,
7✔
867
        }
7✔
868
}
7✔
869

870
func mapGitLabNotesToCommentInfoList(notes []*gitlab.Note, discussionId string) (res []CommentInfo) {
3✔
871
        for _, note := range notes {
8✔
872
                res = append(res, CommentInfo{
5✔
873
                        ID:       int64(note.ID),
5✔
874
                        ThreadID: discussionId,
5✔
875
                        Content:  note.Body,
5✔
876
                        Created:  *note.CreatedAt,
5✔
877
                })
5✔
878
        }
5✔
879
        return
3✔
880
}
881

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

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

906
        return PullRequestInfo{
4✔
907
                ID:     int64(mergeRequest.IID),
4✔
908
                Title:  mergeRequest.Title,
4✔
909
                Body:   body,
4✔
910
                Author: mergeRequest.Author.Username,
4✔
911
                Source: BranchInfo{
4✔
912
                        Name:       mergeRequest.SourceBranch,
4✔
913
                        Repository: repository,
4✔
914
                        Owner:      sourceOwner,
4✔
915
                },
4✔
916
                URL: mergeRequest.WebURL,
4✔
917
                Target: BranchInfo{
4✔
918
                        Name:       mergeRequest.TargetBranch,
4✔
919
                        Repository: repository,
4✔
920
                        Owner:      owner,
4✔
921
                },
4✔
922
        }, nil
4✔
923
}
924

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

941
func mapGitLabPullRequestState(state *vcsutils.PullRequestState) *string {
4✔
942
        var stateStringValue string
4✔
943
        switch *state {
4✔
944
        case vcsutils.Open:
2✔
945
                stateStringValue = "reopen"
2✔
946
        case vcsutils.Closed:
1✔
947
                stateStringValue = "close"
1✔
948
        default:
1✔
949
                return nil
1✔
950
        }
951
        return &stateStringValue
3✔
952
}
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

© 2025 Coveralls, Inc