gitbutlerapp/gitbutler · warning · anyhow::Error

GitNonFastForward

GitNonFastForward

Error message

Err(anyhow::Error::new(err).context(but_error::Code::GitNonFastForward))

What it means

This error wraps a failed compare-and-swap (CAS) reference edit in `repo.edit_references` with the `GitNonFastForward` code. When another process (e.g. a concurrent fetch, push, or another GitButler operation) moved the ref between the read and the write, the expected previous value no longer matches and the edit is rejected. The code marks it as the same retryable signal as a non-fast-forward push so the caller's retry loop can re-fetch and try again.

Solutions

  1. Retry the operation: the caller's retry loop should re-fetch and re-run the reference edit with the fresh ref value.
  2. Avoid running multiple GitButler/CLI processes concurrently on the same repository.
  3. If it persists, check for external automation (CI scripts, IDE plugins) repeatedly pushing or resetting refs and serialize those operations.

Example fix

// before (racy single attempt)
repo.edit_references(edits)?;
// after (retry on CAS mismatch)
for _ in 0..3 {
    match repo.edit_references(&edits) {
        Ok(_) => break,
        Err(e) if is_ref_out_of_date(&e) => { repo.fetch()?; continue; }
        Err(e) => return Err(e.into()),
    }
}
Defensive patterns

Strategy: retry

Type guard

fn is_ref_out_of_date(err: &anyhow::Error) -> bool {
    err.chain().any(|c| c.to_string().contains("reference to old value") || c.to_string().contains("non-fast-forward"))
}

Try / catch

for _ in 0..3 {
    match update_local_target_refs(&repo, &edits) {
        Ok(()) => break,
        Err(e) if e.downcast_ref::<Code>() == Some(&Code::GitNonFastForward) => { repo.fetch()?; continue; }
        Err(e) => return Err(e),
    }
}

Prevention

When it happens

Trigger: Calling `update_local_target_refs` (which invokes `repo.edit_references`) while another writer concurrently updates the same ref, so the expected old value fails the CAS check and `is_ref_out_of_date(&err)` returns true.

Common situations: Two GitButler operations racing on the same project (e.g. a background fetch loop and a user action); a push or fetch from another process moving refs mid-operation; IDE and CLI clients running against the same repository at the same time.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18). Data as JSON: /api/errors/5694aa854effc154. Report an issue: GitHub.

Appendix: source

Thrown at crates/but-api/src/land/deliver.rs:62

            head_ref.try_into()?,
            new_target_oid,
            expected.clone(),
            ref_log_message,
        ),
        RefEdit::update(
            tracking_ref.try_into()?,
            new_target_oid,
            expected,
            ref_log_message,
        ),
    ];

    match repo.edit_references(edits) {
        Ok(_) => Ok(()),
        // A concurrent move broke the compare-and-swap — the same retryable signal as a
        // non-fast-forward push, so the retry loop re-fetches and tries again.
        Err(err) if is_ref_out_of_date(&err) => {
            Err(anyhow::Error::new(err).context(but_error::Code::GitNonFastForward))
        }
        Err(err) => Err(err.into()),
    }
}

/// Whether a `repo.edit_references` failure was a compare-and-swap mismatch (the expected previous
/// value didn't match), i.e. a ref moved concurrently — as opposed to an I/O or lock failure.
fn is_ref_out_of_date(err: &gix::reference::edit::Error) -> bool {
    matches!(
        err,
        gix::reference::edit::Error::FileTransactionPrepare(
            gix::refs::file::transaction::prepare::Error::ReferenceOutOfDate { .. }
        )
    )
}

/// Real-remote path: push the landed commit onto the target branch. Fast-forward by default
/// (no force); a non-fast-forward rejection surfaces as `Code::GitNonFastForward` for the retry.

View on GitHub (pinned to 58e5313667)