Skip to content

[Feature]: Bitbucket Data Center (self-hosted) support #12031

Description

@dcieslak19973

Problem or use case

Orca supports Bitbucket Cloud (src/main/bitbucket/) for read-only hosted-review status. Teams on Bitbucket Data Center (self-hosted Bitbucket Server) get nothing: their remotes are not recognised as Bitbucket at all, and — worse — the Gitea resolver's catch-all claims them, so the user sees a spurious "Set ORCA_GITEA_TOKEN".

Bitbucket DC is not a configuration variant of Cloud. It is a different API dialect (/rest/api/1.0 vs /2.0), a different repo addressing scheme (projects/{KEY}/repos/{slug} vs {workspace}/{slug}), different auth (Bearer PAT; no email:api_token pair), different field names (camelCase, epoch-millis dates), and an arbitrary hostname that may sit under a context path.

Proposed solution

Reuse the existing bitbucket provider id and make the repository ref host-aware, exactly as GitHub Enterprise Server reuses github and carries its host on GitHubApiRepository.

Do not introduce a bitbucket-server provider id. It would cost a new HostedReviewProvider union member, a new persisted linkedBitbucketServerPR threaded through ~14 main files and ~30 renderer files (including the duplicated link chains in src/main/ipc/worktree-remote.ts:841-926 and src/main/runtime/orca-runtime.ts:2226-2309), two hand-duplicated zod enums, and worktree-schemas.ts tri-state entries — for zero user-visible benefit. Decisively: FORGE_PROVIDERS is ordered and first-non-null-resolveRepository wins (src/main/source-control/forge-provider.ts:295-316); bitbucket sits at index 2, ahead of gitea at index 4, so a new id appended after gitea would never win detection.

Phase 1 — read-only PR status (this issue's deliverable)

  • BitbucketRepoRef becomes a discriminated union: { kind: 'cloud', workspace, repoSlug } | { kind: 'server', baseUrl, projectKey, repoSlug }
  • DC remote parser: HTTPS https://host[/context]/scm/{KEY}/{slug}.git, SSH ssh://git@host:7999/{KEY}/{slug}.git, personal repos ~user (tilde must be preserved for REST paths). The context path is recoverable from an HTTPS remote as everything preceding /scm/.
  • Positive host claim only — must not become a second catch-all or it steals Gitea's remotes. Claim signal: configured base URL (ORCA_BITBUCKET_SERVER_URL) plus the /scm/ path convention (Atlassian: /scm is fixed and non-configurable for HTTPS clones).
  • DC request layer: Authorization: Bearer <PAT>, {base}/rest/api/1.0, start/limit/nextPageStart paging, 429 + Retry-After, surface errors[0].message instead of bare HTTP ${status}
  • Separate DC pull-request mapper module (zero field names overlap with the Cloud payload)
  • Widen the bitbucket preflight status to the 5-field gitea/azure shape (baseUrl, tokenConfigured), and mirror it in src/preload/index.ts and src/preload/api-types.ts
  • Settings card + settings-search copy currently says "Bitbucket Cloud" (locale key c97d58a0f3)

Everything downstream — linkedBitbucketPR persistence, the hosted-review branch cache, the sidebar PR badge, the external review link, getProviderName — already handles bitbucket end to end. That is the entire payoff of reusing the id.

Phase 2 — deep links (follow-up)

src/main/git/hosted-remote-url.ts:17-37,145-148 and its renderer twin src/renderer/src/components/right-sidebar/source-control-remote-repo.ts:48-66 are hardcoded host allowlists producing Cloud-shaped paths. DC needs /projects/{KEY}/repos/{slug}/browse/{path}?at=refs%2Fheads%2F{branch} and /commits/{sha}. The renderer has no access to env config, so the DC host must be plumbed through preflight.

Phase 3 — PR creation (follow-up)

Add 'bitbucket' to HostedReviewCreationProvider, implement createReview (POST .../pull-requests, optionally prefetching /rest/default-reviewers/1.0/.../reviewers), gated on ref.kind === 'server'. This also forces fixing a pre-existing bug: resolveHostedReviewCreationProvider collapses bitbucket → github, so Bitbucket users already see GitHub-branded create copy, and ChecksPanel.tsx:277 renders "Open on GitHub" for a Bitbucket review.

Explicitly out of scope

In-app Checks tab, comment threads, and merge UI. ChecksPanel.tsx is hard-forked GitHub-vs-GitLab and calls window.api.gh / window.api.gl directly; every other provider falls back to an external link. No rate-limit accounting is needed — only GitHub has a budget guard, and forge-provider.test.ts:420 asserts other providers are not gated.

Alternatives or additional context

API delta vs the Cloud client we already have

Verified against the Atlassian Bitbucket Data Center OpenAPI spec, info.version 9.6 and 10.4 (identical on every point below).

Concern Cloud v2.0 (current code) Data Center
Base URL https://api.bitbucket.org/2.0 {siteBaseUrl}/rest/api/1.0, arbitrary host + optional context path. There is no /rest/api/2.0 on DC and none is planned.
Repo addressing /repositories/{workspace}/{slug} /projects/{PROJECT_KEY}/repos/{slug}
Auth Bearer, or Basic email:apiToken Bearer PAT, or Basic username:password. ORCA_BITBUCKET_EMAIL has no DC analogue.
Current user GET /user No equivalent. Probe GET /rest/api/1.0/users?limit=1 (401 = bad token); read the account name off the X-AUSERNAME response header.
Branch lookup BBQL q=source.branch.name = "x" ?at=refs/heads/x&direction=OUTGOING&state=ALL&order=NEWEST. direction defaults to INCOMING, which returns PRs targeting the branch — silent wrong-answer bug if missed.
Branch refs source.branch.name, plain fromRef.id, fully-qualified. The existing branch.replace(/^refs\/heads\//, '') must be inverted.
Head SHA source.commit.hash fromRef.latestCommit
Dates updated_on, ISO-8601 string updatedDate, epoch millis int64
Naming snake_case camelCase — no field name survives unchanged
Web URL links.html.href Undocumented (links is modelled as an empty write-only object; the runtime shape is links.self[0].href). Construct {base}/projects/{KEY}/repos/{slug}/pull-requests/{id} instead. mapBitbucketPullRequest hard-returns null when links.html.href is absent (pull-request-mappers.ts:86) — that guard would reject every DC PR.
State enum OPEN/MERGED/DECLINED/SUPERSEDED No SUPERSEDED
Build status /commit/{sha}/statuses/build /rest/build-status/1.0/commits/{sha} is deprecated (7.14) and its replacement requires a key and returns one status. Use /rest/build-status/1.0/commits/stats/{sha} → pre-aggregated {successful, inProgress, failed, cancelled, unknown}, a one-call fit for deriveBitbucketBuildStatus.
Build enum SUCCESSFUL/FAILED/INPROGRESS/STOPPED SUCCESSFUL/FAILED/INPROGRESS/CANCELLED/UNKNOWN
Mergeability hardcoded 'UNKNOWN' Real: GET .../pull-requests/{id}/mergeoutcome: CLEAN | CONFLICTED | UNKNOWN
Pagination pagelen/page/next start/limit/isLastPage/nextPageStart; default 25, server-capped — always read limit off the response and follow nextPageStart, never start + size
Errors { error: { message } } { errors: [{ context, message, exceptionName }] }
Rate limit X-RateLimit-* Admin-toggled token bucket, 429 + Retry-After. Declared on zero endpoints in the spec (injected by a servlet filter ahead of REST), so it must be handled generically in the shared fetch wrapper.

Known risks

  1. Config UX. Orca stores no forge tokens — gitea/bitbucket/azure-devops read process.env on every call, so changing a token needs an app restart, and the settings cards are static prose naming env vars. safeStorage storage exists but IntegrationCredentialService = 'Linear' \| 'Jira'. Shipping DC as another ORCA_* pair is consistent and cheap; a real host+PAT form would make it the first forge with token-entry UI. Phase 1 takes the env-var path.
  2. Multi-instance. Env-var config means one DC instance per session.
  3. HTTP access tokens cannot merge PRs (a merge creates a commit). Relevant if Phase 3 grows a merge action.
  4. SSH remotes carry neither the context path nor a derivable API base — the SSH base URL is configured independently of HTTP and 7999 is only the default. SSH-remote repos require the configured base URL.
  5. Any change to the link surface must touch both ipc/worktree-remote.ts and runtime/orca-runtime.ts; the same duplication exists between main's reviewCopy() and the renderer's localizedHostedReviewCopy.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions