{"record":{"id":"4f2cf916e2715015","repo":"gitbutlerapp/gitbutler","slug":"read-only-metadata-can-t-prune-branch-stack-order","errorCode":null,"errorMessage":"Read-only metadata can't prune branch stack order references","messagePattern":"Read-only metadata can't prune branch stack order references","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/but-meta/src/legacy/mod.rs","lineNumber":895,"sourceCode":"        if self.read_only {\n            bail!(\"Read-only metadata can't rename branch stack order references\");\n        }\n        let Some(db) = self.db.as_mut() else {\n            return Ok(());\n        };\n        db.branch_order_mut()?.rename_reference(\n            old_ref_name.as_bstr().to_str()?,\n            new_ref_name.as_bstr().to_str()?,\n        )?;\n        Ok(())\n    }\n\n    fn remove_missing_branch_stack_order_references(\n        &mut self,\n        existing_ref_names: &[FullName],\n    ) -> anyhow::Result<()> {\n        if self.read_only {\n            bail!(\"Read-only metadata can't prune branch stack order references\");\n        }\n        let Some(db) = self.db.as_mut() else {\n            return Ok(());\n        };\n        let existing_ref_names = existing_ref_names\n            .iter()\n            .map(|ref_name| ref_name.as_bstr().to_str().map(ToOwned::to_owned))\n            .collect::<Result<Vec<_>, _>>()?;\n        db.branch_order_mut()?\n            .remove_missing_references(&existing_ref_names)?;\n        Ok(())\n    }\n\n    fn remove(&mut self, ref_name: &FullNameRef) -> anyhow::Result<bool> {\n        let removed_branch_order =\n            !self.read_only && self.db.is_some() && self.branch_stack_order(ref_name)?.is_some();\n        if !self.read_only\n            && let Some(db) = self.db.as_mut()","sourceCodeStart":877,"sourceCodeEnd":913,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-meta/src/legacy/mod.rs#L877-L913","documentation":"Thrown by the legacy GitButler metadata layer when branch stack order entries would be pruned on a handle that was opened in read-only mode. The legacy store pairs a TOML file with a SQLite branch-order database; `remove_missing_branch_stack_order_references` is a write, so a read-only handle refuses it instead of mutating the DB or touching the write-refresh sentinel.","triggerScenarios":"A legacy metadata handle constructed with the read-only flag (observer/sync paths that must not signal writes) flows into an API that syncs branch stack order against `existing_ref_names`, which internally calls the prune step and hits the `if self.read_only` guard.","commonSituations":"Background watchers or external tooling open metadata read-only to avoid out-of-process write signaling, then reuse that handle on a code path designed for writable handles; refactoring a sync flow so it now prunes stale entries.","solutions":["Open the metadata handle in writable mode for any code path that prunes branch-order entries","Gate the call: skip `remove_missing_branch_stack_order_references` when the handle is read-only","Keep read-only handles strictly on read/observe code paths; move the prune step to the writable flow"],"exampleFix":"// before: read-only handle flows into a prune-capable sync\nlet handle = open_legacy_metadata(&path, /* read_only */ true);\nhandle.sync_branch_order(&existing_ref_names)?; // bails: read-only can't prune\n\n// after: prune only when writable\nif !handle.is_read_only() {\n    handle.sync_branch_order(&existing_ref_names)?;\n}","handlingStrategy":"validation","validationCode":"// Rust: only route prune-capable syncs through a writable handle\nif handle.is_read_only() {\n    // observe-only: skip pruning missing branch-order references\n    return Ok(());\n}\nhandle.sync_branch_order(&existing_ref_names)?;","typeGuard":null,"tryCatchPattern":"Catch the anyhow error from the sync call; if the message starts with 'Read-only metadata', treat it as a control-flow signal — reopen the store writable and retry once, or skip pruning. Do not surface it to the user as a crash.","preventionTips":["Never let observer-mode (read-only) handles flow into prune or rename APIs","Audit call sites when adding new sync steps that write, and gate them on handle mode"],"tags":["gitbutler","metadata","read-only","sqlite","branch-order"],"backgroundTag":"read-only-store-write","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}