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.

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)

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.