gitbutlerapp/gitbutler · error · anyhow::Error

Refusing to check out conflicted commit {new_head_id}

Error message

Refusing to check out conflicted commit {new_head_id}

What it means

The worktree checkout function peels `new_head_id` to an object and, unless `Options::allow_conflicted_commit_checkout` is set, refuses when the target commit is a GitButler conflicted (conflict-crash) commit — checking such a commit out would materialize conflict-marker trees as if they were real content. This is a deliberate safety guard with an explicit opt-in flag.

Source

Thrown at crates/but-core/src/worktree/checkout/function.rs:47

/// If this changes, then the source sid of a rename could also cause conflicts, maybe? It's a bit unclear what it would mean
/// in practice, but I guess that we bring deleted files back instead of conflicting.
#[instrument(skip(repo), err(Debug))]
pub fn safe_checkout_from_head(
    new_head_id: gix::ObjectId,
    repo: &gix::Repository,
    Options {
        skip_head_update,
        merge_base_override,
        allow_conflicted_commit_checkout,
        allow_uncommitted_changes_to_conflict_with_new_head,
    }: Options,
) -> anyhow::Result<Outcome> {
    let new_object = new_head_id.attach(repo).object()?;
    if !allow_conflicted_commit_checkout
        && new_object.kind.is_commit()
        && crate::Commit::from_id(new_head_id.attach(repo))?.is_conflicted()
    {
        bail!("Refusing to check out conflicted commit {new_head_id}");
    }

    let git2_repo = git2::Repository::open(repo.git_dir())?;
    let head_tree_id = repo.head_tree_id_or_empty()?;
    let head_tree = git2_repo.find_tree(head_tree_id.to_git2())?;
    let old_tree = if let Some(id) = merge_base_override {
        let mut opts = git2::DiffOptions::new();
        opts.context_lines(1);
        // Also write the index.
        let tree = git2_repo.find_object(id.to_git2(), None)?.peel_to_tree()?;
        let diff = git2_repo.diff_tree_to_tree(Some(&head_tree), Some(&tree), Some(&mut opts))?;
        if git2_repo
            .apply(&diff, git2::ApplyLocation::Index, None)
            .is_err()
        {
            // Just overwrite the index.
            git2_repo.index()?.read_tree(&tree)?;
        }

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Resolve or abandon the conflict first, then check out the resolved commit.
  2. If you genuinely intend to inspect a conflicted state, pass `allow_conflicted_commit_checkout: true` in the checkout `Options` and handle the conflict trees knowingly.
  3. Verify the commit beforehand: `but_core::Commit::from_id(...)?.is_conflicted()`.

Example fix

// before
let out = but_core::worktree::checkout(repo, target, Options::default())?;

// after — explicitly allow conflicted (conflict-crash) commits
let out = but_core::worktree::checkout(
    repo,
    target,
    Options { allow_conflicted_commit_checkout: true, ..Default::default() },
)?;
Defensive patterns

Strategy: validation

Validate before calling

// Rust — check the target before checkout
let commit = but_core::Commit::from_id(new_head_id.attach(repo))?;
if commit.is_conflicted() && !opts.allow_conflicted_commit_checkout {
    // refuse early with an actionable message / offer conflict resolution
    return Err(anyhow!("target {new_head_id} is a conflicted commit; resolve conflicts first"));
}
let outcome = checkout(repo, new_head_id, opts)?;

Type guard

fn is_safe_checkout_target(commit: &but_core::Commit<'_>, opts: &Options) -> bool {
    opts.allow_conflicted_commit_checkout || !commit.is_conflicted()
}

Try / catch

match checkout(repo, target, opts) {
    Ok(outcome) => outcome,
    Err(err) if err.to_string().contains("Refusing to check out conflicted commit") => {
        // offer resolve-conflicts flow, or re-run with allow flag if inspection was intended
        resolve_conflicts_first(repo, target).await?;
        checkout(repo, target, opts)
    }
    Err(err) => Err(err),
}

Prevention

When it happens

Trigger: Calling the checkout API with a commit id that `Commit::is_conflicted()` returns true for (message/tree carry conflict metadata), without setting `allow_conflicted_commit_checkout: true` in `Options`.

Common situations: Programmatic navigation to a historical conflict state; snapshots/undo trying to restore a conflicted commit; debug tooling checking out arbitrary commit ids.

Related errors


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