gitbutlerapp/gitbutler · error
Invalid conflict stage '{}'
Error message
Invalid conflict stage '{}' What it means
During snapshot restore, conflicted index entries are replayed from recorded paths of the form `<stage>/<path>`, where stage is 1 (base), 2 (ours), or 3 (theirs). This bail fires when the first path component is anything else — the recorded index data does not match the expected layout, i.e. the snapshot's index records are corrupt or were written by an incompatible version.
Source
Thrown at crates/gitbutler-oplog/src/oplog.rs:483
fn restore_index_conflicts(index: &mut gix::index::State, conflict_tree: gix::Id) -> Result<()> {
let conflict_tree = conflict_tree.object()?.try_into_tree()?;
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 path = &record.filepath;
let slash = path
.find_byte(b'/')
.context("BUG: expecting <stage>/<path>")?;
let stage = match &path[..slash] {
b"1" => Stage::Base,
b"2" => Stage::Ours,
b"3" => Stage::Theirs,
stage => bail!("Invalid conflict stage '{}'", stage.as_bstr()),
};
let path = path[slash + 1..].as_bstr();
index.dangerously_push_entry(
Default::default(),
record.oid,
Flags::from_stage(stage),
record.mode.into(),
path,
);
to_remove.insert(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
- Restore from an earlier snapshot taken before the conflicted state
- If the conflict is reproducible, redo the merge and create a fresh snapshot
- Report the snapshot id as a bug — restore should not encounter foreign index layouts
Defensive patterns
Strategy: validation
Validate before calling
// while scanning recorded index entries before restoring
if !matches!(&path[..slash], b"1" | b"2" | b"3") {
return Err(anyhow::anyhow!("snapshot index entry has unknown stage; refusing"));
} Type guard
fn is_valid_conflict_stage(prefix: &[u8]) -> bool {
matches!(prefix, b"1" | b"2" | b"3")
} Try / catch
match oplog.restore(snapshot_id) {
Err(err) if err.to_string().contains("Invalid conflict stage") => { /* fall back to an earlier snapshot */ }
other => other,
} Prevention
- Never modify oplog snapshot trees
- Pin one GitButler version when creating and restoring snapshots of conflicted states
When it happens
Trigger: Restoring a snapshot whose index records contain paths whose stage prefix is not `1/`, `2/`, or `3/` (a path with no `/` at all fails earlier with the BUG context error).
Common situations: Snapshot format drift between GitButler versions; partially written snapshot trees; externally manipulated oplog data.
Related errors
- targetRef in project_meta.toml is not a remote-tracking bran
- targetCommitId in project_meta.toml is null
- snapshot checkout ref is not a local branch
- snapshot checkout commit is null
- commit id mismatch: failed to recreate a commit from its par
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/f17a8a4b2e79b7cf.
Report an issue: GitHub.