{"record":{"id":"72aebf4185dbda43","repo":"jdx/mise","slug":"origin-changed-while-preparing-publication-reconc","errorCode":null,"errorMessage":"origin changed while preparing publication; reconcile again","messagePattern":"origin changed while preparing publication; reconcile again","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/history/sync/publish.rs","lineNumber":24,"sourceCode":"use super::graph::Heads;\nuse super::network::{PushOutcome, Remote};\nuse crate::system::history::shadow::HistoryRepo;\n\n/// Reconciliation and complete live application must precede publication.\n/// A merge that still changes this machine's saved tree needs another pull.\npub(crate) fn build(\n    repo: &HistoryRepo,\n    upstream: Option<&str>,\n    expected_local: Option<&str>,\n    accepted: &BTreeSet<String>,\n    allow_plaintext_history: bool,\n) -> Result<Option<String>> {\n    let heads = Heads::read(repo)?;\n    if heads.local.as_deref() != expected_local {\n        bail!(\"local saved history changed while planning; reconcile again\");\n    }\n    if heads.remote.as_deref() != upstream {\n        bail!(\"origin changed while preparing publication; reconcile again\");\n    }\n    let Some(local) = &heads.local else {\n        return Ok(None);\n    };\n    if heads.remote.as_ref() == Some(local) {\n        return Ok(None);\n    }\n    let tree = repo.output_tree_of(local)?;\n    if let Some(remote) = &heads.remote\n        && heads.base.as_ref() != Some(remote)\n    {\n        let (mut merged, mut conflicts) = repo.merge_tree(local, remote)?;\n        // Enrollment is a keyed inventory, not arbitrary JSON text. Git's\n        // line merge can conflict on independent additions or combine policy\n        // changes into invalid metadata. Validate its structured merge first.\n        if let Some(base) = &heads.base {\n            use crate::system::history::manifest::Manifest;\n            if let (Some(base), Some(ours), Some(theirs)) = (","sourceCodeStart":6,"sourceCodeEnd":42,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/system/history/sync/publish.rs#L6-L42","documentation":"During history-sync publication, the locally saved sync state (Heads) is re-read and compared against the local and remote heads that were captured when publication was planned. If the saved origin head no longer matches what was planned, mise refuses to publish so it never pushes on top of stale assumptions. This prevents racing another sync/pull process that changed the saved history between planning and publishing.","triggerScenarios":"Calling the dot sync/publish path when the saved remote head in the state store was updated between plan and publish time — e.g. another `mise dot sync` or `mise dot pull` ran concurrently and changed heads.remote, or the user reconnected/changed the origin between the two steps.","commonSituations":"Two terminals or an editor plugin and a scheduled sync job both running `mise dot sync`; running publish right after changing origin with `mise dot origin set`; stale long-running publish that overlaps a pull.","solutions":["Re-run `mise dot sync` so planning and publication happen in one pass with fresh heads.","Check for and finish any other running `mise dot sync`/`mise dot pull` process before retrying.","If the origin really changed intentionally, run `mise dot pull` first to reconcile with the new origin state."],"exampleFix":"// before: plan and publish as separate long-lived steps with stale heads\nlet plan = plan_publication(&repo, &heads)?;\n// ...long work...\npublish(&repo, &plan, &stale_expected_local, &stale_upstream)?;\n// after: re-read heads immediately before publishing, or retry the whole sync\nlet heads = Heads::read(repo)?;\nif heads.remote.as_deref() != expected_upstream {\n    return sync::run(store, tracked); // reconcile again with fresh state\n}","handlingStrategy":"retry","validationCode":"// re-read and compare heads before publishing\nlet heads = Heads::read(repo)?;\nif heads.remote.as_deref() != expected_upstream || heads.local.as_deref() != expected_local {\n    eprintln!(\"state changed; rerun sync\");\n}","typeGuard":"fn heads_unchanged(heads: &Heads, expected_local: Option<&str>, upstream: Option<&str>) -> bool {\n    heads.local.as_deref() == expected_local && heads.remote.as_deref() == upstream\n}","tryCatchPattern":"match run_sync() {\n    Err(e) if e.to_string().contains(\"reconcile again\") => retry_sync_with_fresh_heads(),\n    Err(e) => return Err(e),\n    Ok(v) => Ok(v),\n}","preventionTips":["Run sync/publish operations serially — never from two terminals at once.","Keep plan and publish within one command invocation; avoid long gaps between them.","Re-read Heads immediately before publishing instead of caching them.","Avoid changing origin (`mise dot origin set`) while a sync is in flight."],"tags":["git","sync","concurrency","state-conflict"],"backgroundTag":"invalid-state-transition","analyzedSha":"533346cc374382b41ec5ff70536252b2e96e725c","analyzedAt":"2026-09-17T13:35:38.149Z","contentChangedAt":"2026-09-17T13:35:38.149Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}