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

  1. Fix the snapshot writer to never store stage 0 in the index-conflicts tree
  2. Remove or correct the offending '<path>/0' entry and rebuild the snapshot
  3. 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

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


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)