Skip to content

Support Bitbucket#108

Open
jacquev6 wants to merge 38 commits into
mainfrom
jacquev6/document-for-bitbucket
Open

Support Bitbucket#108
jacquev6 wants to merge 38 commits into
mainfrom
jacquev6/document-for-bitbucket

Conversation

@jacquev6

@jacquev6 jacquev6 commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

This PR introduces Bitbucket to the SCM platform.

This is a large PR, but not a complex one.

Here is my advice to reviewers:

  • start by reviewing the modified files (the GitLab provider, the RPC client, the actions): there are few of them, and it's the only place where a regression could have been introduced
  • review the "infrastructure" part of the new provider: its request, get, post, etc. methods
  • don't spend too much time reviewing each action method in the provider: they all pass the new end-to-end test, which also passes on GitHub and GitLab, so everything is proven consistent
  • similarly, you may want to review the new test's "infrastructure" part, which is mostly copied from bin/*-server
  • each individual test is less critical, as it passes on the already validated GitHub and GitLab providers
  • do not review tests/integration/cassettes/test_end_to_end/*.yaml: these files were recorded from actual traffic

@jacquev6
jacquev6 force-pushed the jacquev6/document-for-bitbucket branch 2 times, most recently from 7d8b168 to b50b89a Compare July 7, 2026 10:25
jacquev6 and others added 12 commits July 8, 2026 17:27
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>
@jacquev6
jacquev6 force-pushed the jacquev6/document-for-bitbucket branch from b50b89a to 71841ab Compare July 8, 2026 15:30
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>
@jacquev6
jacquev6 force-pushed the jacquev6/document-for-bitbucket branch from 71841ab to 921864d Compare July 13, 2026 09:31
jacquev6 and others added 8 commits July 13, 2026 11:42
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>
jacquev6 and others added 6 commits July 15, 2026 11:54
…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>
@jacquev6 jacquev6 changed the title Plan for Bitbucket support Support Bitbucket Jul 15, 2026
@jacquev6
jacquev6 force-pushed the jacquev6/document-for-bitbucket branch from 6df33f0 to b88f86b Compare July 15, 2026 14:28
@jacquev6
jacquev6 marked this pull request as ready for review July 15, 2026 14:31
@jacquev6
jacquev6 requested a review from a team as a code owner July 15, 2026 14:31
Comment thread bin/bitbucket-server Outdated
@jacquev6

Copy link
Copy Markdown
Contributor Author

@sentry review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ 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.

Comment thread src/scm/providers/bitbucket/provider.py Outdated
Comment thread pyproject.toml Outdated
jacquev6 and others added 3 commits July 15, 2026 16:45
#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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

@bruno-garcia

Copy link
Copy Markdown
Member

@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

@billyvg

billyvg commented Jul 15, 2026

Copy link
Copy Markdown
Member

@sentry review

Comment on lines +312 to +314
# The first page carries no cursor; only forward one when present.
if "cursor" in pagination:
params["page"] = str(pagination["cursor"])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think i changed it in seer to pass a cursor on first page, but this (per_page) was causing issues

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also be fixed separate from this PR.

Comment thread bin/bitbucket-server
Comment on lines +76 to +86
# 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +638 to +648
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@cmanallen

Copy link
Copy Markdown
Member

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants