gitbutlerapp/gitbutler · error

GitForcePushProtection

GitForcePushProtection

Error message

The force push was blocked because the remote branch contains commits that would be overwritten.

{e}

What it means

Returned from push_with_askpass when the push was initiated with force_push_protection enabled and gix reported ForcePushProtection: the remote ref points at commits that the local push would overwrite (the remote branch contains commits not present locally, i.e. the update is not a strict fast-forward of what was expected). It carries Code::GitForcePushProtection so the UI/API can offer an explicit 'force anyway' action. GerritNoNewChanges and NonFastForward are handled as separate cases right below it.

Source

Thrown at crates/gitbutler-git/src/context.rs:290

    })
    .join()
    .map_err(|panic| {
        let reason = if let Some(message) = panic.downcast_ref::<String>() {
            message.clone()
        } else if let Some(message) = panic.downcast_ref::<&'static str>() {
            (*message).to_owned()
        } else {
            "unknown panic payload".to_owned()
        };

        anyhow!("git push worker thread panicked: {reason}").context(
            but_error::Context::new("git push failed unexpectedly").with_code(Code::Unknown),
        )
    })??;
    match result {
            Ok(stderr) => Ok(stderr),
            Err(err) => match err {
                crate::Error::ForcePushProtection(e) => Err(anyhow!(
                    "The force push was blocked because the remote branch contains commits that would be overwritten.\n\n{e}"
                )
                .context(Code::GitForcePushProtection)),
                crate::Error::GerritNoNewChanges(_) => {
                    // Treat "no new changes" as success for Gerrit.
                    Ok(String::new())
                }
                crate::Error::NonFastForward(_) => Err(err).context(Code::GitNonFastForward),
                _ => Err(map_needs_authorization(err)),
            },
        }
}

fn serialize_branch_to_remote<S>(
    branch_to_remote: &[(String, gix::refs::FullName, String)],
    serializer: S,
) -> std::result::Result<S::Ok, S::Error>
where

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Fetch and integrate the remote commits first (rebase/merge your stack onto the updated origin branch), then push again
  2. If overwriting is intended, push with the force flag while consciously disabling force_push_protection for that call (the UI's 'force push' confirmation does exactly this)
  3. Verify what would be lost: `git log --oneline <local>..<remote-branch>` before forcing
  4. If CI noise causes this, coordinate branch ownership or enable push --force-with-lease semantics

Example fix

// before
ctx.push(head, target_ref, /* with_force */ true, /* force_push_protection */ true, None, askpass, vec![])?;

// after: integrate remote state first, then the protected push succeeds
ctx.fetch(&remote_name, None)?;
// ... rebase the stack onto the updated origin branch ...
ctx.push(head, target_ref, true, /* protection now passes */ true, None, askpass, vec![])?;
Defensive patterns

Strategy: validation

Validate before calling

// pre-check divergence before a protected force push
ctx.fetch(&remote_name, None)?;
let remote_ref = format!("refs/remotes/{remote_name}/{branch}");
let remote_id = repo.find_reference(&remote_ref)?.peel_to_commit()?.id;
let base = repo.merge_base(remote_id, local_head)?;
if base != Some(remote_id) {
    // remote has commits local does not contain: the protection WILL fire
    anyhow::bail!("remote {remote_ref} moved ahead — rebase/merge first or force without protection");
}

Try / catch

use but_error::Code;
match ctx.push(head, target, true, true, None, askpass, vec![]) {
    Ok(stderr) => Ok(stderr),
    Err(err) if err.downcast_ref::<Code>() == Some(&Code::GitForcePushProtection)
        || err.to_string().contains("force push was blocked") =>
    {
        // surface to the user: integrate remote changes, or retry with protection off
        offer_force_push_confirmation()
    }
    Err(err) => Err(err),
}

Prevention

When it happens

Trigger: Pushing a virtual branch/workspace over refs/remotes origin state that has moved ahead (someone else pushed); force_push_protection=true with stale local remote-tracking refs because no fetch ran recently; rebasing/rewriting a stack and pushing with force while protection is on; concurrent pushes from another machine or CI.

Common situations: Two developers or a developer plus CI pushing to the same branch; pushing after history rewrite (rebase, amend) with the safety check enabled; the app's remote-tracking ref is stale relative to the actual server state.

Related errors


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