jdx/mise · error

target changed after unapply planning

Error message

target changed after unapply planning

What it means

Thrown by `validate_unapply` after the unapply plan was built: it re-reads each planned target file and compares its current text with the text captured at planning time (`plan.text`). If they differ, the file changed between planning and validation, so applying the plan could corrupt unintended edits; the error aborts the whole unapply and is aggregated into the problems list.

Solutions

  1. Re-run the unapply command; the newly built plan will match the current file state.
  2. Stop or pause concurrent processes (formatters, watchers, sync tools) that write to the target files while unapplying.
  3. Ensure the path is a regular file, not a symlink, since symlinked targets are not supported for unapply.
  4. Verify no other instance of the tool is operating on the same files concurrently.

Example fix

// before: running unapply while a watcher rewrites config
$ tool edits unapply &
$ formatter --watch  # rewrites file mid-plan
// after: stop the watcher first
$ formatter stop && tool edits unapply
Defensive patterns

Strategy: retry

Validate before calling

let before = fs::read_to_string(&path)?;
// ... plan ...
if fs::read_to_string(&path)? != before {
    eprintln!("{} changed during planning; aborting", path.display());
}

Try / catch

for attempt in 0..3 {
    match tool.unapply() {
        Err(e) if e.to_string().contains("target changed after unapply planning") => continue,
        other => { other?; break; }
    }
}

Prevention

When it happens

Trigger: Calling the unapply flow where the target file's contents change between plan_unapply (which snapshots file text) and validate_unapply (which re-reads it) — e.g. another process writes to the file in that window, or the path is a symlink swapped in between the two steps (symlinked paths bail earlier with SYMLINK_REASON).

Common situations: A file watcher, formatter, or editor auto-saved the file while the unapply command was running; a build tool regenerated config files concurrently; two instances of the tool ran at once on the same machine.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17). Data as JSON: /api/errors/e1fd6ce9b9a95d10. Report an issue: GitHub.

Appendix: source

Thrown at src/system/edits.rs:968

}

/// Ensure template functions or another concurrent actor did not invalidate
/// any edit ownership checks performed during planning.
pub(crate) fn validate_unapply(todo: &[UnapplyPlan<'_>]) -> Result<()> {
    let mut checked = indexmap::IndexSet::new();
    let mut problems = vec![];
    for plan in todo {
        if !checked.insert(plan.req.path.clone()) {
            continue;
        }
        let result = if plan.req.path.is_symlink() {
            Err(eyre::eyre!("{SYMLINK_REASON}"))
        } else {
            file::read_to_string(&plan.req.path).and_then(|text| {
                if text == plan.text {
                    Ok(())
                } else {
                    bail!("target changed after unapply planning")
                }
            })
        };
        if let Err(err) = result {
            problems.push(format!(
                "  \"{}\" ({}): {err}",
                plan.req.path_raw,
                plan.req.describe_op()
            ));
        }
    }
    if !problems.is_empty() {
        bail!(
            "edits: cannot unapply these entries:\n{}",
            problems.join("\n")
        );
    }
    Ok(())

View on GitHub (pinned to 533346cc37)