gitbutlerapp/gitbutler · error · anyhow::Error

cannot mix operations that require `materialize` and `materi

Error message

cannot mix operations that require `materialize` and `materialize_without_checkout`

What it means

Within one transaction, two operations demanded incompatible materialization modes: one requires a full checkout materialize, another requires materialize_without_checkout (or vice versa). The transaction stores a single mode per run — Either adopts the first concrete request, then any conflicting request trips this ensure!.

Source

Thrown at crates/but-transaction/src/lib.rs:969

    {
        let editor = self
            .inner
            .rebase
            .take()
            .expect("rebase is always Some(_)")
            .into_editor();
        let (outcome, materialize_without_checkout, new_rebase) =
            f(editor, &self.inner.commit_mappings)?;

        match (
            self.inner.materialize_without_checkout,
            materialize_without_checkout,
        ) {
            (_, MaterializeWithoutCheckout::Either) => {}
            (MaterializeWithoutCheckout::Either, requested) => {
                self.inner.materialize_without_checkout = requested;
            }
            (demanded, requested) => anyhow::ensure!(
                demanded == requested,
                "cannot mix operations that require `materialize` and `materialize_without_checkout`"
            ),
        }

        self.inner.commit_mappings = CommitMappings(new_rebase.history.commit_mappings());
        self.inner.rebase = Some(new_rebase);
        Ok(outcome)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MaterializeWithoutCheckout {
    Yes,
    No,
    Either,
}

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Split the operations into separate transactions so each uses its own materialization mode
  2. Reorder so all checkout-materializing ops go in one transaction and no-checkout ops in another
  3. If writing a new operation, return MaterializeWithoutCheckout::Either when the op genuinely tolerates both modes

Example fix

// before — one transaction, conflicting modes
tx.move_branch(&src, &tgt)?;   // demands materialize (checkout)
tx.amend_commit(...)?;          // demands materialize_without_checkout → error
// after — split by mode
tx.move_branch(&src, &tgt)?;
tx.commit()?;
let mut tx2 = ...; tx2.amend_commit(...)?; tx2.commit()?;
Defensive patterns

Strategy: validation

Validate before calling

// classify each op's materialization requirement up front and group into transactions
enum Mode { Checkout, NoCheckout }
fn plan(ops: &[Op]) -> Vec<Vec<Op>> { /* group ops by Mode; one transaction per group */ }
// never submit ops of both Modes to a single transaction

Type guard

fn compatible(current: MaterializeWithoutCheckout, requested: MaterializeWithoutCheckout) -> bool {
    matches!(requested, MaterializeWithoutCheckout::Either)
        || matches!(current, MaterializeWithoutCheckout::Either)
        || current == requested
}

Try / catch

match tx.some_op(...) {
    Err(e) if e.to_string().contains("cannot mix operations") => {
        // commit current transaction, open a new one, replay the op there
    }
    r => r,
}

Prevention

When it happens

Trigger: Calling e.g. a move_branch (which materializes with checkout) and an amend/squash-style operation (which demands no checkout) through the same transaction instance; any sequence where closures return different MaterializeWithoutCheckout values, both concrete and unequal.

Common situations: Batching multiple workspace operations into one transaction for atomicity without checking their materialization requirements; refactoring code that previously ran operations in separate transactions.

Related errors


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