From 72fd991a9c923a31abb40626e2663a33f86a1087 Mon Sep 17 00:00:00 2001 From: fuskovic Date: Mon, 20 Apr 2026 17:36:03 +0530 Subject: [PATCH 01/18] add bb open API spec Signed-off-by: fuskovic --- .../bitbucket/spec/bitbucket.gen.json | 1840 +++++++++++++++++ .../bitbucket/spec/oapi-codegen.yaml | 10 + 2 files changed, 1850 insertions(+) create mode 100644 pkg/gitprovider/bitbucket/spec/bitbucket.gen.json create mode 100644 pkg/gitprovider/bitbucket/spec/oapi-codegen.yaml diff --git a/pkg/gitprovider/bitbucket/spec/bitbucket.gen.json b/pkg/gitprovider/bitbucket/spec/bitbucket.gen.json new file mode 100644 index 0000000000..5bfbb31a88 --- /dev/null +++ b/pkg/gitprovider/bitbucket/spec/bitbucket.gen.json @@ -0,0 +1,1840 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Bitbucket API", + "description": "Code against the Bitbucket API to automate simple tasks, embed Bitbucket data into your own site, build mobile or desktop apps, or even add custom UI add-ons into Bitbucket itself using the Connect framework.", + "version": "2.0", + "termsOfService": "https://www.atlassian.com/legal/customer-agreement", + "contact": { + "name": "Bitbucket Support", + "url": "https://support.atlassian.com/bitbucket-cloud/", + "email": "support@bitbucket.org" + } + }, + "servers": [ + { + "url": "https://api.bitbucket.org/2.0" + } + ], + "paths": { + "/repositories/{workspace}/{repo_slug}/pullrequests": { + "get": { + "tags": [ + "Pullrequests" + ], + "description": "Returns all pull requests on the specified repository.\n\nBy default only open pull requests are returned. This can be controlled\nusing the `state` query parameter. To retrieve pull requests that are\nin one of multiple states, repeat the `state` parameter for each\nindividual state.\n\nThis endpoint also supports filtering and sorting of the results. See\n[filtering and sorting](/cloud/bitbucket/rest/intro/#filtering) for more details.", + "summary": "List pull requests", + "responses": { + "200": { + "description": "All pull requests on the specified repository.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/paginated_pullrequests" + } + } + } + }, + "401": { + "description": "If the repository is private and the request was not authenticated." + }, + "404": { + "description": "If the specified repository does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error" + } + } + } + } + }, + "parameters": [ + { + "name": "state", + "in": "query", + "description": "Only return pull requests that are in this state. This parameter can be repeated.", + "schema": { + "type": "string", + "enum": [ + "OPEN", + "MERGED", + "DECLINED", + "SUPERSEDED" + ] + } + } + ], + "security": [ + { + "oauth2": [ + "pullrequest" + ] + }, + { + "basic": [] + }, + { + "api_key": [] + } + ], + "x-atlassian-oauth2-scopes": [ + { + "state": "Current", + "scheme": "oauth2", + "scopes": [ + "read:pullrequest:bitbucket" + ] + } + ], + "x-atlassian-auth-types": [ + "forge-oauth2", + "api-token" + ] + }, + "post": { + "tags": [ + "Pullrequests" + ], + "description": "Creates a new pull request where the destination repository is\nthis repository and the author is the authenticated user.\n\nThe minimum required fields to create a pull request are `title` and\n`source`, specified by a branch name.\n\n```\ncurl https://api.bitbucket.org/2.0/repositories/my-workspace/my-repository/pullrequests \\\n -u my-username:my-password \\\n --request POST \\\n --header 'Content-Type: application/json' \\\n --data '{\n \"title\": \"My Title\",\n \"source\": {\n \"branch\": {\n \"name\": \"staging\"\n }\n }\n }'\n```\n\nIf the pull request's `destination` is not specified, it will default\nto the `repository.mainbranch`. To open a pull request to a\ndifferent branch, say from a feature branch to a staging branch,\nspecify a `destination` (same format as the `source`):\n\n```\n{\n \"title\": \"My Title\",\n \"source\": {\n \"branch\": {\n \"name\": \"my-feature-branch\"\n }\n },\n \"destination\": {\n \"branch\": {\n \"name\": \"staging\"\n }\n }\n}\n```\n\nReviewers can be specified by adding an array of user objects as the\n`reviewers` property.\n\n```\n{\n \"title\": \"My Title\",\n \"source\": {\n \"branch\": {\n \"name\": \"my-feature-branch\"\n }\n },\n \"reviewers\": [\n {\n \"uuid\": \"{504c3b62-8120-4f0c-a7bc-87800b9d6f70}\"\n }\n ]\n}\n```\n\nOther fields:\n\n* `description` - a string\n* `close_source_branch` - boolean that specifies if the source branch should be closed upon merging\n* `draft` - boolean that specifies whether the pull request is a draft", + "summary": "Create a pull request", + "responses": { + "201": { + "description": "The newly created pull request.", + "headers": { + "Location": { + "description": "The URL of new newly created pull request.", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/pullrequest" + } + } + } + }, + "400": { + "description": "If the input document was invalid, or if the caller lacks the privilege to create repositories under the targeted account.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error" + } + } + } + }, + "401": { + "description": "If the request was not authenticated.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/pullrequest" + } + } + }, + "description": "The new pull request.\n\nThe request URL you POST to becomes the destination repository URL. For this reason, you must specify an explicit source repository in the request object if you want to pull from a different repository (fork).\n\nSince not all elements are required or even mutable, you only need to include the elements you want to initialize, such as the source branch and the title." + }, + "security": [ + { + "oauth2": [ + "pullrequest:write" + ] + }, + { + "basic": [] + }, + { + "api_key": [] + } + ], + "x-atlassian-oauth2-scopes": [ + { + "state": "Current", + "scheme": "oauth2", + "scopes": [ + "read:pullrequest:bitbucket", + "write:pullrequest:bitbucket" + ] + } + ], + "x-atlassian-auth-types": [ + "forge-oauth2", + "api-token" + ] + }, + "parameters": [ + { + "name": "repo_slug", + "in": "path", + "description": "This can either be the repository slug or the UUID of the repository,\nsurrounded by curly-braces, for example: `{repository UUID}`.\n", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "workspace", + "in": "path", + "description": "This can either be the workspace ID (slug) or the workspace UUID\nsurrounded by curly-braces, for example: `{workspace UUID}`.\n", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}": { + "get": { + "tags": [ + "Pullrequests" + ], + "description": "Returns the specified pull request.", + "summary": "Get a pull request", + "responses": { + "200": { + "description": "The pull request object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/pullrequest" + } + } + } + }, + "401": { + "description": "If the repository is private and the request was not authenticated." + }, + "404": { + "description": "If the repository or pull request does not exist", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error" + } + } + } + } + }, + "security": [ + { + "oauth2": [ + "pullrequest" + ] + }, + { + "basic": [] + }, + { + "api_key": [] + } + ], + "x-atlassian-oauth2-scopes": [ + { + "state": "Current", + "scheme": "oauth2", + "scopes": [ + "read:pullrequest:bitbucket" + ] + } + ], + "x-atlassian-auth-types": [ + "forge-oauth2", + "api-token" + ] + }, + "parameters": [ + { + "name": "pull_request_id", + "in": "path", + "description": "The id of the pull request.", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "repo_slug", + "in": "path", + "description": "This can either be the repository slug or the UUID of the repository,\nsurrounded by curly-braces, for example: `{repository UUID}`.\n", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "workspace", + "in": "path", + "description": "This can either be the workspace ID (slug) or the workspace UUID\nsurrounded by curly-braces, for example: `{workspace UUID}`.\n", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/merge": { + "post": { + "tags": [ + "Pullrequests" + ], + "description": "Merges the pull request.", + "summary": "Merge a pull request", + "responses": { + "200": { + "description": "The pull request object.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/pullrequest" + } + } + } + }, + "202": { + "description": "In the Location header, the URL to poll for the pull request merge status" + }, + "409": { + "description": "Unable to merge because one of the refs involved changed while attempting to merge" + }, + "555": { + "description": "If the merge took too long and timed out.\nIn this case the caller should retry the request later", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error" + } + } + } + } + }, + "parameters": [ + { + "name": "async", + "in": "query", + "description": "Default value is false.\n\n\nWhen set to true, runs merge asynchronously and\nimmediately returns a 202 with polling link to\nthe task-status API in the Location header.\n\n\nWhen set to false, runs merge and waits for it to\ncomplete, returning 200 when it succeeds. If the\nduration of the merge exceeds a timeout threshold,\nthe API returns a 202 with polling link to the\ntask-status API in the Location header.", + "required": false, + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/pullrequest_merge_parameters" + } + } + } + }, + "security": [ + { + "oauth2": [ + "pullrequest:write" + ] + }, + { + "basic": [] + }, + { + "api_key": [] + } + ], + "x-atlassian-oauth2-scopes": [ + { + "state": "Current", + "scheme": "oauth2", + "scopes": [ + "read:pullrequest:bitbucket", + "write:pullrequest:bitbucket" + ] + } + ], + "x-atlassian-auth-types": [ + "forge-oauth2", + "api-token" + ] + }, + "parameters": [ + { + "name": "pull_request_id", + "in": "path", + "description": "The id of the pull request.", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "repo_slug", + "in": "path", + "description": "This can either be the repository slug or the UUID of the repository,\nsurrounded by curly-braces, for example: `{repository UUID}`.\n", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "workspace", + "in": "path", + "description": "This can either be the workspace ID (slug) or the workspace UUID\nsurrounded by curly-braces, for example: `{workspace UUID}`.\n", + "required": true, + "schema": { + "type": "string" + } + } + ] + }, + "/repositories/{workspace}/{repo_slug}/commit/{commit}": { + "get": { + "tags": [ + "Commits" + ], + "description": "Returns the specified commit.", + "summary": "Get a commit", + "responses": { + "200": { + "description": "The commit object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/commit" + }, + "examples": { + "response": { + "value": { + "rendered": { + "message": { + "raw": "Add a GEORDI_OUTPUT_DIR setting", + "markup": "markdown", + "html": "

Add a GEORDI_OUTPUT_DIR setting

", + "type": "rendered" + } + }, + "hash": "f7591a13eda445d9a9167f98eb870319f4b6c2d8", + "repository": { + "name": "geordi", + "type": "repository", + "full_name": "bitbucket/geordi", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/bitbucket/geordi" + }, + "html": { + "href": "https://bitbucket.org/bitbucket/geordi" + }, + "avatar": { + "href": "https://bytebucket.org/ravatar/%7B85d08b4e-571d-44e9-a507-fa476535aa98%7D?ts=1730260" + } + }, + "uuid": "{85d08b4e-571d-44e9-a507-fa476535aa98}" + }, + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/bitbucket/geordi/commit/f7591a13eda445d9a9167f98eb870319f4b6c2d8" + }, + "comments": { + "href": "https://api.bitbucket.org/2.0/repositories/bitbucket/geordi/commit/f7591a13eda445d9a9167f98eb870319f4b6c2d8/comments" + }, + "patch": { + "href": "https://api.bitbucket.org/2.0/repositories/bitbucket/geordi/patch/f7591a13eda445d9a9167f98eb870319f4b6c2d8" + }, + "html": { + "href": "https://bitbucket.org/bitbucket/geordi/commits/f7591a13eda445d9a9167f98eb870319f4b6c2d8" + }, + "diff": { + "href": "https://api.bitbucket.org/2.0/repositories/bitbucket/geordi/diff/f7591a13eda445d9a9167f98eb870319f4b6c2d8" + }, + "approve": { + "href": "https://api.bitbucket.org/2.0/repositories/bitbucket/geordi/commit/f7591a13eda445d9a9167f98eb870319f4b6c2d8/approve" + }, + "statuses": { + "href": "https://api.bitbucket.org/2.0/repositories/bitbucket/geordi/commit/f7591a13eda445d9a9167f98eb870319f4b6c2d8/statuses" + } + }, + "author": { + "raw": "Brodie Rao ", + "type": "author", + "user": { + "display_name": "Brodie Rao", + "uuid": "{9484702e-c663-4afd-aefb-c93a8cd31c28}", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/users/%7B9484702e-c663-4afd-aefb-c93a8cd31c28%7D" + }, + "html": { + "href": "https://bitbucket.org/%7B9484702e-c663-4afd-aefb-c93a8cd31c28%7D/" + }, + "avatar": { + "href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/557058:3aae1e05-702a-41e5-81c8-f36f29afb6ca/613070db-28b0-421f-8dba-ae8a87e2a5c7/128" + } + }, + "type": "user", + "nickname": "brodie", + "account_id": "557058:3aae1e05-702a-41e5-81c8-f36f29afb6ca" + } + }, + "summary": { + "raw": "Add a GEORDI_OUTPUT_DIR setting", + "markup": "markdown", + "html": "

Add a GEORDI_OUTPUT_DIR setting

", + "type": "rendered" + }, + "participants": [], + "parents": [ + { + "type": "commit", + "hash": "f06941fec4ef6bcb0c2456927a0cf258fa4f899b", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/bitbucket/geordi/commit/f06941fec4ef6bcb0c2456927a0cf258fa4f899b" + }, + "html": { + "href": "https://bitbucket.org/bitbucket/geordi/commits/f06941fec4ef6bcb0c2456927a0cf258fa4f899b" + } + } + } + ], + "date": "2012-07-16T19:37:54+00:00", + "message": "Add a GEORDI_OUTPUT_DIR setting", + "type": "commit" + } + } + } + } + } + }, + "404": { + "description": "If the specified commit or repository does not exist.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error" + } + } + } + } + }, + "security": [ + { + "oauth2": [ + "repository" + ] + }, + { + "basic": [] + }, + { + "api_key": [] + } + ], + "x-atlassian-oauth2-scopes": [ + { + "state": "Current", + "scheme": "oauth2", + "scopes": [ + "read:repository:bitbucket" + ] + } + ], + "x-atlassian-auth-types": [ + "forge-oauth2", + "api-token" + ] + }, + "parameters": [ + { + "name": "commit", + "in": "path", + "description": "The commit's SHA1.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "repo_slug", + "in": "path", + "description": "This can either be the repository slug or the UUID of the repository,\nsurrounded by curly-braces, for example: `{repository UUID}`.\n", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "workspace", + "in": "path", + "description": "This can either be the workspace ID (slug) or the workspace UUID\nsurrounded by curly-braces, for example: `{workspace UUID}`.\n", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "components": { + "schemas": { + "base_commit": { + "allOf": [ + { + "$ref": "#/components/schemas/object" + }, + { + "type": "object", + "title": "Base Commit", + "description": "The common base type for both repository and snippet commits.", + "properties": { + "hash": { + "type": "string", + "pattern": "[0-9a-f]{7,}?" + }, + "date": { + "type": "string", + "format": "date-time" + }, + "author": { + "$ref": "#/components/schemas/author" + }, + "committer": { + "$ref": "#/components/schemas/committer" + }, + "message": { + "type": "string" + }, + "summary": { + "type": "object", + "properties": { + "raw": { + "type": "string", + "description": "The text as it was typed by a user." + }, + "markup": { + "type": "string", + "description": "The type of markup language the raw content is to be interpreted in.", + "enum": [ + "markdown", + "creole", + "plaintext" + ] + }, + "html": { + "type": "string", + "description": "The user's content rendered as HTML." + } + }, + "additionalProperties": false + }, + "parents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/base_commit" + }, + "minItems": 0 + } + }, + "additionalProperties": true + } + ] + }, + "team_links": { + "allOf": [ + { + "$ref": "#/components/schemas/account_links" + }, + { + "type": "object", + "title": "Team Links", + "description": "Links related to a Team.", + "properties": { + "self": { + "$ref": "#/components/schemas/link" + }, + "html": { + "$ref": "#/components/schemas/link" + }, + "members": { + "$ref": "#/components/schemas/link" + }, + "projects": { + "$ref": "#/components/schemas/link" + }, + "repositories": { + "$ref": "#/components/schemas/link" + } + }, + "additionalProperties": true + } + ] + }, + "committer": { + "allOf": [ + { + "$ref": "#/components/schemas/object" + }, + { + "type": "object", + "title": "Committer", + "description": "The committer of a change in a repository", + "properties": { + "raw": { + "type": "string", + "description": "The raw committer value from the repository. This may be the only value available if the committer does not match a user in Bitbucket." + }, + "user": { + "$ref": "#/components/schemas/account" + } + }, + "additionalProperties": true + } + ] + }, + "ref": { + "type": "object", + "title": "Ref", + "description": "A ref object, representing a branch or tag in a repository.", + "properties": { + "type": { + "type": "string" + }, + "links": { + "type": "object", + "properties": { + "self": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "commits": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "html": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "name": { + "type": "string", + "description": "The name of the ref." + }, + "target": { + "$ref": "#/components/schemas/commit" + } + }, + "required": [ + "type" + ], + "additionalProperties": true + }, + "account": { + "allOf": [ + { + "$ref": "#/components/schemas/object" + }, + { + "type": "object", + "title": "Account", + "description": "An account object.", + "properties": { + "links": { + "$ref": "#/components/schemas/account_links" + }, + "created_on": { + "type": "string", + "format": "date-time" + }, + "display_name": { + "type": "string" + }, + "uuid": { + "type": "string" + } + }, + "additionalProperties": true + } + ] + }, + "pullrequest": { + "allOf": [ + { + "$ref": "#/components/schemas/object" + }, + { + "type": "object", + "title": "Pull Request", + "description": "A pull request object.", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "html": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "commits": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "approve": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "diff": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "diffstat": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "comments": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "activity": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "merge": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "decline": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "id": { + "type": "integer", + "description": "The pull request's unique ID. Note that pull request IDs are only unique within their associated repository." + }, + "title": { + "type": "string", + "description": "Title of the pull request." + }, + "rendered": { + "type": "object", + "title": "Rendered Pull Request Markup", + "description": "User provided pull request text, interpreted in a markup language and rendered in HTML", + "properties": { + "title": { + "type": "object", + "properties": { + "raw": { + "type": "string", + "description": "The text as it was typed by a user." + }, + "markup": { + "type": "string", + "description": "The type of markup language the raw content is to be interpreted in.", + "enum": [ + "markdown", + "creole", + "plaintext" + ] + }, + "html": { + "type": "string", + "description": "The user's content rendered as HTML." + } + }, + "additionalProperties": false + }, + "description": { + "type": "object", + "properties": { + "raw": { + "type": "string", + "description": "The text as it was typed by a user." + }, + "markup": { + "type": "string", + "description": "The type of markup language the raw content is to be interpreted in.", + "enum": [ + "markdown", + "creole", + "plaintext" + ] + }, + "html": { + "type": "string", + "description": "The user's content rendered as HTML." + } + }, + "additionalProperties": false + }, + "reason": { + "type": "object", + "properties": { + "raw": { + "type": "string", + "description": "The text as it was typed by a user." + }, + "markup": { + "type": "string", + "description": "The type of markup language the raw content is to be interpreted in.", + "enum": [ + "markdown", + "creole", + "plaintext" + ] + }, + "html": { + "type": "string", + "description": "The user's content rendered as HTML." + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "summary": { + "type": "object", + "properties": { + "raw": { + "type": "string", + "description": "The text as it was typed by a user." + }, + "markup": { + "type": "string", + "description": "The type of markup language the raw content is to be interpreted in.", + "enum": [ + "markdown", + "creole", + "plaintext" + ] + }, + "html": { + "type": "string", + "description": "The user's content rendered as HTML." + } + }, + "additionalProperties": false + }, + "state": { + "type": "string", + "description": "The pull request's current status.", + "enum": [ + "OPEN", + "DRAFT", + "QUEUED", + "MERGED", + "DECLINED", + "SUPERSEDED" + ] + }, + "author": { + "$ref": "#/components/schemas/account" + }, + "source": { + "$ref": "#/components/schemas/pullrequest_endpoint" + }, + "destination": { + "$ref": "#/components/schemas/pullrequest_endpoint" + }, + "merge_commit": { + "type": "object", + "title": "Pull Request Commit", + "properties": { + "hash": { + "type": "string", + "pattern": "[0-9a-f]{7,}?" + } + }, + "additionalProperties": false + }, + "comment_count": { + "type": "integer", + "description": "The number of comments for a specific pull request.", + "minimum": 0 + }, + "task_count": { + "type": "integer", + "description": "The number of open tasks for a specific pull request.", + "minimum": 0 + }, + "close_source_branch": { + "type": "boolean", + "description": "A boolean flag indicating if merging the pull request closes the source branch." + }, + "closed_by": { + "$ref": "#/components/schemas/account" + }, + "reason": { + "type": "string", + "description": "Explains why a pull request was declined. This field is only applicable to pull requests in rejected state." + }, + "created_on": { + "type": "string", + "description": "The ISO8601 timestamp the request was created.", + "format": "date-time" + }, + "updated_on": { + "type": "string", + "description": "The ISO8601 timestamp the request was last updated.", + "format": "date-time" + }, + "reviewers": { + "type": "array", + "description": "The list of users that were added as reviewers on this pull request when it was created. For performance reasons, the API only includes this list on a pull request's `self` URL.", + "items": { + "$ref": "#/components/schemas/account" + } + }, + "participants": { + "type": "array", + "description": " The list of users that are collaborating on this pull request.\n Collaborators are user that:\n\n * are added to the pull request as a reviewer (part of the reviewers\n list)\n * are not explicit reviewers, but have commented on the pull request\n * are not explicit reviewers, but have approved the pull request\n\n Each user is wrapped in an object that indicates the user's role and\n whether they have approved the pull request. For performance reasons,\n the API only returns this list when an API requests a pull request by\n id.\n ", + "items": { + "$ref": "#/components/schemas/participant" + } + }, + "draft": { + "type": "boolean", + "description": "A boolean flag indicating whether the pull request is a draft." + }, + "queued": { + "type": "boolean", + "description": "A boolean flag indicating whether the pull request is queued" + } + }, + "additionalProperties": true + } + ] + }, + "author": { + "allOf": [ + { + "$ref": "#/components/schemas/object" + }, + { + "type": "object", + "title": "Author", + "description": "The author of a change in a repository", + "properties": { + "raw": { + "type": "string", + "description": "The raw author value from the repository. This may be the only value available if the author does not match a user in Bitbucket." + }, + "user": { + "$ref": "#/components/schemas/account" + } + }, + "additionalProperties": true + } + ] + }, + "branch": { + "allOf": [ + { + "$ref": "#/components/schemas/ref" + }, + { + "type": "object", + "title": "Branch", + "description": "A branch object, representing a branch in a repository.", + "properties": { + "merge_strategies": { + "type": "array", + "description": "Available merge strategies for pull requests targeting this branch.", + "items": { + "type": "string", + "enum": [ + "merge_commit", + "squash", + "fast_forward", + "squash_fast_forward", + "rebase_fast_forward", + "rebase_merge" + ] + } + }, + "default_merge_strategy": { + "type": "string", + "description": "The default merge strategy for pull requests targeting this branch." + } + }, + "additionalProperties": true + } + ] + }, + "repository": { + "allOf": [ + { + "$ref": "#/components/schemas/object" + }, + { + "type": "object", + "title": "Repository", + "description": "A Bitbucket repository.", + "properties": { + "links": { + "type": "object", + "properties": { + "self": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "html": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "avatar": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "pullrequests": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "commits": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "forks": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "watchers": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "downloads": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "clone": { + "type": "array", + "items": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "hooks": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "uuid": { + "type": "string", + "description": "The repository's immutable id. This can be used as a substitute for the slug segment in URLs. Doing this guarantees your URLs will survive renaming of the repository by its owner, or even transfer of the repository to a different user." + }, + "full_name": { + "type": "string", + "description": "The concatenation of the repository owner's username and the slugified name, e.g. \"evzijst/interruptingcow\". This is the same string used in Bitbucket URLs." + }, + "is_private": { + "type": "boolean" + }, + "parent": { + "$ref": "#/components/schemas/repository" + }, + "scm": { + "type": "string", + "enum": [ + "git" + ] + }, + "owner": { + "$ref": "#/components/schemas/account" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "created_on": { + "type": "string", + "format": "date-time" + }, + "updated_on": { + "type": "string", + "format": "date-time" + }, + "size": { + "type": "integer" + }, + "language": { + "type": "string" + }, + "has_issues": { + "type": "boolean", + "description": "\nThe issue tracker for this repository is enabled. Issue Tracker\nfeatures are not supported for repositories in workspaces\nadministered through admin.atlassian.com.\n" + }, + "has_wiki": { + "type": "boolean", + "description": "\nThe wiki for this repository is enabled. Wiki\nfeatures are not supported for repositories in workspaces\nadministered through admin.atlassian.com.\n" + }, + "fork_policy": { + "type": "string", + "description": "\nControls the rules for forking this repository.\n\n* **allow_forks**: unrestricted forking\n* **no_public_forks**: restrict forking to private forks (forks cannot\n be made public later)\n* **no_forks**: deny all forking\n", + "enum": [ + "allow_forks", + "no_public_forks", + "no_forks" + ] + }, + "project": { + "$ref": "#/components/schemas/project" + }, + "mainbranch": { + "$ref": "#/components/schemas/branch" + } + }, + "additionalProperties": true + } + ] + }, + "paginated_pullrequests": { + "type": "object", + "title": "Paginated Pull Requests", + "description": "A paginated list of pullrequests.", + "properties": { + "size": { + "type": "integer", + "description": "Total number of objects in the response. This is an optional element that is not provided in all responses, as it can be expensive to compute.", + "minimum": 0 + }, + "page": { + "type": "integer", + "description": "Page number of the current results. This is an optional element that is not provided in all responses.", + "minimum": 1 + }, + "pagelen": { + "type": "integer", + "description": "Current number of objects on the existing page. The default value is 10 with 100 being the maximum allowed value. Individual APIs may enforce different values.", + "minimum": 1 + }, + "next": { + "type": "string", + "description": "Link to the next page if it exists. The last page of a collection does not have this value. Use this link to navigate the result set and refrain from constructing your own URLs.", + "format": "uri" + }, + "previous": { + "type": "string", + "description": "Link to previous page if it exists. A collections first page does not have this value. This is an optional element that is not provided in all responses. Some result sets strictly support forward navigation and never provide previous links. Clients must anticipate that backwards navigation is not always available. Use this link to navigate the result set and refrain from constructing your own URLs.", + "format": "uri" + }, + "values": { + "type": "array", + "items": { + "$ref": "#/components/schemas/pullrequest" + }, + "minItems": 0, + "uniqueItems": true + } + }, + "additionalProperties": false + }, + "pullrequest_endpoint": { + "type": "object", + "title": "Pull Request Endpoint", + "properties": { + "repository": { + "$ref": "#/components/schemas/repository" + }, + "branch": { + "type": "object", + "title": "Pull Request Branch", + "properties": { + "name": { + "type": "string" + }, + "merge_strategies": { + "type": "array", + "description": "Available merge strategies, when this endpoint is the destination of the pull request.", + "items": { + "type": "string", + "enum": [ + "merge_commit", + "squash", + "fast_forward", + "squash_fast_forward", + "rebase_fast_forward", + "rebase_merge" + ] + } + }, + "default_merge_strategy": { + "type": "string", + "description": "The default merge strategy, when this endpoint is the destination of the pull request." + } + }, + "additionalProperties": false + }, + "commit": { + "type": "object", + "title": "Pull Request Commit", + "properties": { + "hash": { + "type": "string", + "pattern": "[0-9a-f]{7,}?" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "team": { + "allOf": [ + { + "$ref": "#/components/schemas/account" + }, + { + "type": "object", + "title": "Team", + "description": "A team object.", + "properties": { + "links": { + "$ref": "#/components/schemas/team_links" + } + }, + "additionalProperties": true + } + ] + }, + "project": { + "allOf": [ + { + "$ref": "#/components/schemas/object" + }, + { + "type": "object", + "title": "Project", + "description": "A Bitbucket project.\n Projects are used by teams to organize repositories.", + "properties": { + "links": { + "type": "object", + "properties": { + "html": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "avatar": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "uuid": { + "type": "string", + "description": "The project's immutable id." + }, + "key": { + "type": "string", + "description": "The project's key." + }, + "owner": { + "$ref": "#/components/schemas/team" + }, + "name": { + "type": "string", + "description": "The name of the project." + }, + "description": { + "type": "string" + }, + "is_private": { + "type": "boolean", + "description": "\nIndicates whether the project is publicly accessible, or whether it is\nprivate to the team and consequently only visible to team members.\nNote that private projects cannot contain public repositories." + }, + "created_on": { + "type": "string", + "format": "date-time" + }, + "updated_on": { + "type": "string", + "format": "date-time" + }, + "has_publicly_visible_repos": { + "type": "boolean", + "description": "\nIndicates whether the project contains publicly visible repositories.\nNote that private projects cannot contain public repositories." + } + }, + "additionalProperties": true + } + ] + }, + "commit": { + "allOf": [ + { + "$ref": "#/components/schemas/base_commit" + }, + { + "type": "object", + "title": "Commit", + "description": "A repository commit object.", + "properties": { + "repository": { + "$ref": "#/components/schemas/repository" + }, + "participants": { + "type": "array", + "items": { + "$ref": "#/components/schemas/participant" + }, + "minItems": 0 + } + }, + "additionalProperties": true + } + ] + }, + "error": { + "type": "object", + "title": "Error", + "description": "Base type for most resource objects. It defines the common `type` element that identifies an object's type. It also identifies the element as Swagger's `discriminator`.", + "properties": { + "type": { + "type": "string" + }, + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "detail": { + "type": "string" + }, + "data": { + "type": "object", + "description": "Optional structured data that is endpoint-specific.", + "properties": {}, + "additionalProperties": true + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + }, + "required": [ + "type" + ], + "additionalProperties": true + }, + "participant": { + "allOf": [ + { + "$ref": "#/components/schemas/object" + }, + { + "type": "object", + "title": "Participant", + "description": "Object describing a user's role on resources like commits or pull requests.", + "properties": { + "user": { + "$ref": "#/components/schemas/account" + }, + "role": { + "type": "string", + "enum": [ + "PARTICIPANT", + "REVIEWER" + ] + }, + "approved": { + "type": "boolean" + }, + "state": { + "type": "string", + "enum": [ + "approved", + "changes_requested", + null + ] + }, + "participated_on": { + "type": "string", + "description": "The ISO8601 timestamp of the participant's action. For approvers, this is the time of their approval. For commenters and pull request reviewers who are not approvers, this is the time they last commented, or null if they have not commented.", + "format": "date-time" + } + }, + "additionalProperties": true + } + ] + }, + "account_links": { + "type": "object", + "title": "Account Links", + "description": "Links related to an Account.", + "properties": { + "avatar": { + "$ref": "#/components/schemas/link" + } + }, + "additionalProperties": true + }, + "link": { + "type": "object", + "title": "Link", + "description": "A link to a resource related to this object.", + "properties": { + "href": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "object": { + "type": "object", + "description": "Base type for most resource objects. It defines the common `type` element that identifies an object's type. It also identifies the element as Swagger's `discriminator`.", + "properties": { + "type": { + "type": "string" + } + }, + "required": [ + "type" + ], + "additionalProperties": true, + "discriminator": { + "propertyName": "type" + } + }, + "pullrequest_merge_parameters": { + "type": "object", + "title": "Pull Request Merge Parameters", + "description": "The metadata that describes a pull request merge.", + "properties": { + "type": { + "type": "string" + }, + "message": { + "type": "string", + "description": "The commit message that will be used on the resulting commit. Note that the size of the message is limited to 128 KiB." + }, + "close_source_branch": { + "type": "boolean", + "description": "Whether the source branch should be deleted. If this is not provided, we fallback to the value used when the pull request was created, which defaults to False" + }, + "merge_strategy": { + "type": "string", + "description": "The merge strategy that will be used to merge the pull request.", + "enum": [ + "merge_commit", + "squash", + "fast_forward", + "squash_fast_forward", + "rebase_fast_forward", + "rebase_merge" + ], + "default": "merge_commit" + } + }, + "required": [ + "type" + ], + "additionalProperties": true + } + } + } +} diff --git a/pkg/gitprovider/bitbucket/spec/oapi-codegen.yaml b/pkg/gitprovider/bitbucket/spec/oapi-codegen.yaml new file mode 100644 index 0000000000..dc439eb2a2 --- /dev/null +++ b/pkg/gitprovider/bitbucket/spec/oapi-codegen.yaml @@ -0,0 +1,10 @@ +package: bitbucket + +generate: + models: true + client: true + +output: bitbucket_client.gen.go + +output-options: + skip-prune: false From 2dffb75cc077c88ae5b3fa3b320081e3b26aa8e1 Mon Sep 17 00:00:00 2001 From: fuskovic Date: Mon, 20 Apr 2026 17:36:41 +0530 Subject: [PATCH 02/18] add open api cli to tooling Signed-off-by: fuskovic --- hack/tools.mk | 15 ++++++- hack/tools/go.mod | 13 ++++++ hack/tools/go.sum | 96 +++++++++++++++++++++++++++++++++++++++++++++ hack/tools/tools.go | 1 + 4 files changed, 124 insertions(+), 1 deletion(-) diff --git a/hack/tools.mk b/hack/tools.mk index 14d78f8208..c973ad28b8 100644 --- a/hack/tools.mk +++ b/hack/tools.mk @@ -30,6 +30,7 @@ QUILL_VERSION ?= v0.5.1 PROTOC_GEN_DOC_VERSION ?= v1.5.1 SWAG_VERSION ?= $(shell grep github.com/swaggo/swag $(TOOLS_MOD_FILE) | awk '{print $$2}') GO_SWAGGER_VERSION ?= $(shell grep github.com/go-swagger/go-swagger $(TOOLS_MOD_FILE) | awk '{print $$2}') +OAPI_CODEGEN_VERSION ?= $(shell grep github.com/oapi-codegen/oapi-codegen $(TOOLS_MOD_FILE) | awk '{print $$2}') TILT_VERSION ?= v0.36.3 CTLPTL_VERSION ?= v0.9.0 KIND_VERSION ?= v0.31.0 @@ -52,6 +53,7 @@ QUILL := $(BIN_DIR)/quill-$(OS)-$(ARCH)-$(QUILL_VERSION) PROTOC_GEN_DOC := $(BIN_DIR)/protoc-gen-doc-$(OS)-$(ARCH)-$(PROTOC_GEN_DOC_VERSION) SWAG := $(BIN_DIR)/swag-$(OS)-$(ARCH)-$(SWAG_VERSION) GO_SWAGGER := $(BIN_DIR)/go-swagger-$(OS)-$(ARCH)-$(GO_SWAGGER_VERSION) +OAPI_CODEGEN := $(BIN_DIR)/oapi-codegen-$(OS)-$(ARCH)-$(OAPI_CODEGEN_VERSION) TILT := $(BIN_DIR)/tilt-$(OS)-$(ARCH)-$(TILT_VERSION) CTLPTL := $(BIN_DIR)/ctlptl-$(OS)-$(ARCH)-$(CTLPTL_VERSION) KIND := $(BIN_DIR)/kind-$(OS)-$(ARCH)-$(KIND_VERSION) @@ -94,6 +96,9 @@ $(SWAG): $(GO_SWAGGER): $(call go-install-tool,$@,github.com/go-swagger/go-swagger/cmd/swagger,$(GO_SWAGGER_VERSION)) +$(OAPI_CODEGEN): + $(call go-install-tool,$@,github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen,$(OAPI_CODEGEN_VERSION)) + $(TILT): $(call install-tilt,$@,$(TILT_VERSION)) @@ -125,6 +130,7 @@ QUILL_LINK := $(BIN_DIR)/quill PROTOC_GEN_DOC_LINK := $(BIN_DIR)/protoc-gen-doc SWAG_LINK := $(BIN_DIR)/swag GO_SWAGGER_LINK := $(BIN_DIR)/go-swagger +OAPI_CODEGEN_LINK := $(BIN_DIR)/oapi-codegen TILT_LINK := $(BIN_DIR)/tilt CTLPTL_LINK := $(BIN_DIR)/ctlptl KIND_LINK := $(BIN_DIR)/kind @@ -179,6 +185,10 @@ $(SWAG_LINK): $(SWAG) $(GO_SWAGGER_LINK): $(GO_SWAGGER) $(call create-symlink,$(GO_SWAGGER),$(GO_SWAGGER_LINK)) +.PHONY: $(OAPI_CODEGEN_LINK) +$(OAPI_CODEGEN_LINK): $(OAPI_CODEGEN) + $(call create-symlink,$(OAPI_CODEGEN),$(OAPI_CODEGEN_LINK)) + .PHONY: $(TILT_LINK) $(TILT_LINK): $(TILT) $(call create-symlink,$(TILT),$(TILT_LINK)) @@ -203,7 +213,7 @@ $(JQ_LINK): $(JQ) # Alias targets # ################################################################################ -TOOLS := install-golangci-lint install-helm install-goimports install-go-to-protobuf install-protoc-gen-gogo install-controller-gen install-protoc install-buf install-quill install-protoc-gen-doc install-swag install-go-swagger install-tilt install-ctlptl install-kind install-k3d install-jq +TOOLS := install-golangci-lint install-helm install-goimports install-go-to-protobuf install-protoc-gen-gogo install-controller-gen install-protoc install-buf install-quill install-protoc-gen-doc install-swag install-go-swagger install-oapi-codegen install-tilt install-ctlptl install-kind install-k3d install-jq .PHONY: install-tools install-tools: $(TOOLS) @@ -244,6 +254,9 @@ install-swag: $(SWAG) $(SWAG_LINK) .PHONY: install-go-swagger install-go-swagger: $(GO_SWAGGER) $(GO_SWAGGER_LINK) +.PHONY: install-oapi-codegen +install-oapi-codegen: $(OAPI_CODEGEN) $(OAPI_CODEGEN_LINK) + .PHONY: install-tilt install-tilt: $(TILT) $(TILT_LINK) diff --git a/hack/tools/go.mod b/hack/tools/go.mod index b511392f69..aab059e77c 100644 --- a/hack/tools/go.mod +++ b/hack/tools/go.mod @@ -6,6 +6,7 @@ require ( github.com/bufbuild/buf v1.61.0 github.com/go-swagger/go-swagger v0.33.2 github.com/golangci/golangci-lint/v2 v2.10.1 + github.com/oapi-codegen/oapi-codegen/v2 v2.6.0 github.com/swaggo/swag v1.16.6 golang.org/x/tools v0.42.0 helm.sh/helm/v3 v3.19.4 @@ -116,6 +117,7 @@ require ( github.com/docker/docker-credential-helpers v0.9.4 // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/ettle/strcase v0.2.0 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect @@ -127,6 +129,7 @@ require ( github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect + github.com/getkin/kin-openapi v0.133.0 // indirect github.com/ghostiam/protogetter v0.3.20 // indirect github.com/go-chi/chi/v5 v5.2.3 // indirect github.com/go-critic/go-critic v0.14.3 // indirect @@ -209,6 +212,7 @@ require ( github.com/jingyugao/rowserrcheck v1.1.1 // indirect github.com/jjti/go-spancheck v0.6.5 // indirect github.com/jmoiron/sqlx v1.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/julz/importas v0.2.0 // indirect github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect @@ -234,6 +238,7 @@ require ( github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/macabu/inamedparam v0.2.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect github.com/manuelarte/funcorder v0.5.0 // indirect github.com/maratori/testableexamples v1.0.1 // indirect @@ -252,6 +257,7 @@ require ( github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/moricho/tparallel v0.3.2 // indirect github.com/morikuni/aec v1.0.0 // indirect @@ -262,10 +268,13 @@ require ( github.com/nishanths/exhaustive v0.12.0 // indirect github.com/nishanths/predeclared v0.2.2 // indirect github.com/nunnatsa/ginkgolinter v0.23.0 // indirect + github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect + github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/oklog/ulid/v2 v2.1.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -303,6 +312,8 @@ require ( github.com/sonatard/noctx v0.4.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/sourcegraph/go-diff v0.7.0 // indirect + github.com/speakeasy-api/jsonpath v0.6.0 // indirect + github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/cobra v1.10.2 // indirect @@ -328,6 +339,8 @@ require ( github.com/uudashr/gocognit v1.2.0 // indirect github.com/uudashr/iface v1.4.1 // indirect github.com/vbatts/tar-split v0.12.1 // indirect + github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect + github.com/woodsbury/decimal128 v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xen0n/gosmopolitan v1.3.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect diff --git a/hack/tools/go.sum b/hack/tools/go.sum index e58f4492a4..3729131474 100644 --- a/hack/tools/go.sum +++ b/hack/tools/go.sum @@ -177,6 +177,9 @@ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0G github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs= github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk= github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= @@ -242,6 +245,9 @@ github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQ github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w= +github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= @@ -262,12 +268,16 @@ github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7Dlme github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= +github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= +github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1f+MzD0= github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI= github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= @@ -339,8 +349,12 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-swagger/go-swagger v0.33.2 h1:L1dxjjI29MKSWpRT0xXTOOaI3jzDWws2vR9oQmtYBZU= github.com/go-swagger/go-swagger v0.33.2/go.mod h1:1HGAWunq7SIuIPIWPHlZEDBURdzUk3BxSPr7x4dFHjc= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8= github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU= github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s= @@ -374,6 +388,15 @@ github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0= @@ -404,15 +427,19 @@ github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU= github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -461,8 +488,10 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jdx/go-netrc v1.0.0 h1:QbLMLyCZGj0NA8glAhxUpf1zDg6cxnWgMBbjq40W0gQ= @@ -479,6 +508,8 @@ github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgY github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ= @@ -495,8 +526,11 @@ github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3J github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kulti/thelper v0.7.1 h1:fI8QITAoFVLx+y+vSyuLBP+rcVIB8jKooNSCT2EiI98= @@ -532,6 +566,8 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE= github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww= github.com/manuelarte/embeddedstructfieldcheck v0.4.0/go.mod h1:z8dFSyXqp+fC6NLDSljRJeNQJJDWnY7RoWFzV3PC6UM= github.com/manuelarte/funcorder v0.5.0 h1:llMuHXXbg7tD0i/LNw8vGnkDTHFpTnWqKPI85Rknc+8= @@ -582,6 +618,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= @@ -602,14 +640,31 @@ github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= github.com/nunnatsa/ginkgolinter v0.23.0 h1:x3o4DGYOWbBMP/VdNQKgSj+25aJKx2Pe6lHr8gBcgf8= github.com/nunnatsa/ginkgolinter v0.23.0/go.mod h1:9qN1+0akwXEccwV1CAcCDfcoBlWXHB+ML9884pL4SZ4= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oapi-codegen/oapi-codegen/v2 v2.6.0 h1:4i+F2cvwBFZeplxCssNdLy3MhNzUD87mI3HnayHZkAU= +github.com/oapi-codegen/oapi-codegen/v2 v2.6.0/go.mod h1:eWHeJSohQJIINJZzzQriVynfGsnlQVh0UkN2UYYcw4Q= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= +github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= +github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -626,6 +681,8 @@ github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 h1:QTvNkZ5ylY0PGgA+Lih+GdboMLY/G9SEGLMEGVjTVA4= @@ -707,6 +764,7 @@ github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w= github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= @@ -724,6 +782,10 @@ github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrel github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= +github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8= +github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= +github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= +github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= @@ -750,6 +812,7 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -781,6 +844,8 @@ github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+ github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= github.com/toqueteos/webbrowser v1.2.1 h1:O7IsnnU7XQyJ1nHMRfAktUUJOAZD3aQyUVnxzhWphCg= github.com/toqueteos/webbrowser v1.2.1/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI= github.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA= github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g= @@ -793,6 +858,10 @@ github.com/uudashr/iface v1.4.1 h1:J16Xl1wyNX9ofhpHmQ9h9gk5rnv2A6lX/2+APLTo0zU= github.com/uudashr/iface v1.4.1/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg= github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= +github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= +github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= +github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= +github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM= @@ -917,14 +986,18 @@ golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= @@ -934,6 +1007,7 @@ golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -945,11 +1019,17 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -958,6 +1038,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -994,6 +1075,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= @@ -1019,21 +1101,35 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hack/tools/tools.go b/hack/tools/tools.go index 42b381e26f..ba5443b2bb 100644 --- a/hack/tools/tools.go +++ b/hack/tools/tools.go @@ -10,6 +10,7 @@ import ( _ "github.com/bufbuild/buf/cmd/buf" _ "github.com/go-swagger/go-swagger/cmd/swagger" _ "github.com/golangci/golangci-lint/v2/cmd/golangci-lint" + _ "github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen" _ "github.com/swaggo/swag/cmd/swag" _ "golang.org/x/tools/cmd/goimports" _ "helm.sh/helm/v3/cmd/helm" From 7dc007a9861001973b42935946dfb35092f3736a Mon Sep 17 00:00:00 2001 From: fuskovic Date: Mon, 20 Apr 2026 17:37:08 +0530 Subject: [PATCH 03/18] add make target for generating bb client Signed-off-by: fuskovic --- Makefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ff40607378..9f6e0c8ca7 100644 --- a/Makefile +++ b/Makefile @@ -216,7 +216,7 @@ build-cli-with-ui: build-ui build-cli ################################################################################ .PHONY: codegen -codegen: codegen-openapi codegen-proto codegen-controller codegen-schema-to-go codegen-ui codegen-docs +codegen: codegen-openapi codegen-proto codegen-controller codegen-schema-to-go codegen-bitbucket-client codegen-ui codegen-docs .PHONY: codegen-openapi codegen-openapi: install-swag install-go-swagger install-jq @@ -259,6 +259,10 @@ codegen-controller: install-controller-gen object:headerFile=hack/boilerplate.go.txt \ paths=./... +.PHONY: codegen-bitbucket-client +codegen-bitbucket-client: install-oapi-codegen + cd pkg/gitprovider/bitbucket && $(OAPI_CODEGEN_LINK) --config spec/oapi-codegen.yaml spec/bitbucket.gen.json + .PHONY: codegen-schema-to-go codegen-schema-to-go: install-goimports npm install -g quicktype@23.0.176 From 5e2c1e4611419436da7bc5a92b86b7a4cd0402a4 Mon Sep 17 00:00:00 2001 From: fuskovic Date: Mon, 20 Apr 2026 17:38:15 +0530 Subject: [PATCH 04/18] generate bb client Signed-off-by: fuskovic --- .../bitbucket/bitbucket_client.gen.go | 4526 +++++++++++++++++ 1 file changed, 4526 insertions(+) create mode 100644 pkg/gitprovider/bitbucket/bitbucket_client.gen.go diff --git a/pkg/gitprovider/bitbucket/bitbucket_client.gen.go b/pkg/gitprovider/bitbucket/bitbucket_client.gen.go new file mode 100644 index 0000000000..d96a3fa541 --- /dev/null +++ b/pkg/gitprovider/bitbucket/bitbucket_client.gen.go @@ -0,0 +1,4526 @@ +// Package bitbucket 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 bitbucket + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/oapi-codegen/runtime" +) + +const ( + Api_keyScopes = "api_key.Scopes" + BasicScopes = "basic.Scopes" + Oauth2Scopes = "oauth2.Scopes" +) + +// Defines values for BaseCommitSummaryMarkup. +const ( + BaseCommitSummaryMarkupCreole BaseCommitSummaryMarkup = "creole" + BaseCommitSummaryMarkupMarkdown BaseCommitSummaryMarkup = "markdown" + BaseCommitSummaryMarkupPlaintext BaseCommitSummaryMarkup = "plaintext" +) + +// Valid indicates whether the value is a known member of the BaseCommitSummaryMarkup enum. +func (e BaseCommitSummaryMarkup) Valid() bool { + switch e { + case BaseCommitSummaryMarkupCreole: + return true + case BaseCommitSummaryMarkupMarkdown: + return true + case BaseCommitSummaryMarkupPlaintext: + return true + default: + return false + } +} + +// Defines values for BranchMergeStrategies. +const ( + BranchMergeStrategiesFastForward BranchMergeStrategies = "fast_forward" + BranchMergeStrategiesMergeCommit BranchMergeStrategies = "merge_commit" + BranchMergeStrategiesRebaseFastForward BranchMergeStrategies = "rebase_fast_forward" + BranchMergeStrategiesRebaseMerge BranchMergeStrategies = "rebase_merge" + BranchMergeStrategiesSquash BranchMergeStrategies = "squash" + BranchMergeStrategiesSquashFastForward BranchMergeStrategies = "squash_fast_forward" +) + +// Valid indicates whether the value is a known member of the BranchMergeStrategies enum. +func (e BranchMergeStrategies) Valid() bool { + switch e { + case BranchMergeStrategiesFastForward: + return true + case BranchMergeStrategiesMergeCommit: + return true + case BranchMergeStrategiesRebaseFastForward: + return true + case BranchMergeStrategiesRebaseMerge: + return true + case BranchMergeStrategiesSquash: + return true + case BranchMergeStrategiesSquashFastForward: + return true + default: + return false + } +} + +// Defines values for CommitSummaryMarkup. +const ( + CommitSummaryMarkupCreole CommitSummaryMarkup = "creole" + CommitSummaryMarkupMarkdown CommitSummaryMarkup = "markdown" + CommitSummaryMarkupPlaintext CommitSummaryMarkup = "plaintext" +) + +// Valid indicates whether the value is a known member of the CommitSummaryMarkup enum. +func (e CommitSummaryMarkup) Valid() bool { + switch e { + case CommitSummaryMarkupCreole: + return true + case CommitSummaryMarkupMarkdown: + return true + case CommitSummaryMarkupPlaintext: + return true + default: + return false + } +} + +// Defines values for ParticipantRole. +const ( + PARTICIPANT ParticipantRole = "PARTICIPANT" + REVIEWER ParticipantRole = "REVIEWER" +) + +// Valid indicates whether the value is a known member of the ParticipantRole enum. +func (e ParticipantRole) Valid() bool { + switch e { + case PARTICIPANT: + return true + case REVIEWER: + return true + default: + return false + } +} + +// Defines values for ParticipantState. +const ( + Approved ParticipantState = "approved" + ChangesRequested ParticipantState = "changes_requested" + LessThannil ParticipantState = "" +) + +// Valid indicates whether the value is a known member of the ParticipantState enum. +func (e ParticipantState) Valid() bool { + switch e { + case Approved: + return true + case ChangesRequested: + return true + case LessThannil: + return true + default: + return false + } +} + +// Defines values for PullrequestRenderedDescriptionMarkup. +const ( + PullrequestRenderedDescriptionMarkupCreole PullrequestRenderedDescriptionMarkup = "creole" + PullrequestRenderedDescriptionMarkupMarkdown PullrequestRenderedDescriptionMarkup = "markdown" + PullrequestRenderedDescriptionMarkupPlaintext PullrequestRenderedDescriptionMarkup = "plaintext" +) + +// Valid indicates whether the value is a known member of the PullrequestRenderedDescriptionMarkup enum. +func (e PullrequestRenderedDescriptionMarkup) Valid() bool { + switch e { + case PullrequestRenderedDescriptionMarkupCreole: + return true + case PullrequestRenderedDescriptionMarkupMarkdown: + return true + case PullrequestRenderedDescriptionMarkupPlaintext: + return true + default: + return false + } +} + +// Defines values for PullrequestRenderedReasonMarkup. +const ( + PullrequestRenderedReasonMarkupCreole PullrequestRenderedReasonMarkup = "creole" + PullrequestRenderedReasonMarkupMarkdown PullrequestRenderedReasonMarkup = "markdown" + PullrequestRenderedReasonMarkupPlaintext PullrequestRenderedReasonMarkup = "plaintext" +) + +// Valid indicates whether the value is a known member of the PullrequestRenderedReasonMarkup enum. +func (e PullrequestRenderedReasonMarkup) Valid() bool { + switch e { + case PullrequestRenderedReasonMarkupCreole: + return true + case PullrequestRenderedReasonMarkupMarkdown: + return true + case PullrequestRenderedReasonMarkupPlaintext: + return true + default: + return false + } +} + +// Defines values for PullrequestRenderedTitleMarkup. +const ( + PullrequestRenderedTitleMarkupCreole PullrequestRenderedTitleMarkup = "creole" + PullrequestRenderedTitleMarkupMarkdown PullrequestRenderedTitleMarkup = "markdown" + PullrequestRenderedTitleMarkupPlaintext PullrequestRenderedTitleMarkup = "plaintext" +) + +// Valid indicates whether the value is a known member of the PullrequestRenderedTitleMarkup enum. +func (e PullrequestRenderedTitleMarkup) Valid() bool { + switch e { + case PullrequestRenderedTitleMarkupCreole: + return true + case PullrequestRenderedTitleMarkupMarkdown: + return true + case PullrequestRenderedTitleMarkupPlaintext: + return true + default: + return false + } +} + +// Defines values for PullrequestState. +const ( + PullrequestStateDECLINED PullrequestState = "DECLINED" + PullrequestStateDRAFT PullrequestState = "DRAFT" + PullrequestStateMERGED PullrequestState = "MERGED" + PullrequestStateOPEN PullrequestState = "OPEN" + PullrequestStateQUEUED PullrequestState = "QUEUED" + PullrequestStateSUPERSEDED PullrequestState = "SUPERSEDED" +) + +// Valid indicates whether the value is a known member of the PullrequestState enum. +func (e PullrequestState) Valid() bool { + switch e { + case PullrequestStateDECLINED: + return true + case PullrequestStateDRAFT: + return true + case PullrequestStateMERGED: + return true + case PullrequestStateOPEN: + return true + case PullrequestStateQUEUED: + return true + case PullrequestStateSUPERSEDED: + return true + default: + return false + } +} + +// Defines values for PullrequestSummaryMarkup. +const ( + PullrequestSummaryMarkupCreole PullrequestSummaryMarkup = "creole" + PullrequestSummaryMarkupMarkdown PullrequestSummaryMarkup = "markdown" + PullrequestSummaryMarkupPlaintext PullrequestSummaryMarkup = "plaintext" +) + +// Valid indicates whether the value is a known member of the PullrequestSummaryMarkup enum. +func (e PullrequestSummaryMarkup) Valid() bool { + switch e { + case PullrequestSummaryMarkupCreole: + return true + case PullrequestSummaryMarkupMarkdown: + return true + case PullrequestSummaryMarkupPlaintext: + return true + default: + return false + } +} + +// Defines values for PullrequestEndpointBranchMergeStrategies. +const ( + PullrequestEndpointBranchMergeStrategiesFastForward PullrequestEndpointBranchMergeStrategies = "fast_forward" + PullrequestEndpointBranchMergeStrategiesMergeCommit PullrequestEndpointBranchMergeStrategies = "merge_commit" + PullrequestEndpointBranchMergeStrategiesRebaseFastForward PullrequestEndpointBranchMergeStrategies = "rebase_fast_forward" + PullrequestEndpointBranchMergeStrategiesRebaseMerge PullrequestEndpointBranchMergeStrategies = "rebase_merge" + PullrequestEndpointBranchMergeStrategiesSquash PullrequestEndpointBranchMergeStrategies = "squash" + PullrequestEndpointBranchMergeStrategiesSquashFastForward PullrequestEndpointBranchMergeStrategies = "squash_fast_forward" +) + +// Valid indicates whether the value is a known member of the PullrequestEndpointBranchMergeStrategies enum. +func (e PullrequestEndpointBranchMergeStrategies) Valid() bool { + switch e { + case PullrequestEndpointBranchMergeStrategiesFastForward: + return true + case PullrequestEndpointBranchMergeStrategiesMergeCommit: + return true + case PullrequestEndpointBranchMergeStrategiesRebaseFastForward: + return true + case PullrequestEndpointBranchMergeStrategiesRebaseMerge: + return true + case PullrequestEndpointBranchMergeStrategiesSquash: + return true + case PullrequestEndpointBranchMergeStrategiesSquashFastForward: + return true + default: + return false + } +} + +// Defines values for PullrequestMergeParametersMergeStrategy. +const ( + FastForward PullrequestMergeParametersMergeStrategy = "fast_forward" + MergeCommit PullrequestMergeParametersMergeStrategy = "merge_commit" + RebaseFastForward PullrequestMergeParametersMergeStrategy = "rebase_fast_forward" + RebaseMerge PullrequestMergeParametersMergeStrategy = "rebase_merge" + Squash PullrequestMergeParametersMergeStrategy = "squash" + SquashFastForward PullrequestMergeParametersMergeStrategy = "squash_fast_forward" +) + +// Valid indicates whether the value is a known member of the PullrequestMergeParametersMergeStrategy enum. +func (e PullrequestMergeParametersMergeStrategy) Valid() bool { + switch e { + case FastForward: + return true + case MergeCommit: + return true + case RebaseFastForward: + return true + case RebaseMerge: + return true + case Squash: + return true + case SquashFastForward: + return true + default: + return false + } +} + +// Defines values for RepositoryForkPolicy. +const ( + AllowForks RepositoryForkPolicy = "allow_forks" + NoForks RepositoryForkPolicy = "no_forks" + NoPublicForks RepositoryForkPolicy = "no_public_forks" +) + +// Valid indicates whether the value is a known member of the RepositoryForkPolicy enum. +func (e RepositoryForkPolicy) Valid() bool { + switch e { + case AllowForks: + return true + case NoForks: + return true + case NoPublicForks: + return true + default: + return false + } +} + +// Defines values for RepositoryScm. +const ( + Git RepositoryScm = "git" +) + +// Valid indicates whether the value is a known member of the RepositoryScm enum. +func (e RepositoryScm) Valid() bool { + switch e { + case Git: + return true + default: + return false + } +} + +// Defines values for GetRepositoriesWorkspaceRepoSlugPullrequestsParamsState. +const ( + GetRepositoriesWorkspaceRepoSlugPullrequestsParamsStateDECLINED GetRepositoriesWorkspaceRepoSlugPullrequestsParamsState = "DECLINED" + GetRepositoriesWorkspaceRepoSlugPullrequestsParamsStateMERGED GetRepositoriesWorkspaceRepoSlugPullrequestsParamsState = "MERGED" + GetRepositoriesWorkspaceRepoSlugPullrequestsParamsStateOPEN GetRepositoriesWorkspaceRepoSlugPullrequestsParamsState = "OPEN" + GetRepositoriesWorkspaceRepoSlugPullrequestsParamsStateSUPERSEDED GetRepositoriesWorkspaceRepoSlugPullrequestsParamsState = "SUPERSEDED" +) + +// Valid indicates whether the value is a known member of the GetRepositoriesWorkspaceRepoSlugPullrequestsParamsState enum. +func (e GetRepositoriesWorkspaceRepoSlugPullrequestsParamsState) Valid() bool { + switch e { + case GetRepositoriesWorkspaceRepoSlugPullrequestsParamsStateDECLINED: + return true + case GetRepositoriesWorkspaceRepoSlugPullrequestsParamsStateMERGED: + return true + case GetRepositoriesWorkspaceRepoSlugPullrequestsParamsStateOPEN: + return true + case GetRepositoriesWorkspaceRepoSlugPullrequestsParamsStateSUPERSEDED: + return true + default: + return false + } +} + +// Account defines model for account. +type Account struct { + CreatedOn *time.Time `json:"created_on,omitempty"` + DisplayName *string `json:"display_name,omitempty"` + + // Links Links related to an Account. + Links *AccountLinks `json:"links,omitempty"` + Type string `json:"type"` + Uuid *string `json:"uuid,omitempty"` + AdditionalProperties map[string]interface{} `json:"-"` +} + +// AccountLinks Links related to an Account. +type AccountLinks struct { + // Avatar A link to a resource related to this object. + Avatar *Link `json:"avatar,omitempty"` + AdditionalProperties map[string]interface{} `json:"-"` +} + +// Author defines model for author. +type Author struct { + // Raw The raw author value from the repository. This may be the only value available if the author does not match a user in Bitbucket. + Raw *string `json:"raw,omitempty"` + Type string `json:"type"` + User *Account `json:"user,omitempty"` + AdditionalProperties map[string]interface{} `json:"-"` +} + +// BaseCommit defines model for base_commit. +type BaseCommit struct { + Author *Author `json:"author,omitempty"` + Committer *Committer `json:"committer,omitempty"` + Date *time.Time `json:"date,omitempty"` + Hash *string `json:"hash,omitempty"` + Message *string `json:"message,omitempty"` + Parents *[]BaseCommit `json:"parents,omitempty"` + Summary *struct { + // Html The user's content rendered as HTML. + Html *string `json:"html,omitempty"` + + // Markup The type of markup language the raw content is to be interpreted in. + Markup *BaseCommitSummaryMarkup `json:"markup,omitempty"` + + // Raw The text as it was typed by a user. + Raw *string `json:"raw,omitempty"` + } `json:"summary,omitempty"` + Type string `json:"type"` + AdditionalProperties map[string]interface{} `json:"-"` +} + +// BaseCommitSummaryMarkup The type of markup language the raw content is to be interpreted in. +type BaseCommitSummaryMarkup string + +// Branch defines model for branch. +type Branch struct { + // DefaultMergeStrategy The default merge strategy for pull requests targeting this branch. + DefaultMergeStrategy *string `json:"default_merge_strategy,omitempty"` + Links *struct { + // Commits A link to a resource related to this object. + Commits *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"commits,omitempty"` + + // Html A link to a resource related to this object. + Html *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"html,omitempty"` + + // Self A link to a resource related to this object. + Self *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"self,omitempty"` + } `json:"links,omitempty"` + + // MergeStrategies Available merge strategies for pull requests targeting this branch. + MergeStrategies *[]BranchMergeStrategies `json:"merge_strategies,omitempty"` + + // Name The name of the ref. + Name *string `json:"name,omitempty"` + Target *Commit `json:"target,omitempty"` + Type string `json:"type"` + AdditionalProperties map[string]interface{} `json:"-"` +} + +// BranchMergeStrategies defines model for Branch.MergeStrategies. +type BranchMergeStrategies string + +// Commit defines model for commit. +type Commit struct { + Author *Author `json:"author,omitempty"` + Committer *Committer `json:"committer,omitempty"` + Date *time.Time `json:"date,omitempty"` + Hash *string `json:"hash,omitempty"` + Message *string `json:"message,omitempty"` + Parents *[]BaseCommit `json:"parents,omitempty"` + Participants *[]Participant `json:"participants,omitempty"` + Repository *Repository `json:"repository,omitempty"` + Summary *struct { + // Html The user's content rendered as HTML. + Html *string `json:"html,omitempty"` + + // Markup The type of markup language the raw content is to be interpreted in. + Markup *CommitSummaryMarkup `json:"markup,omitempty"` + + // Raw The text as it was typed by a user. + Raw *string `json:"raw,omitempty"` + } `json:"summary,omitempty"` + Type string `json:"type"` + AdditionalProperties map[string]interface{} `json:"-"` +} + +// CommitSummaryMarkup The type of markup language the raw content is to be interpreted in. +type CommitSummaryMarkup string + +// Committer defines model for committer. +type Committer struct { + // Raw The raw committer value from the repository. This may be the only value available if the committer does not match a user in Bitbucket. + Raw *string `json:"raw,omitempty"` + Type string `json:"type"` + User *Account `json:"user,omitempty"` + AdditionalProperties map[string]interface{} `json:"-"` +} + +// Error Base type for most resource objects. It defines the common `type` element that identifies an object's type. It also identifies the element as Swagger's `discriminator`. +type Error struct { + Error *struct { + // Data Optional structured data that is endpoint-specific. + Data *map[string]interface{} `json:"data,omitempty"` + Detail *string `json:"detail,omitempty"` + Message string `json:"message"` + } `json:"error,omitempty"` + Type string `json:"type"` + AdditionalProperties map[string]interface{} `json:"-"` +} + +// Link A link to a resource related to this object. +type Link struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` +} + +// Object Base type for most resource objects. It defines the common `type` element that identifies an object's type. It also identifies the element as Swagger's `discriminator`. +type Object struct { + Type string `json:"type"` + AdditionalProperties map[string]interface{} `json:"-"` +} + +// PaginatedPullrequests A paginated list of pullrequests. +type PaginatedPullrequests struct { + // Next Link to the next page if it exists. The last page of a collection does not have this value. Use this link to navigate the result set and refrain from constructing your own URLs. + Next *string `json:"next,omitempty"` + + // Page Page number of the current results. This is an optional element that is not provided in all responses. + Page *int `json:"page,omitempty"` + + // Pagelen Current number of objects on the existing page. The default value is 10 with 100 being the maximum allowed value. Individual APIs may enforce different values. + Pagelen *int `json:"pagelen,omitempty"` + + // Previous Link to previous page if it exists. A collections first page does not have this value. This is an optional element that is not provided in all responses. Some result sets strictly support forward navigation and never provide previous links. Clients must anticipate that backwards navigation is not always available. Use this link to navigate the result set and refrain from constructing your own URLs. + Previous *string `json:"previous,omitempty"` + + // Size Total number of objects in the response. This is an optional element that is not provided in all responses, as it can be expensive to compute. + Size *int `json:"size,omitempty"` + Values *[]Pullrequest `json:"values,omitempty"` +} + +// Participant defines model for participant. +type Participant struct { + Approved *bool `json:"approved,omitempty"` + + // ParticipatedOn The ISO8601 timestamp of the participant's action. For approvers, this is the time of their approval. For commenters and pull request reviewers who are not approvers, this is the time they last commented, or null if they have not commented. + ParticipatedOn *time.Time `json:"participated_on,omitempty"` + Role *ParticipantRole `json:"role,omitempty"` + State *ParticipantState `json:"state,omitempty"` + Type string `json:"type"` + User *Account `json:"user,omitempty"` + AdditionalProperties map[string]interface{} `json:"-"` +} + +// ParticipantRole defines model for Participant.Role. +type ParticipantRole string + +// ParticipantState defines model for Participant.State. +type ParticipantState string + +// Project defines model for project. +type Project struct { + CreatedOn *time.Time `json:"created_on,omitempty"` + Description *string `json:"description,omitempty"` + + // HasPubliclyVisibleRepos + // Indicates whether the project contains publicly visible repositories. + // Note that private projects cannot contain public repositories. + HasPubliclyVisibleRepos *bool `json:"has_publicly_visible_repos,omitempty"` + + // IsPrivate + // Indicates whether the project is publicly accessible, or whether it is + // private to the team and consequently only visible to team members. + // Note that private projects cannot contain public repositories. + IsPrivate *bool `json:"is_private,omitempty"` + + // Key The project's key. + Key *string `json:"key,omitempty"` + Links *struct { + // Avatar A link to a resource related to this object. + Avatar *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"avatar,omitempty"` + + // Html A link to a resource related to this object. + Html *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"html,omitempty"` + } `json:"links,omitempty"` + + // Name The name of the project. + Name *string `json:"name,omitempty"` + Owner *Team `json:"owner,omitempty"` + Type string `json:"type"` + UpdatedOn *time.Time `json:"updated_on,omitempty"` + + // Uuid The project's immutable id. + Uuid *string `json:"uuid,omitempty"` + AdditionalProperties map[string]interface{} `json:"-"` +} + +// Pullrequest defines model for pullrequest. +type Pullrequest struct { + Author *Account `json:"author,omitempty"` + + // CloseSourceBranch A boolean flag indicating if merging the pull request closes the source branch. + CloseSourceBranch *bool `json:"close_source_branch,omitempty"` + ClosedBy *Account `json:"closed_by,omitempty"` + + // CommentCount The number of comments for a specific pull request. + CommentCount *int `json:"comment_count,omitempty"` + + // CreatedOn The ISO8601 timestamp the request was created. + CreatedOn *time.Time `json:"created_on,omitempty"` + Destination *PullrequestEndpoint `json:"destination,omitempty"` + + // Draft A boolean flag indicating whether the pull request is a draft. + Draft *bool `json:"draft,omitempty"` + + // Id The pull request's unique ID. Note that pull request IDs are only unique within their associated repository. + Id *int `json:"id,omitempty"` + Links *struct { + // Activity A link to a resource related to this object. + Activity *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"activity,omitempty"` + + // Approve A link to a resource related to this object. + Approve *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"approve,omitempty"` + + // Comments A link to a resource related to this object. + Comments *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"comments,omitempty"` + + // Commits A link to a resource related to this object. + Commits *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"commits,omitempty"` + + // Decline A link to a resource related to this object. + Decline *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"decline,omitempty"` + + // Diff A link to a resource related to this object. + Diff *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"diff,omitempty"` + + // Diffstat A link to a resource related to this object. + Diffstat *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"diffstat,omitempty"` + + // Html A link to a resource related to this object. + Html *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"html,omitempty"` + + // Merge A link to a resource related to this object. + Merge *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"merge,omitempty"` + + // Self A link to a resource related to this object. + Self *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"self,omitempty"` + } `json:"links,omitempty"` + MergeCommit *struct { + Hash *string `json:"hash,omitempty"` + } `json:"merge_commit,omitempty"` + + // Participants The list of users that are collaborating on this pull request. + // Collaborators are user that: + // + // * are added to the pull request as a reviewer (part of the reviewers + // list) + // * are not explicit reviewers, but have commented on the pull request + // * are not explicit reviewers, but have approved the pull request + // + // Each user is wrapped in an object that indicates the user's role and + // whether they have approved the pull request. For performance reasons, + // the API only returns this list when an API requests a pull request by + // id. + // + Participants *[]Participant `json:"participants,omitempty"` + + // Queued A boolean flag indicating whether the pull request is queued + Queued *bool `json:"queued,omitempty"` + + // Reason Explains why a pull request was declined. This field is only applicable to pull requests in rejected state. + Reason *string `json:"reason,omitempty"` + + // Rendered User provided pull request text, interpreted in a markup language and rendered in HTML + Rendered *struct { + Description *struct { + // Html The user's content rendered as HTML. + Html *string `json:"html,omitempty"` + + // Markup The type of markup language the raw content is to be interpreted in. + Markup *PullrequestRenderedDescriptionMarkup `json:"markup,omitempty"` + + // Raw The text as it was typed by a user. + Raw *string `json:"raw,omitempty"` + } `json:"description,omitempty"` + Reason *struct { + // Html The user's content rendered as HTML. + Html *string `json:"html,omitempty"` + + // Markup The type of markup language the raw content is to be interpreted in. + Markup *PullrequestRenderedReasonMarkup `json:"markup,omitempty"` + + // Raw The text as it was typed by a user. + Raw *string `json:"raw,omitempty"` + } `json:"reason,omitempty"` + Title *struct { + // Html The user's content rendered as HTML. + Html *string `json:"html,omitempty"` + + // Markup The type of markup language the raw content is to be interpreted in. + Markup *PullrequestRenderedTitleMarkup `json:"markup,omitempty"` + + // Raw The text as it was typed by a user. + Raw *string `json:"raw,omitempty"` + } `json:"title,omitempty"` + } `json:"rendered,omitempty"` + + // Reviewers The list of users that were added as reviewers on this pull request when it was created. For performance reasons, the API only includes this list on a pull request's `self` URL. + Reviewers *[]Account `json:"reviewers,omitempty"` + Source *PullrequestEndpoint `json:"source,omitempty"` + + // State The pull request's current status. + State *PullrequestState `json:"state,omitempty"` + Summary *struct { + // Html The user's content rendered as HTML. + Html *string `json:"html,omitempty"` + + // Markup The type of markup language the raw content is to be interpreted in. + Markup *PullrequestSummaryMarkup `json:"markup,omitempty"` + + // Raw The text as it was typed by a user. + Raw *string `json:"raw,omitempty"` + } `json:"summary,omitempty"` + + // TaskCount The number of open tasks for a specific pull request. + TaskCount *int `json:"task_count,omitempty"` + + // Title Title of the pull request. + Title *string `json:"title,omitempty"` + Type string `json:"type"` + + // UpdatedOn The ISO8601 timestamp the request was last updated. + UpdatedOn *time.Time `json:"updated_on,omitempty"` + AdditionalProperties map[string]interface{} `json:"-"` +} + +// PullrequestRenderedDescriptionMarkup The type of markup language the raw content is to be interpreted in. +type PullrequestRenderedDescriptionMarkup string + +// PullrequestRenderedReasonMarkup The type of markup language the raw content is to be interpreted in. +type PullrequestRenderedReasonMarkup string + +// PullrequestRenderedTitleMarkup The type of markup language the raw content is to be interpreted in. +type PullrequestRenderedTitleMarkup string + +// PullrequestState The pull request's current status. +type PullrequestState string + +// PullrequestSummaryMarkup The type of markup language the raw content is to be interpreted in. +type PullrequestSummaryMarkup string + +// PullrequestEndpoint defines model for pullrequest_endpoint. +type PullrequestEndpoint struct { + Branch *struct { + // DefaultMergeStrategy The default merge strategy, when this endpoint is the destination of the pull request. + DefaultMergeStrategy *string `json:"default_merge_strategy,omitempty"` + + // MergeStrategies Available merge strategies, when this endpoint is the destination of the pull request. + MergeStrategies *[]PullrequestEndpointBranchMergeStrategies `json:"merge_strategies,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"branch,omitempty"` + Commit *struct { + Hash *string `json:"hash,omitempty"` + } `json:"commit,omitempty"` + Repository *Repository `json:"repository,omitempty"` +} + +// PullrequestEndpointBranchMergeStrategies defines model for PullrequestEndpoint.Branch.MergeStrategies. +type PullrequestEndpointBranchMergeStrategies string + +// PullrequestMergeParameters The metadata that describes a pull request merge. +type PullrequestMergeParameters struct { + // CloseSourceBranch Whether the source branch should be deleted. If this is not provided, we fallback to the value used when the pull request was created, which defaults to False + CloseSourceBranch *bool `json:"close_source_branch,omitempty"` + + // MergeStrategy The merge strategy that will be used to merge the pull request. + MergeStrategy *PullrequestMergeParametersMergeStrategy `json:"merge_strategy,omitempty"` + + // Message The commit message that will be used on the resulting commit. Note that the size of the message is limited to 128 KiB. + Message *string `json:"message,omitempty"` + Type string `json:"type"` + AdditionalProperties map[string]interface{} `json:"-"` +} + +// PullrequestMergeParametersMergeStrategy The merge strategy that will be used to merge the pull request. +type PullrequestMergeParametersMergeStrategy string + +// Ref A ref object, representing a branch or tag in a repository. +type Ref struct { + Links *struct { + // Commits A link to a resource related to this object. + Commits *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"commits,omitempty"` + + // Html A link to a resource related to this object. + Html *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"html,omitempty"` + + // Self A link to a resource related to this object. + Self *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"self,omitempty"` + } `json:"links,omitempty"` + + // Name The name of the ref. + Name *string `json:"name,omitempty"` + Target *Commit `json:"target,omitempty"` + Type string `json:"type"` + AdditionalProperties map[string]interface{} `json:"-"` +} + +// Repository defines model for repository. +type Repository struct { + CreatedOn *time.Time `json:"created_on,omitempty"` + Description *string `json:"description,omitempty"` + + // ForkPolicy + // Controls the rules for forking this repository. + // + // * **allow_forks**: unrestricted forking + // * **no_public_forks**: restrict forking to private forks (forks cannot + // be made public later) + // * **no_forks**: deny all forking + ForkPolicy *RepositoryForkPolicy `json:"fork_policy,omitempty"` + + // FullName The concatenation of the repository owner's username and the slugified name, e.g. "evzijst/interruptingcow". This is the same string used in Bitbucket URLs. + FullName *string `json:"full_name,omitempty"` + + // HasIssues + // The issue tracker for this repository is enabled. Issue Tracker + // features are not supported for repositories in workspaces + // administered through admin.atlassian.com. + HasIssues *bool `json:"has_issues,omitempty"` + + // HasWiki + // The wiki for this repository is enabled. Wiki + // features are not supported for repositories in workspaces + // administered through admin.atlassian.com. + HasWiki *bool `json:"has_wiki,omitempty"` + IsPrivate *bool `json:"is_private,omitempty"` + Language *string `json:"language,omitempty"` + Links *struct { + // Avatar A link to a resource related to this object. + Avatar *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"avatar,omitempty"` + Clone *[]struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"clone,omitempty"` + + // Commits A link to a resource related to this object. + Commits *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"commits,omitempty"` + + // Downloads A link to a resource related to this object. + Downloads *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"downloads,omitempty"` + + // Forks A link to a resource related to this object. + Forks *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"forks,omitempty"` + + // Hooks A link to a resource related to this object. + Hooks *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"hooks,omitempty"` + + // Html A link to a resource related to this object. + Html *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"html,omitempty"` + + // Pullrequests A link to a resource related to this object. + Pullrequests *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"pullrequests,omitempty"` + + // Self A link to a resource related to this object. + Self *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"self,omitempty"` + + // Watchers A link to a resource related to this object. + Watchers *struct { + Href *string `json:"href,omitempty"` + Name *string `json:"name,omitempty"` + } `json:"watchers,omitempty"` + } `json:"links,omitempty"` + Mainbranch *Branch `json:"mainbranch,omitempty"` + Name *string `json:"name,omitempty"` + Owner *Account `json:"owner,omitempty"` + Parent *Repository `json:"parent,omitempty"` + Project *Project `json:"project,omitempty"` + Scm *RepositoryScm `json:"scm,omitempty"` + Size *int `json:"size,omitempty"` + Type string `json:"type"` + UpdatedOn *time.Time `json:"updated_on,omitempty"` + + // Uuid The repository's immutable id. This can be used as a substitute for the slug segment in URLs. Doing this guarantees your URLs will survive renaming of the repository by its owner, or even transfer of the repository to a different user. + Uuid *string `json:"uuid,omitempty"` + AdditionalProperties map[string]interface{} `json:"-"` +} + +// RepositoryForkPolicy +// Controls the rules for forking this repository. +// +// - **allow_forks**: unrestricted forking +// - **no_public_forks**: restrict forking to private forks (forks cannot +// be made public later) +// - **no_forks**: deny all forking +type RepositoryForkPolicy string + +// RepositoryScm defines model for Repository.Scm. +type RepositoryScm string + +// Team defines model for team. +type Team struct { + CreatedOn *time.Time `json:"created_on,omitempty"` + DisplayName *string `json:"display_name,omitempty"` + Links *TeamLinks `json:"links,omitempty"` + Type string `json:"type"` + Uuid *string `json:"uuid,omitempty"` + AdditionalProperties map[string]interface{} `json:"-"` +} + +// TeamLinks defines model for team_links. +type TeamLinks struct { + // Avatar A link to a resource related to this object. + Avatar *Link `json:"avatar,omitempty"` + + // Html A link to a resource related to this object. + Html *Link `json:"html,omitempty"` + + // Members A link to a resource related to this object. + Members *Link `json:"members,omitempty"` + + // Projects A link to a resource related to this object. + Projects *Link `json:"projects,omitempty"` + + // Repositories A link to a resource related to this object. + Repositories *Link `json:"repositories,omitempty"` + + // Self A link to a resource related to this object. + Self *Link `json:"self,omitempty"` + AdditionalProperties map[string]interface{} `json:"-"` +} + +// GetRepositoriesWorkspaceRepoSlugPullrequestsParams defines parameters for GetRepositoriesWorkspaceRepoSlugPullrequests. +type GetRepositoriesWorkspaceRepoSlugPullrequestsParams struct { + // State Only return pull requests that are in this state. This parameter can be repeated. + State *GetRepositoriesWorkspaceRepoSlugPullrequestsParamsState `form:"state,omitempty" json:"state,omitempty"` +} + +// GetRepositoriesWorkspaceRepoSlugPullrequestsParamsState defines parameters for GetRepositoriesWorkspaceRepoSlugPullrequests. +type GetRepositoriesWorkspaceRepoSlugPullrequestsParamsState string + +// PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams defines parameters for PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMerge. +type PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams struct { + // Async Default value is false. + // + // + // When set to true, runs merge asynchronously and + // immediately returns a 202 with polling link to + // the task-status API in the Location header. + // + // + // When set to false, runs merge and waits for it to + // complete, returning 200 when it succeeds. If the + // duration of the merge exceeds a timeout threshold, + // the API returns a 202 with polling link to the + // task-status API in the Location header. + Async *bool `form:"async,omitempty" json:"async,omitempty"` +} + +// PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody defines body for PostRepositoriesWorkspaceRepoSlugPullrequests for application/json ContentType. +type PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody = Pullrequest + +// PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeJSONRequestBody defines body for PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMerge for application/json ContentType. +type PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeJSONRequestBody = PullrequestMergeParameters + +// Getter for additional properties for Account. Returns the specified +// element and whether it was found +func (a Account) Get(fieldName string) (value interface{}, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for Account +func (a *Account) Set(fieldName string, value interface{}) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]interface{}) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for Account to handle AdditionalProperties +func (a *Account) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["created_on"]; found { + err = json.Unmarshal(raw, &a.CreatedOn) + if err != nil { + return fmt.Errorf("error reading 'created_on': %w", err) + } + delete(object, "created_on") + } + + if raw, found := object["display_name"]; found { + err = json.Unmarshal(raw, &a.DisplayName) + if err != nil { + return fmt.Errorf("error reading 'display_name': %w", err) + } + delete(object, "display_name") + } + + if raw, found := object["links"]; found { + err = json.Unmarshal(raw, &a.Links) + if err != nil { + return fmt.Errorf("error reading 'links': %w", err) + } + delete(object, "links") + } + + if raw, found := object["type"]; found { + err = json.Unmarshal(raw, &a.Type) + if err != nil { + return fmt.Errorf("error reading 'type': %w", err) + } + delete(object, "type") + } + + if raw, found := object["uuid"]; found { + err = json.Unmarshal(raw, &a.Uuid) + if err != nil { + return fmt.Errorf("error reading 'uuid': %w", err) + } + delete(object, "uuid") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]interface{}) + for fieldName, fieldBuf := range object { + var fieldVal interface{} + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for Account to handle AdditionalProperties +func (a Account) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.CreatedOn != nil { + object["created_on"], err = json.Marshal(a.CreatedOn) + if err != nil { + return nil, fmt.Errorf("error marshaling 'created_on': %w", err) + } + } + + if a.DisplayName != nil { + object["display_name"], err = json.Marshal(a.DisplayName) + if err != nil { + return nil, fmt.Errorf("error marshaling 'display_name': %w", err) + } + } + + if a.Links != nil { + object["links"], err = json.Marshal(a.Links) + if err != nil { + return nil, fmt.Errorf("error marshaling 'links': %w", err) + } + } + + object["type"], err = json.Marshal(a.Type) + if err != nil { + return nil, fmt.Errorf("error marshaling 'type': %w", err) + } + + if a.Uuid != nil { + object["uuid"], err = json.Marshal(a.Uuid) + if err != nil { + return nil, fmt.Errorf("error marshaling 'uuid': %w", err) + } + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + +// Getter for additional properties for AccountLinks. Returns the specified +// element and whether it was found +func (a AccountLinks) Get(fieldName string) (value interface{}, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for AccountLinks +func (a *AccountLinks) Set(fieldName string, value interface{}) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]interface{}) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for AccountLinks to handle AdditionalProperties +func (a *AccountLinks) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["avatar"]; found { + err = json.Unmarshal(raw, &a.Avatar) + if err != nil { + return fmt.Errorf("error reading 'avatar': %w", err) + } + delete(object, "avatar") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]interface{}) + for fieldName, fieldBuf := range object { + var fieldVal interface{} + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for AccountLinks to handle AdditionalProperties +func (a AccountLinks) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.Avatar != nil { + object["avatar"], err = json.Marshal(a.Avatar) + if err != nil { + return nil, fmt.Errorf("error marshaling 'avatar': %w", err) + } + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + +// Getter for additional properties for Author. Returns the specified +// element and whether it was found +func (a Author) Get(fieldName string) (value interface{}, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for Author +func (a *Author) Set(fieldName string, value interface{}) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]interface{}) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for Author to handle AdditionalProperties +func (a *Author) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["raw"]; found { + err = json.Unmarshal(raw, &a.Raw) + if err != nil { + return fmt.Errorf("error reading 'raw': %w", err) + } + delete(object, "raw") + } + + if raw, found := object["type"]; found { + err = json.Unmarshal(raw, &a.Type) + if err != nil { + return fmt.Errorf("error reading 'type': %w", err) + } + delete(object, "type") + } + + if raw, found := object["user"]; found { + err = json.Unmarshal(raw, &a.User) + if err != nil { + return fmt.Errorf("error reading 'user': %w", err) + } + delete(object, "user") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]interface{}) + for fieldName, fieldBuf := range object { + var fieldVal interface{} + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for Author to handle AdditionalProperties +func (a Author) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.Raw != nil { + object["raw"], err = json.Marshal(a.Raw) + if err != nil { + return nil, fmt.Errorf("error marshaling 'raw': %w", err) + } + } + + object["type"], err = json.Marshal(a.Type) + if err != nil { + return nil, fmt.Errorf("error marshaling 'type': %w", err) + } + + if a.User != nil { + object["user"], err = json.Marshal(a.User) + if err != nil { + return nil, fmt.Errorf("error marshaling 'user': %w", err) + } + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + +// Getter for additional properties for BaseCommit. Returns the specified +// element and whether it was found +func (a BaseCommit) Get(fieldName string) (value interface{}, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for BaseCommit +func (a *BaseCommit) Set(fieldName string, value interface{}) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]interface{}) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for BaseCommit to handle AdditionalProperties +func (a *BaseCommit) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["author"]; found { + err = json.Unmarshal(raw, &a.Author) + if err != nil { + return fmt.Errorf("error reading 'author': %w", err) + } + delete(object, "author") + } + + if raw, found := object["committer"]; found { + err = json.Unmarshal(raw, &a.Committer) + if err != nil { + return fmt.Errorf("error reading 'committer': %w", err) + } + delete(object, "committer") + } + + if raw, found := object["date"]; found { + err = json.Unmarshal(raw, &a.Date) + if err != nil { + return fmt.Errorf("error reading 'date': %w", err) + } + delete(object, "date") + } + + if raw, found := object["hash"]; found { + err = json.Unmarshal(raw, &a.Hash) + if err != nil { + return fmt.Errorf("error reading 'hash': %w", err) + } + delete(object, "hash") + } + + if raw, found := object["message"]; found { + err = json.Unmarshal(raw, &a.Message) + if err != nil { + return fmt.Errorf("error reading 'message': %w", err) + } + delete(object, "message") + } + + if raw, found := object["parents"]; found { + err = json.Unmarshal(raw, &a.Parents) + if err != nil { + return fmt.Errorf("error reading 'parents': %w", err) + } + delete(object, "parents") + } + + if raw, found := object["summary"]; found { + err = json.Unmarshal(raw, &a.Summary) + if err != nil { + return fmt.Errorf("error reading 'summary': %w", err) + } + delete(object, "summary") + } + + if raw, found := object["type"]; found { + err = json.Unmarshal(raw, &a.Type) + if err != nil { + return fmt.Errorf("error reading 'type': %w", err) + } + delete(object, "type") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]interface{}) + for fieldName, fieldBuf := range object { + var fieldVal interface{} + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for BaseCommit to handle AdditionalProperties +func (a BaseCommit) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.Author != nil { + object["author"], err = json.Marshal(a.Author) + if err != nil { + return nil, fmt.Errorf("error marshaling 'author': %w", err) + } + } + + if a.Committer != nil { + object["committer"], err = json.Marshal(a.Committer) + if err != nil { + return nil, fmt.Errorf("error marshaling 'committer': %w", err) + } + } + + if a.Date != nil { + object["date"], err = json.Marshal(a.Date) + if err != nil { + return nil, fmt.Errorf("error marshaling 'date': %w", err) + } + } + + if a.Hash != nil { + object["hash"], err = json.Marshal(a.Hash) + if err != nil { + return nil, fmt.Errorf("error marshaling 'hash': %w", err) + } + } + + if a.Message != nil { + object["message"], err = json.Marshal(a.Message) + if err != nil { + return nil, fmt.Errorf("error marshaling 'message': %w", err) + } + } + + if a.Parents != nil { + object["parents"], err = json.Marshal(a.Parents) + if err != nil { + return nil, fmt.Errorf("error marshaling 'parents': %w", err) + } + } + + if a.Summary != nil { + object["summary"], err = json.Marshal(a.Summary) + if err != nil { + return nil, fmt.Errorf("error marshaling 'summary': %w", err) + } + } + + object["type"], err = json.Marshal(a.Type) + if err != nil { + return nil, fmt.Errorf("error marshaling 'type': %w", err) + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + +// Getter for additional properties for Branch. Returns the specified +// element and whether it was found +func (a Branch) Get(fieldName string) (value interface{}, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for Branch +func (a *Branch) Set(fieldName string, value interface{}) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]interface{}) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for Branch to handle AdditionalProperties +func (a *Branch) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["default_merge_strategy"]; found { + err = json.Unmarshal(raw, &a.DefaultMergeStrategy) + if err != nil { + return fmt.Errorf("error reading 'default_merge_strategy': %w", err) + } + delete(object, "default_merge_strategy") + } + + if raw, found := object["links"]; found { + err = json.Unmarshal(raw, &a.Links) + if err != nil { + return fmt.Errorf("error reading 'links': %w", err) + } + delete(object, "links") + } + + if raw, found := object["merge_strategies"]; found { + err = json.Unmarshal(raw, &a.MergeStrategies) + if err != nil { + return fmt.Errorf("error reading 'merge_strategies': %w", err) + } + delete(object, "merge_strategies") + } + + if raw, found := object["name"]; found { + err = json.Unmarshal(raw, &a.Name) + if err != nil { + return fmt.Errorf("error reading 'name': %w", err) + } + delete(object, "name") + } + + if raw, found := object["target"]; found { + err = json.Unmarshal(raw, &a.Target) + if err != nil { + return fmt.Errorf("error reading 'target': %w", err) + } + delete(object, "target") + } + + if raw, found := object["type"]; found { + err = json.Unmarshal(raw, &a.Type) + if err != nil { + return fmt.Errorf("error reading 'type': %w", err) + } + delete(object, "type") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]interface{}) + for fieldName, fieldBuf := range object { + var fieldVal interface{} + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for Branch to handle AdditionalProperties +func (a Branch) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.DefaultMergeStrategy != nil { + object["default_merge_strategy"], err = json.Marshal(a.DefaultMergeStrategy) + if err != nil { + return nil, fmt.Errorf("error marshaling 'default_merge_strategy': %w", err) + } + } + + if a.Links != nil { + object["links"], err = json.Marshal(a.Links) + if err != nil { + return nil, fmt.Errorf("error marshaling 'links': %w", err) + } + } + + if a.MergeStrategies != nil { + object["merge_strategies"], err = json.Marshal(a.MergeStrategies) + if err != nil { + return nil, fmt.Errorf("error marshaling 'merge_strategies': %w", err) + } + } + + if a.Name != nil { + object["name"], err = json.Marshal(a.Name) + if err != nil { + return nil, fmt.Errorf("error marshaling 'name': %w", err) + } + } + + if a.Target != nil { + object["target"], err = json.Marshal(a.Target) + if err != nil { + return nil, fmt.Errorf("error marshaling 'target': %w", err) + } + } + + object["type"], err = json.Marshal(a.Type) + if err != nil { + return nil, fmt.Errorf("error marshaling 'type': %w", err) + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + +// Getter for additional properties for Commit. Returns the specified +// element and whether it was found +func (a Commit) Get(fieldName string) (value interface{}, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for Commit +func (a *Commit) Set(fieldName string, value interface{}) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]interface{}) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for Commit to handle AdditionalProperties +func (a *Commit) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["author"]; found { + err = json.Unmarshal(raw, &a.Author) + if err != nil { + return fmt.Errorf("error reading 'author': %w", err) + } + delete(object, "author") + } + + if raw, found := object["committer"]; found { + err = json.Unmarshal(raw, &a.Committer) + if err != nil { + return fmt.Errorf("error reading 'committer': %w", err) + } + delete(object, "committer") + } + + if raw, found := object["date"]; found { + err = json.Unmarshal(raw, &a.Date) + if err != nil { + return fmt.Errorf("error reading 'date': %w", err) + } + delete(object, "date") + } + + if raw, found := object["hash"]; found { + err = json.Unmarshal(raw, &a.Hash) + if err != nil { + return fmt.Errorf("error reading 'hash': %w", err) + } + delete(object, "hash") + } + + if raw, found := object["message"]; found { + err = json.Unmarshal(raw, &a.Message) + if err != nil { + return fmt.Errorf("error reading 'message': %w", err) + } + delete(object, "message") + } + + if raw, found := object["parents"]; found { + err = json.Unmarshal(raw, &a.Parents) + if err != nil { + return fmt.Errorf("error reading 'parents': %w", err) + } + delete(object, "parents") + } + + if raw, found := object["participants"]; found { + err = json.Unmarshal(raw, &a.Participants) + if err != nil { + return fmt.Errorf("error reading 'participants': %w", err) + } + delete(object, "participants") + } + + if raw, found := object["repository"]; found { + err = json.Unmarshal(raw, &a.Repository) + if err != nil { + return fmt.Errorf("error reading 'repository': %w", err) + } + delete(object, "repository") + } + + if raw, found := object["summary"]; found { + err = json.Unmarshal(raw, &a.Summary) + if err != nil { + return fmt.Errorf("error reading 'summary': %w", err) + } + delete(object, "summary") + } + + if raw, found := object["type"]; found { + err = json.Unmarshal(raw, &a.Type) + if err != nil { + return fmt.Errorf("error reading 'type': %w", err) + } + delete(object, "type") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]interface{}) + for fieldName, fieldBuf := range object { + var fieldVal interface{} + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for Commit to handle AdditionalProperties +func (a Commit) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.Author != nil { + object["author"], err = json.Marshal(a.Author) + if err != nil { + return nil, fmt.Errorf("error marshaling 'author': %w", err) + } + } + + if a.Committer != nil { + object["committer"], err = json.Marshal(a.Committer) + if err != nil { + return nil, fmt.Errorf("error marshaling 'committer': %w", err) + } + } + + if a.Date != nil { + object["date"], err = json.Marshal(a.Date) + if err != nil { + return nil, fmt.Errorf("error marshaling 'date': %w", err) + } + } + + if a.Hash != nil { + object["hash"], err = json.Marshal(a.Hash) + if err != nil { + return nil, fmt.Errorf("error marshaling 'hash': %w", err) + } + } + + if a.Message != nil { + object["message"], err = json.Marshal(a.Message) + if err != nil { + return nil, fmt.Errorf("error marshaling 'message': %w", err) + } + } + + if a.Parents != nil { + object["parents"], err = json.Marshal(a.Parents) + if err != nil { + return nil, fmt.Errorf("error marshaling 'parents': %w", err) + } + } + + if a.Participants != nil { + object["participants"], err = json.Marshal(a.Participants) + if err != nil { + return nil, fmt.Errorf("error marshaling 'participants': %w", err) + } + } + + if a.Repository != nil { + object["repository"], err = json.Marshal(a.Repository) + if err != nil { + return nil, fmt.Errorf("error marshaling 'repository': %w", err) + } + } + + if a.Summary != nil { + object["summary"], err = json.Marshal(a.Summary) + if err != nil { + return nil, fmt.Errorf("error marshaling 'summary': %w", err) + } + } + + object["type"], err = json.Marshal(a.Type) + if err != nil { + return nil, fmt.Errorf("error marshaling 'type': %w", err) + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + +// Getter for additional properties for Committer. Returns the specified +// element and whether it was found +func (a Committer) Get(fieldName string) (value interface{}, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for Committer +func (a *Committer) Set(fieldName string, value interface{}) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]interface{}) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for Committer to handle AdditionalProperties +func (a *Committer) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["raw"]; found { + err = json.Unmarshal(raw, &a.Raw) + if err != nil { + return fmt.Errorf("error reading 'raw': %w", err) + } + delete(object, "raw") + } + + if raw, found := object["type"]; found { + err = json.Unmarshal(raw, &a.Type) + if err != nil { + return fmt.Errorf("error reading 'type': %w", err) + } + delete(object, "type") + } + + if raw, found := object["user"]; found { + err = json.Unmarshal(raw, &a.User) + if err != nil { + return fmt.Errorf("error reading 'user': %w", err) + } + delete(object, "user") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]interface{}) + for fieldName, fieldBuf := range object { + var fieldVal interface{} + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for Committer to handle AdditionalProperties +func (a Committer) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.Raw != nil { + object["raw"], err = json.Marshal(a.Raw) + if err != nil { + return nil, fmt.Errorf("error marshaling 'raw': %w", err) + } + } + + object["type"], err = json.Marshal(a.Type) + if err != nil { + return nil, fmt.Errorf("error marshaling 'type': %w", err) + } + + if a.User != nil { + object["user"], err = json.Marshal(a.User) + if err != nil { + return nil, fmt.Errorf("error marshaling 'user': %w", err) + } + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + +// Getter for additional properties for Error. Returns the specified +// element and whether it was found +func (a Error) Get(fieldName string) (value interface{}, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for Error +func (a *Error) Set(fieldName string, value interface{}) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]interface{}) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for Error to handle AdditionalProperties +func (a *Error) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["error"]; found { + err = json.Unmarshal(raw, &a.Error) + if err != nil { + return fmt.Errorf("error reading 'error': %w", err) + } + delete(object, "error") + } + + if raw, found := object["type"]; found { + err = json.Unmarshal(raw, &a.Type) + if err != nil { + return fmt.Errorf("error reading 'type': %w", err) + } + delete(object, "type") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]interface{}) + for fieldName, fieldBuf := range object { + var fieldVal interface{} + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for Error to handle AdditionalProperties +func (a Error) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.Error != nil { + object["error"], err = json.Marshal(a.Error) + if err != nil { + return nil, fmt.Errorf("error marshaling 'error': %w", err) + } + } + + object["type"], err = json.Marshal(a.Type) + if err != nil { + return nil, fmt.Errorf("error marshaling 'type': %w", err) + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + +// Getter for additional properties for Object. Returns the specified +// element and whether it was found +func (a Object) Get(fieldName string) (value interface{}, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for Object +func (a *Object) Set(fieldName string, value interface{}) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]interface{}) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for Object to handle AdditionalProperties +func (a *Object) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["type"]; found { + err = json.Unmarshal(raw, &a.Type) + if err != nil { + return fmt.Errorf("error reading 'type': %w", err) + } + delete(object, "type") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]interface{}) + for fieldName, fieldBuf := range object { + var fieldVal interface{} + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for Object to handle AdditionalProperties +func (a Object) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + object["type"], err = json.Marshal(a.Type) + if err != nil { + return nil, fmt.Errorf("error marshaling 'type': %w", err) + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + +// Getter for additional properties for Participant. Returns the specified +// element and whether it was found +func (a Participant) Get(fieldName string) (value interface{}, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for Participant +func (a *Participant) Set(fieldName string, value interface{}) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]interface{}) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for Participant to handle AdditionalProperties +func (a *Participant) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["approved"]; found { + err = json.Unmarshal(raw, &a.Approved) + if err != nil { + return fmt.Errorf("error reading 'approved': %w", err) + } + delete(object, "approved") + } + + if raw, found := object["participated_on"]; found { + err = json.Unmarshal(raw, &a.ParticipatedOn) + if err != nil { + return fmt.Errorf("error reading 'participated_on': %w", err) + } + delete(object, "participated_on") + } + + if raw, found := object["role"]; found { + err = json.Unmarshal(raw, &a.Role) + if err != nil { + return fmt.Errorf("error reading 'role': %w", err) + } + delete(object, "role") + } + + if raw, found := object["state"]; found { + err = json.Unmarshal(raw, &a.State) + if err != nil { + return fmt.Errorf("error reading 'state': %w", err) + } + delete(object, "state") + } + + if raw, found := object["type"]; found { + err = json.Unmarshal(raw, &a.Type) + if err != nil { + return fmt.Errorf("error reading 'type': %w", err) + } + delete(object, "type") + } + + if raw, found := object["user"]; found { + err = json.Unmarshal(raw, &a.User) + if err != nil { + return fmt.Errorf("error reading 'user': %w", err) + } + delete(object, "user") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]interface{}) + for fieldName, fieldBuf := range object { + var fieldVal interface{} + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for Participant to handle AdditionalProperties +func (a Participant) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.Approved != nil { + object["approved"], err = json.Marshal(a.Approved) + if err != nil { + return nil, fmt.Errorf("error marshaling 'approved': %w", err) + } + } + + if a.ParticipatedOn != nil { + object["participated_on"], err = json.Marshal(a.ParticipatedOn) + if err != nil { + return nil, fmt.Errorf("error marshaling 'participated_on': %w", err) + } + } + + if a.Role != nil { + object["role"], err = json.Marshal(a.Role) + if err != nil { + return nil, fmt.Errorf("error marshaling 'role': %w", err) + } + } + + if a.State != nil { + object["state"], err = json.Marshal(a.State) + if err != nil { + return nil, fmt.Errorf("error marshaling 'state': %w", err) + } + } + + object["type"], err = json.Marshal(a.Type) + if err != nil { + return nil, fmt.Errorf("error marshaling 'type': %w", err) + } + + if a.User != nil { + object["user"], err = json.Marshal(a.User) + if err != nil { + return nil, fmt.Errorf("error marshaling 'user': %w", err) + } + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + +// Getter for additional properties for Project. Returns the specified +// element and whether it was found +func (a Project) Get(fieldName string) (value interface{}, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for Project +func (a *Project) Set(fieldName string, value interface{}) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]interface{}) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for Project to handle AdditionalProperties +func (a *Project) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["created_on"]; found { + err = json.Unmarshal(raw, &a.CreatedOn) + if err != nil { + return fmt.Errorf("error reading 'created_on': %w", err) + } + delete(object, "created_on") + } + + if raw, found := object["description"]; found { + err = json.Unmarshal(raw, &a.Description) + if err != nil { + return fmt.Errorf("error reading 'description': %w", err) + } + delete(object, "description") + } + + if raw, found := object["has_publicly_visible_repos"]; found { + err = json.Unmarshal(raw, &a.HasPubliclyVisibleRepos) + if err != nil { + return fmt.Errorf("error reading 'has_publicly_visible_repos': %w", err) + } + delete(object, "has_publicly_visible_repos") + } + + if raw, found := object["is_private"]; found { + err = json.Unmarshal(raw, &a.IsPrivate) + if err != nil { + return fmt.Errorf("error reading 'is_private': %w", err) + } + delete(object, "is_private") + } + + if raw, found := object["key"]; found { + err = json.Unmarshal(raw, &a.Key) + if err != nil { + return fmt.Errorf("error reading 'key': %w", err) + } + delete(object, "key") + } + + if raw, found := object["links"]; found { + err = json.Unmarshal(raw, &a.Links) + if err != nil { + return fmt.Errorf("error reading 'links': %w", err) + } + delete(object, "links") + } + + if raw, found := object["name"]; found { + err = json.Unmarshal(raw, &a.Name) + if err != nil { + return fmt.Errorf("error reading 'name': %w", err) + } + delete(object, "name") + } + + if raw, found := object["owner"]; found { + err = json.Unmarshal(raw, &a.Owner) + if err != nil { + return fmt.Errorf("error reading 'owner': %w", err) + } + delete(object, "owner") + } + + if raw, found := object["type"]; found { + err = json.Unmarshal(raw, &a.Type) + if err != nil { + return fmt.Errorf("error reading 'type': %w", err) + } + delete(object, "type") + } + + if raw, found := object["updated_on"]; found { + err = json.Unmarshal(raw, &a.UpdatedOn) + if err != nil { + return fmt.Errorf("error reading 'updated_on': %w", err) + } + delete(object, "updated_on") + } + + if raw, found := object["uuid"]; found { + err = json.Unmarshal(raw, &a.Uuid) + if err != nil { + return fmt.Errorf("error reading 'uuid': %w", err) + } + delete(object, "uuid") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]interface{}) + for fieldName, fieldBuf := range object { + var fieldVal interface{} + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for Project to handle AdditionalProperties +func (a Project) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.CreatedOn != nil { + object["created_on"], err = json.Marshal(a.CreatedOn) + if err != nil { + return nil, fmt.Errorf("error marshaling 'created_on': %w", err) + } + } + + if a.Description != nil { + object["description"], err = json.Marshal(a.Description) + if err != nil { + return nil, fmt.Errorf("error marshaling 'description': %w", err) + } + } + + if a.HasPubliclyVisibleRepos != nil { + object["has_publicly_visible_repos"], err = json.Marshal(a.HasPubliclyVisibleRepos) + if err != nil { + return nil, fmt.Errorf("error marshaling 'has_publicly_visible_repos': %w", err) + } + } + + if a.IsPrivate != nil { + object["is_private"], err = json.Marshal(a.IsPrivate) + if err != nil { + return nil, fmt.Errorf("error marshaling 'is_private': %w", err) + } + } + + if a.Key != nil { + object["key"], err = json.Marshal(a.Key) + if err != nil { + return nil, fmt.Errorf("error marshaling 'key': %w", err) + } + } + + if a.Links != nil { + object["links"], err = json.Marshal(a.Links) + if err != nil { + return nil, fmt.Errorf("error marshaling 'links': %w", err) + } + } + + if a.Name != nil { + object["name"], err = json.Marshal(a.Name) + if err != nil { + return nil, fmt.Errorf("error marshaling 'name': %w", err) + } + } + + if a.Owner != nil { + object["owner"], err = json.Marshal(a.Owner) + if err != nil { + return nil, fmt.Errorf("error marshaling 'owner': %w", err) + } + } + + object["type"], err = json.Marshal(a.Type) + if err != nil { + return nil, fmt.Errorf("error marshaling 'type': %w", err) + } + + if a.UpdatedOn != nil { + object["updated_on"], err = json.Marshal(a.UpdatedOn) + if err != nil { + return nil, fmt.Errorf("error marshaling 'updated_on': %w", err) + } + } + + if a.Uuid != nil { + object["uuid"], err = json.Marshal(a.Uuid) + if err != nil { + return nil, fmt.Errorf("error marshaling 'uuid': %w", err) + } + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + +// Getter for additional properties for Pullrequest. Returns the specified +// element and whether it was found +func (a Pullrequest) Get(fieldName string) (value interface{}, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for Pullrequest +func (a *Pullrequest) Set(fieldName string, value interface{}) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]interface{}) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for Pullrequest to handle AdditionalProperties +func (a *Pullrequest) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["author"]; found { + err = json.Unmarshal(raw, &a.Author) + if err != nil { + return fmt.Errorf("error reading 'author': %w", err) + } + delete(object, "author") + } + + if raw, found := object["close_source_branch"]; found { + err = json.Unmarshal(raw, &a.CloseSourceBranch) + if err != nil { + return fmt.Errorf("error reading 'close_source_branch': %w", err) + } + delete(object, "close_source_branch") + } + + if raw, found := object["closed_by"]; found { + err = json.Unmarshal(raw, &a.ClosedBy) + if err != nil { + return fmt.Errorf("error reading 'closed_by': %w", err) + } + delete(object, "closed_by") + } + + if raw, found := object["comment_count"]; found { + err = json.Unmarshal(raw, &a.CommentCount) + if err != nil { + return fmt.Errorf("error reading 'comment_count': %w", err) + } + delete(object, "comment_count") + } + + if raw, found := object["created_on"]; found { + err = json.Unmarshal(raw, &a.CreatedOn) + if err != nil { + return fmt.Errorf("error reading 'created_on': %w", err) + } + delete(object, "created_on") + } + + if raw, found := object["destination"]; found { + err = json.Unmarshal(raw, &a.Destination) + if err != nil { + return fmt.Errorf("error reading 'destination': %w", err) + } + delete(object, "destination") + } + + if raw, found := object["draft"]; found { + err = json.Unmarshal(raw, &a.Draft) + if err != nil { + return fmt.Errorf("error reading 'draft': %w", err) + } + delete(object, "draft") + } + + if raw, found := object["id"]; found { + err = json.Unmarshal(raw, &a.Id) + if err != nil { + return fmt.Errorf("error reading 'id': %w", err) + } + delete(object, "id") + } + + if raw, found := object["links"]; found { + err = json.Unmarshal(raw, &a.Links) + if err != nil { + return fmt.Errorf("error reading 'links': %w", err) + } + delete(object, "links") + } + + if raw, found := object["merge_commit"]; found { + err = json.Unmarshal(raw, &a.MergeCommit) + if err != nil { + return fmt.Errorf("error reading 'merge_commit': %w", err) + } + delete(object, "merge_commit") + } + + if raw, found := object["participants"]; found { + err = json.Unmarshal(raw, &a.Participants) + if err != nil { + return fmt.Errorf("error reading 'participants': %w", err) + } + delete(object, "participants") + } + + if raw, found := object["queued"]; found { + err = json.Unmarshal(raw, &a.Queued) + if err != nil { + return fmt.Errorf("error reading 'queued': %w", err) + } + delete(object, "queued") + } + + if raw, found := object["reason"]; found { + err = json.Unmarshal(raw, &a.Reason) + if err != nil { + return fmt.Errorf("error reading 'reason': %w", err) + } + delete(object, "reason") + } + + if raw, found := object["rendered"]; found { + err = json.Unmarshal(raw, &a.Rendered) + if err != nil { + return fmt.Errorf("error reading 'rendered': %w", err) + } + delete(object, "rendered") + } + + if raw, found := object["reviewers"]; found { + err = json.Unmarshal(raw, &a.Reviewers) + if err != nil { + return fmt.Errorf("error reading 'reviewers': %w", err) + } + delete(object, "reviewers") + } + + if raw, found := object["source"]; found { + err = json.Unmarshal(raw, &a.Source) + if err != nil { + return fmt.Errorf("error reading 'source': %w", err) + } + delete(object, "source") + } + + if raw, found := object["state"]; found { + err = json.Unmarshal(raw, &a.State) + if err != nil { + return fmt.Errorf("error reading 'state': %w", err) + } + delete(object, "state") + } + + if raw, found := object["summary"]; found { + err = json.Unmarshal(raw, &a.Summary) + if err != nil { + return fmt.Errorf("error reading 'summary': %w", err) + } + delete(object, "summary") + } + + if raw, found := object["task_count"]; found { + err = json.Unmarshal(raw, &a.TaskCount) + if err != nil { + return fmt.Errorf("error reading 'task_count': %w", err) + } + delete(object, "task_count") + } + + if raw, found := object["title"]; found { + err = json.Unmarshal(raw, &a.Title) + if err != nil { + return fmt.Errorf("error reading 'title': %w", err) + } + delete(object, "title") + } + + if raw, found := object["type"]; found { + err = json.Unmarshal(raw, &a.Type) + if err != nil { + return fmt.Errorf("error reading 'type': %w", err) + } + delete(object, "type") + } + + if raw, found := object["updated_on"]; found { + err = json.Unmarshal(raw, &a.UpdatedOn) + if err != nil { + return fmt.Errorf("error reading 'updated_on': %w", err) + } + delete(object, "updated_on") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]interface{}) + for fieldName, fieldBuf := range object { + var fieldVal interface{} + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for Pullrequest to handle AdditionalProperties +func (a Pullrequest) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.Author != nil { + object["author"], err = json.Marshal(a.Author) + if err != nil { + return nil, fmt.Errorf("error marshaling 'author': %w", err) + } + } + + if a.CloseSourceBranch != nil { + object["close_source_branch"], err = json.Marshal(a.CloseSourceBranch) + if err != nil { + return nil, fmt.Errorf("error marshaling 'close_source_branch': %w", err) + } + } + + if a.ClosedBy != nil { + object["closed_by"], err = json.Marshal(a.ClosedBy) + if err != nil { + return nil, fmt.Errorf("error marshaling 'closed_by': %w", err) + } + } + + if a.CommentCount != nil { + object["comment_count"], err = json.Marshal(a.CommentCount) + if err != nil { + return nil, fmt.Errorf("error marshaling 'comment_count': %w", err) + } + } + + if a.CreatedOn != nil { + object["created_on"], err = json.Marshal(a.CreatedOn) + if err != nil { + return nil, fmt.Errorf("error marshaling 'created_on': %w", err) + } + } + + if a.Destination != nil { + object["destination"], err = json.Marshal(a.Destination) + if err != nil { + return nil, fmt.Errorf("error marshaling 'destination': %w", err) + } + } + + if a.Draft != nil { + object["draft"], err = json.Marshal(a.Draft) + if err != nil { + return nil, fmt.Errorf("error marshaling 'draft': %w", err) + } + } + + if a.Id != nil { + object["id"], err = json.Marshal(a.Id) + if err != nil { + return nil, fmt.Errorf("error marshaling 'id': %w", err) + } + } + + if a.Links != nil { + object["links"], err = json.Marshal(a.Links) + if err != nil { + return nil, fmt.Errorf("error marshaling 'links': %w", err) + } + } + + if a.MergeCommit != nil { + object["merge_commit"], err = json.Marshal(a.MergeCommit) + if err != nil { + return nil, fmt.Errorf("error marshaling 'merge_commit': %w", err) + } + } + + if a.Participants != nil { + object["participants"], err = json.Marshal(a.Participants) + if err != nil { + return nil, fmt.Errorf("error marshaling 'participants': %w", err) + } + } + + if a.Queued != nil { + object["queued"], err = json.Marshal(a.Queued) + if err != nil { + return nil, fmt.Errorf("error marshaling 'queued': %w", err) + } + } + + if a.Reason != nil { + object["reason"], err = json.Marshal(a.Reason) + if err != nil { + return nil, fmt.Errorf("error marshaling 'reason': %w", err) + } + } + + if a.Rendered != nil { + object["rendered"], err = json.Marshal(a.Rendered) + if err != nil { + return nil, fmt.Errorf("error marshaling 'rendered': %w", err) + } + } + + if a.Reviewers != nil { + object["reviewers"], err = json.Marshal(a.Reviewers) + if err != nil { + return nil, fmt.Errorf("error marshaling 'reviewers': %w", err) + } + } + + if a.Source != nil { + object["source"], err = json.Marshal(a.Source) + if err != nil { + return nil, fmt.Errorf("error marshaling 'source': %w", err) + } + } + + if a.State != nil { + object["state"], err = json.Marshal(a.State) + if err != nil { + return nil, fmt.Errorf("error marshaling 'state': %w", err) + } + } + + if a.Summary != nil { + object["summary"], err = json.Marshal(a.Summary) + if err != nil { + return nil, fmt.Errorf("error marshaling 'summary': %w", err) + } + } + + if a.TaskCount != nil { + object["task_count"], err = json.Marshal(a.TaskCount) + if err != nil { + return nil, fmt.Errorf("error marshaling 'task_count': %w", err) + } + } + + if a.Title != nil { + object["title"], err = json.Marshal(a.Title) + if err != nil { + return nil, fmt.Errorf("error marshaling 'title': %w", err) + } + } + + object["type"], err = json.Marshal(a.Type) + if err != nil { + return nil, fmt.Errorf("error marshaling 'type': %w", err) + } + + if a.UpdatedOn != nil { + object["updated_on"], err = json.Marshal(a.UpdatedOn) + if err != nil { + return nil, fmt.Errorf("error marshaling 'updated_on': %w", err) + } + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + +// Getter for additional properties for PullrequestMergeParameters. Returns the specified +// element and whether it was found +func (a PullrequestMergeParameters) Get(fieldName string) (value interface{}, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for PullrequestMergeParameters +func (a *PullrequestMergeParameters) Set(fieldName string, value interface{}) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]interface{}) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for PullrequestMergeParameters to handle AdditionalProperties +func (a *PullrequestMergeParameters) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["close_source_branch"]; found { + err = json.Unmarshal(raw, &a.CloseSourceBranch) + if err != nil { + return fmt.Errorf("error reading 'close_source_branch': %w", err) + } + delete(object, "close_source_branch") + } + + if raw, found := object["merge_strategy"]; found { + err = json.Unmarshal(raw, &a.MergeStrategy) + if err != nil { + return fmt.Errorf("error reading 'merge_strategy': %w", err) + } + delete(object, "merge_strategy") + } + + if raw, found := object["message"]; found { + err = json.Unmarshal(raw, &a.Message) + if err != nil { + return fmt.Errorf("error reading 'message': %w", err) + } + delete(object, "message") + } + + if raw, found := object["type"]; found { + err = json.Unmarshal(raw, &a.Type) + if err != nil { + return fmt.Errorf("error reading 'type': %w", err) + } + delete(object, "type") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]interface{}) + for fieldName, fieldBuf := range object { + var fieldVal interface{} + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for PullrequestMergeParameters to handle AdditionalProperties +func (a PullrequestMergeParameters) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.CloseSourceBranch != nil { + object["close_source_branch"], err = json.Marshal(a.CloseSourceBranch) + if err != nil { + return nil, fmt.Errorf("error marshaling 'close_source_branch': %w", err) + } + } + + if a.MergeStrategy != nil { + object["merge_strategy"], err = json.Marshal(a.MergeStrategy) + if err != nil { + return nil, fmt.Errorf("error marshaling 'merge_strategy': %w", err) + } + } + + if a.Message != nil { + object["message"], err = json.Marshal(a.Message) + if err != nil { + return nil, fmt.Errorf("error marshaling 'message': %w", err) + } + } + + object["type"], err = json.Marshal(a.Type) + if err != nil { + return nil, fmt.Errorf("error marshaling 'type': %w", err) + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + +// Getter for additional properties for Ref. Returns the specified +// element and whether it was found +func (a Ref) Get(fieldName string) (value interface{}, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for Ref +func (a *Ref) Set(fieldName string, value interface{}) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]interface{}) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for Ref to handle AdditionalProperties +func (a *Ref) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["links"]; found { + err = json.Unmarshal(raw, &a.Links) + if err != nil { + return fmt.Errorf("error reading 'links': %w", err) + } + delete(object, "links") + } + + if raw, found := object["name"]; found { + err = json.Unmarshal(raw, &a.Name) + if err != nil { + return fmt.Errorf("error reading 'name': %w", err) + } + delete(object, "name") + } + + if raw, found := object["target"]; found { + err = json.Unmarshal(raw, &a.Target) + if err != nil { + return fmt.Errorf("error reading 'target': %w", err) + } + delete(object, "target") + } + + if raw, found := object["type"]; found { + err = json.Unmarshal(raw, &a.Type) + if err != nil { + return fmt.Errorf("error reading 'type': %w", err) + } + delete(object, "type") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]interface{}) + for fieldName, fieldBuf := range object { + var fieldVal interface{} + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for Ref to handle AdditionalProperties +func (a Ref) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.Links != nil { + object["links"], err = json.Marshal(a.Links) + if err != nil { + return nil, fmt.Errorf("error marshaling 'links': %w", err) + } + } + + if a.Name != nil { + object["name"], err = json.Marshal(a.Name) + if err != nil { + return nil, fmt.Errorf("error marshaling 'name': %w", err) + } + } + + if a.Target != nil { + object["target"], err = json.Marshal(a.Target) + if err != nil { + return nil, fmt.Errorf("error marshaling 'target': %w", err) + } + } + + object["type"], err = json.Marshal(a.Type) + if err != nil { + return nil, fmt.Errorf("error marshaling 'type': %w", err) + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + +// Getter for additional properties for Repository. Returns the specified +// element and whether it was found +func (a Repository) Get(fieldName string) (value interface{}, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for Repository +func (a *Repository) Set(fieldName string, value interface{}) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]interface{}) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for Repository to handle AdditionalProperties +func (a *Repository) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["created_on"]; found { + err = json.Unmarshal(raw, &a.CreatedOn) + if err != nil { + return fmt.Errorf("error reading 'created_on': %w", err) + } + delete(object, "created_on") + } + + if raw, found := object["description"]; found { + err = json.Unmarshal(raw, &a.Description) + if err != nil { + return fmt.Errorf("error reading 'description': %w", err) + } + delete(object, "description") + } + + if raw, found := object["fork_policy"]; found { + err = json.Unmarshal(raw, &a.ForkPolicy) + if err != nil { + return fmt.Errorf("error reading 'fork_policy': %w", err) + } + delete(object, "fork_policy") + } + + if raw, found := object["full_name"]; found { + err = json.Unmarshal(raw, &a.FullName) + if err != nil { + return fmt.Errorf("error reading 'full_name': %w", err) + } + delete(object, "full_name") + } + + if raw, found := object["has_issues"]; found { + err = json.Unmarshal(raw, &a.HasIssues) + if err != nil { + return fmt.Errorf("error reading 'has_issues': %w", err) + } + delete(object, "has_issues") + } + + if raw, found := object["has_wiki"]; found { + err = json.Unmarshal(raw, &a.HasWiki) + if err != nil { + return fmt.Errorf("error reading 'has_wiki': %w", err) + } + delete(object, "has_wiki") + } + + if raw, found := object["is_private"]; found { + err = json.Unmarshal(raw, &a.IsPrivate) + if err != nil { + return fmt.Errorf("error reading 'is_private': %w", err) + } + delete(object, "is_private") + } + + if raw, found := object["language"]; found { + err = json.Unmarshal(raw, &a.Language) + if err != nil { + return fmt.Errorf("error reading 'language': %w", err) + } + delete(object, "language") + } + + if raw, found := object["links"]; found { + err = json.Unmarshal(raw, &a.Links) + if err != nil { + return fmt.Errorf("error reading 'links': %w", err) + } + delete(object, "links") + } + + if raw, found := object["mainbranch"]; found { + err = json.Unmarshal(raw, &a.Mainbranch) + if err != nil { + return fmt.Errorf("error reading 'mainbranch': %w", err) + } + delete(object, "mainbranch") + } + + if raw, found := object["name"]; found { + err = json.Unmarshal(raw, &a.Name) + if err != nil { + return fmt.Errorf("error reading 'name': %w", err) + } + delete(object, "name") + } + + if raw, found := object["owner"]; found { + err = json.Unmarshal(raw, &a.Owner) + if err != nil { + return fmt.Errorf("error reading 'owner': %w", err) + } + delete(object, "owner") + } + + if raw, found := object["parent"]; found { + err = json.Unmarshal(raw, &a.Parent) + if err != nil { + return fmt.Errorf("error reading 'parent': %w", err) + } + delete(object, "parent") + } + + if raw, found := object["project"]; found { + err = json.Unmarshal(raw, &a.Project) + if err != nil { + return fmt.Errorf("error reading 'project': %w", err) + } + delete(object, "project") + } + + if raw, found := object["scm"]; found { + err = json.Unmarshal(raw, &a.Scm) + if err != nil { + return fmt.Errorf("error reading 'scm': %w", err) + } + delete(object, "scm") + } + + if raw, found := object["size"]; found { + err = json.Unmarshal(raw, &a.Size) + if err != nil { + return fmt.Errorf("error reading 'size': %w", err) + } + delete(object, "size") + } + + if raw, found := object["type"]; found { + err = json.Unmarshal(raw, &a.Type) + if err != nil { + return fmt.Errorf("error reading 'type': %w", err) + } + delete(object, "type") + } + + if raw, found := object["updated_on"]; found { + err = json.Unmarshal(raw, &a.UpdatedOn) + if err != nil { + return fmt.Errorf("error reading 'updated_on': %w", err) + } + delete(object, "updated_on") + } + + if raw, found := object["uuid"]; found { + err = json.Unmarshal(raw, &a.Uuid) + if err != nil { + return fmt.Errorf("error reading 'uuid': %w", err) + } + delete(object, "uuid") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]interface{}) + for fieldName, fieldBuf := range object { + var fieldVal interface{} + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for Repository to handle AdditionalProperties +func (a Repository) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.CreatedOn != nil { + object["created_on"], err = json.Marshal(a.CreatedOn) + if err != nil { + return nil, fmt.Errorf("error marshaling 'created_on': %w", err) + } + } + + if a.Description != nil { + object["description"], err = json.Marshal(a.Description) + if err != nil { + return nil, fmt.Errorf("error marshaling 'description': %w", err) + } + } + + if a.ForkPolicy != nil { + object["fork_policy"], err = json.Marshal(a.ForkPolicy) + if err != nil { + return nil, fmt.Errorf("error marshaling 'fork_policy': %w", err) + } + } + + if a.FullName != nil { + object["full_name"], err = json.Marshal(a.FullName) + if err != nil { + return nil, fmt.Errorf("error marshaling 'full_name': %w", err) + } + } + + if a.HasIssues != nil { + object["has_issues"], err = json.Marshal(a.HasIssues) + if err != nil { + return nil, fmt.Errorf("error marshaling 'has_issues': %w", err) + } + } + + if a.HasWiki != nil { + object["has_wiki"], err = json.Marshal(a.HasWiki) + if err != nil { + return nil, fmt.Errorf("error marshaling 'has_wiki': %w", err) + } + } + + if a.IsPrivate != nil { + object["is_private"], err = json.Marshal(a.IsPrivate) + if err != nil { + return nil, fmt.Errorf("error marshaling 'is_private': %w", err) + } + } + + if a.Language != nil { + object["language"], err = json.Marshal(a.Language) + if err != nil { + return nil, fmt.Errorf("error marshaling 'language': %w", err) + } + } + + if a.Links != nil { + object["links"], err = json.Marshal(a.Links) + if err != nil { + return nil, fmt.Errorf("error marshaling 'links': %w", err) + } + } + + if a.Mainbranch != nil { + object["mainbranch"], err = json.Marshal(a.Mainbranch) + if err != nil { + return nil, fmt.Errorf("error marshaling 'mainbranch': %w", err) + } + } + + if a.Name != nil { + object["name"], err = json.Marshal(a.Name) + if err != nil { + return nil, fmt.Errorf("error marshaling 'name': %w", err) + } + } + + if a.Owner != nil { + object["owner"], err = json.Marshal(a.Owner) + if err != nil { + return nil, fmt.Errorf("error marshaling 'owner': %w", err) + } + } + + if a.Parent != nil { + object["parent"], err = json.Marshal(a.Parent) + if err != nil { + return nil, fmt.Errorf("error marshaling 'parent': %w", err) + } + } + + if a.Project != nil { + object["project"], err = json.Marshal(a.Project) + if err != nil { + return nil, fmt.Errorf("error marshaling 'project': %w", err) + } + } + + if a.Scm != nil { + object["scm"], err = json.Marshal(a.Scm) + if err != nil { + return nil, fmt.Errorf("error marshaling 'scm': %w", err) + } + } + + if a.Size != nil { + object["size"], err = json.Marshal(a.Size) + if err != nil { + return nil, fmt.Errorf("error marshaling 'size': %w", err) + } + } + + object["type"], err = json.Marshal(a.Type) + if err != nil { + return nil, fmt.Errorf("error marshaling 'type': %w", err) + } + + if a.UpdatedOn != nil { + object["updated_on"], err = json.Marshal(a.UpdatedOn) + if err != nil { + return nil, fmt.Errorf("error marshaling 'updated_on': %w", err) + } + } + + if a.Uuid != nil { + object["uuid"], err = json.Marshal(a.Uuid) + if err != nil { + return nil, fmt.Errorf("error marshaling 'uuid': %w", err) + } + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + +// Getter for additional properties for Team. Returns the specified +// element and whether it was found +func (a Team) Get(fieldName string) (value interface{}, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for Team +func (a *Team) Set(fieldName string, value interface{}) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]interface{}) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for Team to handle AdditionalProperties +func (a *Team) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["created_on"]; found { + err = json.Unmarshal(raw, &a.CreatedOn) + if err != nil { + return fmt.Errorf("error reading 'created_on': %w", err) + } + delete(object, "created_on") + } + + if raw, found := object["display_name"]; found { + err = json.Unmarshal(raw, &a.DisplayName) + if err != nil { + return fmt.Errorf("error reading 'display_name': %w", err) + } + delete(object, "display_name") + } + + if raw, found := object["links"]; found { + err = json.Unmarshal(raw, &a.Links) + if err != nil { + return fmt.Errorf("error reading 'links': %w", err) + } + delete(object, "links") + } + + if raw, found := object["type"]; found { + err = json.Unmarshal(raw, &a.Type) + if err != nil { + return fmt.Errorf("error reading 'type': %w", err) + } + delete(object, "type") + } + + if raw, found := object["uuid"]; found { + err = json.Unmarshal(raw, &a.Uuid) + if err != nil { + return fmt.Errorf("error reading 'uuid': %w", err) + } + delete(object, "uuid") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]interface{}) + for fieldName, fieldBuf := range object { + var fieldVal interface{} + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for Team to handle AdditionalProperties +func (a Team) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.CreatedOn != nil { + object["created_on"], err = json.Marshal(a.CreatedOn) + if err != nil { + return nil, fmt.Errorf("error marshaling 'created_on': %w", err) + } + } + + if a.DisplayName != nil { + object["display_name"], err = json.Marshal(a.DisplayName) + if err != nil { + return nil, fmt.Errorf("error marshaling 'display_name': %w", err) + } + } + + if a.Links != nil { + object["links"], err = json.Marshal(a.Links) + if err != nil { + return nil, fmt.Errorf("error marshaling 'links': %w", err) + } + } + + object["type"], err = json.Marshal(a.Type) + if err != nil { + return nil, fmt.Errorf("error marshaling 'type': %w", err) + } + + if a.Uuid != nil { + object["uuid"], err = json.Marshal(a.Uuid) + if err != nil { + return nil, fmt.Errorf("error marshaling 'uuid': %w", err) + } + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + +// Getter for additional properties for TeamLinks. Returns the specified +// element and whether it was found +func (a TeamLinks) Get(fieldName string) (value interface{}, found bool) { + if a.AdditionalProperties != nil { + value, found = a.AdditionalProperties[fieldName] + } + return +} + +// Setter for additional properties for TeamLinks +func (a *TeamLinks) Set(fieldName string, value interface{}) { + if a.AdditionalProperties == nil { + a.AdditionalProperties = make(map[string]interface{}) + } + a.AdditionalProperties[fieldName] = value +} + +// Override default JSON handling for TeamLinks to handle AdditionalProperties +func (a *TeamLinks) UnmarshalJSON(b []byte) error { + object := make(map[string]json.RawMessage) + err := json.Unmarshal(b, &object) + if err != nil { + return err + } + + if raw, found := object["avatar"]; found { + err = json.Unmarshal(raw, &a.Avatar) + if err != nil { + return fmt.Errorf("error reading 'avatar': %w", err) + } + delete(object, "avatar") + } + + if raw, found := object["html"]; found { + err = json.Unmarshal(raw, &a.Html) + if err != nil { + return fmt.Errorf("error reading 'html': %w", err) + } + delete(object, "html") + } + + if raw, found := object["members"]; found { + err = json.Unmarshal(raw, &a.Members) + if err != nil { + return fmt.Errorf("error reading 'members': %w", err) + } + delete(object, "members") + } + + if raw, found := object["projects"]; found { + err = json.Unmarshal(raw, &a.Projects) + if err != nil { + return fmt.Errorf("error reading 'projects': %w", err) + } + delete(object, "projects") + } + + if raw, found := object["repositories"]; found { + err = json.Unmarshal(raw, &a.Repositories) + if err != nil { + return fmt.Errorf("error reading 'repositories': %w", err) + } + delete(object, "repositories") + } + + if raw, found := object["self"]; found { + err = json.Unmarshal(raw, &a.Self) + if err != nil { + return fmt.Errorf("error reading 'self': %w", err) + } + delete(object, "self") + } + + if len(object) != 0 { + a.AdditionalProperties = make(map[string]interface{}) + for fieldName, fieldBuf := range object { + var fieldVal interface{} + err := json.Unmarshal(fieldBuf, &fieldVal) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) + } + a.AdditionalProperties[fieldName] = fieldVal + } + } + return nil +} + +// Override default JSON handling for TeamLinks to handle AdditionalProperties +func (a TeamLinks) MarshalJSON() ([]byte, error) { + var err error + object := make(map[string]json.RawMessage) + + if a.Avatar != nil { + object["avatar"], err = json.Marshal(a.Avatar) + if err != nil { + return nil, fmt.Errorf("error marshaling 'avatar': %w", err) + } + } + + if a.Html != nil { + object["html"], err = json.Marshal(a.Html) + if err != nil { + return nil, fmt.Errorf("error marshaling 'html': %w", err) + } + } + + if a.Members != nil { + object["members"], err = json.Marshal(a.Members) + if err != nil { + return nil, fmt.Errorf("error marshaling 'members': %w", err) + } + } + + if a.Projects != nil { + object["projects"], err = json.Marshal(a.Projects) + if err != nil { + return nil, fmt.Errorf("error marshaling 'projects': %w", err) + } + } + + if a.Repositories != nil { + object["repositories"], err = json.Marshal(a.Repositories) + if err != nil { + return nil, fmt.Errorf("error marshaling 'repositories': %w", err) + } + } + + if a.Self != nil { + object["self"], err = json.Marshal(a.Self) + if err != nil { + return nil, fmt.Errorf("error marshaling 'self': %w", err) + } + } + + for fieldName, field := range a.AdditionalProperties { + object[fieldName], err = json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) + } + } + return json.Marshal(object) +} + +// 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 { + // GetRepositoriesWorkspaceRepoSlugCommitCommit request + GetRepositoriesWorkspaceRepoSlugCommitCommit(ctx context.Context, workspace string, repoSlug string, commit string, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetRepositoriesWorkspaceRepoSlugPullrequests request + GetRepositoriesWorkspaceRepoSlugPullrequests(ctx context.Context, workspace string, repoSlug string, params *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // PostRepositoriesWorkspaceRepoSlugPullrequestsWithBody request with any body + PostRepositoriesWorkspaceRepoSlugPullrequestsWithBody(ctx context.Context, workspace string, repoSlug string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + PostRepositoriesWorkspaceRepoSlugPullrequests(ctx context.Context, workspace string, repoSlug string, body PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestId request + GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestId(ctx context.Context, workspace string, repoSlug string, pullRequestId int, reqEditors ...RequestEditorFn) (*http.Response, error) + + // PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeWithBody request with any body + PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeWithBody(ctx context.Context, workspace string, repoSlug string, pullRequestId int, params *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMerge(ctx context.Context, workspace string, repoSlug string, pullRequestId int, params *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, body PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (c *Client) GetRepositoriesWorkspaceRepoSlugCommitCommit(ctx context.Context, workspace string, repoSlug string, commit string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetRepositoriesWorkspaceRepoSlugCommitCommitRequest(c.Server, workspace, repoSlug, commit) + 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) GetRepositoriesWorkspaceRepoSlugPullrequests(ctx context.Context, workspace string, repoSlug string, params *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetRepositoriesWorkspaceRepoSlugPullrequestsRequest(c.Server, workspace, repoSlug, 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) PostRepositoriesWorkspaceRepoSlugPullrequestsWithBody(ctx context.Context, workspace string, repoSlug string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostRepositoriesWorkspaceRepoSlugPullrequestsRequestWithBody(c.Server, workspace, repoSlug, 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) PostRepositoriesWorkspaceRepoSlugPullrequests(ctx context.Context, workspace string, repoSlug string, body PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostRepositoriesWorkspaceRepoSlugPullrequestsRequest(c.Server, workspace, repoSlug, 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) GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestId(ctx context.Context, workspace string, repoSlug string, pullRequestId int, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdRequest(c.Server, workspace, repoSlug, 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) PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeWithBody(ctx context.Context, workspace string, repoSlug string, pullRequestId int, params *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeRequestWithBody(c.Server, workspace, repoSlug, 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) PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMerge(ctx context.Context, workspace string, repoSlug string, pullRequestId int, params *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, body PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeRequest(c.Server, workspace, repoSlug, 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) +} + +// NewGetRepositoriesWorkspaceRepoSlugCommitCommitRequest generates requests for GetRepositoriesWorkspaceRepoSlugCommitCommit +func NewGetRepositoriesWorkspaceRepoSlugCommitCommitRequest(server string, workspace string, repoSlug string, commit string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "workspace", workspace, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "repo_slug", repoSlug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + var pathParam2 string + + pathParam2, err = runtime.StyleParamWithOptions("simple", false, "commit", commit, 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("/repositories/%s/%s/commit/%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 +} + +// NewGetRepositoriesWorkspaceRepoSlugPullrequestsRequest generates requests for GetRepositoriesWorkspaceRepoSlugPullrequests +func NewGetRepositoriesWorkspaceRepoSlugPullrequestsRequest(server string, workspace string, repoSlug string, params *GetRepositoriesWorkspaceRepoSlugPullrequestsParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "workspace", workspace, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "repo_slug", repoSlug, 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("/repositories/%s/%s/pullrequests", 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) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewPostRepositoriesWorkspaceRepoSlugPullrequestsRequest calls the generic PostRepositoriesWorkspaceRepoSlugPullrequests builder with application/json body +func NewPostRepositoriesWorkspaceRepoSlugPullrequestsRequest(server string, workspace string, repoSlug string, body PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewPostRepositoriesWorkspaceRepoSlugPullrequestsRequestWithBody(server, workspace, repoSlug, "application/json", bodyReader) +} + +// NewPostRepositoriesWorkspaceRepoSlugPullrequestsRequestWithBody generates requests for PostRepositoriesWorkspaceRepoSlugPullrequests with any type of body +func NewPostRepositoriesWorkspaceRepoSlugPullrequestsRequestWithBody(server string, workspace string, repoSlug string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "workspace", workspace, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "repo_slug", repoSlug, 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("/repositories/%s/%s/pullrequests", 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 +} + +// NewGetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdRequest generates requests for GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestId +func NewGetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdRequest(server string, workspace string, repoSlug string, pullRequestId int) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "workspace", workspace, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "repo_slug", repoSlug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + var pathParam2 string + + pathParam2, err = runtime.StyleParamWithOptions("simple", false, "pull_request_id", 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("/repositories/%s/%s/pullrequests/%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 +} + +// NewPostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeRequest calls the generic PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMerge builder with application/json body +func NewPostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeRequest(server string, workspace string, repoSlug string, pullRequestId int, params *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, body PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewPostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeRequestWithBody(server, workspace, repoSlug, pullRequestId, params, "application/json", bodyReader) +} + +// NewPostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeRequestWithBody generates requests for PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMerge with any type of body +func NewPostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeRequestWithBody(server string, workspace string, repoSlug string, pullRequestId int, params *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "workspace", workspace, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "repo_slug", repoSlug, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + var pathParam2 string + + pathParam2, err = runtime.StyleParamWithOptions("simple", false, "pull_request_id", 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("/repositories/%s/%s/pullrequests/%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 params.Async != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "async", *params.Async, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "boolean", 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 { + // GetRepositoriesWorkspaceRepoSlugCommitCommitWithResponse request + GetRepositoriesWorkspaceRepoSlugCommitCommitWithResponse(ctx context.Context, workspace string, repoSlug string, commit string, reqEditors ...RequestEditorFn) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) + + // GetRepositoriesWorkspaceRepoSlugPullrequestsWithResponse request + GetRepositoriesWorkspaceRepoSlugPullrequestsWithResponse(ctx context.Context, workspace string, repoSlug string, params *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, reqEditors ...RequestEditorFn) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) + + // PostRepositoriesWorkspaceRepoSlugPullrequestsWithBodyWithResponse request with any body + PostRepositoriesWorkspaceRepoSlugPullrequestsWithBodyWithResponse(ctx context.Context, workspace string, repoSlug string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) + + PostRepositoriesWorkspaceRepoSlugPullrequestsWithResponse(ctx context.Context, workspace string, repoSlug string, body PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, reqEditors ...RequestEditorFn) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) + + // GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdWithResponse request + GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdWithResponse(ctx context.Context, workspace string, repoSlug string, pullRequestId int, reqEditors ...RequestEditorFn) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) + + // PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeWithBodyWithResponse request with any body + PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeWithBodyWithResponse(ctx context.Context, workspace string, repoSlug string, pullRequestId int, params *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse, error) + + PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeWithResponse(ctx context.Context, workspace string, repoSlug string, pullRequestId int, params *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, body PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeJSONRequestBody, reqEditors ...RequestEditorFn) (*PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse, error) +} + +type GetRepositoriesWorkspaceRepoSlugCommitCommitResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *Commit + JSON404 *Error +} + +// Status returns HTTPResponse.Status +func (r GetRepositoriesWorkspaceRepoSlugCommitCommitResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetRepositoriesWorkspaceRepoSlugCommitCommitResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetRepositoriesWorkspaceRepoSlugPullrequestsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *PaginatedPullrequests + JSON404 *Error +} + +// Status returns HTTPResponse.Status +func (r GetRepositoriesWorkspaceRepoSlugPullrequestsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetRepositoriesWorkspaceRepoSlugPullrequestsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type PostRepositoriesWorkspaceRepoSlugPullrequestsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON201 *Pullrequest + JSON400 *Error + JSON401 *Error +} + +// Status returns HTTPResponse.Status +func (r PostRepositoriesWorkspaceRepoSlugPullrequestsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r PostRepositoriesWorkspaceRepoSlugPullrequestsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *Pullrequest + JSON404 *Error +} + +// Status returns HTTPResponse.Status +func (r GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *Pullrequest + JSON555 *Error +} + +// Status returns HTTPResponse.Status +func (r PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// GetRepositoriesWorkspaceRepoSlugCommitCommitWithResponse request returning *GetRepositoriesWorkspaceRepoSlugCommitCommitResponse +func (c *ClientWithResponses) GetRepositoriesWorkspaceRepoSlugCommitCommitWithResponse(ctx context.Context, workspace string, repoSlug string, commit string, reqEditors ...RequestEditorFn) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) { + rsp, err := c.GetRepositoriesWorkspaceRepoSlugCommitCommit(ctx, workspace, repoSlug, commit, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetRepositoriesWorkspaceRepoSlugCommitCommitResponse(rsp) +} + +// GetRepositoriesWorkspaceRepoSlugPullrequestsWithResponse request returning *GetRepositoriesWorkspaceRepoSlugPullrequestsResponse +func (c *ClientWithResponses) GetRepositoriesWorkspaceRepoSlugPullrequestsWithResponse(ctx context.Context, workspace string, repoSlug string, params *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, reqEditors ...RequestEditorFn) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + rsp, err := c.GetRepositoriesWorkspaceRepoSlugPullrequests(ctx, workspace, repoSlug, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetRepositoriesWorkspaceRepoSlugPullrequestsResponse(rsp) +} + +// PostRepositoriesWorkspaceRepoSlugPullrequestsWithBodyWithResponse request with arbitrary body returning *PostRepositoriesWorkspaceRepoSlugPullrequestsResponse +func (c *ClientWithResponses) PostRepositoriesWorkspaceRepoSlugPullrequestsWithBodyWithResponse(ctx context.Context, workspace string, repoSlug string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + rsp, err := c.PostRepositoriesWorkspaceRepoSlugPullrequestsWithBody(ctx, workspace, repoSlug, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostRepositoriesWorkspaceRepoSlugPullrequestsResponse(rsp) +} + +func (c *ClientWithResponses) PostRepositoriesWorkspaceRepoSlugPullrequestsWithResponse(ctx context.Context, workspace string, repoSlug string, body PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, reqEditors ...RequestEditorFn) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + rsp, err := c.PostRepositoriesWorkspaceRepoSlugPullrequests(ctx, workspace, repoSlug, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostRepositoriesWorkspaceRepoSlugPullrequestsResponse(rsp) +} + +// GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdWithResponse request returning *GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse +func (c *ClientWithResponses) GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdWithResponse(ctx context.Context, workspace string, repoSlug string, pullRequestId int, reqEditors ...RequestEditorFn) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { + rsp, err := c.GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestId(ctx, workspace, repoSlug, pullRequestId, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse(rsp) +} + +// PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeWithBodyWithResponse request with arbitrary body returning *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse +func (c *ClientWithResponses) PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeWithBodyWithResponse(ctx context.Context, workspace string, repoSlug string, pullRequestId int, params *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse, error) { + rsp, err := c.PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeWithBody(ctx, workspace, repoSlug, pullRequestId, params, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse(rsp) +} + +func (c *ClientWithResponses) PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeWithResponse(ctx context.Context, workspace string, repoSlug string, pullRequestId int, params *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, body PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeJSONRequestBody, reqEditors ...RequestEditorFn) (*PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse, error) { + rsp, err := c.PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMerge(ctx, workspace, repoSlug, pullRequestId, params, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse(rsp) +} + +// ParseGetRepositoriesWorkspaceRepoSlugCommitCommitResponse parses an HTTP response from a GetRepositoriesWorkspaceRepoSlugCommitCommitWithResponse call +func ParseGetRepositoriesWorkspaceRepoSlugCommitCommitResponse(rsp *http.Response) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetRepositoriesWorkspaceRepoSlugCommitCommitResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Commit + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + } + + return response, nil +} + +// ParseGetRepositoriesWorkspaceRepoSlugPullrequestsResponse parses an HTTP response from a GetRepositoriesWorkspaceRepoSlugPullrequestsWithResponse call +func ParseGetRepositoriesWorkspaceRepoSlugPullrequestsResponse(rsp *http.Response) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetRepositoriesWorkspaceRepoSlugPullrequestsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest PaginatedPullrequests + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + } + + return response, nil +} + +// ParsePostRepositoriesWorkspaceRepoSlugPullrequestsResponse parses an HTTP response from a PostRepositoriesWorkspaceRepoSlugPullrequestsWithResponse call +func ParsePostRepositoriesWorkspaceRepoSlugPullrequestsResponse(rsp *http.Response) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &PostRepositoriesWorkspaceRepoSlugPullrequestsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest Pullrequest + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON201 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + } + + return response, nil +} + +// ParseGetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse parses an HTTP response from a GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdWithResponse call +func ParseGetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse(rsp *http.Response) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Pullrequest + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + } + + return response, nil +} + +// ParsePostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse parses an HTTP response from a PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeWithResponse call +func ParsePostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse(rsp *http.Response) (*PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Pullrequest + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 555: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON555 = &dest + + } + + return response, nil +} From e0f4bf342da74126f38c49001797116ce787dca2 Mon Sep 17 00:00:00 2001 From: fuskovic Date: Mon, 20 Apr 2026 17:38:37 +0530 Subject: [PATCH 05/18] refactor bb git provider Signed-off-by: fuskovic --- pkg/gitprovider/bitbucket/bitbucket.go | 539 ++++++++++++------------- 1 file changed, 258 insertions(+), 281 deletions(-) diff --git a/pkg/gitprovider/bitbucket/bitbucket.go b/pkg/gitprovider/bitbucket/bitbucket.go index 9c8e2dec1f..eed8482cc0 100644 --- a/pkg/gitprovider/bitbucket/bitbucket.go +++ b/pkg/gitprovider/bitbucket/bitbucket.go @@ -2,16 +2,12 @@ package bitbucket import ( "context" - "encoding/json" - "errors" "fmt" + "net/http" "net/url" - "strconv" "strings" - "time" "github.com/hashicorp/go-cleanhttp" - "github.com/ktrysmt/go-bitbucket" "github.com/akuity/kargo/pkg/gitprovider" "github.com/akuity/kargo/pkg/urls" @@ -25,16 +21,8 @@ const ( // self-hosted Bitbucket "Datacenter" instances. supportedHost = "bitbucket.org" - // prStateOpen is the state of an open pull request. - prStateOpen = "OPEN" - // prStateMerged is the state of a merged pull request. - prStateMerged = "MERGED" - // prStateDeclined is the state of a declined pull request. This is also - // known as "closed" in other Git providers. - prStateDeclined = "DECLINED" - // prStateSuperseded is the state of a superseded pull request. This is also - // known as "closed" in other Git providers. - prStateSuperseded = "SUPERSEDED" + // apiBaseURL is the base URL for the Bitbucket Cloud REST API. + apiBaseURL = "https://api.bitbucket.org/2.0" ) var registration = gitprovider.Registration{ @@ -57,27 +45,18 @@ func init() { gitprovider.Register(ProviderName, registration) } -// pullRequestClient defines the interface for pull request operations. -type pullRequestClient interface { - Create(*bitbucket.PullRequestsOptions) (any, error) - Gets(*bitbucket.PullRequestsOptions) (any, error) - Get(*bitbucket.PullRequestsOptions) (any, error) - GetCommit(*bitbucket.CommitsOptions) (any, error) - Merge(*bitbucket.PullRequestsOptions) (any, error) -} - // provider is a Bitbucket-based implementation of gitprovider.Interface. type provider struct { owner string repoSlug string - client pullRequestClient + client ClientWithResponsesInterface } // NewProvider returns a Bitbucket-based implementation of gitprovider.Interface. func NewProvider( repoURL string, opts *gitprovider.Options, -) (p gitprovider.Interface, err error) { +) (gitprovider.Interface, error) { if opts == nil { opts = &gitprovider.Options{} } @@ -93,34 +72,26 @@ func NewProvider( return nil, fmt.Errorf("unsupported Bitbucket host %q", host) } - // The go-bitbucket library may call log.Fatalf for certain error conditions, - // which causes a panic. We recover from this to return a proper error. - defer func() { - if r := recover(); r != nil { - p = nil - err = fmt.Errorf("failed to create Bitbucket client: %v", r) - } - }() - - client := bitbucket.NewOAuthbearerToken(opts.Token) - client.HttpClient = cleanhttp.DefaultClient() + token := opts.Token + client, err := NewClientWithResponses( + apiBaseURL, + 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 API client: %w", err) + } return &provider{ owner: owner, repoSlug: repoSlug, - client: &clientWrapper{ - Commits: client.Repositories.Commits, - PullRequests: client.Repositories.PullRequests, - }, + client: client, }, nil } -// clientWrapper wraps a bitbucket.Client to implement the prClient interface -type clientWrapper struct { - *bitbucket.Commits - *bitbucket.PullRequests -} - // CreatePullRequest implements gitprovider.Interface. func (p *provider) CreatePullRequest( ctx context.Context, @@ -130,35 +101,47 @@ func (p *provider) CreatePullRequest( opts = &gitprovider.CreatePullRequestOpts{} } - createOpts := &bitbucket.PullRequestsOptions{ - Owner: p.owner, - RepoSlug: p.repoSlug, - Title: opts.Title, - Description: opts.Description, - SourceBranch: opts.Head, - DestinationBranch: opts.Base, + title := opts.Title + body := Pullrequest{Type: "pullrequest", Title: &title} + if opts.Description != "" { + body.Set("description", opts.Description) } - createOpts.WithContext(ctx) - resp, err := p.client.Create(createOpts) - if err != nil { - return nil, err + srcBranch := opts.Head + dstBranch := opts.Base + body.Source = &PullrequestEndpoint{ + Branch: &struct { + DefaultMergeStrategy *string `json:"default_merge_strategy,omitempty"` + MergeStrategies *[]PullrequestEndpointBranchMergeStrategies `json:"merge_strategies,omitempty"` + Name *string `json:"name,omitempty"` + }{Name: &srcBranch}, + } + body.Destination = &PullrequestEndpoint{ + Branch: &struct { + DefaultMergeStrategy *string `json:"default_merge_strategy,omitempty"` + MergeStrategies *[]PullrequestEndpointBranchMergeStrategies `json:"merge_strategies,omitempty"` + Name *string `json:"name,omitempty"` + }{Name: &dstBranch}, } - pr, err := toBitbucketPR(resp) + resp, err := p.client.PostRepositoriesWorkspaceRepoSlugPullrequestsWithResponse( + ctx, + p.owner, + p.repoSlug, + body, + ) if err != nil { - return nil, err + return nil, fmt.Errorf("error creating pull request: %w", err) } - - if pr.MergeCommit.Hash != "" { - fullSHA, err := p.getFullCommitSHA(ctx, pr.MergeCommit.Hash) - if err != nil { - return nil, err - } - pr.MergeCommit.Hash = fullSHA + if resp.JSON201 == nil { + return nil, fmt.Errorf( + "unexpected response %d creating pull request", resp.StatusCode(), + ) } - - return toProviderPR(pr, resp), nil + if err = p.resolveFullMergeCommitSHA(ctx, resp.JSON201); err != nil { + return nil, err + } + return toProviderPR(resp.JSON201), nil } // GetPullRequest implements gitprovider.Interface. @@ -166,32 +149,24 @@ func (p *provider) GetPullRequest( ctx context.Context, id int64, ) (*gitprovider.PullRequest, error) { - getOpts := &bitbucket.PullRequestsOptions{ - Owner: p.owner, - RepoSlug: p.repoSlug, - ID: strconv.FormatInt(id, 10), - } - getOpts.WithContext(ctx) - - resp, err := p.client.Get(getOpts) + resp, err := p.client.GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdWithResponse( + ctx, + p.owner, + p.repoSlug, + int(id), + ) if err != nil { - return nil, err + return nil, fmt.Errorf("error getting pull request %d: %w", id, err) } - - pr, err := toBitbucketPR(resp) - if err != nil { - return nil, err + if resp.JSON200 == nil { + return nil, fmt.Errorf( + "unexpected response %d getting pull request %d", resp.StatusCode(), id, + ) } - - if pr.MergeCommit.Hash != "" { - fullSHA, err := p.getFullCommitSHA(ctx, pr.MergeCommit.Hash) - if err != nil { - return nil, err - } - pr.MergeCommit.Hash = fullSHA + if err = p.resolveFullMergeCommitSHA(ctx, resp.JSON200); err != nil { + return nil, err } - - return toProviderPR(pr, resp), nil + return toProviderPR(resp.JSON200), nil } // ListPullRequests implements gitprovider.Interface. @@ -206,83 +181,97 @@ func (p *provider) ListPullRequests( opts.State = gitprovider.PullRequestStateOpen } - listOpts := &bitbucket.PullRequestsOptions{ - Owner: p.owner, - RepoSlug: p.repoSlug, - } - listOpts.WithContext(ctx) - + var states []string switch opts.State { case gitprovider.PullRequestStateAny: - listOpts.States = []string{prStateOpen, prStateMerged, prStateDeclined, prStateSuperseded} + states = []string{ + string(PullrequestStateOPEN), + string(PullrequestStateMERGED), + string(PullrequestStateDECLINED), + string(PullrequestStateSUPERSEDED), + } case gitprovider.PullRequestStateClosed: - listOpts.States = []string{prStateMerged, prStateDeclined, prStateSuperseded} + states = []string{ + string(PullrequestStateMERGED), + string(PullrequestStateDECLINED), + string(PullrequestStateSUPERSEDED), + } case gitprovider.PullRequestStateOpen: - listOpts.States = []string{prStateOpen} + states = []string{string(PullrequestStateOPEN)} default: return nil, fmt.Errorf("unknown pull request state %q", opts.State) } - resp, err := p.client.Gets(listOpts) + resp, err := p.client.GetRepositoriesWorkspaceRepoSlugPullrequestsWithResponse( + ctx, + p.owner, + p.repoSlug, + &GetRepositoriesWorkspaceRepoSlugPullrequestsParams{}, + withStates(states), + ) if err != nil { - return nil, err - } - - list, ok := resp.(map[string]any) - if !ok { - return nil, fmt.Errorf("unexpected type for list response: %T", resp) - } - - rawPRs, ok := list["values"] - if !ok { - return nil, fmt.Errorf("list response missing 'values' field") + return nil, fmt.Errorf("error listing pull requests: %w", err) } - - prList, ok := rawPRs.([]any) - if !ok { - return nil, fmt.Errorf("unexpected type for list values: %T", rawPRs) + if resp.JSON200 == nil { + return nil, fmt.Errorf( + "unexpected response %d listing pull requests", resp.StatusCode(), + ) } // NB: The Bitbucket API doesn't support filtering by source/destination // branch or commit hash, so we have to filter client-side, which is // highly inefficient. - var prs []gitprovider.PullRequest - for _, pr := range prList { - bbPR, err := toBitbucketPR(pr) - if err != nil { - continue - } - - if opts.HeadBranch != "" && bbPR.Source.Branch.Name != opts.HeadBranch { - continue - } - - if opts.BaseBranch != "" && bbPR.Destination.Branch.Name != opts.BaseBranch { - continue + var result []gitprovider.PullRequest + if resp.JSON200.Values == nil { + return result, nil + } + values := *resp.JSON200.Values + for i := range values { + pr := &values[i] + if opts.HeadBranch != "" { + branchName := "" + if pr.Source != nil && + pr.Source.Branch != nil && + pr.Source.Branch.Name != nil { + branchName = *pr.Source.Branch.Name + } + if branchName != opts.HeadBranch { + continue + } } - - if opts.HeadCommit != "" && bbPR.Source.Commit.Hash != opts.HeadCommit { - continue + if opts.BaseBranch != "" { + branchName := "" + if pr.Destination != nil && + pr.Destination.Branch != nil && + pr.Destination.Branch.Name != nil { + branchName = *pr.Destination.Branch.Name + } + if branchName != opts.BaseBranch { + continue + } } - - if bbPR.MergeCommit.Hash != "" { - fullSHA, err := p.getFullCommitSHA(ctx, bbPR.MergeCommit.Hash) - if err != nil { - return nil, err + if opts.HeadCommit != "" { + commitHash := "" + if pr.Source != nil && + pr.Source.Commit != nil && + pr.Source.Commit.Hash != nil { + commitHash = *pr.Source.Commit.Hash + } + if commitHash != opts.HeadCommit { + continue } - bbPR.MergeCommit.Hash = fullSHA } - - if converted := toProviderPR(bbPR, pr); converted != nil { - prs = append(prs, *converted) + if err = p.resolveFullMergeCommitSHA(ctx, pr); err != nil { + return nil, err } + result = append(result, *toProviderPR(pr)) } - return prs, nil + return result, nil } // MergePullRequest implements gitprovider.Interface. func (p *provider) MergePullRequest( - _ context.Context, + ctx context.Context, id int64, opts *gitprovider.MergePullRequestOpts, ) (*gitprovider.PullRequest, bool, error) { @@ -290,38 +279,40 @@ func (p *provider) MergePullRequest( opts = &gitprovider.MergePullRequestOpts{} } - // Get the PR to check its state - prOpts := &bitbucket.PullRequestsOptions{ - Owner: p.owner, - RepoSlug: p.repoSlug, - ID: strconv.FormatInt(id, 10), - } + prID := int(id) - prResp, err := p.client.Get(prOpts) + getResp, err := p.client.GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdWithResponse( + ctx, + p.owner, + 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 - bbPR, err := toBitbucketPR(prResp) - if err != nil { - return nil, false, fmt.Errorf("error parsing pull request response: %w", err) + var state PullrequestState + if pr.State != nil { + state = *pr.State } - // Check if PR is already merged - if bbPR.State == prStateMerged { - return toProviderPR(bbPR, prResp), true, nil + if state == PullrequestStateMERGED { + return toProviderPR(pr), true, nil } - // Check if PR is closed (any non-open state except merged) - if bbPR.State != prStateOpen { - // If it's not open and not merged, then it's closed + if state != PullrequestStateOPEN { return nil, false, fmt.Errorf( - "pull request %d is closed but not merged (state: %s)", id, bbPR.State, + "pull request %d is closed but not merged (state: %s)", id, state, ) } - // Check if PR is draft - cannot merge draft PRs - if bbPR.Draft { + if pr.Draft != nil && *pr.Draft { return nil, false, nil } @@ -335,180 +326,166 @@ func (p *provider) MergePullRequest( // This limitation makes the "wait" option unreliable for Bitbucket // repositories. - // TODO(krancour): go-bitbucket does not expose the merge method/strategy as - // an option. The merge will always use the repository's default merge - // strategy, so simply fail if any merge strategy was specified. This is - // strictly a limitation of the go-bitbucket library and not the Bitbucket API - // itself, so this can be revisited in the future if the library adds support - // for this or if we decide to implement this API call ourselves instead of - // using the library. + var mergeStrategy *PullrequestMergeParametersMergeStrategy if opts.MergeMethod != "" { - return nil, false, errors.New( // nolint: staticcheck - "Kargo's Bitbucket client does not yet support specifying a merge method", - ) + s := PullrequestMergeParametersMergeStrategy(opts.MergeMethod) + if !s.Valid() { + return nil, false, fmt.Errorf("unsupported merge strategy %q", opts.MergeMethod) + } + mergeStrategy = &s } - mergeOpts := &bitbucket.PullRequestsOptions{ - Owner: p.owner, - RepoSlug: p.repoSlug, - ID: strconv.FormatInt(id, 10), + + mergeBody := PullrequestMergeParameters{ + Type: "pullrequestMergeParameters", + MergeStrategy: mergeStrategy, } - // TODO(krancour): The Bitbucket merge endpoint can return 202 for async - // merges. The go-bitbucket library treats 202 as success (no error), but the - // response body would be a polling URL, not a PR object. This would cause - // toBitbucketPR to fail or produce incorrect results. If async merges are - // encountered in practice, we'll need to detect the 202 and poll for - // completion. - mergeResp, err := p.client.Merge(mergeOpts) + mergeResp, err := p.client.PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeWithResponse( + ctx, + p.owner, + p.repoSlug, + prID, + &PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams{}, + mergeBody, + ) if err != nil { return nil, false, fmt.Errorf("error merging pull request %d: %w", id, err) } - // Parse the merged PR response - mergedBBPR, err := toBitbucketPR(mergeResp) - if err != nil { - return nil, false, fmt.Errorf("error parsing merged pull request response: %w", err) + if mergeResp.StatusCode() == http.StatusAccepted { + return nil, false, fmt.Errorf( + "pull request %d merge was accepted asynchronously and cannot be awaited", id, + ) + } + + if mergeResp.JSON200 == nil { + return nil, false, fmt.Errorf( + "unexpected response %d merging pull request %d", mergeResp.StatusCode(), id, + ) + } + + mergedPR := mergeResp.JSON200 + var mergedState PullrequestState + if mergedPR.State != nil { + mergedState = *mergedPR.State } // Per the Bitbucket API docs, the merge endpoint returns 200 only on - // success (409 for conflicts, 555 for timeout — both surfaced as errors - // above). A 200 response with a non-merged state would be unexpected. - if mergedBBPR.State != prStateMerged { - return nil, false, - fmt.Errorf("unexpected state %q after merging pull request %d", mergedBBPR.State, id) + // success (409 for conflicts, 555 for timeout). A 200 response with a + // non-merged state would be unexpected. + if mergedState != PullrequestStateMERGED { + return nil, false, fmt.Errorf( + "unexpected state %q after merging pull request %d", mergedState, id, + ) } - return toProviderPR(mergedBBPR, mergeResp), true, nil + return toProviderPR(mergedPR), true, nil } // GetCommitURL implements gitprovider.Interface. func (p *provider) GetCommitURL(repoURL string, sha string) (string, error) { normalizedURL := urls.NormalizeGit(repoURL) - parsedURL, err := url.Parse(normalizedURL) if err != nil { return "", fmt.Errorf("error processing repository URL: %s: %s", repoURL, err) } - - commitURL := fmt.Sprintf("https://%s%s/commits/%s", parsedURL.Host, parsedURL.Path, sha) - - return commitURL, nil + return fmt.Sprintf("https://%s%s/commits/%s", parsedURL.Host, parsedURL.Path, sha), nil } -func (p *provider) getFullCommitSHA(ctx context.Context, shortSHA string) (string, error) { - if shortSHA == "" { - return "", nil - } - - commitOpts := &bitbucket.CommitsOptions{ - Owner: p.owner, - RepoSlug: p.repoSlug, - Revision: shortSHA, +// resolveFullMergeCommitSHA replaces the (possibly short) merge commit hash on +// pr with the full SHA. Bitbucket returns a short hash in the pull request +// response, so a separate commit lookup is required. +func (p *provider) resolveFullMergeCommitSHA(ctx context.Context, pr *Pullrequest) error { + if pr.MergeCommit == nil || pr.MergeCommit.Hash == nil || *pr.MergeCommit.Hash == "" { + return nil } - commitOpts.WithContext(ctx) - - resp, err := p.client.GetCommit(commitOpts) + resp, err := p.client.GetRepositoriesWorkspaceRepoSlugCommitCommitWithResponse( + ctx, + p.owner, + p.repoSlug, + *pr.MergeCommit.Hash, + ) if err != nil { - return "", err + return fmt.Errorf("error getting commit: %w", err) } - - commitResp, ok := resp.(map[string]any) - if !ok { - return "", fmt.Errorf("unexpected commit response type: %T", resp) + if resp.JSON200 == nil || resp.JSON200.Hash == nil { + return fmt.Errorf("unexpected response %d getting commit", resp.StatusCode()) } + pr.MergeCommit.Hash = resp.JSON200.Hash + return nil +} - hash, ok := commitResp["hash"].(string) - if !ok || hash == "" { - return "", fmt.Errorf("commit response missing 'hash' field") +// toProviderPR converts a Pullrequest to a gitprovider.PullRequest. +func toProviderPR(pr *Pullrequest) *gitprovider.PullRequest { + if pr == nil { + return nil } - return hash, nil -} -// bitbucketPR represents the structure of a Bitbucket pull request. -// See: https://developer.atlassian.com/cloud/bitbucket/rest/api-group-pullrequests/#api-repositories-workspace-repo-slug-pullrequests-pull-request-id-get -// nolint:lll -type bitbucketPR struct { - ID int64 `json:"id"` - State string `json:"state"` - Links struct { - HTML struct { - Href string `json:"href"` - } `json:"html"` - } `json:"links"` - Source struct { - Branch struct { - Name string `json:"name"` - } `json:"branch"` - Commit struct { - Hash string `json:"hash"` - } `json:"commit"` - } `json:"source"` - Destination struct { - Branch struct { - Name string `json:"name"` - } `json:"branch"` - Commit struct { - Hash string `json:"hash"` - } `json:"commit"` - } `json:"destination"` - MergeCommit struct { - Hash string `json:"hash"` - } `json:"merge_commit"` - Draft bool `json:"draft"` - CreatedOn string `json:"created_on"` -} + var id int64 + if pr.Id != nil { + id = int64(*pr.Id) + } -// toBitbucketPR converts a raw response to a bitbucketPR type. -func toBitbucketPR(resp any) (*bitbucketPR, error) { - b, err := json.Marshal(resp) - if err != nil { - return nil, fmt.Errorf("marshal PR response: %w", err) + var state PullrequestState + if pr.State != nil { + state = *pr.State } - var pr bitbucketPR - if err = json.Unmarshal(b, &pr); err != nil { - return nil, fmt.Errorf("unmarshal PR response: %w", err) + + var prURL string + if pr.Links != nil && pr.Links.Html != nil && pr.Links.Html.Href != nil { + prURL = *pr.Links.Html.Href } - return &pr, nil -} -// toProviderPR converts a bitbucketPR to a gitprovider.PullRequest. -func toProviderPR(pr *bitbucketPR, raw any) *gitprovider.PullRequest { - if pr == nil { - return nil + var headSHA string + if pr.Source != nil && pr.Source.Commit != nil && pr.Source.Commit.Hash != nil { + headSHA = *pr.Source.Commit.Hash } - var createdAt *time.Time - if ts, err := time.Parse("2006-01-02T15:04:05Z", pr.CreatedOn); err == nil { - createdAt = &ts + var mergeCommitSHA string + if pr.MergeCommit != nil && pr.MergeCommit.Hash != nil { + mergeCommitSHA = *pr.MergeCommit.Hash } return &gitprovider.PullRequest{ - Number: pr.ID, - URL: pr.Links.HTML.Href, - Open: pr.State == prStateOpen, - Merged: pr.State == prStateMerged, + Number: id, + URL: prURL, + Open: state == PullrequestStateOPEN, + Merged: state == PullrequestStateMERGED, // NB: As a sign of true craftsmanship, or lack thereof, the Bitbucket // API returns a short commit SHA as merge commit hash. To get the full // commit SHA, we need to fetch the commit details separately. - MergeCommitSHA: pr.MergeCommit.Hash, - HeadSHA: pr.Source.Commit.Hash, - CreatedAt: createdAt, - Object: raw, + MergeCommitSHA: mergeCommitSHA, + HeadSHA: headSHA, + CreatedAt: pr.CreatedOn, + Object: pr, } } -// parseRepoURL extracts host, owner and repo slug from a repository URL +// withStates returns a RequestEditorFn that overrides the state query params. +// The Bitbucket API supports multiple state filters via repeated ?state= params, +// but the generated params struct only supports a single value. +func withStates(states []string) RequestEditorFn { + return func(_ context.Context, req *http.Request) error { + q := req.URL.Query() + q.Del("state") + for _, s := range states { + q.Add("state", s) + } + req.URL.RawQuery = q.Encode() + return nil + } +} + +// parseRepoURL extracts host, owner and repo slug from a repository URL. func parseRepoURL(repoURL string) (host, owner, slug string, err error) { u, err := url.Parse(urls.NormalizeGit(repoURL)) if err != nil { return "", "", "", fmt.Errorf("parse Bitbucket URL %q: %w", repoURL, err) } - path := strings.TrimPrefix(u.Path, "/") parts := strings.Split(path, "/") if len(parts) != 2 { return "", "", "", fmt.Errorf("invalid repository path in URL %q", u) } - return u.Hostname(), parts[0], parts[1], nil } From cd6dbbbc23df898216c7157fcb9e850b79e7a90e Mon Sep 17 00:00:00 2001 From: fuskovic Date: Mon, 20 Apr 2026 17:38:50 +0530 Subject: [PATCH 06/18] refactor tests Signed-off-by: fuskovic --- pkg/gitprovider/bitbucket/bitbucket_test.go | 1479 +++++++++---------- 1 file changed, 668 insertions(+), 811 deletions(-) diff --git a/pkg/gitprovider/bitbucket/bitbucket_test.go b/pkg/gitprovider/bitbucket/bitbucket_test.go index 2b58f9794c..fe18126e71 100644 --- a/pkg/gitprovider/bitbucket/bitbucket_test.go +++ b/pkg/gitprovider/bitbucket/bitbucket_test.go @@ -1,52 +1,149 @@ package bitbucket import ( + "context" + "encoding/json" "errors" + "io" + "net/http" "testing" - "github.com/ktrysmt/go-bitbucket" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/akuity/kargo/pkg/gitprovider" ) -type mockPullRequestClient struct { - createFunc func(opt *bitbucket.PullRequestsOptions) (any, error) - getsFunc func(opt *bitbucket.PullRequestsOptions) (any, error) - getFunc func(opt *bitbucket.PullRequestsOptions) (any, error) - getCommitFunc func(opt *bitbucket.CommitsOptions) (any, error) - mergeFunc func(opt *bitbucket.PullRequestsOptions) (any, error) +// mockClient implements ClientWithResponsesInterface for testing. +type mockClient struct { + getCommitFunc func( + ctx context.Context, + workspace, repoSlug, commit string, + reqEditors ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) + + listPRsFunc func( + ctx context.Context, + workspace, repoSlug string, + params *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, + reqEditors ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) + + createPRFunc func( + ctx context.Context, + workspace, repoSlug string, + body PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, + reqEditors ...RequestEditorFn, + ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) + + getPRFunc func( + ctx context.Context, + workspace, repoSlug string, + pullRequestId int, + reqEditors ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) + + mergePRFunc func( + ctx context.Context, + workspace, repoSlug string, + pullRequestId int, + params *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, + body PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeJSONRequestBody, + reqEditors ...RequestEditorFn, + ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse, error) } -func (m *mockPullRequestClient) Create( - opt *bitbucket.PullRequestsOptions, -) (any, error) { - return m.createFunc(opt) +func (m *mockClient) GetRepositoriesWorkspaceRepoSlugCommitCommitWithResponse( + ctx context.Context, + workspace, repoSlug, commit string, + reqEditors ...RequestEditorFn, +) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) { + return m.getCommitFunc(ctx, workspace, repoSlug, commit, reqEditors...) } -func (m *mockPullRequestClient) Gets( - opt *bitbucket.PullRequestsOptions, -) (any, error) { - return m.getsFunc(opt) +func (m *mockClient) GetRepositoriesWorkspaceRepoSlugPullrequestsWithResponse( + ctx context.Context, + workspace, repoSlug string, + params *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, + reqEditors ...RequestEditorFn, +) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + return m.listPRsFunc(ctx, workspace, repoSlug, params, reqEditors...) } -func (m *mockPullRequestClient) Get( - opt *bitbucket.PullRequestsOptions, -) (any, error) { - return m.getFunc(opt) +func (m *mockClient) PostRepositoriesWorkspaceRepoSlugPullrequestsWithBodyWithResponse( + _ context.Context, + _, _ string, + _ string, + _ io.Reader, + _ ...RequestEditorFn, +) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + return nil, errors.New("not implemented") } -func (m *mockPullRequestClient) GetCommit(opt *bitbucket.CommitsOptions) (any, error) { - return m.getCommitFunc(opt) +func (m *mockClient) PostRepositoriesWorkspaceRepoSlugPullrequestsWithResponse( + ctx context.Context, + workspace, repoSlug string, + body PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, + reqEditors ...RequestEditorFn, +) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + return m.createPRFunc(ctx, workspace, repoSlug, body, reqEditors...) } -func (m *mockPullRequestClient) Merge( - opt *bitbucket.PullRequestsOptions, -) (any, error) { - return m.mergeFunc(opt) +func (m *mockClient) GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdWithResponse( + ctx context.Context, + workspace, repoSlug string, + pullRequestId int, + reqEditors ...RequestEditorFn, +) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { + return m.getPRFunc(ctx, workspace, repoSlug, pullRequestId, reqEditors...) } +func (m *mockClient) PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeWithBodyWithResponse( + _ context.Context, + _, _ string, + _ int, + _ *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, + _ string, + _ io.Reader, + _ ...RequestEditorFn, +) (*PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse, error) { + return nil, errors.New("not implemented") +} + +func (m *mockClient) PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeWithResponse( + ctx context.Context, + workspace, repoSlug string, + pullRequestId int, + params *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, + body PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeJSONRequestBody, + reqEditors ...RequestEditorFn, +) (*PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse, error) { + return m.mergePRFunc(ctx, workspace, repoSlug, pullRequestId, params, body, reqEditors...) +} + +// prFromJSON unmarshals a Pullrequest from JSON, for use in test cases. +func prFromJSON(t *testing.T, s string) *Pullrequest { + t.Helper() + var pr Pullrequest + require.NoError(t, json.Unmarshal([]byte(s), &pr)) + return &pr +} + +// statesFromEditors applies request editors to a fake request and returns the +// state query params — used to verify which states were passed to the list API. +func statesFromEditors(reqEditors []RequestEditorFn) []string { + req, _ := http.NewRequest(http.MethodGet, "http://api.bitbucket.org/test", nil) + for _, ed := range reqEditors { + _ = ed(context.Background(), req) + } + return req.URL.Query()["state"] +} + +func strPtr(s string) *string { return &s } +func intPtr(i int) *int { return &i } + +func statePtr(s PullrequestState) *PullrequestState { return &s } + func TestNewProvider(t *testing.T) { t.Run("successful creation", func(t *testing.T) { provider, err := NewProvider("https://bitbucket.org/owner/repo", &gitprovider.Options{Token: "token"}) @@ -81,49 +178,48 @@ func TestNewProvider(t *testing.T) { func TestCreatePullRequest(t *testing.T) { t.Run("successful creation", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - createFunc: func(*bitbucket.PullRequestsOptions) (any, error) { - return map[string]any{ - "id": int64(1), - "state": prStateOpen, - "links": map[string]any{ - "html": map[string]any{ - "href": "https://bitbucket.org/owner/repo/pull-requests/1", - }, - }, - "source": map[string]any{ - "branch": map[string]any{ - "name": "feature-branch", + mc := &mockClient{ + createPRFunc: func( + _ context.Context, + _, _ string, + body PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, + _ ...RequestEditorFn, + ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + require.NotNil(t, body.Title) + assert.Equal(t, "Test PR", *body.Title) + assert.Equal(t, "PR description", body.AdditionalProperties["description"]) + require.NotNil(t, body.Source) + require.NotNil(t, body.Source.Branch) + require.NotNil(t, body.Source.Branch.Name) + assert.Equal(t, "feature-branch", *body.Source.Branch.Name) + require.NotNil(t, body.Destination) + require.NotNil(t, body.Destination.Branch) + require.NotNil(t, body.Destination.Branch.Name) + assert.Equal(t, "main", *body.Destination.Branch.Name) + return &PostRepositoriesWorkspaceRepoSlugPullrequestsResponse{ + JSON201: prFromJSON(t, `{ + "id": 1, + "state": "OPEN", + "links": {"html": {"href": "https://bitbucket.org/owner/repo/pull-requests/1"}}, + "source": { + "branch": {"name": "feature-branch"}, + "commit": {"hash": "abcdef1234567890"} }, - "commit": map[string]any{ - "hash": "abcdef1234567890", - }, - }, - "destination": map[string]any{ - "branch": map[string]any{ - "name": "main", - }, - }, - "created_on": "2023-01-01T12:00:00Z", + "created_on": "2023-01-01T12:00:00Z", + "type": "pullrequest" + }`), }, nil }, } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, - } - - ctx := t.Context() - opts := &gitprovider.CreatePullRequestOpts{ + p := &provider{client: mc} + pr, err := p.CreatePullRequest(t.Context(), &gitprovider.CreatePullRequestOpts{ Title: "Test PR", Description: "PR description", Head: "feature-branch", Base: "main", - } - pr, err := provider.CreatePullRequest(ctx, opts) + }) assert.NoError(t, err) - assert.NotNil(t, pr) + require.NotNil(t, pr) assert.Equal(t, int64(1), pr.Number) assert.Equal(t, "https://bitbucket.org/owner/repo/pull-requests/1", pr.URL) assert.Equal(t, "abcdef1234567890", pr.HeadSHA) @@ -132,117 +228,106 @@ func TestCreatePullRequest(t *testing.T) { }) t.Run("successful creation with nil options", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - createFunc: func(*bitbucket.PullRequestsOptions) (any, error) { - return map[string]any{ - "id": int64(1), - "state": prStateOpen, + mc := &mockClient{ + createPRFunc: func( + _ context.Context, + _, _ string, + _ PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, + _ ...RequestEditorFn, + ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + return &PostRepositoriesWorkspaceRepoSlugPullrequestsResponse{ + JSON201: &Pullrequest{Id: intPtr(1), State: statePtr(PullrequestStateOPEN)}, }, nil }, } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, - } - - ctx := t.Context() - pr, err := provider.CreatePullRequest(ctx, nil) + p := &provider{client: mc} + pr, err := p.CreatePullRequest(t.Context(), nil) assert.NoError(t, err) - assert.NotNil(t, pr) + require.NotNil(t, pr) assert.Equal(t, int64(1), pr.Number) }) - t.Run("creation with merge commit", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - createFunc: func(*bitbucket.PullRequestsOptions) (any, error) { - return map[string]any{ - "id": int64(1), - "state": prStateOpen, - "merge_commit": map[string]any{ - "hash": "short123", + t.Run("creation with merge commit resolves full SHA", func(t *testing.T) { + mc := &mockClient{ + createPRFunc: func( + _ context.Context, + _, _ string, + _ PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, + _ ...RequestEditorFn, + ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + return &PostRepositoriesWorkspaceRepoSlugPullrequestsResponse{ + JSON201: &Pullrequest{ + Id: intPtr(1), + State: statePtr(PullrequestStateOPEN), + MergeCommit: &struct { + Hash *string `json:"hash,omitempty"` + }{Hash: strPtr("short123")}, }, }, nil }, - getCommitFunc: func(*bitbucket.CommitsOptions) (any, error) { - return map[string]any{ - "hash": "full1234567890abcdef", + getCommitFunc: func( + _ context.Context, + _, _, commit string, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) { + assert.Equal(t, "short123", commit) + return &GetRepositoriesWorkspaceRepoSlugCommitCommitResponse{ + JSON200: &Commit{Hash: strPtr("full1234567890abcdef")}, }, nil }, } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, - } - - ctx := t.Context() - pr, err := provider.CreatePullRequest(ctx, nil) + p := &provider{client: mc} + pr, err := p.CreatePullRequest(t.Context(), nil) assert.NoError(t, err) - assert.NotNil(t, pr) + require.NotNil(t, pr) assert.Equal(t, "full1234567890abcdef", pr.MergeCommitSHA) }) t.Run("error during creation", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - createFunc: func(*bitbucket.PullRequestsOptions) (any, error) { + mc := &mockClient{ + createPRFunc: func( + _ context.Context, + _, _ string, + _ PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, + _ ...RequestEditorFn, + ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { return nil, errors.New("creation failed") }, } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, - } - - ctx := t.Context() - pr, err := provider.CreatePullRequest(ctx, nil) - assert.Error(t, err) - assert.Nil(t, pr) - }) - - t.Run("error converting PR response", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - createFunc: func(*bitbucket.PullRequestsOptions) (any, error) { - // Return something that can't be properly unmarshaled - return make(chan int), nil - }, - } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, - } - - ctx := t.Context() - pr, err := provider.CreatePullRequest(ctx, nil) + p := &provider{client: mc} + pr, err := p.CreatePullRequest(t.Context(), nil) assert.Error(t, err) assert.Nil(t, pr) }) t.Run("error getting full commit SHA", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - createFunc: func(*bitbucket.PullRequestsOptions) (any, error) { - return map[string]any{ - "id": int64(1), - "state": prStateOpen, - "merge_commit": map[string]any{ - "hash": "short123", + mc := &mockClient{ + createPRFunc: func( + _ context.Context, + _, _ string, + _ PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, + _ ...RequestEditorFn, + ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + return &PostRepositoriesWorkspaceRepoSlugPullrequestsResponse{ + JSON201: &Pullrequest{ + Id: intPtr(1), + State: statePtr(PullrequestStateOPEN), + MergeCommit: &struct { + Hash *string `json:"hash,omitempty"` + }{Hash: strPtr("short123")}, }, }, nil }, - getCommitFunc: func(*bitbucket.CommitsOptions) (any, error) { + getCommitFunc: func( + _ context.Context, + _, _, _ string, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) { return nil, errors.New("commit fetch failed") }, } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, - } - - ctx := t.Context() - pr, err := provider.CreatePullRequest(ctx, nil) + p := &provider{client: mc} + pr, err := p.CreatePullRequest(t.Context(), nil) assert.Error(t, err) assert.Nil(t, pr) }) @@ -250,44 +335,33 @@ func TestCreatePullRequest(t *testing.T) { func TestGetPullRequest(t *testing.T) { t.Run("successful retrieval", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - getFunc: func(opt *bitbucket.PullRequestsOptions) (any, error) { - assert.Equal(t, "1", opt.ID) - return map[string]any{ - "id": int64(1), - "state": prStateOpen, - "links": map[string]any{ - "html": map[string]any{ - "href": "https://bitbucket.org/owner/repo/pull-requests/1", - }, - }, - "source": map[string]any{ - "branch": map[string]any{ - "name": "feature-branch", - }, - "commit": map[string]any{ - "hash": "abcdef1234567890", + mc := &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + pullRequestId int, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { + assert.Equal(t, 1, pullRequestId) + return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ + JSON200: prFromJSON(t, `{ + "id": 1, + "state": "OPEN", + "links": {"html": {"href": "https://bitbucket.org/owner/repo/pull-requests/1"}}, + "source": { + "branch": {"name": "feature-branch"}, + "commit": {"hash": "abcdef1234567890"} }, - }, - "destination": map[string]any{ - "branch": map[string]any{ - "name": "main", - }, - }, - "created_on": "2023-01-01T12:00:00Z", + "created_on": "2023-01-01T12:00:00Z", + "type": "pullrequest" + }`), }, nil }, } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, - } - - ctx := t.Context() - pr, err := provider.GetPullRequest(ctx, 1) + p := &provider{client: mc} + pr, err := p.GetPullRequest(t.Context(), 1) assert.NoError(t, err) - assert.NotNil(t, pr) + require.NotNil(t, pr) assert.Equal(t, int64(1), pr.Number) assert.Equal(t, "https://bitbucket.org/owner/repo/pull-requests/1", pr.URL) assert.Equal(t, "abcdef1234567890", pr.HeadSHA) @@ -296,71 +370,56 @@ func TestGetPullRequest(t *testing.T) { assert.NotNil(t, pr.CreatedAt) }) - t.Run("retrieval of merged PR", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - getFunc: func(*bitbucket.PullRequestsOptions) (any, error) { - return map[string]any{ - "id": int64(1), - "state": prStateMerged, - "merge_commit": map[string]any{ - "hash": "short123", + t.Run("retrieval of merged PR resolves full SHA", func(t *testing.T) { + mc := &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { + return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ + JSON200: &Pullrequest{ + Id: intPtr(1), + State: statePtr(PullrequestStateMERGED), + MergeCommit: &struct { + Hash *string `json:"hash,omitempty"` + }{Hash: strPtr("short123")}, }, }, nil }, - getCommitFunc: func(*bitbucket.CommitsOptions) (any, error) { - return map[string]any{ - "hash": "full1234567890abcdef", + getCommitFunc: func( + _ context.Context, + _, _, _ string, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) { + return &GetRepositoriesWorkspaceRepoSlugCommitCommitResponse{ + JSON200: &Commit{Hash: strPtr("full1234567890abcdef")}, }, nil }, } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, - } - - ctx := t.Context() - pr, err := provider.GetPullRequest(ctx, 1) + p := &provider{client: mc} + pr, err := p.GetPullRequest(t.Context(), 1) assert.NoError(t, err) - assert.NotNil(t, pr) + require.NotNil(t, pr) assert.False(t, pr.Open) assert.True(t, pr.Merged) assert.Equal(t, "full1234567890abcdef", pr.MergeCommitSHA) }) t.Run("error during retrieval", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - getFunc: func(*bitbucket.PullRequestsOptions) (any, error) { + mc := &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { return nil, errors.New("retrieval failed") }, } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, - } - - ctx := t.Context() - pr, err := provider.GetPullRequest(ctx, 1) - assert.Error(t, err) - assert.Nil(t, pr) - }) - - t.Run("error converting PR response", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - getFunc: func(*bitbucket.PullRequestsOptions) (any, error) { - // Return something that can't be properly unmarshaled - return make(chan int), nil - }, - } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, - } - - ctx := t.Context() - pr, err := provider.GetPullRequest(ctx, 1) + p := &provider{client: mc} + pr, err := p.GetPullRequest(t.Context(), 1) assert.Error(t, err) assert.Nil(t, pr) }) @@ -368,50 +427,55 @@ func TestGetPullRequest(t *testing.T) { func TestListPullRequests(t *testing.T) { t.Run("list open PRs by default", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - getsFunc: func(opt *bitbucket.PullRequestsOptions) (any, error) { - assert.Equal(t, []string{prStateOpen}, opt.States) - return map[string]any{"values": []any{ - map[string]any{"id": int64(1), "state": prStateOpen}, - map[string]any{"id": int64(2), "state": prStateOpen}, - }}, nil + mc := &mockClient{ + listPRsFunc: func( + _ context.Context, + _, _ string, + _ *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, + reqEditors ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + assert.Equal(t, []string{"OPEN"}, statesFromEditors(reqEditors)) + values := []Pullrequest{ + {Id: intPtr(1), State: statePtr(PullrequestStateOPEN)}, + {Id: intPtr(2), State: statePtr(PullrequestStateOPEN)}, + } + return &GetRepositoriesWorkspaceRepoSlugPullrequestsResponse{ + JSON200: &PaginatedPullrequests{Values: &values}, + }, nil }, } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, - } - - ctx := t.Context() - prs, err := provider.ListPullRequests(ctx, nil) + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), nil) assert.NoError(t, err) assert.Len(t, prs, 2) }) t.Run("list all PRs", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - getsFunc: func(opt *bitbucket.PullRequestsOptions) (any, error) { - assert.Contains(t, opt.States, prStateOpen) - assert.Contains(t, opt.States, prStateMerged) - assert.Contains(t, opt.States, prStateDeclined) - assert.Contains(t, opt.States, prStateSuperseded) - return map[string]any{"values": []any{ - map[string]any{"id": int64(1), "state": prStateOpen}, - map[string]any{"id": int64(2), "state": prStateMerged}, - map[string]any{"id": int64(3), "state": prStateDeclined}, - map[string]any{"id": int64(4), "state": prStateSuperseded}, - }}, nil + mc := &mockClient{ + listPRsFunc: func( + _ context.Context, + _, _ string, + _ *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, + reqEditors ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + states := statesFromEditors(reqEditors) + assert.Contains(t, states, "OPEN") + assert.Contains(t, states, "MERGED") + assert.Contains(t, states, "DECLINED") + assert.Contains(t, states, "SUPERSEDED") + values := []Pullrequest{ + {Id: intPtr(1), State: statePtr(PullrequestStateOPEN)}, + {Id: intPtr(2), State: statePtr(PullrequestStateMERGED)}, + {Id: intPtr(3), State: statePtr(PullrequestStateDECLINED)}, + {Id: intPtr(4), State: statePtr(PullrequestStateSUPERSEDED)}, + } + return &GetRepositoriesWorkspaceRepoSlugPullrequestsResponse{ + JSON200: &PaginatedPullrequests{Values: &values}, + }, nil }, } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, - } - - ctx := t.Context() - prs, err := provider.ListPullRequests(ctx, &gitprovider.ListPullRequestOptions{ + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ State: gitprovider.PullRequestStateAny, }) assert.NoError(t, err) @@ -419,27 +483,30 @@ func TestListPullRequests(t *testing.T) { }) t.Run("list closed PRs", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - getsFunc: func(opt *bitbucket.PullRequestsOptions) (any, error) { - assert.Contains(t, opt.States, prStateMerged) - assert.Contains(t, opt.States, prStateDeclined) - assert.Contains(t, opt.States, prStateSuperseded) - assert.NotContains(t, opt.States, prStateOpen) - return map[string]any{"values": []any{ - map[string]any{"id": int64(2), "state": prStateMerged}, - map[string]any{"id": int64(3), "state": prStateDeclined}, - map[string]any{"id": int64(4), "state": prStateSuperseded}, - }}, nil + mc := &mockClient{ + listPRsFunc: func( + _ context.Context, + _, _ string, + _ *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, + reqEditors ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + states := statesFromEditors(reqEditors) + assert.Contains(t, states, "MERGED") + assert.Contains(t, states, "DECLINED") + assert.Contains(t, states, "SUPERSEDED") + assert.NotContains(t, states, "OPEN") + values := []Pullrequest{ + {Id: intPtr(2), State: statePtr(PullrequestStateMERGED)}, + {Id: intPtr(3), State: statePtr(PullrequestStateDECLINED)}, + {Id: intPtr(4), State: statePtr(PullrequestStateSUPERSEDED)}, + } + return &GetRepositoriesWorkspaceRepoSlugPullrequestsResponse{ + JSON200: &PaginatedPullrequests{Values: &values}, + }, nil }, } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, - } - - ctx := t.Context() - prs, err := provider.ListPullRequests(ctx, &gitprovider.ListPullRequestOptions{ + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ State: gitprovider.PullRequestStateClosed, }) assert.NoError(t, err) @@ -447,54 +514,40 @@ func TestListPullRequests(t *testing.T) { }) t.Run("filter by head branch", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - getsFunc: func(*bitbucket.PullRequestsOptions) (any, error) { - return map[string]any{"values": []any{ - map[string]any{ - "id": int64(1), - "state": prStateOpen, - "source": map[string]any{ - "branch": map[string]any{ - "name": "feature-1", - }, - "commit": map[string]any{ - "hash": "hash1", - }, - }, - "destination": map[string]any{ - "branch": map[string]any{ - "name": "main", - }, - }, + mc := &mockClient{ + listPRsFunc: func( + _ context.Context, + _, _ string, + _ *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + values := []Pullrequest{ + { + Id: intPtr(1), + State: statePtr(PullrequestStateOPEN), + Source: &PullrequestEndpoint{Branch: &struct { + DefaultMergeStrategy *string `json:"default_merge_strategy,omitempty"` + MergeStrategies *[]PullrequestEndpointBranchMergeStrategies `json:"merge_strategies,omitempty"` + Name *string `json:"name,omitempty"` + }{Name: strPtr("feature-1")}}, }, - map[string]any{ - "id": int64(2), - "state": prStateOpen, - "source": map[string]any{ - "branch": map[string]any{ - "name": "feature-2", - }, - "commit": map[string]any{ - "hash": "hash2", - }, - }, - "destination": map[string]any{ - "branch": map[string]any{ - "name": "main", - }, - }, + { + Id: intPtr(2), + State: statePtr(PullrequestStateOPEN), + Source: &PullrequestEndpoint{Branch: &struct { + DefaultMergeStrategy *string `json:"default_merge_strategy,omitempty"` + MergeStrategies *[]PullrequestEndpointBranchMergeStrategies `json:"merge_strategies,omitempty"` + Name *string `json:"name,omitempty"` + }{Name: strPtr("feature-2")}}, }, - }}, nil + } + return &GetRepositoriesWorkspaceRepoSlugPullrequestsResponse{ + JSON200: &PaginatedPullrequests{Values: &values}, + }, nil }, } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, - } - - ctx := t.Context() - prs, err := provider.ListPullRequests(ctx, &gitprovider.ListPullRequestOptions{ + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ HeadBranch: "feature-1", }) assert.NoError(t, err) @@ -503,54 +556,40 @@ func TestListPullRequests(t *testing.T) { }) t.Run("filter by base branch", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - getsFunc: func(*bitbucket.PullRequestsOptions) (any, error) { - return map[string]any{"values": []any{ - map[string]any{ - "id": int64(1), - "state": prStateOpen, - "source": map[string]any{ - "branch": map[string]any{ - "name": "feature-1", - }, - "commit": map[string]any{ - "hash": "hash1", - }, - }, - "destination": map[string]any{ - "branch": map[string]any{ - "name": "main", - }, - }, + mc := &mockClient{ + listPRsFunc: func( + _ context.Context, + _, _ string, + _ *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + values := []Pullrequest{ + { + Id: intPtr(1), + State: statePtr(PullrequestStateOPEN), + Destination: &PullrequestEndpoint{Branch: &struct { + DefaultMergeStrategy *string `json:"default_merge_strategy,omitempty"` + MergeStrategies *[]PullrequestEndpointBranchMergeStrategies `json:"merge_strategies,omitempty"` + Name *string `json:"name,omitempty"` + }{Name: strPtr("main")}}, }, - map[string]any{ - "id": int64(2), - "state": prStateOpen, - "source": map[string]any{ - "branch": map[string]any{ - "name": "feature-2", - }, - "commit": map[string]any{ - "hash": "hash2", - }, - }, - "destination": map[string]any{ - "branch": map[string]any{ - "name": "dev", - }, - }, + { + Id: intPtr(2), + State: statePtr(PullrequestStateOPEN), + Destination: &PullrequestEndpoint{Branch: &struct { + DefaultMergeStrategy *string `json:"default_merge_strategy,omitempty"` + MergeStrategies *[]PullrequestEndpointBranchMergeStrategies `json:"merge_strategies,omitempty"` + Name *string `json:"name,omitempty"` + }{Name: strPtr("dev")}}, }, - }}, nil + } + return &GetRepositoriesWorkspaceRepoSlugPullrequestsResponse{ + JSON200: &PaginatedPullrequests{Values: &values}, + }, nil }, } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, - } - - ctx := t.Context() - prs, err := provider.ListPullRequests(ctx, &gitprovider.ListPullRequestOptions{ + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ BaseBranch: "dev", }) assert.NoError(t, err) @@ -559,44 +598,40 @@ func TestListPullRequests(t *testing.T) { }) t.Run("filter by head commit", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - getsFunc: func(*bitbucket.PullRequestsOptions) (any, error) { - return map[string]any{"values": []any{ - map[string]any{ - "id": int64(1), - "state": prStateOpen, - "source": map[string]any{ - "branch": map[string]any{ - "name": "feature-1", - }, - "commit": map[string]any{ - "hash": "specific-hash", - }, + mc := &mockClient{ + listPRsFunc: func( + _ context.Context, + _, _ string, + _ *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + values := []Pullrequest{ + { + Id: intPtr(1), + State: statePtr(PullrequestStateOPEN), + Source: &PullrequestEndpoint{ + Commit: &struct { + Hash *string `json:"hash,omitempty"` + }{Hash: strPtr("specific-hash")}, }, }, - map[string]any{ - "id": int64(2), - "state": prStateOpen, - "source": map[string]any{ - "branch": map[string]any{ - "name": "feature-2", - }, - "commit": map[string]any{ - "hash": "other-hash", - }, + { + Id: intPtr(2), + State: statePtr(PullrequestStateOPEN), + Source: &PullrequestEndpoint{ + Commit: &struct { + Hash *string `json:"hash,omitempty"` + }{Hash: strPtr("other-hash")}, }, }, - }}, nil + } + return &GetRepositoriesWorkspaceRepoSlugPullrequestsResponse{ + JSON200: &PaginatedPullrequests{Values: &values}, + }, nil }, } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, - } - - ctx := t.Context() - prs, err := provider.ListPullRequests(ctx, &gitprovider.ListPullRequestOptions{ + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ HeadCommit: "specific-hash", }) assert.NoError(t, err) @@ -604,118 +639,62 @@ func TestListPullRequests(t *testing.T) { assert.Equal(t, int64(1), prs[0].Number) }) - t.Run("PR with merge commit", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - getsFunc: func(*bitbucket.PullRequestsOptions) (any, error) { - return map[string]any{"values": []any{ - map[string]any{ - "id": int64(1), - "state": prStateMerged, - "merge_commit": map[string]any{ - "hash": "short123", - }, - }, - }}, nil + t.Run("PR with merge commit resolves full SHA", func(t *testing.T) { + mc := &mockClient{ + listPRsFunc: func( + _ context.Context, + _, _ string, + _ *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + values := []Pullrequest{{ + Id: intPtr(1), + State: statePtr(PullrequestStateMERGED), + MergeCommit: &struct { + Hash *string `json:"hash,omitempty"` + }{Hash: strPtr("short123")}, + }} + return &GetRepositoriesWorkspaceRepoSlugPullrequestsResponse{ + JSON200: &PaginatedPullrequests{Values: &values}, + }, nil }, - getCommitFunc: func(*bitbucket.CommitsOptions) (any, error) { - return map[string]any{ - "hash": "full1234567890abcdef", + getCommitFunc: func( + _ context.Context, + _, _, _ string, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) { + return &GetRepositoriesWorkspaceRepoSlugCommitCommitResponse{ + JSON200: &Commit{Hash: strPtr("full1234567890abcdef")}, }, nil }, } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, - } - - ctx := t.Context() - prs, err := provider.ListPullRequests(ctx, nil) + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), nil) assert.NoError(t, err) assert.Len(t, prs, 1) assert.Equal(t, "full1234567890abcdef", prs[0].MergeCommitSHA) }) t.Run("error during list", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - getsFunc: func(*bitbucket.PullRequestsOptions) (any, error) { + mc := &mockClient{ + listPRsFunc: func( + _ context.Context, + _, _ string, + _ *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { return nil, errors.New("list failed") }, } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, - } - - ctx := t.Context() - prs, err := provider.ListPullRequests(ctx, nil) - assert.Error(t, err) - assert.Nil(t, prs) - }) - - t.Run("invalid response format", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - getsFunc: func(*bitbucket.PullRequestsOptions) (any, error) { - return "not a map", nil - }, - } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, - } - - ctx := t.Context() - prs, err := provider.ListPullRequests(ctx, nil) - assert.Error(t, err) - assert.Nil(t, prs) - }) - - t.Run("missing values field", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - getsFunc: func(*bitbucket.PullRequestsOptions) (any, error) { - return map[string]any{}, nil - }, - } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, - } - - ctx := t.Context() - prs, err := provider.ListPullRequests(ctx, nil) - assert.Error(t, err) - assert.Nil(t, prs) - }) - - t.Run("invalid values type", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - getsFunc: func(*bitbucket.PullRequestsOptions) (any, error) { - return map[string]any{"values": "not an array"}, nil - }, - } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, - } - - ctx := t.Context() - prs, err := provider.ListPullRequests(ctx, nil) + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), nil) assert.Error(t, err) assert.Nil(t, prs) }) t.Run("invalid state", func(t *testing.T) { - provider := &provider{ - owner: "owner", - repoSlug: "repo", - } - - ctx := t.Context() - prs, err := provider.ListPullRequests(ctx, &gitprovider.ListPullRequestOptions{ + p := &provider{client: &mockClient{}} + prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ State: "invalid-state", }) assert.Error(t, err) @@ -728,7 +707,7 @@ func TestMergePullRequest(t *testing.T) { name string prNumber int64 mergeOpts *gitprovider.MergePullRequestOpts - mockClient *mockPullRequestClient + mockClient *mockClient expectedMerged bool expectError bool errorContains string @@ -736,45 +715,36 @@ func TestMergePullRequest(t *testing.T) { { name: "error getting PR", prNumber: 999, - mockClient: &mockPullRequestClient{ - getFunc: func(*bitbucket.PullRequestsOptions) (any, error) { + mockClient: &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { return nil, errors.New("get PR failed") }, }, expectError: true, errorContains: "error getting pull request", }, - { - name: "error parsing PR response", - prNumber: 999, - mockClient: &mockPullRequestClient{ - getFunc: func(*bitbucket.PullRequestsOptions) (any, error) { - return "not a valid response", nil - }, - }, - expectError: true, - errorContains: "error parsing pull request response", - }, { name: "PR already merged", prNumber: 123, - mockClient: &mockPullRequestClient{ - getFunc: func(*bitbucket.PullRequestsOptions) (any, error) { - return map[string]any{ - "id": int64(123), - "state": prStateMerged, - "links": map[string]any{ - "html": map[string]any{ - "href": "https://bitbucket.org/owner/repo/pull-requests/123", - }, - }, - "merge_commit": map[string]any{ - "hash": "merge_sha", - }, - "source": map[string]any{ - "commit": map[string]any{ - "hash": "head_sha", - }, + mockClient: &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { + return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ + JSON200: &Pullrequest{ + Id: intPtr(123), + State: statePtr(PullrequestStateMERGED), + MergeCommit: &struct { + Hash *string `json:"hash,omitempty"` + }{Hash: strPtr("merge_sha")}, }, }, nil }, @@ -784,11 +754,15 @@ func TestMergePullRequest(t *testing.T) { { name: "PR declined", prNumber: 456, - mockClient: &mockPullRequestClient{ - getFunc: func(*bitbucket.PullRequestsOptions) (any, error) { - return map[string]any{ - "id": int64(456), - "state": prStateDeclined, + mockClient: &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { + return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ + JSON200: &Pullrequest{Id: intPtr(456), State: statePtr(PullrequestStateDECLINED)}, }, nil }, }, @@ -798,42 +772,65 @@ func TestMergePullRequest(t *testing.T) { { name: "PR is draft", prNumber: 333, - mockClient: &mockPullRequestClient{ - getFunc: func(*bitbucket.PullRequestsOptions) (any, error) { - return map[string]any{ - "id": int64(333), - "state": prStateOpen, - "draft": true, + mockClient: &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { + isDraft := true + return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ + JSON200: &Pullrequest{ + Id: intPtr(333), + State: statePtr(PullrequestStateOPEN), + Draft: &isDraft, + }, }, nil }, }, }, { - name: "merge method specified", + name: "unsupported merge strategy", prNumber: 100, - mergeOpts: &gitprovider.MergePullRequestOpts{MergeMethod: "squash"}, - mockClient: &mockPullRequestClient{ - getFunc: func(*bitbucket.PullRequestsOptions) (any, error) { - return map[string]any{ - "id": int64(100), - "state": prStateOpen, + mergeOpts: &gitprovider.MergePullRequestOpts{MergeMethod: "rebase"}, + mockClient: &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { + return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ + JSON200: &Pullrequest{Id: intPtr(100), State: statePtr(PullrequestStateOPEN)}, }, nil }, }, expectError: true, - errorContains: "does not yet support specifying a merge method", + errorContains: "unsupported merge strategy", }, { name: "merge operation fails", prNumber: 888, - mockClient: &mockPullRequestClient{ - getFunc: func(*bitbucket.PullRequestsOptions) (any, error) { - return map[string]any{ - "id": int64(888), - "state": prStateOpen, + mockClient: &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { + return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ + JSON200: &Pullrequest{Id: intPtr(888), State: statePtr(PullrequestStateOPEN)}, }, nil }, - mergeFunc: func(*bitbucket.PullRequestsOptions) (any, error) { + mergePRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, + _ PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeJSONRequestBody, + _ ...RequestEditorFn, + ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse, error) { return nil, errors.New("merge failed") }, }, @@ -841,58 +838,67 @@ func TestMergePullRequest(t *testing.T) { errorContains: "error merging pull request", }, { - name: "error parsing merge response", - prNumber: 777, - mockClient: &mockPullRequestClient{ - getFunc: func(*bitbucket.PullRequestsOptions) (any, error) { - return map[string]any{ - "id": int64(777), - "state": prStateOpen, + name: "successful merge with strategy", + prNumber: 200, + mergeOpts: &gitprovider.MergePullRequestOpts{MergeMethod: "squash"}, + mockClient: &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { + return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ + JSON200: &Pullrequest{Id: intPtr(200), State: statePtr(PullrequestStateOPEN)}, }, nil }, - mergeFunc: func(*bitbucket.PullRequestsOptions) (any, error) { - return "not a valid response", nil + mergePRFunc: func( + _ context.Context, + _, _ string, + pullRequestId int, + _ *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, + body PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeJSONRequestBody, + _ ...RequestEditorFn, + ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse, error) { + assert.Equal(t, 200, pullRequestId) + require.NotNil(t, body.MergeStrategy) + assert.Equal(t, Squash, *body.MergeStrategy) + return &PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse{ + JSON200: &Pullrequest{Id: intPtr(200), State: statePtr(PullrequestStateMERGED)}, + }, nil }, }, - expectError: true, - errorContains: "error parsing merged pull request response", + expectedMerged: true, }, { name: "successful merge", prNumber: 1234, - mockClient: &mockPullRequestClient{ - getFunc: func(*bitbucket.PullRequestsOptions) (any, error) { - return map[string]any{ - "id": int64(1234), - "state": prStateOpen, - "links": map[string]any{ - "html": map[string]any{ - "href": "https://bitbucket.org/owner/repo/pull-requests/1234", - }, - }, - "source": map[string]any{ - "commit": map[string]any{ - "hash": "head_sha", - }, - }, + mockClient: &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { + return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ + JSON200: &Pullrequest{Id: intPtr(1234), State: statePtr(PullrequestStateOPEN)}, }, nil }, - mergeFunc: func(*bitbucket.PullRequestsOptions) (any, error) { - return map[string]any{ - "id": int64(1234), - "state": prStateMerged, - "links": map[string]any{ - "html": map[string]any{ - "href": "https://bitbucket.org/owner/repo/pull-requests/1234", - }, - }, - "merge_commit": map[string]any{ - "hash": "merge_sha", - }, - "source": map[string]any{ - "commit": map[string]any{ - "hash": "head_sha", - }, + mergePRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, + _ PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeJSONRequestBody, + _ ...RequestEditorFn, + ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse, error) { + return &PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse{ + JSON200: &Pullrequest{ + Id: intPtr(1234), + State: statePtr(PullrequestStateMERGED), + MergeCommit: &struct { + Hash *string `json:"hash,omitempty"` + }{Hash: strPtr("merge_sha")}, }, }, nil }, @@ -903,14 +909,8 @@ func TestMergePullRequest(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - p := &provider{ - owner: "owner", - repoSlug: "repo", - client: tc.mockClient, - } - + p := &provider{client: tc.mockClient} pr, merged, err := p.MergePullRequest(t.Context(), tc.prNumber, tc.mergeOpts) - if tc.expectError { require.Error(t, err) require.Contains(t, err.Error(), tc.errorContains) @@ -918,7 +918,6 @@ func TestMergePullRequest(t *testing.T) { require.Nil(t, pr) return } - require.NoError(t, err) require.Equal(t, tc.expectedMerged, merged) if tc.expectedMerged { @@ -929,112 +928,69 @@ func TestMergePullRequest(t *testing.T) { } } -func TestGetFullCommitSHA(t *testing.T) { - t.Run("successful retrieval", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - getCommitFunc: func(opt *bitbucket.CommitsOptions) (any, error) { - assert.Equal(t, "short123", opt.Revision) - return map[string]any{ - "hash": "full1234567890abcdef", - }, nil - }, - } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, - } - - ctx := t.Context() - sha, err := provider.getFullCommitSHA(ctx, "short123") +func TestResolveFullMergeCommitSHA(t *testing.T) { + t.Run("no-op when merge commit is nil", func(t *testing.T) { + p := &provider{client: &mockClient{}} + pr := &Pullrequest{} + err := p.resolveFullMergeCommitSHA(t.Context(), pr) assert.NoError(t, err) - assert.Equal(t, "full1234567890abcdef", sha) + assert.Nil(t, pr.MergeCommit) }) - t.Run("empty SHA input", func(t *testing.T) { - provider := &provider{ - owner: "owner", - repoSlug: "repo", + t.Run("no-op when merge commit hash is empty", func(t *testing.T) { + p := &provider{client: &mockClient{}} + pr := &Pullrequest{ + MergeCommit: &struct { + Hash *string `json:"hash,omitempty"` + }{Hash: strPtr("")}, } - - ctx := t.Context() - sha, err := provider.getFullCommitSHA(ctx, "") + err := p.resolveFullMergeCommitSHA(t.Context(), pr) assert.NoError(t, err) - assert.Equal(t, "", sha) }) - t.Run("error during retrieval", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - getCommitFunc: func(*bitbucket.CommitsOptions) (any, error) { - return nil, errors.New("retrieval failed") - }, - } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, - } - - ctx := t.Context() - sha, err := provider.getFullCommitSHA(ctx, "short123") - assert.Error(t, err) - assert.Equal(t, "", sha) - }) - - t.Run("invalid response format", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - getCommitFunc: func(*bitbucket.CommitsOptions) (any, error) { - return "not a map", nil - }, - } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, - } - - ctx := t.Context() - sha, err := provider.getFullCommitSHA(ctx, "short123") - assert.Error(t, err) - assert.Equal(t, "", sha) - }) - - t.Run("missing hash field", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - getCommitFunc: func(*bitbucket.CommitsOptions) (any, error) { - return map[string]any{}, nil + t.Run("resolves short SHA to full SHA", func(t *testing.T) { + mc := &mockClient{ + getCommitFunc: func( + _ context.Context, + _, _, commit string, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) { + assert.Equal(t, "short123", commit) + return &GetRepositoriesWorkspaceRepoSlugCommitCommitResponse{ + JSON200: &Commit{Hash: strPtr("full1234567890abcdef")}, + }, nil }, } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, + p := &provider{client: mc} + pr := &Pullrequest{ + MergeCommit: &struct { + Hash *string `json:"hash,omitempty"` + }{Hash: strPtr("short123")}, } - - ctx := t.Context() - sha, err := provider.getFullCommitSHA(ctx, "short123") - assert.Error(t, err) - assert.Equal(t, "", sha) + err := p.resolveFullMergeCommitSHA(t.Context(), pr) + assert.NoError(t, err) + require.NotNil(t, pr.MergeCommit.Hash) + assert.Equal(t, "full1234567890abcdef", *pr.MergeCommit.Hash) }) - t.Run("invalid hash type", func(t *testing.T) { - mockClient := &mockPullRequestClient{ - getCommitFunc: func(*bitbucket.CommitsOptions) (any, error) { - return map[string]any{ - "hash": 12345, // Not a string - }, nil + t.Run("error from getCommit is propagated", func(t *testing.T) { + mc := &mockClient{ + getCommitFunc: func( + _ context.Context, + _, _, _ string, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) { + return nil, errors.New("retrieval failed") }, } - provider := &provider{ - owner: "owner", - repoSlug: "repo", - client: mockClient, + p := &provider{client: mc} + pr := &Pullrequest{ + MergeCommit: &struct { + Hash *string `json:"hash,omitempty"` + }{Hash: strPtr("short123")}, } - - ctx := t.Context() - sha, err := provider.getFullCommitSHA(ctx, "short123") + err := p.resolveFullMergeCommitSHA(t.Context(), pr) assert.Error(t, err) - assert.Equal(t, "", sha) }) } @@ -1053,7 +1009,6 @@ func TestParseRepoURL(t *testing.T) { wantHost: "bitbucket.org", wantOwner: "owner", wantSlug: "repo", - wantErr: false, }, { name: "valid URL with trailing slash", @@ -1061,7 +1016,6 @@ func TestParseRepoURL(t *testing.T) { wantHost: "bitbucket.org", wantOwner: "owner", wantSlug: "repo", - wantErr: false, }, { name: "valid URL with .git suffix", @@ -1069,7 +1023,6 @@ func TestParseRepoURL(t *testing.T) { wantHost: "bitbucket.org", wantOwner: "owner", wantSlug: "repo", - wantErr: false, }, { name: "valid SSH URL", @@ -1077,7 +1030,6 @@ func TestParseRepoURL(t *testing.T) { wantHost: "bitbucket.org", wantOwner: "owner", wantSlug: "repo", - wantErr: false, }, { name: "invalid URL format", @@ -1111,98 +1063,17 @@ func TestParseRepoURL(t *testing.T) { } } -func Test_toBitbucketPR(t *testing.T) { - t.Run("valid conversion", func(t *testing.T) { - resp := map[string]any{ - "id": int64(1), - "state": prStateOpen, - "links": map[string]any{ - "html": map[string]any{ - "href": "https://bitbucket.org/owner/repo/pull-requests/1", - }, - }, - "source": map[string]any{ - "branch": map[string]any{ - "name": "feature-branch", - }, - "commit": map[string]any{ - "hash": "abcdef1234567890", - }, - }, - "destination": map[string]any{ - "branch": map[string]any{ - "name": "main", - }, - "commit": map[string]any{ - "hash": "1234567890abcdef", - }, - }, - "merge_commit": map[string]any{ - "hash": "merged1234567890", - }, - "created_on": "2023-01-01T12:00:00Z", - } - - pr, err := toBitbucketPR(resp) - require.NoError(t, err) - assert.Equal(t, int64(1), pr.ID) - assert.Equal(t, prStateOpen, pr.State) - assert.Equal(t, "https://bitbucket.org/owner/repo/pull-requests/1", pr.Links.HTML.Href) - assert.Equal(t, "feature-branch", pr.Source.Branch.Name) - assert.Equal(t, "abcdef1234567890", pr.Source.Commit.Hash) - assert.Equal(t, "main", pr.Destination.Branch.Name) - assert.Equal(t, "1234567890abcdef", pr.Destination.Commit.Hash) - assert.Equal(t, "merged1234567890", pr.MergeCommit.Hash) - assert.Equal(t, "2023-01-01T12:00:00Z", pr.CreatedOn) - }) - - t.Run("invalid input type", func(t *testing.T) { - pr, err := toBitbucketPR(make(chan int)) - assert.Error(t, err) - assert.Nil(t, pr) - }) -} - func Test_toProviderPR(t *testing.T) { t.Run("valid conversion: open PR", func(t *testing.T) { - bbPR := &bitbucketPR{ - ID: 1, - State: prStateOpen, - Links: struct { - HTML struct { - Href string `json:"href"` - } `json:"html"` - }{ - HTML: struct { - Href string `json:"href"` - }{ - Href: "https://bitbucket.org/owner/repo/pull-requests/1", - }, - }, - Source: struct { - Branch struct { - Name string `json:"name"` - } `json:"branch"` - Commit struct { - Hash string `json:"hash"` - } `json:"commit"` - }{ - Branch: struct { - Name string `json:"name"` - }{ - Name: "feature-branch", - }, - Commit: struct { - Hash string `json:"hash"` - }{ - Hash: "abcdef1234567890", - }, - }, - CreatedOn: "2023-01-01T12:00:00Z", - } - - raw := map[string]any{"id": 1} - pr := toProviderPR(bbPR, raw) + bbPR := prFromJSON(t, `{ + "id": 1, + "state": "OPEN", + "links": {"html": {"href": "https://bitbucket.org/owner/repo/pull-requests/1"}}, + "source": {"commit": {"hash": "abcdef1234567890"}}, + "created_on": "2023-01-01T12:00:00Z", + "type": "pullrequest" + }`) + pr := toProviderPR(bbPR) require.NotNil(t, pr) assert.Equal(t, int64(1), pr.Number) assert.Equal(t, "https://bitbucket.org/owner/repo/pull-requests/1", pr.URL) @@ -1210,22 +1081,18 @@ func Test_toProviderPR(t *testing.T) { assert.True(t, pr.Open) assert.False(t, pr.Merged) assert.NotNil(t, pr.CreatedAt) - assert.Equal(t, raw, pr.Object) + assert.Equal(t, bbPR, pr.Object) }) t.Run("valid conversion: merged PR", func(t *testing.T) { - bbPR := &bitbucketPR{ - ID: 1, - State: prStateMerged, - MergeCommit: struct { - Hash string `json:"hash"` - }{ - Hash: "merged1234567890", - }, - CreatedOn: "2023-01-01T12:00:00Z", + bbPR := &Pullrequest{ + Id: intPtr(1), + State: statePtr(PullrequestStateMERGED), + MergeCommit: &struct { + Hash *string `json:"hash,omitempty"` + }{Hash: strPtr("merged1234567890")}, } - - pr := toProviderPR(bbPR, nil) + pr := toProviderPR(bbPR) require.NotNil(t, pr) assert.False(t, pr.Open) assert.True(t, pr.Merged) @@ -1233,17 +1100,11 @@ func Test_toProviderPR(t *testing.T) { }) t.Run("nil input", func(t *testing.T) { - pr := toProviderPR(nil, nil) - assert.Nil(t, pr) + assert.Nil(t, toProviderPR(nil)) }) - t.Run("invalid date", func(t *testing.T) { - bbPR := &bitbucketPR{ - ID: 1, - CreatedOn: "not-a-date", - } - - pr := toProviderPR(bbPR, nil) + t.Run("no created_on", func(t *testing.T) { + pr := toProviderPR(&Pullrequest{Id: intPtr(1)}) require.NotNil(t, pr) assert.Nil(t, pr.CreatedAt) }) @@ -1251,18 +1112,15 @@ func Test_toProviderPR(t *testing.T) { func Test_registration(t *testing.T) { t.Run("predicate matches bitbucket.org URL", func(t *testing.T) { - result := registration.Predicate("https://bitbucket.org/owner/repo") - assert.True(t, result) + assert.True(t, registration.Predicate("https://bitbucket.org/owner/repo")) }) t.Run("predicate doesn't match other URLs", func(t *testing.T) { - result := registration.Predicate("https://github.com/owner/repo") - assert.False(t, result) + assert.False(t, registration.Predicate("https://github.com/owner/repo")) }) t.Run("predicate handles invalid URLs", func(t *testing.T) { - result := registration.Predicate("://invalid-url") - assert.False(t, result) + assert.False(t, registration.Predicate("://invalid-url")) }) t.Run("NewProvider factory works", func(t *testing.T) { @@ -1300,13 +1158,12 @@ func TestGetCommitURL(t *testing.T) { }, } - prov := &provider{} - - for _, testCase := range testCases { - t.Run(testCase.repoURL, func(t *testing.T) { - commitURL, err := prov.GetCommitURL(testCase.repoURL, testCase.sha) + p := &provider{} + for _, tc := range testCases { + t.Run(tc.repoURL, func(t *testing.T) { + commitURL, err := p.GetCommitURL(tc.repoURL, tc.sha) require.NoError(t, err) - require.Equal(t, testCase.expectedCommitURL, commitURL) + require.Equal(t, tc.expectedCommitURL, commitURL) }) } } From b58463d7b882e3ffa73edd07cbbddfa5d058a04b Mon Sep 17 00:00:00 2001 From: fuskovic Date: Mon, 20 Apr 2026 17:39:53 +0530 Subject: [PATCH 07/18] go mod tidy Signed-off-by: fuskovic --- go.mod | 6 +++--- go.sum | 14 ++++++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index db05f115ac..0c43e3de96 100644 --- a/go.mod +++ b/go.mod @@ -52,7 +52,6 @@ require ( github.com/jferrl/go-githubauth v1.5.1 github.com/kelseyhightower/envconfig v1.4.0 github.com/klauspost/compress v1.18.5 - github.com/ktrysmt/go-bitbucket v0.9.87 github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0 github.com/oklog/ulid/v2 v2.1.1 github.com/otiai10/copy v1.14.1 @@ -110,6 +109,7 @@ require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect @@ -209,14 +209,13 @@ require ( github.com/leodido/go-urn v1.4.0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/spdystream v0.5.1 // indirect github.com/moby/term v0.5.2 // indirect @@ -225,6 +224,7 @@ require ( github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/oapi-codegen/runtime v1.4.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/otiai10/mint v1.6.3 // indirect diff --git a/go.sum b/go.sum index a26e1fb47e..af5b2ccd6b 100644 --- a/go.sum +++ b/go.sum @@ -52,8 +52,11 @@ github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8 github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= @@ -94,6 +97,7 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bmatcuk/doublestar/v4 v4.9.2 h1:b0mc6WyRSYLjzofB2v/0cuDUZ+MqoGyH3r0dVij35GI= github.com/bmatcuk/doublestar/v4 v4.9.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= @@ -354,6 +358,7 @@ github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= @@ -370,8 +375,6 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/ktrysmt/go-bitbucket v0.9.87 h1:eR7E4ndyKpO2+HdBwUlsC5K/40nEDpjMLEWsPLY97oQ= -github.com/ktrysmt/go-bitbucket v0.9.87/go.mod h1:slSdGm9Vh3L2ZOU1r7Fu2B9rPJvsflYgneRCoPA83eY= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= @@ -387,6 +390,8 @@ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9 github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -407,8 +412,6 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/spdystream v0.5.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y= @@ -427,6 +430,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/oapi-codegen/runtime v1.4.0 h1:KLOSFOp7UzkbS7Cs1ms6NBEKYr0WmH2wZG0KKbd2er4= +github.com/oapi-codegen/runtime v1.4.0/go.mod h1:5sw5fxCDmnOzKNYmkVNF8d34kyUeejJEY8HNT2WaPec= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= @@ -502,6 +507,7 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= From db875ce98f000c5cf9f535e5843422b5bcd89229 Mon Sep 17 00:00:00 2001 From: fuskovic Date: Mon, 20 Apr 2026 17:40:01 +0530 Subject: [PATCH 08/18] add doc for merge strategy support Signed-off-by: fuskovic --- .../60-reference-docs/30-promotion-steps/git-merge-pr.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 871f857436..2432c1b106 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 @@ -61,7 +61,7 @@ currently supported Git hosting providers. | Provider | Supported Methods | Default | | -------- | ----------------- | ------- | | Azure |
  • `noFastForward`
  • `rebase`
  • `rebaseMerge`
  • `squash`
| First allowed strategy per the target branch's merge type policy; merge commit if no policy is configured | -| BitBucket | Client does not yet support specifying a merge strategy | The repository's configured default merge strategy | +| BitBucket |
  • `fast_forward`
  • `merge_commit`
  • `squash`
| The repository's configured default merge strategy | | Gitea |
  • `fast-forward-only`
  • `manually-merged`
  • `merge`
  • `rebase`
  • `rebase-merge`
  • `squash`
| `merge` | | GitHub |
  • `merge`
  • `rebase`
  • `squash`
| `merge` | | GitLab |
  • `merge`
  • `squash`
| Defers to the merge request and project-level squash settings | From fdd9093b018b6fdd4234441e1ece9ca4d58feffc Mon Sep 17 00:00:00 2001 From: fuskovic Date: Fri, 24 Apr 2026 19:54:25 +0530 Subject: [PATCH 09/18] prefix 'zz_' to generated client Signed-off-by: fuskovic --- pkg/gitprovider/bitbucket/spec/oapi-codegen.yaml | 2 +- .../{bitbucket_client.gen.go => zz_bitbucket_client.gen.go} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename pkg/gitprovider/bitbucket/{bitbucket_client.gen.go => zz_bitbucket_client.gen.go} (100%) diff --git a/pkg/gitprovider/bitbucket/spec/oapi-codegen.yaml b/pkg/gitprovider/bitbucket/spec/oapi-codegen.yaml index dc439eb2a2..4c89eae3b3 100644 --- a/pkg/gitprovider/bitbucket/spec/oapi-codegen.yaml +++ b/pkg/gitprovider/bitbucket/spec/oapi-codegen.yaml @@ -4,7 +4,7 @@ generate: models: true client: true -output: bitbucket_client.gen.go +output: zz_bitbucket_client.gen.go output-options: skip-prune: false diff --git a/pkg/gitprovider/bitbucket/bitbucket_client.gen.go b/pkg/gitprovider/bitbucket/zz_bitbucket_client.gen.go similarity index 100% rename from pkg/gitprovider/bitbucket/bitbucket_client.gen.go rename to pkg/gitprovider/bitbucket/zz_bitbucket_client.gen.go From 91de519ac852b97200408cbb62f4e3ca72fa7d61 Mon Sep 17 00:00:00 2001 From: fuskovic Date: Fri, 24 Apr 2026 19:55:59 +0530 Subject: [PATCH 10/18] remove 'codegen-bitbucket-client' from codegen target Signed-off-by: fuskovic --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b367ea1981..7d62fecbd4 100644 --- a/Makefile +++ b/Makefile @@ -218,7 +218,7 @@ build-cli-with-ui: build-ui build-cli ################################################################################ .PHONY: codegen -codegen: codegen-openapi codegen-proto codegen-controller codegen-schema-to-go codegen-bitbucket-client codegen-ui codegen-docs +codegen: codegen-openapi codegen-proto codegen-controller codegen-schema-to-go codegen-ui codegen-docs .PHONY: codegen-openapi codegen-openapi: install-swag install-go-swagger install-jq From 412ded4fcde16821e5846b6a478bb38ac2379aff Mon Sep 17 00:00:00 2001 From: fuskovic Date: Fri, 24 Apr 2026 19:58:27 +0530 Subject: [PATCH 11/18] include 'cloud' in generated client Signed-off-by: fuskovic --- pkg/gitprovider/bitbucket/spec/oapi-codegen.yaml | 2 +- ...bitbucket_client.gen.go => zz_bitbucket_cloud_client.gen.go} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename pkg/gitprovider/bitbucket/{zz_bitbucket_client.gen.go => zz_bitbucket_cloud_client.gen.go} (100%) diff --git a/pkg/gitprovider/bitbucket/spec/oapi-codegen.yaml b/pkg/gitprovider/bitbucket/spec/oapi-codegen.yaml index 4c89eae3b3..9e8ef2d271 100644 --- a/pkg/gitprovider/bitbucket/spec/oapi-codegen.yaml +++ b/pkg/gitprovider/bitbucket/spec/oapi-codegen.yaml @@ -4,7 +4,7 @@ generate: models: true client: true -output: zz_bitbucket_client.gen.go +output: zz_bitbucket_cloud_client.gen.go output-options: skip-prune: false diff --git a/pkg/gitprovider/bitbucket/zz_bitbucket_client.gen.go b/pkg/gitprovider/bitbucket/zz_bitbucket_cloud_client.gen.go similarity index 100% rename from pkg/gitprovider/bitbucket/zz_bitbucket_client.gen.go rename to pkg/gitprovider/bitbucket/zz_bitbucket_cloud_client.gen.go From 49ce9b9bd54dcd5f7602102f3c02897860b3f6a1 Mon Sep 17 00:00:00 2001 From: fuskovic Date: Fri, 24 Apr 2026 20:31:40 +0530 Subject: [PATCH 12/18] support datacenter Signed-off-by: fuskovic --- Makefile | 3 +- pkg/gitprovider/bitbucket/bitbucket.go | 463 +------ pkg/gitprovider/bitbucket/bitbucket_test.go | 1154 +---------------- pkg/gitprovider/bitbucket/cloud/provider.go | 461 +++++++ .../bitbucket/cloud/provider_test.go | 1143 ++++++++++++++++ .../{ => cloud}/spec/bitbucket.gen.json | 0 .../{ => cloud}/spec/oapi-codegen.yaml | 2 +- .../zz_bitbucket_cloud_client.gen.go | 4 +- .../bitbucket/datacenter/provider.go | 53 + .../spec/bitbucket-datacenter.gen.json | 357 +++++ .../datacenter/spec/oapi-codegen.yaml | 10 + .../zz_bitbucket_datacenter_client.gen.go | 1078 +++++++++++++++ 12 files changed, 3140 insertions(+), 1588 deletions(-) create mode 100644 pkg/gitprovider/bitbucket/cloud/provider.go create mode 100644 pkg/gitprovider/bitbucket/cloud/provider_test.go rename pkg/gitprovider/bitbucket/{ => cloud}/spec/bitbucket.gen.json (100%) rename pkg/gitprovider/bitbucket/{ => cloud}/spec/oapi-codegen.yaml (86%) rename pkg/gitprovider/bitbucket/{ => cloud}/zz_bitbucket_cloud_client.gen.go (99%) create mode 100644 pkg/gitprovider/bitbucket/datacenter/provider.go create mode 100644 pkg/gitprovider/bitbucket/datacenter/spec/bitbucket-datacenter.gen.json create mode 100644 pkg/gitprovider/bitbucket/datacenter/spec/oapi-codegen.yaml create mode 100644 pkg/gitprovider/bitbucket/datacenter/zz_bitbucket_datacenter_client.gen.go diff --git a/Makefile b/Makefile index 7d62fecbd4..98e4f9826e 100644 --- a/Makefile +++ b/Makefile @@ -263,7 +263,8 @@ codegen-controller: install-controller-gen .PHONY: codegen-bitbucket-client codegen-bitbucket-client: install-oapi-codegen - cd pkg/gitprovider/bitbucket && $(OAPI_CODEGEN_LINK) --config spec/oapi-codegen.yaml spec/bitbucket.gen.json + 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/pkg/gitprovider/bitbucket/bitbucket.go b/pkg/gitprovider/bitbucket/bitbucket.go index eed8482cc0..9095ba33b7 100644 --- a/pkg/gitprovider/bitbucket/bitbucket.go +++ b/pkg/gitprovider/bitbucket/bitbucket.go @@ -1,29 +1,16 @@ package bitbucket import ( - "context" "fmt" - "net/http" "net/url" - "strings" - - "github.com/hashicorp/go-cleanhttp" "github.com/akuity/kargo/pkg/gitprovider" + "github.com/akuity/kargo/pkg/gitprovider/bitbucket/cloud" + "github.com/akuity/kargo/pkg/gitprovider/bitbucket/datacenter" "github.com/akuity/kargo/pkg/urls" ) -const ( - ProviderName = "bitbucket" - - // supportedHost is the hostname of the Bitbucket instance that this provider - // supports. As of now, this provider only supports Bitbucket "Cloud", and not - // self-hosted Bitbucket "Datacenter" instances. - supportedHost = "bitbucket.org" - - // apiBaseURL is the base URL for the Bitbucket Cloud REST API. - apiBaseURL = "https://api.bitbucket.org/2.0" -) +const ProviderName = "bitbucket" var registration = gitprovider.Registration{ Predicate: func(repoURL string) bool { @@ -31,7 +18,8 @@ var registration = gitprovider.Registration{ if err != nil { return false } - return u.Hostname() == supportedHost + // For now, only Cloud is supported. Data Center support is forthcoming. + return u.Hostname() == cloud.Host }, NewProvider: func( repoURL string, @@ -45,447 +33,18 @@ func init() { gitprovider.Register(ProviderName, registration) } -// provider is a Bitbucket-based implementation of gitprovider.Interface. -type provider struct { - owner string - repoSlug string - client ClientWithResponsesInterface -} - -// NewProvider returns a Bitbucket-based implementation of gitprovider.Interface. +// NewProvider returns a Bitbucket Cloud or Data Center implementation of +// gitprovider.Interface based on the repository URL host. func NewProvider( repoURL string, opts *gitprovider.Options, ) (gitprovider.Interface, error) { - if opts == nil { - opts = &gitprovider.Options{} - } - - host, owner, repoSlug, err := parseRepoURL(repoURL) - if err != nil { - return nil, err - } - - // The provider only supports Bitbucket "Cloud", and not self-hosted - // Bitbucket "Datacenter" instances — these require a different API client. - if host != supportedHost { - return nil, fmt.Errorf("unsupported Bitbucket host %q", host) - } - - token := opts.Token - client, err := NewClientWithResponses( - apiBaseURL, - 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 API client: %w", err) - } - - return &provider{ - owner: owner, - 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{} - } - - title := opts.Title - body := Pullrequest{Type: "pullrequest", Title: &title} - if opts.Description != "" { - body.Set("description", opts.Description) - } - - srcBranch := opts.Head - dstBranch := opts.Base - body.Source = &PullrequestEndpoint{ - Branch: &struct { - DefaultMergeStrategy *string `json:"default_merge_strategy,omitempty"` - MergeStrategies *[]PullrequestEndpointBranchMergeStrategies `json:"merge_strategies,omitempty"` - Name *string `json:"name,omitempty"` - }{Name: &srcBranch}, - } - body.Destination = &PullrequestEndpoint{ - Branch: &struct { - DefaultMergeStrategy *string `json:"default_merge_strategy,omitempty"` - MergeStrategies *[]PullrequestEndpointBranchMergeStrategies `json:"merge_strategies,omitempty"` - Name *string `json:"name,omitempty"` - }{Name: &dstBranch}, - } - - resp, err := p.client.PostRepositoriesWorkspaceRepoSlugPullrequestsWithResponse( - ctx, - p.owner, - 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(), - ) - } - if err = p.resolveFullMergeCommitSHA(ctx, resp.JSON201); err != nil { - return nil, err - } - 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.GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdWithResponse( - ctx, - p.owner, - 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, - ) - } - if err = p.resolveFullMergeCommitSHA(ctx, resp.JSON200); err != nil { - return nil, err - } - 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 states []string - switch opts.State { - case gitprovider.PullRequestStateAny: - states = []string{ - string(PullrequestStateOPEN), - string(PullrequestStateMERGED), - string(PullrequestStateDECLINED), - string(PullrequestStateSUPERSEDED), - } - case gitprovider.PullRequestStateClosed: - states = []string{ - string(PullrequestStateMERGED), - string(PullrequestStateDECLINED), - string(PullrequestStateSUPERSEDED), - } - case gitprovider.PullRequestStateOpen: - states = []string{string(PullrequestStateOPEN)} - default: - return nil, fmt.Errorf("unknown pull request state %q", opts.State) - } - - resp, err := p.client.GetRepositoriesWorkspaceRepoSlugPullrequestsWithResponse( - ctx, - p.owner, - p.repoSlug, - &GetRepositoriesWorkspaceRepoSlugPullrequestsParams{}, - withStates(states), - ) - 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(), - ) - } - - // NB: The Bitbucket API doesn't support filtering by source/destination - // branch or commit hash, so we have to filter client-side, which is - // highly inefficient. - var result []gitprovider.PullRequest - if resp.JSON200.Values == nil { - return result, nil - } - values := *resp.JSON200.Values - for i := range values { - pr := &values[i] - if opts.HeadBranch != "" { - branchName := "" - if pr.Source != nil && - pr.Source.Branch != nil && - pr.Source.Branch.Name != nil { - branchName = *pr.Source.Branch.Name - } - if branchName != opts.HeadBranch { - continue - } - } - if opts.BaseBranch != "" { - branchName := "" - if pr.Destination != nil && - pr.Destination.Branch != nil && - pr.Destination.Branch.Name != nil { - branchName = *pr.Destination.Branch.Name - } - if branchName != opts.BaseBranch { - continue - } - } - if opts.HeadCommit != "" { - commitHash := "" - if pr.Source != nil && - pr.Source.Commit != nil && - pr.Source.Commit.Hash != nil { - commitHash = *pr.Source.Commit.Hash - } - if commitHash != opts.HeadCommit { - continue - } - } - if err = p.resolveFullMergeCommitSHA(ctx, pr); err != nil { - return nil, err - } - 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.GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdWithResponse( - ctx, - p.owner, - 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 PullrequestState - if pr.State != nil { - state = *pr.State - } - - if state == PullrequestStateMERGED { - return toProviderPR(pr), true, nil - } - - if state != PullrequestStateOPEN { - 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 - } - - // TODO: The Bitbucket API lacks comprehensive merge eligibility checks. We - // cannot reliably determine if a PR is mergeable due to conflicts, failing - // checks, or other blocking conditions before attempting the merge. This - // means we have no choice but to attempt the merge and hope for the best. - // - // See: https://jira.atlassian.com/browse/BCLOUD-22014 - // - // This limitation makes the "wait" option unreliable for Bitbucket - // repositories. - - var mergeStrategy *PullrequestMergeParametersMergeStrategy - if opts.MergeMethod != "" { - s := PullrequestMergeParametersMergeStrategy(opts.MergeMethod) - if !s.Valid() { - return nil, false, fmt.Errorf("unsupported merge strategy %q", opts.MergeMethod) - } - mergeStrategy = &s - } - - mergeBody := PullrequestMergeParameters{ - Type: "pullrequestMergeParameters", - MergeStrategy: mergeStrategy, - } - - mergeResp, err := p.client.PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeWithResponse( - ctx, - p.owner, - p.repoSlug, - prID, - &PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams{}, - mergeBody, - ) - if err != nil { - return nil, false, fmt.Errorf("error merging pull request %d: %w", id, err) - } - - if mergeResp.StatusCode() == http.StatusAccepted { - return nil, false, fmt.Errorf( - "pull request %d merge was accepted asynchronously and cannot be awaited", id, - ) - } - - if mergeResp.JSON200 == nil { - return nil, false, fmt.Errorf( - "unexpected response %d merging pull request %d", mergeResp.StatusCode(), id, - ) - } - - mergedPR := mergeResp.JSON200 - var mergedState PullrequestState - if mergedPR.State != nil { - mergedState = *mergedPR.State - } - - // Per the Bitbucket API docs, the merge endpoint returns 200 only on - // success (409 for conflicts, 555 for timeout). A 200 response with a - // non-merged state would be unexpected. - if mergedState != PullrequestStateMERGED { - 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(repoURL string, sha string) (string, error) { - normalizedURL := urls.NormalizeGit(repoURL) - parsedURL, err := url.Parse(normalizedURL) - if err != nil { - return "", fmt.Errorf("error processing repository URL: %s: %s", repoURL, err) - } - return fmt.Sprintf("https://%s%s/commits/%s", parsedURL.Host, parsedURL.Path, sha), nil -} - -// resolveFullMergeCommitSHA replaces the (possibly short) merge commit hash on -// pr with the full SHA. Bitbucket returns a short hash in the pull request -// response, so a separate commit lookup is required. -func (p *provider) resolveFullMergeCommitSHA(ctx context.Context, pr *Pullrequest) error { - if pr.MergeCommit == nil || pr.MergeCommit.Hash == nil || *pr.MergeCommit.Hash == "" { - return nil - } - resp, err := p.client.GetRepositoriesWorkspaceRepoSlugCommitCommitWithResponse( - ctx, - p.owner, - p.repoSlug, - *pr.MergeCommit.Hash, - ) - if err != nil { - return fmt.Errorf("error getting commit: %w", err) - } - if resp.JSON200 == nil || resp.JSON200.Hash == nil { - return fmt.Errorf("unexpected response %d getting commit", resp.StatusCode()) - } - pr.MergeCommit.Hash = resp.JSON200.Hash - return nil -} - -// toProviderPR converts a Pullrequest to a gitprovider.PullRequest. -func toProviderPR(pr *Pullrequest) *gitprovider.PullRequest { - if pr == nil { - return nil - } - - var id int64 - if pr.Id != nil { - id = int64(*pr.Id) - } - - var state PullrequestState - if pr.State != nil { - state = *pr.State - } - - var prURL string - if pr.Links != nil && pr.Links.Html != nil && pr.Links.Html.Href != nil { - prURL = *pr.Links.Html.Href - } - - var headSHA string - if pr.Source != nil && pr.Source.Commit != nil && pr.Source.Commit.Hash != nil { - headSHA = *pr.Source.Commit.Hash - } - - var mergeCommitSHA string - if pr.MergeCommit != nil && pr.MergeCommit.Hash != nil { - mergeCommitSHA = *pr.MergeCommit.Hash - } - - return &gitprovider.PullRequest{ - Number: id, - URL: prURL, - Open: state == PullrequestStateOPEN, - Merged: state == PullrequestStateMERGED, - // NB: As a sign of true craftsmanship, or lack thereof, the Bitbucket - // API returns a short commit SHA as merge commit hash. To get the full - // commit SHA, we need to fetch the commit details separately. - MergeCommitSHA: mergeCommitSHA, - HeadSHA: headSHA, - CreatedAt: pr.CreatedOn, - Object: pr, - } -} - -// withStates returns a RequestEditorFn that overrides the state query params. -// The Bitbucket API supports multiple state filters via repeated ?state= params, -// but the generated params struct only supports a single value. -func withStates(states []string) RequestEditorFn { - return func(_ context.Context, req *http.Request) error { - q := req.URL.Query() - q.Del("state") - for _, s := range states { - q.Add("state", s) - } - req.URL.RawQuery = q.Encode() - return nil - } -} - -// parseRepoURL extracts host, owner and repo slug from a repository URL. -func parseRepoURL(repoURL string) (host, owner, slug string, err error) { u, err := url.Parse(urls.NormalizeGit(repoURL)) if err != nil { - return "", "", "", fmt.Errorf("parse Bitbucket URL %q: %w", repoURL, err) + return nil, fmt.Errorf("parse Bitbucket URL %q: %w", repoURL, err) } - path := strings.TrimPrefix(u.Path, "/") - parts := strings.Split(path, "/") - if len(parts) != 2 { - return "", "", "", fmt.Errorf("invalid repository path in URL %q", u) + if u.Hostname() == cloud.Host { + return cloud.NewProvider(repoURL, opts) } - return u.Hostname(), parts[0], parts[1], nil + return datacenter.NewProvider(repoURL, opts) } diff --git a/pkg/gitprovider/bitbucket/bitbucket_test.go b/pkg/gitprovider/bitbucket/bitbucket_test.go index fe18126e71..47e724ffd1 100644 --- a/pkg/gitprovider/bitbucket/bitbucket_test.go +++ b/pkg/gitprovider/bitbucket/bitbucket_test.go @@ -1,1112 +1,32 @@ package bitbucket import ( - "context" - "encoding/json" - "errors" - "io" - "net/http" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/akuity/kargo/pkg/gitprovider" + "github.com/akuity/kargo/pkg/gitprovider/bitbucket/cloud" ) -// mockClient implements ClientWithResponsesInterface for testing. -type mockClient struct { - getCommitFunc func( - ctx context.Context, - workspace, repoSlug, commit string, - reqEditors ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) - - listPRsFunc func( - ctx context.Context, - workspace, repoSlug string, - params *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, - reqEditors ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) - - createPRFunc func( - ctx context.Context, - workspace, repoSlug string, - body PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, - reqEditors ...RequestEditorFn, - ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) - - getPRFunc func( - ctx context.Context, - workspace, repoSlug string, - pullRequestId int, - reqEditors ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) - - mergePRFunc func( - ctx context.Context, - workspace, repoSlug string, - pullRequestId int, - params *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, - body PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeJSONRequestBody, - reqEditors ...RequestEditorFn, - ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse, error) -} - -func (m *mockClient) GetRepositoriesWorkspaceRepoSlugCommitCommitWithResponse( - ctx context.Context, - workspace, repoSlug, commit string, - reqEditors ...RequestEditorFn, -) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) { - return m.getCommitFunc(ctx, workspace, repoSlug, commit, reqEditors...) -} - -func (m *mockClient) GetRepositoriesWorkspaceRepoSlugPullrequestsWithResponse( - ctx context.Context, - workspace, repoSlug string, - params *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, - reqEditors ...RequestEditorFn, -) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { - return m.listPRsFunc(ctx, workspace, repoSlug, params, reqEditors...) -} - -func (m *mockClient) PostRepositoriesWorkspaceRepoSlugPullrequestsWithBodyWithResponse( - _ context.Context, - _, _ string, - _ string, - _ io.Reader, - _ ...RequestEditorFn, -) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { - return nil, errors.New("not implemented") -} - -func (m *mockClient) PostRepositoriesWorkspaceRepoSlugPullrequestsWithResponse( - ctx context.Context, - workspace, repoSlug string, - body PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, - reqEditors ...RequestEditorFn, -) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { - return m.createPRFunc(ctx, workspace, repoSlug, body, reqEditors...) -} - -func (m *mockClient) GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdWithResponse( - ctx context.Context, - workspace, repoSlug string, - pullRequestId int, - reqEditors ...RequestEditorFn, -) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { - return m.getPRFunc(ctx, workspace, repoSlug, pullRequestId, reqEditors...) -} - -func (m *mockClient) PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeWithBodyWithResponse( - _ context.Context, - _, _ string, - _ int, - _ *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, - _ string, - _ io.Reader, - _ ...RequestEditorFn, -) (*PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse, error) { - return nil, errors.New("not implemented") -} - -func (m *mockClient) PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeWithResponse( - ctx context.Context, - workspace, repoSlug string, - pullRequestId int, - params *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, - body PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeJSONRequestBody, - reqEditors ...RequestEditorFn, -) (*PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse, error) { - return m.mergePRFunc(ctx, workspace, repoSlug, pullRequestId, params, body, reqEditors...) -} - -// prFromJSON unmarshals a Pullrequest from JSON, for use in test cases. -func prFromJSON(t *testing.T, s string) *Pullrequest { - t.Helper() - var pr Pullrequest - require.NoError(t, json.Unmarshal([]byte(s), &pr)) - return &pr -} - -// statesFromEditors applies request editors to a fake request and returns the -// state query params — used to verify which states were passed to the list API. -func statesFromEditors(reqEditors []RequestEditorFn) []string { - req, _ := http.NewRequest(http.MethodGet, "http://api.bitbucket.org/test", nil) - for _, ed := range reqEditors { - _ = ed(context.Background(), req) - } - return req.URL.Query()["state"] -} - -func strPtr(s string) *string { return &s } -func intPtr(i int) *int { return &i } - -func statePtr(s PullrequestState) *PullrequestState { return &s } - func TestNewProvider(t *testing.T) { - t.Run("successful creation", func(t *testing.T) { - provider, err := NewProvider("https://bitbucket.org/owner/repo", &gitprovider.Options{Token: "token"}) - assert.NoError(t, err) - assert.NotNil(t, provider) - }) - - t.Run("successful creation with nil options", func(t *testing.T) { - provider, err := NewProvider("https://bitbucket.org/owner/repo", nil) - assert.NoError(t, err) - assert.NotNil(t, provider) - }) - - t.Run("error with invalid URL", func(t *testing.T) { - provider, err := NewProvider("://invalid-url", &gitprovider.Options{Token: "token"}) - assert.Error(t, err) - assert.Nil(t, provider) - }) - - t.Run("error with unsupported host", func(t *testing.T) { - provider, err := NewProvider("https://not-bitbucket.org/owner/repo", &gitprovider.Options{Token: "token"}) - assert.Error(t, err) - assert.Nil(t, provider) - }) - - t.Run("error with invalid path", func(t *testing.T) { - provider, err := NewProvider("https://bitbucket.org/invalid-path", &gitprovider.Options{Token: "token"}) - assert.Error(t, err) - assert.Nil(t, provider) - }) -} - -func TestCreatePullRequest(t *testing.T) { - t.Run("successful creation", func(t *testing.T) { - mc := &mockClient{ - createPRFunc: func( - _ context.Context, - _, _ string, - body PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, - _ ...RequestEditorFn, - ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { - require.NotNil(t, body.Title) - assert.Equal(t, "Test PR", *body.Title) - assert.Equal(t, "PR description", body.AdditionalProperties["description"]) - require.NotNil(t, body.Source) - require.NotNil(t, body.Source.Branch) - require.NotNil(t, body.Source.Branch.Name) - assert.Equal(t, "feature-branch", *body.Source.Branch.Name) - require.NotNil(t, body.Destination) - require.NotNil(t, body.Destination.Branch) - require.NotNil(t, body.Destination.Branch.Name) - assert.Equal(t, "main", *body.Destination.Branch.Name) - return &PostRepositoriesWorkspaceRepoSlugPullrequestsResponse{ - JSON201: prFromJSON(t, `{ - "id": 1, - "state": "OPEN", - "links": {"html": {"href": "https://bitbucket.org/owner/repo/pull-requests/1"}}, - "source": { - "branch": {"name": "feature-branch"}, - "commit": {"hash": "abcdef1234567890"} - }, - "created_on": "2023-01-01T12:00:00Z", - "type": "pullrequest" - }`), - }, nil - }, - } - p := &provider{client: mc} - pr, err := p.CreatePullRequest(t.Context(), &gitprovider.CreatePullRequestOpts{ - Title: "Test PR", - Description: "PR description", - Head: "feature-branch", - Base: "main", - }) - assert.NoError(t, err) - require.NotNil(t, pr) - assert.Equal(t, int64(1), pr.Number) - assert.Equal(t, "https://bitbucket.org/owner/repo/pull-requests/1", pr.URL) - assert.Equal(t, "abcdef1234567890", pr.HeadSHA) - assert.True(t, pr.Open) - assert.False(t, pr.Merged) - }) - - t.Run("successful creation with nil options", func(t *testing.T) { - mc := &mockClient{ - createPRFunc: func( - _ context.Context, - _, _ string, - _ PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, - _ ...RequestEditorFn, - ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { - return &PostRepositoriesWorkspaceRepoSlugPullrequestsResponse{ - JSON201: &Pullrequest{Id: intPtr(1), State: statePtr(PullrequestStateOPEN)}, - }, nil - }, - } - p := &provider{client: mc} - pr, err := p.CreatePullRequest(t.Context(), nil) - assert.NoError(t, err) - require.NotNil(t, pr) - assert.Equal(t, int64(1), pr.Number) - }) - - t.Run("creation with merge commit resolves full SHA", func(t *testing.T) { - mc := &mockClient{ - createPRFunc: func( - _ context.Context, - _, _ string, - _ PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, - _ ...RequestEditorFn, - ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { - return &PostRepositoriesWorkspaceRepoSlugPullrequestsResponse{ - JSON201: &Pullrequest{ - Id: intPtr(1), - State: statePtr(PullrequestStateOPEN), - MergeCommit: &struct { - Hash *string `json:"hash,omitempty"` - }{Hash: strPtr("short123")}, - }, - }, nil - }, - getCommitFunc: func( - _ context.Context, - _, _, commit string, - _ ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) { - assert.Equal(t, "short123", commit) - return &GetRepositoriesWorkspaceRepoSlugCommitCommitResponse{ - JSON200: &Commit{Hash: strPtr("full1234567890abcdef")}, - }, nil - }, - } - p := &provider{client: mc} - pr, err := p.CreatePullRequest(t.Context(), nil) - assert.NoError(t, err) - require.NotNil(t, pr) - assert.Equal(t, "full1234567890abcdef", pr.MergeCommitSHA) - }) - - t.Run("error during creation", func(t *testing.T) { - mc := &mockClient{ - createPRFunc: func( - _ context.Context, - _, _ string, - _ PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, - _ ...RequestEditorFn, - ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { - return nil, errors.New("creation failed") - }, - } - p := &provider{client: mc} - pr, err := p.CreatePullRequest(t.Context(), nil) - assert.Error(t, err) - assert.Nil(t, pr) - }) - - t.Run("error getting full commit SHA", func(t *testing.T) { - mc := &mockClient{ - createPRFunc: func( - _ context.Context, - _, _ string, - _ PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, - _ ...RequestEditorFn, - ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { - return &PostRepositoriesWorkspaceRepoSlugPullrequestsResponse{ - JSON201: &Pullrequest{ - Id: intPtr(1), - State: statePtr(PullrequestStateOPEN), - MergeCommit: &struct { - Hash *string `json:"hash,omitempty"` - }{Hash: strPtr("short123")}, - }, - }, nil - }, - getCommitFunc: func( - _ context.Context, - _, _, _ string, - _ ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) { - return nil, errors.New("commit fetch failed") - }, - } - p := &provider{client: mc} - pr, err := p.CreatePullRequest(t.Context(), nil) - assert.Error(t, err) - assert.Nil(t, pr) + t.Run("cloud URL returns cloud provider", func(t *testing.T) { + p, err := NewProvider("https://bitbucket.org/owner/repo", &gitprovider.Options{Token: "token"}) + require.NoError(t, err) + require.NotNil(t, p) }) -} -func TestGetPullRequest(t *testing.T) { - t.Run("successful retrieval", func(t *testing.T) { - mc := &mockClient{ - getPRFunc: func( - _ context.Context, - _, _ string, - pullRequestId int, - _ ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { - assert.Equal(t, 1, pullRequestId) - return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ - JSON200: prFromJSON(t, `{ - "id": 1, - "state": "OPEN", - "links": {"html": {"href": "https://bitbucket.org/owner/repo/pull-requests/1"}}, - "source": { - "branch": {"name": "feature-branch"}, - "commit": {"hash": "abcdef1234567890"} - }, - "created_on": "2023-01-01T12:00:00Z", - "type": "pullrequest" - }`), - }, nil - }, - } - p := &provider{client: mc} - pr, err := p.GetPullRequest(t.Context(), 1) - assert.NoError(t, err) - require.NotNil(t, pr) - assert.Equal(t, int64(1), pr.Number) - assert.Equal(t, "https://bitbucket.org/owner/repo/pull-requests/1", pr.URL) - assert.Equal(t, "abcdef1234567890", pr.HeadSHA) - assert.True(t, pr.Open) - assert.False(t, pr.Merged) - assert.NotNil(t, pr.CreatedAt) + t.Run("non-cloud URL returns datacenter provider", func(t *testing.T) { + p, err := NewProvider("https://bitbucket.example.com/projects/PROJ/repos/repo", nil) + require.NoError(t, err) + require.NotNil(t, p) }) - t.Run("retrieval of merged PR resolves full SHA", func(t *testing.T) { - mc := &mockClient{ - getPRFunc: func( - _ context.Context, - _, _ string, - _ int, - _ ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { - return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ - JSON200: &Pullrequest{ - Id: intPtr(1), - State: statePtr(PullrequestStateMERGED), - MergeCommit: &struct { - Hash *string `json:"hash,omitempty"` - }{Hash: strPtr("short123")}, - }, - }, nil - }, - getCommitFunc: func( - _ context.Context, - _, _, _ string, - _ ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) { - return &GetRepositoriesWorkspaceRepoSlugCommitCommitResponse{ - JSON200: &Commit{Hash: strPtr("full1234567890abcdef")}, - }, nil - }, - } - p := &provider{client: mc} - pr, err := p.GetPullRequest(t.Context(), 1) - assert.NoError(t, err) - require.NotNil(t, pr) - assert.False(t, pr.Open) - assert.True(t, pr.Merged) - assert.Equal(t, "full1234567890abcdef", pr.MergeCommitSHA) - }) - - t.Run("error during retrieval", func(t *testing.T) { - mc := &mockClient{ - getPRFunc: func( - _ context.Context, - _, _ string, - _ int, - _ ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { - return nil, errors.New("retrieval failed") - }, - } - 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.Run("list open PRs by default", func(t *testing.T) { - mc := &mockClient{ - listPRsFunc: func( - _ context.Context, - _, _ string, - _ *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, - reqEditors ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { - assert.Equal(t, []string{"OPEN"}, statesFromEditors(reqEditors)) - values := []Pullrequest{ - {Id: intPtr(1), State: statePtr(PullrequestStateOPEN)}, - {Id: intPtr(2), State: statePtr(PullrequestStateOPEN)}, - } - return &GetRepositoriesWorkspaceRepoSlugPullrequestsResponse{ - JSON200: &PaginatedPullrequests{Values: &values}, - }, nil - }, - } - p := &provider{client: mc} - prs, err := p.ListPullRequests(t.Context(), nil) - assert.NoError(t, err) - assert.Len(t, prs, 2) - }) - - t.Run("list all PRs", func(t *testing.T) { - mc := &mockClient{ - listPRsFunc: func( - _ context.Context, - _, _ string, - _ *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, - reqEditors ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { - states := statesFromEditors(reqEditors) - assert.Contains(t, states, "OPEN") - assert.Contains(t, states, "MERGED") - assert.Contains(t, states, "DECLINED") - assert.Contains(t, states, "SUPERSEDED") - values := []Pullrequest{ - {Id: intPtr(1), State: statePtr(PullrequestStateOPEN)}, - {Id: intPtr(2), State: statePtr(PullrequestStateMERGED)}, - {Id: intPtr(3), State: statePtr(PullrequestStateDECLINED)}, - {Id: intPtr(4), State: statePtr(PullrequestStateSUPERSEDED)}, - } - return &GetRepositoriesWorkspaceRepoSlugPullrequestsResponse{ - JSON200: &PaginatedPullrequests{Values: &values}, - }, nil - }, - } - p := &provider{client: mc} - prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ - State: gitprovider.PullRequestStateAny, - }) - assert.NoError(t, err) - assert.Len(t, prs, 4) - }) - - t.Run("list closed PRs", func(t *testing.T) { - mc := &mockClient{ - listPRsFunc: func( - _ context.Context, - _, _ string, - _ *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, - reqEditors ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { - states := statesFromEditors(reqEditors) - assert.Contains(t, states, "MERGED") - assert.Contains(t, states, "DECLINED") - assert.Contains(t, states, "SUPERSEDED") - assert.NotContains(t, states, "OPEN") - values := []Pullrequest{ - {Id: intPtr(2), State: statePtr(PullrequestStateMERGED)}, - {Id: intPtr(3), State: statePtr(PullrequestStateDECLINED)}, - {Id: intPtr(4), State: statePtr(PullrequestStateSUPERSEDED)}, - } - return &GetRepositoriesWorkspaceRepoSlugPullrequestsResponse{ - JSON200: &PaginatedPullrequests{Values: &values}, - }, nil - }, - } - p := &provider{client: mc} - prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ - State: gitprovider.PullRequestStateClosed, - }) - assert.NoError(t, err) - assert.Len(t, prs, 3) - }) - - t.Run("filter by head branch", func(t *testing.T) { - mc := &mockClient{ - listPRsFunc: func( - _ context.Context, - _, _ string, - _ *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, - _ ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { - values := []Pullrequest{ - { - Id: intPtr(1), - State: statePtr(PullrequestStateOPEN), - Source: &PullrequestEndpoint{Branch: &struct { - DefaultMergeStrategy *string `json:"default_merge_strategy,omitempty"` - MergeStrategies *[]PullrequestEndpointBranchMergeStrategies `json:"merge_strategies,omitempty"` - Name *string `json:"name,omitempty"` - }{Name: strPtr("feature-1")}}, - }, - { - Id: intPtr(2), - State: statePtr(PullrequestStateOPEN), - Source: &PullrequestEndpoint{Branch: &struct { - DefaultMergeStrategy *string `json:"default_merge_strategy,omitempty"` - MergeStrategies *[]PullrequestEndpointBranchMergeStrategies `json:"merge_strategies,omitempty"` - Name *string `json:"name,omitempty"` - }{Name: strPtr("feature-2")}}, - }, - } - return &GetRepositoriesWorkspaceRepoSlugPullrequestsResponse{ - JSON200: &PaginatedPullrequests{Values: &values}, - }, nil - }, - } - p := &provider{client: mc} - prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ - HeadBranch: "feature-1", - }) - assert.NoError(t, err) - assert.Len(t, prs, 1) - assert.Equal(t, int64(1), prs[0].Number) - }) - - t.Run("filter by base branch", func(t *testing.T) { - mc := &mockClient{ - listPRsFunc: func( - _ context.Context, - _, _ string, - _ *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, - _ ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { - values := []Pullrequest{ - { - Id: intPtr(1), - State: statePtr(PullrequestStateOPEN), - Destination: &PullrequestEndpoint{Branch: &struct { - DefaultMergeStrategy *string `json:"default_merge_strategy,omitempty"` - MergeStrategies *[]PullrequestEndpointBranchMergeStrategies `json:"merge_strategies,omitempty"` - Name *string `json:"name,omitempty"` - }{Name: strPtr("main")}}, - }, - { - Id: intPtr(2), - State: statePtr(PullrequestStateOPEN), - Destination: &PullrequestEndpoint{Branch: &struct { - DefaultMergeStrategy *string `json:"default_merge_strategy,omitempty"` - MergeStrategies *[]PullrequestEndpointBranchMergeStrategies `json:"merge_strategies,omitempty"` - Name *string `json:"name,omitempty"` - }{Name: strPtr("dev")}}, - }, - } - return &GetRepositoriesWorkspaceRepoSlugPullrequestsResponse{ - JSON200: &PaginatedPullrequests{Values: &values}, - }, nil - }, - } - p := &provider{client: mc} - prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ - BaseBranch: "dev", - }) - assert.NoError(t, err) - assert.Len(t, prs, 1) - assert.Equal(t, int64(2), prs[0].Number) - }) - - t.Run("filter by head commit", func(t *testing.T) { - mc := &mockClient{ - listPRsFunc: func( - _ context.Context, - _, _ string, - _ *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, - _ ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { - values := []Pullrequest{ - { - Id: intPtr(1), - State: statePtr(PullrequestStateOPEN), - Source: &PullrequestEndpoint{ - Commit: &struct { - Hash *string `json:"hash,omitempty"` - }{Hash: strPtr("specific-hash")}, - }, - }, - { - Id: intPtr(2), - State: statePtr(PullrequestStateOPEN), - Source: &PullrequestEndpoint{ - Commit: &struct { - Hash *string `json:"hash,omitempty"` - }{Hash: strPtr("other-hash")}, - }, - }, - } - return &GetRepositoriesWorkspaceRepoSlugPullrequestsResponse{ - JSON200: &PaginatedPullrequests{Values: &values}, - }, nil - }, - } - p := &provider{client: mc} - prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ - HeadCommit: "specific-hash", - }) - assert.NoError(t, err) - assert.Len(t, prs, 1) - assert.Equal(t, int64(1), prs[0].Number) - }) - - t.Run("PR with merge commit resolves full SHA", func(t *testing.T) { - mc := &mockClient{ - listPRsFunc: func( - _ context.Context, - _, _ string, - _ *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, - _ ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { - values := []Pullrequest{{ - Id: intPtr(1), - State: statePtr(PullrequestStateMERGED), - MergeCommit: &struct { - Hash *string `json:"hash,omitempty"` - }{Hash: strPtr("short123")}, - }} - return &GetRepositoriesWorkspaceRepoSlugPullrequestsResponse{ - JSON200: &PaginatedPullrequests{Values: &values}, - }, nil - }, - getCommitFunc: func( - _ context.Context, - _, _, _ string, - _ ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) { - return &GetRepositoriesWorkspaceRepoSlugCommitCommitResponse{ - JSON200: &Commit{Hash: strPtr("full1234567890abcdef")}, - }, nil - }, - } - p := &provider{client: mc} - prs, err := p.ListPullRequests(t.Context(), nil) - assert.NoError(t, err) - assert.Len(t, prs, 1) - assert.Equal(t, "full1234567890abcdef", prs[0].MergeCommitSHA) - }) - - t.Run("error during list", func(t *testing.T) { - mc := &mockClient{ - listPRsFunc: func( - _ context.Context, - _, _ string, - _ *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, - _ ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { - return nil, errors.New("list failed") - }, - } - p := &provider{client: mc} - prs, err := p.ListPullRequests(t.Context(), nil) - assert.Error(t, err) - assert.Nil(t, prs) - }) - - t.Run("invalid state", func(t *testing.T) { - p := &provider{client: &mockClient{}} - prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ - State: "invalid-state", - }) - assert.Error(t, err) - assert.Nil(t, prs) - }) -} - -func TestMergePullRequest(t *testing.T) { - testCases := []struct { - name string - prNumber int64 - mergeOpts *gitprovider.MergePullRequestOpts - mockClient *mockClient - expectedMerged bool - expectError bool - errorContains string - }{ - { - name: "error getting PR", - prNumber: 999, - mockClient: &mockClient{ - getPRFunc: func( - _ context.Context, - _, _ string, - _ int, - _ ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { - return nil, errors.New("get PR failed") - }, - }, - expectError: true, - errorContains: "error getting pull request", - }, - { - name: "PR already merged", - prNumber: 123, - mockClient: &mockClient{ - getPRFunc: func( - _ context.Context, - _, _ string, - _ int, - _ ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { - return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ - JSON200: &Pullrequest{ - Id: intPtr(123), - State: statePtr(PullrequestStateMERGED), - MergeCommit: &struct { - Hash *string `json:"hash,omitempty"` - }{Hash: strPtr("merge_sha")}, - }, - }, nil - }, - }, - expectedMerged: true, - }, - { - name: "PR declined", - prNumber: 456, - mockClient: &mockClient{ - getPRFunc: func( - _ context.Context, - _, _ string, - _ int, - _ ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { - return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ - JSON200: &Pullrequest{Id: intPtr(456), State: statePtr(PullrequestStateDECLINED)}, - }, nil - }, - }, - expectError: true, - errorContains: "closed but not merged", - }, - { - name: "PR is draft", - prNumber: 333, - mockClient: &mockClient{ - getPRFunc: func( - _ context.Context, - _, _ string, - _ int, - _ ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { - isDraft := true - return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ - JSON200: &Pullrequest{ - Id: intPtr(333), - State: statePtr(PullrequestStateOPEN), - Draft: &isDraft, - }, - }, nil - }, - }, - }, - { - name: "unsupported merge strategy", - prNumber: 100, - mergeOpts: &gitprovider.MergePullRequestOpts{MergeMethod: "rebase"}, - mockClient: &mockClient{ - getPRFunc: func( - _ context.Context, - _, _ string, - _ int, - _ ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { - return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ - JSON200: &Pullrequest{Id: intPtr(100), State: statePtr(PullrequestStateOPEN)}, - }, nil - }, - }, - expectError: true, - errorContains: "unsupported merge strategy", - }, - { - name: "merge operation fails", - prNumber: 888, - mockClient: &mockClient{ - getPRFunc: func( - _ context.Context, - _, _ string, - _ int, - _ ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { - return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ - JSON200: &Pullrequest{Id: intPtr(888), State: statePtr(PullrequestStateOPEN)}, - }, nil - }, - mergePRFunc: func( - _ context.Context, - _, _ string, - _ int, - _ *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, - _ PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeJSONRequestBody, - _ ...RequestEditorFn, - ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse, error) { - return nil, errors.New("merge failed") - }, - }, - expectError: true, - errorContains: "error merging pull request", - }, - { - name: "successful merge with strategy", - prNumber: 200, - mergeOpts: &gitprovider.MergePullRequestOpts{MergeMethod: "squash"}, - mockClient: &mockClient{ - getPRFunc: func( - _ context.Context, - _, _ string, - _ int, - _ ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { - return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ - JSON200: &Pullrequest{Id: intPtr(200), State: statePtr(PullrequestStateOPEN)}, - }, nil - }, - mergePRFunc: func( - _ context.Context, - _, _ string, - pullRequestId int, - _ *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, - body PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeJSONRequestBody, - _ ...RequestEditorFn, - ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse, error) { - assert.Equal(t, 200, pullRequestId) - require.NotNil(t, body.MergeStrategy) - assert.Equal(t, Squash, *body.MergeStrategy) - return &PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse{ - JSON200: &Pullrequest{Id: intPtr(200), State: statePtr(PullrequestStateMERGED)}, - }, nil - }, - }, - expectedMerged: true, - }, - { - name: "successful merge", - prNumber: 1234, - mockClient: &mockClient{ - getPRFunc: func( - _ context.Context, - _, _ string, - _ int, - _ ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { - return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ - JSON200: &Pullrequest{Id: intPtr(1234), State: statePtr(PullrequestStateOPEN)}, - }, nil - }, - mergePRFunc: func( - _ context.Context, - _, _ string, - _ int, - _ *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, - _ PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeJSONRequestBody, - _ ...RequestEditorFn, - ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse, error) { - return &PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse{ - JSON200: &Pullrequest{ - Id: intPtr(1234), - State: statePtr(PullrequestStateMERGED), - MergeCommit: &struct { - Hash *string `json:"hash,omitempty"` - }{Hash: strPtr("merge_sha")}, - }, - }, nil - }, - }, - expectedMerged: true, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - p := &provider{client: tc.mockClient} - pr, merged, err := p.MergePullRequest(t.Context(), tc.prNumber, tc.mergeOpts) - if tc.expectError { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errorContains) - require.False(t, merged) - require.Nil(t, pr) - return - } - require.NoError(t, err) - require.Equal(t, tc.expectedMerged, merged) - if tc.expectedMerged { - require.NotNil(t, pr) - require.Equal(t, tc.prNumber, pr.Number) - } - }) - } -} - -func TestResolveFullMergeCommitSHA(t *testing.T) { - t.Run("no-op when merge commit is nil", func(t *testing.T) { - p := &provider{client: &mockClient{}} - pr := &Pullrequest{} - err := p.resolveFullMergeCommitSHA(t.Context(), pr) - assert.NoError(t, err) - assert.Nil(t, pr.MergeCommit) - }) - - t.Run("no-op when merge commit hash is empty", func(t *testing.T) { - p := &provider{client: &mockClient{}} - pr := &Pullrequest{ - MergeCommit: &struct { - Hash *string `json:"hash,omitempty"` - }{Hash: strPtr("")}, - } - err := p.resolveFullMergeCommitSHA(t.Context(), pr) - assert.NoError(t, err) - }) - - t.Run("resolves short SHA to full SHA", func(t *testing.T) { - mc := &mockClient{ - getCommitFunc: func( - _ context.Context, - _, _, commit string, - _ ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) { - assert.Equal(t, "short123", commit) - return &GetRepositoriesWorkspaceRepoSlugCommitCommitResponse{ - JSON200: &Commit{Hash: strPtr("full1234567890abcdef")}, - }, nil - }, - } - p := &provider{client: mc} - pr := &Pullrequest{ - MergeCommit: &struct { - Hash *string `json:"hash,omitempty"` - }{Hash: strPtr("short123")}, - } - err := p.resolveFullMergeCommitSHA(t.Context(), pr) - assert.NoError(t, err) - require.NotNil(t, pr.MergeCommit.Hash) - assert.Equal(t, "full1234567890abcdef", *pr.MergeCommit.Hash) - }) - - t.Run("error from getCommit is propagated", func(t *testing.T) { - mc := &mockClient{ - getCommitFunc: func( - _ context.Context, - _, _, _ string, - _ ...RequestEditorFn, - ) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) { - return nil, errors.New("retrieval failed") - }, - } - p := &provider{client: mc} - pr := &Pullrequest{ - MergeCommit: &struct { - Hash *string `json:"hash,omitempty"` - }{Hash: strPtr("short123")}, - } - err := p.resolveFullMergeCommitSHA(t.Context(), pr) - assert.Error(t, err) - }) -} - -func TestParseRepoURL(t *testing.T) { - tests := []struct { - name string - url string - wantHost string - wantOwner string - wantSlug string - wantErr bool - }{ - { - name: "valid URL", - url: "https://bitbucket.org/owner/repo", - wantHost: "bitbucket.org", - wantOwner: "owner", - wantSlug: "repo", - }, - { - name: "valid URL with trailing slash", - url: "https://bitbucket.org/owner/repo/", - wantHost: "bitbucket.org", - wantOwner: "owner", - wantSlug: "repo", - }, - { - name: "valid URL with .git suffix", - url: "https://bitbucket.org/owner/repo.git", - wantHost: "bitbucket.org", - wantOwner: "owner", - wantSlug: "repo", - }, - { - name: "valid SSH URL", - url: "git@bitbucket.org:owner/repo.git", - wantHost: "bitbucket.org", - wantOwner: "owner", - wantSlug: "repo", - }, - { - name: "invalid URL format", - url: "://invalid-url", - wantErr: true, - }, - { - name: "missing repository name", - url: "https://bitbucket.org/owner", - wantErr: true, - }, - { - name: "too many path segments", - url: "https://bitbucket.org/owner/repo/extra", - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - host, owner, slug, err := parseRepoURL(tt.url) - if tt.wantErr { - assert.Error(t, err) - return - } - require.NoError(t, err) - assert.Equal(t, tt.wantHost, host) - assert.Equal(t, tt.wantOwner, owner) - assert.Equal(t, tt.wantSlug, slug) - }) - } -} - -func Test_toProviderPR(t *testing.T) { - t.Run("valid conversion: open PR", func(t *testing.T) { - bbPR := prFromJSON(t, `{ - "id": 1, - "state": "OPEN", - "links": {"html": {"href": "https://bitbucket.org/owner/repo/pull-requests/1"}}, - "source": {"commit": {"hash": "abcdef1234567890"}}, - "created_on": "2023-01-01T12:00:00Z", - "type": "pullrequest" - }`) - pr := toProviderPR(bbPR) - require.NotNil(t, pr) - assert.Equal(t, int64(1), pr.Number) - assert.Equal(t, "https://bitbucket.org/owner/repo/pull-requests/1", pr.URL) - assert.Equal(t, "abcdef1234567890", pr.HeadSHA) - assert.True(t, pr.Open) - assert.False(t, pr.Merged) - assert.NotNil(t, pr.CreatedAt) - assert.Equal(t, bbPR, pr.Object) - }) - - t.Run("valid conversion: merged PR", func(t *testing.T) { - bbPR := &Pullrequest{ - Id: intPtr(1), - State: statePtr(PullrequestStateMERGED), - MergeCommit: &struct { - Hash *string `json:"hash,omitempty"` - }{Hash: strPtr("merged1234567890")}, - } - pr := toProviderPR(bbPR) - require.NotNil(t, pr) - assert.False(t, pr.Open) - assert.True(t, pr.Merged) - assert.Equal(t, "merged1234567890", pr.MergeCommitSHA) - }) - - t.Run("nil input", func(t *testing.T) { - assert.Nil(t, toProviderPR(nil)) - }) - - t.Run("no created_on", func(t *testing.T) { - pr := toProviderPR(&Pullrequest{Id: intPtr(1)}) - require.NotNil(t, pr) - assert.Nil(t, pr.CreatedAt) + t.Run("invalid URL returns error", func(t *testing.T) { + p, err := NewProvider("://invalid-url", nil) + require.Error(t, err) + require.Nil(t, p) }) } @@ -1119,51 +39,21 @@ func Test_registration(t *testing.T) { assert.False(t, registration.Predicate("https://github.com/owner/repo")) }) + t.Run("predicate doesn't match self-hosted URLs", func(t *testing.T) { + assert.False(t, registration.Predicate("https://bitbucket.example.com/projects/PROJ/repos/repo")) + }) + t.Run("predicate handles invalid URLs", func(t *testing.T) { assert.False(t, registration.Predicate("://invalid-url")) }) - t.Run("NewProvider factory works", func(t *testing.T) { - provider, err := registration.NewProvider("https://bitbucket.org/owner/repo", nil) + t.Run("NewProvider factory works for cloud URL", func(t *testing.T) { + p, err := registration.NewProvider("https://bitbucket.org/owner/repo", nil) assert.NoError(t, err) - assert.NotNil(t, provider) + assert.NotNil(t, p) }) } -func TestGetCommitURL(t *testing.T) { - testCases := []struct { - repoURL string - sha string - expectedCommitURL string - }{ - { - repoURL: "ssh://git@bitbucket.org/akuity/kargo.git", - sha: "sha", - expectedCommitURL: "https://bitbucket.org/akuity/kargo/commits/sha", - }, - { - repoURL: "git@bitbucket.org:akuity/kargo.git", - sha: "sha", - expectedCommitURL: "https://bitbucket.org/akuity/kargo/commits/sha", - }, - { - repoURL: "https://username@bitbucket.org/akuity/kargo", - sha: "sha", - expectedCommitURL: "https://bitbucket.org/akuity/kargo/commits/sha", - }, - { - repoURL: "http://bitbucket.org/akuity/kargo.git", - sha: "sha", - expectedCommitURL: "https://bitbucket.org/akuity/kargo/commits/sha", - }, - } - - p := &provider{} - for _, tc := range testCases { - t.Run(tc.repoURL, func(t *testing.T) { - commitURL, err := p.GetCommitURL(tc.repoURL, tc.sha) - require.NoError(t, err) - require.Equal(t, tc.expectedCommitURL, commitURL) - }) - } +func Test_cloudHostConst(t *testing.T) { + assert.Equal(t, "bitbucket.org", cloud.Host) } diff --git a/pkg/gitprovider/bitbucket/cloud/provider.go b/pkg/gitprovider/bitbucket/cloud/provider.go new file mode 100644 index 0000000000..307057f4f7 --- /dev/null +++ b/pkg/gitprovider/bitbucket/cloud/provider.go @@ -0,0 +1,461 @@ +package cloud + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/hashicorp/go-cleanhttp" + + "github.com/akuity/kargo/pkg/gitprovider" + "github.com/akuity/kargo/pkg/urls" +) + +const ( + // Host is the hostname of the Bitbucket Cloud instance. + Host = "bitbucket.org" + + // apiBaseURL is the base URL for the Bitbucket Cloud REST API. + apiBaseURL = "https://api.bitbucket.org/2.0" +) + +// provider is a Bitbucket Cloud implementation of gitprovider.Interface. +type provider struct { + owner string + repoSlug string + client ClientWithResponsesInterface +} + +// NewProvider returns a Bitbucket Cloud implementation of gitprovider.Interface. +func NewProvider( + repoURL string, + opts *gitprovider.Options, +) (gitprovider.Interface, error) { + if opts == nil { + opts = &gitprovider.Options{} + } + + _, owner, repoSlug, err := parseRepoURL(repoURL) + if err != nil { + return nil, err + } + + token := opts.Token + client, err := NewClientWithResponses( + apiBaseURL, + 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 Cloud API client: %w", err) + } + + return &provider{ + owner: owner, + 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{} + } + + title := opts.Title + body := Pullrequest{Type: "pullrequest", Title: &title} + if opts.Description != "" { + body.Set("description", opts.Description) + } + + srcBranch := opts.Head + dstBranch := opts.Base + body.Source = &PullrequestEndpoint{ + Branch: &struct { + DefaultMergeStrategy *string `json:"default_merge_strategy,omitempty"` + MergeStrategies *[]PullrequestEndpointBranchMergeStrategies `json:"merge_strategies,omitempty"` + Name *string `json:"name,omitempty"` + }{Name: &srcBranch}, + } + body.Destination = &PullrequestEndpoint{ + Branch: &struct { + DefaultMergeStrategy *string `json:"default_merge_strategy,omitempty"` + MergeStrategies *[]PullrequestEndpointBranchMergeStrategies `json:"merge_strategies,omitempty"` + Name *string `json:"name,omitempty"` + }{Name: &dstBranch}, + } + + resp, err := p.client.PostRepositoriesWorkspaceRepoSlugPullrequestsWithResponse( + ctx, + p.owner, + 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(), + ) + } + if err = p.resolveFullMergeCommitSHA(ctx, resp.JSON201); err != nil { + return nil, err + } + 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.GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdWithResponse( + ctx, + p.owner, + 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, + ) + } + if err = p.resolveFullMergeCommitSHA(ctx, resp.JSON200); err != nil { + return nil, err + } + 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 states []string + switch opts.State { + case gitprovider.PullRequestStateAny: + states = []string{ + string(PullrequestStateOPEN), + string(PullrequestStateMERGED), + string(PullrequestStateDECLINED), + string(PullrequestStateSUPERSEDED), + } + case gitprovider.PullRequestStateClosed: + states = []string{ + string(PullrequestStateMERGED), + string(PullrequestStateDECLINED), + string(PullrequestStateSUPERSEDED), + } + case gitprovider.PullRequestStateOpen: + states = []string{string(PullrequestStateOPEN)} + default: + return nil, fmt.Errorf("unknown pull request state %q", opts.State) + } + + resp, err := p.client.GetRepositoriesWorkspaceRepoSlugPullrequestsWithResponse( + ctx, + p.owner, + p.repoSlug, + &GetRepositoriesWorkspaceRepoSlugPullrequestsParams{}, + withStates(states), + ) + 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(), + ) + } + + // NB: The Bitbucket API doesn't support filtering by source/destination + // branch or commit hash, so we have to filter client-side, which is + // highly inefficient. + var result []gitprovider.PullRequest + if resp.JSON200.Values == nil { + return result, nil + } + values := *resp.JSON200.Values + for i := range values { + pr := &values[i] + if opts.HeadBranch != "" { + branchName := "" + if pr.Source != nil && + pr.Source.Branch != nil && + pr.Source.Branch.Name != nil { + branchName = *pr.Source.Branch.Name + } + if branchName != opts.HeadBranch { + continue + } + } + if opts.BaseBranch != "" { + branchName := "" + if pr.Destination != nil && + pr.Destination.Branch != nil && + pr.Destination.Branch.Name != nil { + branchName = *pr.Destination.Branch.Name + } + if branchName != opts.BaseBranch { + continue + } + } + if opts.HeadCommit != "" { + commitHash := "" + if pr.Source != nil && + pr.Source.Commit != nil && + pr.Source.Commit.Hash != nil { + commitHash = *pr.Source.Commit.Hash + } + if commitHash != opts.HeadCommit { + continue + } + } + if err = p.resolveFullMergeCommitSHA(ctx, pr); err != nil { + return nil, err + } + 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.GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdWithResponse( + ctx, + p.owner, + 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 PullrequestState + if pr.State != nil { + state = *pr.State + } + + if state == PullrequestStateMERGED { + return toProviderPR(pr), true, nil + } + + if state != PullrequestStateOPEN { + 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 + } + + // TODO: The Bitbucket API lacks comprehensive merge eligibility checks. We + // cannot reliably determine if a PR is mergeable due to conflicts, failing + // checks, or other blocking conditions before attempting the merge. This + // means we have no choice but to attempt the merge and hope for the best. + // + // See: https://jira.atlassian.com/browse/BCLOUD-22014 + // + // This limitation makes the "wait" option unreliable for Bitbucket + // repositories. + + var mergeStrategy *PullrequestMergeParametersMergeStrategy + if opts.MergeMethod != "" { + s := PullrequestMergeParametersMergeStrategy(opts.MergeMethod) + if !s.Valid() { + return nil, false, fmt.Errorf("unsupported merge strategy %q", opts.MergeMethod) + } + mergeStrategy = &s + } + + mergeBody := PullrequestMergeParameters{ + Type: "pullrequestMergeParameters", + MergeStrategy: mergeStrategy, + } + + mergeResp, err := p.client.PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeWithResponse( + ctx, + p.owner, + p.repoSlug, + prID, + &PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams{}, + mergeBody, + ) + if err != nil { + return nil, false, fmt.Errorf("error merging pull request %d: %w", id, err) + } + + if mergeResp.StatusCode() == http.StatusAccepted { + return nil, false, fmt.Errorf( + "pull request %d merge was accepted asynchronously and cannot be awaited", id, + ) + } + + if mergeResp.JSON200 == nil { + return nil, false, fmt.Errorf( + "unexpected response %d merging pull request %d", mergeResp.StatusCode(), id, + ) + } + + mergedPR := mergeResp.JSON200 + var mergedState PullrequestState + if mergedPR.State != nil { + mergedState = *mergedPR.State + } + + // Per the Bitbucket API docs, the merge endpoint returns 200 only on + // success (409 for conflicts, 555 for timeout). A 200 response with a + // non-merged state would be unexpected. + if mergedState != PullrequestStateMERGED { + 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(repoURL string, sha string) (string, error) { + normalizedURL := urls.NormalizeGit(repoURL) + parsedURL, err := url.Parse(normalizedURL) + if err != nil { + return "", fmt.Errorf("error processing repository URL: %s: %s", repoURL, err) + } + return fmt.Sprintf("https://%s%s/commits/%s", parsedURL.Host, parsedURL.Path, sha), nil +} + +// resolveFullMergeCommitSHA replaces the (possibly short) merge commit hash on +// pr with the full SHA. Bitbucket returns a short hash in the pull request +// response, so a separate commit lookup is required. +func (p *provider) resolveFullMergeCommitSHA(ctx context.Context, pr *Pullrequest) error { + if pr.MergeCommit == nil || pr.MergeCommit.Hash == nil || *pr.MergeCommit.Hash == "" { + return nil + } + resp, err := p.client.GetRepositoriesWorkspaceRepoSlugCommitCommitWithResponse( + ctx, + p.owner, + p.repoSlug, + *pr.MergeCommit.Hash, + ) + if err != nil { + return fmt.Errorf("error getting commit: %w", err) + } + if resp.JSON200 == nil || resp.JSON200.Hash == nil { + return fmt.Errorf("unexpected response %d getting commit", resp.StatusCode()) + } + pr.MergeCommit.Hash = resp.JSON200.Hash + return nil +} + +// toProviderPR converts a Pullrequest to a gitprovider.PullRequest. +func toProviderPR(pr *Pullrequest) *gitprovider.PullRequest { + if pr == nil { + return nil + } + + var id int64 + if pr.Id != nil { + id = int64(*pr.Id) + } + + var state PullrequestState + if pr.State != nil { + state = *pr.State + } + + var prURL string + if pr.Links != nil && pr.Links.Html != nil && pr.Links.Html.Href != nil { + prURL = *pr.Links.Html.Href + } + + var headSHA string + if pr.Source != nil && pr.Source.Commit != nil && pr.Source.Commit.Hash != nil { + headSHA = *pr.Source.Commit.Hash + } + + var mergeCommitSHA string + if pr.MergeCommit != nil && pr.MergeCommit.Hash != nil { + mergeCommitSHA = *pr.MergeCommit.Hash + } + + return &gitprovider.PullRequest{ + Number: id, + URL: prURL, + Open: state == PullrequestStateOPEN, + Merged: state == PullrequestStateMERGED, + // NB: As a sign of true craftsmanship, or lack thereof, the Bitbucket + // API returns a short commit SHA as merge commit hash. To get the full + // commit SHA, we need to fetch the commit details separately. + MergeCommitSHA: mergeCommitSHA, + HeadSHA: headSHA, + CreatedAt: pr.CreatedOn, + Object: pr, + } +} + +// withStates returns a RequestEditorFn that overrides the state query params. +// The Bitbucket API supports multiple state filters via repeated ?state= params, +// but the generated params struct only supports a single value. +func withStates(states []string) RequestEditorFn { + return func(_ context.Context, req *http.Request) error { + q := req.URL.Query() + q.Del("state") + for _, s := range states { + q.Add("state", s) + } + req.URL.RawQuery = q.Encode() + return nil + } +} + +// parseRepoURL extracts host, owner and repo slug from a Bitbucket Cloud repository URL. +func parseRepoURL(repoURL string) (host, owner, slug string, err error) { + u, err := url.Parse(urls.NormalizeGit(repoURL)) + if err != nil { + return "", "", "", fmt.Errorf("parse Bitbucket Cloud URL %q: %w", repoURL, err) + } + path := strings.TrimPrefix(u.Path, "/") + parts := strings.Split(path, "/") + if len(parts) != 2 { + return "", "", "", fmt.Errorf("invalid repository path in URL %q", u) + } + return u.Hostname(), parts[0], parts[1], nil +} diff --git a/pkg/gitprovider/bitbucket/cloud/provider_test.go b/pkg/gitprovider/bitbucket/cloud/provider_test.go new file mode 100644 index 0000000000..eb6cf2a989 --- /dev/null +++ b/pkg/gitprovider/bitbucket/cloud/provider_test.go @@ -0,0 +1,1143 @@ +package cloud + +import ( + "context" + "encoding/json" + "errors" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/akuity/kargo/pkg/gitprovider" +) + +// mockClient implements ClientWithResponsesInterface for testing. +type mockClient struct { + getCommitFunc func( + ctx context.Context, + workspace, repoSlug, commit string, + reqEditors ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) + + listPRsFunc func( + ctx context.Context, + workspace, repoSlug string, + params *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, + reqEditors ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) + + createPRFunc func( + ctx context.Context, + workspace, repoSlug string, + body PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, + reqEditors ...RequestEditorFn, + ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) + + getPRFunc func( + ctx context.Context, + workspace, repoSlug string, + pullRequestId int, + reqEditors ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) + + mergePRFunc func( + ctx context.Context, + workspace, repoSlug string, + pullRequestId int, + params *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, + body PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeJSONRequestBody, + reqEditors ...RequestEditorFn, + ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse, error) +} + +func (m *mockClient) GetRepositoriesWorkspaceRepoSlugCommitCommitWithResponse( + ctx context.Context, + workspace, repoSlug, commit string, + reqEditors ...RequestEditorFn, +) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) { + return m.getCommitFunc(ctx, workspace, repoSlug, commit, reqEditors...) +} + +func (m *mockClient) GetRepositoriesWorkspaceRepoSlugPullrequestsWithResponse( + ctx context.Context, + workspace, repoSlug string, + params *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, + reqEditors ...RequestEditorFn, +) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + return m.listPRsFunc(ctx, workspace, repoSlug, params, reqEditors...) +} + +func (m *mockClient) PostRepositoriesWorkspaceRepoSlugPullrequestsWithBodyWithResponse( + _ context.Context, + _, _ string, + _ string, + _ io.Reader, + _ ...RequestEditorFn, +) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + return nil, errors.New("not implemented") +} + +func (m *mockClient) PostRepositoriesWorkspaceRepoSlugPullrequestsWithResponse( + ctx context.Context, + workspace, repoSlug string, + body PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, + reqEditors ...RequestEditorFn, +) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + return m.createPRFunc(ctx, workspace, repoSlug, body, reqEditors...) +} + +func (m *mockClient) GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdWithResponse( + ctx context.Context, + workspace, repoSlug string, + pullRequestId int, + reqEditors ...RequestEditorFn, +) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { + return m.getPRFunc(ctx, workspace, repoSlug, pullRequestId, reqEditors...) +} + +func (m *mockClient) PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeWithBodyWithResponse( + _ context.Context, + _, _ string, + _ int, + _ *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, + _ string, + _ io.Reader, + _ ...RequestEditorFn, +) (*PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse, error) { + return nil, errors.New("not implemented") +} + +func (m *mockClient) PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeWithResponse( + ctx context.Context, + workspace, repoSlug string, + pullRequestId int, + params *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, + body PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeJSONRequestBody, + reqEditors ...RequestEditorFn, +) (*PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse, error) { + return m.mergePRFunc(ctx, workspace, repoSlug, pullRequestId, params, body, reqEditors...) +} + +// prFromJSON unmarshals a Pullrequest from JSON, for use in test cases. +func prFromJSON(t *testing.T, s string) *Pullrequest { + t.Helper() + var pr Pullrequest + require.NoError(t, json.Unmarshal([]byte(s), &pr)) + return &pr +} + +// statesFromEditors applies request editors to a fake request and returns the +// state query params — used to verify which states were passed to the list API. +func statesFromEditors(reqEditors []RequestEditorFn) []string { + req, _ := http.NewRequest(http.MethodGet, "http://api.bitbucket.org/test", nil) + for _, ed := range reqEditors { + _ = ed(context.Background(), req) + } + return req.URL.Query()["state"] +} + +func strPtr(s string) *string { return &s } +func intPtr(i int) *int { return &i } + +func statePtr(s PullrequestState) *PullrequestState { return &s } + +func TestNewProvider(t *testing.T) { + t.Run("successful creation", func(t *testing.T) { + provider, err := NewProvider("https://bitbucket.org/owner/repo", &gitprovider.Options{Token: "token"}) + assert.NoError(t, err) + assert.NotNil(t, provider) + }) + + t.Run("successful creation with nil options", func(t *testing.T) { + provider, err := NewProvider("https://bitbucket.org/owner/repo", nil) + assert.NoError(t, err) + assert.NotNil(t, provider) + }) + + t.Run("error with invalid URL", func(t *testing.T) { + provider, err := NewProvider("://invalid-url", &gitprovider.Options{Token: "token"}) + assert.Error(t, err) + assert.Nil(t, provider) + }) + + t.Run("error with invalid path", func(t *testing.T) { + provider, err := NewProvider("https://bitbucket.org/invalid-path", &gitprovider.Options{Token: "token"}) + assert.Error(t, err) + assert.Nil(t, provider) + }) +} + +func TestCreatePullRequest(t *testing.T) { + t.Run("successful creation", func(t *testing.T) { + mc := &mockClient{ + createPRFunc: func( + _ context.Context, + _, _ string, + body PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, + _ ...RequestEditorFn, + ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + require.NotNil(t, body.Title) + assert.Equal(t, "Test PR", *body.Title) + assert.Equal(t, "PR description", body.AdditionalProperties["description"]) + require.NotNil(t, body.Source) + require.NotNil(t, body.Source.Branch) + require.NotNil(t, body.Source.Branch.Name) + assert.Equal(t, "feature-branch", *body.Source.Branch.Name) + require.NotNil(t, body.Destination) + require.NotNil(t, body.Destination.Branch) + require.NotNil(t, body.Destination.Branch.Name) + assert.Equal(t, "main", *body.Destination.Branch.Name) + return &PostRepositoriesWorkspaceRepoSlugPullrequestsResponse{ + JSON201: prFromJSON(t, `{ + "id": 1, + "state": "OPEN", + "links": {"html": {"href": "https://bitbucket.org/owner/repo/pull-requests/1"}}, + "source": { + "branch": {"name": "feature-branch"}, + "commit": {"hash": "abcdef1234567890"} + }, + "created_on": "2023-01-01T12:00:00Z", + "type": "pullrequest" + }`), + }, nil + }, + } + p := &provider{client: mc} + pr, err := p.CreatePullRequest(t.Context(), &gitprovider.CreatePullRequestOpts{ + Title: "Test PR", + Description: "PR description", + Head: "feature-branch", + Base: "main", + }) + assert.NoError(t, err) + require.NotNil(t, pr) + assert.Equal(t, int64(1), pr.Number) + assert.Equal(t, "https://bitbucket.org/owner/repo/pull-requests/1", pr.URL) + assert.Equal(t, "abcdef1234567890", pr.HeadSHA) + assert.True(t, pr.Open) + assert.False(t, pr.Merged) + }) + + t.Run("successful creation with nil options", func(t *testing.T) { + mc := &mockClient{ + createPRFunc: func( + _ context.Context, + _, _ string, + _ PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, + _ ...RequestEditorFn, + ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + return &PostRepositoriesWorkspaceRepoSlugPullrequestsResponse{ + JSON201: &Pullrequest{Id: intPtr(1), State: statePtr(PullrequestStateOPEN)}, + }, nil + }, + } + p := &provider{client: mc} + pr, err := p.CreatePullRequest(t.Context(), nil) + assert.NoError(t, err) + require.NotNil(t, pr) + assert.Equal(t, int64(1), pr.Number) + }) + + t.Run("creation with merge commit resolves full SHA", func(t *testing.T) { + mc := &mockClient{ + createPRFunc: func( + _ context.Context, + _, _ string, + _ PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, + _ ...RequestEditorFn, + ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + return &PostRepositoriesWorkspaceRepoSlugPullrequestsResponse{ + JSON201: &Pullrequest{ + Id: intPtr(1), + State: statePtr(PullrequestStateOPEN), + MergeCommit: &struct { + Hash *string `json:"hash,omitempty"` + }{Hash: strPtr("short123")}, + }, + }, nil + }, + getCommitFunc: func( + _ context.Context, + _, _, commit string, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) { + assert.Equal(t, "short123", commit) + return &GetRepositoriesWorkspaceRepoSlugCommitCommitResponse{ + JSON200: &Commit{Hash: strPtr("full1234567890abcdef")}, + }, nil + }, + } + p := &provider{client: mc} + pr, err := p.CreatePullRequest(t.Context(), nil) + assert.NoError(t, err) + require.NotNil(t, pr) + assert.Equal(t, "full1234567890abcdef", pr.MergeCommitSHA) + }) + + t.Run("error during creation", func(t *testing.T) { + mc := &mockClient{ + createPRFunc: func( + _ context.Context, + _, _ string, + _ PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, + _ ...RequestEditorFn, + ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + return nil, errors.New("creation failed") + }, + } + p := &provider{client: mc} + pr, err := p.CreatePullRequest(t.Context(), nil) + assert.Error(t, err) + assert.Nil(t, pr) + }) + + t.Run("error getting full commit SHA", func(t *testing.T) { + mc := &mockClient{ + createPRFunc: func( + _ context.Context, + _, _ string, + _ PostRepositoriesWorkspaceRepoSlugPullrequestsJSONRequestBody, + _ ...RequestEditorFn, + ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + return &PostRepositoriesWorkspaceRepoSlugPullrequestsResponse{ + JSON201: &Pullrequest{ + Id: intPtr(1), + State: statePtr(PullrequestStateOPEN), + MergeCommit: &struct { + Hash *string `json:"hash,omitempty"` + }{Hash: strPtr("short123")}, + }, + }, nil + }, + getCommitFunc: func( + _ context.Context, + _, _, _ string, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) { + return nil, errors.New("commit fetch failed") + }, + } + 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.Run("successful retrieval", func(t *testing.T) { + mc := &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + pullRequestId int, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { + assert.Equal(t, 1, pullRequestId) + return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ + JSON200: prFromJSON(t, `{ + "id": 1, + "state": "OPEN", + "links": {"html": {"href": "https://bitbucket.org/owner/repo/pull-requests/1"}}, + "source": { + "branch": {"name": "feature-branch"}, + "commit": {"hash": "abcdef1234567890"} + }, + "created_on": "2023-01-01T12:00:00Z", + "type": "pullrequest" + }`), + }, nil + }, + } + p := &provider{client: mc} + pr, err := p.GetPullRequest(t.Context(), 1) + assert.NoError(t, err) + require.NotNil(t, pr) + assert.Equal(t, int64(1), pr.Number) + assert.Equal(t, "https://bitbucket.org/owner/repo/pull-requests/1", pr.URL) + assert.Equal(t, "abcdef1234567890", pr.HeadSHA) + assert.True(t, pr.Open) + assert.False(t, pr.Merged) + assert.NotNil(t, pr.CreatedAt) + }) + + t.Run("retrieval of merged PR resolves full SHA", func(t *testing.T) { + mc := &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { + return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ + JSON200: &Pullrequest{ + Id: intPtr(1), + State: statePtr(PullrequestStateMERGED), + MergeCommit: &struct { + Hash *string `json:"hash,omitempty"` + }{Hash: strPtr("short123")}, + }, + }, nil + }, + getCommitFunc: func( + _ context.Context, + _, _, _ string, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) { + return &GetRepositoriesWorkspaceRepoSlugCommitCommitResponse{ + JSON200: &Commit{Hash: strPtr("full1234567890abcdef")}, + }, nil + }, + } + p := &provider{client: mc} + pr, err := p.GetPullRequest(t.Context(), 1) + assert.NoError(t, err) + require.NotNil(t, pr) + assert.False(t, pr.Open) + assert.True(t, pr.Merged) + assert.Equal(t, "full1234567890abcdef", pr.MergeCommitSHA) + }) + + t.Run("error during retrieval", func(t *testing.T) { + mc := &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { + return nil, errors.New("retrieval failed") + }, + } + 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.Run("list open PRs by default", func(t *testing.T) { + mc := &mockClient{ + listPRsFunc: func( + _ context.Context, + _, _ string, + _ *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, + reqEditors ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + assert.Equal(t, []string{"OPEN"}, statesFromEditors(reqEditors)) + values := []Pullrequest{ + {Id: intPtr(1), State: statePtr(PullrequestStateOPEN)}, + {Id: intPtr(2), State: statePtr(PullrequestStateOPEN)}, + } + return &GetRepositoriesWorkspaceRepoSlugPullrequestsResponse{ + JSON200: &PaginatedPullrequests{Values: &values}, + }, nil + }, + } + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), nil) + assert.NoError(t, err) + assert.Len(t, prs, 2) + }) + + t.Run("list all PRs", func(t *testing.T) { + mc := &mockClient{ + listPRsFunc: func( + _ context.Context, + _, _ string, + _ *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, + reqEditors ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + states := statesFromEditors(reqEditors) + assert.Contains(t, states, "OPEN") + assert.Contains(t, states, "MERGED") + assert.Contains(t, states, "DECLINED") + assert.Contains(t, states, "SUPERSEDED") + values := []Pullrequest{ + {Id: intPtr(1), State: statePtr(PullrequestStateOPEN)}, + {Id: intPtr(2), State: statePtr(PullrequestStateMERGED)}, + {Id: intPtr(3), State: statePtr(PullrequestStateDECLINED)}, + {Id: intPtr(4), State: statePtr(PullrequestStateSUPERSEDED)}, + } + return &GetRepositoriesWorkspaceRepoSlugPullrequestsResponse{ + JSON200: &PaginatedPullrequests{Values: &values}, + }, nil + }, + } + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ + State: gitprovider.PullRequestStateAny, + }) + assert.NoError(t, err) + assert.Len(t, prs, 4) + }) + + t.Run("list closed PRs", func(t *testing.T) { + mc := &mockClient{ + listPRsFunc: func( + _ context.Context, + _, _ string, + _ *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, + reqEditors ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + states := statesFromEditors(reqEditors) + assert.Contains(t, states, "MERGED") + assert.Contains(t, states, "DECLINED") + assert.Contains(t, states, "SUPERSEDED") + assert.NotContains(t, states, "OPEN") + values := []Pullrequest{ + {Id: intPtr(2), State: statePtr(PullrequestStateMERGED)}, + {Id: intPtr(3), State: statePtr(PullrequestStateDECLINED)}, + {Id: intPtr(4), State: statePtr(PullrequestStateSUPERSEDED)}, + } + return &GetRepositoriesWorkspaceRepoSlugPullrequestsResponse{ + JSON200: &PaginatedPullrequests{Values: &values}, + }, nil + }, + } + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ + State: gitprovider.PullRequestStateClosed, + }) + assert.NoError(t, err) + assert.Len(t, prs, 3) + }) + + t.Run("filter by head branch", func(t *testing.T) { + mc := &mockClient{ + listPRsFunc: func( + _ context.Context, + _, _ string, + _ *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + values := []Pullrequest{ + { + Id: intPtr(1), + State: statePtr(PullrequestStateOPEN), + Source: &PullrequestEndpoint{Branch: &struct { + DefaultMergeStrategy *string `json:"default_merge_strategy,omitempty"` + MergeStrategies *[]PullrequestEndpointBranchMergeStrategies `json:"merge_strategies,omitempty"` + Name *string `json:"name,omitempty"` + }{Name: strPtr("feature-1")}}, + }, + { + Id: intPtr(2), + State: statePtr(PullrequestStateOPEN), + Source: &PullrequestEndpoint{Branch: &struct { + DefaultMergeStrategy *string `json:"default_merge_strategy,omitempty"` + MergeStrategies *[]PullrequestEndpointBranchMergeStrategies `json:"merge_strategies,omitempty"` + Name *string `json:"name,omitempty"` + }{Name: strPtr("feature-2")}}, + }, + } + return &GetRepositoriesWorkspaceRepoSlugPullrequestsResponse{ + JSON200: &PaginatedPullrequests{Values: &values}, + }, nil + }, + } + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ + HeadBranch: "feature-1", + }) + assert.NoError(t, err) + assert.Len(t, prs, 1) + assert.Equal(t, int64(1), prs[0].Number) + }) + + t.Run("filter by base branch", func(t *testing.T) { + mc := &mockClient{ + listPRsFunc: func( + _ context.Context, + _, _ string, + _ *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + values := []Pullrequest{ + { + Id: intPtr(1), + State: statePtr(PullrequestStateOPEN), + Destination: &PullrequestEndpoint{Branch: &struct { + DefaultMergeStrategy *string `json:"default_merge_strategy,omitempty"` + MergeStrategies *[]PullrequestEndpointBranchMergeStrategies `json:"merge_strategies,omitempty"` + Name *string `json:"name,omitempty"` + }{Name: strPtr("main")}}, + }, + { + Id: intPtr(2), + State: statePtr(PullrequestStateOPEN), + Destination: &PullrequestEndpoint{Branch: &struct { + DefaultMergeStrategy *string `json:"default_merge_strategy,omitempty"` + MergeStrategies *[]PullrequestEndpointBranchMergeStrategies `json:"merge_strategies,omitempty"` + Name *string `json:"name,omitempty"` + }{Name: strPtr("dev")}}, + }, + } + return &GetRepositoriesWorkspaceRepoSlugPullrequestsResponse{ + JSON200: &PaginatedPullrequests{Values: &values}, + }, nil + }, + } + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ + BaseBranch: "dev", + }) + assert.NoError(t, err) + assert.Len(t, prs, 1) + assert.Equal(t, int64(2), prs[0].Number) + }) + + t.Run("filter by head commit", func(t *testing.T) { + mc := &mockClient{ + listPRsFunc: func( + _ context.Context, + _, _ string, + _ *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + values := []Pullrequest{ + { + Id: intPtr(1), + State: statePtr(PullrequestStateOPEN), + Source: &PullrequestEndpoint{ + Commit: &struct { + Hash *string `json:"hash,omitempty"` + }{Hash: strPtr("specific-hash")}, + }, + }, + { + Id: intPtr(2), + State: statePtr(PullrequestStateOPEN), + Source: &PullrequestEndpoint{ + Commit: &struct { + Hash *string `json:"hash,omitempty"` + }{Hash: strPtr("other-hash")}, + }, + }, + } + return &GetRepositoriesWorkspaceRepoSlugPullrequestsResponse{ + JSON200: &PaginatedPullrequests{Values: &values}, + }, nil + }, + } + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ + HeadCommit: "specific-hash", + }) + assert.NoError(t, err) + assert.Len(t, prs, 1) + assert.Equal(t, int64(1), prs[0].Number) + }) + + t.Run("PR with merge commit resolves full SHA", func(t *testing.T) { + mc := &mockClient{ + listPRsFunc: func( + _ context.Context, + _, _ string, + _ *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + values := []Pullrequest{{ + Id: intPtr(1), + State: statePtr(PullrequestStateMERGED), + MergeCommit: &struct { + Hash *string `json:"hash,omitempty"` + }{Hash: strPtr("short123")}, + }} + return &GetRepositoriesWorkspaceRepoSlugPullrequestsResponse{ + JSON200: &PaginatedPullrequests{Values: &values}, + }, nil + }, + getCommitFunc: func( + _ context.Context, + _, _, _ string, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) { + return &GetRepositoriesWorkspaceRepoSlugCommitCommitResponse{ + JSON200: &Commit{Hash: strPtr("full1234567890abcdef")}, + }, nil + }, + } + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), nil) + assert.NoError(t, err) + assert.Len(t, prs, 1) + assert.Equal(t, "full1234567890abcdef", prs[0].MergeCommitSHA) + }) + + t.Run("error during list", func(t *testing.T) { + mc := &mockClient{ + listPRsFunc: func( + _ context.Context, + _, _ string, + _ *GetRepositoriesWorkspaceRepoSlugPullrequestsParams, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsResponse, error) { + return nil, errors.New("list failed") + }, + } + p := &provider{client: mc} + prs, err := p.ListPullRequests(t.Context(), nil) + assert.Error(t, err) + assert.Nil(t, prs) + }) + + t.Run("invalid state", func(t *testing.T) { + p := &provider{client: &mockClient{}} + prs, err := p.ListPullRequests(t.Context(), &gitprovider.ListPullRequestOptions{ + State: "invalid-state", + }) + assert.Error(t, err) + assert.Nil(t, prs) + }) +} + +func TestMergePullRequest(t *testing.T) { + testCases := []struct { + name string + prNumber int64 + mergeOpts *gitprovider.MergePullRequestOpts + mockClient *mockClient + expectedMerged bool + expectError bool + errorContains string + }{ + { + name: "error getting PR", + prNumber: 999, + mockClient: &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { + return nil, errors.New("get PR failed") + }, + }, + expectError: true, + errorContains: "error getting pull request", + }, + { + name: "PR already merged", + prNumber: 123, + mockClient: &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { + return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ + JSON200: &Pullrequest{ + Id: intPtr(123), + State: statePtr(PullrequestStateMERGED), + MergeCommit: &struct { + Hash *string `json:"hash,omitempty"` + }{Hash: strPtr("merge_sha")}, + }, + }, nil + }, + }, + expectedMerged: true, + }, + { + name: "PR declined", + prNumber: 456, + mockClient: &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { + return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ + JSON200: &Pullrequest{Id: intPtr(456), State: statePtr(PullrequestStateDECLINED)}, + }, nil + }, + }, + expectError: true, + errorContains: "closed but not merged", + }, + { + name: "PR is draft", + prNumber: 333, + mockClient: &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { + isDraft := true + return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ + JSON200: &Pullrequest{ + Id: intPtr(333), + State: statePtr(PullrequestStateOPEN), + Draft: &isDraft, + }, + }, nil + }, + }, + }, + { + name: "unsupported merge strategy", + prNumber: 100, + mergeOpts: &gitprovider.MergePullRequestOpts{MergeMethod: "rebase"}, + mockClient: &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { + return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ + JSON200: &Pullrequest{Id: intPtr(100), State: statePtr(PullrequestStateOPEN)}, + }, nil + }, + }, + expectError: true, + errorContains: "unsupported merge strategy", + }, + { + name: "merge operation fails", + prNumber: 888, + mockClient: &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { + return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ + JSON200: &Pullrequest{Id: intPtr(888), State: statePtr(PullrequestStateOPEN)}, + }, nil + }, + mergePRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, + _ PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeJSONRequestBody, + _ ...RequestEditorFn, + ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse, error) { + return nil, errors.New("merge failed") + }, + }, + expectError: true, + errorContains: "error merging pull request", + }, + { + name: "successful merge with strategy", + prNumber: 200, + mergeOpts: &gitprovider.MergePullRequestOpts{MergeMethod: "squash"}, + mockClient: &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { + return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ + JSON200: &Pullrequest{Id: intPtr(200), State: statePtr(PullrequestStateOPEN)}, + }, nil + }, + mergePRFunc: func( + _ context.Context, + _, _ string, + pullRequestId int, + _ *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, + body PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeJSONRequestBody, + _ ...RequestEditorFn, + ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse, error) { + assert.Equal(t, 200, pullRequestId) + require.NotNil(t, body.MergeStrategy) + assert.Equal(t, Squash, *body.MergeStrategy) + return &PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse{ + JSON200: &Pullrequest{Id: intPtr(200), State: statePtr(PullrequestStateMERGED)}, + }, nil + }, + }, + expectedMerged: true, + }, + { + name: "successful merge", + prNumber: 1234, + mockClient: &mockClient{ + getPRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse, error) { + return &GetRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdResponse{ + JSON200: &Pullrequest{Id: intPtr(1234), State: statePtr(PullrequestStateOPEN)}, + }, nil + }, + mergePRFunc: func( + _ context.Context, + _, _ string, + _ int, + _ *PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeParams, + _ PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeJSONRequestBody, + _ ...RequestEditorFn, + ) (*PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse, error) { + return &PostRepositoriesWorkspaceRepoSlugPullrequestsPullRequestIdMergeResponse{ + JSON200: &Pullrequest{ + Id: intPtr(1234), + State: statePtr(PullrequestStateMERGED), + MergeCommit: &struct { + Hash *string `json:"hash,omitempty"` + }{Hash: strPtr("merge_sha")}, + }, + }, nil + }, + }, + expectedMerged: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + p := &provider{client: tc.mockClient} + pr, merged, err := p.MergePullRequest(t.Context(), tc.prNumber, tc.mergeOpts) + if tc.expectError { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errorContains) + require.False(t, merged) + require.Nil(t, pr) + return + } + require.NoError(t, err) + require.Equal(t, tc.expectedMerged, merged) + if tc.expectedMerged { + require.NotNil(t, pr) + require.Equal(t, tc.prNumber, pr.Number) + } + }) + } +} + +func TestResolveFullMergeCommitSHA(t *testing.T) { + t.Run("no-op when merge commit is nil", func(t *testing.T) { + p := &provider{client: &mockClient{}} + pr := &Pullrequest{} + err := p.resolveFullMergeCommitSHA(t.Context(), pr) + assert.NoError(t, err) + assert.Nil(t, pr.MergeCommit) + }) + + t.Run("no-op when merge commit hash is empty", func(t *testing.T) { + p := &provider{client: &mockClient{}} + pr := &Pullrequest{ + MergeCommit: &struct { + Hash *string `json:"hash,omitempty"` + }{Hash: strPtr("")}, + } + err := p.resolveFullMergeCommitSHA(t.Context(), pr) + assert.NoError(t, err) + }) + + t.Run("resolves short SHA to full SHA", func(t *testing.T) { + mc := &mockClient{ + getCommitFunc: func( + _ context.Context, + _, _, commit string, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) { + assert.Equal(t, "short123", commit) + return &GetRepositoriesWorkspaceRepoSlugCommitCommitResponse{ + JSON200: &Commit{Hash: strPtr("full1234567890abcdef")}, + }, nil + }, + } + p := &provider{client: mc} + pr := &Pullrequest{ + MergeCommit: &struct { + Hash *string `json:"hash,omitempty"` + }{Hash: strPtr("short123")}, + } + err := p.resolveFullMergeCommitSHA(t.Context(), pr) + assert.NoError(t, err) + require.NotNil(t, pr.MergeCommit.Hash) + assert.Equal(t, "full1234567890abcdef", *pr.MergeCommit.Hash) + }) + + t.Run("error from getCommit is propagated", func(t *testing.T) { + mc := &mockClient{ + getCommitFunc: func( + _ context.Context, + _, _, _ string, + _ ...RequestEditorFn, + ) (*GetRepositoriesWorkspaceRepoSlugCommitCommitResponse, error) { + return nil, errors.New("retrieval failed") + }, + } + p := &provider{client: mc} + pr := &Pullrequest{ + MergeCommit: &struct { + Hash *string `json:"hash,omitempty"` + }{Hash: strPtr("short123")}, + } + err := p.resolveFullMergeCommitSHA(t.Context(), pr) + assert.Error(t, err) + }) +} + +func TestParseRepoURL(t *testing.T) { + tests := []struct { + name string + url string + wantHost string + wantOwner string + wantSlug string + wantErr bool + }{ + { + name: "valid URL", + url: "https://bitbucket.org/owner/repo", + wantHost: "bitbucket.org", + wantOwner: "owner", + wantSlug: "repo", + }, + { + name: "valid URL with trailing slash", + url: "https://bitbucket.org/owner/repo/", + wantHost: "bitbucket.org", + wantOwner: "owner", + wantSlug: "repo", + }, + { + name: "valid URL with .git suffix", + url: "https://bitbucket.org/owner/repo.git", + wantHost: "bitbucket.org", + wantOwner: "owner", + wantSlug: "repo", + }, + { + name: "valid SSH URL", + url: "git@bitbucket.org:owner/repo.git", + wantHost: "bitbucket.org", + wantOwner: "owner", + wantSlug: "repo", + }, + { + name: "invalid URL format", + url: "://invalid-url", + wantErr: true, + }, + { + name: "missing repository name", + url: "https://bitbucket.org/owner", + wantErr: true, + }, + { + name: "too many path segments", + url: "https://bitbucket.org/owner/repo/extra", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + host, owner, slug, err := parseRepoURL(tt.url) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantHost, host) + assert.Equal(t, tt.wantOwner, owner) + assert.Equal(t, tt.wantSlug, slug) + }) + } +} + +func Test_toProviderPR(t *testing.T) { + t.Run("valid conversion: open PR", func(t *testing.T) { + bbPR := prFromJSON(t, `{ + "id": 1, + "state": "OPEN", + "links": {"html": {"href": "https://bitbucket.org/owner/repo/pull-requests/1"}}, + "source": {"commit": {"hash": "abcdef1234567890"}}, + "created_on": "2023-01-01T12:00:00Z", + "type": "pullrequest" + }`) + pr := toProviderPR(bbPR) + require.NotNil(t, pr) + assert.Equal(t, int64(1), pr.Number) + assert.Equal(t, "https://bitbucket.org/owner/repo/pull-requests/1", pr.URL) + assert.Equal(t, "abcdef1234567890", pr.HeadSHA) + assert.True(t, pr.Open) + assert.False(t, pr.Merged) + assert.NotNil(t, pr.CreatedAt) + assert.Equal(t, bbPR, pr.Object) + }) + + t.Run("valid conversion: merged PR", func(t *testing.T) { + bbPR := &Pullrequest{ + Id: intPtr(1), + State: statePtr(PullrequestStateMERGED), + MergeCommit: &struct { + Hash *string `json:"hash,omitempty"` + }{Hash: strPtr("merged1234567890")}, + } + pr := toProviderPR(bbPR) + require.NotNil(t, pr) + assert.False(t, pr.Open) + assert.True(t, pr.Merged) + assert.Equal(t, "merged1234567890", pr.MergeCommitSHA) + }) + + t.Run("nil input", func(t *testing.T) { + assert.Nil(t, toProviderPR(nil)) + }) + + t.Run("no created_on", func(t *testing.T) { + pr := toProviderPR(&Pullrequest{Id: intPtr(1)}) + require.NotNil(t, pr) + assert.Nil(t, pr.CreatedAt) + }) +} + +func TestGetCommitURL(t *testing.T) { + testCases := []struct { + repoURL string + sha string + expectedCommitURL string + }{ + { + repoURL: "ssh://git@bitbucket.org/akuity/kargo.git", + sha: "sha", + expectedCommitURL: "https://bitbucket.org/akuity/kargo/commits/sha", + }, + { + repoURL: "git@bitbucket.org:akuity/kargo.git", + sha: "sha", + expectedCommitURL: "https://bitbucket.org/akuity/kargo/commits/sha", + }, + { + repoURL: "https://username@bitbucket.org/akuity/kargo", + sha: "sha", + expectedCommitURL: "https://bitbucket.org/akuity/kargo/commits/sha", + }, + { + repoURL: "http://bitbucket.org/akuity/kargo.git", + sha: "sha", + expectedCommitURL: "https://bitbucket.org/akuity/kargo/commits/sha", + }, + } + + p := &provider{} + for _, tc := range testCases { + t.Run(tc.repoURL, func(t *testing.T) { + commitURL, err := p.GetCommitURL(tc.repoURL, tc.sha) + require.NoError(t, err) + require.Equal(t, tc.expectedCommitURL, commitURL) + }) + } +} diff --git a/pkg/gitprovider/bitbucket/spec/bitbucket.gen.json b/pkg/gitprovider/bitbucket/cloud/spec/bitbucket.gen.json similarity index 100% rename from pkg/gitprovider/bitbucket/spec/bitbucket.gen.json rename to pkg/gitprovider/bitbucket/cloud/spec/bitbucket.gen.json diff --git a/pkg/gitprovider/bitbucket/spec/oapi-codegen.yaml b/pkg/gitprovider/bitbucket/cloud/spec/oapi-codegen.yaml similarity index 86% rename from pkg/gitprovider/bitbucket/spec/oapi-codegen.yaml rename to pkg/gitprovider/bitbucket/cloud/spec/oapi-codegen.yaml index 9e8ef2d271..b1bdc1b07f 100644 --- a/pkg/gitprovider/bitbucket/spec/oapi-codegen.yaml +++ b/pkg/gitprovider/bitbucket/cloud/spec/oapi-codegen.yaml @@ -1,4 +1,4 @@ -package: bitbucket +package: cloud generate: models: true diff --git a/pkg/gitprovider/bitbucket/zz_bitbucket_cloud_client.gen.go b/pkg/gitprovider/bitbucket/cloud/zz_bitbucket_cloud_client.gen.go similarity index 99% rename from pkg/gitprovider/bitbucket/zz_bitbucket_cloud_client.gen.go rename to pkg/gitprovider/bitbucket/cloud/zz_bitbucket_cloud_client.gen.go index d96a3fa541..312578fbf5 100644 --- a/pkg/gitprovider/bitbucket/zz_bitbucket_cloud_client.gen.go +++ b/pkg/gitprovider/bitbucket/cloud/zz_bitbucket_cloud_client.gen.go @@ -1,7 +1,7 @@ -// Package bitbucket provides primitives to interact with the openapi HTTP API. +// Package cloud 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 bitbucket +package cloud import ( "bytes" diff --git a/pkg/gitprovider/bitbucket/datacenter/provider.go b/pkg/gitprovider/bitbucket/datacenter/provider.go new file mode 100644 index 0000000000..68ca8addc1 --- /dev/null +++ b/pkg/gitprovider/bitbucket/datacenter/provider.go @@ -0,0 +1,53 @@ +package datacenter + +import ( + "context" + "fmt" + + "github.com/akuity/kargo/pkg/gitprovider" +) + +// provider is a Bitbucket Data Center implementation of gitprovider.Interface. +type provider struct{} + +// NewProvider returns a Bitbucket Data Center implementation of gitprovider.Interface. +func NewProvider( + _ string, + _ *gitprovider.Options, +) (gitprovider.Interface, error) { + // TODO: Implement Bitbucket Data Center provider. + return &provider{}, nil +} + +func (p *provider) CreatePullRequest( + _ context.Context, + _ *gitprovider.CreatePullRequestOpts, +) (*gitprovider.PullRequest, error) { + return nil, fmt.Errorf("Bitbucket Data Center provider is not yet implemented") +} + +func (p *provider) GetPullRequest( + _ context.Context, + _ int64, +) (*gitprovider.PullRequest, error) { + return nil, fmt.Errorf("Bitbucket Data Center provider is not yet implemented") +} + +func (p *provider) ListPullRequests( + _ context.Context, + _ *gitprovider.ListPullRequestOptions, +) ([]gitprovider.PullRequest, error) { + return nil, fmt.Errorf("Bitbucket Data Center provider is not yet implemented") +} + +func (p *provider) MergePullRequest( + _ context.Context, + _ int64, + _ *gitprovider.MergePullRequestOpts, +) (*gitprovider.PullRequest, bool, error) { + return nil, false, fmt.Errorf("Bitbucket Data Center provider is not yet implemented") +} + +func (p *provider) GetCommitURL(_ string, _ string) (string, error) { + return "", fmt.Errorf("Bitbucket Data Center provider is not yet implemented") +} 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 +} From 6646ef95f83023eb1fcac4f5368ee234f176849c Mon Sep 17 00:00:00 2001 From: fuskovic Date: Fri, 24 Apr 2026 20:57:52 +0530 Subject: [PATCH 13/18] use separate registration paths Signed-off-by: fuskovic --- pkg/gitprovider/bitbucket/bitbucket.go | 50 ---------------- pkg/gitprovider/bitbucket/bitbucket_test.go | 59 ------------------- .../{ => cloud}/pr_integration_test.go | 2 +- pkg/gitprovider/bitbucket/cloud/provider.go | 23 ++++++++ .../bitbucket/cloud/provider_test.go | 24 ++++++++ .../bitbucket/datacenter/provider.go | 32 ++++++++++ .../bitbucket/datacenter/provider_test.go | 35 +++++++++++ pkg/promotion/runner/builtin/git_pr_merger.go | 3 +- pkg/promotion/runner/builtin/git_pr_opener.go | 3 +- pkg/promotion/runner/builtin/git_pr_waiter.go | 3 +- 10 files changed, 121 insertions(+), 113 deletions(-) delete mode 100644 pkg/gitprovider/bitbucket/bitbucket.go delete mode 100644 pkg/gitprovider/bitbucket/bitbucket_test.go rename pkg/gitprovider/bitbucket/{ => cloud}/pr_integration_test.go (98%) create mode 100644 pkg/gitprovider/bitbucket/datacenter/provider_test.go diff --git a/pkg/gitprovider/bitbucket/bitbucket.go b/pkg/gitprovider/bitbucket/bitbucket.go deleted file mode 100644 index 9095ba33b7..0000000000 --- a/pkg/gitprovider/bitbucket/bitbucket.go +++ /dev/null @@ -1,50 +0,0 @@ -package bitbucket - -import ( - "fmt" - "net/url" - - "github.com/akuity/kargo/pkg/gitprovider" - "github.com/akuity/kargo/pkg/gitprovider/bitbucket/cloud" - "github.com/akuity/kargo/pkg/gitprovider/bitbucket/datacenter" - "github.com/akuity/kargo/pkg/urls" -) - -const ProviderName = "bitbucket" - -var registration = gitprovider.Registration{ - Predicate: func(repoURL string) bool { - u, err := url.Parse(repoURL) - if err != nil { - return false - } - // For now, only Cloud is supported. Data Center support is forthcoming. - return u.Hostname() == cloud.Host - }, - NewProvider: func( - repoURL string, - opts *gitprovider.Options, - ) (gitprovider.Interface, error) { - return NewProvider(repoURL, opts) - }, -} - -func init() { - gitprovider.Register(ProviderName, registration) -} - -// NewProvider returns a Bitbucket Cloud or Data Center implementation of -// gitprovider.Interface based on the repository URL host. -func NewProvider( - repoURL string, - opts *gitprovider.Options, -) (gitprovider.Interface, error) { - u, err := url.Parse(urls.NormalizeGit(repoURL)) - if err != nil { - return nil, fmt.Errorf("parse Bitbucket URL %q: %w", repoURL, err) - } - if u.Hostname() == cloud.Host { - return cloud.NewProvider(repoURL, opts) - } - return datacenter.NewProvider(repoURL, opts) -} diff --git a/pkg/gitprovider/bitbucket/bitbucket_test.go b/pkg/gitprovider/bitbucket/bitbucket_test.go deleted file mode 100644 index 47e724ffd1..0000000000 --- a/pkg/gitprovider/bitbucket/bitbucket_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package bitbucket - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/akuity/kargo/pkg/gitprovider" - "github.com/akuity/kargo/pkg/gitprovider/bitbucket/cloud" -) - -func TestNewProvider(t *testing.T) { - t.Run("cloud URL returns cloud provider", func(t *testing.T) { - p, err := NewProvider("https://bitbucket.org/owner/repo", &gitprovider.Options{Token: "token"}) - require.NoError(t, err) - require.NotNil(t, p) - }) - - t.Run("non-cloud URL returns datacenter provider", func(t *testing.T) { - p, err := NewProvider("https://bitbucket.example.com/projects/PROJ/repos/repo", nil) - require.NoError(t, err) - require.NotNil(t, p) - }) - - t.Run("invalid URL returns error", func(t *testing.T) { - p, err := NewProvider("://invalid-url", nil) - require.Error(t, err) - require.Nil(t, p) - }) -} - -func Test_registration(t *testing.T) { - t.Run("predicate matches bitbucket.org URL", func(t *testing.T) { - assert.True(t, registration.Predicate("https://bitbucket.org/owner/repo")) - }) - - t.Run("predicate doesn't match other URLs", func(t *testing.T) { - assert.False(t, registration.Predicate("https://github.com/owner/repo")) - }) - - t.Run("predicate doesn't match self-hosted URLs", func(t *testing.T) { - assert.False(t, registration.Predicate("https://bitbucket.example.com/projects/PROJ/repos/repo")) - }) - - t.Run("predicate handles invalid URLs", func(t *testing.T) { - assert.False(t, registration.Predicate("://invalid-url")) - }) - - t.Run("NewProvider factory works for cloud URL", func(t *testing.T) { - p, err := registration.NewProvider("https://bitbucket.org/owner/repo", nil) - assert.NoError(t, err) - assert.NotNil(t, p) - }) -} - -func Test_cloudHostConst(t *testing.T) { - assert.Equal(t, "bitbucket.org", cloud.Host) -} diff --git a/pkg/gitprovider/bitbucket/pr_integration_test.go b/pkg/gitprovider/bitbucket/cloud/pr_integration_test.go similarity index 98% rename from pkg/gitprovider/bitbucket/pr_integration_test.go rename to pkg/gitprovider/bitbucket/cloud/pr_integration_test.go index 334b44fc91..5db60c1379 100644 --- a/pkg/gitprovider/bitbucket/pr_integration_test.go +++ b/pkg/gitprovider/bitbucket/cloud/pr_integration_test.go @@ -1,6 +1,6 @@ //go:build integration && bitbucket -package bitbucket +package cloud import ( "testing" diff --git a/pkg/gitprovider/bitbucket/cloud/provider.go b/pkg/gitprovider/bitbucket/cloud/provider.go index 307057f4f7..3cb3cefb20 100644 --- a/pkg/gitprovider/bitbucket/cloud/provider.go +++ b/pkg/gitprovider/bitbucket/cloud/provider.go @@ -14,6 +14,9 @@ import ( ) const ( + // ProviderName is the name used to register the Bitbucket Cloud provider. + ProviderName = "bitbucket" + // Host is the hostname of the Bitbucket Cloud instance. Host = "bitbucket.org" @@ -21,6 +24,26 @@ const ( apiBaseURL = "https://api.bitbucket.org/2.0" ) +var registration = gitprovider.Registration{ + Predicate: func(repoURL string) bool { + u, err := url.Parse(repoURL) + if err != nil { + return false + } + return u.Hostname() == Host + }, + NewProvider: func( + repoURL string, + opts *gitprovider.Options, + ) (gitprovider.Interface, error) { + return NewProvider(repoURL, opts) + }, +} + +func init() { + gitprovider.Register(ProviderName, registration) +} + // provider is a Bitbucket Cloud implementation of gitprovider.Interface. type provider struct { owner string diff --git a/pkg/gitprovider/bitbucket/cloud/provider_test.go b/pkg/gitprovider/bitbucket/cloud/provider_test.go index eb6cf2a989..d2c0ad1490 100644 --- a/pkg/gitprovider/bitbucket/cloud/provider_test.go +++ b/pkg/gitprovider/bitbucket/cloud/provider_test.go @@ -144,6 +144,30 @@ func intPtr(i int) *int { return &i } func statePtr(s PullrequestState) *PullrequestState { return &s } +func Test_registration(t *testing.T) { + t.Run("predicate matches bitbucket.org", func(t *testing.T) { + assert.True(t, registration.Predicate("https://bitbucket.org/owner/repo")) + }) + + t.Run("predicate does not match self-hosted URLs", func(t *testing.T) { + assert.False(t, registration.Predicate("https://bitbucket.example.com/projects/PROJ/repos/repo")) + }) + + t.Run("predicate does not match other providers", func(t *testing.T) { + assert.False(t, registration.Predicate("https://github.com/owner/repo")) + }) + + t.Run("predicate handles invalid URLs", func(t *testing.T) { + assert.False(t, registration.Predicate("://invalid-url")) + }) + + t.Run("NewProvider factory works", func(t *testing.T) { + p, err := registration.NewProvider("https://bitbucket.org/owner/repo", nil) + assert.NoError(t, err) + assert.NotNil(t, p) + }) +} + func TestNewProvider(t *testing.T) { t.Run("successful creation", func(t *testing.T) { provider, err := NewProvider("https://bitbucket.org/owner/repo", &gitprovider.Options{Token: "token"}) diff --git a/pkg/gitprovider/bitbucket/datacenter/provider.go b/pkg/gitprovider/bitbucket/datacenter/provider.go index 68ca8addc1..2b8a996409 100644 --- a/pkg/gitprovider/bitbucket/datacenter/provider.go +++ b/pkg/gitprovider/bitbucket/datacenter/provider.go @@ -3,10 +3,42 @@ package datacenter import ( "context" "fmt" + "net/url" + "strings" "github.com/akuity/kargo/pkg/gitprovider" ) +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{} diff --git a/pkg/gitprovider/bitbucket/datacenter/provider_test.go b/pkg/gitprovider/bitbucket/datacenter/provider_test.go new file mode 100644 index 0000000000..c0b18d586b --- /dev/null +++ b/pkg/gitprovider/bitbucket/datacenter/provider_test.go @@ -0,0 +1,35 @@ +package datacenter + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_registration(t *testing.T) { + t.Run("predicate matches self-hosted bitbucket hostname", func(t *testing.T) { + assert.True(t, registration.Predicate("https://bitbucket.example.com/projects/PROJ/repos/repo")) + }) + + t.Run("predicate matches subdomain of bitbucket", func(t *testing.T) { + 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) { + assert.False(t, registration.Predicate("https://bitbucket.org/owner/repo")) + }) + + t.Run("predicate does not match other providers", func(t *testing.T) { + assert.False(t, registration.Predicate("https://github.com/owner/repo")) + }) + + t.Run("predicate handles invalid URLs", func(t *testing.T) { + assert.False(t, registration.Predicate("://invalid-url")) + }) + + t.Run("NewProvider factory works", func(t *testing.T) { + p, err := registration.NewProvider("https://bitbucket.example.com/projects/PROJ/repos/repo", nil) + assert.NoError(t, err) + assert.NotNil(t, p) + }) +} diff --git a/pkg/promotion/runner/builtin/git_pr_merger.go b/pkg/promotion/runner/builtin/git_pr_merger.go index 37287ffac9..55eba0940d 100644 --- a/pkg/promotion/runner/builtin/git_pr_merger.go +++ b/pkg/promotion/runner/builtin/git_pr_merger.go @@ -15,7 +15,8 @@ import ( "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" // Bitbucket 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 diff --git a/pkg/promotion/runner/builtin/git_pr_opener.go b/pkg/promotion/runner/builtin/git_pr_opener.go index ccb1bd6c5b..fb354a6f54 100644 --- a/pkg/promotion/runner/builtin/git_pr_opener.go +++ b/pkg/promotion/runner/builtin/git_pr_opener.go @@ -17,7 +17,8 @@ import ( "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" // Bitbucket 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 diff --git a/pkg/promotion/runner/builtin/git_pr_waiter.go b/pkg/promotion/runner/builtin/git_pr_waiter.go index 36ac69a784..991574fb06 100644 --- a/pkg/promotion/runner/builtin/git_pr_waiter.go +++ b/pkg/promotion/runner/builtin/git_pr_waiter.go @@ -14,7 +14,8 @@ import ( "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" // Bitbucket 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 From 76b46edcf404a590f2bac64f63711af108091c1f Mon Sep 17 00:00:00 2001 From: fuskovic Date: Fri, 24 Apr 2026 21:12:13 +0530 Subject: [PATCH 14/18] flush out datacenter git provider implementation Signed-off-by: fuskovic --- .../bitbucket/cloud/pr_integration_test.go | 38 - .../bitbucket/datacenter/provider.go | 389 +++++- .../bitbucket/datacenter/provider_test.go | 1170 ++++++++++++++++- pkg/promotion/runner/builtin/git_pr_merger.go | 8 +- pkg/promotion/runner/builtin/git_pr_opener.go | 8 +- pkg/promotion/runner/builtin/git_pr_waiter.go | 8 +- 6 files changed, 1550 insertions(+), 71 deletions(-) delete mode 100644 pkg/gitprovider/bitbucket/cloud/pr_integration_test.go diff --git a/pkg/gitprovider/bitbucket/cloud/pr_integration_test.go b/pkg/gitprovider/bitbucket/cloud/pr_integration_test.go deleted file mode 100644 index 5db60c1379..0000000000 --- a/pkg/gitprovider/bitbucket/cloud/pr_integration_test.go +++ /dev/null @@ -1,38 +0,0 @@ -//go:build integration && bitbucket - -package cloud - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/akuity/kargo/pkg/gitprovider" - gptest "github.com/akuity/kargo/pkg/gitprovider/testing" -) - -func TestCreateAndMergePullRequest(t *testing.T) { - repoURL := gptest.RequireEnv(t, "TEST_BITBUCKET_REPO_URL") - token := gptest.RequireEnv(t, "TEST_BITBUCKET_TOKEN") - - repoCfg := gptest.RepoConfig{ - RepoURL: repoURL, - Token: token, - GitUsername: gptest.RequireEnv(t, "TEST_BITBUCKET_USERNAME"), - } - - prov, err := NewProvider(repoURL, &gitprovider.Options{Token: token}) - require.NoError(t, err) - - gptest.RunPRTests(t, repoCfg, prov, []gptest.PRTestCase{ - { - Name: "unspecified merge method", - ExpectedParents: 2, // Repo default is merge commit - }, - { - Name: "explicit merge method", - MergeMethod: "squash", - ExpectMergeErr: true, // Library limitation - }, - }) -} diff --git a/pkg/gitprovider/bitbucket/datacenter/provider.go b/pkg/gitprovider/bitbucket/datacenter/provider.go index 2b8a996409..c41a85c429 100644 --- a/pkg/gitprovider/bitbucket/datacenter/provider.go +++ b/pkg/gitprovider/bitbucket/datacenter/provider.go @@ -3,10 +3,15 @@ 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 ( @@ -40,46 +45,390 @@ func init() { } // provider is a Bitbucket Data Center implementation of gitprovider.Interface. -type provider struct{} +type provider struct { + baseURL string + projectKey string + repoSlug string + client ClientWithResponsesInterface +} // NewProvider returns a Bitbucket Data Center implementation of gitprovider.Interface. func NewProvider( - _ string, - _ *gitprovider.Options, + repoURL string, + opts *gitprovider.Options, ) (gitprovider.Interface, error) { - // TODO: Implement Bitbucket Data Center provider. - return &provider{}, nil + 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( - _ context.Context, - _ *gitprovider.CreatePullRequestOpts, + ctx context.Context, + opts *gitprovider.CreatePullRequestOpts, ) (*gitprovider.PullRequest, error) { - return nil, fmt.Errorf("Bitbucket Data Center provider is not yet implemented") + 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( - _ context.Context, - _ int64, + ctx context.Context, + id int64, ) (*gitprovider.PullRequest, error) { - return nil, fmt.Errorf("Bitbucket Data Center provider is not yet implemented") + 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( - _ context.Context, - _ *gitprovider.ListPullRequestOptions, + ctx context.Context, + opts *gitprovider.ListPullRequestOptions, ) ([]gitprovider.PullRequest, error) { - return nil, fmt.Errorf("Bitbucket Data Center provider is not yet implemented") + 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( - _ context.Context, - _ int64, - _ *gitprovider.MergePullRequestOpts, + ctx context.Context, + id int64, + opts *gitprovider.MergePullRequestOpts, ) (*gitprovider.PullRequest, bool, error) { - return nil, false, fmt.Errorf("Bitbucket Data Center provider is not yet implemented") + 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 } -func (p *provider) GetCommitURL(_ string, _ string) (string, error) { - return "", fmt.Errorf("Bitbucket Data Center provider is not yet implemented") +// 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 index c0b18d586b..a62d693935 100644 --- a/pkg/gitprovider/bitbucket/datacenter/provider_test.go +++ b/pkg/gitprovider/bitbucket/datacenter/provider_test.go @@ -1,35 +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) { - p, err := registration.NewProvider("https://bitbucket.example.com/projects/PROJ/repos/repo", nil) + 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/promotion/runner/builtin/git_pr_merger.go b/pkg/promotion/runner/builtin/git_pr_merger.go index 55eba0940d..47be689e85 100644 --- a/pkg/promotion/runner/builtin/git_pr_merger.go +++ b/pkg/promotion/runner/builtin/git_pr_merger.go @@ -14,12 +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/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 + _ "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 fb354a6f54..53d6450905 100644 --- a/pkg/promotion/runner/builtin/git_pr_opener.go +++ b/pkg/promotion/runner/builtin/git_pr_opener.go @@ -16,12 +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/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 + _ "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 991574fb06..f0add8c020 100644 --- a/pkg/promotion/runner/builtin/git_pr_waiter.go +++ b/pkg/promotion/runner/builtin/git_pr_waiter.go @@ -13,12 +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/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 + _ "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" From 4ae10c12665f87ab6c16ed4e22b9994d071036bf Mon Sep 17 00:00:00 2001 From: fuskovic Date: Fri, 24 Apr 2026 21:27:28 +0530 Subject: [PATCH 15/18] doc updates Signed-off-by: fuskovic --- .../60-reference-docs/30-promotion-steps/git-merge-pr.md | 7 ++++--- .../60-reference-docs/30-promotion-steps/git-open-pr.md | 5 +---- .../60-reference-docs/30-promotion-steps/git-push.md | 2 +- .../30-promotion-steps/git-wait-for-pr.md | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) 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 2432c1b106..88ae160c0e 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. | @@ -49,7 +49,7 @@ system to access the git repos. :::warning -The `wait` option is unreliable for repositories hosted by Bitbucket due to API limitations. +The `wait` option is unreliable for repositories hosted by Bitbucket Cloud due to API limitations. ::: @@ -61,7 +61,8 @@ currently supported Git hosting providers. | Provider | Supported Methods | Default | | -------- | ----------------- | ------- | | Azure |
  • `noFastForward`
  • `rebase`
  • `rebaseMerge`
  • `squash`
| First allowed strategy per the target branch's merge type policy; merge commit if no policy is configured | -| BitBucket |
  • `fast_forward`
  • `merge_commit`
  • `squash`
| The repository's configured default merge strategy | +| Bitbucket Cloud |
  • `fast_forward`
  • `merge_commit`
  • `squash`
| The repository's configured default merge strategy | +| Bitbucket Data Center |
  • `ff`
  • `ff-only`
  • `no-ff`
  • `rebase-ff-only`
  • `rebase-no-ff`
  • `squash`
  • `squash-ff-only`
| The repository's configured default merge strategy | | Gitea |
  • `fast-forward-only`
  • `manually-merged`
  • `merge`
  • `rebase`
  • `rebase-merge`
  • `squash`
| `merge` | | GitHub |
  • `merge`
  • `rebase`
  • `squash`
| `merge` | | GitLab |
  • `merge`
  • `squash`
| 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 e38a2c3433..2eca79fa0e 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 @@ -10,9 +10,6 @@ specified source and target branches. This step is often used after a [`git-push` step](git-push.md) and is commonly followed by a [`git-wait-for-pr` step](git-wait-for-pr.md). -At present, this feature only supports GitHub, Gitea, Azure DevOps, and -GitLab pull/merge requests. - ## Credentials Git steps are utilizing the [repository credentials](../../50-security/30-managing-secrets.md#repository-credentials) @@ -23,7 +20,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 d1a6713704..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. | From d055b9bc45418294794735d5f43e1a7c6580dc41 Mon Sep 17 00:00:00 2001 From: fuskovic Date: Tue, 19 May 2026 15:53:17 +0530 Subject: [PATCH 16/18] remove all references to datacenter Signed-off-by: fuskovic --- Makefile | 1 - .../30-promotion-steps/git-merge-pr.md | 3 +- .../30-promotion-steps/git-open-pr.md | 2 +- .../30-promotion-steps/git-push.md | 2 +- .../30-promotion-steps/git-wait-for-pr.md | 2 +- .../bitbucket/datacenter/provider.go | 434 ------ .../bitbucket/datacenter/provider_test.go | 1203 ----------------- .../spec/bitbucket-datacenter.gen.json | 357 ----- .../datacenter/spec/oapi-codegen.yaml | 10 - .../zz_bitbucket_datacenter_client.gen.go | 1078 --------------- pkg/promotion/runner/builtin/git_pr_merger.go | 11 +- pkg/promotion/runner/builtin/git_pr_opener.go | 11 +- pkg/promotion/runner/builtin/git_pr_waiter.go | 11 +- .../open-container-initiative-utils.test.ts | 11 - 14 files changed, 19 insertions(+), 3117 deletions(-) delete mode 100644 pkg/gitprovider/bitbucket/datacenter/provider.go delete mode 100644 pkg/gitprovider/bitbucket/datacenter/provider_test.go delete mode 100644 pkg/gitprovider/bitbucket/datacenter/spec/bitbucket-datacenter.gen.json delete mode 100644 pkg/gitprovider/bitbucket/datacenter/spec/oapi-codegen.yaml delete mode 100644 pkg/gitprovider/bitbucket/datacenter/zz_bitbucket_datacenter_client.gen.go diff --git a/Makefile b/Makefile index 3ad06bb165..a581114b16 100644 --- a/Makefile +++ b/Makefile @@ -283,7 +283,6 @@ 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 88ae160c0e..e99de286b0 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`, `bitbucket-datacenter`, `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`, `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. | @@ -62,7 +62,6 @@ currently supported Git hosting providers. | -------- | ----------------- | ------- | | Azure |
  • `noFastForward`
  • `rebase`
  • `rebaseMerge`
  • `squash`
| First allowed strategy per the target branch's merge type policy; merge commit if no policy is configured | | Bitbucket Cloud |
  • `fast_forward`
  • `merge_commit`
  • `squash`
| The repository's configured default merge strategy | -| Bitbucket Data Center |
  • `ff`
  • `ff-only`
  • `no-ff`
  • `rebase-ff-only`
  • `rebase-no-ff`
  • `squash`
  • `squash-ff-only`
| The repository's configured default merge strategy | | Gitea |
  • `fast-forward-only`
  • `manually-merged`
  • `merge`
  • `rebase`
  • `rebase-merge`
  • `squash`
| `merge` | | GitHub |
  • `merge`
  • `rebase`
  • `squash`
| `merge` | | GitLab |
  • `merge`
  • `squash`
| 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 947aa86493..9ae4789624 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 @@ -20,7 +20,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`, `bitbucket-datacenter`, `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`, `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 82689ac00c..9c947c722f 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`, `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. | +| `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. | ## 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 db5edbfa7a..0e17dfaddc 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`, `bitbucket-datacenter`, `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`, `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 deleted file mode 100644 index c41a85c429..0000000000 --- a/pkg/gitprovider/bitbucket/datacenter/provider.go +++ /dev/null @@ -1,434 +0,0 @@ -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 deleted file mode 100644 index a62d693935..0000000000 --- a/pkg/gitprovider/bitbucket/datacenter/provider_test.go +++ /dev/null @@ -1,1203 +0,0 @@ -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 deleted file mode 100644 index 6829fb92c9..0000000000 --- a/pkg/gitprovider/bitbucket/datacenter/spec/bitbucket-datacenter.gen.json +++ /dev/null @@ -1,357 +0,0 @@ -{ - "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 deleted file mode 100644 index ab67b475ea..0000000000 --- a/pkg/gitprovider/bitbucket/datacenter/spec/oapi-codegen.yaml +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index dd9c6136b3..0000000000 --- a/pkg/gitprovider/bitbucket/datacenter/zz_bitbucket_datacenter_client.gen.go +++ /dev/null @@ -1,1078 +0,0 @@ -// 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 47be689e85..92f8a7823c 100644 --- a/pkg/promotion/runner/builtin/git_pr_merger.go +++ b/pkg/promotion/runner/builtin/git_pr_merger.go @@ -14,12 +14,11 @@ 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/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 + _ "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 ) 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 53d6450905..8f377ce9dd 100644 --- a/pkg/promotion/runner/builtin/git_pr_opener.go +++ b/pkg/promotion/runner/builtin/git_pr_opener.go @@ -16,12 +16,11 @@ 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/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 + _ "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 ) 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 f0add8c020..ec6dbd22a8 100644 --- a/pkg/promotion/runner/builtin/git_pr_waiter.go +++ b/pkg/promotion/runner/builtin/git_pr_waiter.go @@ -13,12 +13,11 @@ 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/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 + _ "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 ) const stepKindGitWaitForPR = "git-wait-for-pr" diff --git a/ui/src/features/freight-timeline/open-container-initiative-utils.test.ts b/ui/src/features/freight-timeline/open-container-initiative-utils.test.ts index b604968eb2..af2ceba472 100644 --- a/ui/src/features/freight-timeline/open-container-initiative-utils.test.ts +++ b/ui/src/features/freight-timeline/open-container-initiative-utils.test.ts @@ -56,17 +56,6 @@ describe('getGitCommitURL', () => { url: 'git@bitbucket.org:akuity/kargo.git', expected: 'https://bitbucket.org/akuity/kargo/commits/abc1234' }, - // Bitbucket Data Center — self-hosted - { - name: 'bitbucket data center custom domain', - url: 'https://bitbucket.internal.net/akuity/kargo.git', - expected: 'https://bitbucket.internal.net/akuity/kargo/commits/abc1234' - }, - { - name: 'bitbucket data center SSH', - url: 'git@bitbucket.internal.net:akuity/kargo.git', - expected: 'https://bitbucket.internal.net/akuity/kargo/commits/abc1234' - }, // Unknown provider — fall back to original url { name: 'unknown provider returns original url', From b0bbc4daea88503fc14bb003fdf4dd7e53d6f206 Mon Sep 17 00:00:00 2001 From: fuskovic Date: Tue, 19 May 2026 17:42:42 +0530 Subject: [PATCH 17/18] undo unnecessary changes Signed-off-by: fuskovic --- .../60-reference-docs/30-promotion-steps/git-merge-pr.md | 4 ++-- .../60-reference-docs/30-promotion-steps/git-open-pr.md | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) 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 e99de286b0..2432c1b106 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 @@ -49,7 +49,7 @@ system to access the git repos. :::warning -The `wait` option is unreliable for repositories hosted by Bitbucket Cloud due to API limitations. +The `wait` option is unreliable for repositories hosted by Bitbucket due to API limitations. ::: @@ -61,7 +61,7 @@ currently supported Git hosting providers. | Provider | Supported Methods | Default | | -------- | ----------------- | ------- | | Azure |
  • `noFastForward`
  • `rebase`
  • `rebaseMerge`
  • `squash`
| First allowed strategy per the target branch's merge type policy; merge commit if no policy is configured | -| Bitbucket Cloud |
  • `fast_forward`
  • `merge_commit`
  • `squash`
| The repository's configured default merge strategy | +| BitBucket |
  • `fast_forward`
  • `merge_commit`
  • `squash`
| The repository's configured default merge strategy | | Gitea |
  • `fast-forward-only`
  • `manually-merged`
  • `merge`
  • `rebase`
  • `rebase-merge`
  • `squash`
| `merge` | | GitHub |
  • `merge`
  • `rebase`
  • `squash`
| `merge` | | GitLab |
  • `merge`
  • `squash`
| 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 9ae4789624..471945f336 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 @@ -10,6 +10,9 @@ specified source and target branches. This step is often used after a [`git-push` step](git-push.md) and is commonly followed by a [`git-wait-for-pr` step](git-wait-for-pr.md). +At present, this feature only supports GitHub, Gitea, Azure DevOps, GitLab and +BitBucket pull/merge requests. + ## Credentials Git steps are utilizing the [repository credentials](../../50-security/30-managing-secrets.md#repository-credentials) From fca109c208485dc776b37ac0a734b1c3e6ae770f Mon Sep 17 00:00:00 2001 From: fuskovic Date: Tue, 26 May 2026 15:54:47 +0530 Subject: [PATCH 18/18] suggested changes Signed-off-by: fuskovic --- .../60-reference-docs/30-promotion-steps/git-merge-pr.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 2432c1b106..2a8219c321 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 @@ -49,7 +49,11 @@ system to access the git repos. :::warning -The `wait` option is unreliable for repositories hosted by Bitbucket due to API limitations. +The `wait` option is unreliable for repositories hosted by Bitbucket. The +Bitbucket Cloud API does not provide a way to check merge eligibility before +attempting a merge, so Kargo cannot determine in advance whether a PR is blocked +by conflicts, failing checks, or other conditions. As a result, Kargo will +attempt the merge regardless, which may fail unexpectedly. :::