- MovePlan gains moves[] (file move = 1 relocation), left_behind[] for unindexable files that deliberately stay, prune_dirs[] applied after the batch (remove_dir only succeeds when empty, leftovers keep their home) - plan::dir: nesting/target-exists guards, merged per-file rewrites - apply: atomic N-rename batch (per-file git mv), rollback restores every move, rewrite and created dir newest-first - mv_reject accepts dirs; --json adds moved_files/would_move_files only for real dir moves (single-file contract byte-identical); summary shows (N files) and left-behind count - engine rollback/prune tests moved to tests/apply.rs (public API; a mid-batch failure cannot be forced through the CLI), java-rule e2e split into its own binary to keep every file under the 250-line budget
133 lines
4.2 KiB
Rust
133 lines
4.2 KiB
Rust
//! End-to-end tests for the git integration of `mv`: tracked files move
|
|
//! through `git mv` (staged rename, history kept), `--no-git` opts out.
|
|
//! Self-contained: it builds a real git repo from the `typescript/basic`
|
|
//! fixture instead of reusing the fake `.git` marker of `copy_fixture`.
|
|
|
|
use std::fs;
|
|
use std::path::Path;
|
|
use std::process::Command as Git;
|
|
|
|
use assert_cmd::Command;
|
|
use predicates::prelude::*;
|
|
use tempfile::{TempDir, tempdir};
|
|
|
|
/// Copy the fixture into a tempdir, `git init` it and commit everything.
|
|
fn git_fixture(name: &str) -> TempDir {
|
|
let tmp = tempdir().expect("tempdir");
|
|
let src = Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
.join("tests")
|
|
.join("typescript")
|
|
.join(name);
|
|
copy_dir(&src, tmp.path());
|
|
git(tmp.path(), &["init", "-q", "-b", "main", "."]);
|
|
git(tmp.path(), &["config", "user.name", "jmove-test"]);
|
|
git(tmp.path(), &["config", "user.email", "test@test"]);
|
|
git(tmp.path(), &["add", "-A"]);
|
|
git(tmp.path(), &["commit", "-qm", "init"]);
|
|
tmp
|
|
}
|
|
|
|
fn copy_dir(from: &Path, to: &Path) {
|
|
fs::create_dir_all(to).expect("mkdir");
|
|
for entry in fs::read_dir(from).expect("readdir") {
|
|
let entry = entry.expect("entry");
|
|
let target = to.join(entry.file_name());
|
|
if entry.file_type().expect("filetype").is_dir() {
|
|
copy_dir(&entry.path(), &target);
|
|
} else {
|
|
fs::copy(entry.path(), target).expect("copy");
|
|
}
|
|
}
|
|
}
|
|
|
|
fn jmove(root: &TempDir, args: &[&str]) -> assert_cmd::assert::Assert {
|
|
let mut cmd = Command::cargo_bin("jmove").expect("jmove binary");
|
|
cmd.arg("--root").arg(root.path()).args(args);
|
|
cmd.assert()
|
|
}
|
|
|
|
fn git(root: &Path, args: &[&str]) {
|
|
let status = Git::new("git")
|
|
.arg("-C")
|
|
.arg(root)
|
|
.args(args)
|
|
.status()
|
|
.expect("git");
|
|
assert!(status.success(), "`git {args:?}` failed");
|
|
}
|
|
|
|
fn git_output(root: &Path, args: &[&str]) -> String {
|
|
let out = Git::new("git")
|
|
.arg("-C")
|
|
.arg(root)
|
|
.args(args)
|
|
.output()
|
|
.expect("git");
|
|
assert!(out.status.success(), "`git {args:?}` failed");
|
|
String::from_utf8(out.stdout).expect("utf-8")
|
|
}
|
|
|
|
#[test]
|
|
fn mv_in_a_git_repo_moves_via_git_mv_and_stages_it() {
|
|
let tmp = git_fixture("basic");
|
|
jmove(&tmp, &["mv", "lib/sum.ts", "utils/sum.ts", "--json"])
|
|
.success()
|
|
.stdout(predicate::str::contains("\"moved_via\": \"git\""));
|
|
let staged = git_output(tmp.path(), &["diff", "--cached", "-M", "--name-status"]);
|
|
assert!(
|
|
staged.contains("lib/sum.ts") && staged.contains("utils/sum.ts"),
|
|
"rename must be staged: {staged}"
|
|
);
|
|
jmove(&tmp, &["check"]).success();
|
|
}
|
|
|
|
#[test]
|
|
fn mv_dry_run_reports_would_use_git() {
|
|
let tmp = git_fixture("basic");
|
|
jmove(
|
|
&tmp,
|
|
&["mv", "lib/sum.ts", "utils/sum.ts", "--dry-run", "--json"],
|
|
)
|
|
.success()
|
|
.stdout(predicate::str::contains("\"would_move_via\": \"git\""));
|
|
assert!(
|
|
tmp.path().join("lib/sum.ts").is_file(),
|
|
"dry-run writes nothing"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mv_no_git_keeps_the_plain_rename_unstaged() {
|
|
let tmp = git_fixture("basic");
|
|
jmove(
|
|
&tmp,
|
|
&["mv", "lib/sum.ts", "utils/sum.ts", "--no-git", "--json"],
|
|
)
|
|
.success()
|
|
.stdout(predicate::str::contains("\"moved_via\": \"fs\""));
|
|
let staged = git_output(tmp.path(), &["diff", "--cached", "--name-only"]);
|
|
assert!(
|
|
staged.trim().is_empty(),
|
|
"--no-git must not stage: {staged}"
|
|
);
|
|
let target = git_output(tmp.path(), &["status", "--porcelain", "--", "utils/sum.ts"]);
|
|
assert!(target.starts_with("??"), "{target}");
|
|
}
|
|
|
|
#[test]
|
|
fn dir_move_stages_every_rename_and_prunes_the_source_dir() {
|
|
let tmp = git_fixture("basic");
|
|
jmove(&tmp, &["mv", "lib", "vendor/lib"])
|
|
.success()
|
|
.stdout(predicate::str::contains("(via git mv)"));
|
|
let staged = git_output(tmp.path(), &["diff", "--cached", "-M", "--name-status"]);
|
|
assert!(
|
|
staged.contains("vendor/lib/sum.ts") && staged.contains("lib/sum.ts"),
|
|
"{staged}"
|
|
);
|
|
assert!(tmp.path().join("vendor/lib/sum.ts").is_file());
|
|
assert!(
|
|
!tmp.path().join("lib").exists(),
|
|
"emptied source dir pruned"
|
|
);
|
|
}
|