gitbutlerapp/gitbutler · error · anyhow::Error
Invalid stage '{other}' for '{rela_path}'
Error message
Invalid stage '{other}' for '{rela_path}' What it means
Thrown while but-core reconstructs a conflicted git index from a snapshot tree. The snapshot's 'index-conflicts' subtree stores each unmerged entry as a record whose path is `<path>/<stage>`, and git only defines stages 1=base, 2=ours, 3=theirs (0 is unconflicted and is rejected just above). This error fires when the trailing number parses as an integer but is 4 or higher - a stage stock git never writes, so the stored snapshot data itself is bad.
Source
Thrown at crates/but-core/src/snapshot/resolve_tree.rs:165
for record in &recorder.records {
if record.mode.is_tree() {
continue;
}
let rela_path = &record.filepath;
let rslash_pos = rela_path
.rfind("/")
.context("BUG: expecting <path>/<stage>")?;
let stage: usize = rela_path[rslash_pos + 1..]
.to_str()?
.parse()
.context("Failed to parse stage that should only be [1,2,3]")?;
let rela_path = rela_path[..rslash_pos].as_bstr();
let stage = match stage {
0 => bail!("Unconflicted stage for '{rela_path}' is unexpected"),
1 => Stage::Base,
2 => Stage::Ours,
3 => Stage::Theirs,
other => bail!("Invalid stage '{other}' for '{rela_path}'"),
};
index.dangerously_push_entry(
Default::default(),
record.oid,
Flags::from_stage(stage),
record.mode.into(),
rela_path,
);
to_remove.insert(rela_path);
}
index.remove_entries(|_idx, path, entry| {
entry.flags.stage() == Stage::Unconflicted && to_remove.contains(path)
});
index.sort_entries();
Ok(())View on GitHub (pinned to caf1f223d3)
Solutions
- Run `git status` and `git fsck` on the affected repo; re-create the conflicted state if the index is damaged
- Discard the bad snapshot and take a fresh one so the index-conflicts entries are rewritten by the current code
- Verify no external tool wrote to .git or the snapshot refs
- If it reproduces from a snapshot created by the same version, report a but-core bug with the snapshot tree id
Defensive patterns
Strategy: try-catch
Validate before calling
let mut recorder = gix::traverse::tree::Recorder::default();
conflict_tree.traverse().depthfirst(&mut recorder)?;
let valid = recorder.records.iter()
.filter(|r| !r.mode.is_tree())
.all(|r| {
r.filepath.rfind('/').is_some_and(|p| {
matches!(r.filepath[p + 1..].to_str(), Some("1" | "2" | "3"))
})
});
if !valid { anyhow::bail!("snapshot conflict records carry invalid stages"); } Type guard
fn is_conflict_stage_suffix(suffix: &str) -> bool {
matches!(suffix, "1" | "2" | "3")
} Try / catch
match resolve_tree(snapshot_tree, target, opts) {
Err(e) if e.to_string().starts_with("Invalid stage") => {
// unrecoverable snapshot corruption: re-take the snapshot, do not retry
re_snapshot_and_retry()?;
}
other => other?,
} Prevention
- Create and resolve snapshots with the same but-core version
- Never hand-edit snapshot objects or .git/index
- Keep external processes from writing to the repo during snapshot operations
When it happens
Trigger: Calling crate::snapshot::resolve_tree on a snapshot tree whose 'index-conflicts' tree contains a record with a numeric suffix >= 4: snapshots produced by a different but-core version with another conflict encoding, hand-crafted or partially written snapshot objects, or an index corrupted by a crash mid-snapshot.
Common situations: Snapshot trees written by older/newer but-core code; repository object database partially corrupted; external tooling writing objects into the snapshot refs; restored backups with damaged trees.
Related errors
- BUG: this must have been deactivated
- infinite loop should find a unique name
- we never return these as the status iteration is configured
- BUG: we use 'matching' so there are no directories
- disabled
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/c41672af2c9bdf3d.
Report an issue: GitHub.