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

  1. Open the metadata handle in writable mode for any code path that prunes branch-order entries
  2. Gate the call: skip `remove_missing_branch_stack_order_references` when the handle is read-only
  3. 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

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


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