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:
loki5512344 2026-09-15 16:10:04 +02:00
parent 0216fd6523
commit 9dda5f4562
Signed by: boba
GPG key ID: 253067914055423B
37 changed files with 2521 additions and 234 deletions

View file

@ -7,8 +7,8 @@
use serde::Serialize;
use crate::core::JmoveError;
use crate::core::plan::MovePlan;
use crate::core::{JmoveError, rel_str};
use super::output;
@ -98,6 +98,10 @@ impl ErrorData {
"PLAN_REJECTED",
"run `jmove check --json` to inspect the import graph",
),
JmoveError::Git(_) => (
"GIT_ERROR",
"retry with --no-git to move without touching git",
),
};
Self::new(code, err.to_string(), Some(hint.to_owned()))
}
@ -136,18 +140,21 @@ pub struct MvData {
pub moved: usize,
/// Total specifiers rewritten across all importers.
pub updated_imports: usize,
/// Rename backend: `"git"` (staged in the index) or `"fs"`.
pub moved_via: &'static str,
}
impl MvData {
/// Assemble the payload from an applied plan and its change details.
#[must_use]
pub fn new(plan: &MovePlan, changed_files: Vec<ChangedFile>) -> Self {
pub fn new(plan: &MovePlan, changed_files: Vec<ChangedFile>, via_git: bool) -> Self {
Self {
source: plan.source.display().to_string(),
target: plan.target.display().to_string(),
source: rel_str(&plan.source),
target: rel_str(&plan.target),
changed_files,
moved: 1,
updated_imports: plan.rewrites.len(),
moved_via: if via_git { "git" } else { "fs" },
}
}
}
@ -166,21 +173,24 @@ pub struct MvDryRunData {
pub affected_files: Vec<String>,
/// Unified diff (rewrites + rename) of the whole plan.
pub diff: String,
/// Rename backend a real run would use: `"git"` or `"fs"`.
pub would_move_via: &'static str,
}
impl MvDryRunData {
/// Assemble the preview payload from a plan and its rendered diff.
#[must_use]
pub fn new(plan: &MovePlan, diff: String) -> Self {
pub fn new(plan: &MovePlan, diff: String, via_git: bool) -> Self {
Self {
would_move: plan.source.display().to_string(),
target: plan.target.display().to_string(),
would_move: rel_str(&plan.source),
target: rel_str(&plan.target),
would_update: plan.rewrites.len(),
affected_files: output::group_by_file(&plan.rewrites)
.into_iter()
.map(|(file, _)| file.display().to_string())
.map(|(file, _)| rel_str(file))
.collect(),
diff,
would_move_via: if via_git { "git" } else { "fs" },
}
}
}