diff --git a/Makefile b/Makefile index a581114b16..3ad06bb165 100644 --- a/Makefile +++ b/Makefile @@ -283,6 +283,7 @@ codegen-controller: install-controller-gen .PHONY: codegen-bitbucket-client codegen-bitbucket-client: install-oapi-codegen cd pkg/gitprovider/bitbucket/cloud && $(OAPI_CODEGEN_LINK) --config spec/oapi-codegen.yaml spec/bitbucket.gen.json + cd pkg/gitprovider/bitbucket/datacenter && $(OAPI_CODEGEN_LINK) --config spec/oapi-codegen.yaml spec/bitbucket-datacenter.gen.json .PHONY: codegen-schema-to-go codegen-schema-to-go: install-goimports diff --git a/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/git-merge-pr.md b/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/git-merge-pr.md index 2a8219c321..8c9fb38156 100644 --- a/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/git-merge-pr.md +++ b/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/git-merge-pr.md @@ -41,7 +41,7 @@ system to access the git repos. | Name | Type | Required | Description | | ----------------------- | --------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `repoURL` | `string` | Y | The URL of a remote Git repository. **Deprecated:** Support for SSH URLs (`ssh://` and SCP-style `git@host:path`) is deprecated as of v1.10.0 and will be removed in v1.13.0. Use HTTPS URLs instead. | -| `provider` | `string` | N | The name of the Git provider to use. Currently `azure`, `bitbucket`, `gitea`, `github`, and `gitlab` are supported. Kargo will try to infer the provider if it is not explicitly specified. | +| `provider` | `string` | N | The name of the Git provider to use. Currently `azure`, `bitbucket`, `bitbucket-datacenter`, `gitea`, `github`, and `gitlab` are supported. Kargo will try to infer the provider if it is not explicitly specified. | | `insecureSkipTLSVerify` | `boolean` | N | Indicates whether to bypass TLS certificate verification when interfacing with the Git provider. Setting this to `true` is highly discouraged in production. | | `prNumber` | `integer` | Y | The pull request number to merge. | | `mergeMethod` | `string` | N | The merge method to use when merging the pull request. The supported methods are provider-specific; refer to the [Merge Method](#merge-method) section. | @@ -65,7 +65,8 @@ currently supported Git hosting providers. | Provider | Supported Methods | Default | | -------- | ----------------- | ------- | | Azure | | First allowed strategy per the target branch's merge type policy; merge commit if no policy is configured | -| BitBucket | | The repository's configured default merge strategy | +| Bitbucket Cloud | | The repository's configured default merge strategy | +| Bitbucket Data Center | | The repository's configured default merge strategy | | Gitea | | `merge` | | GitHub | | `merge` | | GitLab | | Defers to the merge request and project-level squash settings | diff --git a/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/git-open-pr.md b/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/git-open-pr.md index 02862aae60..bf4145a54e 100644 --- a/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/git-open-pr.md +++ b/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/git-open-pr.md @@ -23,7 +23,7 @@ system to access the git repos. | Name | Type | Required | Description | |------|------|----------|-------------| | `repoURL` | `string` | Y | The URL of a remote Git repository. **Deprecated:** Support for SSH URLs (`ssh://` and SCP-style `git@host:path`) is deprecated as of v1.10.0 and will be removed in v1.13.0. Use HTTPS URLs instead. | -| `provider` | `string` | N | The name of the Git provider to use. Currently `azure`, `bitbucket`, `gitea`, `github`, and `gitlab` are supported. Kargo will try to infer the provider if it is not explicitly specified. | +| `provider` | `string` | N | The name of the Git provider to use. Currently `azure`, `bitbucket`, `bitbucket-datacenter`, `gitea`, `github`, and `gitlab` are supported. Kargo will try to infer the provider if it is not explicitly specified. | | `insecureSkipTLSVerify` | `boolean` | N | Indicates whether to bypass TLS certificate verification when interfacing with the Git provider. Setting this to `true` is highly discouraged in production. | | `sourceBranch` | `string` | Y | Specifies the source branch for the pull request. | | `targetBranch` | `string` | N | The branch to which the changes should be merged. | diff --git a/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/git-push.md b/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/git-push.md index 9c947c722f..82689ac00c 100644 --- a/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/git-push.md +++ b/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/git-push.md @@ -106,7 +106,7 @@ system to access Git repositories. | `generateTargetBranch` | `boolean` | N | Whether to push to a remote branch named like `kargo/promotion/`. If such a branch does not already exist, it will be created. A value of `true` is mutually exclusive with `targetBranch` and `tag`. If none of these are provided, the target branch will be the currently checked out branch. This option is useful when a subsequent promotion step will open a pull request against a Stage-specific branch. In such a case, the generated target branch pushed to by the `git-push` step can later be utilized as the source branch of the pull request. | | `tag` | `string` | N | An tag to push to the remote repository. Mutually exclusive with `generateTargetBranch` and `targetBranch`. | | `force` | `boolean` | N | Whether to force push to the target branch, overwriting any existing history. This is useful for scenarios where you want to completely replace the branch content (e.g., pushing rendered manifests that don't depend on previous state). **Use with caution** as this will overwrite any commits that exist on the remote branch but not in your local branch. Default is `false`. A value of `true` is mutually exclusive with `tag`. | -| `provider` | `string` | N | The name of the Git provider to use. Currently `azure`, `bitbucket`, `gitea`, `github`, and `gitlab` are supported. Kargo will try to infer the provider if it is not explicitly specified. This setting does not affect the push operation but helps generate the correct [`commitURL` output](#output) when working with repositories where the provider cannot be automatically determined, such as self-hosted instances. | +| `provider` | `string` | N | The name of the Git provider to use. Currently `azure`, `bitbucket`, `bitbucket-datacenter`, `gitea`, `github`, and `gitlab` are supported. Kargo will try to infer the provider if it is not explicitly specified. This setting does not affect the push operation but helps generate the correct [`commitURL` output](#output) when working with repositories where the provider cannot be automatically determined, such as self-hosted instances. | ## Output diff --git a/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/git-wait-for-pr.md b/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/git-wait-for-pr.md index 0e17dfaddc..db5edbfa7a 100644 --- a/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/git-wait-for-pr.md +++ b/docs/docs/50-user-guide/60-reference-docs/30-promotion-steps/git-wait-for-pr.md @@ -35,7 +35,7 @@ system to access the git repos. | Name | Type | Required | Description | |------|------|----------|-------------| | `repoURL` | `string` | Y | The URL of a remote Git repository. **Deprecated:** Support for SSH URLs (`ssh://` and SCP-style `git@host:path`) is deprecated as of v1.10.0 and will be removed in v1.13.0. Use HTTPS URLs instead. | -| `provider` | `string` | N | The name of the Git provider to use. Currently `azure`, `bitbucket`, `gitea`, `github`, and `gitlab` are supported. Kargo will try to infer the provider if it is not explicitly specified. | +| `provider` | `string` | N | The name of the Git provider to use. Currently `azure`, `bitbucket`, `bitbucket-datacenter`, `gitea`, `github`, and `gitlab` are supported. Kargo will try to infer the provider if it is not explicitly specified. | | `insecureSkipTLSVerify` | `boolean` | N | Indicates whether to bypass TLS certificate verification when interfacing with the Git provider. Setting this to `true` is highly discouraged in production. | | `prNumber` | `integer` | Y | The pull request number to wait for. | diff --git a/pkg/gitprovider/bitbucket/datacenter/provider.go b/pkg/gitprovider/bitbucket/datacenter/provider.go new file mode 100644 index 0000000000..c41a85c429 --- /dev/null +++ b/pkg/gitprovider/bitbucket/datacenter/provider.go @@ -0,0 +1,434 @@ +package datacenter + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "github.com/hashicorp/go-cleanhttp" + + "github.com/akuity/kargo/pkg/gitprovider" + "github.com/akuity/kargo/pkg/urls" +) + +const ( + // ProviderName is the name used to register the Bitbucket Data Center provider. + ProviderName = "bitbucket-datacenter" +) + +var registration = gitprovider.Registration{ + Predicate: func(repoURL string) bool { + u, err := url.Parse(repoURL) + if err != nil { + return false + } + // We assume that any hostname containing "bitbucket" that is not + // bitbucket.org is a self-hosted Data Center instance. Instances that + // don't include "bitbucket" in their hostname won't be auto-detected and + // will require explicit provider configuration via opts.Name. + host := u.Hostname() + return strings.Contains(host, "bitbucket") && host != "bitbucket.org" + }, + NewProvider: func( + repoURL string, + opts *gitprovider.Options, + ) (gitprovider.Interface, error) { + return NewProvider(repoURL, opts) + }, +} + +func init() { + gitprovider.Register(ProviderName, registration) +} + +// provider is a Bitbucket Data Center implementation of gitprovider.Interface. +type provider struct { + baseURL string + projectKey string + repoSlug string + client ClientWithResponsesInterface +} + +// NewProvider returns a Bitbucket Data Center implementation of gitprovider.Interface. +func NewProvider( + repoURL string, + opts *gitprovider.Options, +) (gitprovider.Interface, error) { + if opts == nil { + opts = &gitprovider.Options{} + } + + baseURL, projectKey, repoSlug, err := parseRepoURL(repoURL) + if err != nil { + return nil, err + } + + token := opts.Token + client, err := NewClientWithResponses( + baseURL, + WithHTTPClient(cleanhttp.DefaultClient()), + WithRequestEditorFn(func(_ context.Context, req *http.Request) error { + req.Header.Set("Authorization", "Bearer "+token) + return nil + }), + ) + if err != nil { + return nil, fmt.Errorf("error creating Bitbucket Data Center API client: %w", err) + } + + return &provider{ + baseURL: baseURL, + projectKey: projectKey, + repoSlug: repoSlug, + client: client, + }, nil +} + +// CreatePullRequest implements gitprovider.Interface. +func (p *provider) CreatePullRequest( + ctx context.Context, + opts *gitprovider.CreatePullRequestOpts, +) (*gitprovider.PullRequest, error) { + if opts == nil { + opts = &gitprovider.CreatePullRequestOpts{} + } + + projectKey := p.projectKey + repoSlug := p.repoSlug + project := RestProject{Key: &projectKey} + + body := CreatePullRequestJSONRequestBody{ + Title: opts.Title, + FromRef: RestCreateRef{ + Id: "refs/heads/" + opts.Head, + Repository: RestRefRepository{ + Slug: &repoSlug, + Project: &project, + }, + }, + ToRef: RestCreateRef{ + Id: "refs/heads/" + opts.Base, + Repository: RestRefRepository{ + Slug: &repoSlug, + Project: &project, + }, + }, + } + if opts.Description != "" { + body.Description = &opts.Description + } + + resp, err := p.client.CreatePullRequestWithResponse(ctx, p.projectKey, p.repoSlug, body) + if err != nil { + return nil, fmt.Errorf("error creating pull request: %w", err) + } + if resp.JSON201 == nil { + return nil, fmt.Errorf( + "unexpected response %d creating pull request", resp.StatusCode(), + ) + } + return toProviderPR(resp.JSON201), nil +} + +// GetPullRequest implements gitprovider.Interface. +func (p *provider) GetPullRequest( + ctx context.Context, + id int64, +) (*gitprovider.PullRequest, error) { + resp, err := p.client.GetPullRequestWithResponse(ctx, p.projectKey, p.repoSlug, int(id)) + if err != nil { + return nil, fmt.Errorf("error getting pull request %d: %w", id, err) + } + if resp.JSON200 == nil { + return nil, fmt.Errorf( + "unexpected response %d getting pull request %d", resp.StatusCode(), id, + ) + } + return toProviderPR(resp.JSON200), nil +} + +// ListPullRequests implements gitprovider.Interface. +func (p *provider) ListPullRequests( + ctx context.Context, + opts *gitprovider.ListPullRequestOptions, +) ([]gitprovider.PullRequest, error) { + if opts == nil { + opts = &gitprovider.ListPullRequestOptions{} + } + if opts.State == "" { + opts.State = gitprovider.PullRequestStateOpen + } + + var state GetPullRequestsParamsState + switch opts.State { + case gitprovider.PullRequestStateOpen: + state = GetPullRequestsParamsStateOPEN + case gitprovider.PullRequestStateClosed, gitprovider.PullRequestStateAny: + state = GetPullRequestsParamsStateALL + default: + return nil, fmt.Errorf("unknown pull request state %q", opts.State) + } + + resp, err := p.client.GetPullRequestsWithResponse( + ctx, + p.projectKey, + p.repoSlug, + &GetPullRequestsParams{State: &state}, + ) + if err != nil { + return nil, fmt.Errorf("error listing pull requests: %w", err) + } + if resp.JSON200 == nil { + return nil, fmt.Errorf( + "unexpected response %d listing pull requests", resp.StatusCode(), + ) + } + + var result []gitprovider.PullRequest + if resp.JSON200.Values == nil { + return result, nil + } + for i := range *resp.JSON200.Values { + pr := &(*resp.JSON200.Values)[i] + // When requesting ALL states for the closed filter, exclude open PRs. + if opts.State == gitprovider.PullRequestStateClosed { + var prState RestPullRequestState + if pr.State != nil { + prState = *pr.State + } + if prState == RestPullRequestStateOPEN { + continue + } + } + // NB: The Bitbucket Data Center API doesn't support filtering by + // source/destination branch or commit hash, so we filter client-side. + if opts.HeadBranch != "" { + branchName := "" + if pr.FromRef != nil && pr.FromRef.DisplayId != nil { + branchName = *pr.FromRef.DisplayId + } + if branchName != opts.HeadBranch { + continue + } + } + if opts.BaseBranch != "" { + branchName := "" + if pr.ToRef != nil && pr.ToRef.DisplayId != nil { + branchName = *pr.ToRef.DisplayId + } + if branchName != opts.BaseBranch { + continue + } + } + if opts.HeadCommit != "" { + commitHash := "" + if pr.FromRef != nil && pr.FromRef.LatestCommit != nil { + commitHash = *pr.FromRef.LatestCommit + } + if commitHash != opts.HeadCommit { + continue + } + } + result = append(result, *toProviderPR(pr)) + } + return result, nil +} + +// MergePullRequest implements gitprovider.Interface. +func (p *provider) MergePullRequest( + ctx context.Context, + id int64, + opts *gitprovider.MergePullRequestOpts, +) (*gitprovider.PullRequest, bool, error) { + if opts == nil { + opts = &gitprovider.MergePullRequestOpts{} + } + + prID := int(id) + + getResp, err := p.client.GetPullRequestWithResponse(ctx, p.projectKey, p.repoSlug, prID) + if err != nil { + return nil, false, fmt.Errorf("error getting pull request %d: %w", id, err) + } + if getResp.JSON200 == nil { + return nil, false, fmt.Errorf( + "unexpected response %d getting pull request %d", getResp.StatusCode(), id, + ) + } + pr := getResp.JSON200 + + var state RestPullRequestState + if pr.State != nil { + state = *pr.State + } + + if state == RestPullRequestStateMERGED { + return toProviderPR(pr), true, nil + } + + if state != RestPullRequestStateOPEN { + return nil, false, fmt.Errorf( + "pull request %d is closed but not merged (state: %s)", id, state, + ) + } + + if pr.Draft != nil && *pr.Draft { + return nil, false, nil + } + + var version int + if pr.Version != nil { + version = *pr.Version + } + + var mergeStrategy *RestMergeStrategy + if opts.MergeMethod != "" { + s := RestMergeStrategyId(opts.MergeMethod) + if !s.Valid() { + return nil, false, fmt.Errorf("unsupported merge strategy %q", opts.MergeMethod) + } + mergeStrategy = &RestMergeStrategy{Id: &s} + } + + mergeResp, err := p.client.MergePullRequestWithResponse( + ctx, + p.projectKey, + p.repoSlug, + prID, + &MergePullRequestParams{Version: version}, + MergePullRequestJSONRequestBody{Strategy: mergeStrategy}, + ) + if err != nil { + return nil, false, fmt.Errorf("error merging pull request %d: %w", id, err) + } + if mergeResp.JSON200 == nil { + return nil, false, fmt.Errorf( + "unexpected response %d merging pull request %d", mergeResp.StatusCode(), id, + ) + } + + mergedPR := mergeResp.JSON200 + var mergedState RestPullRequestState + if mergedPR.State != nil { + mergedState = *mergedPR.State + } + + if mergedState != RestPullRequestStateMERGED { + return nil, false, fmt.Errorf( + "unexpected state %q after merging pull request %d", mergedState, id, + ) + } + + return toProviderPR(mergedPR), true, nil +} + +// GetCommitURL implements gitprovider.Interface. +func (p *provider) GetCommitURL(_ string, sha string) (string, error) { + var projectPath string + if strings.HasPrefix(p.projectKey, "~") { + projectPath = fmt.Sprintf( + "/users/%s/repos/%s", + strings.TrimPrefix(p.projectKey, "~"), + p.repoSlug, + ) + } else { + projectPath = fmt.Sprintf("/projects/%s/repos/%s", p.projectKey, p.repoSlug) + } + return fmt.Sprintf("%s%s/commits/%s", p.baseURL, projectPath, sha), nil +} + +// toProviderPR converts a RestPullRequest to a gitprovider.PullRequest. +func toProviderPR(pr *RestPullRequest) *gitprovider.PullRequest { + if pr == nil { + return nil + } + + var id int64 + if pr.Id != nil { + id = int64(*pr.Id) + } + + var state RestPullRequestState + if pr.State != nil { + state = *pr.State + } + + var prURL string + if pr.Links != nil && pr.Links.Self != nil && len(*pr.Links.Self) > 0 { + if href := (*pr.Links.Self)[0].Href; href != nil { + prURL = *href + } + } + + var headSHA string + if pr.FromRef != nil && pr.FromRef.LatestCommit != nil { + headSHA = *pr.FromRef.LatestCommit + } + + var createdAt *time.Time + if pr.CreatedDate != nil { + t := time.UnixMilli(*pr.CreatedDate).UTC() + createdAt = &t + } + + return &gitprovider.PullRequest{ + Number: id, + URL: prURL, + Open: state == RestPullRequestStateOPEN, + Merged: state == RestPullRequestStateMERGED, + HeadSHA: headSHA, + CreatedAt: createdAt, + Object: pr, + } +} + +// parseRepoURL extracts the API base URL, project key, and repo slug from a +// Bitbucket Data Center repository URL. It handles three formats: +// +// - Web UI: https://host/projects/{key}/repos/{slug} +// https://host/users/{username}/repos/{slug} +// - HTTP clone: https://host/scm/{key}/{slug} +// - SSH clone: ssh://host/{key}/{slug} (after NormalizeGit) +func parseRepoURL(repoURL string) (baseURL, projectKey, repoSlug string, err error) { + u, err := url.Parse(urls.NormalizeGit(repoURL)) + if err != nil { + return "", "", "", fmt.Errorf("parse Bitbucket Data Center URL %q: %w", repoURL, err) + } + + host := u.Hostname() + if port := u.Port(); port != "" { + host = host + ":" + port + } + baseURL = "https://" + host + + path := strings.TrimPrefix(u.Path, "/") + parts := strings.Split(path, "/") + + switch { + case len(parts) == 4 && parts[2] == "repos" && (parts[0] == "projects" || parts[0] == "users"): + // Web UI URL: /projects/{key}/repos/{slug} or /users/{username}/repos/{slug} + if parts[0] == "users" { + projectKey = "~" + parts[1] + } else { + projectKey = parts[1] + } + repoSlug = parts[3] + case len(parts) == 3 && parts[0] == "scm": + // HTTP clone URL: /scm/{key}/{slug} + projectKey = parts[1] + repoSlug = parts[2] + case len(parts) == 2: + // SSH clone URL (after NormalizeGit): /{key}/{slug} or /~{username}/{slug} + projectKey = parts[0] + repoSlug = parts[1] + default: + return "", "", "", fmt.Errorf( + "invalid repository path in URL %q: expected /projects/{key}/repos/{slug}, /scm/{key}/{slug}, or /{key}/{slug}", + repoURL, + ) + } + return baseURL, projectKey, repoSlug, nil +} diff --git a/pkg/gitprovider/bitbucket/datacenter/provider_test.go b/pkg/gitprovider/bitbucket/datacenter/provider_test.go new file mode 100644 index 0000000000..a62d693935 --- /dev/null +++ b/pkg/gitprovider/bitbucket/datacenter/provider_test.go @@ -0,0 +1,1203 @@ +package datacenter + +import ( + "context" + "errors" + "io" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/akuity/kargo/pkg/gitprovider" +) + +// mockClient implements ClientWithResponsesInterface for testing. +type mockClient struct { + getPRsFunc func( + ctx context.Context, + projectKey, repoSlug string, + params *GetPullRequestsParams, + reqEditors ...RequestEditorFn, + ) (*GetPullRequestsResponse, error) + + createPRFunc func( + ctx context.Context, + projectKey, repoSlug string, + body CreatePullRequestJSONRequestBody, + reqEditors ...RequestEditorFn, + ) (*CreatePullRequestResponse, error) + + getPRFunc func( + ctx context.Context, + projectKey, repoSlug string, + pullRequestId int, + reqEditors ...RequestEditorFn, + ) (*GetPullRequestResponse, error) + + mergePRFunc func( + ctx context.Context, + projectKey, repoSlug string, + pullRequestId int, + params *MergePullRequestParams, + body MergePullRequestJSONRequestBody, + reqEditors ...RequestEditorFn, + ) (*MergePullRequestResponse, error) +} + +func (m *mockClient) GetCommitWithResponse( + _ context.Context, + _, _, _ string, + _ ...RequestEditorFn, +) (*GetCommitResponse, error) { + return nil, errors.New("not implemented") +} + +func (m *mockClient) GetPullRequestsWithResponse( + ctx context.Context, + projectKey, repoSlug string, + params *GetPullRequestsParams, + reqEditors ...RequestEditorFn, +) (*GetPullRequestsResponse, error) { + return m.getPRsFunc(ctx, projectKey, repoSlug, params, reqEditors...) +} + +func (m *mockClient) CreatePullRequestWithBodyWithResponse( + _ context.Context, + _, _ string, + _ string, + _ io.Reader, + _ ...RequestEditorFn, +) (*CreatePullRequestResponse, error) { + return nil, errors.New("not implemented") +} + +func (m *mockClient) CreatePullRequestWithResponse( + ctx context.Context, + projectKey, repoSlug string, + body CreatePullRequestJSONRequestBody, + reqEditors ...RequestEditorFn, +) (*CreatePullRequestResponse, error) { + return m.createPRFunc(ctx, projectKey, repoSlug, body, reqEditors...) +} + +func (m *mockClient) GetPullRequestWithResponse( + ctx context.Context, + projectKey, repoSlug string, + pullRequestId int, + reqEditors ...RequestEditorFn, +) (*GetPullRequestResponse, error) { + return m.getPRFunc(ctx, projectKey, repoSlug, pullRequestId, reqEditors...) +} + +func (m *mockClient) MergePullRequestWithBodyWithResponse( + _ context.Context, + _, _ string, + _ int, + _ *MergePullRequestParams, + _ string, + _ io.Reader, + _ ...RequestEditorFn, +) (*MergePullRequestResponse, error) { + return nil, errors.New("not implemented") +} + +func (m *mockClient) MergePullRequestWithResponse( + ctx context.Context, + projectKey, repoSlug string, + pullRequestId int, + params *MergePullRequestParams, + body MergePullRequestJSONRequestBody, + reqEditors ...RequestEditorFn, +) (*MergePullRequestResponse, error) { + return m.mergePRFunc(ctx, projectKey, repoSlug, pullRequestId, params, body, reqEditors...) +} + +func intPtr(i int) *int { return &i } +func strPtr(s string) *string { return &s } +func boolPtr(b bool) *bool { return &b } +func statePtr(s RestPullRequestState) *RestPullRequestState { return &s } + +// makePR builds a minimal RestPullRequest for use in test cases. +func makePR(id int, state RestPullRequestState) *RestPullRequest { + return &RestPullRequest{ + Id: &id, + State: statePtr(state), + Version: intPtr(1), + } +} + +func Test_registration(t *testing.T) { + t.Parallel() + + t.Run("predicate matches self-hosted bitbucket hostname", func(t *testing.T) { + t.Parallel() + assert.True(t, registration.Predicate("https://bitbucket.example.com/projects/PROJ/repos/repo")) + }) + + t.Run("predicate matches subdomain of bitbucket", func(t *testing.T) { + t.Parallel() + assert.True(t, registration.Predicate("https://git.bitbucket.corp.io/projects/PROJ/repos/repo")) + }) + + t.Run("predicate does not match bitbucket.org (Cloud)", func(t *testing.T) { + t.Parallel() + assert.False(t, registration.Predicate("https://bitbucket.org/owner/repo")) + }) + + t.Run("predicate does not match other providers", func(t *testing.T) { + t.Parallel() + assert.False(t, registration.Predicate("https://github.com/owner/repo")) + }) + + t.Run("predicate handles invalid URLs", func(t *testing.T) { + t.Parallel() + assert.False(t, registration.Predicate("://invalid-url")) + }) + + t.Run("NewProvider factory works", func(t *testing.T) { + t.Parallel() + p, err := registration.NewProvider( + "https://bitbucket.example.com/projects/PROJ/repos/repo", + nil, + ) + assert.NoError(t, err) + assert.NotNil(t, p) + }) +} + +func TestNewProvider(t *testing.T) { + t.Parallel() + + t.Run("successful creation with token", func(t *testing.T) { + t.Parallel() + p, err := NewProvider( + "https://bitbucket.example.com/projects/PROJ/repos/myrepo", + &gitprovider.Options{Token: "token"}, + ) + assert.NoError(t, err) + assert.NotNil(t, p) + }) + + t.Run("successful creation with nil options", func(t *testing.T) { + t.Parallel() + p, err := NewProvider( + "https://bitbucket.example.com/projects/PROJ/repos/myrepo", + nil, + ) + assert.NoError(t, err) + assert.NotNil(t, p) + }) + + t.Run("error with invalid URL path", func(t *testing.T) { + t.Parallel() + p, err := NewProvider( + "https://bitbucket.example.com/invalid", + &gitprovider.Options{}, + ) + assert.Error(t, err) + assert.Nil(t, p) + }) +} + +func TestCreatePullRequest(t *testing.T) { + t.Parallel() + + t.Run("successful creation", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + createPRFunc: func( + _ context.Context, + projectKey, repoSlug string, + body CreatePullRequestJSONRequestBody, + _ ...RequestEditorFn, + ) (*CreatePullRequestResponse, error) { + assert.Equal(t, "PROJ", projectKey) + assert.Equal(t, "myrepo", repoSlug) + assert.Equal(t, "Test PR", body.Title) + assert.Equal(t, "refs/heads/feature", body.FromRef.Id) + assert.Equal(t, "refs/heads/main", body.ToRef.Id) + require.NotNil(t, body.Description) + assert.Equal(t, "PR description", *body.Description) + + prURL := "https://bitbucket.example.com/projects/PROJ/repos/myrepo/pull-requests/1" + return &CreatePullRequestResponse{ + JSON201: &RestPullRequest{ + Id: intPtr(1), + State: statePtr(RestPullRequestStateOPEN), + Links: &RestPullRequestLinks{ + Self: &[]RestHref{{Href: &prURL}}, + }, + FromRef: &RestRef{ + LatestCommit: strPtr("abc123"), + }, + }, + }, nil + }, + } + p := &provider{ + projectKey: "PROJ", + repoSlug: "myrepo", + client: mc, + } + pr, err := p.CreatePullRequest(t.Context(), &gitprovider.CreatePullRequestOpts{ + Title: "Test PR", + Description: "PR description", + Head: "feature", + Base: "main", + }) + require.NoError(t, err) + require.NotNil(t, pr) + assert.Equal(t, int64(1), pr.Number) + assert.Equal(t, "https://bitbucket.example.com/projects/PROJ/repos/myrepo/pull-requests/1", pr.URL) + assert.Equal(t, "abc123", pr.HeadSHA) + assert.True(t, pr.Open) + assert.False(t, pr.Merged) + }) + + t.Run("successful creation with nil options", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + createPRFunc: func( + _ context.Context, + _, _ string, + body CreatePullRequestJSONRequestBody, + _ ...RequestEditorFn, + ) (*CreatePullRequestResponse, error) { + assert.Nil(t, body.Description) // no description set when empty + return &CreatePullRequestResponse{ + JSON201: makePR(42, RestPullRequestStateOPEN), + }, nil + }, + } + p := &provider{client: mc} + pr, err := p.CreatePullRequest(t.Context(), nil) + require.NoError(t, err) + require.NotNil(t, pr) + assert.Equal(t, int64(42), pr.Number) + }) + + t.Run("error from API", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + createPRFunc: func( + _ context.Context, + _, _ string, + _ CreatePullRequestJSONRequestBody, + _ ...RequestEditorFn, + ) (*CreatePullRequestResponse, error) { + return nil, errors.New("network error") + }, + } + p := &provider{client: mc} + pr, err := p.CreatePullRequest(t.Context(), nil) + assert.Error(t, err) + assert.Nil(t, pr) + }) + + t.Run("unexpected non-201 response", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + createPRFunc: func( + _ context.Context, + _, _ string, + _ CreatePullRequestJSONRequestBody, + _ ...RequestEditorFn, + ) (*CreatePullRequestResponse, error) { + return &CreatePullRequestResponse{}, nil // JSON201 is nil + }, + } + p := &provider{client: mc} + pr, err := p.CreatePullRequest(t.Context(), nil) + assert.Error(t, err) + assert.Nil(t, pr) + }) +} + +func TestGetPullRequest(t *testing.T) { + t.Parallel() + + t.Run("successful retrieval of open PR", func(t *testing.T) { + t.Parallel() + prURL := "https://bitbucket.example.com/projects/PROJ/repos/myrepo/pull-requests/5" + createdMs := int64(1672574400000) // 2023-01-01T12:00:00Z + mc := &mockClient{ + getPRFunc: func( + _ context.Context, + projectKey, repoSlug string, + pullRequestId int, + _ ...RequestEditorFn, + ) (*GetPullRequestResponse, error) { + assert.Equal(t, "PROJ", projectKey) + assert.Equal(t, "myrepo", repoSlug) + assert.Equal(t, 5, pullRequestId) + return &GetPullRequestResponse{ + JSON200: &RestPullRequest{ + Id: intPtr(5), + State: statePtr(RestPullRequestStateOPEN), + Version: intPtr(2), + CreatedDate: &createdMs, + Links: &RestPullRequestLinks{Self: &[]RestHref{{Href: &prURL}}}, + FromRef: &RestRef{LatestCommit: strPtr("deadbeef")}, + }, + }, nil + }, + } + p := &provider{ + projectKey: "PROJ", + repoSlug: "myrepo", + client: mc, + } + pr, err := p.GetPullRequest(t.Context(), 5) + require.NoError(t, err) + require.NotNil(t, pr) + assert.Equal(t, int64(5), pr.Number) + assert.Equal(t, prURL, pr.URL) + assert.Equal(t, "deadbeef", pr.HeadSHA) + assert.True(t, pr.Open) + assert.False(t, pr.Merged) + require.NotNil(t, pr.CreatedAt) + assert.Equal(t, time.UnixMilli(createdMs).UTC(), *pr.CreatedAt) + }) + + t.Run("successful retrieval of merged PR", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetPullRequestResponse, error) { + return &GetPullRequestResponse{ + JSON200: makePR(3, RestPullRequestStateMERGED), + }, nil + }, + } + p := &provider{client: mc} + pr, err := p.GetPullRequest(t.Context(), 3) + require.NoError(t, err) + require.NotNil(t, pr) + assert.False(t, pr.Open) + assert.True(t, pr.Merged) + assert.Equal(t, "", pr.MergeCommitSHA) // Data Center does not include merge commit SHA + }) + + t.Run("error from API", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetPullRequestResponse, error) { + return nil, errors.New("not found") + }, + } + p := &provider{client: mc} + pr, err := p.GetPullRequest(t.Context(), 1) + assert.Error(t, err) + assert.Nil(t, pr) + }) + + t.Run("unexpected non-200 response", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetPullRequestResponse, error) { + return &GetPullRequestResponse{}, nil // JSON200 is nil + }, + } + p := &provider{client: mc} + pr, err := p.GetPullRequest(t.Context(), 1) + assert.Error(t, err) + assert.Nil(t, pr) + }) +} + +func TestListPullRequests(t *testing.T) { + t.Parallel() + + open := RestPullRequestStateOPEN + merged := RestPullRequestStateMERGED + declined := RestPullRequestStateDECLINED + + t.Run("open state uses OPEN param", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + getPRsFunc: func( + _ context.Context, + _, _ string, + params *GetPullRequestsParams, + _ ...RequestEditorFn, + ) (*GetPullRequestsResponse, error) { + require.NotNil(t, params.State) + assert.Equal(t, GetPullRequestsParamsStateOPEN, *params.State) + return &GetPullRequestsResponse{ + JSON200: &RestPullRequestPage{ + Values: &[]RestPullRequest{ + *makePR(1, RestPullRequestStateOPEN), + }, + }, + }, nil + }, + } + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ + State: gitprovider.PullRequestStateOpen, + }) + require.NoError(t, err) + assert.Len(t, prs, 1) + }) + + t.Run("nil options defaults to open state", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + getPRsFunc: func( + _ context.Context, + _, _ string, + params *GetPullRequestsParams, + _ ...RequestEditorFn, + ) (*GetPullRequestsResponse, error) { + require.NotNil(t, params.State) + assert.Equal(t, GetPullRequestsParamsStateOPEN, *params.State) + return &GetPullRequestsResponse{ + JSON200: &RestPullRequestPage{Values: &[]RestPullRequest{}}, + }, nil + }, + } + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), nil) + require.NoError(t, err) + assert.Empty(t, prs) + }) + + t.Run("closed state uses ALL and filters out OPEN PRs", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + getPRsFunc: func( + _ context.Context, + _, _ string, + params *GetPullRequestsParams, + _ ...RequestEditorFn, + ) (*GetPullRequestsResponse, error) { + require.NotNil(t, params.State) + assert.Equal(t, GetPullRequestsParamsStateALL, *params.State) + return &GetPullRequestsResponse{ + JSON200: &RestPullRequestPage{ + Values: &[]RestPullRequest{ + {Id: intPtr(1), State: &open}, + {Id: intPtr(2), State: &merged}, + {Id: intPtr(3), State: &declined}, + }, + }, + }, nil + }, + } + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ + State: gitprovider.PullRequestStateClosed, + }) + require.NoError(t, err) + // OPEN PR should be excluded; MERGED and DECLINED included + assert.Len(t, prs, 2) + }) + + t.Run("any state uses ALL and returns everything", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + getPRsFunc: func( + _ context.Context, + _, _ string, + params *GetPullRequestsParams, + _ ...RequestEditorFn, + ) (*GetPullRequestsResponse, error) { + require.NotNil(t, params.State) + assert.Equal(t, GetPullRequestsParamsStateALL, *params.State) + return &GetPullRequestsResponse{ + JSON200: &RestPullRequestPage{ + Values: &[]RestPullRequest{ + {Id: intPtr(1), State: &open}, + {Id: intPtr(2), State: &merged}, + }, + }, + }, nil + }, + } + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ + State: gitprovider.PullRequestStateAny, + }) + require.NoError(t, err) + assert.Len(t, prs, 2) + }) + + t.Run("filters by head branch", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + getPRsFunc: func( + _ context.Context, + _, _ string, + _ *GetPullRequestsParams, + _ ...RequestEditorFn, + ) (*GetPullRequestsResponse, error) { + return &GetPullRequestsResponse{ + JSON200: &RestPullRequestPage{ + Values: &[]RestPullRequest{ + {Id: intPtr(1), State: &open, FromRef: &RestRef{DisplayId: strPtr("feature")}}, + {Id: intPtr(2), State: &open, FromRef: &RestRef{DisplayId: strPtr("other")}}, + }, + }, + }, nil + }, + } + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ + State: gitprovider.PullRequestStateOpen, + HeadBranch: "feature", + }) + require.NoError(t, err) + require.Len(t, prs, 1) + assert.Equal(t, int64(1), prs[0].Number) + }) + + t.Run("filters by base branch", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + getPRsFunc: func( + _ context.Context, + _, _ string, + _ *GetPullRequestsParams, + _ ...RequestEditorFn, + ) (*GetPullRequestsResponse, error) { + return &GetPullRequestsResponse{ + JSON200: &RestPullRequestPage{ + Values: &[]RestPullRequest{ + {Id: intPtr(1), State: &open, ToRef: &RestRef{DisplayId: strPtr("main")}}, + {Id: intPtr(2), State: &open, ToRef: &RestRef{DisplayId: strPtr("dev")}}, + }, + }, + }, nil + }, + } + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ + State: gitprovider.PullRequestStateOpen, + BaseBranch: "main", + }) + require.NoError(t, err) + require.Len(t, prs, 1) + assert.Equal(t, int64(1), prs[0].Number) + }) + + t.Run("filters by head commit", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + getPRsFunc: func( + _ context.Context, + _, _ string, + _ *GetPullRequestsParams, + _ ...RequestEditorFn, + ) (*GetPullRequestsResponse, error) { + return &GetPullRequestsResponse{ + JSON200: &RestPullRequestPage{ + Values: &[]RestPullRequest{ + {Id: intPtr(1), State: &open, FromRef: &RestRef{LatestCommit: strPtr("abc123")}}, + {Id: intPtr(2), State: &open, FromRef: &RestRef{LatestCommit: strPtr("def456")}}, + }, + }, + }, nil + }, + } + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ + State: gitprovider.PullRequestStateOpen, + HeadCommit: "abc123", + }) + require.NoError(t, err) + require.Len(t, prs, 1) + assert.Equal(t, int64(1), prs[0].Number) + }) + + t.Run("nil values returns empty slice", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + getPRsFunc: func( + _ context.Context, + _, _ string, + _ *GetPullRequestsParams, + _ ...RequestEditorFn, + ) (*GetPullRequestsResponse, error) { + return &GetPullRequestsResponse{ + JSON200: &RestPullRequestPage{Values: nil}, + }, nil + }, + } + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), nil) + require.NoError(t, err) + assert.Empty(t, prs) + }) + + t.Run("error from API", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + getPRsFunc: func( + _ context.Context, + _, _ string, + _ *GetPullRequestsParams, + _ ...RequestEditorFn, + ) (*GetPullRequestsResponse, error) { + return nil, errors.New("server error") + }, + } + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), nil) + assert.Error(t, err) + assert.Nil(t, prs) + }) + + t.Run("unexpected non-200 response", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + getPRsFunc: func( + _ context.Context, + _, _ string, + _ *GetPullRequestsParams, + _ ...RequestEditorFn, + ) (*GetPullRequestsResponse, error) { + return &GetPullRequestsResponse{}, nil // JSON200 is nil + }, + } + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), nil) + assert.Error(t, err) + assert.Nil(t, prs) + }) + + t.Run("unknown state returns error", func(t *testing.T) { + t.Parallel() + p := &provider{client: &mockClient{}} + prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ + State: "bogus", + }) + assert.Error(t, err) + assert.Nil(t, prs) + }) +} + +func TestMergePullRequest(t *testing.T) { + t.Parallel() + + t.Run("already merged PR returns early", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetPullRequestResponse, error) { + return &GetPullRequestResponse{ + JSON200: makePR(1, RestPullRequestStateMERGED), + }, nil + }, + } + p := &provider{client: mc} + pr, merged, err := p.MergePullRequest(t.Context(), 1, nil) + require.NoError(t, err) + assert.True(t, merged) + require.NotNil(t, pr) + assert.True(t, pr.Merged) + }) + + t.Run("declined PR returns error", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetPullRequestResponse, error) { + return &GetPullRequestResponse{ + JSON200: makePR(1, RestPullRequestStateDECLINED), + }, nil + }, + } + p := &provider{client: mc} + pr, merged, err := p.MergePullRequest(t.Context(), 1, nil) + assert.Error(t, err) + assert.False(t, merged) + assert.Nil(t, pr) + }) + + t.Run("draft PR returns not-ready (nil, false, nil)", func(t *testing.T) { + t.Parallel() + pr := makePR(1, RestPullRequestStateOPEN) + pr.Draft = boolPtr(true) + mc := &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetPullRequestResponse, error) { + return &GetPullRequestResponse{JSON200: pr}, nil + }, + } + p := &provider{client: mc} + result, merged, err := p.MergePullRequest(t.Context(), 1, nil) + assert.NoError(t, err) + assert.False(t, merged) + assert.Nil(t, result) + }) + + t.Run("successful merge uses version from GET response", func(t *testing.T) { + t.Parallel() + getPR := makePR(7, RestPullRequestStateOPEN) + getPR.Version = intPtr(3) + mc := &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + pullRequestId int, + _ ...RequestEditorFn, + ) (*GetPullRequestResponse, error) { + assert.Equal(t, 7, pullRequestId) + return &GetPullRequestResponse{JSON200: getPR}, nil + }, + mergePRFunc: func( + _ context.Context, + _, _ string, + pullRequestId int, + params *MergePullRequestParams, + _ MergePullRequestJSONRequestBody, + _ ...RequestEditorFn, + ) (*MergePullRequestResponse, error) { + assert.Equal(t, 7, pullRequestId) + assert.Equal(t, 3, params.Version) + return &MergePullRequestResponse{ + JSON200: makePR(7, RestPullRequestStateMERGED), + }, nil + }, + } + p := &provider{client: mc} + pr, merged, err := p.MergePullRequest(t.Context(), 7, nil) + require.NoError(t, err) + assert.True(t, merged) + require.NotNil(t, pr) + assert.True(t, pr.Merged) + }) + + t.Run("merge with explicit strategy", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetPullRequestResponse, error) { + return &GetPullRequestResponse{JSON200: makePR(1, RestPullRequestStateOPEN)}, nil + }, + mergePRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ *MergePullRequestParams, + body MergePullRequestJSONRequestBody, + _ ...RequestEditorFn, + ) (*MergePullRequestResponse, error) { + require.NotNil(t, body.Strategy) + require.NotNil(t, body.Strategy.Id) + assert.Equal(t, Squash, *body.Strategy.Id) + return &MergePullRequestResponse{ + JSON200: makePR(1, RestPullRequestStateMERGED), + }, nil + }, + } + p := &provider{client: mc} + pr, merged, err := p.MergePullRequest(t.Context(), 1, &gitprovider.MergePullRequestOpts{ + MergeMethod: "squash", + }) + require.NoError(t, err) + assert.True(t, merged) + assert.NotNil(t, pr) + }) + + t.Run("invalid merge strategy returns error", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetPullRequestResponse, error) { + return &GetPullRequestResponse{JSON200: makePR(1, RestPullRequestStateOPEN)}, nil + }, + } + p := &provider{client: mc} + pr, merged, err := p.MergePullRequest(t.Context(), 1, &gitprovider.MergePullRequestOpts{ + MergeMethod: "bad-strategy", + }) + assert.Error(t, err) + assert.False(t, merged) + assert.Nil(t, pr) + }) + + t.Run("error getting PR before merge", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetPullRequestResponse, error) { + return nil, errors.New("network error") + }, + } + p := &provider{client: mc} + pr, merged, err := p.MergePullRequest(t.Context(), 1, nil) + assert.Error(t, err) + assert.False(t, merged) + assert.Nil(t, pr) + }) + + t.Run("non-200 GET response returns error", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetPullRequestResponse, error) { + return &GetPullRequestResponse{}, nil // JSON200 is nil + }, + } + p := &provider{client: mc} + pr, merged, err := p.MergePullRequest(t.Context(), 1, nil) + assert.Error(t, err) + assert.False(t, merged) + assert.Nil(t, pr) + }) + + t.Run("error during merge API call", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetPullRequestResponse, error) { + return &GetPullRequestResponse{JSON200: makePR(1, RestPullRequestStateOPEN)}, nil + }, + mergePRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ *MergePullRequestParams, + _ MergePullRequestJSONRequestBody, + _ ...RequestEditorFn, + ) (*MergePullRequestResponse, error) { + return nil, errors.New("merge conflict") + }, + } + p := &provider{client: mc} + pr, merged, err := p.MergePullRequest(t.Context(), 1, nil) + assert.Error(t, err) + assert.False(t, merged) + assert.Nil(t, pr) + }) + + t.Run("non-200 merge response returns error", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetPullRequestResponse, error) { + return &GetPullRequestResponse{JSON200: makePR(1, RestPullRequestStateOPEN)}, nil + }, + mergePRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ *MergePullRequestParams, + _ MergePullRequestJSONRequestBody, + _ ...RequestEditorFn, + ) (*MergePullRequestResponse, error) { + return &MergePullRequestResponse{}, nil // JSON200 is nil + }, + } + p := &provider{client: mc} + pr, merged, err := p.MergePullRequest(t.Context(), 1, nil) + assert.Error(t, err) + assert.False(t, merged) + assert.Nil(t, pr) + }) + + t.Run("merge response in non-MERGED state returns error", func(t *testing.T) { + t.Parallel() + mc := &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetPullRequestResponse, error) { + return &GetPullRequestResponse{JSON200: makePR(1, RestPullRequestStateOPEN)}, nil + }, + mergePRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ *MergePullRequestParams, + _ MergePullRequestJSONRequestBody, + _ ...RequestEditorFn, + ) (*MergePullRequestResponse, error) { + return &MergePullRequestResponse{ + JSON200: makePR(1, RestPullRequestStateOPEN), // Still open — unexpected + }, nil + }, + } + p := &provider{client: mc} + pr, merged, err := p.MergePullRequest(t.Context(), 1, nil) + assert.Error(t, err) + assert.False(t, merged) + assert.Nil(t, pr) + }) +} + +func Test_toProviderPR(t *testing.T) { + t.Parallel() + + t.Run("nil input returns nil", func(t *testing.T) { + t.Parallel() + assert.Nil(t, toProviderPR(nil)) + }) + + t.Run("open PR", func(t *testing.T) { + t.Parallel() + pr := toProviderPR(&RestPullRequest{ + Id: intPtr(10), + State: statePtr(RestPullRequestStateOPEN), + }) + require.NotNil(t, pr) + assert.Equal(t, int64(10), pr.Number) + assert.True(t, pr.Open) + assert.False(t, pr.Merged) + }) + + t.Run("merged PR", func(t *testing.T) { + t.Parallel() + pr := toProviderPR(&RestPullRequest{ + Id: intPtr(11), + State: statePtr(RestPullRequestStateMERGED), + }) + require.NotNil(t, pr) + assert.False(t, pr.Open) + assert.True(t, pr.Merged) + assert.Equal(t, "", pr.MergeCommitSHA) // never populated for Data Center + }) + + t.Run("URL from self links", func(t *testing.T) { + t.Parallel() + prURL := "https://bitbucket.example.com/pr/1" + pr := toProviderPR(&RestPullRequest{ + State: statePtr(RestPullRequestStateOPEN), + Links: &RestPullRequestLinks{ + Self: &[]RestHref{{Href: &prURL}}, + }, + }) + require.NotNil(t, pr) + assert.Equal(t, prURL, pr.URL) + }) + + t.Run("empty self links gives empty URL", func(t *testing.T) { + t.Parallel() + pr := toProviderPR(&RestPullRequest{ + State: statePtr(RestPullRequestStateOPEN), + Links: &RestPullRequestLinks{Self: &[]RestHref{}}, + }) + require.NotNil(t, pr) + assert.Equal(t, "", pr.URL) + }) + + t.Run("head SHA from FromRef.LatestCommit", func(t *testing.T) { + t.Parallel() + pr := toProviderPR(&RestPullRequest{ + State: statePtr(RestPullRequestStateOPEN), + FromRef: &RestRef{LatestCommit: strPtr("sha123")}, + }) + require.NotNil(t, pr) + assert.Equal(t, "sha123", pr.HeadSHA) + }) + + t.Run("createdAt from unix-ms timestamp", func(t *testing.T) { + t.Parallel() + ms := int64(1672574400000) + pr := toProviderPR(&RestPullRequest{ + State: statePtr(RestPullRequestStateOPEN), + CreatedDate: &ms, + }) + require.NotNil(t, pr) + require.NotNil(t, pr.CreatedAt) + assert.Equal(t, time.UnixMilli(ms).UTC(), *pr.CreatedAt) + }) + + t.Run("nil timestamps give nil createdAt", func(t *testing.T) { + t.Parallel() + pr := toProviderPR(&RestPullRequest{State: statePtr(RestPullRequestStateOPEN)}) + require.NotNil(t, pr) + assert.Nil(t, pr.CreatedAt) + }) + + t.Run("object field is set", func(t *testing.T) { + t.Parallel() + raw := &RestPullRequest{Id: intPtr(99), State: statePtr(RestPullRequestStateOPEN)} + pr := toProviderPR(raw) + require.NotNil(t, pr) + obj, ok := pr.Object.(*RestPullRequest) + require.True(t, ok) + assert.Same(t, raw, obj) + }) +} + +func TestParseRepoURL(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + repoURL string + wantBaseURL string + wantProjectKey string + wantRepoSlug string + wantErrContains string + }{ + { + // NormalizeGit lowercases the entire path, so "PROJ" → "proj" + name: "web UI project URL", + repoURL: "https://bitbucket.example.com/projects/PROJ/repos/myrepo", + wantBaseURL: "https://bitbucket.example.com", + wantProjectKey: "proj", + wantRepoSlug: "myrepo", + }, + { + name: "web UI user URL", + repoURL: "https://bitbucket.example.com/users/alice/repos/myrepo", + wantBaseURL: "https://bitbucket.example.com", + wantProjectKey: "~alice", + wantRepoSlug: "myrepo", + }, + { + name: "HTTP clone URL with /scm/ prefix", + repoURL: "https://bitbucket.example.com/scm/PROJ/myrepo.git", + wantBaseURL: "https://bitbucket.example.com", + wantProjectKey: "proj", // NormalizeGit lowercases + wantRepoSlug: "myrepo", + }, + { + name: "SSH clone URL (two path segments)", + repoURL: "ssh://git@bitbucket.example.com/PROJ/myrepo.git", + wantBaseURL: "https://bitbucket.example.com", + wantProjectKey: "proj", // NormalizeGit lowercases + wantRepoSlug: "myrepo", + }, + { + // NormalizeGit lowercases the entire path, so "PROJ" → "proj" + name: "host with explicit port is preserved", + repoURL: "https://bitbucket.example.com:7990/projects/PROJ/repos/myrepo", + wantBaseURL: "https://bitbucket.example.com:7990", + wantProjectKey: "proj", + wantRepoSlug: "myrepo", + }, + { + name: "invalid URL", + repoURL: "://invalid", + wantErrContains: "parse", + }, + { + // 3 segments where first is not "scm" → no matching case → error + name: "unrecognized path format", + repoURL: "https://bitbucket.example.com/foo/bar/baz", + wantErrContains: "invalid repository path", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + baseURL, projectKey, repoSlug, err := parseRepoURL(tc.repoURL) + if tc.wantErrContains != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErrContains) + return + } + require.NoError(t, err) + assert.Equal(t, tc.wantBaseURL, baseURL) + assert.Equal(t, tc.wantProjectKey, projectKey) + assert.Equal(t, tc.wantRepoSlug, repoSlug) + }) + } +} + +func TestGetCommitURL(t *testing.T) { + t.Parallel() + + t.Run("regular project commit URL", func(t *testing.T) { + t.Parallel() + p := &provider{ + baseURL: "https://bitbucket.example.com", + projectKey: "PROJ", + repoSlug: "myrepo", + } + u, err := p.GetCommitURL("", "abc123") + require.NoError(t, err) + assert.Equal( + t, + "https://bitbucket.example.com/projects/PROJ/repos/myrepo/commits/abc123", + u, + ) + }) + + t.Run("personal repo commit URL", func(t *testing.T) { + t.Parallel() + p := &provider{ + baseURL: "https://bitbucket.example.com", + projectKey: "~alice", + repoSlug: "myrepo", + } + u, err := p.GetCommitURL("", "deadbeef") + require.NoError(t, err) + assert.Equal( + t, + "https://bitbucket.example.com/users/alice/repos/myrepo/commits/deadbeef", + u, + ) + }) + + t.Run("repo URL argument is ignored", func(t *testing.T) { + t.Parallel() + p := &provider{ + baseURL: "https://bitbucket.example.com", + projectKey: "PROJ", + repoSlug: "myrepo", + } + u1, _ := p.GetCommitURL("https://any-url.example.com/foo/bar", "sha") + u2, _ := p.GetCommitURL("", "sha") + assert.Equal(t, u1, u2) + }) +} diff --git a/pkg/gitprovider/bitbucket/datacenter/spec/bitbucket-datacenter.gen.json b/pkg/gitprovider/bitbucket/datacenter/spec/bitbucket-datacenter.gen.json new file mode 100644 index 0000000000..6829fb92c9 --- /dev/null +++ b/pkg/gitprovider/bitbucket/datacenter/spec/bitbucket-datacenter.gen.json @@ -0,0 +1,357 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Bitbucket Data Center API", + "description": "Minimal Bitbucket Data Center REST API spec covering pull request and commit operations used by the Kargo git provider integration.", + "version": "1.0" + }, + "servers": [ + { + "url": "{baseUrl}", + "variables": { + "baseUrl": { + "default": "https://bitbucket.example.com" + } + } + } + ], + "paths": { + "/rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/pull-requests": { + "get": { + "operationId": "GetPullRequests", + "summary": "List pull requests", + "parameters": [ + { + "name": "projectKey", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "repositorySlug", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "state", + "in": "query", + "description": "Filter by state. ALL returns pull requests in any state.", + "schema": { + "type": "string", + "enum": ["ALL", "OPEN", "MERGED", "DECLINED"] + } + }, + { + "name": "start", + "in": "query", + "schema": { "type": "integer" } + }, + { + "name": "limit", + "in": "query", + "schema": { "type": "integer" } + } + ], + "responses": { + "200": { + "description": "A paginated list of pull requests.", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/RestPullRequestPage" } + } + } + } + } + }, + "post": { + "operationId": "CreatePullRequest", + "summary": "Create a pull request", + "parameters": [ + { + "name": "projectKey", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "repositorySlug", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/RestPullRequestCreate" } + } + } + }, + "responses": { + "201": { + "description": "The newly created pull request.", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/RestPullRequest" } + } + } + } + } + } + }, + "/rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}": { + "get": { + "operationId": "GetPullRequest", + "summary": "Get a pull request", + "parameters": [ + { + "name": "projectKey", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "repositorySlug", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "pullRequestId", + "in": "path", + "required": true, + "schema": { "type": "integer" } + } + ], + "responses": { + "200": { + "description": "The pull request.", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/RestPullRequest" } + } + } + } + } + } + }, + "/rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/merge": { + "post": { + "operationId": "MergePullRequest", + "summary": "Merge a pull request", + "parameters": [ + { + "name": "projectKey", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "repositorySlug", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "pullRequestId", + "in": "path", + "required": true, + "schema": { "type": "integer" } + }, + { + "name": "version", + "in": "query", + "description": "The current version of the pull request. Required to prevent merging a stale PR.", + "required": true, + "schema": { "type": "integer" } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/RestMergeConfig" } + } + } + }, + "responses": { + "200": { + "description": "The merged pull request.", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/RestPullRequest" } + } + } + }, + "409": { + "description": "The pull request has conflicts and cannot be merged." + } + } + } + }, + "/rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/commits/{commitId}": { + "get": { + "operationId": "GetCommit", + "summary": "Get a commit", + "parameters": [ + { + "name": "projectKey", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "repositorySlug", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "commitId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "The commit.", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/RestCommit" } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "RestPullRequest": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "version": { "type": "integer" }, + "title": { "type": "string" }, + "description": { "type": "string" }, + "state": { + "type": "string", + "enum": ["OPEN", "MERGED", "DECLINED"] + }, + "open": { "type": "boolean" }, + "closed": { "type": "boolean" }, + "draft": { "type": "boolean" }, + "createdDate": { "type": "integer", "format": "int64" }, + "updatedDate": { "type": "integer", "format": "int64" }, + "fromRef": { "$ref": "#/components/schemas/RestRef" }, + "toRef": { "$ref": "#/components/schemas/RestRef" }, + "locked": { "type": "boolean" }, + "links": { "$ref": "#/components/schemas/RestPullRequestLinks" } + } + }, + "RestRef": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "displayId": { "type": "string" }, + "latestCommit": { "type": "string" }, + "repository": { "$ref": "#/components/schemas/RestRefRepository" } + } + }, + "RestRefRepository": { + "type": "object", + "properties": { + "slug": { "type": "string" }, + "project": { "$ref": "#/components/schemas/RestProject" } + } + }, + "RestProject": { + "type": "object", + "properties": { + "key": { "type": "string" } + } + }, + "RestPullRequestLinks": { + "type": "object", + "properties": { + "self": { + "type": "array", + "items": { "$ref": "#/components/schemas/RestHref" } + } + } + }, + "RestHref": { + "type": "object", + "properties": { + "href": { "type": "string" } + } + }, + "RestPullRequestPage": { + "type": "object", + "properties": { + "size": { "type": "integer" }, + "limit": { "type": "integer" }, + "start": { "type": "integer" }, + "isLastPage": { "type": "boolean" }, + "nextPageStart": { "type": "integer" }, + "values": { + "type": "array", + "items": { "$ref": "#/components/schemas/RestPullRequest" } + } + } + }, + "RestPullRequestCreate": { + "type": "object", + "required": ["title", "fromRef", "toRef"], + "properties": { + "title": { "type": "string" }, + "description": { "type": "string" }, + "fromRef": { "$ref": "#/components/schemas/RestCreateRef" }, + "toRef": { "$ref": "#/components/schemas/RestCreateRef" } + } + }, + "RestCreateRef": { + "type": "object", + "required": ["id", "repository"], + "properties": { + "id": { "type": "string" }, + "repository": { "$ref": "#/components/schemas/RestRefRepository" } + } + }, + "RestMergeConfig": { + "type": "object", + "properties": { + "message": { "type": "string" }, + "strategy": { "$ref": "#/components/schemas/RestMergeStrategy" } + } + }, + "RestMergeStrategy": { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": [ + "no-ff", + "ff", + "ff-only", + "squash", + "squash-ff-only", + "rebase-no-ff", + "rebase-ff-only" + ] + } + } + }, + "RestCommit": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "displayId": { "type": "string" }, + "message": { "type": "string" } + } + } + } + } +} diff --git a/pkg/gitprovider/bitbucket/datacenter/spec/oapi-codegen.yaml b/pkg/gitprovider/bitbucket/datacenter/spec/oapi-codegen.yaml new file mode 100644 index 0000000000..ab67b475ea --- /dev/null +++ b/pkg/gitprovider/bitbucket/datacenter/spec/oapi-codegen.yaml @@ -0,0 +1,10 @@ +package: datacenter + +generate: + models: true + client: true + +output: zz_bitbucket_datacenter_client.gen.go + +output-options: + skip-prune: false diff --git a/pkg/gitprovider/bitbucket/datacenter/zz_bitbucket_datacenter_client.gen.go b/pkg/gitprovider/bitbucket/datacenter/zz_bitbucket_datacenter_client.gen.go new file mode 100644 index 0000000000..dd9c6136b3 --- /dev/null +++ b/pkg/gitprovider/bitbucket/datacenter/zz_bitbucket_datacenter_client.gen.go @@ -0,0 +1,1078 @@ +// Package datacenter provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.6.0 DO NOT EDIT. +package datacenter + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/oapi-codegen/runtime" +) + +// Defines values for RestMergeStrategyId. +const ( + Ff RestMergeStrategyId = "ff" + FfOnly RestMergeStrategyId = "ff-only" + NoFf RestMergeStrategyId = "no-ff" + RebaseFfOnly RestMergeStrategyId = "rebase-ff-only" + RebaseNoFf RestMergeStrategyId = "rebase-no-ff" + Squash RestMergeStrategyId = "squash" + SquashFfOnly RestMergeStrategyId = "squash-ff-only" +) + +// Valid indicates whether the value is a known member of the RestMergeStrategyId enum. +func (e RestMergeStrategyId) Valid() bool { + switch e { + case Ff: + return true + case FfOnly: + return true + case NoFf: + return true + case RebaseFfOnly: + return true + case RebaseNoFf: + return true + case Squash: + return true + case SquashFfOnly: + return true + default: + return false + } +} + +// Defines values for RestPullRequestState. +const ( + RestPullRequestStateDECLINED RestPullRequestState = "DECLINED" + RestPullRequestStateMERGED RestPullRequestState = "MERGED" + RestPullRequestStateOPEN RestPullRequestState = "OPEN" +) + +// Valid indicates whether the value is a known member of the RestPullRequestState enum. +func (e RestPullRequestState) Valid() bool { + switch e { + case RestPullRequestStateDECLINED: + return true + case RestPullRequestStateMERGED: + return true + case RestPullRequestStateOPEN: + return true + default: + return false + } +} + +// Defines values for GetPullRequestsParamsState. +const ( + GetPullRequestsParamsStateALL GetPullRequestsParamsState = "ALL" + GetPullRequestsParamsStateDECLINED GetPullRequestsParamsState = "DECLINED" + GetPullRequestsParamsStateMERGED GetPullRequestsParamsState = "MERGED" + GetPullRequestsParamsStateOPEN GetPullRequestsParamsState = "OPEN" +) + +// Valid indicates whether the value is a known member of the GetPullRequestsParamsState enum. +func (e GetPullRequestsParamsState) Valid() bool { + switch e { + case GetPullRequestsParamsStateALL: + return true + case GetPullRequestsParamsStateDECLINED: + return true + case GetPullRequestsParamsStateMERGED: + return true + case GetPullRequestsParamsStateOPEN: + return true + default: + return false + } +} + +// RestCommit defines model for RestCommit. +type RestCommit struct { + DisplayId *string `json:"displayId,omitempty"` + Id *string `json:"id,omitempty"` + Message *string `json:"message,omitempty"` +} + +// RestCreateRef defines model for RestCreateRef. +type RestCreateRef struct { + Id string `json:"id"` + Repository RestRefRepository `json:"repository"` +} + +// RestHref defines model for RestHref. +type RestHref struct { + Href *string `json:"href,omitempty"` +} + +// RestMergeConfig defines model for RestMergeConfig. +type RestMergeConfig struct { + Message *string `json:"message,omitempty"` + Strategy *RestMergeStrategy `json:"strategy,omitempty"` +} + +// RestMergeStrategy defines model for RestMergeStrategy. +type RestMergeStrategy struct { + Id *RestMergeStrategyId `json:"id,omitempty"` +} + +// RestMergeStrategyId defines model for RestMergeStrategy.Id. +type RestMergeStrategyId string + +// RestProject defines model for RestProject. +type RestProject struct { + Key *string `json:"key,omitempty"` +} + +// RestPullRequest defines model for RestPullRequest. +type RestPullRequest struct { + Closed *bool `json:"closed,omitempty"` + CreatedDate *int64 `json:"createdDate,omitempty"` + Description *string `json:"description,omitempty"` + Draft *bool `json:"draft,omitempty"` + FromRef *RestRef `json:"fromRef,omitempty"` + Id *int `json:"id,omitempty"` + Links *RestPullRequestLinks `json:"links,omitempty"` + Locked *bool `json:"locked,omitempty"` + Open *bool `json:"open,omitempty"` + State *RestPullRequestState `json:"state,omitempty"` + Title *string `json:"title,omitempty"` + ToRef *RestRef `json:"toRef,omitempty"` + UpdatedDate *int64 `json:"updatedDate,omitempty"` + Version *int `json:"version,omitempty"` +} + +// RestPullRequestState defines model for RestPullRequest.State. +type RestPullRequestState string + +// RestPullRequestCreate defines model for RestPullRequestCreate. +type RestPullRequestCreate struct { + Description *string `json:"description,omitempty"` + FromRef RestCreateRef `json:"fromRef"` + Title string `json:"title"` + ToRef RestCreateRef `json:"toRef"` +} + +// RestPullRequestLinks defines model for RestPullRequestLinks. +type RestPullRequestLinks struct { + Self *[]RestHref `json:"self,omitempty"` +} + +// RestPullRequestPage defines model for RestPullRequestPage. +type RestPullRequestPage struct { + IsLastPage *bool `json:"isLastPage,omitempty"` + Limit *int `json:"limit,omitempty"` + NextPageStart *int `json:"nextPageStart,omitempty"` + Size *int `json:"size,omitempty"` + Start *int `json:"start,omitempty"` + Values *[]RestPullRequest `json:"values,omitempty"` +} + +// RestRef defines model for RestRef. +type RestRef struct { + DisplayId *string `json:"displayId,omitempty"` + Id *string `json:"id,omitempty"` + LatestCommit *string `json:"latestCommit,omitempty"` + Repository *RestRefRepository `json:"repository,omitempty"` +} + +// RestRefRepository defines model for RestRefRepository. +type RestRefRepository struct { + Project *RestProject `json:"project,omitempty"` + Slug *string `json:"slug,omitempty"` +} + +// GetPullRequestsParams defines parameters for GetPullRequests. +type GetPullRequestsParams struct { + // State Filter by state. ALL returns pull requests in any state. + State *GetPullRequestsParamsState `form:"state,omitempty" json:"state,omitempty"` + Start *int `form:"start,omitempty" json:"start,omitempty"` + Limit *int `form:"limit,omitempty" json:"limit,omitempty"` +} + +// GetPullRequestsParamsState defines parameters for GetPullRequests. +type GetPullRequestsParamsState string + +// MergePullRequestParams defines parameters for MergePullRequest. +type MergePullRequestParams struct { + // Version The current version of the pull request. Required to prevent merging a stale PR. + Version int `form:"version" json:"version"` +} + +// CreatePullRequestJSONRequestBody defines body for CreatePullRequest for application/json ContentType. +type CreatePullRequestJSONRequestBody = RestPullRequestCreate + +// MergePullRequestJSONRequestBody defines body for MergePullRequest for application/json ContentType. +type MergePullRequestJSONRequestBody = RestMergeConfig + +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// Doer performs HTTP requests. +// +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // Doer for performing requests, typically a *http.Client with any + // customized settings, such as certificate chains. + Client HttpRequestDoer + + // A list of callbacks for modifying requests which are generated before sending over + // the network. + RequestEditors []RequestEditorFn +} + +// ClientOption allows setting custom parameters during construction +type ClientOption func(*Client) error + +// Creates a new Client, with reasonable defaults +func NewClient(server string, opts ...ClientOption) (*Client, error) { + // create a client with sane default values + client := Client{ + Server: server, + } + // mutate client and add all optional params + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // create httpClient, if not already present + if client.Client == nil { + client.Client = &http.Client{} + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditors = append(c.RequestEditors, fn) + return nil + } +} + +// The interface specification for the client above. +type ClientInterface interface { + // GetCommit request + GetCommit(ctx context.Context, projectKey string, repositorySlug string, commitId string, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetPullRequests request + GetPullRequests(ctx context.Context, projectKey string, repositorySlug string, params *GetPullRequestsParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // CreatePullRequestWithBody request with any body + CreatePullRequestWithBody(ctx context.Context, projectKey string, repositorySlug string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + CreatePullRequest(ctx context.Context, projectKey string, repositorySlug string, body CreatePullRequestJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetPullRequest request + GetPullRequest(ctx context.Context, projectKey string, repositorySlug string, pullRequestId int, reqEditors ...RequestEditorFn) (*http.Response, error) + + // MergePullRequestWithBody request with any body + MergePullRequestWithBody(ctx context.Context, projectKey string, repositorySlug string, pullRequestId int, params *MergePullRequestParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + MergePullRequest(ctx context.Context, projectKey string, repositorySlug string, pullRequestId int, params *MergePullRequestParams, body MergePullRequestJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (c *Client) GetCommit(ctx context.Context, projectKey string, repositorySlug string, commitId string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetCommitRequest(c.Server, projectKey, repositorySlug, commitId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetPullRequests(ctx context.Context, projectKey string, repositorySlug string, params *GetPullRequestsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetPullRequestsRequest(c.Server, projectKey, repositorySlug, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) CreatePullRequestWithBody(ctx context.Context, projectKey string, repositorySlug string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreatePullRequestRequestWithBody(c.Server, projectKey, repositorySlug, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) CreatePullRequest(ctx context.Context, projectKey string, repositorySlug string, body CreatePullRequestJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreatePullRequestRequest(c.Server, projectKey, repositorySlug, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetPullRequest(ctx context.Context, projectKey string, repositorySlug string, pullRequestId int, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetPullRequestRequest(c.Server, projectKey, repositorySlug, pullRequestId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) MergePullRequestWithBody(ctx context.Context, projectKey string, repositorySlug string, pullRequestId int, params *MergePullRequestParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewMergePullRequestRequestWithBody(c.Server, projectKey, repositorySlug, pullRequestId, params, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) MergePullRequest(ctx context.Context, projectKey string, repositorySlug string, pullRequestId int, params *MergePullRequestParams, body MergePullRequestJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewMergePullRequestRequest(c.Server, projectKey, repositorySlug, pullRequestId, params, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +// NewGetCommitRequest generates requests for GetCommit +func NewGetCommitRequest(server string, projectKey string, repositorySlug string, commitId string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "projectKey", projectKey, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "repositorySlug", repositorySlug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + var pathParam2 string + + pathParam2, err = runtime.StyleParamWithOptions("simple", false, "commitId", commitId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/rest/api/1.0/projects/%s/repos/%s/commits/%s", pathParam0, pathParam1, pathParam2) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetPullRequestsRequest generates requests for GetPullRequests +func NewGetPullRequestsRequest(server string, projectKey string, repositorySlug string, params *GetPullRequestsParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "projectKey", projectKey, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "repositorySlug", repositorySlug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/rest/api/1.0/projects/%s/repos/%s/pull-requests", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.State != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "state", *params.State, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Start != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "start", *params.Start, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: ""}); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Limit != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "limit", *params.Limit, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: ""}); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewCreatePullRequestRequest calls the generic CreatePullRequest builder with application/json body +func NewCreatePullRequestRequest(server string, projectKey string, repositorySlug string, body CreatePullRequestJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewCreatePullRequestRequestWithBody(server, projectKey, repositorySlug, "application/json", bodyReader) +} + +// NewCreatePullRequestRequestWithBody generates requests for CreatePullRequest with any type of body +func NewCreatePullRequestRequestWithBody(server string, projectKey string, repositorySlug string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "projectKey", projectKey, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "repositorySlug", repositorySlug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/rest/api/1.0/projects/%s/repos/%s/pull-requests", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewGetPullRequestRequest generates requests for GetPullRequest +func NewGetPullRequestRequest(server string, projectKey string, repositorySlug string, pullRequestId int) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "projectKey", projectKey, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "repositorySlug", repositorySlug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + var pathParam2 string + + pathParam2, err = runtime.StyleParamWithOptions("simple", false, "pullRequestId", pullRequestId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "integer", Format: ""}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/rest/api/1.0/projects/%s/repos/%s/pull-requests/%s", pathParam0, pathParam1, pathParam2) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewMergePullRequestRequest calls the generic MergePullRequest builder with application/json body +func NewMergePullRequestRequest(server string, projectKey string, repositorySlug string, pullRequestId int, params *MergePullRequestParams, body MergePullRequestJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewMergePullRequestRequestWithBody(server, projectKey, repositorySlug, pullRequestId, params, "application/json", bodyReader) +} + +// NewMergePullRequestRequestWithBody generates requests for MergePullRequest with any type of body +func NewMergePullRequestRequestWithBody(server string, projectKey string, repositorySlug string, pullRequestId int, params *MergePullRequestParams, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "projectKey", projectKey, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "repositorySlug", repositorySlug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + var pathParam2 string + + pathParam2, err = runtime.StyleParamWithOptions("simple", false, "pullRequestId", pullRequestId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "integer", Format: ""}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/rest/api/1.0/projects/%s/repos/%s/pull-requests/%s/merge", pathParam0, pathParam1, pathParam2) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "version", params.Version, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: ""}); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.RequestEditors { + if err := r(ctx, req); err != nil { + return err + } + } + for _, r := range additionalEditors { + if err := r(ctx, req); err != nil { + return err + } + } + return nil +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + // GetCommitWithResponse request + GetCommitWithResponse(ctx context.Context, projectKey string, repositorySlug string, commitId string, reqEditors ...RequestEditorFn) (*GetCommitResponse, error) + + // GetPullRequestsWithResponse request + GetPullRequestsWithResponse(ctx context.Context, projectKey string, repositorySlug string, params *GetPullRequestsParams, reqEditors ...RequestEditorFn) (*GetPullRequestsResponse, error) + + // CreatePullRequestWithBodyWithResponse request with any body + CreatePullRequestWithBodyWithResponse(ctx context.Context, projectKey string, repositorySlug string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreatePullRequestResponse, error) + + CreatePullRequestWithResponse(ctx context.Context, projectKey string, repositorySlug string, body CreatePullRequestJSONRequestBody, reqEditors ...RequestEditorFn) (*CreatePullRequestResponse, error) + + // GetPullRequestWithResponse request + GetPullRequestWithResponse(ctx context.Context, projectKey string, repositorySlug string, pullRequestId int, reqEditors ...RequestEditorFn) (*GetPullRequestResponse, error) + + // MergePullRequestWithBodyWithResponse request with any body + MergePullRequestWithBodyWithResponse(ctx context.Context, projectKey string, repositorySlug string, pullRequestId int, params *MergePullRequestParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*MergePullRequestResponse, error) + + MergePullRequestWithResponse(ctx context.Context, projectKey string, repositorySlug string, pullRequestId int, params *MergePullRequestParams, body MergePullRequestJSONRequestBody, reqEditors ...RequestEditorFn) (*MergePullRequestResponse, error) +} + +type GetCommitResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *RestCommit +} + +// Status returns HTTPResponse.Status +func (r GetCommitResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetCommitResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetPullRequestsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *RestPullRequestPage +} + +// Status returns HTTPResponse.Status +func (r GetPullRequestsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetPullRequestsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type CreatePullRequestResponse struct { + Body []byte + HTTPResponse *http.Response + JSON201 *RestPullRequest +} + +// Status returns HTTPResponse.Status +func (r CreatePullRequestResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r CreatePullRequestResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetPullRequestResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *RestPullRequest +} + +// Status returns HTTPResponse.Status +func (r GetPullRequestResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetPullRequestResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type MergePullRequestResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *RestPullRequest +} + +// Status returns HTTPResponse.Status +func (r MergePullRequestResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r MergePullRequestResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// GetCommitWithResponse request returning *GetCommitResponse +func (c *ClientWithResponses) GetCommitWithResponse(ctx context.Context, projectKey string, repositorySlug string, commitId string, reqEditors ...RequestEditorFn) (*GetCommitResponse, error) { + rsp, err := c.GetCommit(ctx, projectKey, repositorySlug, commitId, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetCommitResponse(rsp) +} + +// GetPullRequestsWithResponse request returning *GetPullRequestsResponse +func (c *ClientWithResponses) GetPullRequestsWithResponse(ctx context.Context, projectKey string, repositorySlug string, params *GetPullRequestsParams, reqEditors ...RequestEditorFn) (*GetPullRequestsResponse, error) { + rsp, err := c.GetPullRequests(ctx, projectKey, repositorySlug, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetPullRequestsResponse(rsp) +} + +// CreatePullRequestWithBodyWithResponse request with arbitrary body returning *CreatePullRequestResponse +func (c *ClientWithResponses) CreatePullRequestWithBodyWithResponse(ctx context.Context, projectKey string, repositorySlug string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreatePullRequestResponse, error) { + rsp, err := c.CreatePullRequestWithBody(ctx, projectKey, repositorySlug, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreatePullRequestResponse(rsp) +} + +func (c *ClientWithResponses) CreatePullRequestWithResponse(ctx context.Context, projectKey string, repositorySlug string, body CreatePullRequestJSONRequestBody, reqEditors ...RequestEditorFn) (*CreatePullRequestResponse, error) { + rsp, err := c.CreatePullRequest(ctx, projectKey, repositorySlug, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreatePullRequestResponse(rsp) +} + +// GetPullRequestWithResponse request returning *GetPullRequestResponse +func (c *ClientWithResponses) GetPullRequestWithResponse(ctx context.Context, projectKey string, repositorySlug string, pullRequestId int, reqEditors ...RequestEditorFn) (*GetPullRequestResponse, error) { + rsp, err := c.GetPullRequest(ctx, projectKey, repositorySlug, pullRequestId, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetPullRequestResponse(rsp) +} + +// MergePullRequestWithBodyWithResponse request with arbitrary body returning *MergePullRequestResponse +func (c *ClientWithResponses) MergePullRequestWithBodyWithResponse(ctx context.Context, projectKey string, repositorySlug string, pullRequestId int, params *MergePullRequestParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*MergePullRequestResponse, error) { + rsp, err := c.MergePullRequestWithBody(ctx, projectKey, repositorySlug, pullRequestId, params, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseMergePullRequestResponse(rsp) +} + +func (c *ClientWithResponses) MergePullRequestWithResponse(ctx context.Context, projectKey string, repositorySlug string, pullRequestId int, params *MergePullRequestParams, body MergePullRequestJSONRequestBody, reqEditors ...RequestEditorFn) (*MergePullRequestResponse, error) { + rsp, err := c.MergePullRequest(ctx, projectKey, repositorySlug, pullRequestId, params, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseMergePullRequestResponse(rsp) +} + +// ParseGetCommitResponse parses an HTTP response from a GetCommitWithResponse call +func ParseGetCommitResponse(rsp *http.Response) (*GetCommitResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetCommitResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest RestCommit + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseGetPullRequestsResponse parses an HTTP response from a GetPullRequestsWithResponse call +func ParseGetPullRequestsResponse(rsp *http.Response) (*GetPullRequestsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetPullRequestsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest RestPullRequestPage + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseCreatePullRequestResponse parses an HTTP response from a CreatePullRequestWithResponse call +func ParseCreatePullRequestResponse(rsp *http.Response) (*CreatePullRequestResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &CreatePullRequestResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest RestPullRequest + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON201 = &dest + + } + + return response, nil +} + +// ParseGetPullRequestResponse parses an HTTP response from a GetPullRequestWithResponse call +func ParseGetPullRequestResponse(rsp *http.Response) (*GetPullRequestResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetPullRequestResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest RestPullRequest + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseMergePullRequestResponse parses an HTTP response from a MergePullRequestWithResponse call +func ParseMergePullRequestResponse(rsp *http.Response) (*MergePullRequestResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &MergePullRequestResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest RestPullRequest + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} diff --git a/pkg/promotion/runner/builtin/git_pr_merger.go b/pkg/promotion/runner/builtin/git_pr_merger.go index 92f8a7823c..47be689e85 100644 --- a/pkg/promotion/runner/builtin/git_pr_merger.go +++ b/pkg/promotion/runner/builtin/git_pr_merger.go @@ -14,11 +14,12 @@ import ( "github.com/akuity/kargo/pkg/promotion" "github.com/akuity/kargo/pkg/x/promotion/runner/builtin" - _ "github.com/akuity/kargo/pkg/gitprovider/azure" // Azure provider registration - _ "github.com/akuity/kargo/pkg/gitprovider/bitbucket/cloud" // Bitbucket Cloud provider registration - _ "github.com/akuity/kargo/pkg/gitprovider/gitea" // Gitea provider registration - _ "github.com/akuity/kargo/pkg/gitprovider/github" // GitHub provider registration - _ "github.com/akuity/kargo/pkg/gitprovider/gitlab" // GitLab provider registration + _ "github.com/akuity/kargo/pkg/gitprovider/azure" // Azure provider registration + _ "github.com/akuity/kargo/pkg/gitprovider/bitbucket/cloud" // Bitbucket Cloud provider registration + _ "github.com/akuity/kargo/pkg/gitprovider/bitbucket/datacenter" // Bitbucket Data Center provider registration + _ "github.com/akuity/kargo/pkg/gitprovider/gitea" // Gitea provider registration + _ "github.com/akuity/kargo/pkg/gitprovider/github" // GitHub provider registration + _ "github.com/akuity/kargo/pkg/gitprovider/gitlab" // GitLab provider registration ) const stepKindGitMergePR = "git-merge-pr" diff --git a/pkg/promotion/runner/builtin/git_pr_opener.go b/pkg/promotion/runner/builtin/git_pr_opener.go index 8f377ce9dd..53d6450905 100644 --- a/pkg/promotion/runner/builtin/git_pr_opener.go +++ b/pkg/promotion/runner/builtin/git_pr_opener.go @@ -16,11 +16,12 @@ import ( "github.com/akuity/kargo/pkg/promotion" "github.com/akuity/kargo/pkg/x/promotion/runner/builtin" - _ "github.com/akuity/kargo/pkg/gitprovider/azure" // Azure provider registration - _ "github.com/akuity/kargo/pkg/gitprovider/bitbucket/cloud" // Bitbucket Cloud provider registration - _ "github.com/akuity/kargo/pkg/gitprovider/gitea" // Gitea provider registration - _ "github.com/akuity/kargo/pkg/gitprovider/github" // GitHub provider registration - _ "github.com/akuity/kargo/pkg/gitprovider/gitlab" // GitLab provider registration + _ "github.com/akuity/kargo/pkg/gitprovider/azure" // Azure provider registration + _ "github.com/akuity/kargo/pkg/gitprovider/bitbucket/cloud" // Bitbucket Cloud provider registration + _ "github.com/akuity/kargo/pkg/gitprovider/bitbucket/datacenter" // Bitbucket Data Center provider registration + _ "github.com/akuity/kargo/pkg/gitprovider/gitea" // Gitea provider registration + _ "github.com/akuity/kargo/pkg/gitprovider/github" // GitHub provider registration + _ "github.com/akuity/kargo/pkg/gitprovider/gitlab" // GitLab provider registration ) const stepKindGitOpenPR = "git-open-pr" diff --git a/pkg/promotion/runner/builtin/git_pr_waiter.go b/pkg/promotion/runner/builtin/git_pr_waiter.go index ec6dbd22a8..f0add8c020 100644 --- a/pkg/promotion/runner/builtin/git_pr_waiter.go +++ b/pkg/promotion/runner/builtin/git_pr_waiter.go @@ -13,11 +13,12 @@ import ( "github.com/akuity/kargo/pkg/promotion" "github.com/akuity/kargo/pkg/x/promotion/runner/builtin" - _ "github.com/akuity/kargo/pkg/gitprovider/azure" // Azure provider registration - _ "github.com/akuity/kargo/pkg/gitprovider/bitbucket/cloud" // Bitbucket Cloud provider registration - _ "github.com/akuity/kargo/pkg/gitprovider/gitea" // Gitea provider registration - _ "github.com/akuity/kargo/pkg/gitprovider/github" // GitHub provider registration - _ "github.com/akuity/kargo/pkg/gitprovider/gitlab" // GitLab provider registration + _ "github.com/akuity/kargo/pkg/gitprovider/azure" // Azure provider registration + _ "github.com/akuity/kargo/pkg/gitprovider/bitbucket/cloud" // Bitbucket Cloud provider registration + _ "github.com/akuity/kargo/pkg/gitprovider/bitbucket/datacenter" // Bitbucket Data Center provider registration + _ "github.com/akuity/kargo/pkg/gitprovider/gitea" // Gitea provider registration + _ "github.com/akuity/kargo/pkg/gitprovider/github" // GitHub provider registration + _ "github.com/akuity/kargo/pkg/gitprovider/gitlab" // GitLab provider registration ) const stepKindGitWaitForPR = "git-wait-for-pr"