• 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

82.86
/vcsclient/bitbucketcloud.go
1
package vcsclient
2

3
import (
4
        "bytes"
5
        "context"
6
        "encoding/json"
7
        "errors"
8
        "fmt"
9
        "github.com/jfrog/gofrog/datastructures"
10
        "github.com/ktrysmt/go-bitbucket"
11
        "net/http"
12
        "net/url"
13
        "sort"
14
        "strconv"
15
        "strings"
16
        "time"
17

18
        "github.com/mitchellh/mapstructure"
19

20
        "github.com/jfrog/froggit-go/vcsutils"
21
)
22

23
// BitbucketCloudClient API version 2.0
24
type BitbucketCloudClient struct {
25
        vcsInfo VcsInfo
26
        url     *url.URL
27
        logger  vcsutils.Log
28
}
29

30
// NewBitbucketCloudClient create a new BitbucketCloudClient
31
func NewBitbucketCloudClient(vcsInfo VcsInfo, logger vcsutils.Log) (*BitbucketCloudClient, error) {
62✔
32
        bitbucketClient := &BitbucketCloudClient{
62✔
33
                vcsInfo: vcsInfo,
62✔
34
                logger:  logger,
62✔
35
        }
62✔
36
        if vcsInfo.APIEndpoint != "" {
93✔
37
                url, err := url.Parse(vcsInfo.APIEndpoint)
31✔
38
                if err != nil {
32✔
39
                        return nil, err
1✔
40
                }
1✔
41
                bitbucketClient.url = url
30✔
42
        }
43
        return bitbucketClient, nil
61✔
44
}
45

46
func (client *BitbucketCloudClient) buildBitbucketCloudClient(_ context.Context) *bitbucket.Client {
30✔
47
        bitbucketClient := bitbucket.NewBasicAuth(client.vcsInfo.Username, client.vcsInfo.Token)
30✔
48
        if client.url != nil {
58✔
49
                bitbucketClient.SetApiBaseURL(*client.url)
28✔
50
        }
28✔
51
        return bitbucketClient
30✔
52
}
53

54
// TestConnection on Bitbucket cloud
55
func (client *BitbucketCloudClient) TestConnection(ctx context.Context) error {
1✔
56
        bitbucketClient := client.buildBitbucketCloudClient(ctx)
1✔
57
        _, err := bitbucketClient.User.Profile()
1✔
58
        return err
1✔
59
}
1✔
60

61
// ListRepositories on Bitbucket cloud
62
func (client *BitbucketCloudClient) ListRepositories(ctx context.Context) (map[string][]string, error) {
1✔
63
        bitbucketClient := client.buildBitbucketCloudClient(ctx)
1✔
64
        results := make(map[string][]string)
1✔
65
        workspaces, err := bitbucketClient.Workspaces.List()
1✔
66
        if err != nil {
1✔
67
                return nil, err
×
68
        }
×
69
        for _, workspace := range workspaces.Workspaces {
2✔
70
                repositoriesRes, err := bitbucketClient.Repositories.ListForAccount(&bitbucket.RepositoriesOptions{Owner: workspace.Slug})
1✔
71
                if err != nil {
1✔
72
                        return nil, err
×
73
                }
×
74
                for _, repo := range repositoriesRes.Items {
3✔
75
                        results[workspace.Slug] = append(results[workspace.Slug], repo.Slug)
2✔
76
                }
2✔
77
        }
78
        return results, nil
1✔
79
}
80

81
// ListBranches on Bitbucket cloud
82
func (client *BitbucketCloudClient) ListBranches(ctx context.Context, owner, repository string) ([]string, error) {
1✔
83
        bitbucketClient := client.buildBitbucketCloudClient(ctx)
1✔
84
        branches, err := bitbucketClient.Repositories.Repository.ListBranches(&bitbucket.RepositoryBranchOptions{Owner: owner, RepoSlug: repository})
1✔
85
        if err != nil {
1✔
86
                return nil, err
×
87
        }
×
88

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

96
// AddSshKeyToRepository on Bitbucket cloud, the deploy-key is always read-only.
97
func (client *BitbucketCloudClient) AddSshKeyToRepository(ctx context.Context, owner, repository, keyName, publicKey string, _ Permission) (err error) {
7✔
98
        err = validateParametersNotBlank(map[string]string{
7✔
99
                "owner":      owner,
7✔
100
                "repository": repository,
7✔
101
                "key name":   keyName,
7✔
102
                "public key": publicKey,
7✔
103
        })
7✔
104
        if err != nil {
12✔
105
                return
5✔
106
        }
5✔
107
        endpoint := client.vcsInfo.APIEndpoint
2✔
108
        if endpoint == "" {
2✔
109
                endpoint = bitbucket.DEFAULT_BITBUCKET_API_BASE_URL
×
110
        }
×
111
        u := fmt.Sprintf("%s/repositories/%s/%s/deploy-keys", endpoint, owner, repository)
2✔
112
        addKeyRequest := bitbucketCloudAddSSHKeyRequest{
2✔
113
                Label: keyName,
2✔
114
                Key:   publicKey,
2✔
115
        }
2✔
116

2✔
117
        body := new(bytes.Buffer)
2✔
118
        err = json.NewEncoder(body).Encode(addKeyRequest)
2✔
119
        if err != nil {
2✔
120
                return
×
121
        }
×
122
        req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, body)
2✔
123
        if err != nil {
2✔
124
                return
×
125
        }
×
126
        req.Header.Set("Content-Type", "application/json")
2✔
127
        req.SetBasicAuth(client.vcsInfo.Username, client.vcsInfo.Token)
2✔
128

2✔
129
        bitbucketClient := client.buildBitbucketCloudClient(ctx)
2✔
130
        response, err := bitbucketClient.HttpClient.Do(req)
2✔
131
        if err != nil {
2✔
132
                return
×
133
        }
×
134
        defer func() {
4✔
135
                err = errors.Join(err, vcsutils.DiscardResponseBody(response), response.Body.Close())
2✔
136
        }()
2✔
137

138
        if response.StatusCode >= 300 {
3✔
139
                err = fmt.Errorf(response.Status)
1✔
140
        }
1✔
141
        return
2✔
142
}
143

144
type bitbucketCloudAddSSHKeyRequest struct {
145
        Key   string `json:"key"`
146
        Label string `json:"label"`
147
}
148

149
// CreateWebhook on Bitbucket cloud
150
func (client *BitbucketCloudClient) CreateWebhook(ctx context.Context, owner, repository, _, payloadURL string,
151
        webhookEvents ...vcsutils.WebhookEvent) (string, string, error) {
1✔
152
        bitbucketClient := client.buildBitbucketCloudClient(ctx)
1✔
153
        token := vcsutils.CreateToken()
1✔
154
        options := &bitbucket.WebhooksOptions{
1✔
155
                Active:   true,
1✔
156
                Owner:    owner,
1✔
157
                RepoSlug: repository,
1✔
158
                Url:      payloadURL + "?token=" + url.QueryEscape(token),
1✔
159
                Events:   getBitbucketCloudWebhookEvents(webhookEvents...),
1✔
160
        }
1✔
161
        response, err := bitbucketClient.Repositories.Webhooks.Create(options)
1✔
162
        if err != nil {
1✔
163
                return "", "", err
×
164
        }
×
165
        id, err := getBitbucketCloudWebhookID(response)
1✔
166
        if err != nil {
1✔
167
                return "", "", err
×
168
        }
×
169
        return id, token, err
1✔
170
}
171

172
// UpdateWebhook on Bitbucket cloud
173
func (client *BitbucketCloudClient) UpdateWebhook(ctx context.Context, owner, repository, _, payloadURL, token,
174
        webhookID string, webhookEvents ...vcsutils.WebhookEvent) error {
1✔
175
        bitbucketClient := client.buildBitbucketCloudClient(ctx)
1✔
176
        options := &bitbucket.WebhooksOptions{
1✔
177
                Active:   true,
1✔
178
                Uuid:     webhookID,
1✔
179
                Owner:    owner,
1✔
180
                RepoSlug: repository,
1✔
181
                Url:      payloadURL + "?token=" + url.QueryEscape(token),
1✔
182
                Events:   getBitbucketCloudWebhookEvents(webhookEvents...),
1✔
183
        }
1✔
184
        _, err := bitbucketClient.Repositories.Webhooks.Update(options)
1✔
185
        return err
1✔
186
}
1✔
187

188
// DeleteWebhook on Bitbucket cloud
189
func (client *BitbucketCloudClient) DeleteWebhook(ctx context.Context, owner, repository, webhookID string) error {
1✔
190
        bitbucketClient := client.buildBitbucketCloudClient(ctx)
1✔
191
        options := &bitbucket.WebhooksOptions{
1✔
192
                Uuid:     webhookID,
1✔
193
                Owner:    owner,
1✔
194
                RepoSlug: repository,
1✔
195
        }
1✔
196
        _, err := bitbucketClient.Repositories.Webhooks.Delete(options)
1✔
197
        return err
1✔
198
}
1✔
199

200
// SetCommitStatus on Bitbucket cloud
201
func (client *BitbucketCloudClient) SetCommitStatus(ctx context.Context, commitStatus CommitStatus, owner, repository,
202
        ref, title, description, detailsURL string) error {
1✔
203
        bitbucketClient := client.buildBitbucketCloudClient(ctx)
1✔
204
        commitOptions := &bitbucket.CommitsOptions{
1✔
205
                Owner:    owner,
1✔
206
                RepoSlug: repository,
1✔
207
                Revision: ref,
1✔
208
        }
1✔
209
        commitStatusOptions := &bitbucket.CommitStatusOptions{
1✔
210
                State:       getBitbucketCommitState(commitStatus),
1✔
211
                Key:         title,
1✔
212
                Description: description,
1✔
213
                Url:         detailsURL,
1✔
214
        }
1✔
215
        _, err := bitbucketClient.Repositories.Commits.CreateCommitStatus(commitOptions, commitStatusOptions)
1✔
216
        return err
1✔
217
}
1✔
218

219
// GetCommitStatuses on Bitbucket cloud
220
func (client *BitbucketCloudClient) GetCommitStatuses(ctx context.Context, owner, repository, ref string) (status []CommitStatusInfo, err error) {
2✔
221
        bitbucketClient := client.buildBitbucketCloudClient(ctx)
2✔
222
        commitOptions := &bitbucket.CommitsOptions{
2✔
223
                Owner:    owner,
2✔
224
                RepoSlug: repository,
2✔
225
                Revision: ref,
2✔
226
        }
2✔
227
        rawStatuses, err := bitbucketClient.Repositories.Commits.GetCommitStatuses(commitOptions)
2✔
228
        if err != nil {
2✔
229
                return nil, err
×
230
        }
×
231
        results, err := bitbucketParseCommitStatuses(rawStatuses, vcsutils.BitbucketCloud)
2✔
232
        if err != nil {
2✔
233
                return nil, err
×
234
        }
×
235
        return results, err
2✔
236
}
237

238
// DownloadRepository on Bitbucket cloud
239
func (client *BitbucketCloudClient) DownloadRepository(ctx context.Context, owner, repository, branch,
240
        localPath string) error {
1✔
241
        bitbucketClient := client.buildBitbucketCloudClient(ctx)
1✔
242
        client.logger.Debug("getting Bitbucket Cloud archive link to download")
1✔
243
        repo, err := bitbucketClient.Repositories.Repository.Get(&bitbucket.RepositoryOptions{
1✔
244
                Owner:    owner,
1✔
245
                RepoSlug: repository,
1✔
246
        })
1✔
247
        if err != nil {
1✔
248
                return err
×
249
        }
×
250

251
        downloadLink, err := getDownloadLink(repo, branch)
1✔
252
        if err != nil {
1✔
253
                return err
×
254
        }
×
255
        client.logger.Debug("received archive url:", downloadLink)
1✔
256
        getRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadLink, nil)
1✔
257
        if err != nil {
1✔
258
                return err
×
259
        }
×
260
        if len(client.vcsInfo.Username) > 0 || len(client.vcsInfo.Token) > 0 {
1✔
261
                getRequest.SetBasicAuth(client.vcsInfo.Username, client.vcsInfo.Token)
×
262
        }
×
263

264
        response, err := bitbucketClient.HttpClient.Do(getRequest)
1✔
265
        if err != nil {
1✔
266
                return err
×
267
        }
×
268
        if err = vcsutils.CheckResponseStatusWithBody(response, http.StatusOK); err != nil {
1✔
269
                return err
×
270
        }
×
271
        client.logger.Info(repository, vcsutils.SuccessfulRepoDownload)
1✔
272
        err = vcsutils.Untar(localPath, response.Body, true)
1✔
273
        if err != nil {
1✔
274
                return err
×
275
        }
×
276
        client.logger.Info(vcsutils.SuccessfulRepoExtraction)
1✔
277
        repositoryInfo, err := client.GetRepositoryInfo(ctx, owner, repository)
1✔
278
        if err != nil {
1✔
279
                return err
×
280
        }
×
281
        // Generate .git folder with remote details
282
        return vcsutils.CreateDotGitFolderWithRemote(localPath, "origin", repositoryInfo.CloneInfo.HTTP)
1✔
283
}
284

285
func (client *BitbucketCloudClient) GetPullRequestCommentSizeLimit() int {
×
286
        return bitbucketPrContentSizeLimit
×
287
}
×
288

289
func (client *BitbucketCloudClient) GetPullRequestDetailsSizeLimit() int {
×
290
        return bitbucketPrContentSizeLimit
×
291
}
×
292

293
// CreatePullRequest on Bitbucket cloud
294
func (client *BitbucketCloudClient) CreatePullRequest(ctx context.Context, owner, repository, sourceBranch,
295
        targetBranch, title, description string) error {
1✔
296
        bitbucketClient := client.buildBitbucketCloudClient(ctx)
1✔
297
        client.logger.Debug(vcsutils.CreatingPullRequest, title)
1✔
298
        options := &bitbucket.PullRequestsOptions{
1✔
299
                Owner:             owner,
1✔
300
                SourceRepository:  owner + "/" + repository,
1✔
301
                RepoSlug:          repository,
1✔
302
                SourceBranch:      sourceBranch,
1✔
303
                DestinationBranch: targetBranch,
1✔
304
                Title:             title,
1✔
305
                Description:       description,
1✔
306
        }
1✔
307
        _, err := bitbucketClient.Repositories.PullRequests.Create(options)
1✔
308
        return err
1✔
309
}
1✔
310

311
// UpdatePullRequest on Bitbucket cloud
312
func (client *BitbucketCloudClient) UpdatePullRequest(ctx context.Context, owner, repository, title, body, targetBranchName string, prId int, state vcsutils.PullRequestState) error {
1✔
313
        bitbucketClient := client.buildBitbucketCloudClient(ctx)
1✔
314
        client.logger.Debug(vcsutils.CreatingPullRequest, title)
1✔
315
        options := &bitbucket.PullRequestsOptions{
1✔
316
                Owner:             owner,
1✔
317
                SourceRepository:  owner + "/" + repository,
1✔
318
                RepoSlug:          repository,
1✔
319
                Title:             title,
1✔
320
                Description:       body,
1✔
321
                DestinationBranch: targetBranchName,
1✔
322
                ID:                strconv.Itoa(prId),
1✔
323
                States:            []string{*vcsutils.MapPullRequestState(&state)},
1✔
324
        }
1✔
325
        _, err := bitbucketClient.Repositories.PullRequests.Update(options)
1✔
326
        return err
1✔
327
}
1✔
328

329
// ListOpenPullRequestsWithBody on Bitbucket cloud
330
func (client *BitbucketCloudClient) ListOpenPullRequestsWithBody(ctx context.Context, owner, repository string) (res []PullRequestInfo, err error) {
1✔
331
        return client.getOpenPullRequests(ctx, owner, repository, true)
1✔
332
}
1✔
333

334
// ListOpenPullRequests on Bitbucket cloud
335
func (client *BitbucketCloudClient) ListOpenPullRequests(ctx context.Context, owner, repository string) (res []PullRequestInfo, err error) {
1✔
336
        return client.getOpenPullRequests(ctx, owner, repository, false)
1✔
337
}
1✔
338

339
func (client *BitbucketCloudClient) getOpenPullRequests(ctx context.Context, owner, repository string, withBody bool) (res []PullRequestInfo, err error) {
2✔
340
        err = validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
2✔
341
        if err != nil {
2✔
342
                return nil, err
×
343
        }
×
344
        bitbucketClient := client.buildBitbucketCloudClient(ctx)
2✔
345
        client.logger.Debug(vcsutils.FetchingOpenPullRequests, repository)
2✔
346
        options := &bitbucket.PullRequestsOptions{
2✔
347
                Owner:    owner,
2✔
348
                RepoSlug: repository,
2✔
349
                States:   []string{"OPEN"},
2✔
350
        }
2✔
351
        pullRequests, err := bitbucketClient.Repositories.PullRequests.Gets(options)
2✔
352
        if err != nil {
2✔
353
                return
×
354
        }
×
355
        parsedPullRequests, err := vcsutils.RemapFields[pullRequestsResponse](pullRequests, "json")
2✔
356
        if err != nil {
2✔
357
                return
×
358
        }
×
359
        return mapBitbucketCloudPullRequestToPullRequestInfo(&parsedPullRequests, withBody), nil
2✔
360
}
361

362
func (client *BitbucketCloudClient) GetPullRequestByID(ctx context.Context, owner, repository string, pullRequestId int) (pullRequestInfo PullRequestInfo, err error) {
3✔
363
        err = validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
3✔
364
        if err != nil {
4✔
365
                return
1✔
366
        }
1✔
367
        bitbucketClient := client.buildBitbucketCloudClient(ctx)
2✔
368
        client.logger.Debug(vcsutils.FetchingPullRequestById, repository)
2✔
369
        prIdStr := strconv.Itoa(pullRequestId)
2✔
370
        options := &bitbucket.PullRequestsOptions{
2✔
371
                Owner:    owner,
2✔
372
                RepoSlug: repository,
2✔
373
                ID:       prIdStr,
2✔
374
        }
2✔
375
        pullRequestRaw, err := bitbucketClient.Repositories.PullRequests.Get(options)
2✔
376
        if err != nil {
2✔
377
                return
×
378
        }
×
379
        pullRequestDetails, err := vcsutils.RemapFields[pullRequestsDetails](pullRequestRaw, "json")
2✔
380
        if err != nil {
3✔
381
                return
1✔
382
        }
1✔
383

384
        sourceOwner, sourceRepository := splitBitbucketCloudRepoName(pullRequestDetails.Source.Repository.Name)
1✔
385
        targetOwner, targetRepository := splitBitbucketCloudRepoName(pullRequestDetails.Target.Repository.Name)
1✔
386

1✔
387
        pullRequestInfo = PullRequestInfo{
1✔
388
                ID:     pullRequestDetails.ID,
1✔
389
                Title:  pullRequestDetails.Title,
1✔
390
                Author: pullRequestDetails.Author.DisplayName,
1✔
391
                Source: BranchInfo{
1✔
392
                        Name:       pullRequestDetails.Source.Name.Str,
1✔
393
                        Repository: sourceRepository,
1✔
394
                        Owner:      sourceOwner,
1✔
395
                },
1✔
396
                Target: BranchInfo{
1✔
397
                        Name:       pullRequestDetails.Target.Name.Str,
1✔
398
                        Repository: targetRepository,
1✔
399
                        Owner:      targetOwner,
1✔
400
                },
1✔
401
        }
1✔
402
        return
1✔
403
}
404

405
// AddPullRequestComment on Bitbucket cloud
406
func (client *BitbucketCloudClient) AddPullRequestComment(ctx context.Context, owner, repository, content string, pullRequestID int) error {
5✔
407
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "content": content})
5✔
408
        if err != nil {
9✔
409
                return err
4✔
410
        }
4✔
411
        bitbucketClient := client.buildBitbucketCloudClient(ctx)
1✔
412
        options := &bitbucket.PullRequestCommentOptions{
1✔
413
                Owner:         owner,
1✔
414
                RepoSlug:      repository,
1✔
415
                PullRequestID: fmt.Sprint(pullRequestID),
1✔
416
                Content:       content,
1✔
417
        }
1✔
418
        _, err = bitbucketClient.Repositories.PullRequests.AddComment(options)
1✔
419
        return err
1✔
420
}
421

422
// AddPullRequestReviewComments on Bitbucket cloud
423
func (client *BitbucketCloudClient) AddPullRequestReviewComments(_ context.Context, _, _ string, _ int, _ ...PullRequestComment) error {
1✔
424
        return errBitbucketAddPullRequestReviewCommentsNotSupported
1✔
425
}
1✔
426

427
// ListPullRequestReviewComments on Bitbucket cloud
428
func (client *BitbucketCloudClient) ListPullRequestReviewComments(_ context.Context, _, _ string, _ int) ([]CommentInfo, error) {
1✔
429
        return nil, errBitbucketListPullRequestReviewCommentsNotSupported
1✔
430
}
1✔
431

432
func (client *BitbucketCloudClient) ListPullRequestReviews(ctx context.Context, owner, repository string, pullRequestID int) ([]PullRequestReviewDetails, error) {
1✔
433
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
1✔
434
        if err != nil {
1✔
435
                return nil, err
×
436
        }
×
437

438
        bitbucketClient := client.buildBitbucketCloudClient(ctx)
1✔
439
        options := &bitbucket.PullRequestsOptions{
1✔
440
                Owner:    owner,
1✔
441
                RepoSlug: repository,
1✔
442
                ID:       fmt.Sprint(pullRequestID),
1✔
443
        }
1✔
444

1✔
445
        comments, err := bitbucketClient.Repositories.PullRequests.GetComments(options)
1✔
446
        if err != nil {
1✔
447
                return nil, err
×
448
        }
×
449

450
        parsedComments, err := vcsutils.RemapFields[commentsResponse](comments, "json")
1✔
451
        if err != nil {
1✔
452
                return nil, err
×
453
        }
×
454

455
        var reviewInfos []PullRequestReviewDetails
1✔
456
        for _, comment := range parsedComments.Values {
3✔
457
                reviewInfos = append(reviewInfos, PullRequestReviewDetails{
2✔
458
                        ID:          comment.ID,
2✔
459
                        Reviewer:    comment.User.DisplayName,
2✔
460
                        Body:        comment.Content.Raw,
2✔
461
                        SubmittedAt: comment.Created.Format(time.RFC3339),
2✔
462
                        CommitID:    "", // Bitbucket Cloud comments do not have a commit ID
2✔
463
                })
2✔
464
        }
2✔
465

466
        return reviewInfos, nil
1✔
467
}
468

469
func (client *BitbucketCloudClient) ListPullRequestsAssociatedWithCommit(ctx context.Context, owner, repository, commitSHA string) ([]PullRequestInfo, error) {
1✔
470
        return nil, errBitbucketListPullRequestAssociatedCommitsNotSupported
1✔
471
}
1✔
472

473
// ListPullRequestComments on Bitbucket cloud
474
func (client *BitbucketCloudClient) ListPullRequestComments(ctx context.Context, owner, repository string, pullRequestID int) (res []CommentInfo, err error) {
1✔
475
        err = validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
1✔
476
        if err != nil {
1✔
477
                return nil, err
×
478
        }
×
479
        bitbucketClient := client.buildBitbucketCloudClient(ctx)
1✔
480
        options := &bitbucket.PullRequestsOptions{
1✔
481
                Owner:    owner,
1✔
482
                RepoSlug: repository,
1✔
483
                ID:       fmt.Sprint(pullRequestID),
1✔
484
        }
1✔
485
        comments, err := bitbucketClient.Repositories.PullRequests.GetComments(options)
1✔
486
        if err != nil {
1✔
487
                return
×
488
        }
×
489
        parsedComments, err := vcsutils.RemapFields[commentsResponse](comments, "json")
1✔
490
        if err != nil {
1✔
491
                return
×
492
        }
×
493
        return mapBitbucketCloudCommentToCommentInfo(&parsedComments), nil
1✔
494
}
495

496
// DeletePullRequestReviewComments on Bitbucket cloud
497
func (client *BitbucketCloudClient) DeletePullRequestReviewComments(_ context.Context, _, _ string, _ int, _ ...CommentInfo) error {
1✔
498
        return errBitbucketDeletePullRequestComment
1✔
499
}
1✔
500

501
// DeletePullRequestComment on Bitbucket cloud
502
func (client *BitbucketCloudClient) DeletePullRequestComment(_ context.Context, _, _ string, _, _ int) error {
1✔
503
        return errBitbucketDeletePullRequestComment
1✔
504
}
1✔
505

506
// GetLatestCommit on Bitbucket cloud
507
func (client *BitbucketCloudClient) GetLatestCommit(ctx context.Context, owner, repository, branch string) (CommitInfo, error) {
7✔
508
        err := validateParametersNotBlank(map[string]string{
7✔
509
                "owner":      owner,
7✔
510
                "repository": repository,
7✔
511
                "branch":     branch,
7✔
512
        })
7✔
513
        if err != nil {
11✔
514
                return CommitInfo{}, err
4✔
515
        }
4✔
516
        bitbucketClient := client.buildBitbucketCloudClient(ctx)
3✔
517
        bitbucketClient.Pagelen = 1
3✔
518
        options := &bitbucket.CommitsOptions{
3✔
519
                Owner:       owner,
3✔
520
                RepoSlug:    repository,
3✔
521
                Branchortag: branch,
3✔
522
        }
3✔
523
        commits, err := bitbucketClient.Repositories.Commits.GetCommits(options)
3✔
524
        if err != nil {
5✔
525
                return CommitInfo{}, err
2✔
526
        }
2✔
527
        parsedCommits, err := vcsutils.RemapFields[commitResponse](commits, "json")
1✔
528
        if err != nil {
1✔
529
                return CommitInfo{}, err
×
530
        }
×
531
        if len(parsedCommits.Values) > 0 {
2✔
532
                latestCommit := parsedCommits.Values[0]
1✔
533
                return mapBitbucketCloudCommitToCommitInfo(latestCommit), nil
1✔
534
        }
1✔
535
        return CommitInfo{}, nil
×
536
}
537

538
// GetCommits on Bitbucket Cloud
539
func (client *BitbucketCloudClient) GetCommits(_ context.Context, _, _, _ string) ([]CommitInfo, error) {
1✔
540
        return nil, errBitbucketGetCommitsNotSupported
1✔
541
}
1✔
542

543
func (client *BitbucketCloudClient) GetCommitsWithQueryOptions(ctx context.Context, owner, repository string, listOptions GitCommitsQueryOptions) ([]CommitInfo, error) {
×
544
        return nil, errBitbucketGetCommitsWithOptionsNotSupported
×
545
}
×
546

547
// GetRepositoryInfo on Bitbucket cloud
548
func (client *BitbucketCloudClient) GetRepositoryInfo(ctx context.Context, owner, repository string) (RepositoryInfo, error) {
5✔
549
        if err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository}); err != nil {
8✔
550
                return RepositoryInfo{}, err
3✔
551
        }
3✔
552
        bitbucketClient := client.buildBitbucketCloudClient(ctx)
2✔
553
        repo, err := bitbucketClient.Repositories.Repository.Get(&bitbucket.RepositoryOptions{
2✔
554
                Owner:    owner,
2✔
555
                RepoSlug: repository,
2✔
556
        })
2✔
557
        if err != nil {
2✔
558
                return RepositoryInfo{}, err
×
559
        }
×
560

561
        holder := struct {
2✔
562
                Clone []struct {
2✔
563
                        Name string `mapstructure:"name"`
2✔
564
                        HRef string `mapstructure:"href"`
2✔
565
                } `mapstructure:"clone"`
2✔
566
        }{}
2✔
567

2✔
568
        if err := mapstructure.Decode(repo.Links, &holder); err != nil {
2✔
569
                return RepositoryInfo{}, err
×
570
        }
×
571

572
        var info CloneInfo
2✔
573
        for _, link := range holder.Clone {
6✔
574
                switch strings.ToLower(link.Name) {
4✔
575
                case "https":
2✔
576
                        info.HTTP = link.HRef
2✔
577
                case "ssh":
2✔
578
                        info.SSH = link.HRef
2✔
579
                }
580
        }
581
        return RepositoryInfo{RepositoryVisibility: getBitbucketCloudRepositoryVisibility(repo), CloneInfo: info}, nil
2✔
582
}
583

584
// GetCommitBySha on Bitbucket cloud
585
func (client *BitbucketCloudClient) GetCommitBySha(ctx context.Context, owner, repository, sha string) (CommitInfo, error) {
6✔
586
        err := validateParametersNotBlank(map[string]string{
6✔
587
                "owner":      owner,
6✔
588
                "repository": repository,
6✔
589
                "sha":        sha,
6✔
590
        })
6✔
591
        if err != nil {
10✔
592
                return CommitInfo{}, err
4✔
593
        }
4✔
594

595
        bitbucketClient := client.buildBitbucketCloudClient(ctx)
2✔
596
        options := &bitbucket.CommitsOptions{
2✔
597
                Owner:    owner,
2✔
598
                RepoSlug: repository,
2✔
599
                Revision: sha,
2✔
600
        }
2✔
601
        commit, err := bitbucketClient.Repositories.Commits.GetCommit(options)
2✔
602
        if err != nil {
3✔
603
                return CommitInfo{}, err
1✔
604
        }
1✔
605
        parsedCommit, err := vcsutils.RemapFields[commitDetails](commit, "json")
1✔
606
        if err != nil {
1✔
607
                return CommitInfo{}, err
×
608
        }
×
609
        return mapBitbucketCloudCommitToCommitInfo(parsedCommit), nil
1✔
610
}
611

612
// CreateLabel on Bitbucket cloud
613
func (client *BitbucketCloudClient) CreateLabel(ctx context.Context, owner, repository string, labelInfo LabelInfo) error {
1✔
614
        return errLabelsNotSupported
1✔
615
}
1✔
616

617
// GetLabel on Bitbucket cloud
618
func (client *BitbucketCloudClient) GetLabel(ctx context.Context, owner, repository, name string) (*LabelInfo, error) {
1✔
619
        return nil, errLabelsNotSupported
1✔
620
}
1✔
621

622
// ListPullRequestLabels on Bitbucket cloud
623
func (client *BitbucketCloudClient) ListPullRequestLabels(ctx context.Context, owner, repository string, pullRequestID int) ([]string, error) {
1✔
624
        return nil, errLabelsNotSupported
1✔
625
}
1✔
626

627
// UnlabelPullRequest on Bitbucket cloud
628
func (client *BitbucketCloudClient) UnlabelPullRequest(ctx context.Context, owner, repository, name string, pullRequestID int) error {
1✔
629
        return errLabelsNotSupported
1✔
630
}
1✔
631

632
// UploadCodeScanning on Bitbucket cloud
633
func (client *BitbucketCloudClient) UploadCodeScanning(ctx context.Context, owner string, repository string, branch string, scanResults string) (string, error) {
×
634
        return "", errBitbucketCodeScanningNotSupported
×
635
}
×
636

637
// DownloadFileFromRepo on Bitbucket cloud
638
func (client *BitbucketCloudClient) DownloadFileFromRepo(ctx context.Context, owner, repository, branch, path string) ([]byte, int, error) {
1✔
639
        return nil, 0, errBitbucketDownloadFileFromRepoNotSupported
1✔
640
}
1✔
641

642
// GetRepositoryEnvironmentInfo on Bitbucket cloud
643
func (client *BitbucketCloudClient) GetRepositoryEnvironmentInfo(ctx context.Context, owner, repository, name string) (RepositoryEnvironmentInfo, error) {
1✔
644
        return RepositoryEnvironmentInfo{}, errBitbucketGetRepoEnvironmentInfoNotSupported
1✔
645
}
1✔
646

