gitbutlerapp/gitbutler · error

Snapshot tree needs a 'HEAD' entry if it has only a…

Error message

Snapshot tree {id} needs a 'HEAD' entry if it has only a 'index-conflicts' entry

What it means

resolve_tree requires a 'HEAD' entry as the base whenever 'index-conflicts' is present: conflicts are restored by replacing entries in an index built from the worktree/HEAD base. A snapshot with only 'index-conflicts' (no HEAD, no worktree) has no base index, so it bails.

Solutions

  1. Rebuild or delete the malformed snapshot so it is regenerated with a proper 'HEAD' base
  2. Include a 'HEAD' entry in the snapshot whenever 'index-conflicts' is written
  3. Fix the snapshot writer to enforce the HEAD+index-conflicts invariant
Defensive patterns

Strategy: validation

Validate before calling

let has = |k: &str| snapshot_tree.entries.iter().any(|e| e.filename == k);
if has("index-conflicts") && !has("HEAD") && !has("worktree") { return Err(anyhow!("snapshot has index-conflicts without base")); }

Try / catch

match resolve_tree(repo, snapshot_tree) { Ok(t) => t, Err(_) => { clear_snapshot(repo); resolve_tree(repo, fresh_snapshot(repo)?)? } }

Prevention

When it happens

Trigger: Resolving a snapshot tree containing an 'index-conflicts' entry but neither a 'HEAD' nor 'worktree' entry, so there is no index to restore conflicts into.

Common situations: Corrupted or partially written snapshots; snapshots edited or produced by a mismatched writer version; synthetic snapshot trees built incorrectly in tests.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18). Data as JSON: /api/errors/7ba6455d9fe61ed6. Report an issue: GitHub.

Appendix: source

Thrown at crates/but-core/src/snapshot/resolve_tree.rs:121

            (None, None) | (Some(_), None) => None,
        };

        let index = match (&head_tree, index, index_conflicts) {
            (_, Some(index_tree), Some(index_conflicts)) => {
                let (mut index, _path) = repo.index_from_tree(&index_tree.id())?.into_parts();
                replace_entries_with_their_restored_conflicts(&mut index, index_conflicts.id())?;
                Some(index)
            }
            (_, Some(index_tree), None) => {
                let (index, _path) = repo.index_from_tree(&index_tree.id())?.into_parts();
                Some(index)
            }
            (Some(worktree_base), None, Some(index_conflicts)) => {
                let (mut index, _path) = repo.index_from_tree(&worktree_base.id())?.into_parts();
                replace_entries_with_their_restored_conflicts(&mut index, index_conflicts.id())?;
                Some(index)
            }
            (None, None, Some(_index_conflicts)) => bail!(
                "Snapshot tree {id} needs a 'HEAD' entry if it has only a 'index-conflicts' entry",
                id = snapshot_tree.id
            ),
            (_, None, None) => None,
        };

        Ok(Outcome {
            worktree_cherry_pick,
            index,
            workspace_references: None,
            metadata: None,
        })
    }

    #[expect(clippy::indexing_slicing)]
    fn replace_entries_with_their_restored_conflicts(
        index: &mut gix::index::State,
        conflict_tree: gix::Id,

View on GitHub (pinned to 58e5313667)