{"record":{"id":"2df720318c1648b0","repo":"gitbutlerapp/gitbutler","slug":"failed-to-merge-merge-request","errorCode":null,"errorMessage":"Failed to merge merge request: {}","messagePattern":"Failed to merge merge request: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/but-gitlab/src/client.rs","lineNumber":428,"sourceCode":"        #[derive(Serialize)]\n        struct MergeMergeRequestBody {\n            #[serde(skip_serializing_if = \"Option::is_none\")]\n            squash: Option<bool>,\n        }\n\n        let url = format!(\n            \"{}/projects/{}/merge_requests/{}/merge\",\n            self.base_url, params.project_id, params.mr_iid\n        );\n\n        let body = MergeMergeRequestBody {\n            squash: params.squash,\n        };\n\n        let response = self.client.put(&url).json(&body).send().await?;\n\n        if !response.status().is_success() {\n            bail!(\"Failed to merge merge request: {}\", response.status());\n        }\n\n        Ok(())\n    }\n\n    pub async fn set_merge_request_draft_state(\n        &self,\n        params: &SetMergeRequestDraftStateParams,\n    ) -> Result<()> {\n        let project_id = params.project_id.clone();\n        let mr = self\n            .get_merge_request(project_id.clone(), params.mr_iid)\n            .await?;\n        let next_title = update_draft_state_in_title(&mr.title, params.is_draft);\n\n        if next_title == mr.title {\n            return Ok(());\n        }","sourceCodeStart":410,"sourceCodeEnd":446,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-gitlab/src/client.rs#L410-L446","documentation":"Thrown by GitLabClient::merge_merge_request when PUT /projects/:id/merge_requests/:iid/merge returns non-2xx. GitLab uses specific codes here: 405 Method Not Allowed when the MR cannot be merged (conflicts, missing commits, draft, closed), and 406 Not Acceptable when project settings block the merge (pipeline must succeed, only allow merge when discussions resolved). Only the status code is embedded, so decode intent from 401/403/404/405/406.","triggerScenarios":"Merging while merge_status is not can_be_merged (conflicts) → 405; merging a Draft: MR → 405; merging with a failed or running pipeline while 'Pipelines must succeed' is enforced → 406; unresolved required discussions → 406; protected target branch the account cannot push to → 403; MR already merged → 405/406.","commonSituations":"Clicking merge right after pushing conflicting commits; CI still running; draft prefix not stripped before merge attempt; user without Maintainer role merging into a protected branch; double-merge race when two clients merge simultaneously.","solutions":["Pre-check with get_mr_merge_status and only call merge when is_mergeable == true (see validation snippet)","On 405: fetch the MR and surface conflicts (has_conflicts) so the user can rebase/resolve","On 406: check pipeline status and 'merge when pipeline succeeds' instead of merging now (set_merge_request_auto_merge)","Strip the Draft: prefix first via set_merge_request_draft_state(false)","On 403: verify the account can push to the protected target branch (Maintainer or allowed role)"],"exampleFix":"// before\nclient.merge_merge_request(&params).await?; // fires 405/406 straight into an error dialog\n\n// after\nlet status = client.get_mr_merge_status(params.project_id.clone(), params.mr_iid).await?;\nif !status.is_mergeable {\n    anyhow::bail!(\"MR is not mergeable (state: {:?}) — resolve conflicts or wait for checks\", status.mergeable_state);\n}\nclient.merge_merge_request(&params).await?;","handlingStrategy":"validation","validationCode":"// Gate the merge on GitLab's own mergeability verdict\nlet status = client.get_mr_merge_status(params.project_id.clone(), params.mr_iid).await?;\nensure!(status.is_mergeable, \"MR not mergeable (state: {:?})\", status.mergeable_state);\nclient.merge_merge_request(&params).await?;","typeGuard":null,"tryCatchPattern":"match client.merge_merge_request(&params).await {\n    Ok(()) => Ok(()),\n    Err(err) => {\n        let m = err.to_string();\n        if m.contains(\"405\") { anyhow::bail!(\"conflicts or draft state block the merge\") }\n        else if m.contains(\"406\") { anyhow::bail!(\"merge checks (pipeline/discussions) not satisfied\") }\n        else { Err(err) }\n    }\n}","preventionTips":["Strip Draft: prefix (set_merge_request_draft_state) before merging","When 406 is due to a running pipeline, offer auto-merge instead of an immediate merge"],"tags":["gitlab","http","merge-request","merge-conflict","rust"],"backgroundTag":"gitlab-mr-merge-failed","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}