Suggested priority: P1 — data integrity.
Moving a session onto a clean destination checkout can reset away that checkout's unpublished commits. The current protection checks only uncommitted changes.
Reproduced failure
Using the production diri_engine::migrate::prepare against a disposable bare origin and two local clones:
- Both clones start on the same
main baseline.
- In the destination, commit a new
destination-only.txt without pushing it. Its working tree is clean.
- Prepare migration from the source to the destination.
The call returned Ok(Prepared { ... }). The destination HEAD changed from its unpublished commit to the source/origin baseline, and destination-only.txt disappeared from the checkout. An assertion that the destination commit remain preserved failed.
This removes the commit from the branch; the object may still be recoverable through Git's reflog. It is not a claim of immediate permanent object deletion.
Product path and cause
- The sidebar exposes Move to Local / Move to host, which reaches session_migrate.
- prepare checks
git status --porcelain, then fetches and runs git checkout -B <branch> origin/<branch>.
- A clean working tree says nothing about commits ahead of or diverged from origin. There is no destination-history ancestry check or durable recovery ref before resetting the branch.
- ensure_target_worktree also uses
checkout -B for existing worktrees and worktree add -B for new ones, so all branch-reset paths need the same protection.
This is particularly dangerous on Move to Local, where an ordinary local checkout may contain committed work that has never been pushed.
Regression fixture
In the existing migrate.rs test module, using its seeded_repos helper:
let temp = tempfile::tempdir().unwrap();
let (source, target) = seeded_repos(temp.path());
std::fs::write(target.join("destination-only.txt"), "valuable work\n").unwrap();
git(&target, &["add", "."]);
git(&target, &["commit", "-q", "-m", "unpushed destination work"]);
let before = git_out(&target, &["rev-parse", "HEAD"]);
assert!(git_out(&target, &["status", "--porcelain"]).is_empty());
let result = prepare(
source.to_str().unwrap(), None, None,
target.to_str().unwrap(), "target",
);
assert_eq!(git_out(&target, &["rev-parse", "HEAD"]), before);
assert!(result.is_err(), "unsafe destination must be rejected");
Expected behavior
Do not overwrite unrelated destination commits as a side effect of moving a session. Verify destination history before mutation and refuse an unsafe reset, preserve it explicitly, or select a separate checkout. Distinguish Diri-owned recovery snapshots used by legitimate round trips from unrelated unpublished commits. Cover ahead, diverged, and existing off-checkout branch cases.
Validation: reproduced on main 1196d89 by a separate Cargo harness depending on the unchanged production Engine crate. All Git activity used disposable local repositories; no real remote host, user checkout, or product source file was modified.
Suggested priority: P1 — data integrity.
Moving a session onto a clean destination checkout can reset away that checkout's unpublished commits. The current protection checks only uncommitted changes.
Reproduced failure
Using the production
diri_engine::migrate::prepareagainst a disposable bare origin and two local clones:mainbaseline.destination-only.txtwithout pushing it. Its working tree is clean.The call returned
Ok(Prepared { ... }). The destination HEAD changed from its unpublished commit to the source/origin baseline, anddestination-only.txtdisappeared from the checkout. An assertion that the destination commit remain preserved failed.This removes the commit from the branch; the object may still be recoverable through Git's reflog. It is not a claim of immediate permanent object deletion.
Product path and cause
git status --porcelain, then fetches and runsgit checkout -B <branch> origin/<branch>.checkout -Bfor existing worktrees andworktree add -Bfor new ones, so all branch-reset paths need the same protection.This is particularly dangerous on Move to Local, where an ordinary local checkout may contain committed work that has never been pushed.
Regression fixture
In the existing
migrate.rstest module, using itsseeded_reposhelper:Expected behavior
Do not overwrite unrelated destination commits as a side effect of moving a session. Verify destination history before mutation and refuse an unsafe reset, preserve it explicitly, or select a separate checkout. Distinguish Diri-owned recovery snapshots used by legitimate round trips from unrelated unpublished commits. Cover ahead, diverged, and existing off-checkout branch cases.
Validation: reproduced on main
1196d89by a separate Cargo harness depending on the unchanged production Engine crate. All Git activity used disposable local repositories; no real remote host, user checkout, or product source file was modified.