gitbutlerapp/gitbutler · error · anyhow::Error

Failed to merge merge request: {}

Error message

Failed to merge merge request: {}

What it means

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.

Source

Thrown at crates/but-gitlab/src/client.rs:428

        #[derive(Serialize)]
        struct MergeMergeRequestBody {
            #[serde(skip_serializing_if = "Option::is_none")]
            squash: Option<bool>,
        }

        let url = format!(
            "{}/projects/{}/merge_requests/{}/merge",
            self.base_url, params.project_id, params.mr_iid
        );

        let body = MergeMergeRequestBody {
            squash: params.squash,
        };

        let response = self.client.put(&url).json(&body).send().await?;

        if !response.status().is_success() {
            bail!("Failed to merge merge request: {}", response.status());
        }

        Ok(())
    }

    pub async fn set_merge_request_draft_state(
        &self,
        params: &SetMergeRequestDraftStateParams,
    ) -> Result<()> {
        let project_id = params.project_id.clone();
        let mr = self
            .get_merge_request(project_id.clone(), params.mr_iid)
            .await?;
        let next_title = update_draft_state_in_title(&mr.title, params.is_draft);

        if next_title == mr.title {
            return Ok(());
        }

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Pre-check with get_mr_merge_status and only call merge when is_mergeable == true (see validation snippet)
  2. On 405: fetch the MR and surface conflicts (has_conflicts) so the user can rebase/resolve
  3. On 406: check pipeline status and 'merge when pipeline succeeds' instead of merging now (set_merge_request_auto_merge)
  4. Strip the Draft: prefix first via set_merge_request_draft_state(false)
  5. On 403: verify the account can push to the protected target branch (Maintainer or allowed role)

Example fix

// before
client.merge_merge_request(&params).await?; // fires 405/406 straight into an error dialog

// after
let status = client.get_mr_merge_status(params.project_id.clone(), params.mr_iid).await?;
if !status.is_mergeable {
    anyhow::bail!("MR is not mergeable (state: {:?}) — resolve conflicts or wait for checks", status.mergeable_state);
}
client.merge_merge_request(&params).await?;
Defensive patterns

Strategy: validation

Validate before calling

// Gate the merge on GitLab's own mergeability verdict
let status = client.get_mr_merge_status(params.project_id.clone(), params.mr_iid).await?;
ensure!(status.is_mergeable, "MR not mergeable (state: {:?})", status.mergeable_state);
client.merge_merge_request(&params).await?;

Try / catch

match client.merge_merge_request(&params).await {
    Ok(()) => Ok(()),
    Err(err) => {
        let m = err.to_string();
        if m.contains("405") { anyhow::bail!("conflicts or draft state block the merge") }
        else if m.contains("406") { anyhow::bail!("merge checks (pipeline/discussions) not satisfied") }
        else { Err(err) }
    }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/2df720318c1648b0. Report an issue: GitHub.