feat: directory moves — mirrored batch relocation of every indexed file under a dir
- 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
This commit is contained in:
parent
b96deeaf2f
commit
6f7a2190e6
17 changed files with 842 additions and 293 deletions
|
|
@ -2,12 +2,14 @@
|
|||
//!
|
||||
//! A plan is pure data (no disk writes), so dry-run and `--json` can render
|
||||
//! it without touching the filesystem. Specifier arithmetic lives in
|
||||
//! [`specifier`]; the Java package/directory flavour in [`java`].
|
||||
//! [`specifier`]; the Java package/directory flavour in [`java`], and the
|
||||
//! mirrored batch move of a whole directory in [`dir`].
|
||||
|
||||
mod dir;
|
||||
mod java;
|
||||
mod specifier;
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests_support;
|
||||
pub(crate) use dir::tests_support;
|
||||
mod specifier;
|
||||
|
||||
pub use specifier::relative_specifier;
|
||||
|
||||
|
|
@ -42,24 +44,44 @@ impl From<&Rewrite> for Edit {
|
|||
}
|
||||
}
|
||||
|
||||
/// Complete plan for moving `source` to `target`.
|
||||
/// One physical file relocation inside a plan.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MovePlan {
|
||||
/// Project-relative path being moved.
|
||||
pub struct FileMove {
|
||||
/// Project-relative file being moved.
|
||||
pub source: PathBuf,
|
||||
/// Project-relative destination path.
|
||||
pub target: PathBuf,
|
||||
/// Specifier rewrites, sorted by (file, span).
|
||||
pub rewrites: Vec<Rewrite>,
|
||||
}
|
||||
|
||||
/// Compute the rewrite plan for `source -> target`.
|
||||
/// Complete plan for moving `source` to `target`: a file move produces one
|
||||
/// [`FileMove`], a directory move one per indexed member (see [`dir`]).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MovePlan {
|
||||
/// Requested source: the file, or the directory whose members move.
|
||||
pub source: PathBuf,
|
||||
/// Requested destination.
|
||||
pub target: PathBuf,
|
||||
/// Every physical relocation, in sorted source order.
|
||||
pub moves: Vec<FileMove>,
|
||||
/// Specifier/package rewrites, merged across moves, sorted by (file, span).
|
||||
pub rewrites: Vec<Rewrite>,
|
||||
/// Directory moves only: real files under `source` that no plan step
|
||||
/// moves (unindexable assets) — reported, never silently relocated.
|
||||
pub left_behind: Vec<PathBuf>,
|
||||
/// Directory moves only: source directories to prune (deepest first)
|
||||
/// once every move landed. Removal only succeeds when a directory is
|
||||
/// empty, so `left_behind` files keep their home in place — exactly right.
|
||||
pub prune_dirs: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
/// Compute the rewrite plan for `source -> target`; a `source` that is a
|
||||
/// directory on disk becomes a mirrored move of all indexed members below it.
|
||||
///
|
||||
/// TS/JS: every indexed import whose resolved target equals `source` gets a
|
||||
/// new relative specifier from the importer's directory to `target` (see
|
||||
/// [`relative_specifier`]). Java moves additionally rewrite the moved file's
|
||||
/// `package` declaration (see [`java`]). Rewrites whose result equals the
|
||||
/// old specifier are dropped; the result is sorted by (file, span).
|
||||
/// TS/JS: every indexed import whose resolved target equals a moved file gets
|
||||
/// a new relative specifier from the importer's directory to its destination
|
||||
/// (see [`relative_specifier`]). Java moves additionally rewrite the moved
|
||||
/// file's `package` declaration (see [`java`]). Rewrites whose result equals
|
||||
/// the old specifier are dropped; the result is sorted by (file, span).
|
||||
pub fn plan_move(index: &Index, source: &Path, target: &Path) -> JmoveResult<MovePlan> {
|
||||
let rel = |label: &str, p: &Path| match normalize_rel_path(p) {
|
||||
Some(r) => Ok(r),
|
||||
|
|
@ -69,36 +91,54 @@ pub fn plan_move(index: &Index, source: &Path, target: &Path) -> JmoveResult<Mov
|
|||
))),
|
||||
};
|
||||
let (source, target) = (rel("source path", source)?, rel("target path", target)?);
|
||||
if source == target {
|
||||
return Err(JmoveError::PlanRejected(
|
||||
"source and target are the same".into(),
|
||||
));
|
||||
}
|
||||
// A directory on disk turns the command into a mirrored batch move.
|
||||
if index.root.join(&source).is_dir() {
|
||||
return dir::plan_dir(index, &source, &target);
|
||||
}
|
||||
if !index.files.contains(&source) {
|
||||
let s = rel_str(&source);
|
||||
return Err(JmoveError::InvalidArgument(format!(
|
||||
"source '{s}' is not an indexed file"
|
||||
)));
|
||||
}
|
||||
if source == target {
|
||||
return Err(JmoveError::PlanRejected(
|
||||
"source and target are the same".into(),
|
||||
));
|
||||
}
|
||||
if index.files.contains(&target) {
|
||||
let t = rel_str(&target);
|
||||
return Err(JmoveError::PlanRejected(format!(
|
||||
"target '{t}' already exists"
|
||||
)));
|
||||
}
|
||||
|
||||
let rewrites = if SourceLanguage::for_path(&source) == Some(SourceLanguage::Java) {
|
||||
java::java_rewrites(index, &source, &target)?
|
||||
} else {
|
||||
ts_rewrites(index, &source, &target)
|
||||
};
|
||||
let rewrites = file_rewrites(index, &source, &target)?;
|
||||
Ok(MovePlan {
|
||||
source,
|
||||
target,
|
||||
source: source.clone(),
|
||||
target: target.clone(),
|
||||
moves: vec![FileMove { source, target }],
|
||||
rewrites,
|
||||
left_behind: Vec::new(),
|
||||
prune_dirs: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// The per-file rewrite set: Java moves rewrite the `package` declaration
|
||||
/// too; shared by single-file and directory planning.
|
||||
pub(super) fn file_rewrites(
|
||||
index: &Index,
|
||||
source: &Path,
|
||||
target: &Path,
|
||||
) -> JmoveResult<Vec<Rewrite>> {
|
||||
Ok(
|
||||
if SourceLanguage::for_path(source) == Some(SourceLanguage::Java) {
|
||||
java::java_rewrites(index, source, target)?
|
||||
} else {
|
||||
ts_rewrites(index, source, target)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Relative-specifier rewrites for the TS/JS flavour of the graph.
|
||||
fn ts_rewrites(index: &Index, source: &Path, target: &Path) -> Vec<Rewrite> {
|
||||
let mut rewrites = Vec::new();
|
||||
|
|
@ -126,10 +166,10 @@ fn ts_rewrites(index: &Index, source: &Path, target: &Path) -> Vec<Rewrite> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::tests_support::edge;
|
||||
use super::{Rewrite, plan_move};
|
||||
use crate::core::JmoveError;
|
||||
use crate::core::index::{Index, ResolvedImport};
|
||||
use crate::core::plan::tests_support::edge;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn index_with(files: &[&str], imports: &[(&str, Vec<ResolvedImport>)]) -> Index {
|
||||
|
|
@ -170,6 +210,7 @@ mod tests {
|
|||
new_text: "./sub/deep/s".into(),
|
||||
}
|
||||
);
|
||||
assert_eq!(plan.moves.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue