{"record":{"id":"9a9ba2b6a5b8d215","repo":"gitbutlerapp/gitbutler","slug":"bitbucket-does-not-support-reopening-a-declined-pu","errorCode":null,"errorMessage":"Bitbucket does not support reopening a declined pull request via the API.","messagePattern":"Bitbucket does not support reopening a declined pull request via the API\\.","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/but-forge/src/review.rs","lineNumber":2013,"sourceCode":"            // a `state` field. Map the forge-agnostic ReviewState onto that.\n            let state_event = state.as_ref().map(|s| s.as_gitlab_state_event());\n            let params = but_gitlab::UpdateMergeRequestParams {\n                project_id,\n                mr_iid,\n                title: title.as_deref(),\n                description: body.as_deref(),\n                target_branch: target_base.as_deref(),\n                state_event,\n            };\n            but_gitlab::mr::update(preferred_account, params, storage).await?;\n            Ok(())\n        }\n        ForgeName::Bitbucket => {\n            let preferred_account = preferred_forge_user\n                .as_ref()\n                .and_then(|user| user.bitbucket());\n            if matches!(state, Some(ReviewState::Open)) {\n                return Err(anyhow::anyhow!(\n                    \"Bitbucket does not support reopening a declined pull request via the API.\"\n                ));\n            }\n            if title.is_some() || body.is_some() || target_base.is_some() {\n                let id = review_number\n                    .try_into()\n                    .context(\"PR: Failed to cast usize to i64, somehow\")?;\n                let params = but_bitbucket::UpdatePullRequestParams {\n                    workspace: owner,\n                    repo_slug: repo,\n                    id,\n                    title: title.as_deref(),\n                    description: body.as_deref(),\n                    target_branch: target_base.as_deref(),\n                };\n                but_bitbucket::pr::update(preferred_account, params, storage).await?;\n            }\n            if matches!(state, Some(ReviewState::Closed)) {","sourceCodeStart":1995,"sourceCodeEnd":2031,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-forge/src/review.rs#L1995-L2031","documentation":"In `update_review()`, the Bitbucket arm rejects `ReviewState::Open` up front: Bitbucket Cloud's API cannot reopen a declined PR through the endpoints used here (closing maps to `pr::decline`, edits map to `pr::update`). The check runs before any HTTP call, so the PR is left untouched.","triggerScenarios":"Calling `update_review` with `forge: bitbucket` and `ReviewUpdatePayload.state == Some(ReviewState::Open)` — typically a 'Reopen' action on a declined Bitbucket PR. Title/body/target-base edits and `Some(ReviewState::Closed)` (decline) proceed normally.","commonSituations":"UIs reusing GitHub semantics ('reopen after close') across forges; users expecting declined Bitbucket PRs to be reopenable; automations that normalize state transitions per forge.","solutions":["Don't offer 'reopen' for declined Bitbucket PRs — hide the action when the forge is Bitbucket.","Create a new PR from the same branches instead of reopening the declined one.","Pass `state: None` when you only want to edit title/description/target branch on Bitbucket.","If reopening is a hard requirement, call Bitbucket's REST API outside but-forge."],"exampleFix":"// before\nupdate_review(&user, &info, number,\n    ReviewUpdatePayload { state: Some(ReviewState::Open), ..Default::default() },\n    &storage).await?;\n\n// after\nif matches!(info.forge, ForgeName::Bitbucket)\n    && matches!(payload.state, Some(ReviewState::Open))\n{\n    // reopening a declined PR is unsupported; offer 'new PR from branch' instead\n    return Ok(());\n}\nupdate_review(&user, &info, number, payload, &storage).await?;","handlingStrategy":"validation","validationCode":"use but_forge::forge::ForgeName;\nuse but_forge::review::ReviewState;\n\nif matches!(info.forge, ForgeName::Bitbucket)\n    && matches!(payload.state, Some(ReviewState::Open))\n{\n    // hide 'reopen'; Bitbucket declined PRs cannot reopen via this API\n}","typeGuard":"fn reopen_supported(forge: &but_forge::forge::ForgeName) -> bool {\n    !matches!(forge, ForgeName::Bitbucket)\n}","tryCatchPattern":"match update_review(&user, &info, number, payload, &storage).await {\n    Ok(()) => {}\n    Err(e) if e.to_string().contains(\"does not support reopening\") => {\n        // surface 'create a new PR from the branch' guidance instead\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Keep per-forge state-transition tables: Bitbucket maps Closed to decline and cannot reopen.","For Bitbucket, treat 'declined' as terminal in the UI and offer 'new PR from branch' instead of 'reopen'.","Send edits with `state: None` on Bitbucket to avoid tripping the guard."],"tags":["rust","but-forge","bitbucket","pull-request","reopen","unsupported-operation"],"backgroundTag":"unsupported-api-operation","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}