gitbutlerapp/gitbutler · error · anyhow::Error

Failed to get MR merge status

Error message

Failed to get MR merge status: {}

What it means

Thrown by GitLabClient::get_mr_merge_status when GET /projects/:id/merge_requests/:iid (the same endpoint used for full MR fetches) returns a non-2xx status while probing only the merge_status and user_notes_count fields. It exists as a cheap pre-check before attempting a merge: is_mergeable is true only when GitLab reports merge_status == "can_be_merged". Like the other status bails in this client, only the status code is reported, not GitLab's error body.

Solutions

  1. Verify the MR still exists (browser or curl the same endpoint) before polling status
  2. Re-run `but config forge auth` if the token expired (401)
  3. Stop or debounce the polling loop once an MR is detected as closed/merged so it does not query deleted MRs
  4. If 403, confirm the account still has access to the project

Example fix

// before
let status = client.get_mr_merge_status(project_id, mr_iid).await?; // error bubbles into the whole view

// after
let status = match client.get_mr_merge_status(project_id, mr_iid).await {
    Ok(s) => s,
    Err(err) if err.to_string().contains("404") => {
        MergeRequestMergeStatus { mergeable_state: None, comments_count: 0, is_mergeable: false } // MR gone; not mergeable
    }
    Err(err) => return Err(err),
};
Defensive patterns

Strategy: try-catch

Validate before calling

// Only poll status for MRs you know exist (from a prior list/get)
ensure!(known_mr_ids.contains(&mr_iid), "skipping status poll for unknown MR {mr_iid}");
let status = client.get_mr_merge_status(project_id, mr_iid).await?;

Try / catch

let mergeable = match client.get_mr_merge_status(project_id, mr_iid).await {
    Ok(s) => s.is_mergeable,
    Err(_) => false, // unknown state is not mergeable; never enable merge actions on it
};

Prevention

When it happens

Trigger: Calling get_mr_merge_status(project_id, mr_iid) for an MR that was deleted or is not visible to the token (404), a forbidden project (403), an unauthenticated request (401), or passing mr_iid = 0 / a negative value that GitLab rejects.

Common situations: UI polls merge status on a timer for an MR that got closed+deleted while the poll was in flight; token lost permissions when the user was removed from the project; stale MR list cached locally referencing deleted MRs.

Related errors


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

Appendix: source

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

        &self,
        project_id: GitLabProjectId,
        mr_iid: i64,
    ) -> Result<MergeRequestMergeStatus> {
        #[derive(Debug, Deserialize)]
        struct MrMergeStatusResponse {
            #[serde(default)]
            merge_status: Option<String>,
            #[serde(default)]
            user_notes_count: i64,
        }

        let url = format!(
            "{}/projects/{}/merge_requests/{}",
            self.base_url, project_id, mr_iid
        );
        let response = self.client.get(&url).send().await?;
        if !response.status().is_success() {
            bail!("Failed to get MR merge status: {}", response.status());
        }
        let body: MrMergeStatusResponse = response.json().await?;
        let is_mergeable = matches!(body.merge_status.as_deref(), Some("can_be_merged"));
        Ok(MergeRequestMergeStatus {
            mergeable_state: body.merge_status,
            comments_count: body.user_notes_count,
            is_mergeable,
        })
    }

    pub async fn update_merge_request(
        &self,
        params: &UpdateMergeRequestParams<'_>,
    ) -> Result<MergeRequest> {
        #[derive(Serialize)]
        struct UpdateMergeRequestBody<'a> {
            #[serde(skip_serializing_if = "Option::is_none")]
            title: Option<&'a str>,

View on GitHub (pinned to caf1f223d3)