github.com/jenkins-x/jx/v2@v2.1.155/pkg/gits/bitbucket_server.go (about) 1 package gits 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "net/url" 8 "os" 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/blang/semver" 14 "github.com/pkg/errors" 15 16 "github.com/google/go-github/v32/github" 17 "github.com/mitchellh/mapstructure" 18 19 bitbucket "github.com/gfleury/go-bitbucket-v1" 20 "github.com/jenkins-x/jx-logging/pkg/log" 21 "github.com/jenkins-x/jx/v2/pkg/auth" 22 "github.com/jenkins-x/jx/v2/pkg/util" 23 ) 24 25 // pageLimit is used for the page size for API responses 26 const pageLimit = 25 27 28 var ( 29 // BaseWebHooks are webhooks we enable on all versions 30 BaseWebHooks = []string{"repo:refs_changed", "repo:modified", "repo:forked", "repo:comment:added", "repo:comment:edited", "repo:comment:deleted", "pr:opened", "pr:reviewer:approved", "pr:reviewer:unapproved", "pr:reviewer:needs_work", "pr:merged", "pr:declined", "pr:deleted", "pr:comment:added", "pr:comment:edited", "pr:comment:deleted", "pr:modified"} 31 // Ver7WebHooks are additional webhooks we enable on server versions >= 7.x 32 Ver7WebHooks = []string{"pr:from_ref_updated"} 33 ) 34 35 // BitbucketServerProvider implements GitProvider interface for a bitbucket server 36 type BitbucketServerProvider struct { 37 Client *bitbucket.APIClient 38 Username string 39 Context context.Context 40 41 Server auth.AuthServer 42 User auth.UserAuth 43 Git Gitter 44 } 45 46 type projectsPage struct { 47 Size int `json:"size"` 48 Limit int `json:"limit"` 49 Start int `json:"start"` 50 NextPageStart int `json:"nextPageStart"` 51 IsLastPage bool `json:"isLastPage"` 52 Values []bitbucket.Project `json:"values"` 53 } 54 55 type commitsPage struct { 56 Size int `json:"size"` 57 Limit int `json:"limit"` 58 Start int `json:"start"` 59 NextPageStart int `json:"nextPageStart"` 60 IsLastPage bool `json:"isLastPage"` 61 Values []bitbucket.Commit `json:"values"` 62 } 63 64 type buildStatusesPage struct { 65 Size int `json:"size"` 66 Limit int `json:"limit"` 67 Start int `json:"start"` 68 IsLastPage bool `json:"isLastPage"` 69 Values []bitbucket.BuildStatus `json:"values"` 70 } 71 72 type reposPage struct { 73 Size int `json:"size"` 74 Limit int `json:"limit"` 75 Start int `json:"start"` 76 NextPageStart int `json:"nextPageStart"` 77 IsLastPage bool `json:"isLastPage"` 78 Values []bitbucket.Repository `json:"values"` 79 } 80 81 type pullRequestPage struct { 82 Size int `json:"size"` 83 Limit int `json:"limit"` 84 Start int `json:"start"` 85 NextPageStart int `json:"nextPageStart"` 86 IsLastPage bool `json:"isLastPage"` 87 Values []bitbucket.PullRequest `json:"values"` 88 } 89 90 type webHooksPage struct { 91 Size int `json:"size"` 92 Limit int `json:"limit"` 93 Start int `json:"start"` 94 NextPageStart int `json:"nextPageStart"` 95 IsLastPage bool `json:"isLastPage"` 96 Values []webHook `json:"values"` 97 } 98 99 type webHook struct { 100 ID int64 `json:"id"` 101 Name string `json:"name"` 102 CreatedDate int64 `json:"createdDate"` 103 UpdatedDate int64 `json:"updatedDate"` 104 Events []string `json:"events"` 105 Configuration map[string]interface{} `json:"configuration"` 106 URL string `json:"url"` 107 Active bool `json:"active"` 108 } 109 110 func NewBitbucketServerProvider(server *auth.AuthServer, user *auth.UserAuth, git Gitter) (GitProvider, error) { 111 ctx := context.Background() 112 apiKeyAuthContext := context.WithValue(ctx, bitbucket.ContextAccessToken, user.ApiToken) 113 114 provider := BitbucketServerProvider{ 115 Server: *server, 116 User: *user, 117 Username: user.Username, 118 Context: apiKeyAuthContext, 119 Git: git, 120 } 121 122 cfg := bitbucket.NewConfiguration(server.URL + "/rest") 123 provider.Client = bitbucket.NewAPIClient(apiKeyAuthContext, cfg) 124 125 return &provider, nil 126 } 127 128 func BitbucketServerRepositoryToGitRepository(bRepo bitbucket.Repository) *GitRepository { 129 var sshURL string 130 var httpCloneURL string 131 for _, link := range bRepo.Links.Clone { 132 if link.Name == "ssh" { 133 sshURL = link.Href 134 } 135 } 136 isFork := false 137 138 if httpCloneURL == "" { 139 cloneLinks := bRepo.Links.Clone 140 141 for _, link := range cloneLinks { 142 if link.Name == "http" { 143 httpCloneURL = link.Href 144 if !strings.HasSuffix(httpCloneURL, ".git") { 145 httpCloneURL += ".git" 146 } 147 } 148 } 149 } 150 if httpCloneURL == "" { 151 httpCloneURL = sshURL 152 } 153 154 htmlURL := bRepo.Links.Self[0].Href 155 return &GitRepository{ 156 Name: bRepo.Name, 157 HTMLURL: htmlURL, 158 CloneURL: httpCloneURL, 159 SSHURL: sshURL, 160 URL: htmlURL, 161 Fork: isFork, 162 Private: !bRepo.Public, 163 Organisation: strings.ToLower(bRepo.Project.Key), 164 Project: bRepo.Project.Key, 165 } 166 } 167 168 func (b *BitbucketServerProvider) GetRepository(org string, name string) (*GitRepository, error) { 169 var repo bitbucket.Repository 170 apiResponse, err := b.Client.DefaultApi.GetRepository(org, name) 171 172 if err != nil { 173 return nil, err 174 } 175 176 err = mapstructure.Decode(apiResponse.Values, &repo) 177 if err != nil { 178 return nil, err 179 } 180 181 return BitbucketServerRepositoryToGitRepository(repo), nil 182 } 183 184 func (b *BitbucketServerProvider) ListOrganisations() ([]GitOrganisation, error) { 185 var orgsPage projectsPage 186 orgsList := []GitOrganisation{} 187 paginationOptions := make(map[string]interface{}) 188 189 paginationOptions["start"] = 0 190 paginationOptions["limit"] = pageLimit 191 for { 192 apiResponse, err := b.Client.DefaultApi.GetProjects(paginationOptions) 193 if err != nil { 194 return nil, err 195 } 196 197 err = mapstructure.Decode(apiResponse.Values, &orgsPage) 198 if err != nil { 199 return nil, err 200 } 201 202 for _, project := range orgsPage.Values { 203 orgsList = append(orgsList, GitOrganisation{Login: project.Key}) 204 } 205 206 if orgsPage.IsLastPage { 207 break 208 } 209 if !b.moveToNextPage(paginationOptions, orgsPage.NextPageStart) { 210 break 211 } 212 } 213 214 return orgsList, nil 215 } 216 217 func (b *BitbucketServerProvider) ListRepositories(org string) ([]*GitRepository, error) { 218 var reposPage reposPage 219 repos := []*GitRepository{} 220 paginationOptions := make(map[string]interface{}) 221 222 paginationOptions["start"] = 0 223 paginationOptions["limit"] = pageLimit 224 225 for { 226 apiResponse, err := b.Client.DefaultApi.GetRepositoriesWithOptions(org, paginationOptions) 227 if err != nil { 228 return nil, err 229 } 230 231 err = mapstructure.Decode(apiResponse.Values, &reposPage) 232 if err != nil { 233 return nil, err 234 } 235 236 for _, bRepo := range reposPage.Values { 237 repos = append(repos, BitbucketServerRepositoryToGitRepository(bRepo)) 238 } 239 240 if reposPage.IsLastPage { 241 break 242 } 243 if !b.moveToNextPage(paginationOptions, reposPage.NextPageStart) { 244 break 245 } 246 } 247 248 return repos, nil 249 } 250 251 func (b *BitbucketServerProvider) CreateRepository(org, name string, private bool) (*GitRepository, error) { 252 var repo bitbucket.Repository 253 254 repoRequest := map[string]interface{}{ 255 "name": name, 256 "public": !private, 257 } 258 259 requestBody, err := json.Marshal(repoRequest) 260 if err != nil { 261 return nil, err 262 } 263 264 apiResponse, err := b.Client.DefaultApi.CreateRepositoryWithOptions(org, requestBody, []string{"application/json"}) 265 if err != nil { 266 return nil, err 267 } 268 269 err = mapstructure.Decode(apiResponse.Values, &repo) 270 if err != nil { 271 return nil, err 272 } 273 274 return BitbucketServerRepositoryToGitRepository(repo), nil 275 } 276 277 func (b *BitbucketServerProvider) DeleteRepository(org, name string) error { 278 _, err := b.Client.DefaultApi.DeleteRepository(org, name) 279 280 return err 281 } 282 283 func (b *BitbucketServerProvider) RenameRepository(org, name, newName string) (*GitRepository, error) { 284 var repo bitbucket.Repository 285 var options = map[string]interface{}{ 286 "name": newName, 287 } 288 289 requestBody, err := json.Marshal(options) 290 if err != nil { 291 return nil, err 292 } 293 294 apiResponse, err := b.Client.DefaultApi.UpdateRepositoryWithOptions(org, name, requestBody, []string{"application/json"}) 295 if err != nil { 296 return nil, err 297 } 298 299 err = mapstructure.Decode(apiResponse.Values, &repo) 300 if err != nil { 301 return nil, err 302 } 303 304 return BitbucketServerRepositoryToGitRepository(repo), nil 305 } 306 307 func (b *BitbucketServerProvider) ValidateRepositoryName(org, name string) error { 308 apiResponse, err := b.Client.DefaultApi.GetRepository(org, name) 309 310 if apiResponse != nil && apiResponse.Response.StatusCode == 404 { 311 return nil 312 } 313 314 if err == nil { 315 return fmt.Errorf("repository %s/%s already exists", b.Username, name) 316 } 317 318 return err 319 } 320 321 func (b *BitbucketServerProvider) ForkRepository(originalOrg, name, destinationOrg string) (*GitRepository, error) { 322 var repo bitbucket.Repository 323 var apiResponse *bitbucket.APIResponse 324 var options = map[string]interface{}{} 325 326 if destinationOrg != "" { 327 options["project"] = map[string]interface{}{ 328 "key": destinationOrg, 329 } 330 } 331 332 requestBody, err := json.Marshal(options) 333 if err != nil { 334 return nil, err 335 } 336 337 _, err = b.Client.DefaultApi.ForkRepository(originalOrg, name, requestBody, []string{"application/json"}) 338 if err != nil { 339 return nil, err 340 } 341 342 // Wait up to 1 minute for the fork to be ready 343 for i := 0; i < 30; i++ { 344 time.Sleep(2 * time.Second) 345 346 if destinationOrg == "" { 347 apiResponse, err = b.Client.DefaultApi.GetUserRepository(b.CurrentUsername(), name) 348 } else { 349 apiResponse, err = b.Client.DefaultApi.GetRepository(destinationOrg, name) 350 } 351 352 if err == nil { 353 break 354 } 355 } 356 if err != nil { 357 return nil, err 358 } 359 360 err = mapstructure.Decode(apiResponse.Values, &repo) 361 if err != nil { 362 return nil, err 363 } 364 365 return BitbucketServerRepositoryToGitRepository(repo), nil 366 } 367 368 func (b *BitbucketServerProvider) CreatePullRequest(data *GitPullRequestArguments) (*GitPullRequest, error) { 369 if data.GitRepository.Organisation == "" { 370 data.GitRepository.Organisation = data.GitRepository.Project 371 } 372 var bPullRequest, bPR bitbucket.PullRequest 373 var options = map[string]interface{}{ 374 "title": data.Title, 375 "description": data.Body, 376 "state": "OPEN", 377 "open": true, 378 "closed": false, 379 "fromRef": map[string]interface{}{ 380 "id": data.Head, 381 "repository": map[string]interface{}{ 382 "slug": data.GitRepository.Name, 383 "project": map[string]interface{}{ 384 "key": strings.ToUpper(data.GitRepository.Organisation), 385 }, 386 }, 387 }, 388 "toRef": map[string]interface{}{ 389 "id": data.Base, 390 "repository": map[string]interface{}{ 391 "slug": data.GitRepository.Name, 392 "project": map[string]interface{}{ 393 "key": strings.ToUpper(data.GitRepository.Organisation), 394 }, 395 }, 396 }, 397 } 398 399 requestBody, err := json.Marshal(options) 400 if err != nil { 401 return nil, err 402 } 403 404 apiResponse, err := b.Client.DefaultApi.CreatePullRequestWithOptions(strings.ToUpper(data.GitRepository.Organisation), data.GitRepository.Name, requestBody) 405 if err != nil { 406 return nil, err 407 } 408 409 err = mapstructure.Decode(apiResponse.Values, &bPullRequest) 410 if err != nil { 411 return nil, err 412 } 413 414 // Wait up to 1 minute for the pull request to be ready 415 for i := 0; i < 30; i++ { 416 time.Sleep(2 * time.Second) 417 418 apiResponse, err = b.Client.DefaultApi.GetPullRequest(strings.ToUpper(data.GitRepository.Project), data.GitRepository.Name, bPullRequest.ID) 419 if err == nil { 420 break 421 } 422 } 423 if err != nil { 424 return nil, err 425 } 426 427 err = mapstructure.Decode(apiResponse.Values, &bPR) 428 if err != nil { 429 return nil, err 430 } 431 432 return &GitPullRequest{ 433 URL: bPR.Links.Self[0].Href, 434 Owner: bPR.Author.User.Name, 435 Repo: bPR.ToRef.Repository.Name, 436 Number: &bPR.ID, 437 State: &bPR.State, 438 Title: bPR.Title, 439 }, nil 440 } 441 442 // UpdatePullRequest updates pull request number with data 443 func (b *BitbucketServerProvider) UpdatePullRequest(data *GitPullRequestArguments, number int) (*GitPullRequest, error) { 444 return nil, errors.Errorf("Not yet implemented for bitbucket") 445 } 446 447 func parseBitBucketServerURL(URL string) (string, string) { 448 var projectKey, repoName, subString string 449 var projectsIndex, reposIndex, repoEndIndex int 450 451 if strings.HasSuffix(URL, ".git") { 452 subString = strings.TrimSuffix(URL, ".git") 453 reposIndex = strings.LastIndex(subString, "/") 454 repoName = subString[reposIndex+1:] 455 456 subString = strings.TrimSuffix(subString, "/"+repoName) 457 projectsIndex = strings.LastIndex(subString, "/") 458 projectKey = subString[projectsIndex+1:] 459 460 } else { 461 projectsIndex = strings.Index(URL, "projects/") 462 subString = URL[projectsIndex+9:] 463 projectKey = subString[:strings.Index(subString, "/")] 464 465 reposIndex = strings.Index(subString, "repos/") 466 subString = subString[reposIndex+6:] 467 468 repoEndIndex = strings.Index(subString, "/") 469 470 if repoEndIndex == -1 { 471 repoName = subString 472 } else { 473 repoName = subString[:repoEndIndex] 474 } 475 } 476 477 return projectKey, repoName 478 } 479 480 func getMergeCommitSHAFromPRActivity(prActivity map[string]interface{}) *string { 481 var activity []map[string]interface{} 482 var mergeCommit map[string]interface{} 483 484 mapstructure.Decode(prActivity["values"], &activity) //nolint:errcheck 485 for _, act := range activity { 486 if act["action"] == "MERGED" { 487 mapstructure.Decode(act["commit"], &mergeCommit) //nolint:errcheck 488 break 489 } 490 } 491 commitSHA := "" 492 if _, ok := mergeCommit["id"]; ok { 493 commitSHA = mergeCommit["id"].(string) 494 } 495 return &commitSHA 496 } 497 498 func getLastCommitSHAFromPRCommits(prCommits map[string]interface{}) string { 499 return getLastCommitFromPRCommits(prCommits).ID 500 } 501 502 func getLastCommitFromPRCommits(prCommits map[string]interface{}) *bitbucket.Commit { 503 var commits []bitbucket.Commit 504 mapstructure.Decode(prCommits["values"], &commits) //nolint:errcheck 505 return &commits[0] 506 } 507 508 func (b *BitbucketServerProvider) UpdatePullRequestStatus(pr *GitPullRequest) error { 509 var bitbucketPR bitbucket.PullRequest 510 511 prID := *pr.Number 512 projectKey, repo := parseBitBucketServerURL(pr.URL) 513 apiResponse, err := b.Client.DefaultApi.GetPullRequest(projectKey, repo, prID) 514 if err != nil { 515 return err 516 } 517 518 err = mapstructure.Decode(apiResponse.Values, &bitbucketPR) 519 if err != nil { 520 return err 521 } 522 523 err = b.populatePullRequest(pr, &bitbucketPR) 524 if err != nil { 525 return err 526 } 527 return nil 528 } 529 530 func (b *BitbucketServerProvider) GetPullRequest(owner string, repo *GitRepository, number int) (*GitPullRequest, error) { 531 var bPR *bitbucket.PullRequest 532 533 apiResponse, err := b.Client.DefaultApi.GetPullRequest(strings.ToUpper(owner), repo.Name, number) 534 if err != nil { 535 return nil, err 536 } 537 538 err = mapstructure.Decode(apiResponse.Values, &bPR) 539 if err != nil { 540 return nil, err 541 } 542 543 return b.toPullRequest(bPR) 544 } 545 546 func (b *BitbucketServerProvider) toPullRequest(bPR *bitbucket.PullRequest) (*GitPullRequest, error) { 547 answer := &GitPullRequest{} 548 549 err := b.populatePullRequest(answer, bPR) 550 if err != nil { 551 return nil, err 552 } 553 return answer, nil 554 } 555 556 func (b *BitbucketServerProvider) populatePullRequest(answer *GitPullRequest, bPR *bitbucket.PullRequest) error { 557 var prCommits, prActivity map[string]interface{} 558 author := &GitUser{ 559 URL: bPR.Author.User.Links.Self[0].Href, 560 Login: bPR.Author.User.Slug, 561 Name: bPR.Author.User.Name, 562 Email: bPR.Author.User.EmailAddress, 563 } 564 answer.URL = bPR.Links.Self[0].Href 565 answer.Owner = bPR.ToRef.Repository.Project.Key 566 answer.Repo = bPR.ToRef.Repository.Name 567 answer.Number = &bPR.ID 568 answer.State = &bPR.State 569 answer.Author = author 570 answer.LastCommitSha = bPR.FromRef.LatestCommit 571 answer.Title = bPR.Title 572 answer.Body = bPR.Description 573 574 if bPR.State == "MERGED" { 575 merged := true 576 answer.Merged = &merged 577 apiResponse, err := b.Client.DefaultApi.GetPullRequestActivity(answer.Owner, answer.Repo, *answer.Number) 578 if err != nil { 579 return err 580 } 581 582 err = mapstructure.Decode(apiResponse.Values, &prActivity) 583 if err != nil { 584 return err 585 } 586 answer.MergeCommitSHA = getMergeCommitSHAFromPRActivity(prActivity) 587 } 588 diffURL := bPR.Links.Self[0].Href + "/diff" 589 answer.DiffURL = &diffURL 590 591 apiResponse, err := b.Client.DefaultApi.GetPullRequestCommits(answer.Owner, answer.Repo, *answer.Number) 592 if err != nil { 593 return err 594 } 595 err = mapstructure.Decode(apiResponse.Values, &prCommits) 596 if err != nil { 597 return err 598 } 599 answer.LastCommitSha = getLastCommitSHAFromPRCommits(prCommits) 600 return nil 601 } 602 603 // ListOpenPullRequests lists the open pull requests 604 func (b *BitbucketServerProvider) ListOpenPullRequests(owner string, repo string) ([]*GitPullRequest, error) { 605 answer := []*GitPullRequest{} 606 var pullRequests pullRequestPage 607 608 paginationOptions := make(map[string]interface{}) 609 610 paginationOptions["start"] = 0 611 paginationOptions["limit"] = pageLimit 612 613 // TODO how to pass in the owner and repo and status? these are total guesses 614 paginationOptions["owner"] = owner 615 paginationOptions["repo"] = repo 616 paginationOptions["state"] = "open" 617 618 for { 619 apiResponse, err := b.Client.DefaultApi.GetPullRequests(paginationOptions) 620 if err != nil { 621 return nil, err 622 } 623 624 err = mapstructure.Decode(apiResponse.Values, &pullRequests) 625 if err != nil { 626 return nil, err 627 } 628 629 for _, pr := range pullRequests.Values { 630 p := pr 631 actualPR, err := b.toPullRequest(&p) 632 if err != nil { 633 return nil, err 634 } 635 answer = append(answer, actualPR) 636 } 637 638 if pullRequests.IsLastPage { 639 break 640 } 641 if !b.moveToNextPage(paginationOptions, pullRequests.NextPageStart) { 642 break 643 } 644 } 645 return answer, nil 646 } 647 648 // moveToNextPage returns true if we should move to the next page 649 func (b *BitbucketServerProvider) moveToNextPage(paginationOptions map[string]interface{}, nextPage int) bool { 650 lastStartValue := paginationOptions["start"] 651 lastStart, _ := lastStartValue.(int) 652 if lastStart < 0 { 653 lastStart = 0 654 } 655 if nextPage < 0 { 656 return false 657 } 658 if nextPage <= lastStart { 659 return false 660 } 661 paginationOptions["start"] = nextPage 662 return true 663 } 664 665 func convertBitBucketCommitToGitCommit(bCommit *bitbucket.Commit, repo *GitRepository) *GitCommit { 666 return &GitCommit{ 667 SHA: bCommit.ID, 668 Message: bCommit.Message, 669 Author: &GitUser{ 670 Login: bCommit.Author.Name, 671 Name: bCommit.Author.DisplayName, 672 Email: bCommit.Author.EmailAddress, 673 }, 674 URL: repo.URL + "/commits/" + bCommit.ID, 675 Committer: &GitUser{ 676 Login: bCommit.Committer.Name, 677 Name: bCommit.Committer.DisplayName, 678 Email: bCommit.Committer.EmailAddress, 679 }, 680 } 681 } 682 683 func (b *BitbucketServerProvider) GetPullRequestCommits(owner string, repository *GitRepository, number int) ([]*GitCommit, error) { 684 var commitsPage commitsPage 685 commits := []*GitCommit{} 686 paginationOptions := make(map[string]interface{}) 687 688 paginationOptions["start"] = 0 689 paginationOptions["limit"] = pageLimit 690 for { 691 apiResponse, err := b.Client.DefaultApi.GetPullRequestCommitsWithOptions(strings.ToUpper(owner), repository.Name, number, paginationOptions) 692 if err != nil { 693 return nil, err 694 } 695 696 err = mapstructure.Decode(apiResponse.Values, &commitsPage) 697 if err != nil { 698 return nil, err 699 } 700 701 for _, commit := range commitsPage.Values { 702 c := commit 703 commits = append(commits, convertBitBucketCommitToGitCommit(&c, repository)) 704 } 705 706 if commitsPage.IsLastPage { 707 break 708 } 709 if !b.moveToNextPage(paginationOptions, commitsPage.NextPageStart) { 710 break 711 } 712 } 713 714 return commits, nil 715 } 716 717 func (b *BitbucketServerProvider) PullRequestLastCommitStatus(pr *GitPullRequest) (string, error) { 718 var prCommits map[string]interface{} 719 var buildStatusesPage buildStatusesPage 720 721 projectKey, repo := parseBitBucketServerURL(pr.URL) 722 apiResponse, err := b.Client.DefaultApi.GetPullRequestCommits(projectKey, repo, *pr.Number) 723 if err != nil { 724 return "", err 725 } 726 err = mapstructure.Decode(apiResponse.Values, &prCommits) 727 if err != nil { 728 return "", err 729 } 730 lastCommit := getLastCommitFromPRCommits(prCommits) 731 lastCommitSha := lastCommit.ID 732 733 apiResponse, err = b.Client.DefaultApi.GetCommitBuildStatuses(lastCommitSha) 734 if err != nil { 735 return "", err 736 } 737 738 err = mapstructure.Decode(apiResponse.Values, &buildStatusesPage) 739 if err != nil { 740 return "", err 741 } 742 if buildStatusesPage.Size == 0 { 743 return "success", nil 744 } 745 746 for _, buildStatus := range buildStatusesPage.Values { 747 if time.Unix(buildStatus.DateAdded, 0).After(time.Unix(lastCommit.CommitterTimestamp, 0)) { 748 // var from BitBucketCloudProvider 749 return stateMap[buildStatus.State], nil 750 } 751 } 752 753 return "success", nil 754 } 755 756 func (b *BitbucketServerProvider) ListCommitStatus(org, repo, sha string) ([]*GitRepoStatus, error) { 757 var buildStatusesPage buildStatusesPage 758 statuses := []*GitRepoStatus{} 759 760 for { 761 apiResponse, err := b.Client.DefaultApi.GetCommitBuildStatuses(sha) 762 if err != nil { 763 return nil, err 764 } 765 766 err = mapstructure.Decode(apiResponse.Values, &buildStatusesPage) 767 if err != nil { 768 return nil, err 769 } 770 771 for _, buildStatus := range buildStatusesPage.Values { 772 b := buildStatus 773 statuses = append(statuses, convertBitBucketBuildStatusToGitStatus(&b)) 774 } 775 776 if buildStatusesPage.IsLastPage { 777 break 778 } 779 } 780 781 return statuses, nil 782 } 783 784 func (b *BitbucketServerProvider) UpdateCommitStatus(org string, repo string, sha string, status *GitRepoStatus) (*GitRepoStatus, error) { 785 return &GitRepoStatus{}, errors.New("TODO") 786 } 787 788 func convertBitBucketBuildStatusToGitStatus(buildStatus *bitbucket.BuildStatus) *GitRepoStatus { 789 return &GitRepoStatus{ 790 ID: buildStatus.Key, 791 URL: buildStatus.Url, 792 // var from BitBucketCloudProvider 793 State: stateMap[buildStatus.State], 794 TargetURL: buildStatus.Url, 795 Description: buildStatus.Description, 796 Context: buildStatus.Name, 797 } 798 } 799 800 func (b *BitbucketServerProvider) MergePullRequest(pr *GitPullRequest, message string) error { 801 var currentPR bitbucket.PullRequest 802 projectKey, repo := parseBitBucketServerURL(pr.URL) 803 queryParams := map[string]interface{}{} 804 805 apiResponse, err := b.Client.DefaultApi.GetPullRequest(projectKey, repo, *pr.Number) 806 if err != nil { 807 return err 808 } 809 810 err = mapstructure.Decode(apiResponse.Values, ¤tPR) 811 if err != nil { 812 return err 813 } 814 queryParams["version"] = currentPR.Version 815 816 var options = map[string]interface{}{ 817 "message": message, 818 } 819 820 requestBody, err := json.Marshal(options) 821 if err != nil { 822 return err 823 } 824 825 apiResponse, err = b.Client.DefaultApi.Merge(projectKey, repo, *pr.Number, queryParams, requestBody, []string{"application/json"}) 826 if err != nil { 827 return err 828 } 829 830 return nil 831 } 832 833 func (b *BitbucketServerProvider) parseWebHookURL(data *GitWebHookArguments) (string, string, error) { 834 repoURL := data.Repo.URL 835 owner := data.Repo.Organisation 836 repoName := data.Repo.Name 837 if repoURL == "" { 838 repository, err := b.GetRepository(owner, repoName) 839 if err != nil { 840 return "", "", errors.Wrapf(err, "failed to find repository %s/%s in server %s", owner, repoName, b.Server.URL) 841 } 842 repoURL = repository.URL 843 if repoURL == "" { 844 repoURL = repository.HTMLURL 845 } 846 if repoURL == "" { 847 return "", "", errors.Wrapf(err, "repository %s/%s on server %s has no URL", owner, repoName, b.Server.URL) 848 } 849 } 850 projectKey, repo := parseBitBucketServerURL(repoURL) 851 return projectKey, repo, nil 852 } 853 854 type appProps struct { 855 Version string `json:"version"` 856 BuildNumber string `json:"buildNumber"` 857 BuildDate string `json:"buildDate"` 858 DisplayName string `json:"displayName"` 859 } 860 861 func (b *BitbucketServerProvider) getServerVersion() (*semver.Version, error) { 862 apiResponse, err := b.Client.DefaultApi.GetApplicationProperties() 863 if err != nil { 864 return nil, errors.Wrapf(err, "failed to get BitBucket Server version") 865 } 866 867 var props *appProps 868 err = mapstructure.Decode(apiResponse.Values, &props) 869 if err != nil { 870 return nil, errors.Wrap(err, "failed to decode response from application properties") 871 } 872 873 rawVersion := props.Version 874 if rawVersion == "" { 875 return semver.New("0.0.0") 876 } 877 878 return semver.New(rawVersion) 879 } 880 881 func (b *BitbucketServerProvider) webHooksForServer() ([]string, error) { 882 v, err := b.getServerVersion() 883 if err != nil { 884 return nil, err 885 } 886 versionSeven, _ := semver.New("7.0.0") 887 888 var webhooks []string 889 webhooks = append(webhooks, BaseWebHooks...) 890 if v.GTE(*versionSeven) { 891 webhooks = append(webhooks, Ver7WebHooks...) 892 } 893 return webhooks, nil 894 } 895 896 // CreateWebHook adds a new webhook to a git repository 897 func (b *BitbucketServerProvider) CreateWebHook(data *GitWebHookArguments) error { 898 projectKey, repo, err := b.parseWebHookURL(data) 899 if err != nil { 900 return err 901 } 902 903 if data.URL == "" { 904 return errors.New("missing property URL") 905 } 906 907 hooks, err := b.ListWebHooks(projectKey, repo) 908 if err != nil { 909 return errors.Wrapf(err, "error querying webhooks on %s/%s\n", projectKey, repo) 910 } 911 for _, hook := range hooks { 912 if data.URL == hook.URL { 913 log.Logger().Warnf("Already has a webhook registered for %s", data.URL) 914 return nil 915 } 916 } 917 918 webhooks, err := b.webHooksForServer() 919 if err != nil { 920 return errors.Wrapf(err, "failed to determine webhooks for server version") 921 } 922 var options = map[string]interface{}{ 923 "url": data.URL, 924 "name": "Jenkins X Web Hook", 925 "active": true, 926 "events": webhooks, 927 } 928 929 if data.Secret != "" { 930 options["configuration"] = map[string]interface{}{ 931 "secret": data.Secret, 932 } 933 } 934 935 requestBody, err := json.Marshal(options) 936 if err != nil { 937 return errors.Wrap(err, "failed to JSON encode webhook request body for creation") 938 } 939 940 _, err = b.Client.DefaultApi.CreateWebhook(projectKey, repo, requestBody, []string{"application/json"}) 941 942 if err != nil { 943 return errors.Wrapf(err, "create webhook request failed on %s/%s", projectKey, repo) 944 } 945 return nil 946 } 947 948 // ListWebHooks lists all of the webhooks on a given git repository 949 func (b *BitbucketServerProvider) ListWebHooks(owner string, repo string) ([]*GitWebHookArguments, error) { 950 var webHooksPage webHooksPage 951 var webHooks []*GitWebHookArguments 952 953 paginationOptions := make(map[string]interface{}) 954 paginationOptions["start"] = 0 955 paginationOptions["limit"] = pageLimit 956 957 for { 958 apiResponse, err := b.Client.DefaultApi.FindWebhooks(owner, repo, paginationOptions) 959 if err != nil { 960 return nil, errors.Wrapf(err, "failed to list webhooks on repository %s/%s", owner, repo) 961 } 962 963 err = mapstructure.Decode(apiResponse.Values, &webHooksPage) 964 if err != nil { 965 return nil, errors.Wrap(err, "failed to decode response from list webhooks") 966 } 967 968 for _, wh := range webHooksPage.Values { 969 secret := "" 970 if cfg, ok := wh.Configuration["secret"].(string); ok { 971 secret = cfg 972 } 973 974 webHooks = append(webHooks, &GitWebHookArguments{ 975 ID: wh.ID, 976 Owner: owner, 977 Repo: nil, 978 URL: wh.URL, 979 Secret: secret, 980 }) 981 } 982 983 if webHooksPage.IsLastPage { 984 break 985 } 986 if !b.moveToNextPage(paginationOptions, webHooksPage.NextPageStart) { 987 break 988 } 989 } 990 991 return webHooks, nil 992 } 993 994 // UpdateWebHook is used to update a webhook on a git repository. It is best to pass in the webhook ID. 995 func (b *BitbucketServerProvider) UpdateWebHook(data *GitWebHookArguments) error { 996 projectKey, repo, err := b.parseWebHookURL(data) 997 if err != nil { 998 return err 999 } 1000 1001 if data.URL == "" { 1002 return errors.New("missing property URL") 1003 } 1004 1005 dataID := data.ID 1006 if dataID == 0 && data.ExistingURL != "" { 1007 hooks, err := b.ListWebHooks(projectKey, repo) 1008 if err != nil { 1009 log.Logger().Errorf("Error querying webhooks on %s/%s: %s", projectKey, repo, err) 1010 } 1011 for _, hook := range hooks { 1012 if data.ExistingURL == hook.URL { 1013 log.Logger().Warnf("Found existing webhook for url %s", data.ExistingURL) 1014 dataID = hook.ID 1015 } 1016 } 1017 } 1018 if dataID == 0 { 1019 log.Logger().Warn("No webhooks found to update") 1020 return nil 1021 } 1022 id := int32(dataID) 1023 if int64(id) != dataID { 1024 return errors.Errorf("Failed to update webhook with ID = %d due to int32 conversion failure", dataID) 1025 } 1026 1027 webhooks, err := b.webHooksForServer() 1028 if err != nil { 1029 return errors.Wrapf(err, "failed to determine webhooks for server version") 1030 } 1031 var options = map[string]interface{}{ 1032 "url": data.URL, 1033 "name": "Jenkins X Web Hook", 1034 "active": true, 1035 "events": webhooks, 1036 } 1037 1038 if data.Secret != "" { 1039 options["configuration"] = map[string]interface{}{ 1040 "secret": data.Secret, 1041 } 1042 } 1043 1044 requestBody, err := json.Marshal(options) 1045 if err != nil { 1046 return errors.Wrap(err, "failed to JSON encode webhook request body for update") 1047 } 1048 1049 log.Logger().Infof("Updating Bitbucket server webhook for %s/%s for url %s", util.ColorInfo(projectKey), util.ColorInfo(repo), util.ColorInfo(data.URL)) 1050 _, err = b.Client.DefaultApi.UpdateWebhook(projectKey, repo, id, requestBody, []string{"application/json"}) 1051 1052 if err != nil { 1053 return errors.Wrapf(err, "failed to update webhook on %s/%s", projectKey, repo) 1054 } 1055 return nil 1056 } 1057 1058 func (b *BitbucketServerProvider) SearchIssues(org string, name string, query string) ([]*GitIssue, error) { 1059 1060 gitIssues := []*GitIssue{} 1061 1062 log.Logger().Warn("Searching issues on bitbucket server is not supported at this moment") 1063 1064 return gitIssues, nil 1065 } 1066 1067 func (b *BitbucketServerProvider) SearchIssuesClosedSince(org string, name string, t time.Time) ([]*GitIssue, error) { 1068 issues, err := b.SearchIssues(org, name, "") 1069 if err != nil { 1070 return issues, err 1071 } 1072 return FilterIssuesClosedSince(issues, t), nil 1073 } 1074 1075 func (b *BitbucketServerProvider) GetIssue(org string, name string, number int) (*GitIssue, error) { 1076 1077 log.Logger().Warn("Finding an issue on bitbucket server is not supported at this moment") 1078 return &GitIssue{}, nil 1079 } 1080 1081 func (b *BitbucketServerProvider) IssueURL(org string, name string, number int, isPull bool) string { 1082 serverPrefix := b.Server.URL 1083 if strings.Index(serverPrefix, "://") < 0 { 1084 serverPrefix = "https://" + serverPrefix 1085 } 1086 path := "issues" 1087 if isPull { 1088 path = "pull" 1089 } 1090 url := util.UrlJoin(serverPrefix, org, name, path, strconv.Itoa(number)) 1091 return url 1092 } 1093 1094 func (b *BitbucketServerProvider) CreateIssue(owner string, repo string, issue *GitIssue) (*GitIssue, error) { 1095 1096 log.Logger().Warn("Creating an issue on bitbucket server is not suuported at this moment") 1097 return &GitIssue{}, nil 1098 } 1099 1100 func (b *BitbucketServerProvider) AddPRComment(pr *GitPullRequest, comment string) error { 1101 1102 if pr.Number == nil { 1103 return fmt.Errorf("Missing Number for GitPullRequest %#v", pr) 1104 } 1105 n := *pr.Number 1106 1107 prComment := `{ 1108 "text": "` + comment + `" 1109 }` 1110 _, err := b.Client.DefaultApi.CreateComment_1(pr.Owner, pr.Repo, n, prComment, []string{"application/json"}) 1111 return err 1112 } 1113 1114 func (b *BitbucketServerProvider) CreateIssueComment(owner string, repo string, number int, comment string) error { 1115 log.Logger().Warn("Bitbucket Server doesn't support adding issue comments via the REST API") 1116 return nil 1117 } 1118 1119 func (b *BitbucketServerProvider) HasIssues() bool { 1120 return true 1121 } 1122 1123 func (b *BitbucketServerProvider) IsGitHub() bool { 1124 return false 1125 } 1126 1127 func (b *BitbucketServerProvider) IsGitea() bool { 1128 return false 1129 } 1130 1131 func (b *BitbucketServerProvider) IsBitbucketCloud() bool { 1132 return false 1133 } 1134 1135 func (b *BitbucketServerProvider) IsBitbucketServer() bool { 1136 return true 1137 } 1138 1139 func (b *BitbucketServerProvider) IsGerrit() bool { 1140 return false 1141 } 1142 1143 func (b *BitbucketServerProvider) Kind() string { 1144 return "bitbucketserver" 1145 } 1146 1147 // Exposed by Jenkins plugin; this one is for https://wiki.jenkins.io/display/JENKINS/BitBucket+Plugin 1148 func (b *BitbucketServerProvider) JenkinsWebHookPath(gitURL string, secret string) string { 1149 return "/bitbucket-scmsource-hook/notify?server_url=" + url.QueryEscape(b.Server.URL) 1150 } 1151 1152 func (b *BitbucketServerProvider) Label() string { 1153 return b.Server.Label() 1154 } 1155 1156 func (b *BitbucketServerProvider) ServerURL() string { 1157 return b.Server.URL 1158 } 1159 1160 func (b *BitbucketServerProvider) BranchArchiveURL(org string, name string, branch string) string { 1161 return util.UrlJoin(b.ServerURL(), "rest/api/1.0/projects", org, "repos", name, "archive?format=zip&at="+branch) 1162 } 1163 1164 func (b *BitbucketServerProvider) CurrentUsername() string { 1165 return b.Username 1166 } 1167 1168 func (b *BitbucketServerProvider) UserAuth() auth.UserAuth { 1169 return b.User 1170 } 1171 1172 func (b *BitbucketServerProvider) UserInfo(username string) *GitUser { 1173 var user bitbucket.UserWithLinks 1174 apiResponse, err := b.Client.DefaultApi.GetUser(username) 1175 if err != nil { 1176 log.Logger().Error("Unable to fetch user info for " + username + " due to " + err.Error()) 1177 return nil 1178 } 1179 err = mapstructure.Decode(apiResponse.Values, &user) 1180 1181 return &GitUser{ 1182 Login: username, 1183 Name: user.DisplayName, 1184 Email: user.EmailAddress, 1185 URL: user.Links.Self[0].Href, 1186 } 1187 } 1188 1189 func (b *BitbucketServerProvider) UpdateRelease(owner string, repo string, tag string, releaseInfo *GitRelease) error { 1190 log.Logger().Warn("Bitbucket Server doesn't support releases") 1191 return nil 1192 } 1193 1194 // UpdateReleaseStatus is not supported for this git provider 1195 func (b *BitbucketServerProvider) UpdateReleaseStatus(owner string, repo string, tag string, releaseInfo *GitRelease) error { 1196 log.Logger().Warn("Bitbucket Server doesn't support releases") 1197 return nil 1198 } 1199 1200 func (b *BitbucketServerProvider) ListReleases(org string, name string) ([]*GitRelease, error) { 1201 answer := []*GitRelease{} 1202 log.Logger().Warn("Bitbucket Server doesn't support releases") 1203 return answer, nil 1204 } 1205 1206 // GetRelease is unsupported on bitbucket as releases are not supported 1207 func (b *BitbucketServerProvider) GetRelease(org string, name string, tag string) (*GitRelease, error) { 1208 log.Logger().Warn("Bitbucket Cloud doesn't support releases") 1209 return nil, nil 1210 } 1211 1212 func (b *BitbucketServerProvider) AddCollaborator(user string, organisation string, repo string) error { 1213 options := make(map[string]interface{}) 1214 options["name"] = user 1215 options["permission"] = "REPO_WRITE" 1216 _, err := b.Client.DefaultApi.SetPermissionForUser(organisation, repo, options) 1217 return err 1218 } 1219 1220 func (b *BitbucketServerProvider) ListInvitations() ([]*github.RepositoryInvitation, *github.Response, error) { 1221 log.Logger().Infof("Automatically adding the pipeline user as a collaborator is currently not implemented for bitbucket.") 1222 return []*github.RepositoryInvitation{}, &github.Response{}, nil 1223 } 1224 1225 func (b *BitbucketServerProvider) AcceptInvitation(ID int64) (*github.Response, error) { 1226 log.Logger().Infof("Automatically adding the pipeline user as a collaborator is currently not implemented for bitbucket.") 1227 return &github.Response{}, nil 1228 } 1229 1230 func (b *BitbucketServerProvider) GetContent(org string, name string, path string, ref string) (*GitFileContent, error) { 1231 return nil, fmt.Errorf("Getting content not supported on bitbucket") 1232 } 1233 1234 // ShouldForkForPullReques treturns true if we should create a personal fork of this repository 1235 // before creating a pull request 1236 func (b *BitbucketServerProvider) ShouldForkForPullRequest(originalOwner string, repoName string, username string) bool { 1237 // return originalOwner != username 1238 // TODO assuming forking doesn't work yet? 1239 return false 1240 } 1241 1242 func BitBucketServerAccessTokenURL(url string) string { 1243 // TODO with github we can default the scopes/flags we need on a token via adding 1244 // ?scopes=repo,read:user,user:email,write:repo_hook 1245 // 1246 // is there a way to do that for bitbucket? 1247 return util.UrlJoin(url, "/plugins/servlet/access-tokens/manage") 1248 } 1249 1250 // ListCommits lists the commits for the specified repo and owner 1251 func (b *BitbucketServerProvider) ListCommits(owner, repoName string, opt *ListCommitsArguments) ([]*GitCommit, error) { 1252 options := make(map[string]interface{}) 1253 options["limit"] = pageLimit 1254 options["start"] = 0 1255 options["until"] = opt.SHA 1256 var commitsPage commitsPage 1257 commits := []*GitCommit{} 1258 1259 repo, err := b.GetRepository(owner, repoName) 1260 if err != nil { 1261 return nil, err 1262 } 1263 apiResponse, err := b.Client.DefaultApi.GetCommits(strings.ToUpper(owner), repo.Name, options) 1264 if err != nil { 1265 return nil, err 1266 } 1267 1268 err = mapstructure.Decode(apiResponse.Values, &commitsPage) 1269 if err != nil { 1270 return nil, err 1271 } 1272 1273 for _, commit := range commitsPage.Values { 1274 c := commit 1275 commits = append(commits, convertBitBucketCommitToGitCommit(&c, repo)) 1276 } 1277 1278 return commits, nil 1279 } 1280 1281 // AddLabelsToIssue adds labels to issues or pullrequests 1282 func (b *BitbucketServerProvider) AddLabelsToIssue(owner, repo string, number int, labels []string) error { 1283 log.Logger().Warnf("Adding labels not supported on bitbucket server yet for repo %s/%s issue %d labels %v", owner, repo, number, labels) 1284 return nil 1285 } 1286 1287 // GetLatestRelease fetches the latest release from the git provider for org and name 1288 func (b *BitbucketServerProvider) GetLatestRelease(org string, name string) (*GitRelease, error) { 1289 return nil, nil 1290 } 1291 1292 // UploadReleaseAsset will upload an asset to org/repo to a release with id, giving it a name, it will return the release asset from the git provider 1293 func (b *BitbucketServerProvider) UploadReleaseAsset(org string, repo string, id int64, name string, asset *os.File) (*GitReleaseAsset, error) { 1294 return nil, nil 1295 } 1296 1297 // GetBranch returns the branch information for an owner/repo, including the commit at the tip 1298 func (b *BitbucketServerProvider) GetBranch(owner string, repo string, branch string) (*GitBranch, error) { 1299 return nil, nil 1300 } 1301 1302 // GetProjects returns all the git projects in owner/repo 1303 func (b *BitbucketServerProvider) GetProjects(owner string, repo string) ([]GitProject, error) { 1304 return nil, nil 1305 } 1306 1307 //ConfigureFeatures sets specific features as enabled or disabled for owner/repo 1308 func (b *BitbucketServerProvider) ConfigureFeatures(owner string, repo string, issues *bool, projects *bool, wikis *bool) (*GitRepository, error) { 1309 return nil, nil 1310 } 1311 1312 // IsWikiEnabled returns true if a wiki is enabled for owner/repo 1313 func (b *BitbucketServerProvider) IsWikiEnabled(owner string, repo string) (bool, error) { 1314 return false, nil 1315 }