Support Bitbucket#108
Conversation
7d8b168 to
b50b89a
Compare
Implement the pull-request-related actions for the Bitbucket Cloud provider: get_pull_request, get_pull_requests, create_pull_request, and update_pull_request, each exposed through bin/bitbucket-client. get_pull_requests introduces the provider's paginated-result helper, which reads Bitbucket's in-body 'values' list and derives the next cursor from the 'next' URL (Bitbucket paginates in the body, not via headers). update_pull_request handles state changes through the separate /decline endpoint, since Bitbucket's PUT body has no state field; reopening is not supported by the API and is rejected. This and other divergences are captured in the new bitbucket-quirks.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implement get_commits and get_commits_by_path for the Bitbucket Cloud
provider, both exposed through bin/bitbucket-client.
get_commits maps 'ref' to Bitbucket's 'include' filter (defaulting to the main
branch). get_commits_by_path uses the file-history endpoint, whose {commit}
segment is required, so it falls back to the repository's default branch when
no ref is given; since file-history entries embed only an abbreviated commit,
it hydrates each with a concurrent GET .../commit/{hash} fan-out.
Neither Bitbucket endpoint supports date filtering, so since/until raise
ResourceBadRequest rather than silently returning unfiltered commits. These
divergences are recorded in bitbucket-quirks.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implement get_pull_request_comments, create_pull_request_comment, and delete_pull_request_comment for the Bitbucket Cloud provider, each exposed through bin/bitbucket-client. get_pull_request_comments filters out inline (review) comments and deleted tombstones so its output matches GitHub's conversation-only 'list issue comments' semantics. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implement get_branch, create_branch, and delete_branch for the Bitbucket Cloud provider, each exposed through bin/bitbucket-client, using the refs/branches endpoints. update_branch stays unimplemented: Bitbucket's refs API exposes no PUT/PATCH to move a branch pointer, and emulating it via delete+recreate would be non-atomic and unable to honor the fast-forward-only (force=False) contract. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fetch file contents via Bitbucket's /src endpoint, exposed through bin/bitbucket-client. The /src endpoint returns raw bytes with no JSON metadata, so we base64-encode the response, derive size from its byte length, and compute the git blob SHA locally (sha1 of "blob <len>\0" + content) so FileContent.sha matches the blob SHA GitHub and GitLab report. Recorded in bitbucket-quirks.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Compare two commits by combining Bitbucket's commits and diffstat endpoints, exposed through bin/bitbucket-client. Bitbucket has no single compare endpoint, so we list the commits reachable from end_sha but not start_sha (paginated) and walk the diffstat in full for the changed-file list. Bitbucket's diffstat spec orders commits opposite to git, so we pass end..start to obtain git's start..end diff. Recorded in bitbucket-quirks.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fetch a single commit via Bitbucket's commit endpoint, exposed through bin/bitbucket-client. The endpoint returns commit metadata without the file diff, so files is None (use get_commit_changes for changed files). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
List a pull request's changed files via Bitbucket's diffstat endpoint, exposed through bin/bitbucket-client. Diffstat reports per-file line counts but no patch text, so patch is None and sha is empty. Recorded in bitbucket-quirks.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
List a pull request's commits via Bitbucket's commits endpoint, exposed through bin/bitbucket-client. Bitbucket returns them newest-first, so the page is reversed to match GitHub/GitLab's oldest-first order. Also factor the shared commit-author parsing into a _commit_author helper used by the commit mappers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implement the review-creation actions for the Bitbucket Cloud provider, exposed through bin/bitbucket-client: - create_review_comment_file: inline comment anchored to a file (inline.path) - create_review_comment_line: inline comment on a single diff line (inline from/to keyed by side) - create_review_comment_reply: reply referencing the parent comment's id - create_review: fan-out of inline comments, review body, and approve, since Bitbucket has no atomic review endpoint create_review_comment_multiline is left unsupported: Bitbucket's inline anchor addresses a single line per side with no line range. Divergences are recorded in bitbucket-quirks.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implement create_check_run, get_check_run, and update_check_run for the
Bitbucket Cloud provider, exposed through bin/bitbucket-client, mapping check
runs to commit build statuses (check_run_id is "{sha}:{key}", key derived from
external_id or name).
Bitbucket build statuses are lower-fidelity than GitHub checks: states collapse
to INPROGRESS/SUCCESSFUL/FAILED/STOPPED, timestamps are ignored, only the output
title is forwarded as the description, url defaults to the commit page, and the
key is capped at 40 chars. Recorded in bitbucket-quirks.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
b50b89a to
71841ab
Compare
The end-to-end test previously required three bin/*-server proxies, live repositories on GitHub/GitLab/Bitbucket in a specific state, and personal credentials -- so it could only run on one machine. Replace that with an in-process client (SourceCodeManager.make_client) whose provider talks to the real service directly through requests, and record every interaction once with vcrpy. On replay the suite needs no servers, no live repos, and no credentials; the first run records live from .credentials. Provider thread-pool fan-out is run inline during tests (vcrpy's cassette is not thread-safe), a fixed 'now' keeps generated values deterministic across record and replay, and secrets (auth headers, installation tokens, the App installation id) are scrubbed or normalized so cassettes are safe to commit and replay is credential-free. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
71841ab to
921864d
Compare
Create draft pull requests via Bitbucket's draft flag, exposed through bin/bitbucket-client. The draft key is added to the create payload only when drafting, so the non-draft create request is unchanged. Add an end-to-end test (all three providers) with recorded cassettes: GitLab encodes draft-ness as a 'Draft:' title prefix, while GitHub and Bitbucket expose a draft flag on the pull request. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion and get_repository_assignees
- get_authenticated_actor: GET /user
- get_app_installation: GET /user/workspaces/{workspace}/permission
(the per-repo permission endpoint was deprecated -- CHANGE-2770)
- get_repository_assignees: GET /workspaces/{workspace}/permissions/repositories/{repo_slug}
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implement the ResolveReviewThreadProtocol pair and the comment-collapsing
actions for Bitbucket:
- get_thread_id_from_review_comment_unique_id: local (a comment roots its
own thread, so its id is the thread id)
- resolve_review_thread: POST .../comments/{comment_id}/resolve
- collapse_pull_request_comment: delegates to resolve_review_thread
- update_and_collapse_pull_request_comment: PUT the body, then resolve
Bitbucket's resolve is not idempotent (409 on re-resolve), so the
end-to-end test resolves each thread exactly once.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PUT .../comments/{comment_id} with the new body. update_and_collapse now
delegates to it instead of inlining the PUT, mirroring the GitLab provider.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…_template
- get_pull_request_diff: GET .../pullrequests/{id}/diff (raw unified diff)
- get_pull_request_template: single .bitbucket/pull_request_template.md read
from the source branch (Bitbucket's only template location)
Fix get_file_content for refs containing '/': Bitbucket's /src endpoint parses
the commit only up to the first '/' (even URL-encoded), so a slash-containing
branch/tag ref is resolved to a commit hash first. Apply the same resolution in
get_commits_by_path, the other action that puts a ref in a path segment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…request_url Pure URL builders (no HTTP), hardcoding https://bitbucket.org like the existing _commit_web_url helper. get_file_url uses Bitbucket's #lines-N / #lines-N:M anchors. Also fix the GitLab test fixture, which fed the provider malformed data (a double-netloc external_id and a project-ref name instead of the human slug), making its web URLs come out wrong. The GitLab provider itself was correct for production inputs; update the two assertions that had encoded the bad output. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
List the repository tree via the /src browsing endpoint, mapping each commit_file/commit_directory entry to a TreeEntry (mode from attributes; sha empty, since /src exposes no git object id). get_full_tree walks every page. Also make the paginated get() forward the page/cursor param only when a cursor is present (Bitbucket /src pages by an opaque token, so a synthetic page=1 is rejected; the first page must omit it). Applied the same cursor-optional handling to GitLab for consistency. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
get_directory_contents lists a directory's direct children via the /src endpoint (non-recursive), mapping each entry to a content-less FileContent (empty sha, since /src exposes no git object id); a file path yields PathIsNotDirectory. get_readme scans the repo root for a known README name and returns its content, raising ReadmeNotFound otherwise (Bitbucket has no dedicated README endpoint). The root scan walks pages starting without a cursor, since /src pages by an opaque token. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…git_commit create_commit uses Bitbucket's form-encoded POST /src: written files are form fields keyed by path, deletions are repeated `files` fields, and the new commit's SHA is read from the response's Location header (the 201 has no body). Divergences from GitHub/GitLab, all documented in bitbucket-quirks: - force is emulated by deleting the branch first, since /src rejects a non-fast-forward commit (parents must be the branch head). - moves are not native: read the old file, write it at the new path, and delete the old (Bitbucket reports a rename). - chmod is unsupported, and content is text-only (binary is rejected). get_commit_changes reads the diffstat endpoint (patch is always None); get_git_commit reads the commit endpoint, setting tree.sha to the commit SHA (Bitbucket exposes no tree-object id, like GitLab). The terminal ApiClient must send /src as a form body rather than JSON; the request Content-Type survives the RPC proxy (it is in ALLOWED_HEADERS) and the Location response header survives normalize_headers, so the proxied path works once the terminal client honors the content type. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bitbucket has no thread/discussion resource, so we page through all of a pull request's comments and assemble inline review threads locally: each non-deleted inline comment with no parent roots a thread, and replies join it via parent (general, non-inline comments are excluded). is_resolved comes from the comment's resolution object, is_outdated from inline.outdated, and the line/path from the inline anchor. All threads are returned at once (next_cursor is always None) and include_reactions is a no-op, since Bitbucket has no reactions on comments. Extends test_review_comments to read back the threads it creates: it walks every page (providers return a single page each) and looks up the created threads by id, asserting the line thread everywhere and the file-level thread on GitHub/Bitbucket (GitLab only surfaces line-anchored discussions). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bitbucket serves repository archives only from the web host (bitbucket.org/<workspace>/<repo>/get/<ref>.<ext>), with no api.bitbucket.org equivalent. That host is unreachable through the RPC proxy (is_safe_path only accepts relative paths), so the action is left unimplemented rather than working only for direct clients. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6df33f0 to
b88f86b
Compare
|
@sentry review |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f6464af. Configure here.
#108 (comment) map_review_comment set thread_id to the comment's own id even for replies, and get_thread_id_from_review_comment_unique_id returned the unique id unchanged. Bitbucket's resolve endpoint only accepts the root (top-level) comment id -- resolving via a reply id returns 403 "Comment is not a top-level comment" -- so deriving a thread from a reply targeted the wrong comment and failed to resolve. Now map_review_comment roots a reply at its parent, and get_thread_id_from_review_comment_unique_id fetches the comment and returns its parent id (or its own id when top-level), mirroring how GitHub looks the thread up and GitLab derives it from the discussion id. test_review_comments gains a regression that resolves a thread derived from a reply. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@jacquev6 ideally we split the bitbucket code from the integration test into two PRs though. It's pretty hard to review this on GitHub's UI in this state. The integration test PR can be stacked on the bitbucket one and once we approve the BB by itself, we can just merge the test into it and merge it to main |
|
@sentry review |
| # The first page carries no cursor; only forward one when present. | ||
| if "cursor" in pagination: | ||
| params["page"] = str(pagination["cursor"]) |
There was a problem hiding this comment.
I think i changed it in seer to pass a cursor on first page, but this (per_page) was causing issues
There was a problem hiding this comment.
This should also be fixed separate from this PR.
| # Most Bitbucket writes are JSON, but the create-commit endpoint (POST /src) | ||
| # is form-encoded; the provider signals that via the Content-Type header. | ||
| content_type = next((v for k, v in (headers or {}).items() if k.lower() == "content-type"), "") | ||
| if "form-urlencoded" in content_type: | ||
| kwargs["data"] = data | ||
| else: | ||
| kwargs["json"] = data | ||
| if params is not None: | ||
| kwargs["params"] = params | ||
| if allow_redirects is not None: | ||
| kwargs["allow_redirects"] = allow_redirects |
There was a problem hiding this comment.
Bug: The BitbucketApiClient in bin/bitbucket-server always JSON-serializes request data, ignoring the Content-Type header, which breaks form-encoded endpoints.
Severity: CRITICAL
Suggested Fix
Update the BitbucketApiClient.request() method in bin/bitbucket-server to inspect the Content-Type header. If the header indicates application/x-www-form-urlencoded, use kwargs["data"] = data to send form-encoded data. Otherwise, default to using kwargs["json"] = data for JSON payloads. This logic is already correctly implemented in the test environment's client.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: bin/bitbucket-server#L75-L86
Potential issue: The `BitbucketApiClient.request()` method in `bin/bitbucket-server`
unconditionally serializes the request body as JSON by setting `kwargs["json"] = data`.
This ignores the `Content-Type` header. As a result, actions like `create_commit`, which
require `application/x-www-form-urlencoded` data, will fail. The server will incorrectly
send a JSON payload, causing the Bitbucket API's `/src` endpoint to reject the request.
This bug affects the standalone server, which is a production-facing component.
Also affects:
src/scm/providers/bitbucket/provider.py:1
Did we get this right? 👍 / 👎 to inform future reviews.
| # author association, and anchor to the PR rather than a commit. | ||
| diff_hunk=None, | ||
| review_id=None, | ||
| author_association=None, |
There was a problem hiding this comment.
Bug: The map_review_comment function incorrectly sets thread_id to a reply's own ID instead of the root comment's ID, preventing thread resolution.
Severity: MEDIUM
Suggested Fix
Modify map_review_comment to correctly identify reply comments. When a comment is a reply (i.e., it has a parent field in the Bitbucket API response), the thread_id should be set to the parent['id']. For root comments, the thread_id should remain the comment's own ID.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: src/scm/providers/bitbucket/provider.py#L1482
Potential issue: The `map_review_comment` function unconditionally sets `thread_id` to
the `comment_id` for all comments. When a reply is created via
`create_review_comment_reply`, the resulting `ReviewComment` object has its `thread_id`
set to the reply's own ID, not the parent thread's ID. Consequently, any attempt to
resolve the thread using this `thread_id` with `resolve_review_thread` will fail with a
403 error from the Bitbucket API, which requires the root comment's ID for thread
resolution.
| self, | ||
| branch: BranchName, | ||
| parent_sha: SHA, | ||
| message: str, | ||
| actions: list[ChmodCommitAction | DeleteCommitAction | MoveCommitAction | WriteCommitAction], | ||
| force: bool = False, | ||
| create_branch: bool = False, | ||
| ) -> ActionResult[Commit]: | ||
| """Create a commit on ``branch`` via Bitbucket's ``POST /src`` endpoint. | ||
|
|
||
| Unlike GitHub (git-data API) and GitLab (a JSON actions array), Bitbucket |
There was a problem hiding this comment.
Bug: The BitbucketProvider.create_commit method is missing the author parameter defined in its protocol, causing a TypeError when a commit is created with a custom author.
Severity: HIGH
Suggested Fix
Add the author: CommitAuthorParam | None = None parameter to the create_commit method signature in src/scm/providers/bitbucket/provider.py. The method should then be updated to handle this new parameter, either by passing it to the underlying Bitbucket API or ignoring it if the API does not support custom authors.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: src/scm/providers/bitbucket/provider.py#L637-L648
Potential issue: The `BitbucketProvider.create_commit` method signature does not match
the `CreateCommitProtocol` it is supposed to implement, as it is missing the `author`
parameter. A generic wrapper function, `actions.create_commit`, accepts an `author` and
passes it to the provider's implementation. When this function is called for a Bitbucket
repository with an `author` argument, it will raise a `TypeError` because
`BitbucketProvider.create_commit` does not accept the `author` keyword argument. This
causes a runtime crash for any operation attempting to create a commit with a custom
author on Bitbucket.
Also affects:
src/scm/actions.py:1137~1142
|
@jacquev6 I would add a test that asserts the BitBucket provider is an instance of all the protocols it supports. There are some type errors which should be identifiable and we'll need the regression coverage in the future. |

This PR introduces Bitbucket to the SCM platform.
This is a large PR, but not a complex one.
Here is my advice to reviewers:
request,get,post, etc. methodsbin/*-servertests/integration/cassettes/test_end_to_end/*.yaml: these files were recorded from actual traffic