gitbutlerapp/gitbutler · error
Unconflicted stage for
Error message
Unconflicted stage for '{rela_path}' is unexpected What it means
Snapshot trees encode index conflicts as entries under '<path>/<stage>'. Stage 0 means the file is not conflicted (a single stage-0 entry is normal, not a conflict stage), so encountering an encoded stage 0 is an invariant violation and the resolver bails.
Solutions
- Fix the snapshot writer to never store stage 0 in the index-conflicts tree
- Remove or correct the offending '<path>/0' entry and rebuild the snapshot
- Treat stage 0 entries as plain index entries rather than conflicts in the writer
Defensive patterns
Strategy: validation
Validate before calling
// when writing snapshot conflicts, skip unconflicted entries
if entry.stage() != 0 { push_conflict_entry(entry)?; } Prevention
- Only store stages 1-3 in the index-conflicts tree
- Filter stage-0 entries out of conflict snapshots
- Test round-tripping conflicted and unconflicted indexes through snapshots
When it happens
Trigger: Resolving 'index-conflicts' whose tree contains an entry 'rela_path/0', i.e. a path recorded with conflict stage 0.
Common situations: Snapshot written by buggy code that pushed unconflicted entries into the conflicts tree; manual test fixtures using stage 0; corrupted snapshot data.
Understand the failure class
Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.
Related errors
- Invalid stage ' ' for
- Cannot reconcile : branch name ' ' occurs more than once
- Snapshot tree needs a 'HEAD' entry if it has only a…
- stale pre-commit index backup at
- Aborting due to empty branch name
AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18).
Data as JSON: /api/errors/9d566fde59233d42.
Report an issue: GitHub.
Appendix: source
Thrown at crates/but-core/src/snapshot/resolve_tree.rs:161
let mut recorder = gix::traverse::tree::Recorder::default();
conflict_tree.traverse().depthfirst(&mut recorder)?;
let mut to_remove = BTreeSet::new();
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)View on GitHub (pinned to 58e5313667)