{"record":{"id":"7141062691833bea","repo":"gitbutlerapp/gitbutler","slug":"refusing-to-check-out-conflicted-commit-new-head","errorCode":null,"errorMessage":"Refusing to check out conflicted commit {new_head_id}","messagePattern":"Refusing to check out conflicted commit (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/but-core/src/worktree/checkout/function.rs","lineNumber":47,"sourceCode":"/// If this changes, then the source sid of a rename could also cause conflicts, maybe? It's a bit unclear what it would mean\n/// in practice, but I guess that we bring deleted files back instead of conflicting.\n#[instrument(skip(repo), err(Debug))]\npub fn safe_checkout_from_head(\n    new_head_id: gix::ObjectId,\n    repo: &gix::Repository,\n    Options {\n        skip_head_update,\n        merge_base_override,\n        allow_conflicted_commit_checkout,\n        allow_uncommitted_changes_to_conflict_with_new_head,\n    }: Options,\n) -> anyhow::Result<Outcome> {\n    let new_object = new_head_id.attach(repo).object()?;\n    if !allow_conflicted_commit_checkout\n        && new_object.kind.is_commit()\n        && crate::Commit::from_id(new_head_id.attach(repo))?.is_conflicted()\n    {\n        bail!(\"Refusing to check out conflicted commit {new_head_id}\");\n    }\n\n    let git2_repo = git2::Repository::open(repo.git_dir())?;\n    let head_tree_id = repo.head_tree_id_or_empty()?;\n    let head_tree = git2_repo.find_tree(head_tree_id.to_git2())?;\n    let old_tree = if let Some(id) = merge_base_override {\n        let mut opts = git2::DiffOptions::new();\n        opts.context_lines(1);\n        // Also write the index.\n        let tree = git2_repo.find_object(id.to_git2(), None)?.peel_to_tree()?;\n        let diff = git2_repo.diff_tree_to_tree(Some(&head_tree), Some(&tree), Some(&mut opts))?;\n        if git2_repo\n            .apply(&diff, git2::ApplyLocation::Index, None)\n            .is_err()\n        {\n            // Just overwrite the index.\n            git2_repo.index()?.read_tree(&tree)?;\n        }","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-core/src/worktree/checkout/function.rs#L29-L65","documentation":"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.","triggerScenarios":"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`.","commonSituations":"Programmatic navigation to a historical conflict state; snapshots/undo trying to restore a conflicted commit; debug tooling checking out arbitrary commit ids.","solutions":["Resolve or abandon the conflict first, then check out the resolved commit.","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.","Verify the commit beforehand: `but_core::Commit::from_id(...)?.is_conflicted()`."],"exampleFix":"// before\nlet out = but_core::worktree::checkout(repo, target, Options::default())?;\n\n// after — explicitly allow conflicted (conflict-crash) commits\nlet out = but_core::worktree::checkout(\n    repo,\n    target,\n    Options { allow_conflicted_commit_checkout: true, ..Default::default() },\n)?;","handlingStrategy":"validation","validationCode":"// Rust — check the target before checkout\nlet commit = but_core::Commit::from_id(new_head_id.attach(repo))?;\nif commit.is_conflicted() && !opts.allow_conflicted_commit_checkout {\n    // refuse early with an actionable message / offer conflict resolution\n    return Err(anyhow!(\"target {new_head_id} is a conflicted commit; resolve conflicts first\"));\n}\nlet outcome = checkout(repo, new_head_id, opts)?;","typeGuard":"fn is_safe_checkout_target(commit: &but_core::Commit<'_>, opts: &Options) -> bool {\n    opts.allow_conflicted_commit_checkout || !commit.is_conflicted()\n}","tryCatchPattern":"match checkout(repo, target, opts) {\n    Ok(outcome) => outcome,\n    Err(err) if err.to_string().contains(\"Refusing to check out conflicted commit\") => {\n        // offer resolve-conflicts flow, or re-run with allow flag if inspection was intended\n        resolve_conflicts_first(repo, target).await?;\n        checkout(repo, target, opts)\n    }\n    Err(err) => Err(err),\n}","preventionTips":["Filter conflicted commits out of any programmatically-built list of checkout targets.","Only set `allow_conflicted_commit_checkout` in read-only/inspection tooling that understands conflict trees.","Resolve or undo conflict states before navigating history in a workspace."],"tags":["checkout","conflict","worktree","guard-rail"],"backgroundTag":"conflicted-commit-checkout","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}