{"record":{"id":"e1fd6ce9b9a95d10","repo":"jdx/mise","slug":"target-changed-after-unapply-planning","errorCode":null,"errorMessage":"target changed after unapply planning","messagePattern":"target changed after unapply planning","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/edits.rs","lineNumber":968,"sourceCode":"}\n\n/// Ensure template functions or another concurrent actor did not invalidate\n/// any edit ownership checks performed during planning.\npub(crate) fn validate_unapply(todo: &[UnapplyPlan<'_>]) -> Result<()> {\n    let mut checked = indexmap::IndexSet::new();\n    let mut problems = vec![];\n    for plan in todo {\n        if !checked.insert(plan.req.path.clone()) {\n            continue;\n        }\n        let result = if plan.req.path.is_symlink() {\n            Err(eyre::eyre!(\"{SYMLINK_REASON}\"))\n        } else {\n            file::read_to_string(&plan.req.path).and_then(|text| {\n                if text == plan.text {\n                    Ok(())\n                } else {\n                    bail!(\"target changed after unapply planning\")\n                }\n            })\n        };\n        if let Err(err) = result {\n            problems.push(format!(\n                \"  \\\"{}\\\" ({}): {err}\",\n                plan.req.path_raw,\n                plan.req.describe_op()\n            ));\n        }\n    }\n    if !problems.is_empty() {\n        bail!(\n            \"edits: cannot unapply these entries:\\n{}\",\n            problems.join(\"\\n\")\n        );\n    }\n    Ok(())","sourceCodeStart":950,"sourceCodeEnd":986,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/system/edits.rs#L950-L986","documentation":"During unapply, mise first snapshots each target file's text (the plan), then validate_unapply re-reads the file and compares it to the planned text. If the file's content changed between planning and validation — including switching to/from a symlink — mise aborts rather than unapply against stale assumptions.","triggerScenarios":"Calling validate_unapply when file::read_to_string(plan.req.path) returns text different from plan.text, i.e. the file was modified after plan_unapply ran (or the path became a symlink, which is rejected separately with SYMLINK_REASON).","commonSituations":"An editor, formatter, file watcher, or another mise/command run rewrote the dotfile between planning and execution of an unapply; or a sync process (e.g. Dropbox, a dotfile manager) touched the file mid-run.","solutions":["Re-run the unapply command — a fresh plan will pick up the new file contents.","Find what modified the file concurrently (editor autosave, sync daemon, watcher) and pause it.","If the file flipped to a symlink, edit the real file manually instead."],"exampleFix":"# before: file changed mid-run\n$ mise edits unapply\nerror: target changed after unapply planning\n\n# after: retry when nothing else touches the files\n$ mise edits unapply  # fresh plan succeeds","handlingStrategy":"retry","validationCode":"# snapshot and compare to detect concurrent writers\nmd5sum ~/.zshrc; mise edits unapply; md5sum ~/.zshrc","typeGuard":null,"tryCatchPattern":"// retry the whole unapply once with a fresh plan\nmatch run_unapply().await {\n    Err(e) if e.to_string().contains(\"target changed after unapply planning\") => retry_with_fresh_plan().await,\n    other => other,\n}","preventionTips":["Close editors/formatters/ watchers that rewrite dotfiles during mise commands","Pause sync daemons (Dropbox etc.) while unapplying","Re-run the command — each run rebuilds the plan from current contents"],"tags":["rust","dotfiles","race-condition","unapply"],"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"}