GitoxideLabs/gitoxide · error

BUG: cannot call commit() before prepare(…)

Error message

BUG: cannot call commit() before prepare(…)

What it means

Panic raised when `packed::Transaction::commit()` is called without a preceding `prepare(...)`. `self.edits` is an `Option` populated only by `prepare`, so `expect` proves the caller skipped the required phase. This is a deliberate API-misuse guard, analogous to the loose-ref transaction guard.

Solutions

  1. Call `transaction.prepare(...)` (with a lock context) before `transaction.commit()`.
  2. Ensure the prepare step's Result is checked — a failed prepare must abort, not fall through to commit.
  3. Restructure so prepare and commit are always paired in the same code path.

Example fix

// before
let tx = store.packed_transaction(lock, gix_ref::transaction::PackedRefs::DeletionsAndNonSymbolicUpdates)?;
tx.commit()?;
// after
let mut tx = store.packed_transaction(lock, gix_ref::transaction::PackedRefs::DeletionsAndNonSymbolicUpdates)?;
tx.prepare(edits, &objects, forward_or_reverse)?;
tx.commit()?;
Defensive patterns

Strategy: type-guard

Validate before calling

// always prepare a packed transaction before committing
let mut tx = store.packed_transaction(lock, packed_refs_opt)?;
tx.prepare(edits, &objects, /*move_to_top*/ true)?;
tx.commit()?;

Type guard

fn commit_packed(mut tx: gix_ref::transaction::PackedTransaction, edits: Vec<RefEdit>, objects: &dyn gix_object::Find) -> Result<(), gix_ref::store::packed::transaction::commit::Error> {
    tx.prepare(edits, objects, true)?;
    tx.commit()
}

Try / catch

// panics are not Result errors; enforce phase order by construction
let mut tx = ...;
if !tx.is_prepared() { tx.prepare(edits, &objects, true)?; }
tx.commit()?;

Prevention

When it happens

Trigger: Creating a `gix_ref::store::packed::Transaction` via `Store::packed_transaction()` or `File::transaction()`, then calling `.commit()` directly instead of `.prepare(...)` first.

Common situations: Code that conditionally prepares but unconditionally commits; refactors moving prepare into another function; misunderstandings of the two-phase packed-refs API.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08). Data as JSON: /api/errors/726682314199c2c2. Report an issue: GitHub.

Appendix: source

Thrown at gix-ref/src/store/packed/transaction.rs:158

                .take()
                .map(gix_lock::File::close)
                .transpose()
                .map_err(prepare::Error::CloseLock)?;
        } else {
            // NOTE that we don't do any additional checks here but apply all edits unconditionally.
            // This is because this transaction system is internal and will be used correctly from the
            // loose ref store transactions, which do the necessary checking.
        }
        self.edits = Some(edits);
        Ok(self)
    }

    /// Commit the prepared transaction.
    ///
    /// Please note that actual edits invalidated existing packed buffers.
    /// Note: There is the potential to write changes into memory and return such a packed-refs buffer for reuse.
    pub fn commit(self) -> Result<(), commit::Error> {
        let mut edits = self.edits.expect("BUG: cannot call commit() before prepare(…)");
        if edits.is_empty() {
            return Ok(());
        }

        let mut file = self.lock.expect("a write lock for applying changes");
        let refs_sorted: Box<dyn Iterator<Item = Result<packed::Reference<'_>, packed::iter::Error>>> =
            match self.buffer.as_ref() {
                Some(buffer) => Box::new(buffer.iter()?),
                None => Box::new(std::iter::empty()),
            };

        let mut refs_sorted = refs_sorted.peekable();

        edits.sort_by(|l, r| l.inner.name.as_bstr().cmp(r.inner.name.as_bstr()));
        let mut peekable_sorted_edits = edits.iter().peekable();

        file.with_mut(|f| f.write_all(HEADER_LINE))?;

View on GitHub (pinned to e73179060b)