feat: fix engine (Phase 1.6) + git-aware mv + windows-safe path output
- Edit engine: generic span edits (replace/insert/delete) with stale-index guard; mv and fix share one atomic apply/rollback/diff/--json pipeline - jmove mv|check|fix commands; java rules: unused-import, missing-import (unique-FQN insert, ambiguous -> candidates for agents), import-order (google style; cross-rule byte conflicts prune by severity and converge) - mv of tracked files goes through git mv (staged rename, --no-git opt-out, moved_via/would_move_via in --json, GIT_ERROR code) - fix java extract_package losing single-segment packages (bare identifier, not scoped_identifier) — broke the FQN index for 'package p;' projects - core::rel_str: project-relative paths rendered with '/' on every platform (human/JSON/diff headers/git pathspecs), Path::display() no longer used for output; e2e fixtures: java/fix, java/fix_missing, java/order
This commit is contained in:
parent
0216fd6523
commit
9dda5f4562
37 changed files with 2521 additions and 234 deletions
115
tests/cli_git.rs
Normal file
115
tests/cli_git.rs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
//! 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}");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue