GitoxideLabs/gitoxide · error

BUG: must call prepare before commit

Error message

BUG: must call prepare before commit

What it means

Panic raised when `file::Transaction::commit()` is called on a reference transaction that was never `prepare()`d. `commit_inner` unwraps `self.updates`, which is `Option::take`-cleared/consumed during `prepare`; `None` proves the prepare step was skipped. The API contract requires `prepare()` before `commit()`, though docs note prepare happens automatically in normal flows.

Solutions

  1. Call `transaction.prepare(edits_or_packed_refs)` before `transaction.commit(committer)`.
  2. If you don't need two-phase control, use the flow that prepares automatically as documented.
  3. Restructure code so the transaction isn't committed after its prepare state was consumed.

Example fix

// before
let tx = store.transaction();
let edits = tx.commit(Some(committer))?;
// after
let mut tx = store.transaction();
tx.prepare(edit_vec, None, /*start_over*/ false)?;
let edits = tx.commit(Some(committer))?;
Defensive patterns

Strategy: type-guard

Validate before calling

// ensure the transaction is in the prepared state before committing
let tx = store.transaction();
tx.prepare(edits, None, false)?; // prepare is required
tx.commit(committer)?;

Type guard

fn commit_prepared(tx: gix_ref::transaction::FileTransaction, c: Option<gix_actor::SignatureRef<'_>>) -> Result<Vec<gix_ref::gix_fmt::RefEdit>, gix_ref::transaction::commit::Error> {
    // commit() itself panics unless prepare() ran; always pair them
    tx.commit(c)
}

Try / catch

// panics are not catchable results; ensure pairing statically
let mut tx = store.transaction();
tx.prepare(edits, packed_refs, false).map_err(|e| ...)?;
let edits = tx.commit(committer).map_err(|e| ...)?;

Prevention

When it happens

Trigger: Constructing a `gix_ref::store::file::Transaction` (via `Store::transaction()`), then calling `.commit(...)` directly without first calling `.prepare(...)` — only possible when bypassing the auto-prepare flow or holding the transaction across custom update edits.

Common situations: Custom ref-update code that adds edits after prepare, refactors that split prepare/commit incorrectly, or upgrading gix-ref and relying on an old flow that allowed bare commits.

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/186e423f62399b76. Report an issue: GitHub.

Appendix: source

Thrown at gix-ref/src/store/file/transaction/commit.rs:33

    /// On error the transaction may have been performed partially, depending on the nature of the error, and no attempt to roll back
    /// partial changes is made.
    ///
    /// In this stage, we perform the following operations:
    ///
    /// * update the ref log
    /// * move updated refs into place
    /// * delete reflogs and empty parent directories
    /// * delete packed refs
    /// * delete their corresponding reference (if applicable)
    ///   along with empty parent directories
    ///
    /// Note that transactions will be prepared automatically as needed.
    pub fn commit<'a>(self, committer: impl Into<Option<gix_actor::SignatureRef<'a>>>) -> Result<Vec<RefEdit>, Error> {
        self.commit_inner(committer.into())
    }

    fn commit_inner(self, committer: Option<gix_actor::SignatureRef<'_>>) -> Result<Vec<RefEdit>, Error> {
        let mut updates = self.updates.expect("BUG: must call prepare before commit");
        let delete_loose_refs = matches!(
            self.packed_refs,
            PackedRefs::DeletionsAndNonSymbolicUpdatesRemoveLooseSourceReference(_)
        );

        // Perform updates first so live commits remain referenced
        for change in &mut updates {
            assert!(!change.update.deref, "Deref mode is turned into splits and turned off");
            match &change.update.change {
                // reflog first, then reference
                Change::Update { log, new, expected } => {
                    let lock = change.lock.take();
                    let (update_ref, update_reflog) = match log.mode {
                        RefLog::Only => (false, true),
                        RefLog::AndReference => (true, true),
                    };
                    if update_reflog {
                        let log_update = match new {

View on GitHub (pinned to e73179060b)