gitbutlerapp/gitbutler · error

The working tree differs from the original commit. A forced

Error message

The working tree differs from the original commit. A forced abort is necessary.\nIf you are seeing this message, please report it as a bug. The UI should have prevented this line getting hit.

What it means

Aborting edit mode via `abort_and_return_to_workspace` refuses to proceed without `force` when the working tree still differs from the commit edit mode started from (`changes_from_initial` returns entries). The GitButler UI is supposed to detect this divergence and get user confirmation before forcing, so the message itself says hitting it is a bug in whatever called the API unguarded.

Source

Thrown at crates/gitbutler-edit-mode/src/lib.rs:281

    for ref_name in [EDIT_BRANCH_REF, UNCOMMITTED_CHANGES_REF] {
        // Tolerate an already-absent ref, but surface real lookup failures.
        if let Some(reference) = repo.try_find_reference(ref_name)? {
            reference
                .delete()
                .with_context(|| format!("Failed to delete reference {ref_name}"))?;
        }
    }
    delete_edit_mode_metadata(ctx)?;
    Ok(())
}

pub(crate) fn abort_and_return_to_workspace(
    ctx: &Context,
    force: bool,
    perm: &mut RepoExclusive,
) -> Result<()> {
    if !force && !changes_from_initial(ctx, perm.read_permission())?.is_empty() {
        bail!(
            "The working tree differs from the original commit. A forced abort is necessary.\nIf you are seeing this message, please report it as a bug. The UI should have prevented this line getting hit."
        );
    }

    #[expect(deprecated, reason = "checkout/materialization boundary")]
    let repo = &*ctx.git2_repo.get()?;

    // Checkout gitbutler workspace branch
    repo.set_head(WORKSPACE_BRANCH_REF)
        .context("Failed to set head reference")?;

    let uncommited_changes = get_uncommitted_changes(&*ctx.repo.get()?)?;
    let uncommited_changes = repo.find_tree(uncommited_changes.to_git2())?;

    repo.checkout_tree(
        uncommited_changes.as_object(),
        Some(CheckoutBuilder::new().force().remove_untracked(true)),
    )?;

View on GitHub (pinned to caf1f223d3)

Solutions

  1. If discarding the divergence is intended, pass `force: true` — exactly what the UI does after user confirmation
  2. Otherwise commit or revert the working-tree changes first, then abort edit mode without force
  3. If reached through the GitButler app UI, report it as a bug — the UI should have prevented it

Example fix

// before
abort_and_return_to_workspace(&ctx, /* force */ false, &mut perm)?;

// after: caller decides, like the UI does
let force = user_confirmed_discard();
abort_and_return_to_workspace(&ctx, force, &mut perm)?;
Defensive patterns

Strategy: validation

Validate before calling

// ask the edit-mode status API whether the worktree diverged before aborting
if edit_mode_reports_changes(&ctx)? && !user_confirmed_discard {
    return Err(anyhow::anyhow!("working tree diverged; confirm discard to force abort"));
}

Try / catch

match abort_and_return_to_workspace(&ctx, force, &mut perm) {
    Err(err) if err.to_string().contains("forced abort is necessary") => {
        // prompt the user, then retry with force = true
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling `abort_and_return_to_workspace(ctx, force = false, ...)` while the working tree has modifications relative to the original edit-mode commit — edits made during edit mode that were never committed or reverted.

Common situations: Scripts or tests calling the edit-mode abort API directly; races where UI state says clean but the worktree is dirty; programmatic operating-mode switching that skips the confirmation step.

Related errors


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