├── .gitignore ├── internal ├── const.go ├── util.go ├── config.go ├── pr.go ├── project_list_test.go ├── repo_branch.go ├── project_clone_test.go ├── repo_pr_approve.go ├── project.go ├── repo_pr.go ├── client.go ├── repo.go ├── repo_pr_merge.go ├── repo_security.go ├── project_list.go ├── cli.go ├── repo_pr_list.go ├── repo_security_scan.go ├── auth.go ├── repo_branch_list.go ├── repo_security_result.go ├── pr_list.go ├── project_clone.go ├── repo_branch_compare.go └── repo_pr_create.go ├── Makefile ├── commit_convention.yml ├── Dockerfile ├── test └── util.go ├── LICENSE ├── .github └── workflows │ └── docker-build-and-release.yml ├── go.mod ├── cmd └── bitbucket-cli │ └── bitbucket.go ├── README.md └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | /build 3 | -------------------------------------------------------------------------------- /internal/const.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | const errSpecifySubcommand = "please specify a subcommand" 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | mkdir -p build/ 3 | go build -o ./build/bitbucket-cli ./cmd/bitbucket-cli 4 | 5 | clean: 6 | rm -rf build/ -------------------------------------------------------------------------------- /internal/util.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | func validValue(value string, validValues []string) bool { 4 | for _,v := range validValues { 5 | if value == v { 6 | return true 7 | } 8 | } 9 | return false 10 | } 11 | -------------------------------------------------------------------------------- /commit_convention.yml: -------------------------------------------------------------------------------- 1 | ## commit_convention.yml 2 | ## omit the commit_types to use the default angular-style commit types 3 | # commit_types: 4 | # - type: description of what the short-form "type" means 5 | scopes: 6 | test: 7 | -------------------------------------------------------------------------------- /internal/config.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | type Config struct { 4 | Username string `yaml:"username"` 5 | Password string `yaml:"password"` 6 | AccessToken string `yaml:"access_token"` 7 | Url string `yaml:"url"` 8 | } 9 | -------------------------------------------------------------------------------- /internal/pr.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | type PrCmd struct { 4 | List *PrListCmd `arg:"subcommand:list"` 5 | } 6 | 7 | func (b *BitbucketCLI) RunPRCmd(cmd *PrCmd) { 8 | if cmd == nil { 9 | return 10 | } 11 | if cmd.List != nil { 12 | b.RunPRListCmd(cmd.List) 13 | return 14 | } 15 | b.logger.Fatal(errSpecifySubcommand) 16 | } 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.18 as build 2 | 3 | WORKDIR /go/src/bitbucket-cli 4 | COPY . . 5 | 6 | RUN go mod download 7 | RUN CGO_ENABLED=0 go build -o /go/src/bitbucket-cli ./cmd/bitbucket-cli 8 | 9 | # Now copy it into our base image. 10 | FROM gcr.io/distroless/static-debian11 11 | COPY --from=build /go/src/bitbucket-cli / 12 | ENTRYPOINT ["/bitbucket-cli"] 13 | -------------------------------------------------------------------------------- /internal/project_list_test.go: -------------------------------------------------------------------------------- 1 | package cli_test 2 | 3 | import ( 4 | cli "github.com/swisscom/bitbucket-cli/internal" 5 | "github.com/swisscom/bitbucket-cli/test" 6 | "testing" 7 | ) 8 | 9 | func TestProjectList(t *testing.T) { 10 | c := test.MustGetCLI() 11 | c.RunProjectCmd(&cli.ProjectCmd{ 12 | Key: "TOOL", 13 | List: &cli.ProjectListCmd{}, 14 | Clone: nil, 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /internal/repo_branch.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | type BranchCmd struct { 4 | Compare *RepoBranchCompareCmd `arg:"subcommand:compare"` 5 | List *RepoBranchListCmd `arg:"subcommand:list"` 6 | } 7 | 8 | func (b *BitbucketCLI) branchCmd(cmd *RepoCmd) { 9 | if cmd.BranchCmd.Compare != nil { 10 | b.branchCmdCompare(cmd) 11 | return 12 | } 13 | if cmd.BranchCmd.List != nil { 14 | b.branchCmdList(cmd) 15 | return 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/util.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | cli "github.com/swisscom/bitbucket-cli/internal" 5 | "os" 6 | ) 7 | 8 | func MustGetCLI() *cli.BitbucketCLI { 9 | c, err := cli.NewCLI( 10 | &cli.BasicAuth{ 11 | Username: os.Getenv("BITBUCKET_USERNAME"), 12 | Password: os.Getenv("BITBUCKET_PASSWORD"), 13 | }, 14 | os.Getenv("BITBUCKET_URL"), 15 | ) 16 | if err != nil { 17 | panic(err) 18 | } 19 | return c 20 | } 21 | -------------------------------------------------------------------------------- /internal/project_clone_test.go: -------------------------------------------------------------------------------- 1 | package cli_test 2 | 3 | import ( 4 | cli "github.com/swisscom/bitbucket-cli/internal" 5 | "github.com/swisscom/bitbucket-cli/test" 6 | "testing" 7 | ) 8 | 9 | func TestProjectClone(t *testing.T) { 10 | c := test.MustGetCLI() 11 | c.RunProjectCmd(&cli.ProjectCmd{ 12 | Key: "TOOL", 13 | List: nil, 14 | Clone: &cli.ProjectCloneCmd{ 15 | Output: "/tmp/git-repo", 16 | Branch: "master", 17 | }, 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /internal/repo_pr_approve.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type RepoPrApproveCmd struct { 8 | Id int64 `arg:"-i,--id" help:"id of the PR"` 9 | } 10 | 11 | func (b *BitbucketCLI) repoPrApprove(cmd *RepoCmd) { 12 | if cmd == nil || cmd.PrCmd == nil || cmd.PrCmd.Approve == nil { 13 | return 14 | } 15 | approve := cmd.PrCmd.Approve 16 | 17 | if _, err := b.client.DefaultApi.Approve(cmd.ProjectKey, cmd.Slug, approve.Id); err != nil { 18 | b.logger.Fatalf("unable to approve PR: %v", err) 19 | } else { 20 | fmt.Printf("PR %d succesfully approved", approve.Id) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/project.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | ) 6 | 7 | type ProjectCmd struct { 8 | Key string `arg:"-k,required"` 9 | List *ProjectListCmd `arg:"subcommand:list"` 10 | Clone *ProjectCloneCmd `arg:"subcommand:clone"` 11 | } 12 | 13 | func (b *BitbucketCLI) RunProjectCmd(cmd *ProjectCmd){ 14 | if cmd == nil { 15 | b.logger.Fatal("unable to execute command") 16 | return 17 | } 18 | 19 | if cmd.Key == "" { 20 | logrus.Fatal("A project key must be provided") 21 | } 22 | 23 | if cmd.List != nil { 24 | b.projectList(cmd) 25 | return 26 | } else if cmd.Clone != nil { 27 | b.projectClone(cmd) 28 | return 29 | } 30 | 31 | b.logger.Fatal(errSpecifySubcommand) 32 | } -------------------------------------------------------------------------------- /internal/repo_pr.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | type RepoPrCmd struct { 4 | Approve *RepoPrApproveCmd `arg:"subcommand:approve"` 5 | Create *RepoPrCreateCmd `arg:"subcommand:create"` 6 | List *RepoPrListCmd `arg:"subcommand:list"` 7 | Merge *RepoPrMergeCmd `arg:"subcommand:merge"` 8 | } 9 | 10 | func (b *BitbucketCLI) repoPrCmd(cmd *RepoCmd) { 11 | if cmd == nil || cmd.PrCmd == nil { 12 | return 13 | } 14 | 15 | prCmd := cmd.PrCmd 16 | 17 | if prCmd.Approve != nil { 18 | b.repoPrApprove(cmd) 19 | return 20 | } else if prCmd.Create != nil { 21 | b.repoPrCreate(cmd) 22 | return 23 | } else if prCmd.List != nil { 24 | b.repoPrList(cmd) 25 | return 26 | } else if prCmd.Merge != nil { 27 | b.repoPrMerge(cmd) 28 | return 29 | } 30 | 31 | b.logger.Fatal(errSpecifySubcommand) 32 | } 33 | -------------------------------------------------------------------------------- /internal/client.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | ) 8 | 9 | // Custom Client to perform custom REST requests 10 | 11 | func (b *BitbucketCLI) doReq(req *http.Request) (*http.Response, error) { 12 | err := b.prepareRequest(req) 13 | if err != nil { 14 | return nil, err 15 | } 16 | return b.httpClient.Do(req) 17 | } 18 | 19 | func (b *BitbucketCLI) getUrl() *url.URL { 20 | return b.restUrl 21 | } 22 | 23 | func (b *BitbucketCLI) prepareRequest(req *http.Request) error { 24 | if req == nil { 25 | return fmt.Errorf("request cannot be nil") 26 | } 27 | 28 | b.auth.AddHeaders(req) 29 | 30 | // https://confluence.atlassian.com/cloudkb/xsrf-check-failed-when-calling-cloud-apis-826874382.html 31 | req.Header.Set("X-Atlassian-Token", "no-check") 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /internal/repo.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | type RepoCmd struct { 4 | ProjectKey string `arg:"-k,--key,required,env:BITBUCKET_PROJECT" help:"Project AccessToken (e.g: TOOL)"` 5 | Slug string `arg:"-n,--name,required,env:BITBUCKET_REPO" help:"Slug of the repository"` 6 | 7 | PrCmd *RepoPrCmd `arg:"subcommand:pr"` 8 | BranchCmd *BranchCmd `arg:"subcommand:branch"` 9 | SecurityCmd *SecurityCmd `arg:"subcommand:security"` 10 | } 11 | 12 | func (b *BitbucketCLI) RunRepoCmd(cmd *RepoCmd) { 13 | if cmd == nil { 14 | return 15 | } 16 | 17 | if cmd.PrCmd != nil { 18 | b.repoPrCmd(cmd) 19 | return 20 | } 21 | 22 | if cmd.BranchCmd != nil { 23 | b.branchCmd(cmd) 24 | return 25 | } 26 | 27 | if cmd.SecurityCmd != nil { 28 | b.securityCmd(cmd) 29 | return 30 | } 31 | 32 | b.logger.Fatal(errSpecifySubcommand) 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Swisscom (Schweiz) AG 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /internal/repo_pr_merge.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | goBitBucket "github.com/gfleury/go-bitbucket-v1" 6 | ) 7 | 8 | type RepoPrMergeCmd struct { 9 | Id int `arg:"-i,--id" help:"id of the PR"` 10 | } 11 | 12 | func (b *BitbucketCLI) repoPrMerge(cmd *RepoCmd) { 13 | if cmd == nil || cmd.PrCmd == nil || cmd.PrCmd.Merge == nil { 14 | return 15 | } 16 | merge := cmd.PrCmd.Merge 17 | if prResponse, err := b.client.DefaultApi.GetPullRequest(cmd.ProjectKey, cmd.Slug, merge.Id); err != nil { 18 | b.logger.Fatalf("unable to find PR: %v", err) 19 | } else if pr, err := goBitBucket.GetPullRequestResponse(prResponse); err != nil { 20 | b.logger.Fatalf("could not retrieve PR from response: %v", err) 21 | } else if !pr.Open { 22 | b.logger.Fatal("PR is in a closed state") 23 | } else { 24 | if mergeResponse, err := b.client.DefaultApi.Merge( 25 | cmd.ProjectKey, 26 | cmd.Slug, 27 | merge.Id, 28 | map[string]interface{}{"version": pr.Version}, 29 | nil, 30 | nil); err != nil { 31 | b.logger.Fatalf("unable to approve PR: %v", err) 32 | } else { 33 | fmt.Println(mergeResponse.Message) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/repo_security.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "github.com/fatih/color" 6 | ) 7 | 8 | type SecurityCmd struct { 9 | Scan *SecurityScanCmd `arg:"subcommand:scan"` 10 | Result *SecurityResultCmd `arg:"subcommand:result"` 11 | } 12 | 13 | func printGreenBackground(text string) { 14 | c := color.New(color.FgBlack).Add(color.BgGreen) 15 | _, _ = c.Printf(" %s ", text) 16 | } 17 | 18 | func printRedBackground(text string) { 19 | c := color.New(color.FgBlack).Add(color.BgRed) 20 | _, _ = c.Printf(" %s ", text) 21 | } 22 | 23 | func (b *BitbucketCLI) securityCmd(cmd *RepoCmd) { 24 | if cmd.SecurityCmd.Scan != nil { 25 | // Do scan 26 | b.triggerRepoScan(cmd.ProjectKey, cmd.Slug) 27 | return 28 | } 29 | 30 | if cmd.SecurityCmd.Result != nil { 31 | scanResult := b.getScanResult(cmd.ProjectKey, cmd.Slug) 32 | if scanResult.Total == 0 { 33 | printGreenBackground(fmt.Sprintf( 34 | "No issues found in %s/%s", cmd.ProjectKey, cmd.Slug), 35 | ) 36 | } else { 37 | printRedBackground( 38 | fmt.Sprintf("%d issues found in %s/%s", scanResult.Total, cmd.ProjectKey, cmd.Slug), 39 | ) 40 | } 41 | fmt.Printf("\n") 42 | return 43 | } 44 | 45 | b.logger.Fatal(errSpecifySubcommand) 46 | } 47 | -------------------------------------------------------------------------------- /internal/project_list.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | bitbucket "github.com/gfleury/go-bitbucket-v1" 6 | "github.com/sirupsen/logrus" 7 | "net/url" 8 | ) 9 | 10 | type ProjectListCmd struct { 11 | } 12 | 13 | func (b *BitbucketCLI) projectList(cmd *ProjectCmd) { 14 | if cmd == nil { 15 | return 16 | } 17 | 18 | var repositories []bitbucket.Repository 19 | start := 0 20 | 21 | // Fetch all repos 22 | for { 23 | // List project repositories 24 | res, err := b.client.DefaultApi.GetRepositoriesWithOptions(cmd.Key, 25 | map[string]interface{}{ 26 | "start": start, 27 | }, 28 | ) 29 | if err != nil { 30 | logrus.Fatal(err) 31 | } 32 | pageRepos, err := bitbucket.GetRepositoriesResponse(res) 33 | if err != nil { 34 | logrus.Fatalf("unable to parse repositories response: %v", err) 35 | } 36 | repositories = append(repositories, pageRepos...) 37 | hasNextPage, nextPageStart := bitbucket.HasNextPage(res) 38 | if !hasNextPage { 39 | break 40 | } 41 | start = nextPageStart 42 | } 43 | 44 | for _, v := range repositories { 45 | // Get HTTP Clone URL 46 | var cloneUrl = "" 47 | for _, cUrl := range v.Links.Clone { 48 | mUrl, err := url.Parse(cUrl.Href) 49 | if err != nil { 50 | continue 51 | } 52 | 53 | if mUrl.Scheme == "https" { 54 | cloneUrl = mUrl.String() 55 | break 56 | } 57 | } 58 | fmt.Printf("%s\t%s\n", v.Name, cloneUrl) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/docker-build-and-release.yml: -------------------------------------------------------------------------------- 1 | name: Create and publish a Docker image 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | tags: 8 | - 'v*' 9 | # pull_request: # only for demonstration in the pr 10 | # branches: 11 | # - 'master' 12 | 13 | env: 14 | REGISTRY: ghcr.io 15 | IMAGE_NAME: ${{ github.repository }} 16 | 17 | jobs: 18 | build-and-push-image: 19 | runs-on: ubuntu-latest 20 | permissions: 21 | contents: read 22 | packages: write 23 | 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@v3 27 | 28 | - name: Log in to the Container registry 29 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 30 | with: 31 | registry: ${{ env.REGISTRY }} 32 | username: ${{ github.actor }} 33 | password: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - name: Extract metadata (tags, labels) for Docker 36 | id: meta 37 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 38 | with: 39 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 40 | 41 | - name: Build and push Docker image 42 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 43 | with: 44 | context: . 45 | push: true 46 | tags: ${{ steps.meta.outputs.tags }} 47 | labels: ${{ steps.meta.outputs.labels }} 48 | -------------------------------------------------------------------------------- /internal/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | bitbucket "github.com/gfleury/go-bitbucket-v1" 7 | gitHttp "github.com/go-git/go-git/v5/plumbing/transport/http" 8 | "github.com/sirupsen/logrus" 9 | "net/http" 10 | "net/url" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | type BitbucketCLI struct { 16 | cloneCredentials gitHttp.BasicAuth 17 | restUrl *url.URL 18 | client *bitbucket.APIClient 19 | logger *logrus.Logger 20 | httpClient *http.Client 21 | auth Authenticator 22 | } 23 | 24 | func (b *BitbucketCLI) SetLogger(logger *logrus.Logger) { 25 | if logger == nil { 26 | // We don't set nil loggers 27 | return 28 | } 29 | b.logger = logger 30 | } 31 | 32 | func NewCLI(auth Authenticator, restUrl string) (*BitbucketCLI, error) { 33 | mUrl, err := url.Parse(restUrl) 34 | if err != nil { 35 | return nil, fmt.Errorf("unable to parse URL: %v", err) 36 | } 37 | 38 | ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) 39 | ctx = auth.GetContext(ctx) 40 | c := bitbucket.NewAPIClient(ctx, bitbucket.NewConfiguration( 41 | strings.TrimRight(mUrl.String(), "/"), // https://git.example.com/rest/ -> https://git.example.com/rest 42 | )) 43 | logger := logrus.New() 44 | 45 | return &BitbucketCLI{ 46 | cloneCredentials: auth.GetCloneCredentials(), 47 | restUrl: mUrl, 48 | auth: auth, 49 | client: c, 50 | logger: logger, 51 | httpClient: http.DefaultClient, 52 | }, nil 53 | } 54 | -------------------------------------------------------------------------------- /internal/repo_pr_list.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | bitbucket "github.com/gfleury/go-bitbucket-v1" 6 | "strings" 7 | ) 8 | 9 | type RepoPrListCmd struct { 10 | State string `arg:"-s,--state,env:BITBUCKET_LIST" help:"PR State, any of: ALL, OPEN, DECLINED, MERGED"` 11 | } 12 | 13 | func (b *BitbucketCLI) repoPrList(cmd *RepoCmd) { 14 | if cmd == nil || cmd.PrCmd == nil || cmd.PrCmd.List == nil { 15 | return 16 | } 17 | 18 | lCmd := cmd.PrCmd.List 19 | 20 | opts := map[string]interface{}{} 21 | if lCmd.State != "" { 22 | validStates := []string{ 23 | "ALL", 24 | "OPEN", 25 | "DECLINED", 26 | "MERGED", 27 | } 28 | 29 | inputUpper := strings.ToUpper(lCmd.State) 30 | 31 | if !validValue(inputUpper, validStates) { 32 | b.logger.Fatalf("invalid value \"%s\" for state: accepted values are: \n%s", 33 | lCmd.State, 34 | strings.Join(validStates, "\n"), 35 | ) 36 | return 37 | } 38 | opts["state"] = inputUpper 39 | } 40 | 41 | prs, err := b.client.DefaultApi.GetPullRequestsPage(cmd.ProjectKey, cmd.Slug, opts) 42 | if err != nil { 43 | b.logger.Fatalf("unable to get PRs: %v", err) 44 | return 45 | } 46 | 47 | prsResponse, err := bitbucket.GetPullRequestsResponse(prs) 48 | if err != nil { 49 | b.logger.Fatalf("unable to parse PRs response: %v", err) 50 | } 51 | 52 | var lines []string 53 | 54 | for _, pr := range prsResponse { 55 | lines = append(lines, fmt.Sprintf("%s (ID: %d)", pr.Title, pr.ID)) 56 | } 57 | 58 | fmt.Print(strings.Join(lines, "\n")) 59 | } 60 | -------------------------------------------------------------------------------- /internal/repo_security_scan.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "net/url" 8 | ) 9 | 10 | type SecurityScanCmd struct{} 11 | 12 | func (b *BitbucketCLI) triggerRepoScan(projectKey string, slug string) { 13 | b.logger.Debugf("triggering repo scan for %s/%s", projectKey, slug) 14 | 15 | // Custom endpoint! 16 | // https://docs.soteri.io/security-for-bitbucket/3.17.0/(3.17.0)-REST-API-for-Scripting-&-Automation.14602141697.html#id-(3.17.0)RESTAPIforScripting&Automation-Kickingoffanewrepositoryscan 17 | 18 | triggerScanUrl, err := b.restUrl.Parse( 19 | fmt.Sprintf("security/1.0/scan/%s/repos/%s", 20 | url.PathEscape(projectKey), 21 | url.PathEscape(slug), 22 | ), 23 | ) 24 | 25 | if err != nil { 26 | b.logger.Fatalf("unable to parse url: %v", err) 27 | } 28 | 29 | b.logger.Debugf("POST %s", triggerScanUrl.String()) 30 | 31 | req, err := http.NewRequest(http.MethodPost, triggerScanUrl.String(), nil) 32 | if err != nil { 33 | b.logger.Fatalf("unable to create request: %v", err) 34 | } 35 | 36 | err = b.prepareRequest(req) 37 | if err != nil { 38 | b.logger.Fatalf("unable to prepare request: %v", err) 39 | } 40 | 41 | res, err := b.doReq(req) 42 | if err != nil { 43 | b.logger.Fatalf("unable to do request: %v", err) 44 | } 45 | 46 | if res.StatusCode != http.StatusOK { 47 | resBody, err := ioutil.ReadAll(res.Body) 48 | if err == nil { 49 | b.logger.Debugf("resp=%v", string(resBody)) 50 | } 51 | b.logger.Fatalf("invalid status code received: 200 OK expected but %s received", res.Status) 52 | } 53 | 54 | b.logger.Infof("Scan successfully triggered") 55 | } 56 | -------------------------------------------------------------------------------- /internal/auth.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | bitbucket "github.com/gfleury/go-bitbucket-v1" 7 | git_http "github.com/go-git/go-git/v5/plumbing/transport/http" 8 | "net/http" 9 | ) 10 | 11 | type BasicAuth struct { 12 | Username string 13 | Password string 14 | } 15 | 16 | func (b BasicAuth) AddHeaders(req *http.Request) { 17 | req.SetBasicAuth(b.Username, b.Password) 18 | } 19 | 20 | func (b BasicAuth) GetCloneCredentials() git_http.BasicAuth { 21 | return git_http.BasicAuth{Username: b.Username, Password: b.Password} 22 | } 23 | 24 | func (b BasicAuth) GetContext(ctx context.Context) context.Context { 25 | return context.WithValue(ctx, bitbucket.ContextBasicAuth, 26 | bitbucket.BasicAuth{ 27 | UserName: b.Username, 28 | Password: b.Password, 29 | }) 30 | } 31 | 32 | type AccessToken struct { 33 | Username string 34 | AccessToken string 35 | } 36 | 37 | func (a AccessToken) AddHeaders(req *http.Request) { 38 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", a.AccessToken)) 39 | } 40 | 41 | func (a AccessToken) GetCloneCredentials() git_http.BasicAuth { 42 | return git_http.BasicAuth{ 43 | Username: a.Username, 44 | Password: a.AccessToken, 45 | } 46 | } 47 | 48 | func (a AccessToken) GetContext(ctx context.Context) context.Context { 49 | return context.WithValue(ctx, bitbucket.ContextAccessToken, a.AccessToken) 50 | } 51 | 52 | var _ Authenticator = (*BasicAuth)(nil) 53 | var _ Authenticator = (*AccessToken)(nil) 54 | 55 | type Authenticator interface { 56 | GetContext(ctx context.Context) context.Context 57 | GetCloneCredentials() git_http.BasicAuth 58 | AddHeaders(req *http.Request) 59 | } 60 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/swisscom/bitbucket-cli 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/adrg/xdg v0.4.0 7 | github.com/alexflint/go-arg v1.3.0 8 | github.com/fatih/color v1.13.0 9 | github.com/gfleury/go-bitbucket-v1 v0.0.0-20200327185123-b3c7f99f7f5c 10 | github.com/go-git/go-git/v5 v5.1.0 11 | github.com/sirupsen/logrus v1.6.0 12 | gopkg.in/yaml.v2 v2.2.8 13 | ) 14 | 15 | require ( 16 | github.com/alexflint/go-scalar v1.0.0 // indirect 17 | github.com/emirpasic/gods v1.12.0 // indirect 18 | github.com/go-git/gcfg v1.5.0 // indirect 19 | github.com/go-git/go-billy/v5 v5.0.0 // indirect 20 | github.com/golang/protobuf v1.2.0 // indirect 21 | github.com/imdario/mergo v0.3.9 // indirect 22 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 23 | github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd // indirect 24 | github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect 25 | github.com/mattn/go-colorable v0.1.9 // indirect 26 | github.com/mattn/go-isatty v0.0.14 // indirect 27 | github.com/mitchellh/go-homedir v1.1.0 // indirect 28 | github.com/mitchellh/mapstructure v1.1.2 // indirect 29 | github.com/sergi/go-diff v1.1.0 // indirect 30 | github.com/xanzy/ssh-agent v0.2.1 // indirect 31 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 // indirect 32 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a // indirect 33 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 // indirect 34 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect 35 | golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 // indirect 36 | google.golang.org/appengine v1.4.0 // indirect 37 | gopkg.in/warnings.v0 v0.1.2 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /internal/repo_branch_list.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | bitbucketv1 "github.com/gfleury/go-bitbucket-v1" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | type RepoBranchListCmd struct { 11 | Filter string `arg:"-f,--filter" help:"Filter to match branch names against (contains)"` 12 | Prefix string `arg:"-p,--prefix" help:"Only list branches that start with this prefix"` 13 | Regex string `arg:"-r,--regex" help:"Only list branches that start with this prefix"` 14 | } 15 | 16 | func (b *BitbucketCLI) branchCmdList(cmd *RepoCmd) { 17 | if cmd == nil || cmd.BranchCmd == nil || cmd.BranchCmd.List == nil { 18 | return 19 | } 20 | 21 | filterFunction := func(branch bitbucketv1.Branch) bool { return true } 22 | 23 | list := cmd.BranchCmd.List 24 | if list.Prefix != "" { 25 | prevFilter := filterFunction 26 | filterFunction = func(branch bitbucketv1.Branch) bool { 27 | return prevFilter(branch) && strings.HasPrefix(branch.DisplayID, list.Prefix) 28 | } 29 | } 30 | 31 | if list.Regex != "" { 32 | regex, err := regexp.Compile(list.Regex) 33 | 34 | if err != nil { 35 | b.logger.Warnf("Regex %s is not valid, will be skipped", list.Regex) 36 | } else { 37 | prevFilter := filterFunction 38 | filterFunction = func(branch bitbucketv1.Branch) bool { 39 | return prevFilter(branch) && regex.MatchString(branch.DisplayID) 40 | } 41 | } 42 | } 43 | optionals := make(map[string]interface{}) 44 | if list.Filter != "" { 45 | optionals["filterText"] = list.Filter 46 | } 47 | response, err := b.client.DefaultApi.GetBranches(cmd.ProjectKey, cmd.Slug, optionals) 48 | 49 | if err != nil { 50 | b.logger.Fatalf("Failed to fetch branches %s", err.Error()) 51 | return 52 | } 53 | branches, err := bitbucketv1.GetBranchesResponse(response) 54 | 55 | if err != nil { 56 | b.logger.Fatalf("Failed to parse branches response %s", err.Error()) 57 | return 58 | } 59 | 60 | for _, branch := range branches { 61 | if filterFunction(branch) { 62 | fmt.Printf("%s \n", branch.DisplayID) 63 | } 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /internal/repo_security_result.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | ) 10 | 11 | type SecurityResultCmd struct{} 12 | 13 | type ScanResult struct { 14 | ScanKey string `json:"scanKey"` 15 | Scanned bool `json:"scanned"` 16 | Actual bool `json:"actual"` 17 | Progress int `json:"progress"` 18 | Running bool `json:"running"` 19 | Scheduled bool `json:"scheduled"` 20 | InvalidLines []interface{} `json:"invalidLines"` 21 | Total int `json:"total"` 22 | } 23 | 24 | func (b *BitbucketCLI) getScanResult(projectKey string, slug string) ScanResult { 25 | b.logger.Debugf("fetching security scan result for %s/%s", projectKey, slug) 26 | 27 | // Custom endpoint! 28 | // https://docs.soteri.io/security-for-bitbucket/3.17.0/(3.17.0)-REST-API-for-Scripting-&-Automation.14602141697.html#id-(3.17.0)RESTAPIforScripting&Automation-Fetchingscanresultsforaspecificbranch 29 | 30 | triggerScanUrl, err := b.restUrl.Parse( 31 | fmt.Sprintf("security/1.0/scan/%s/repos/%s", 32 | url.PathEscape(projectKey), 33 | url.PathEscape(slug), 34 | ), 35 | ) 36 | if err != nil { 37 | b.logger.Fatalf("unable to parse url: %v", err) 38 | } 39 | 40 | b.logger.Debugf("GET %s", triggerScanUrl.String()) 41 | 42 | req, err := http.NewRequest(http.MethodGet, triggerScanUrl.String(), nil) 43 | if err != nil { 44 | b.logger.Fatalf("unable to create request: %v", err) 45 | } 46 | 47 | err = b.prepareRequest(req) 48 | if err != nil { 49 | b.logger.Fatalf("unable to prepare request: %v", err) 50 | } 51 | 52 | res, err := b.doReq(req) 53 | if err != nil { 54 | b.logger.Fatalf("unable to do request: %v", err) 55 | } 56 | 57 | if res.StatusCode != http.StatusOK { 58 | resBody, err := ioutil.ReadAll(res.Body) 59 | if err == nil { 60 | b.logger.Debugf("resp=%v", string(resBody)) 61 | } 62 | b.logger.Fatalf("invalid status code received: 200 OK expected but %s received", res.Status) 63 | } 64 | 65 | // Decode response 66 | var scanResult ScanResult 67 | dec := json.NewDecoder(res.Body) 68 | dec.DisallowUnknownFields() 69 | err = dec.Decode(&scanResult) 70 | 71 | if err != nil { 72 | b.logger.Fatalf("unable to decode JSON: %v", err) 73 | } 74 | 75 | return scanResult 76 | } 77 | -------------------------------------------------------------------------------- /internal/pr_list.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | bitbucket "github.com/gfleury/go-bitbucket-v1" 7 | "regexp" 8 | "strings" 9 | ) 10 | 11 | type PrListCmd struct { 12 | State string `arg:"-s,--state"` 13 | Output string `arg:"-o,--output"` 14 | 15 | FilterTitleRegex string `arg:"-t,--filter-title"` 16 | FilterDescRegex string `arg:"-d,--filter-desc"` 17 | } 18 | 19 | /* 20 | /rest/api/1.0/dashboard/pull-requests?limit=100&state=OPEN 21 | */ 22 | 23 | func (b *BitbucketCLI) RunPRListCmd(cmd *PrListCmd) { 24 | if cmd == nil { 25 | return 26 | } 27 | 28 | var filterTitle *regexp.Regexp 29 | var filterDesc *regexp.Regexp 30 | var err error 31 | if cmd.FilterTitleRegex != "" { 32 | filterTitle, err = regexp.Compile(cmd.FilterTitleRegex) 33 | if err != nil { 34 | b.logger.Fatalf("unable to compile regex for title filtering: %v", err) 35 | } 36 | } 37 | 38 | if cmd.FilterDescRegex != "" { 39 | filterDesc, err = regexp.Compile(cmd.FilterDescRegex) 40 | if err != nil { 41 | b.logger.Fatalf("unable to compile regex for description filtering: %v", err) 42 | } 43 | } 44 | 45 | options := map[string]interface{}{} 46 | if cmd.State != "" { 47 | options["state"] = strings.ToUpper(cmd.State) 48 | } 49 | 50 | res, err := b.client.DefaultApi.GetPullRequests(options) 51 | if err != nil { 52 | b.logger.Fatalf("unable to list pull requests: %v", err) 53 | return 54 | } 55 | 56 | pr, err := bitbucket.GetPullRequestsResponse(res) 57 | if err != nil { 58 | b.logger.Fatalf("unable to parse PRs list: %v", err) 59 | return 60 | } 61 | 62 | var filteredPrs []bitbucket.PullRequest 63 | for _, v := range pr { 64 | if filterTitle != nil { 65 | if !filterTitle.MatchString(v.Title) { 66 | continue 67 | } 68 | } 69 | 70 | if filterDesc != nil { 71 | if !filterDesc.MatchString(v.Description) { 72 | continue 73 | } 74 | } 75 | 76 | filteredPrs = append(filteredPrs, v) 77 | } 78 | 79 | switch strings.ToLower(cmd.Output) { 80 | case "json": 81 | jsonOutput, err := json.Marshal(pr) 82 | if err != nil { 83 | b.logger.Fatalf("unable to marshal JSON: %v", err) 84 | } 85 | fmt.Printf("%s", jsonOutput) 86 | default: 87 | for _, v := range pr { 88 | fmt.Printf("%s - %s - %s\n", v.Title, v.Author.User.DisplayName, v.Links.Self[0].Href) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /internal/project_clone.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | bitbucket "github.com/gfleury/go-bitbucket-v1" 6 | "github.com/go-git/go-git/v5" 7 | "github.com/go-git/go-git/v5/plumbing" 8 | "github.com/sirupsen/logrus" 9 | "net/url" 10 | "path/filepath" 11 | ) 12 | 13 | type ProjectCloneCmd struct { 14 | Output string `arg:"-o,--output-path" default:"./"` 15 | Branch string `arg:"-b,--branch" default:"master"` 16 | } 17 | 18 | func (b *BitbucketCLI) projectClone(cmd *ProjectCmd) { 19 | // Clones all the projects 20 | 21 | var repositories []bitbucket.Repository 22 | var err error 23 | start := 0 24 | 25 | for { 26 | // List project repositories 27 | res, err := b.client.DefaultApi.GetRepositoriesWithOptions(cmd.Key, 28 | map[string]interface{}{ 29 | "start": start, 30 | }, 31 | ) 32 | if err != nil { 33 | logrus.Fatal(err) 34 | } 35 | pageRepos, err := bitbucket.GetRepositoriesResponse(res) 36 | if err != nil { 37 | logrus.Fatalf("unable to parse repositories response: %v", err) 38 | } 39 | repositories = append(repositories, pageRepos...) 40 | hasNextPage, nextPageStart := bitbucket.HasNextPage(res) 41 | if !hasNextPage { 42 | break 43 | } 44 | start = nextPageStart 45 | } 46 | 47 | for _, v := range repositories { 48 | // Get HTTP Clone URL 49 | var cloneUrl = "" 50 | for _, cUrl := range v.Links.Clone { 51 | mUrl, err := url.Parse(cUrl.Href) 52 | if err != nil { 53 | continue 54 | } 55 | 56 | if mUrl.Scheme == "https" { 57 | cloneUrl = mUrl.String() 58 | break 59 | } 60 | } 61 | 62 | repoPath := filepath.Join(cmd.Clone.Output, v.Slug) 63 | if err != nil { 64 | logrus.Warnf("Skipping %s (%s): unable to resolve path: %v", v.Name, v.Slug, err) 65 | continue 66 | } 67 | 68 | // Clone repo 69 | repo, err := git.PlainClone(repoPath, false, &git.CloneOptions{ 70 | SingleBranch: true, 71 | ReferenceName: plumbing.NewBranchReferenceName(cmd.Clone.Branch), 72 | URL: cloneUrl, 73 | Auth: &b.cloneCredentials, 74 | }) 75 | 76 | if err != nil { 77 | logrus.Warnf("Unable to clone %s (%s): %v", v.Name, v.Slug, err) 78 | continue 79 | } 80 | 81 | head, err := repo.Head() 82 | if err != nil { 83 | logrus.Warnf("Unable to get head: %s", err) 84 | continue 85 | } 86 | 87 | fmt.Printf("head: %s\n", head.String()) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /internal/repo_branch_compare.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | bitbucketv1 "github.com/gfleury/go-bitbucket-v1" 6 | "strings" 7 | ) 8 | 9 | type RepoBranchCompareCmd struct { 10 | FromBranch string `arg:"-f,--from,required" help:"Name of the branch to be used as base"` 11 | ToBranch string `arg:"-t,--to" help:"Name of the branch to be compared to the base"` 12 | Count bool `arg:"-c,--count" help:"Just output the number of commits between to and from branch"` 13 | } 14 | 15 | func (b *BitbucketCLI) branchCmdCompare(cmd *RepoCmd) { 16 | if cmd == nil || cmd.BranchCmd == nil || cmd.BranchCmd.Compare == nil { 17 | return 18 | } 19 | compare := cmd.BranchCmd.Compare 20 | 21 | response, err := b.client.DefaultApi.GetBranches(cmd.ProjectKey, cmd.Slug, nil) 22 | 23 | if err != nil { 24 | b.logger.Fatalf("Failed to fetch branches %s", err.Error()) 25 | return 26 | } 27 | branches, err := bitbucketv1.GetBranchesResponse(response) 28 | 29 | fromBranch := findBranch(branches, cmd.BranchCmd.Compare.FromBranch) 30 | 31 | if fromBranch == nil { 32 | b.logger.Fatalf("Failed to find branch with name %s", compare.FromBranch) 33 | return 34 | } 35 | 36 | toBranch := findBranch(branches, cmd.BranchCmd.Compare.ToBranch) 37 | if toBranch == nil { 38 | b.logger.Fatalf("Failed to find branch with name %s", compare.FromBranch) 39 | return 40 | } 41 | 42 | optionals := make(map[string]interface{}) 43 | if compare.Count { 44 | optionals["withCounts"] = true 45 | } 46 | 47 | optionals["since"] = fromBranch.LatestCommit 48 | optionals["until"] = toBranch.LatestCommit 49 | 50 | commitResponse, err := b.client.DefaultApi.GetCommits(cmd.ProjectKey, cmd.Slug, optionals) 51 | 52 | if err != nil { 53 | b.logger.Fatalf("Failed to fetch commits %s", err.Error()) 54 | return 55 | } 56 | 57 | commits, err := bitbucketv1.GetCommitsResponse(commitResponse) 58 | 59 | if err != nil { 60 | b.logger.Fatalf("Failed to parse commits %s", err.Error()) 61 | return 62 | } 63 | 64 | if compare.Count { 65 | fmt.Printf("%d", len(commits)) 66 | return 67 | } 68 | for _, c := range commits { 69 | fmt.Printf("%s %s \n", c.DisplayID, strings.Split(c.Message, "\n")[0]) 70 | } 71 | } 72 | 73 | func findBranch(branches []bitbucketv1.Branch, branchName string) *bitbucketv1.Branch { 74 | for _, b := range branches { 75 | if b.DisplayID == branchName { 76 | return &b 77 | } 78 | } 79 | 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /internal/repo_pr_create.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | bitbucketv1 "github.com/gfleury/go-bitbucket-v1" 6 | "strings" 7 | ) 8 | 9 | type RepoPrCreateCmd struct { 10 | Title string `arg:"-t,--title,required" help:"Title of this PR"` 11 | Description string `arg:"-d,--description" help:"Description of the PR"` 12 | 13 | FromRef string `arg:"-F,--from-ref,required" help:"Reference of the incoming PR, e.g: refs/heads/feature-ABC-123"` // e.g: refs/heads/feature-ABC-123 14 | ToRef string `arg:"-T,--to-ref,required" help:"Target reference, e.g: refs/heads/master"` 15 | 16 | // From which repo? Defaults to self 17 | FromRepoKey string `arg:"-K,--from-key" help:"Project AccessToken of the \"from\" repository"` 18 | FromRepoSlug string `arg:"-S,--from-slug" help:"Repository slug of the \"from\" repository"` 19 | 20 | Reviewers string `arg:"-r,--reviewers,env:BITBUCKET_REVIEWERS" help:"Comma separated list of reviewers"` 21 | } 22 | 23 | func (b BitbucketCLI) GetReviewers(revList string) []bitbucketv1.UserWithMetadata { 24 | if revList == "" { 25 | return nil 26 | } 27 | var reviewers []bitbucketv1.UserWithMetadata 28 | for _, user := range strings.Split(revList, ",") { 29 | if usersResponse, err := b.client.DefaultApi.GetUsers(map[string]interface{}{"filter": user}); err != nil { 30 | b.logger.Fatalf("Error while retrieving user %s: %e", user, err) 31 | } else if users, err := bitbucketv1.GetUsersResponse(usersResponse); err != nil { 32 | b.logger.Fatalf("Error while parsing list of users for user %s: %e", user, err) 33 | } else if len(users) == 0 { 34 | b.logger.Fatalf("user %s does not exist", user) 35 | } else if len(users) > 1 { 36 | var found []string 37 | for _, bbUser := range users { 38 | found = append(found, fmt.Sprintf("%s: %s (%s)", bbUser.Slug, bbUser.Name, bbUser.EmailAddress)) 39 | } 40 | b.logger.Fatalf("multiple users found for user %s: %s", user, strings.Join(found, ", ")) 41 | } else { 42 | bbUser := users[0] 43 | reviewers = append(reviewers, bitbucketv1.UserWithMetadata{ 44 | User: bitbucketv1.UserWithLinks{ 45 | Name: bbUser.Name, 46 | EmailAddress: bbUser.EmailAddress, 47 | Slug: bbUser.Slug, 48 | }, 49 | }) 50 | } 51 | } 52 | return reviewers 53 | } 54 | 55 | func (b *BitbucketCLI) repoPrCreate(cmd *RepoCmd) { 56 | if cmd == nil || cmd.PrCmd == nil || cmd.PrCmd.Create == nil { 57 | return 58 | } 59 | create := cmd.PrCmd.Create 60 | 61 | if create.FromRepoKey == "" && create.FromRepoSlug == "" { 62 | // From = To 63 | create.FromRepoKey = cmd.ProjectKey 64 | create.FromRepoSlug = cmd.Slug 65 | } 66 | 67 | pr := bitbucketv1.PullRequest{ 68 | Title: create.Title, 69 | Description: create.Description, 70 | FromRef: bitbucketv1.PullRequestRef{ 71 | ID: create.FromRef, 72 | Repository: bitbucketv1.Repository{ 73 | Slug: create.FromRepoSlug, 74 | Project: &bitbucketv1.Project{Key: create.FromRepoKey}, 75 | }, 76 | }, 77 | ToRef: bitbucketv1.PullRequestRef{ 78 | ID: create.ToRef, 79 | Repository: bitbucketv1.Repository{ 80 | Slug: cmd.Slug, 81 | Project: &bitbucketv1.Project{Key: cmd.ProjectKey}, 82 | }, 83 | }, 84 | Reviewers: b.GetReviewers(create.Reviewers), 85 | } 86 | 87 | resp, err := b.client.DefaultApi.CreatePullRequest( 88 | cmd.ProjectKey, 89 | cmd.Slug, 90 | pr, 91 | ) 92 | if err != nil { 93 | b.logger.Fatalf("unable to create PR: %v", err) 94 | } 95 | 96 | // Parse resp 97 | prRes, err := bitbucketv1.GetPullRequestResponse(resp) 98 | if err != nil { 99 | b.logger.Fatalf("unable to parse PR: %v", err) 100 | } 101 | 102 | fmt.Printf("%s", prRes.Links.Self[0].Href) 103 | } 104 | -------------------------------------------------------------------------------- /cmd/bitbucket-cli/bitbucket.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/adrg/xdg" 5 | "github.com/alexflint/go-arg" 6 | "github.com/sirupsen/logrus" 7 | cli "github.com/swisscom/bitbucket-cli/internal" 8 | "gopkg.in/yaml.v2" 9 | "os" 10 | "path" 11 | ) 12 | 13 | type Args struct { 14 | Debug *bool `arg:"-D,--debug"` 15 | Username string `arg:"-u,--username,env:BITBUCKET_USERNAME"` 16 | Password string `arg:"-p,--password,env:BITBUCKET_PASSWORD"` 17 | AccessToken string `arg:"-t,--access-token,env:BITBUCKET_ACCESS_TOKEN" help:"A Personal Access Token"` 18 | Url string `arg:"-u,--url,env:BITBUCKET_URL" help:"URL to the REST API of Bitbucket, e.g: https://git.example.com/rest"` 19 | Config string `arg:"-c,--config"` 20 | Project *cli.ProjectCmd `arg:"subcommand:project"` 21 | Repo *cli.RepoCmd `arg:"subcommand:repo"` 22 | Pr *cli.PrCmd `arg:"subcommand:pr"` 23 | } 24 | 25 | var args Args 26 | 27 | func main() { 28 | p := arg.MustParse(&args) 29 | logger := logrus.New() 30 | 31 | if args.Debug != nil && *args.Debug { 32 | logger.SetLevel(logrus.DebugLevel) 33 | } 34 | 35 | loadConfigIntoArgs(logger) 36 | 37 | if args.Url == "" { 38 | logger.Fatalf("please specifiy a Bitbucket URL") 39 | } 40 | 41 | if args.Username == "" { 42 | logger.Fatalf("An username is required") 43 | } 44 | 45 | var auth cli.Authenticator 46 | if args.Password != "" { 47 | // Basic Auth 48 | auth = cli.BasicAuth{Username: args.Username, Password: args.Password} 49 | } else if args.AccessToken != "" { 50 | auth = cli.AccessToken{Username: args.Username, AccessToken: args.AccessToken} 51 | } else { 52 | logger.Fatalf("either a password or an access token must be provided") 53 | } 54 | 55 | c, err := cli.NewCLI(auth, args.Url) 56 | if err != nil { 57 | logger.Fatalf("unable to create CLI: %v", err) 58 | } 59 | c.SetLogger(logger) 60 | 61 | if args.Project != nil { 62 | c.RunProjectCmd(args.Project) 63 | return 64 | } 65 | 66 | if args.Repo != nil { 67 | c.RunRepoCmd(args.Repo) 68 | return 69 | } 70 | 71 | if args.Pr != nil { 72 | c.RunPRCmd(args.Pr) 73 | return 74 | } 75 | 76 | p.Fail("Command must be specified") 77 | 78 | } 79 | 80 | func loadConfigIntoArgs(logger *logrus.Logger) { 81 | var cfg *cli.Config 82 | var err error 83 | 84 | if args.Config != "" { 85 | // Load config if specified 86 | cfg, err = loadConfig(logger, args.Config) 87 | if err != nil { 88 | logger.Fatalf("unable to load config: %v", err) 89 | } 90 | } else { 91 | stdCfgFilePath := path.Join(xdg.ConfigHome, "bitbucket-cli", "config.yml") 92 | logger.Debugf("loading config from %v", stdCfgFilePath) 93 | _, err = os.Stat(stdCfgFilePath) 94 | if err == nil { 95 | // File exists, let's load the config 96 | cfg, err = loadConfig(logger, stdCfgFilePath) 97 | if err != nil { 98 | logger.Fatalf("cannot parse config (%s): %v", stdCfgFilePath, err) 99 | } 100 | } 101 | } 102 | 103 | if cfg != nil { 104 | args.Username = setIfEmpty(args.Username, cfg.Username) 105 | args.Password = setIfEmpty(args.Password, cfg.Password) 106 | args.AccessToken = setIfEmpty(args.AccessToken, cfg.AccessToken) 107 | args.Url = setIfEmpty(args.Url, cfg.Url) 108 | } 109 | } 110 | 111 | func setIfEmpty(argValue string, cfgValue string) string { 112 | if argValue != "" { 113 | return argValue 114 | } 115 | 116 | return cfgValue 117 | } 118 | 119 | func loadConfig(logger *logrus.Logger, configPath string) (*cli.Config, error) { 120 | if configPath == "" { 121 | return nil, nil 122 | } 123 | 124 | f, err := os.Open(configPath) 125 | if err != nil { 126 | logger.Fatalf("unable to open config: %v", err) 127 | } 128 | 129 | var config cli.Config 130 | dec := yaml.NewDecoder(f) 131 | err = dec.Decode(&config) 132 | if err != nil { 133 | logger.Fatalf("unable to parse config: %v", err) 134 | } 135 | 136 | return &config, nil 137 | } 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bitbucket-cli 2 | 3 | A [Bitbucket Enterprise](https://bitbucket.org/product/enterprise) CLI. 4 | 5 | ``` 6 | Usage: bitbucket-cli [--username USERNAME] [--password PASSWORD] --url URL [] 7 | 8 | Options: 9 | --username USERNAME, -u USERNAME 10 | --password PASSWORD, -p PASSWORD 11 | --url URL, -u URL 12 | --help, -h display this help and exit 13 | 14 | Commands: 15 | project 16 | ``` 17 | 18 | # Docker container 19 | 20 | A docker container for this project can be obtained [here](https://github.com/swisscom/bitbucket-cli/pkgs/container/bitbucket-cli). 21 | 22 | ## Project 23 | 24 | ### List 25 | 26 | Lists the repositories in a project 27 | 28 | ``` 29 | $ export BITBUCKET_USERNAME="my-bitbucket-username" 30 | $ read -s BITBUCKET_PASSWORD # Type your password and then press ENTER 31 | $ export BITBUCKET_PASSWORD 32 | $ bitbucket-cli --url https://your-bitbucket-hostname/rest project list -k PRJKEY 33 | 34 | project-1 https://your-bitbucket-hostname/scm/prjkey/project-1.git 35 | project-2 https://your-bitbucket-hostname/scm/prjkey/project-2.git 36 | project-3 https://your-bitbucket-hostname/scm/prjkey/project-3.git 37 | 38 | ``` 39 | 40 | ### Clone 41 | 42 | Clones all the repositories in a project: 43 | 44 | ``` 45 | $ export BITBUCKET_USERNAME="my-bitbucket-username" 46 | $ read -r -s BITBUCKET_PASSWORD # Type your password and then press ENTER 47 | $ export BITBUCKET_PASSWORD 48 | $ bitbucket-cli --url https://your-bitbucket-hostname/rest project clone -k PRJKEY -o /tmp/test/ 49 | 50 | head: 987a5d8c25d8adb5ba013cf1cb88cd56a189241e5048b9702f319fb6e641cf81 refs/heads/master 51 | head: df2b794192904e6a9265975f33510eebe680177013e86fd7002850f45389ad34 refs/heads/master 52 | head: 2cf20bee2c59c3b8cae6ec0820a1353ff0ca2adeecdb84ba773845cff91ab121 refs/heads/master 53 | 54 | $ ls -la /tmp/test 55 | total 0 56 | drwxr-xr-x 13 dvitali dvitali 260 Jul 21 18:09 . 57 | drwxrwxrwt 29 root root 1400 Jul 21 18:11 .. 58 | drwxr-xr-x 3 dvitali dvitali 120 Jul 21 18:09 project-1 59 | drwxr-xr-x 4 dvitali dvitali 140 Jul 21 18:09 project-2 60 | drwxr-xr-x 3 dvitali dvitali 100 Jul 21 18:09 project-3 61 | ``` 62 | 63 | 64 | ## Repo 65 | 66 | This main subcommand requires two arguments: 67 | 68 | - `-k KEY` 69 | - `-n NAME` 70 | 71 | These are basically the identifiers for your repository, not including one of the twos in all of the 72 | subcommands will result in an error. 73 | 74 | ### PR 75 | 76 | This subcommand deals with PRs, please check its subcommands. 77 | 78 | #### Create 79 | 80 | This command, subcommand of (`repo pr`) allows you to create a Pull Request. 81 | 82 | Use it as follows: 83 | 84 | ``` 85 | bitbucket-cli repo -k "KEY" \ 86 | -n "bitbucket-playground" \ 87 | pr create \ 88 | -t "Some Title" \ 89 | -d "Some Description :thumbsup:" \ 90 | -F "refs/heads/feature/2" -T "refs/heads/master" 91 | ``` 92 | 93 | 94 | ##### Usage 95 | 96 | ``` 97 | Usage: bitbucket-cli repo pr create --title TITLE [--description DESCRIPTION] --from-ref FROM-REF --to-ref TO-REF [--from-key FROM-KEY] [--from-slug FROM-SLUG] 98 | 99 | Options: 100 | --title TITLE, -t TITLE 101 | Title of this PR 102 | --description DESCRIPTION, -d DESCRIPTION 103 | Description of the PR 104 | --from-ref FROM-REF, -F FROM-REF 105 | Reference of the incoming PR, e.g: refs/heads/feature-ABC-123 106 | --to-ref TO-REF, -T TO-REF 107 | Target reference, e.g: refs/heads/master 108 | --from-key FROM-KEY, -K FROM-KEY 109 | Project Key of the "from" repository 110 | --from-slug FROM-SLUG, -S FROM-SLUG 111 | Repository slug of the "from" repository 112 | --help, -h display this help and exit 113 | ``` 114 | 115 | #### List 116 | 117 | Lists all the PRs for the chosen repository 118 | 119 | ``` 120 | $ bitbucket-cli repo -k KEY -n bitbucket-playground pr list 121 | Some Title (ID: 2) 122 | feature 1 (ID: 1) 123 | ``` 124 | 125 | ``` 126 | $ bitbucket-cli repo -k KEY -n bitbucket-playground pr list -s DECLINED 127 | feature 1 (ID: 1) 128 | ``` 129 | 130 | ##### Usage 131 | 132 | ```plain 133 | Usage: bitbucket-cli repo pr list [--state STATE] 134 | 135 | Options: 136 | --state STATE, -s STATE 137 | PR State, any of: ALL, OPEN, DECLINED, MERGED 138 | --help, -h display this help and exit 139 | ``` 140 | 141 | ### Security 142 | 143 | #### Scan 144 | 145 | ##### Usage 146 | 147 | ```plain 148 | Usage: bitbucket-cli repo security scan 149 | 150 | Options: 151 | --help, -h display this help and exit 152 | ``` 153 | 154 | ##### Example 155 | 156 | ```plain 157 | bitbucket-cli repo -k ABC -n some-repo security scan 158 | ``` 159 | 160 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= 3 | github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= 4 | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= 5 | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= 6 | github.com/alexflint/go-arg v1.3.0 h1:UfldqSdFWeLtoOuVRosqofU4nmhI1pYEbT4ZFS34Bdo= 7 | github.com/alexflint/go-arg v1.3.0/go.mod h1:9iRbDxne7LcR/GSvEr7ma++GLpdIU1zrghf2y2768kM= 8 | github.com/alexflint/go-scalar v1.0.0 h1:NGupf1XV/Xb04wXskDFzS0KWOLH632W/EO4fAFi+A70= 9 | github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw= 10 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= 11 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 12 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 13 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 14 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 15 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 17 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= 19 | github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= 20 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 21 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 22 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 23 | github.com/gfleury/go-bitbucket-v1 v0.0.0-20200327185123-b3c7f99f7f5c h1:g+MQyHCXOctDwLAnMTf3BNWbcEWD3E4WPc25BA+8r3c= 24 | github.com/gfleury/go-bitbucket-v1 v0.0.0-20200327185123-b3c7f99f7f5c/go.mod h1:LB3osS9X2JMYmTzcCArHHLrndBAfcVLQAvUddfs+ONs= 25 | github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= 26 | github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 27 | github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= 28 | github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= 29 | github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM= 30 | github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= 31 | github.com/go-git/go-git-fixtures/v4 v4.0.1 h1:q+IFMfLx200Q3scvt2hN79JsEzy4AmBTp/pqnefH+Bc= 32 | github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= 33 | github.com/go-git/go-git/v5 v5.1.0 h1:HxJn9g/E7eYvKW3Fm7Jt4ee8LXfPOm/H1cdDu8vEssk= 34 | github.com/go-git/go-git/v5 v5.1.0/go.mod h1:ZKfuPUoY1ZqIG4QG9BDBh3G4gLM5zvPuSJAozQrZuyM= 35 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 36 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 37 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 38 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 39 | github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg= 40 | github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 41 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 42 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 43 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 44 | github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= 45 | github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 46 | github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= 47 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 48 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 49 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 50 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 51 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 52 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 53 | github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= 54 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 55 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 56 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 57 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 58 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 59 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 60 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 61 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 62 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 63 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 64 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 65 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 66 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 67 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 68 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 69 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 70 | github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= 71 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 72 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 73 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 74 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 75 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 76 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 77 | github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= 78 | github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= 79 | golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 80 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 81 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= 82 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 83 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 84 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 85 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 86 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= 87 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 88 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= 89 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 90 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 91 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= 92 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 93 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 94 | golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 95 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 96 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 97 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 98 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 99 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 100 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 101 | golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 h1:2B5p2L5IfGiD7+b9BOoRMC6DgObAVZV+Fsp050NqXik= 102 | golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 103 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 104 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 105 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 106 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 107 | google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= 108 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 109 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 110 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 111 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 112 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 113 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 114 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 115 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 116 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 117 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 118 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 119 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 120 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 121 | --------------------------------------------------------------------------------