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
- Retry the operation: the caller's retry loop should re-fetch and re-run the reference edit with the fresh ref value.
- Avoid running multiple GitButler/CLI processes concurrently on the same repository.
- 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
- Serialize write operations per repository (single writer) to avoid CAS races.
- Re-read the ref value immediately before each edit.
- Keep retry loops bounded and re-fetch between attempts.
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
- another pre-commit hook is already using the repository…
- Can only delete local branches under refs/heads, got
- Expected ' ' to be a remote tracking branch, but was
- failed to determine remote for non remote-tracking branch
- HEAD does not point to a local branch
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)