Skip to content

feat(core): add Bitbucket Cloud provider - #117

Closed
lukaso-bot wants to merge 2 commits into
mainfrom
feat/bitbucket-provider
Closed

feat(core): add Bitbucket Cloud provider#117
lukaso-bot wants to merge 2 commits into
mainfrom
feat/bitbucket-provider

Conversation

@lukaso-bot

Copy link
Copy Markdown
Collaborator

Adds the Bitbucket Cloud (bitbucket.org) provider — partial #5 (Gitea/Forgejo are in flight via #111; Sourcehut remains as a distinct API → separate PR).

REST 2.0 implementation of the Provider interface, mirroring the GitLab provider's shape. pnpm validate green end-to-end (314 tests).

Ancestry direction (the core risk — proven, not assumed)

find-release calls compareCommits(repo, tagSha, targetCommit) and treats behind | identical as "the tag contains the commit." Bitbucket has no GitHub/GitLab-style 4-way compare, so compareCommits uses the commits endpoint's include/exclude set difference:

commits?include={head}&exclude={base}   →   commits reachable from head but NOT base
  empty   → head ⊆ base → head is ancestor of base → base CONTAINS head → 'behind'
  present → not contained → 'ahead'

This mirrors GitLab's from=base&to=head (commits in head-not-in-base) exactly.

Mutation proof: a test asserts the requested URL carries include={head}&exclude={base} AND the status. Flipping include/exclude makes it go red (expected … to contain 'include=aaaaaaaa'; status flips); restored → green. Recorded in BitbucketProvider.compareCommits — ANCESTRY DIRECTION.

Methods

Method Bitbucket mapping
getPullRequest state === 'MERGED' + merge_commit.hash; DECLINED/SUPERSEDEDPrNotMergedError; missing hash → PrMergeCommitUnavailableError
getCommit /commits/{sha}hash / date / first-line subject
listTagsWithDates /refs/tags?sort=-target.date, follows the body next link (MAX_TAG_PAGES=5, parity with GitLab)
compareCommits include=head&exclude=base (above)
getReleaseNotes returns null without a network call — Bitbucket has no Releases API
getIssueClosingCommit best-effort: open / closed_without_fix — Bitbucket's issue→commit linkage isn't reliably exposed via REST
containingTags / getTagDate unset — no endpoint; the algorithm falls back to galloping bisect (same as GitHub)

terms = { mergeRequest: 'Pull request', mergeRequestPrefix: '#' }. Rate-limit headers read when present, null otherwise (Bitbucket emits them inconsistently).

Wiring

  • Provider.kind widened to 'github' | 'gitlab' | 'bitbucket'.
  • providers/index.ts: routes bitbucket.orgmakeBitbucketProvider, added to isKnownHost + the unsupported-host error's supported list, exported. bitbucket.org is a single fixed host (like github.com) — no extraBitbucketHosts opt needed.
  • parse-input.ts: recognizes /commits|src/{sha}, /pull-requests/{N}, /issues/{N}, and surfaces a shape error (not UnsupportedHostError) for unrecognized bitbucket.org URLs.
  • Web Worker routes automatically via core's providerFor — no web-side plumbing.

Not in this PR (deliberate)

REST 2.0 implementation of the Provider interface for bitbucket.org, mirroring
the GitLab provider's shape. Partial #5 (Gitea/Forgejo in flight via #111;
Sourcehut remains — distinct API, separate PR).

Ancestry (the core risk — find-release treats compareCommits(repo, tagSha,
targetCommit) status behind|identical as "tag contains commit"): Bitbucket has
no GitHub/GitLab-style 4-way compare, so compareCommits uses the commits
endpoint's include/exclude set difference: include=head&exclude=base; empty ⟹
head is an ancestor of base ⟹ the base tag CONTAINS head → 'behind'. This
mirrors GitLab's from=base&to=head exactly. Direction locked by a URL-asserting
test and proven by mutation (swapping include/exclude turns it red, restored
green).

- getPullRequest: state MERGED + merge_commit.hash (DECLINED/SUPERSEDED →
  PrNotMergedError; missing hash → PrMergeCommitUnavailableError).
- getCommit: /commits/{sha} → hash/date/first-line subject.
- listTagsWithDates: /refs/tags?sort=-target.date, follows the body `next` link.
- getReleaseNotes: returns null without a call — Bitbucket has no Releases API.
- getIssueClosingCommit: best-effort (open/closed_without_fix) — Bitbucket's
  issue→commit linkage isn't reliably exposed via REST.
- containingTags/getTagDate: left unset (no endpoint) → algorithm bisects.

Wiring: kind widened to 'bitbucket'; providers/index.ts routes bitbucket.org +
isKnownHost + export; parse-input.ts recognizes /commits|src/{sha},
/pull-requests/{N}, /issues/{N} and surfaces a shape error (not unsupported)
for unrecognized bitbucket.org URLs. Web Worker routes automatically via core's
providerFor (bitbucket.org is a fixed host like github.com — no extra-hosts opt).

Co-Authored-By: Claude <noreply@anthropic.com>

@lukaso-bot lukaso-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Blocking

getCommit calls the wrong endpoint (packages/core/src/providers/bitbucket/client.ts:151) — see inline. The plural /commits/{sha} list endpoint returns {values:[...]}; getCommit reads it as a single object, so every Bitbucket commit and PR lookup is broken. One-char fix: /commits/ -> /commit/. The unit test passes only because it mocks the wrong shape too (see inline on the test). Likely copied from GitHub's /commits/{sha}, which does return a single object; Bitbucket's does not.

Not in this diff, but reachable for Bitbucket once merged

Pre-existing github-vs-gitlab binaries; this PR turns them on for Bitbucket without updating them, so the provider ships half-wired:

  • packages/web/src/auth.ts:22 (resolveProviderToken) branches GitHub vs GitLab only. For bitbucket.org it falls into the GitLab path: an inbound x-user-gitlab-token header would be forwarded to api.bitbucket.org as a Bearer token (cross-host leak, same invariant as the GITLAB_TOKEN guard), and there is no BITBUCKET_TOKEN path, so private repos always 401. Add a bitbucket branch (or short-circuit to undefined).
  • packages/cli/src/auth.ts:30 (resolveToken) — same binary. Bitbucket runs glab auth status --hostname bitbucket.org (always fails, 2s wasted) and never reads a Bitbucket env var. Private repos can only auth via --token.
  • packages/web/src/routes/issue.tsx:524 (renderIssueTransient) — host === 'github.com' ? 'issues' : '-/issues' builds bitbucket.org/.../-/issues/N, which parseInput rejects. The normal issue path uses provider.urls.issue(); this error-render path was missed.
  • packages/web/src/ui/layout.tsx:208 — Slack snippet hardcodes host === 'github.com' ? '#' : '!', so an unreleased Bitbucket PR renders !7 (GitLab MR syntax) instead of #7. terms.mergeRequestPrefix is #. Narrow: only the slack format, only unreleased PRs.

Minor / cleanup

  • BITBUCKET_HOST = 'bitbucket.org' is defined in three files (client.ts:47, parse-input.ts, providers/index.ts where it's exported). The other two could import the export. Low priority — mirrors the existing GitHub local-def pattern.
  • bitbucket-client.test.ts: urlOf (reads _harnessUrl, which is never set) plus the trailing void urlOf; is dead code; the tests read module-level lastUrl directly. Delete both.

Ancestry direction in compareCommits (include=head, exclude=base, empty -> behind -> contains) verified correct, and locked by the test at line 171.

Comment thread packages/core/src/providers/bitbucket/client.ts Outdated
Comment thread packages/core/test/bitbucket-client.test.ts
getCommit called the plural /commits/{sha} list endpoint but read the
response as a single object (body.hash/date/message). The plural endpoint
returns a {values:[...]} envelope, so body.hash was undefined, fullSha
became undefined, and every Bitbucket commit and PR lookup silently
reported "not yet released".

Switch to the singular /commit/{sha} endpoint, which returns the single
commit object the field reads expect. compareCommits and listTagsWithDates
keep the plural list endpoint (they read body.values). Adds a URL guard
test asserting the singular path so it cannot regress.

Co-Authored-By: Claude <noreply@anthropic.com>
@lukaso

lukaso commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Won't do until there is demand.

@lukaso lukaso closed this Jul 26, 2026
@lukaso-bot lukaso-bot mentioned this pull request Sep 1, 2026
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.

2 participants