{"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":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-gitlab/src/client.rs","lineNumber":494,"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":476,"sourceCodeEnd":512,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/58e5313667b857ef39a730e380af31816a7b1768/crates/but-gitlab/src/client.rs#L476-L512","documentation":"GitLabClient::merge_merge_request sends a PUT to the GitLab merge-request merge endpoint and treats any non-2xx HTTP status from GitLab as a failure. The GitLab API rejected the merge request; the response status (e.g. 405, 406, 409, 422) is included in the message. The error body text is NOT captured here, so the message only reveals the status code.","triggerScenarios":"Calling merge_merge_request when GitLab returns a non-success status: MR is not mergeable (unresolved discussions, failing pipelines/merge checks), MR already merged or closed, insufficient permissions to merge, sha mismatch (MR was updated since fetched), or squash/merge params invalid for the project settings.","commonSituations":"Merging an MR whose branch was just updated by another commit (stale sha); CI pipeline still running with 'Pipelines must succeed' enabled; user lacks Developer+ role; MR in draft state when project blocks merging drafts; MR already merged by someone else.","solutions":["Check the status code in the message: 405 usually means the MR is not mergeable (check `merge_status` / `detailed_merge_status` first), 409 means sha mismatch or already merged.","Re-fetch the MR and verify it is mergeable (no unresolved threads, pipeline green, not draft) before calling merge_merge_request.","Pass the correct `sha` parameter (the head commit at fetch time) so GitLab can reject stale merges explicitly instead of silently merging; if 409, re-fetch and retry.","Verify the authenticated token has at least Developer role and merge rights on the target branch (protected branch rules).","Improve diagnosability by reading the response body like the sibling methods do (response.text()) and including it in the error."],"exampleFix":"// before\nlet response = self.client.put(&url).json(&body).send().await?;\nif !response.status().is_success() {\n    bail!(\"Failed to merge merge request: {}\", response.status());\n}\n// after\nlet response = self.client.put(&url).json(&body).send().await?;\nif !response.status().is_success() {\n    let status = response.status();\n    let error_text = response.text().await.unwrap_or_default();\n    bail!(\"Failed to merge merge request: {status} - {error_text}\");\n}","handlingStrategy":"try-catch","validationCode":"let mr = client.get_merge_request(project_id, mr_iid).await?;\nassert_eq!(mr.draft, false);\nassert!(mr.merge_status == \"can_be_merged\");\nassert!(mr.blocking_discussions_resolved == Some(true));","typeGuard":null,"tryCatchPattern":"match client.merge_merge_request(project_id, mr_iid, params).await {\n    Ok(()) => println!(\"merged\"),\n    Err(e) => {\n        let msg = e.to_string();\n        if msg.contains(\"409\") {\n            // stale sha or already merged: re-fetch MR and retry once\n        } else if msg.contains(\"405\") {\n            // not mergeable: check pipeline/discussions\n        } else {\n            return Err(e);\n        }\n    }\n}","preventionTips":["Check detailed_merge_status and blocking_discussions_resolved before merging","Always pass the sha you fetched the MR with so stale merges fail fast","Log the response body (patch the client or wrap the call) for actionable GitLab errors","Verify branch protection rules allow the token's user to merge the target branch"],"tags":["gitlab","http-error","merge-request","api"],"backgroundTag":"http-error-response","analyzedSha":"58e5313667b857ef39a730e380af31816a7b1768","analyzedAt":"2026-09-18T06:50:32.052Z","contentChangedAt":"2026-09-18T06:50:32.052Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}