647
func (client *BitbucketCloudClient) GetModifiedFiles(ctx context.Context, owner, repository, refBefore, refAfter string) ([]string, error) {
6✔
648
        err := validateParametersNotBlank(map[string]string{
6✔
649
                "owner":      owner,
6✔
650
                "repository": repository,
6✔
651
                "refBefore":  refBefore,
6✔
652
                "refAfter":   refAfter,
6✔
653
        })
6✔
654
        if err != nil {
10✔
655
                return nil, err
4✔
656
        }
4✔
657

658
        bitbucketClient := client.buildBitbucketCloudClient(ctx)
2✔
659
        options := &bitbucket.DiffStatOptions{
2✔
660
                Owner:    owner,
2✔
661
                RepoSlug: repository,
2✔
662
                // We use 2 dots for spec because of the case described at the page:
2✔
663
                // https://developer.atlassian.com/cloud/bitbucket/rest/api-group-commits/#two-commit-spec
2✔
664
                // As there is no `topic` set it will be treated as `refAfter...refBefore` actually.
2✔
665
                Spec:    refAfter + ".." + refBefore,
2✔
666
                Renames: true,
2✔
667
                Merge:   true,
2✔
668
        }
2✔
669

2✔
670
        fileNamesSet := datastructures.MakeSet[string]()
2✔
671
        nextPage := 1
2✔
672

2✔
673
        for nextPage > 0 {
4✔
674
                options.PageNum = nextPage
2✔
675
                diffStatRes, err := bitbucketClient.Repositories.Diff.GetDiffStat(options)
2✔
676
                if err != nil {
3✔
677
                        return nil, err
1✔
678
                }
1✔
679

680
                if diffStatRes.Next == "" {
2✔
681
                        nextPage = -1
1✔
682
                } else {
1✔
683
                        nextPage++
×
684
                }
×
685

686
                for _, diffStat := range diffStatRes.DiffStats {
4✔
687
                        if path, ok := diffStat.New["path"].(string); ok {
6✔
688
                                fileNamesSet.Add(path)
3✔
689
                        }
3✔
690
                        if path, ok := diffStat.Old["path"].(string); ok {
6✔
691
                                fileNamesSet.Add(path)
3✔
692
                        }
3✔
693
                }
694
        }
695
        _ = fileNamesSet.Remove("") // Make sure there are no blank filepath.
1✔
696
        fileNamesList := fileNamesSet.ToSlice()
1✔
697
        sort.Strings(fileNamesList)
1✔
698
        return fileNamesList, nil
1✔
699
}
700

NEW
701
func (client *BitbucketCloudClient) CreateBranch(ctx context.Context, owner, repository, sourceBranch, newBranch string) error {
×
NEW
702
        return errBitbucketCreateBranchNotSupported
×
NEW
703
}
×
704

NEW
705
func (client *BitbucketCloudClient) AllowWorkflows(ctx context.Context, owner string) error {
×
NEW
706
        return errBitbucketAllowWorkflowsNotSupported
×
NEW
707
}
×
708

NEW
709
func (client *BitbucketCloudClient) AddOrganizationSecret(ctx context.Context, owner, secretName, secretValue string) error {
×
NEW
710
        return errBitbucketAddOrganizationSecretNotSupported
×
NEW
711
}
×
712

NEW
713
func (client *BitbucketCloudClient) CommitAndPushFiles(ctx context.Context, owner, repo, sourceBranch, commitMessage, authorName, authorEmail string, files []FileToCommit) error {
×
NEW
714
        return errBitbucketCommitAndPushFilesNotSupported
×
NEW
715
}
×
716

NEW
717
func (client *BitbucketCloudClient) GetRepoCollaborators(ctx context.Context, owner, repo, affiliation, permission string) ([]string, error) {
×
NEW
718
        return nil, errBitbucketGetRepoCollaboratorsNotSupported
×
NEW
719
}
×
720

NEW
721
func (client *BitbucketCloudClient) GetRepoTeamsByPermissions(ctx context.Context, owner, repo string, permissions []string) ([]int64, error) {
×
NEW
722
        return nil, errBitbucketGetRepoTeamsByPermissionsNotSupported
×
NEW
723
}
×
724

NEW
725
func (client *BitbucketCloudClient) CreateOrUpdateEnvironment(ctx context.Context, owner, repo, envName string, teams []int64, users []string) error {
×
NEW
726
        return errBitbucketCreateOrUpdateEnvironmentNotSupported
×
NEW
727
}
×
728

NEW
729
func (client *BitbucketCloudClient) MergePullRequest(ctx context.Context, owner, repo string, prNumber int, commitMessage string) error {
×
NEW
730
        return errBitbucketMergePullRequestNotSupported
×
NEW
731
}
×
732

733
type pullRequestsResponse struct {
734
        Values []pullRequestsDetails `json:"values"`
735
}
736

737
type pullRequestsDetails struct {
738
        ID     int64             `json:"id"`
739
        Title  string            `json:"title"`
740
        Body   string            `json:"description"`
741
        Author Author            `json:"author"`
742
        Source pullRequestBranch `json:"source"`
743
        Target pullRequestBranch `json:"destination"`
744
}
745

746
type Author struct {
747
        DisplayName string `json:"display_name"`
748
}
749

750
type pullRequestBranch struct {
751
        Name struct {
752
                Str string `json:"name"`
753
        } `json:"branch"`
754
        Repository pullRequestRepository `json:"repository"`
755
}
756

757
type pullRequestRepository struct {
758
        Name string `json:"full_name"`
759
}
760

761
type commentsResponse struct {
762
        Values []commentDetails `json:"values"`
763
}
764

765
type commentDetails struct {
766
        ID        int64          `json:"id"`
767
        User      user           `json:"user"`
768
        IsDeleted bool           `json:"deleted"`
769
        Content   commentContent `json:"content"`
770
        Created   time.Time      `json:"created_on"`
771
}
772

773
type commentContent struct {
774
        Raw string `json:"raw"`
775
}
776

777
type commitResponse struct {
778
        Values []commitDetails `json:"values"`
779
}
780

781
type commitDetails struct {
782
        Hash    string    `json:"hash"`
783
        Date    time.Time `json:"date"`
784
        Message string    `json:"message"`
785
        Author  struct {
786
                User user `json:"user"`
787
        } `json:"author"`
788
        Links struct {
789
                Self link `json:"self"`
790
        } `json:"links"`
791
        Parents []struct {
792
                Hash string `json:"hash"`
793
        } `json:"parents"`
794
}
795

796
type user struct {
797
        DisplayName string `json:"display_name"`
798
}
799
type link struct {
800
        Href string `json:"href"`
801
}
802

803
// Extract the webhook ID from the webhook create response
804
func getBitbucketCloudWebhookID(r interface{}) (string, error) {
1✔
805
        webhook := &bitbucket.WebhooksOptions{}
1✔
806
        b, err := json.Marshal(r)
1✔
807
        if err != nil {
1✔
808
                return "", err
×
809
        }
×
810
        err = json.Unmarshal(b, &webhook)
1✔
811
        if err != nil {
1✔
812
                return "", err
×
813
        }
×
814
        return strings.TrimRight(strings.TrimLeft(webhook.Uuid, "{"), "}"), nil
1✔
815
}
816

817
// Get varargs of webhook events and return a slice of Bitbucket cloud webhook events
818
func getBitbucketCloudWebhookEvents(webhookEvents ...vcsutils.WebhookEvent) []string {
2✔
819
        events := datastructures.MakeSet[string]()
2✔
820
        for _, event := range webhookEvents {
9✔
821
                switch event {
7✔
822
                case vcsutils.PrOpened:
1✔
823
                        events.Add("pullrequest:created")
1✔
824
                case vcsutils.PrEdited:
1✔
825
                        events.Add("pullrequest:updated")
1✔
826
                case vcsutils.PrRejected:
1✔
827
                        events.Add("pullrequest:rejected")
1✔
828
                case vcsutils.PrMerged:
1✔
829
                        events.Add("pullrequest:fulfilled")
1✔
830
                case vcsutils.Push, vcsutils.TagPushed, vcsutils.TagRemoved:
3✔
831
                        events.Add("repo:push")
3✔
832
                }
833
        }
834
        return events.ToSlice()
2✔
835
}
836

837
// The get repository request returns HTTP link to the repository - extract the link from the response.
838
func getDownloadLink(repo *bitbucket.Repository, branch string) (string, error) {
1✔
839
        repositoryHTMLLinks := &link{}
1✔
840
        b, err := json.Marshal(repo.Links["html"])
1✔
841
        if err != nil {
1✔
842
                return "", err
×
843
        }
×
844
        err = json.Unmarshal(b, repositoryHTMLLinks)
1✔
845
        if err != nil {
1✔
846
                return "", err
×
847
        }
×
848
        htmlLink := repositoryHTMLLinks.Href
1✔
849
        if htmlLink == "" {
1✔
850
                return "", fmt.Errorf("couldn't find repository HTML link: %s", repo.Links["html"])
×
851
        }
×
852
        return htmlLink + "/get/" + branch + ".tar.gz", err
1✔
853
}
854

855
func mapBitbucketCloudCommitToCommitInfo(parsedCommit commitDetails) CommitInfo {
2✔
856
        parents := make([]string, len(parsedCommit.Parents))
2✔
857
        for i, p := range parsedCommit.Parents {
5✔
858
                parents[i] = p.Hash
3✔
859
        }
3✔
860
        return CommitInfo{
2✔
861
                Hash:          parsedCommit.Hash,
2✔
862
                AuthorName:    parsedCommit.Author.User.DisplayName,
2✔
863
                CommitterName: "", // not provided
2✔
864
                Url:           parsedCommit.Links.Self.Href,
2✔
865
                Timestamp:     parsedCommit.Date.UTC().Unix(),
2✔
866
                Message:       parsedCommit.Message,
2✔
867
                ParentHashes:  parents,
2✔
868
        }
2✔
869
}
870

871
func mapBitbucketCloudCommentToCommentInfo(parsedComments *commentsResponse) []CommentInfo {
1✔
872
        comments := make([]CommentInfo, len(parsedComments.Values))
1✔
873
        for i, comment := range parsedComments.Values {
3✔
874
                comments[i] = CommentInfo{
2✔
875
                        ID:      comment.ID,
2✔
876
                        Content: comment.Content.Raw,
2✔
877
                        Created: comment.Created,
2✔
878
                }
2✔
879
        }
2✔
880
        return comments
1✔
881
}
882

883
func mapBitbucketCloudPullRequestToPullRequestInfo(parsedPullRequests *pullRequestsResponse, withBody bool) []PullRequestInfo {
2✔
884
        pullRequests := make([]PullRequestInfo, len(parsedPullRequests.Values))
2✔
885
        for i, pullRequest := range parsedPullRequests.Values {
8✔
886
                var body string
6✔
887
                if withBody {
9✔
888
                        body = pullRequest.Body
3✔
889
                }
3✔
890
                pullRequests[i] = PullRequestInfo{
6✔
891
                        ID:     pullRequest.ID,
6✔
892
                        Title:  pullRequest.Title,
6✔
893
                        Body:   body,
6✔
894
                        Author: pullRequest.Author.DisplayName,
6✔
895
                        Source: BranchInfo{
6✔
896
                                Name:       pullRequest.Source.Name.Str,
6✔
897
                                Repository: pullRequest.Source.Repository.Name,
6✔
898
                        },
6✔
899
                        Target: BranchInfo{
6✔
900
                                Name:       pullRequest.Target.Name.Str,
6✔
901
                                Repository: pullRequest.Target.Repository.Name,
6✔
902
                        },
6✔
903
                }
6✔
904
        }
905
        return pullRequests
2✔
906
}
907

908
func getBitbucketCloudRepositoryVisibility(repo *bitbucket.Repository) RepositoryVisibility {
4✔
909
        if repo.Is_private {
5✔
910
                return Private
1✔
911
        }
1✔
912
        return Public
3✔
913
}
914

915
// Bitbucket cloud repository name is a combination of workspace/repository
916
// Return the two separate elements
917
func splitBitbucketCloudRepoName(name string) (string, string) {
4✔
918
        split := strings.Split(name, "/")
4✔
919
        if len(split) < 2 {
5✔
920
                return "", ""
1✔
921
        }
1✔
922
        return split[0], split[1]
3✔
923
}
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