• 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.2
/vcsclient/bitbucketserver.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
        "io"
11
        "net/http"
12
        "sort"
13
        "strconv"
14
        "strings"
15
        "time"
16

17
        bitbucketv1 "github.com/gfleury/go-bitbucket-v1"
18
        "github.com/jfrog/froggit-go/vcsutils"
19
        "github.com/mitchellh/mapstructure"
20
        "golang.org/x/oauth2"
21
)
22

23
// BitbucketServerClient API version 1.0
24
type BitbucketServerClient struct {
25
        vcsInfo VcsInfo
26
        logger  vcsutils.Log
27
}
28

29
// NewBitbucketServerClient create a new BitbucketServerClient
30
func NewBitbucketServerClient(vcsInfo VcsInfo, logger vcsutils.Log) (*BitbucketServerClient, error) {
91✔
31
        bitbucketServerClient := &BitbucketServerClient{
91✔
32
                vcsInfo: vcsInfo,
91✔
33
                logger:  logger,
91✔
34
        }
91✔
35
        return bitbucketServerClient, nil
91✔
36
}
91✔
37

38
func (client *BitbucketServerClient) buildBitbucketClient(ctx context.Context) *bitbucketv1.DefaultApiService {
65✔
39
        // Bitbucket API Endpoint ends with '/rest'
65✔
40
        if !strings.HasSuffix(client.vcsInfo.APIEndpoint, "/rest") {
124✔
41
                client.vcsInfo.APIEndpoint += "/rest"
59✔
42
        }
59✔
43

44
        bbClient := bitbucketv1.NewAPIClient(ctx, &bitbucketv1.Configuration{
65✔
45
                HTTPClient: client.buildHTTPClient(ctx),
65✔
46
                BasePath:   client.vcsInfo.APIEndpoint,
65✔
47
        })
65✔
48
        return bbClient.DefaultApi
65✔
49
}
50

51
func (client *BitbucketServerClient) buildHTTPClient(ctx context.Context) *http.Client {
68✔
52
        httpClient := &http.Client{}
68✔
53
        if client.vcsInfo.Token != "" {
112✔
54
                httpClient = oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{AccessToken: client.vcsInfo.Token}))
44✔
55
        }
44✔
56
        return httpClient
68✔
57
}
58

59
// TestConnection on Bitbucket server
60
func (client *BitbucketServerClient) TestConnection(ctx context.Context) error {
4✔
61
        bitbucketClient := client.buildBitbucketClient(ctx)
4✔
62

4✔
63
        options := map[string]interface{}{"limit": 1}
4✔
64
        _, err := bitbucketClient.GetUsers(options)
4✔
65
        return err
4✔
66
}
4✔
67

68
// ListRepositories on Bitbucket server
69
func (client *BitbucketServerClient) ListRepositories(ctx context.Context) (map[string][]string, error) {
2✔
70
        bitbucketClient := client.buildBitbucketClient(ctx)
2✔
71
        projects, err := client.listProjects(bitbucketClient)
2✔
72
        if err != nil {
3✔
73
                return nil, err
1✔
74
        }
1✔
75

76
        results := make(map[string][]string)
1✔
77
        for _, project := range projects {
3✔
78
                var apiResponse *bitbucketv1.APIResponse
2✔
79
                for isLastReposPage, nextReposPageStart := true, 0; isLastReposPage; isLastReposPage, nextReposPageStart = bitbucketv1.HasNextPage(apiResponse) {
4✔
80
                        // Get all repositories for which the authenticated user has the REPO_READ permission
2✔
81
                        apiResponse, err = bitbucketClient.GetRepositoriesWithOptions(project, createPaginationOptions(nextReposPageStart))
2✔
82
                        if err != nil {
2✔
83
                                return nil, err
×
84
                        }
×
85

86
                        repos, err := bitbucketv1.GetRepositoriesResponse(apiResponse)
2✔
87
                        if err != nil {
2✔
88
                                return nil, err
×
89
                        }
×
90
                        for _, repo := range repos {
4✔
91
                                results[project] = append(results[project], repo.Slug)
2✔
92
                        }
2✔
93
                }
94
        }
95
        return results, nil
1✔
96
}
97

98
// ListBranches on Bitbucket server
99
func (client *BitbucketServerClient) ListBranches(ctx context.Context, owner, repository string) ([]string, error) {
2✔
100
        bitbucketClient := client.buildBitbucketClient(ctx)
2✔
101
        var results []string
2✔
102
        var apiResponse *bitbucketv1.APIResponse
2✔
103
        for isLastPage, nextPageStart := true, 0; isLastPage; isLastPage, nextPageStart = bitbucketv1.HasNextPage(apiResponse) {
4✔
104
                var err error
2✔
105
                apiResponse, err = bitbucketClient.GetBranches(owner, repository, createPaginationOptions(nextPageStart))
2✔
106
                if err != nil {
3✔
107
                        return nil, err
1✔
108
                }
1✔
109
                branches, err := bitbucketv1.GetBranchesResponse(apiResponse)
1✔
110
                if err != nil {
1✔
111
                        return nil, err
×
112
                }
×
113

114
                for _, branch := range branches {
3✔
115
                        results = append(results, branch.ID)
2✔
116
                }
2✔
117
        }
118

119
        return results, nil
1✔
120
}
121

