You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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)
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.tsandsrc/preload/api-types.ts
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}/merge → outcome: 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
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.
Multi-instance. Env-var config means one DC instance per session.
HTTP access tokens cannot merge PRs (a merge creates a commit). Relevant if Phase 3 grows a merge action.
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.
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.
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 "SetORCA_GITEA_TOKEN".Bitbucket DC is not a configuration variant of Cloud. It is a different API dialect (
/rest/api/1.0vs/2.0), a different repo addressing scheme (projects/{KEY}/repos/{slug}vs{workspace}/{slug}), different auth (Bearer PAT; noemail:api_tokenpair), different field names (camelCase, epoch-millis dates), and an arbitrary hostname that may sit under a context path.Proposed solution
Reuse the existing
bitbucketprovider id and make the repository ref host-aware, exactly as GitHub Enterprise Server reusesgithuband carries its host onGitHubApiRepository.Do not introduce a
bitbucket-serverprovider id. It would cost a newHostedReviewProviderunion member, a new persistedlinkedBitbucketServerPRthreaded through ~14 main files and ~30 renderer files (including the duplicated link chains insrc/main/ipc/worktree-remote.ts:841-926andsrc/main/runtime/orca-runtime.ts:2226-2309), two hand-duplicated zod enums, andworktree-schemas.tstri-state entries — for zero user-visible benefit. Decisively:FORGE_PROVIDERSis ordered and first-non-null-resolveRepositorywins (src/main/source-control/forge-provider.ts:295-316);bitbucketsits at index 2, ahead ofgiteaat index 4, so a new id appended after gitea would never win detection.Phase 1 — read-only PR status (this issue's deliverable)
BitbucketRepoRefbecomes a discriminated union:{ kind: 'cloud', workspace, repoSlug } | { kind: 'server', baseUrl, projectKey, repoSlug }https://host[/context]/scm/{KEY}/{slug}.git, SSHssh://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/.ORCA_BITBUCKET_SERVER_URL) plus the/scm/path convention (Atlassian:/scmis fixed and non-configurable for HTTPS clones).Authorization: Bearer <PAT>,{base}/rest/api/1.0,start/limit/nextPageStartpaging,429+Retry-After, surfaceerrors[0].messageinstead of bareHTTP ${status}bitbucketpreflight status to the 5-field gitea/azure shape (baseUrl,tokenConfigured), and mirror it insrc/preload/index.tsandsrc/preload/api-types.tsc97d58a0f3)Everything downstream —
linkedBitbucketPRpersistence, the hosted-review branch cache, the sidebar PR badge, the external review link,getProviderName— already handlesbitbucketend 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-148and its renderer twinsrc/renderer/src/components/right-sidebar/source-control-remote-repo.ts:48-66are 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'toHostedReviewCreationProvider, implementcreateReview(POST .../pull-requests, optionally prefetching/rest/default-reviewers/1.0/.../reviewers), gated onref.kind === 'server'. This also forces fixing a pre-existing bug:resolveHostedReviewCreationProvidercollapsesbitbucket → github, so Bitbucket users already see GitHub-branded create copy, andChecksPanel.tsx:277renders "Open on GitHub" for a Bitbucket review.Explicitly out of scope
In-app Checks tab, comment threads, and merge UI.
ChecksPanel.tsxis hard-forked GitHub-vs-GitLab and callswindow.api.gh/window.api.gldirectly; every other provider falls back to an external link. No rate-limit accounting is needed — only GitHub has a budget guard, andforge-provider.test.ts:420asserts 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.version9.6 and 10.4 (identical on every point below).https://api.bitbucket.org/2.0{siteBaseUrl}/rest/api/1.0, arbitrary host + optional context path. There is no/rest/api/2.0on DC and none is planned./repositories/{workspace}/{slug}/projects/{PROJECT_KEY}/repos/{slug}email:apiTokenusername:password.ORCA_BITBUCKET_EMAILhas no DC analogue.GET /userGET /rest/api/1.0/users?limit=1(401 = bad token); read the account name off theX-AUSERNAMEresponse header.q=source.branch.name = "x"?at=refs/heads/x&direction=OUTGOING&state=ALL&order=NEWEST.directiondefaults toINCOMING, which returns PRs targeting the branch — silent wrong-answer bug if missed.source.branch.name, plainfromRef.id, fully-qualified. The existingbranch.replace(/^refs\/heads\//, '')must be inverted.source.commit.hashfromRef.latestCommitupdated_on, ISO-8601 stringupdatedDate, epoch millis int64snake_casecamelCase— no field name survives unchangedlinks.html.hreflinksis modelled as an empty write-only object; the runtime shape islinks.self[0].href). Construct{base}/projects/{KEY}/repos/{slug}/pull-requests/{id}instead.mapBitbucketPullRequesthard-returnsnullwhenlinks.html.hrefis absent (pull-request-mappers.ts:86) — that guard would reject every DC PR.OPEN/MERGED/DECLINED/SUPERSEDEDSUPERSEDED/commit/{sha}/statuses/build/rest/build-status/1.0/commits/{sha}is deprecated (7.14) and its replacement requires akeyand returns one status. Use/rest/build-status/1.0/commits/stats/{sha}→ pre-aggregated{successful, inProgress, failed, cancelled, unknown}, a one-call fit forderiveBitbucketBuildStatus.SUCCESSFUL/FAILED/INPROGRESS/STOPPEDSUCCESSFUL/FAILED/INPROGRESS/CANCELLED/UNKNOWN'UNKNOWN'GET .../pull-requests/{id}/merge→outcome: CLEAN | CONFLICTED | UNKNOWNpagelen/page/nextstart/limit/isLastPage/nextPageStart; default 25, server-capped — always readlimitoff the response and follownextPageStart, neverstart + size{ error: { message } }{ errors: [{ context, message, exceptionName }] }X-RateLimit-*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
process.envon every call, so changing a token needs an app restart, and the settings cards are static prose naming env vars.safeStoragestorage exists butIntegrationCredentialService = 'Linear' \| 'Jira'. Shipping DC as anotherORCA_*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.7999is only the default. SSH-remote repos require the configured base URL.ipc/worktree-remote.tsandruntime/orca-runtime.ts; the same duplication exists between main'sreviewCopy()and the renderer'slocalizedHostedReviewCopy.