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

  1. Run `git status` and `git fsck` on the affected repo; re-create the conflicted state if the index is damaged
  2. Discard the bad snapshot and take a fresh one so the index-conflicts entries are rewritten by the current code
  3. Verify no external tool wrote to .git or the snapshot refs
  4. 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

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


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/c41672af2c9bdf3d. Report an issue: GitHub.