Skip to content

Move session resets clean destination branches and discards unpushed commits #272

Description

@cristicretu

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:

  1. Both clones start on the same main baseline.
  2. In the destination, commit a new destination-only.txt without pushing it. Its working tree is clean.
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions