{"record":{"id":"5694aa854effc154","repo":"gitbutlerapp/gitbutler","slug":"gitnonfastforward","errorCode":"GitNonFastForward","errorMessage":"Err(anyhow::Error::new(err).context(but_error::Code::GitNonFastForward))","messagePattern":"Err\\(anyhow::Error::new\\(err\\)\\.context\\(but_error::Code::GitNonFastForward\\)\\)","errorType":"error_code","errorClass":"anyhow::Error","httpStatus":null,"severity":"warning","filePath":"crates/but-api/src/land/deliver.rs","lineNumber":62,"sourceCode":"            head_ref.try_into()?,\n            new_target_oid,\n            expected.clone(),\n            ref_log_message,\n        ),\n        RefEdit::update(\n            tracking_ref.try_into()?,\n            new_target_oid,\n            expected,\n            ref_log_message,\n        ),\n    ];\n\n    match repo.edit_references(edits) {\n        Ok(_) => Ok(()),\n        // A concurrent move broke the compare-and-swap — the same retryable signal as a\n        // non-fast-forward push, so the retry loop re-fetches and tries again.\n        Err(err) if is_ref_out_of_date(&err) => {\n            Err(anyhow::Error::new(err).context(but_error::Code::GitNonFastForward))\n        }\n        Err(err) => Err(err.into()),\n    }\n}\n\n/// Whether a `repo.edit_references` failure was a compare-and-swap mismatch (the expected previous\n/// value didn't match), i.e. a ref moved concurrently — as opposed to an I/O or lock failure.\nfn is_ref_out_of_date(err: &gix::reference::edit::Error) -> bool {\n    matches!(\n        err,\n        gix::reference::edit::Error::FileTransactionPrepare(\n            gix::refs::file::transaction::prepare::Error::ReferenceOutOfDate { .. }\n        )\n    )\n}\n\n/// Real-remote path: push the landed commit onto the target branch. Fast-forward by default\n/// (no force); a non-fast-forward rejection surfaces as `Code::GitNonFastForward` for the retry.","sourceCodeStart":44,"sourceCodeEnd":80,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/58e5313667b857ef39a730e380af31816a7b1768/crates/but-api/src/land/deliver.rs#L44-L80","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (racy single attempt)\nrepo.edit_references(edits)?;\n// after (retry on CAS mismatch)\nfor _ in 0..3 {\n    match repo.edit_references(&edits) {\n        Ok(_) => break,\n        Err(e) if is_ref_out_of_date(&e) => { repo.fetch()?; continue; }\n        Err(e) => return Err(e.into()),\n    }\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":"fn is_ref_out_of_date(err: &anyhow::Error) -> bool {\n    err.chain().any(|c| c.to_string().contains(\"reference to old value\") || c.to_string().contains(\"non-fast-forward\"))\n}","tryCatchPattern":"for _ in 0..3 {\n    match update_local_target_refs(&repo, &edits) {\n        Ok(()) => break,\n        Err(e) if e.downcast_ref::<Code>() == Some(&Code::GitNonFastForward) => { repo.fetch()?; continue; }\n        Err(e) => return Err(e),\n    }\n}","preventionTips":["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."],"tags":["git","concurrency","retry","refs"],"backgroundTag":"git-non-fast-forward","analyzedSha":"58e5313667b857ef39a730e380af31816a7b1768","analyzedAt":"2026-09-18T06:50:32.052Z","contentChangedAt":"2026-09-18T06:50:32.052Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}