{"record":{"id":"13d770957ef28d1a","repo":"jdx/mise","slug":"origin-changed-while-applying-this-setup-reconcil","errorCode":null,"errorMessage":"origin changed while applying this setup; reconcile again before publication","messagePattern":"origin changed while applying this setup; reconcile again before publication","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/history/sync/graph.rs","lineNumber":90,"sourceCode":"                {\n                    remote.clone()\n                } else {\n                    repo.commit_tree(tree, vec![local, remote], \"merge origin dotfiles\")?\n                }\n            }\n        };\n        Ok(Some(Candidate {\n            commit,\n            expected_local: self.local.clone(),\n            expected_remote: self.remote.clone(),\n        }))\n    }\n}\n\nimpl Candidate {\n    pub(crate) fn adopt(&self, repo: &HistoryRepo) -> Result<()> {\n        if repo.ref_oid(UPSTREAM_REF)? != self.expected_remote {\n            bail!(\"origin changed while applying this setup; reconcile again before publication\");\n        }\n        // Compare-and-swap also verifies no local boundary/save was inserted\n        // during application. The caller must recompute instead of dropping it.\n        repo.update_history_head(&self.commit, self.expected_local.as_deref())\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn fresh_adoption_keeps_remote_identity_and_rejects_a_changed_fetch() {\n        let temporary = tempfile::tempdir().unwrap();\n        let repo = HistoryRepo::open_or_init_in(temporary.path())\n            .unwrap()\n            .unwrap();\n        let incoming_tree = tree(&repo, b\"incoming\");","sourceCodeStart":72,"sourceCodeEnd":108,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/system/history/sync/graph.rs#L72-L108","documentation":"`Candidate::adopt` in src/system/history/sync/graph.rs performs a compare-and-swap when applying a reconciliation plan: before advancing the history head it re-reads the upstream ref (`refs/remotes/origin/setup`) and verifies it still points at the OID the candidate was built against. If the origin moved between planning and adoption, the library refuses to apply, because the plan's merge or fast-forward may no longer be correct. This is an optimistic-concurrency guard; the caller must fetch and recompute instead of dropping or forcing the plan.","triggerScenarios":"Calling `candidate.adopt(&repo)` when `repo.ref_oid(UPSTREAM_REF)` no longer equals `candidate.expected_remote` — i.e. a fetch updated `refs/remotes/origin/setup` (someone else pushed to the origin setup branch) after `Heads::candidate()` created this candidate but before `adopt()` ran. `repo.update_history_head` can also fail its own compare-and-swap if a local save was inserted in the same window.","commonSituations":"A teammate pushed new setup commits to the shared origin while you were applying a setup; the periodic background fetch landed mid-apply; a long conflict-resolution session allowed the remote to advance; two machines syncing the same setup repository simultaneously.","solutions":["Fetch the origin again and rebuild the plan: run `Heads::read`, recompute the candidate, and `adopt()` the fresh candidate — the message says to 'reconcile again before publication'.","Minimize the window between `candidate()` and `adopt()`; re-plan immediately after resolving conflicts.","Coordinate with the other machine/user pushing to the setup branch, or push promptly after applying to reduce divergence.","Do not force-update the history head or the origin branch; forced publication is not supported and would bypass this guard."],"exampleFix":"// before: adopt a candidate built long ago\ncandidate.adopt(&repo)?; // origin may have advanced meanwhile\n\n// after: re-plan and adopt atomically-ish\nlet heads = Heads::read(&repo)?;\nif let Some(fresh) = heads.candidate(&repo, &tree)? {\n    fresh.adopt(&repo)?;\n}","handlingStrategy":"retry","validationCode":"// verify the origin ref still matches what the plan expects, before adopting\nif repo.ref_oid(UPSTREAM_REF)? != candidate.expected_remote {\n    // fetch and rebuild the plan instead of adopting\n}","typeGuard":null,"tryCatchPattern":"// Rust\nmatch candidate.adopt(&repo) {\n    Ok(()) => {/* published */},\n    Err(e) if e.to_string().contains(\"origin changed\") => {\n        fetch_origin()?;\n        let heads = Heads::read(&repo)?;\n        let fresh = heads.candidate(&repo, &tree)?;\n        if let Some(fresh) = fresh { fresh.adopt(&repo)?; }\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Adopt immediately after building the candidate; don't hold plans across user-interactive conflict resolution","Pause periodic background fetches while applying, or re-fetch right before adopting","Coordinate pushes to the shared setup branch (push promptly, keep sessions short)","Never force-update the origin branch; always reconcile via a fresh plan"],"tags":["git","race-condition","concurrency","compare-and-swap"],"backgroundTag":"invalid-state-transition","analyzedSha":"afd2eddd3a50c16190efc1c7e94404b48f72af57","analyzedAt":"2026-09-09T01:38:25.179Z","contentChangedAt":"2026-09-09T01:38:25.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}