122
// AddSshKeyToRepository on Bitbucket server
123
func (client *BitbucketServerClient) AddSshKeyToRepository(ctx context.Context, owner, repository, keyName, publicKey string, permission Permission) (err error) {
9✔
124
        // https://docs.atlassian.com/bitbucket-server/rest/5.16.0/bitbucket-ssh-rest.html
9✔
125
        err = validateParametersNotBlank(map[string]string{
9✔
126
                "owner":      owner,
9✔
127
                "repository": repository,
9✔
128
                "key name":   keyName,
9✔
129
                "public key": publicKey,
9✔
130
        })
9✔
131
        if err != nil {
14✔
132
                return err
5✔
133
        }
5✔
134

135
        accessPermission := "REPO_READ"
4✔
136
        if permission == ReadWrite {
5✔
137
                accessPermission = "REPO_WRITE"
1✔
138
        }
1✔
139

140
        url := fmt.Sprintf("%s/keys/1.0/projects/%s/repos/%s/ssh", client.vcsInfo.APIEndpoint, owner, repository)
4✔
141
        addKeyRequest := bitbucketServerAddSSHKeyRequest{
4✔
142
                Key:        bitbucketServerSSHKey{Text: publicKey, Label: keyName},
4✔
143
                Permission: accessPermission,
4✔
144
        }
4✔
145

4✔
146
        body := new(bytes.Buffer)
4✔
147
        err = json.NewEncoder(body).Encode(addKeyRequest)
4✔
148
        if err != nil {
4✔
149
                return err
×
150
        }
×
151
        req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
4✔
152
        if err != nil {
5✔
153
                return err
1✔
154
        }
1✔
155
        req.Header.Set("Content-Type", "application/json")
3✔
156

3✔
157
        httpClient := client.buildHTTPClient(ctx)
3✔
158
        response, err := httpClient.Do(req)
3✔
159
        if err != nil {
3✔
160
                return err
×
161
        }
×
162
        defer func() {
6✔
163
                err = errors.Join(err, vcsutils.DiscardResponseBody(response), response.Body.Close())
3✔
164
        }()
3✔
165

166
        if response.StatusCode >= 300 {
4✔
167
                var bodyBytes []byte
1✔
168
                bodyBytes, err = io.ReadAll(response.Body)
1✔
169
                if err != nil {
1✔
170
                        return
×
171
                }
×
172
                return fmt.Errorf("status: %v, body: %s", response.Status, bodyBytes)
1✔
173
        }
174
        return nil
2✔
175
}
176

177
type bitbucketServerAddSSHKeyRequest struct {
178
        Key        bitbucketServerSSHKey `json:"key"`
179
        Permission string                `json:"permission"`
180
}
181

182
type bitbucketServerSSHKey struct {
183
        Text  string `json:"text"`
184
        Label string `json:"label"`
185
}
186

187
// CreateWebhook on Bitbucket server
188
func (client *BitbucketServerClient) CreateWebhook(ctx context.Context, owner, repository, _, payloadURL string,
189
        webhookEvents ...vcsutils.WebhookEvent) (string, string, error) {
2✔
190
        bitbucketClient := client.buildBitbucketClient(ctx)
2✔
191
        token := vcsutils.CreateToken()
2✔
192
        hook := createBitbucketServerHook(token, payloadURL, webhookEvents...)
2✔
193
        response, err := bitbucketClient.CreateWebhook(owner, repository, hook, []string{})
2✔
194
        if err != nil {
3✔
195
                return "", "", err
1✔
196
        }
1✔
197
        webhoodID, err := getBitbucketServerWebhookID(response)
1✔
198
        if err != nil {
1✔
199
                return "", "", err
×
200
        }
×
201
        return webhoodID, token, err
1✔
202
}
203

204
// UpdateWebhook on Bitbucket server
205
func (client *BitbucketServerClient) UpdateWebhook(ctx context.Context, owner, repository, _, payloadURL, token,
206
        webhookID string, webhookEvents ...vcsutils.WebhookEvent) error {
2✔
207
        bitbucketClient := client.buildBitbucketClient(ctx)
2✔
208
        webhookIDInt32, err := strconv.ParseInt(webhookID, 10, 32)
2✔
209
        if err != nil {
2✔
210
                return err
×
211
        }
×
212
        hook := createBitbucketServerHook(token, payloadURL, webhookEvents...)
2✔
213
        _, err = bitbucketClient.UpdateWebhook(owner, repository, int32(webhookIDInt32), hook, []string{})
2✔
214
        return err
2✔
215
}
216

217
// DeleteWebhook on Bitbucket server
218
func (client *BitbucketServerClient) DeleteWebhook(ctx context.Context, owner, repository, webhookID string) error {
2✔
219
        bitbucketClient := client.buildBitbucketClient(ctx)
2✔
220
        webhookIDInt32, err := strconv.ParseInt(webhookID, 10, 32)
2✔
221
        if err != nil {
2✔
222
                return err
×
223
        }
×
224
        _, err = bitbucketClient.DeleteWebhook(owner, repository, int32(webhookIDInt32))
2✔
225
        return err
2✔
226
}
227

228
// SetCommitStatus on Bitbucket server
229
func (client *BitbucketServerClient) SetCommitStatus(ctx context.Context, commitStatus CommitStatus, _, _, ref, title,
230
        description, detailsURL string) error {
2✔
231
        bitbucketClient := client.buildBitbucketClient(ctx)
2✔
232
        _, err := bitbucketClient.SetCommitStatus(ref, bitbucketv1.BuildStatus{
2✔
233
                State:       getBitbucketCommitState(commitStatus),
2✔
234
                Key:         title,
2✔
235
                Description: description,
2✔
236
                Url:         detailsURL,
2✔
237
        })
2✔
238
        return err
2✔
239
}
2✔
240

241
// GetCommitStatuses on Bitbucket server
242
func (client *BitbucketServerClient) GetCommitStatuses(ctx context.Context, owner, repository, ref string) (status []CommitStatusInfo, err error) {
4✔
243
        bitbucketClient := client.buildBitbucketClient(ctx)
4✔
244
        response, err := bitbucketClient.GetCommitStatus(ref)
4✔
245
        if err != nil {
5✔
246
                return nil, err
1✔
247
        }
1✔
248
        return bitbucketParseCommitStatuses(response.Values, vcsutils.BitbucketServer)
3✔
249
}
250

251
// DownloadRepository on Bitbucket server
252
func (client *BitbucketServerClient) DownloadRepository(ctx context.Context, owner, repository, branch, localPath string) error {
2✔
253
        bitbucketClient := client.buildBitbucketClient(ctx)
2✔
254
        params := map[string]interface{}{"format": "tgz"}
2✔
255
        branch = strings.TrimSpace(branch)
2✔
256
        if branch != "" {
3✔
257
                params["at"] = branch
1✔
258
        }
1✔
259
        response, err := bitbucketClient.GetArchive(owner, repository, params)
2✔
260
        if err != nil {
3✔
261
                return err
1✔
262
        }
1✔
263
        client.logger.Info(repository, vcsutils.SuccessfulRepoDownload)
1✔
264
        err = vcsutils.Untar(localPath, bytes.NewReader(response.Payload), false)
1✔
265
        if err != nil {
1✔
266
                return err
×
267
        }
×
268
        client.logger.Info(vcsutils.SuccessfulRepoExtraction)
1✔
269
        repositoryInfo, err := client.GetRepositoryInfo(ctx, owner, repository)
1✔
270
        if err != nil {
1✔
271
                return err
×
272
        }
×
273
        // Generate .git folder with remote details
274
        return vcsutils.CreateDotGitFolderWithRemote(
1✔
275
                localPath,
1✔
276
                vcsutils.RemoteName,
1✔
277
                repositoryInfo.CloneInfo.HTTP)
1✔
278
}
279

280
func (client *BitbucketServerClient) GetPullRequestCommentSizeLimit() int {
×
281
        return bitbucketPrContentSizeLimit
×
282
}
×
283

284
func (client *BitbucketServerClient) GetPullRequestDetailsSizeLimit() int {
×
285
        return bitbucketPrContentSizeLimit
×
286
}
×
287

288
// CreatePullRequest on Bitbucket server
289
func (client *BitbucketServerClient) CreatePullRequest(ctx context.Context, owner, repository, sourceBranch, targetBranch,
290
        title, description string) error {
2✔
291
        bitbucketClient := client.buildBitbucketClient(ctx)
2✔
292
        bitbucketRepo := &bitbucketv1.Repository{
2✔
293
                Slug: repository,
2✔
294
                Project: &bitbucketv1.Project{
2✔
295
                        Key: owner,
2✔
296
                },
2✔
297
        }
2✔
298
        options := bitbucketv1.PullRequest{
2✔
299
                Title:       title,
2✔
300
                Description: description,
2✔
301
                FromRef: bitbucketv1.PullRequestRef{
2✔
302
                        ID:         vcsutils.AddBranchPrefix(sourceBranch),
2✔
303
                        Repository: *bitbucketRepo,
2✔
304
                },
2✔
305
                ToRef: bitbucketv1.PullRequestRef{
2✔
306
                        ID:         vcsutils.AddBranchPrefix(targetBranch),
2✔
307
                        Repository: *bitbucketRepo,
2✔
308
                },
2✔
309
        }
2✔
310
        _, err := bitbucketClient.CreatePullRequest(owner, repository, options)
2✔
311
        return err
2✔
312
}
2✔
313

314
// UpdatePullRequest on bitbucket server
315
// Changing targetBranchRef currently not supported.
316
func (client *BitbucketServerClient) UpdatePullRequest(ctx context.Context, owner, repository, title, body, targetBranchRef string, prId int, state vcsutils.PullRequestState) (err error) {
2✔
317
        bitbucketClient := client.buildBitbucketClient(ctx)
2✔
318
        apiResponse, err := bitbucketClient.GetPullRequest(owner, repository, prId)
2✔
319
        if err != nil {
3✔
320
                return
1✔
321
        }
1✔
322
        version := apiResponse.Values["version"]
1✔
323
        editOptions := bitbucketv1.EditPullRequestOptions{
1✔
324
                Version:     fmt.Sprintf("%v", version),
1✔
325
                ID:          int64(prId),
1✔
326
                State:       *vcsutils.MapPullRequestState(&state),
1✔
327
                Title:       title,
1✔
328
                Description: body,
1✔
329
        }
1✔
330
        _, err = bitbucketClient.UpdatePullRequest(owner, repository, &editOptions)
1✔
331
        return err
1✔
332
}
333

334
// ListOpenPullRequestsWithBody on Bitbucket server
335
func (client *BitbucketServerClient) ListOpenPullRequestsWithBody(ctx context.Context, owner, repository string) ([]PullRequestInfo, error) {
1✔
336
        return client.getOpenPullRequests(ctx, owner, repository, true)
1✔
337
}
1✔
338

339
// ListOpenPullRequests on Bitbucket server
340
func (client *BitbucketServerClient) ListOpenPullRequests(ctx context.Context, owner, repository string) ([]PullRequestInfo, error) {
1✔
341
        return client.getOpenPullRequests(ctx, owner, repository, false)
1✔
342
}
1✔
343

344
func (client *BitbucketServerClient) getOpenPullRequests(ctx context.Context, owner, repository string, withBody bool) ([]PullRequestInfo, error) {
2✔
345
        bitbucketClient := client.buildBitbucketClient(ctx)
2✔
346
        var results []PullRequestInfo
2✔
347
        var apiResponse *bitbucketv1.APIResponse
2✔
348
        for isLastPage, nextPageStart := true, 0; isLastPage; isLastPage, nextPageStart = bitbucketv1.HasNextPage(apiResponse) {
4✔
349
                var err error
2✔
350
                apiResponse, err = bitbucketClient.GetPullRequestsPage(owner, repository, createPaginationOptions(nextPageStart))
2✔
351
                if err != nil {
2✔
352
                        return nil, err
×
353
                }
×
354
                var pullRequests []bitbucketv1.PullRequest
2✔
355
                pullRequests, err = bitbucketv1.GetPullRequestsResponse(apiResponse)
2✔
356
                if err != nil {
2✔
357
                        return nil, err
×
358
                }
×
359
                for _, pullRequest := range pullRequests {
4✔
360
                        if pullRequest.Open {
4✔
361
                                var pullRequestInfo PullRequestInfo
2✔
362
                                if pullRequestInfo, err = mapBitbucketServerPullRequestToPullRequestInfo(pullRequest, withBody, owner); err != nil {
2✔
363
                                        return nil, err
×
364
                                }
×
365
                                results = append(results, pullRequestInfo)
2✔
366
                        }
367
                }
368
        }
369
        return results, nil
2✔
370
}
371

