gitbutlerapp/gitbutler · critical
Missing stack '{stack_id}' for head '{name}'
Error message
Missing stack '{stack_id}' for head '{name}' What it means
Thrown during legacy metadata migration in crates/but-meta/src/legacy/storage.rs: while converting the old virtual_branches.json heads list, a head entry references a stack_id that does not exist in the stacks map built from the same file. The migration refuses to attach the head to a nonexistent stack, which means the persisted legacy state is internally inconsistent (corruption or a hand-edited file).
Source
Thrown at crates/but-meta/src/legacy/storage.rs:511
},
);
}
for head in heads {
let VbStackHead {
stack_id,
position: _, // previously set based on vec position
name,
head_sha,
pr_number,
archived,
review_id,
} = head;
let stack_id = StackId::from_str(stack_id)
.with_context(|| format!("Invalid stack id '{stack_id}'"))?;
let stack = branches
.get_mut(&stack_id)
.ok_or_else(|| anyhow!("Missing stack '{stack_id}' for head '{name}'"))?;
stack.heads.push(StackBranch {
head: gix::ObjectId::from_str(head_sha)
.with_context(|| format!("Invalid head sha '{head_sha}' on '{name}'"))?,
name: name.clone(),
pr_number: pr_number
.map(usize::try_from)
.transpose()
.with_context(|| {
format!(
"Invalid pr_number '{}' on stack '{stack_id}'",
pr_number.unwrap_or_default(),
)
})?,
archived: *archived,
review_id: review_id.clone(),
});
}
View on GitHub (pinned to caf1f223d3)
Solutions
- Restore the workspace metadata from backup (the file is versioned; check history or the automatic snapshots) and retry.
- If no backup exists, archive the repo's .gitbutler metadata aside and let GitButler reinitialize the workspace, then recreate virtual branches.
- Inspect virtual_branches.json: cross-check every heads[].stack_id against the stacks[].id values to locate the orphan.
- Report the file (sanitized) to GitButler — an internally inconsistent file usually indicates an old writer bug worth tracking.
Defensive patterns
Strategy: validation
Validate before calling
// Before migrating legacy metadata, cross-check head -> stack references
use std::collections::HashSet;
fn heads_reference_existing_stacks(heads: &[LegacyHead], stacks: &[LegacyStack]) -> bool {
let ids: HashSet<String> = stacks.iter().map(|s| s.stack_id.clone()).collect();
heads.iter().all(|h| ids.contains(&h.stack_id))
} Type guard
fn legacy_metadata_is_consistent(
heads: &[HeadEntry],
stack_ids: &std::collections::HashSet<String>,
) -> bool {
heads.iter().all(|h| stack_ids.contains(&h.stack_id))
} Try / catch
match migrate_legacy_storage(&vb_json) {
Ok(v2) => Ok(v2),
Err(e) if e.to_string().contains("Missing stack") => {
// quarantine the file, restore from backup/snapshot, then retry
Err(e)
},
Err(e) => Err(e),
} Prevention
- Validate head->stack referential integrity before running the legacy migration.
- Keep metadata backups/snapshots so an inconsistent file can be restored instead of hand-repaired.
- Never hand-edit virtual_branches.json; let the app rewrite it atomically.
When it happens
Trigger: Loading a legacy virtual_branches.json whose heads array contains a head with stack_id absent from the stacks section; truncated or hand-edited metadata; files written by a buggy older version that desynchronized stacks and heads.
Common situations: Users upgrading from very old GitButler versions with damaged workspace metadata; manually edited or merged .gitbutler metadata files; a crash during an old metadata write leaving stacks and heads out of sync.
Related errors
- but_meta::Stack::derived_name: Stack is uninitialized
- ownership ranges cannot be empty
- No uncommitted changes assigned to branch: {branch_name}
- Unable to determine target commit for unassigned change: {}
- Cannot unapply anonymous segments yet even if they have a st
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/c185c2f484d7161d.
Report an issue: GitHub.