gitbutlerapp/gitbutler · error · anyhow::Error
Read-only metadata can't prune branch stack order references
Error message
Read-only metadata can't prune branch stack order references
What it means
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.
Source
Thrown at crates/but-meta/src/legacy/mod.rs:895
if self.read_only {
bail!("Read-only metadata can't rename branch stack order references");
}
let Some(db) = self.db.as_mut() else {
return Ok(());
};
db.branch_order_mut()?.rename_reference(
old_ref_name.as_bstr().to_str()?,
new_ref_name.as_bstr().to_str()?,
)?;
Ok(())
}
fn remove_missing_branch_stack_order_references(
&mut self,
existing_ref_names: &[FullName],
) -> anyhow::Result<()> {
if self.read_only {
bail!("Read-only metadata can't prune branch stack order references");
}
let Some(db) = self.db.as_mut() else {
return Ok(());
};
let existing_ref_names = existing_ref_names
.iter()
.map(|ref_name| ref_name.as_bstr().to_str().map(ToOwned::to_owned))
.collect::<Result<Vec<_>, _>>()?;
db.branch_order_mut()?
.remove_missing_references(&existing_ref_names)?;
Ok(())
}
fn remove(&mut self, ref_name: &FullNameRef) -> anyhow::Result<bool> {
let removed_branch_order =
!self.read_only && self.db.is_some() && self.branch_stack_order(ref_name)?.is_some();
if !self.read_only
&& let Some(db) = self.db.as_mut()View on GitHub (pinned to caf1f223d3)
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
Example fix
// before: read-only handle flows into a prune-capable sync
let handle = open_legacy_metadata(&path, /* read_only */ true);
handle.sync_branch_order(&existing_ref_names)?; // bails: read-only can't prune
// after: prune only when writable
if !handle.is_read_only() {
handle.sync_branch_order(&existing_ref_names)?;
} Defensive patterns
Strategy: validation
Validate before calling
// Rust: only route prune-capable syncs through a writable handle
if handle.is_read_only() {
// observe-only: skip pruning missing branch-order references
return Ok(());
}
handle.sync_branch_order(&existing_ref_names)?; Try / catch
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.
Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Cannot reorder '{subject_branch_name}' in single-branch mode
- Cannot import TOML into DB: TOML does not exist or is invali
- Cannot unapply branch '{branch}' from an ad-hoc workspace be
- targetCommitId in project_meta.toml is null
- anchor is always present in the order at this point
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/4f2cf916e2715015.
Report an issue: GitHub.