gitbutlerapp/gitbutler · error

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

Error message

Snapshot tree {id} needs a 'HEAD' entry if it has a 'worktree' entry

What it means

resolve_tree decodes a snapshot tree into a HEAD tree/index. A snapshot entry 'worktree' is only meaningful relative to a HEAD entry; if a snapshot tree contains 'worktree' but no 'HEAD' entry, resolution cannot proceed and it bails with the tree id.

Solutions

  1. Recreate the snapshot so it contains both 'HEAD' and 'worktree' entries
  2. Drop or rebuild the corrupted snapshot for this project so it is regenerated from current state
  3. Check the snapshot-writing code path to guarantee 'HEAD' is written whenever 'worktree' is
Defensive patterns

Strategy: validation

Validate before calling

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

Try / catch

match resolve_tree(repo, snapshot_tree) { Ok(t) => t, Err(_) => rebuild_snapshot(repo) }

Prevention

When it happens

Trigger: Resolving a snapshot tree whose entries contain 'worktree' but lack a 'HEAD' entry (invalid or hand-crafted snapshot, partially written snapshot, version mismatch between writer and reader).

Common situations: Corrupted or truncated snapshot data; snapshots written by an older/newer GitButler version with different entry conventions; manually constructed snapshot trees 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/6530a5f855a8fd0a. Report an issue: GitHub.

Appendix: source

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

        let worktree_cherry_pick = match (&head_tree, worktree) {
            (Some(worktree_base), Some(worktree)) => {
                let base = merge_base_override.unwrap_or_else(|| worktree_base.object_id());
                let ours = target_worktree_tree_id;
                let theirs = worktree.object_id();

                repo.merge_trees(
                    base,
                    ours,
                    theirs,
                    repo.default_merge_labels(),
                    worktree_cherry_pick_options
                        .map(Ok)
                        .unwrap_or_else(|| repo.tree_merge_options())?,
                )?
                .into()
            }
            (None, Some(_worktree)) => {
                bail!(
                    "Snapshot tree {id} needs a 'HEAD' entry if it has a 'worktree' entry",
                    id = snapshot_tree.id
                )
            }
            (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)) => {

View on GitHub (pinned to 58e5313667)