372
// GetPullRequestInfoById on bitbucket server
373
func (client *BitbucketServerClient) GetPullRequestByID(ctx context.Context, owner, repository string, pullRequestId int) (pullRequestInfo PullRequestInfo, err error) {
4✔
374
        client.logger.Debug("fetching pull request by ID in ", repository)
4✔
375
        bitbucketClient := client.buildBitbucketClient(ctx)
4✔
376
        apiResponse, err := bitbucketClient.GetPullRequest(owner, repository, pullRequestId)
4✔
377
        if err != nil {
6✔
378
                return PullRequestInfo{}, err
2✔
379
        }
2✔
380
        if apiResponse != nil {
4✔
381
                if err = vcsutils.CheckResponseStatusWithBody(apiResponse.Response, http.StatusOK); err != nil {
2✔
382
                        return PullRequestInfo{}, err
×
383
                }
×
384
        }
385
        pullRequest, err := bitbucketv1.GetPullRequestResponse(apiResponse)
2✔
386
        if err != nil {
2✔
387
                return
×
388
        }
×
389
        pullRequestInfo, err = mapBitbucketServerPullRequestToPullRequestInfo(pullRequest, false, owner)
2✔
390
        return
2✔
391
}
392

393
func mapBitbucketServerPullRequestToPullRequestInfo(pullRequest bitbucketv1.PullRequest, withBody bool, owner string) (PullRequestInfo, error) {
4✔
394
        sourceOwner, err := getSourceRepositoryOwner(pullRequest)
4✔
395
        if err != nil {
5✔
396
                return PullRequestInfo{}, err
1✔
397
        }
1✔
398
        var body string
3✔
399
        if withBody {
4✔
400
                body = pullRequest.Description
1✔
401
        }
1✔
402
        return PullRequestInfo{
3✔
403
                ID:     int64(pullRequest.ID),
3✔
404
                Title:  pullRequest.Title,
3✔
405
                Author: pullRequest.Author.User.Name,
3✔
406
                Source: BranchInfo{Name: pullRequest.FromRef.DisplayID, Repository: pullRequest.ToRef.Repository.Slug, Owner: sourceOwner},
3✔
407
                Target: BranchInfo{Name: pullRequest.ToRef.DisplayID, Repository: pullRequest.ToRef.Repository.Slug, Owner: owner},
3✔
408
                Body:   body,
3✔
409
                URL:    pullRequest.Links.Self[0].Href,
3✔
410
        }, nil
3✔
411
}
412

413
// AddPullRequestComment on Bitbucket server
414
func (client *BitbucketServerClient) AddPullRequestComment(ctx context.Context, owner, repository, content string, pullRequestID int) error {
6✔
415
        return client.addPullRequestComment(ctx, owner, repository, pullRequestID, PullRequestComment{CommentInfo: CommentInfo{Content: content}})
6✔
416
}
6✔
417

418
// AddPullRequestReviewComments on Bitbucket server
419
func (client *BitbucketServerClient) AddPullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int, comments ...PullRequestComment) error {
2✔
420
        if len(comments) == 0 {
2✔
421
                return errors.New(vcsutils.ErrNoCommentsProvided)
×
422
        }
×
423
        for _, comment := range comments {
4✔
424
                if err := client.addPullRequestComment(ctx, owner, repository, pullRequestID, comment); err != nil {
3✔
425
                        return err
1✔
426
                }
1✔
427
        }
428
        return nil
1✔
429
}
430

431
func (client *BitbucketServerClient) addPullRequestComment(ctx context.Context, owner, repository string, pullRequestID int, comment PullRequestComment) error {
8✔
432
        err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository, "content": comment.Content})
8✔
433
        if err != nil {
12✔
434
                return err
4✔
435
        }
4✔
436
        bitbucketClient := client.buildBitbucketClient(ctx)
4✔
437
        // Determine the file path and anchor
4✔
438
        var anchor *bitbucketv1.Anchor
4✔
439
        if filePath := vcsutils.GetPullRequestFilePath(comment.NewFilePath); filePath != "" {
4✔
440
                anchor = &bitbucketv1.Anchor{
×
441
                        Line:     comment.NewStartLine,
×
442
                        LineType: "CONTEXT",
×
443
                        FileType: "FROM",
×
444
                        Path:     filePath,
×
445
                        SrcPath:  filePath,
×
446
                }
×
447
        }
×
448

449
        // Create the pull request comment
450
        commentData := bitbucketv1.Comment{
4✔
451
                Text:   comment.Content,
4✔
452
                Anchor: anchor,
4✔
453
        }
4✔
454
        _, err = bitbucketClient.CreatePullRequestComment(owner, repository, pullRequestID, commentData, []string{"application/json"})
4✔
455
        return err
4✔
456
}
457

458
// ListPullRequestReviewComments on Bitbucket server
459
func (client *BitbucketServerClient) ListPullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int) ([]CommentInfo, error) {
×
460
        return client.ListPullRequestComments(ctx, owner, repository, pullRequestID)
×
461
}
×
462

463
// ListPullRequestComments on Bitbucket server
464
func (client *BitbucketServerClient) ListPullRequestComments(ctx context.Context, owner, repository string, pullRequestID int) ([]CommentInfo, error) {
6✔
465
        bitbucketClient := client.buildBitbucketClient(ctx)
6✔
466
        var results []CommentInfo
6✔
467
        var apiResponse *bitbucketv1.APIResponse
6✔
468
        for isLastPage, nextPageStart := true, 0; isLastPage; isLastPage, nextPageStart = bitbucketv1.HasNextPage(apiResponse) {
12✔
469
                var err error
6✔
470
                apiResponse, err = bitbucketClient.GetActivities(owner, repository, int64(pullRequestID), createPaginationOptions(nextPageStart))
6✔
471
                if err != nil {
8✔
472
                        return nil, err
2✔
473
                }
2✔
474
                activities, err := bitbucketv1.GetActivitiesResponse(apiResponse)
4✔
475
                if err != nil {
4✔
476
                        return nil, err
×
477
                }
×
478
                for _, activity := range activities.Values {
10✔
479
                        // Add activity only if from type new comment.
6✔
480
                        if activity.Action == "COMMENTED" && activity.CommentAction == "ADDED" {
8✔
481
                                results = append(results, CommentInfo{
2✔
482
                                        ID:      int64(activity.Comment.ID),
2✔
483
                                        Created: time.Unix(activity.Comment.CreatedDate, 0),
2✔
484
                                        Content: activity.Comment.Text,
2✔
485
                                        Version: activity.Comment.Version,
2✔
486
                                })
2✔
487
                        }
2✔
488
                }
489
        }
490
        return results, nil
4✔
491
}
492

493
// DeletePullRequestReviewComments on Bitbucket server
494
func (client *BitbucketServerClient) DeletePullRequestReviewComments(ctx context.Context, owner, repository string, pullRequestID int, comments ...CommentInfo) error {
2✔
495
        for _, comment := range comments {
4✔
496
                if err := client.DeletePullRequestComment(ctx, owner, repository, pullRequestID, int(comment.ID)); err != nil {
3✔
497
                        return err
1✔
498
                }
1✔
499
        }
500
        return nil
1✔
501
}
502

503
func (client *BitbucketServerClient) ListPullRequestReviews(ctx context.Context, owner, repository string, pullRequestID int) ([]PullRequestReviewDetails, error) {
1✔
504
        return nil, errBitbucketListListPullRequestReviewsNotSupported
1✔
505
}
1✔
506

507
func (client *BitbucketServerClient) ListPullRequestsAssociatedWithCommit(ctx context.Context, owner, repository string, commitSHA string) ([]PullRequestInfo, error) {
1✔
508
        return nil, errBitbucketListPullRequestAssociatedCommitsNotSupported
1✔
509
}
1✔
510

511
// DeletePullRequestComment on Bitbucket Server
512
func (client *BitbucketServerClient) DeletePullRequestComment(ctx context.Context, owner, repository string, pullRequestID, commentID int) error {
4✔
513
        bitbucketClient := client.buildBitbucketClient(ctx)
4✔
514
        comments, err := client.ListPullRequestComments(ctx, owner, repository, pullRequestID)
4✔
515
        if err != nil {
6✔
516
                return err
2✔
517
        }
2✔
518
        commentVersion := 0
2✔
519
        for _, comment := range comments {
2✔
520
                if comment.ID == int64(commentID) {
×
521
                        commentVersion = comment.Version
×
522
                        break
×
523
                }
524
        }
525
        // #nosec G115
526
        if _, err = bitbucketClient.DeleteComment_2(owner, repository, int64(pullRequestID), int64(commentID), map[string]interface{}{"version": int32(commentVersion)}); err != nil && err != io.EOF {
2✔
527
                return fmt.Errorf("an error occurred while deleting pull request comment:\n%s", err.Error())
×
528
        }
×
529
        return nil
2✔
530
}
531

532
type projectsResponse struct {
533
        Values []struct {
534
                Key string `json:"key,omitempty"`
535
        } `json:"values,omitempty"`
536
}
537

538
// GetLatestCommit on Bitbucket server
539
func (client *BitbucketServerClient) GetLatestCommit(ctx context.Context, owner, repository, branch string) (CommitInfo, error) {
8✔
540
        commits, err := client.GetCommits(ctx, owner, repository, branch)
8✔
541
        if err != nil {
15✔
542
                return CommitInfo{}, err
7✔
543
        }
7✔
544

545
        latestCommit := CommitInfo{}
1✔
546
        if len(commits) > 0 {
2✔
547
                latestCommit = commits[0]
1✔
548
        }
1✔
549
        return latestCommit, nil
1✔
550
}
551

552
// GetCommits on Bitbucket server
553
func (client *BitbucketServerClient) GetCommits(ctx context.Context, owner, repository, branch string) ([]CommitInfo, error) {
10✔
554
        err := validateParametersNotBlank(map[string]string{
10✔
555
                "owner":      owner,
10✔
556
                "repository": repository,
10✔
557
                "branch":     branch,
10✔
558
        })
10✔
559
        if err != nil {
14✔
560
                return nil, err
4✔
561
        }
4✔
562

563
        options := map[string]interface{}{
6✔
564
                "limit": vcsutils.NumberOfCommitsToFetch,
6✔
565
                "until": branch,
6✔
566
        }
6✔
567
        return client.getCommitsWithQueryOptions(ctx, owner, repository, options)
6✔
568
}
569

570
func (client *BitbucketServerClient) GetCommitsWithQueryOptions(ctx context.Context, owner, repository string, listOptions GitCommitsQueryOptions) ([]CommitInfo, error) {
2✔
571
        err := validateParametersNotBlank(map[string]string{
2✔
572
                "owner":      owner,
2✔
573
                "repository": repository,
2✔
574
        })
2✔
575
        if err != nil {
2✔
576
                return nil, err
×
577
        }
×
578
        commits, err := client.getCommitsWithQueryOptions(ctx, owner, repository, convertToBitbucketOptionsMap(listOptions))
2✔
579
        if err != nil {
3✔
580
                return nil, err
1✔
581
        }
1✔
582
        return getCommitsInDateRate(commits, listOptions), nil
1✔
583
}
584

585
// Bitbucket doesn't support filtering by date, so we need to filter the commits by date ourselves.
586
func getCommitsInDateRate(commits []CommitInfo, options GitCommitsQueryOptions) []CommitInfo {
5✔
587
        commitsNumber := len(commits)
5✔
588
        if commitsNumber == 0 {
5✔
589
                return commits
×
590
        }
×
591

592
        firstCommit := time.Unix(commits[0].Timestamp, 0).UTC()
5✔
593
        lastCommit := time.Unix(commits[commitsNumber-1].Timestamp, 0).UTC()
5✔
594

5✔
595
        // If all commits are in the range return all.
5✔
596
        if lastCommit.After(options.Since) || lastCommit.Equal(options.Since) {
8✔
597
                return commits
3✔
598
        }
3✔
599
        // If the first commit is older than the "since" timestamp, all commits are out of range, return an empty list.
600
        if firstCommit.Before(options.Since) {
3✔
601
                return []CommitInfo{}
1✔
602
        }
1✔
603
        // Find the first commit that is older than the "since" timestamp.
604
        for i, commit := range commits {
4✔
605
                if time.Unix(commit.Timestamp, 0).UTC().Before(options.Since) {
4✔
606
                        return commits[:i]
1✔
607
                }
1✔
608
        }
609
        return []CommitInfo{}
×
610
}
611

612
func (client *BitbucketServerClient) getCommitsWithQueryOptions(ctx context.Context, owner, repository string, options map[string]interface{}) ([]CommitInfo, error) {
8✔
613
        bitbucketClient := client.buildBitbucketClient(ctx)
8✔
614

8✔
615
        apiResponse, err := bitbucketClient.GetCommits(owner, repository, options)
8✔
616
        if err != nil {
13✔
617
                return nil, err
5✔
618
        }
5✔
619
        commits, err := bitbucketv1.GetCommitsResponse(apiResponse)
3✔
620
        if err != nil {
3✔
621
                return nil, err
×
622
        }
×
623
        var commitsInfo []CommitInfo
3✔
624
        for _, commit := range commits {
9✔
625
                commitInfo := client.mapBitbucketServerCommitToCommitInfo(commit, owner, repository)
6✔
626
                commitsInfo = append(commitsInfo, commitInfo)
6✔
627
        }
6✔
628
        return commitsInfo, nil
3✔
629
}
630

631
func convertToBitbucketOptionsMap(listOptions GitCommitsQueryOptions) map[string]interface{} {
2✔
632
        return map[string]interface{}{
2✔
633
                "limit": listOptions.PerPage,
2✔
634
                "start": (listOptions.Page - 1) * listOptions.PerPage,
2✔
635
        }
2✔
636
}
2✔
637

638
// GetRepositoryInfo on Bitbucket server
639
func (client *BitbucketServerClient) GetRepositoryInfo(ctx context.Context, owner, repository string) (RepositoryInfo, error) {
6✔
640
        if err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository}); err != nil {
9✔
641
                return RepositoryInfo{}, err
3✔
642
        }
3✔
643

644
        bitbucketClient := client.buildBitbucketClient(ctx)
3✔
645

3✔
646
        repo, err := bitbucketClient.GetRepository(owner, repository)
3✔
647
        if err != nil {
4✔
648
                return RepositoryInfo{}, err
1✔
649
        }
1✔
650

651
        holder := struct {
2✔
652
                Links struct {
2✔
653
                        Clone []struct {
2✔
654
                                Name string `mapstructure:"name"`
2✔
655
                                HRef string `mapstructure:"href"`
2✔
656
                        } `mapstructure:"clone"`
2✔
657
                } `mapstructure:"links"`
2✔
658
                Public bool `mapstructure:"public"`
2✔
659
        }{}
2✔
660

2✔
661
        if err := mapstructure.Decode(repo.Values, &holder); err != nil {
2✔
662
                return RepositoryInfo{}, err
×
663
        }
×
664

665
        var info CloneInfo
2✔
666
        for _, cloneLink := range holder.Links.Clone {
6✔
667
                switch cloneLink.Name {
4✔
668
                case "http":
2✔
669
                        info.HTTP = cloneLink.HRef
2✔
670
                case "ssh":
2✔
671
                        info.SSH = cloneLink.HRef
2✔
672
                }
673
        }
674

675
        return RepositoryInfo{RepositoryVisibility: getBitbucketServerRepositoryVisibility(holder.Public), CloneInfo: info}, nil
2✔
676
}
677

678
// GetCommitBySha on Bitbucket server
679
func (client *BitbucketServerClient) GetCommitBySha(ctx context.Context, owner, repository, sha string) (CommitInfo, error) {
7✔
680
        err := validateParametersNotBlank(map[string]string{
7✔
681
                "owner":      owner,
7✔
682
                "repository": repository,
7✔
683
                "sha":        sha,
7✔
684
        })
7✔
685
        if err != nil {
11✔
686
                return CommitInfo{}, err
4✔
687
        }
4✔
688

689
        bitbucketClient := client.buildBitbucketClient(ctx)
3✔
690

3✔
691
        apiResponse, err := bitbucketClient.GetCommit(owner, repository, sha, nil)
3✔
692
        if err != nil {
5✔
693
                return CommitInfo{}, err
2✔
694
        }
2✔
695
        commit := bitbucketv1.Commit{}
1✔
696
        err = unmarshalAPIResponseValues(apiResponse, &commit)
1✔
697
        if err != nil {
1✔
698
                return CommitInfo{}, err
×
699
        }
×
700
        return client.mapBitbucketServerCommitToCommitInfo(commit, owner, repository), nil
1✔
701
}
702

703
// CreateLabel on Bitbucket server
704
func (client *BitbucketServerClient) CreateLabel(ctx context.Context, owner, repository string, labelInfo LabelInfo) error {
1✔
705
        return errLabelsNotSupported
1✔
706
}
1✔
707

708
// GetLabel on Bitbucket server
709
func (client *BitbucketServerClient) GetLabel(ctx context.Context, owner, repository, name string) (*LabelInfo, error) {
1✔
710
        return nil, errLabelsNotSupported
1✔
711
}
1✔
712

713
// ListPullRequestLabels on Bitbucket server
714
func (client *BitbucketServerClient) ListPullRequestLabels(ctx context.Context, owner, repository string, pullRequestID int) ([]string, error) {
1✔
715
        return nil, errLabelsNotSupported
1✔
716
}
1✔
717

718
// UnlabelPullRequest on Bitbucket server
719
func (client *BitbucketServerClient) UnlabelPullRequest(ctx context.Context, owner, repository, name string, pullRequestID int) error {
1✔
720
        return errLabelsNotSupported
1✔
721
}
1✔
722

723
// GetRepositoryEnvironmentInfo on Bitbucket server
724
func (client *BitbucketServerClient) GetRepositoryEnvironmentInfo(ctx context.Context, owner, repository, name string) (RepositoryEnvironmentInfo, error) {
1✔
725
        return RepositoryEnvironmentInfo{}, errBitbucketGetRepoEnvironmentInfoNotSupported
1✔
726
}
1✔
727

728
// Get all projects for which the authenticated user has the PROJECT_VIEW permission
729
func (client *BitbucketServerClient) listProjects(bitbucketClient *bitbucketv1.DefaultApiService) ([]string, error) {
2✔
730
        var apiResponse *bitbucketv1.APIResponse
2✔
731
        var err error
2✔
732
        var projects []string
2✔
733
        for isLastProjectsPage, nextProjectsPageStart := true, 0; isLastProjectsPage; isLastProjectsPage, nextProjectsPageStart = bitbucketv1.HasNextPage(apiResponse) {
4✔
734
                apiResponse, err = bitbucketClient.GetProjects(createPaginationOptions(nextProjectsPageStart))
2✔
735
                if err != nil {
3✔
736
                        return nil, err
1✔
737
                }
1✔
738
                projectsResponse := &projectsResponse{}
1✔
739
                err = unmarshalAPIResponseValues(apiResponse, projectsResponse)
1✔
740
                if err != nil {
1✔
741
                        return nil, err
×
742
                }
×
743
                for _, project := range projectsResponse.Values {
2✔
744
                        projects = append(projects, project.Key)
1✔
745
                }
1✔
746
        }
747
        // Add user's private project
748
        username := apiResponse.Header.Get("X-Ausername")
1✔
749
        if username == "" {
1✔
750
                return []string{}, errors.New("X-Ausername header is missing")
×
751
        }
×
752
        // project keys are upper case
753
        projects = append(projects, "~"+strings.ToUpper(username))
1✔
754
        return projects, nil
1✔
755
}
756

757
// DownloadFileFromRepo on Bitbucket server
758
func (client *BitbucketServerClient) DownloadFileFromRepo(ctx context.Context, owner, repository, branch, path string) ([]byte, int, error) {
3✔
759
        bitbucketClient := client.buildBitbucketClient(ctx)
3✔
760

3✔
761
        var statusCode int
3✔
762
        bbResp, err := bitbucketClient.GetContent_11(owner, repository, path, map[string]interface{}{"at": branch})
3✔
763
        if bbResp != nil && bbResp.Response != nil {
5✔
764
                statusCode = bbResp.Response.StatusCode
2✔
765
        }
2✔
766
        if err != nil {
5✔
767
                return nil, statusCode, err
2✔
768
        }
2✔
769
        return bbResp.Payload, statusCode, err
1✔
770
}
771

772
func createPaginationOptions(nextPageStart int) map[string]interface{} {
14✔
773
        return map[string]interface{}{"start": nextPageStart}
14✔
774
}
14✔
775

776
func unmarshalAPIResponseValues(response *bitbucketv1.APIResponse, target interface{}) error {
3✔
777
        responseBytes, err := json.Marshal(response.Values)
3✔
778
        if err != nil {
3✔
779
                return err
×
780
        }
×
781
        return json.Unmarshal(responseBytes, &target)
3✔
782
}
783

784
func getBitbucketServerWebhookID(r *bitbucketv1.APIResponse) (string, error) {
1✔
785
        webhook := &bitbucketv1.Webhook{}
1✔
786
        err := unmarshalAPIResponseValues(r, webhook)
1✔
787
        if err != nil {
1✔
788
                return "", err
×
789
        }
×
790
        return strconv.Itoa(webhook.ID), nil
1✔
791
}
792

793
func createBitbucketServerHook(token, payloadURL string, webhookEvents ...vcsutils.WebhookEvent) *map[string]interface{} {
4✔
794
        return &map[string]interface{}{
4✔
795
                "url":           payloadURL,
4✔
796
                "configuration": map[string]interface{}{"secret": token},
4✔
797
                "events":        getBitbucketServerWebhookEvents(webhookEvents...),
4✔
798
        }
4✔
799
}
4✔
800

801
// Get varargs of webhook events and return a slice of Bitbucket server webhook events
802
func getBitbucketServerWebhookEvents(webhookEvents ...vcsutils.WebhookEvent) []string {
4✔
803
        events := make([]string, 0, len(webhookEvents))
4✔
804
        for _, event := range webhookEvents {
16✔
805
                switch event {
12✔
806
                case vcsutils.PrOpened:
2✔
807
                        events = append(events, "pr:opened")
2✔
808
                case vcsutils.PrEdited:
2✔
809
                        events = append(events, "pr:from_ref_updated")
2✔
810
                case vcsutils.PrMerged:
2✔
811
                        events = append(events, "pr:merged")
2✔
812
                case vcsutils.PrRejected:
2✔
813
                        events = append(events, "pr:declined", "pr:deleted")
2✔
814
                case vcsutils.Push, vcsutils.TagPushed, vcsutils.TagRemoved:
4✔
815
                        events = append(events, "repo:refs_changed")
4✔
816
                }
817
        }
818
        return events
4✔
819
}
820

821
func (client *BitbucketServerClient) mapBitbucketServerCommitToCommitInfo(commit bitbucketv1.Commit,
822
        owner, repo string) CommitInfo {
7✔
823
        parents := make([]string, len(commit.Parents))
7✔
824
        for i, p := range commit.Parents {
20✔
825
                parents[i] = p.ID
13✔
826
        }
13✔
827
        url := fmt.Sprintf("%s/projects/%s/repos/%s/commits/%s",
7✔
828
                strings.TrimSuffix(client.vcsInfo.APIEndpoint, "/rest"), owner, repo, commit.ID)
7✔
829
        return CommitInfo{
7✔
830
                Hash:          commit.ID,
7✔
831
                AuthorName:    commit.Author.Name,
7✔
832
                CommitterName: commit.Committer.Name,
7✔
833
                Url:           url,
7✔
834
                // Convert from bitbucket millisecond timestamp to CommitInfo seconds timestamp.
7✔
835
                Timestamp:    commit.CommitterTimestamp / 1000,
7✔
836
                Message:      commit.Message,
7✔
837
                ParentHashes: parents,
7✔
838
                AuthorEmail:  commit.Author.EmailAddress,
7✔
839
        }
7✔
840
}
841

842
func (client *BitbucketServerClient) UploadCodeScanning(ctx context.Context, owner string, repository string, branch string, scanResults string) (string, error) {
1✔
843
        return "", errBitbucketCodeScanningNotSupported
1✔
844
}
1✔
845

846
type diffPayload struct {
847
        Diffs []struct {
848
                Source struct {
849
                        ToString string `mapstructure:"toString"`
850
                } `mapstructure:"source"`
851
                Destination struct {
852
                        ToString string `mapstructure:"toString"`
853
                } `mapstructure:"destination"`
854
        } `mapstructure:"diffs"`
855
}
856

857
func (client *BitbucketServerClient) GetModifiedFiles(ctx context.Context, owner, repository, refBefore, refAfter string) ([]string, error) {
6✔
858
        err := validateParametersNotBlank(map[string]string{
6✔
859
                "owner":      owner,
6✔
860
                "repository": repository,
6✔
861
                "refBefore":  refBefore,
6✔
862
                "refAfter":   refAfter,
6✔
863
        })
6✔
864
        if err != nil {
10✔
865
                return nil, err
4✔
866
        }
4✔
867

868
        bitbucketClient := client.buildBitbucketClient(ctx)
2✔
869

2✔
870
        params := map[string]interface{}{"contextLines": int32(0), "from": refAfter, "to": refBefore}
2✔
871
        resp, err := bitbucketClient.StreamDiff_37(owner, repository, "", params)
2✔
872
        if err != nil {
3✔
873
                return nil, err
1✔
874
        }
1✔
875

876
        dst, err := vcsutils.RemapFields[diffPayload](resp.Values, "")
1✔
877
        if err != nil {
1✔
878
                return nil, err
×
879
        }
×
880

881
        fileNamesSet := datastructures.MakeSet[string]()
1✔
882
        for _, diff := range dst.Diffs {
4✔
883
                fileNamesSet.Add(diff.Source.ToString)
3✔
884
                fileNamesSet.Add(diff.Destination.ToString)
3✔
885
        }
3✔
886
        _ = fileNamesSet.Remove("") // Make sure there are no blank filepath.
1✔
887
        fileNamesList := fileNamesSet.ToSlice()
1✔
888
        sort.Strings(fileNamesList)
1✔
889
        return fileNamesList, nil
1✔
890
}
891

NEW
892
func (client *BitbucketServerClient) CreateBranch(ctx context.Context, owner, repository, sourceBranch, newBranch string) error {
×
NEW
893
        return errBitbucketCreateBranchNotSupported
×
NEW
894
}
×
895

NEW
896
func (client *BitbucketServerClient) AllowWorkflows(ctx context.Context, owner string) error {
×
NEW
897
        return errBitbucketAllowWorkflowsNotSupported
×
NEW
898
}
×
899

NEW
900
func (client *BitbucketServerClient) AddOrganizationSecret(ctx context.Context, owner, secretName, secretValue string) error {
×
NEW
901
        return errBitbucketAddOrganizationSecretNotSupported
×
NEW
902
}
×
903

NEW
904
func (client *BitbucketServerClient) CommitAndPushFiles(ctx context.Context, owner, repo, sourceBranch, commitMessage, authorName, authorEmail string, files []FileToCommit) error {
×
NEW
905
        return errBitbucketCommitAndPushFilesNotSupported
×
NEW
906
}
×
907

NEW
908
func (client *BitbucketServerClient) GetRepoCollaborators(ctx context.Context, owner, repo, affiliation, permission string) ([]string, error) {
×
NEW
909
        return nil, errBitbucketGetRepoCollaboratorsNotSupported
×
NEW
910
}
×
911

NEW
912
func (client *BitbucketServerClient) GetRepoTeamsByPermissions(ctx context.Context, owner, repo string, permissions []string) ([]int64, error) {
×
NEW
913
        return nil, errBitbucketGetRepoTeamsByPermissionsNotSupported
×
NEW
914
}
×
915

NEW
916
func (client *BitbucketServerClient) CreateOrUpdateEnvironment(ctx context.Context, owner, repo, envName string, teams []int64, users []string) error {
×
NEW
917
        return errBitbucketCreateOrUpdateEnvironmentNotSupported
×
NEW
918
}
×
919

NEW
920
func (client *BitbucketServerClient) MergePullRequest(ctx context.Context, owner, repo string, prNumber int, commitMessage string) error {
×
NEW
921
        return errBitbucketMergePullRequestNotSupported
×
NEW
922
}
×
923

924
func getBitbucketServerRepositoryVisibility(public bool) RepositoryVisibility {
4✔
925
        if public {
7✔
926
                return Public
3✔
927
        }
3✔
928
        return Private
1✔
929
}
930

931
func getSourceRepositoryOwner(pullRequest bitbucketv1.PullRequest) (string, error) {
4✔
932
        project := pullRequest.FromRef.Repository.Project
4✔
933
        if project == nil {
5✔
934
                return "", fmt.Errorf("failed to get source repository owner, project is nil. (PR - %s, repository - %s)", pullRequest.FromRef.DisplayID, pullRequest.FromRef.Repository.Slug)
1✔
935
        }
1✔
936
        return project.Key, nil
3✔
937
}
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