{"record":{"id":"f17a8a4b2e79b7cf","repo":"gitbutlerapp/gitbutler","slug":"invalid-conflict-stage","errorCode":null,"errorMessage":"Invalid conflict stage '{}'","messagePattern":"Invalid conflict stage '(.+?)'","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/gitbutler-oplog/src/oplog.rs","lineNumber":483,"sourceCode":"fn restore_index_conflicts(index: &mut gix::index::State, conflict_tree: gix::Id) -> Result<()> {\n    let conflict_tree = conflict_tree.object()?.try_into_tree()?;\n    let mut recorder = gix::traverse::tree::Recorder::default();\n    conflict_tree.traverse().depthfirst(&mut recorder)?;\n\n    let mut to_remove = BTreeSet::new();\n    for record in &recorder.records {\n        if record.mode.is_tree() {\n            continue;\n        }\n        let path = &record.filepath;\n        let slash = path\n            .find_byte(b'/')\n            .context(\"BUG: expecting <stage>/<path>\")?;\n        let stage = match &path[..slash] {\n            b\"1\" => Stage::Base,\n            b\"2\" => Stage::Ours,\n            b\"3\" => Stage::Theirs,\n            stage => bail!(\"Invalid conflict stage '{}'\", stage.as_bstr()),\n        };\n        let path = path[slash + 1..].as_bstr();\n\n        index.dangerously_push_entry(\n            Default::default(),\n            record.oid,\n            Flags::from_stage(stage),\n            record.mode.into(),\n            path,\n        );\n        to_remove.insert(path);\n    }\n    index.remove_entries(|_idx, path, entry| {\n        entry.flags.stage() == Stage::Unconflicted && to_remove.contains(path)\n    });\n    index.sort_entries();\n    Ok(())\n}","sourceCodeStart":465,"sourceCodeEnd":501,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/gitbutler-oplog/src/oplog.rs#L465-L501","documentation":"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.","triggerScenarios":"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).","commonSituations":"Snapshot format drift between GitButler versions; partially written snapshot trees; externally manipulated oplog data.","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"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// while scanning recorded index entries before restoring\nif !matches!(&path[..slash], b\"1\" | b\"2\" | b\"3\") {\n    return Err(anyhow::anyhow!(\"snapshot index entry has unknown stage; refusing\"));\n}","typeGuard":"fn is_valid_conflict_stage(prefix: &[u8]) -> bool {\n    matches!(prefix, b\"1\" | b\"2\" | b\"3\")\n}","tryCatchPattern":"match oplog.restore(snapshot_id) {\n    Err(err) if err.to_string().contains(\"Invalid conflict stage\") => { /* fall back to an earlier snapshot */ }\n    other => other,\n}","preventionTips":["Never modify oplog snapshot trees","Pin one GitButler version when creating and restoring snapshots of conflicted states"],"tags":["rust","gitbutler","oplog","restore","conflicts","index","corruption"],"backgroundTag":"corrupt-snapshot-data","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}