use crate::prelude::*;
use rayon::prelude::*;
use std::sync::{Arc, Mutex};
use walkdir::{DirEntry, WalkDir};
use super::{
cli::{Args, Mode, OutputFormat},
collision_detector::{CollisionDetector, CollisionType},
file_ops::FileOperations,
patterns_file::PatternPair,
progress::{ProgressTracker, SimpleOutput},
};
use crate::{utils, ItemType, RenameConfig, RenameItem, RenameStats};
#[derive(Debug, Clone)]
pub struct FileChangeReport {
pub path: PathBuf,
pub content_changes: Option<usize>, pub rename_target: Option<PathBuf>, pub item_type: ItemType,
}
#[derive(Debug, Clone)]
pub struct DetailedChangeReport {
pub file_changes: Vec<FileChangeReport>,
pub total_stats: RenameStats,
}
#[derive(Debug, Clone)]
pub struct ValidationError {
pub location: PathBuf,
pub error_type: ValidationErrorType,
pub message: String,
pub suggestion: Option<String>,
}
#[derive(Debug, Clone)]
pub enum ValidationErrorType {
FileNotFound,
PermissionDenied,
EncodingError,
NotAFile,
NotADirectory,
ReadOnlyFile,
TargetExists,
ParentDirectoryError,
ContentNotFound,
EmptyDirectoryIssue,
}
pub struct RenameEngine {
config: RenameConfig,
pairs: Vec<PatternPair>,
mode: Mode,
file_ops: FileOperations,
progress: Option<ProgressTracker>,
simple_output: Option<SimpleOutput>,
thread_count: usize,
output_format: OutputFormat,
max_depth: Option<usize>,
include_patterns: Vec<String>,
exclude_patterns: Vec<String>,
ignore_case: bool,
use_regex: bool,
include_hidden: bool,
binary_names: bool,
}
impl RenameEngine {
pub fn new(args: Args) -> Result<Self> {
Self::new_with_pairs(args, Vec::new())
}
pub fn new_with_pairs(args: Args, pairs: Vec<PatternPair>) -> Result<Self> {
args.validate().map_err(|e| anyhow::anyhow!(e))?;
let pairs = if pairs.is_empty() {
let pattern = args
.pattern
.clone()
.ok_or_else(|| anyhow::anyhow!("Pattern is required"))?;
let substitute = args
.substitute
.clone()
.ok_or_else(|| anyhow::anyhow!("Substitute is required"))?;
vec![PatternPair {
pattern,
substitute,
}]
} else {
pairs
};
let config = RenameConfig::new(
&args.root_dir,
pairs[0].pattern.clone(),
pairs[0].substitute.clone(),
)?
.with_assume_yes(args.assume_yes)
.with_verbose(args.verbose)
.with_follow_symlinks(args.follow_symlinks)
.with_backup(args.backup);
let show_progress = match args.progress {
super::cli::ProgressMode::Always => true,
super::cli::ProgressMode::Never => false,
super::cli::ProgressMode::Auto => atty::is(atty::Stream::Stdout),
};
let (progress, simple_output) = if show_progress && args.format == OutputFormat::Human {
(Some(ProgressTracker::new(true, args.verbose)), None)
} else {
(None, Some(SimpleOutput::new(args.verbose)))
};
Ok(Self {
config,
pairs,
mode: args.get_mode(),
file_ops: FileOperations::new().with_backup(args.backup),
progress,
simple_output,
thread_count: args.get_thread_count(),
output_format: args.format,
max_depth: if args.max_depth > 0 {
Some(args.max_depth)
} else {
None
},
include_patterns: args.include_patterns,
exclude_patterns: args.exclude_patterns,
ignore_case: args.ignore_case,
use_regex: args.use_regex,
include_hidden: args.include_hidden,
binary_names: args.binary_names,
})
}
pub fn execute(&self) -> Result<()> {
self.print_header()?;
self.print_info("Phase 1: Discovering files and directories...")?;
let (content_files, rename_items) = self.discover_items()?;
self.print_info("Phase 2: Checking for naming collisions...")?;
self.check_collisions(&rename_items)?;
self.print_info("Phase 3: Validating all operations...")?;
self.validate_all_operations(&content_files, &rename_items)?;
let stats = self.show_summary(&content_files, &rename_items)?;
if stats.total_changes() == 0 {
self.print_success("No changes needed.")?;
return Ok(());
}
if !content_files.is_empty() && self.output_format == OutputFormat::Human {
self.show_diff_preview(&content_files)?;
}
if !self.confirm_changes()? {
self.print_info("Operation cancelled by user.")?;
return Ok(());
}
self.execute_changes(&content_files, &rename_items)?;
self.show_final_report(&stats)?;
Ok(())
}
fn discover_items(&self) -> Result<(Vec<PathBuf>, Vec<RenameItem>)> {
let mut content_files = Vec::new();
let mut rename_items = Vec::new();
if let Some(progress) = &self.progress {
progress.init_main_progress(0, "Scanning files and directories...");
}
let walker = WalkDir::new(&self.config.root_dir)
.follow_links(self.config.follow_symlinks)
.max_depth(self.max_depth.unwrap_or(usize::MAX))
.into_iter()
.filter_entry(|e| self.should_process_entry(e));
for entry in walker {
let entry = entry.with_context(|| "Failed to read directory entry")?;
let path = entry.path();
if path == self.config.root_dir {
continue;
}
if !self.matches_patterns(path)? {
continue;
}
if self.should_process_content() && self.should_process_files() && path.is_file() {
if self.file_needs_content_replacement(path)? {
content_files.push(path.to_path_buf());
}
}
if self.should_process_names() {
if let Some(rename_item) = self.create_rename_item(path)? {
rename_items.push(rename_item);
}
}
if let Some(progress) = &self.progress {
progress.update_main(&format!("Scanned: {}", path.display()));
}
}
rename_items.sort_by(|a, b| match (&a.item_type, &b.item_type) {
(ItemType::File, ItemType::Directory) => std::cmp::Ordering::Less,
(ItemType::Directory, ItemType::File) => std::cmp::Ordering::Greater,
(ItemType::File, ItemType::File) => b.depth.cmp(&a.depth),
(ItemType::Directory, ItemType::Directory) => b.depth.cmp(&a.depth),
});
if let Some(progress) = &self.progress {
progress.finish_main("Discovery complete");
}
Ok((content_files, rename_items))
}
fn should_process_entry(&self, entry: &DirEntry) -> bool {
let path = entry.path();
if path == self.config.root_dir {
return true;
}
if let Some(name) = path.file_name() {
if let Some(name_str) = name.to_str() {
if name_str.starts_with('.') {
if self.include_hidden {
} else {
let should_include = self
.include_patterns
.iter()
.any(|p| p == ".*" || p.contains("*"));
if !should_include {
return false;
}
}
}
}
}
match self.mode {
Mode::FilesOnly => path.is_file(),
Mode::DirsOnly => path.is_dir(),
_ => true,
}
}
fn matches_patterns(&self, path: &Path) -> Result<bool> {
if !self.include_patterns.is_empty() {
let matches = self
.include_patterns
.iter()
.any(|pattern| self.path_matches_pattern(path, pattern));
if !matches {
return Ok(false);
}
}
if !self.exclude_patterns.is_empty() {
let excluded = self
.exclude_patterns
.iter()
.any(|pattern| self.path_matches_pattern(path, pattern));
if excluded {
return Ok(false);
}
}
Ok(true)
}
fn path_matches_pattern(&self, path: &Path, pattern: &str) -> bool {
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
let pattern_str = if self.ignore_case {
pattern.to_lowercase()
} else {
pattern.to_string()
};
let compare_str = if self.ignore_case {
file_name.to_lowercase()
} else {
file_name.to_string()
};
if self.use_regex {
if let Ok(regex) = regex::Regex::new(&pattern_str) {
return regex.is_match(&compare_str);
}
}
if pattern_str == ".*" {
return compare_str.starts_with('.');
}
if pattern_str.contains('*') {
let parts: Vec<&str> = pattern_str.split('*').collect();
if parts.len() == 2 {
return compare_str.starts_with(parts[0]) && compare_str.ends_with(parts[1]);
}
}
compare_str.contains(&pattern_str)
}
fn file_needs_content_replacement(&self, path: &Path) -> Result<bool> {
if !self.file_ops.is_text_file(path)? {
return Ok(false);
}
let search_strings: Vec<&str> = self
.pairs
.iter()
.map(|p| {
if self.ignore_case {
p.pattern.as_str()
} else {
p.pattern.as_str()
}
})
.collect();
self.file_ops
.file_contains_any_string(path, &search_strings)
}
fn apply_pairs_to_name(&self, name: &str) -> String {
let mut result = name.to_string();
for pair in &self.pairs {
if self.ignore_case {
result = result
.to_lowercase()
.replace(&pair.pattern.to_lowercase(), &pair.substitute);
} else {
result = result.replace(&pair.pattern, &pair.substitute);
}
}
result
}
fn create_rename_item(&self, path: &Path) -> Result<Option<RenameItem>> {
let file_name = path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| anyhow::anyhow!("Invalid file name: {}", path.display()))?;
let final_name = self.apply_pairs_to_name(file_name);
if final_name == file_name {
return Ok(None);
}
if path.is_file() {
match self.file_ops.is_text_file(path) {
Ok(false) => {
if !self.binary_names {
return Ok(None);
}
}
Err(_) => {
if !self.binary_names {
return Ok(None);
}
}
Ok(true) => {}
}
}
let item_type = if path.is_file() {
if !self.should_process_files() {
return Ok(None);
}
ItemType::File
} else {
if !self.should_process_dirs() {
return Ok(None);
}
ItemType::Directory
};
let depth = utils::calculate_depth(path, &self.config.root_dir);
let new_path = path.with_file_name(&final_name);
Ok(Some(RenameItem {
original_path: path.to_path_buf(),
new_path,
item_type,
depth,
}))
}
fn check_collisions(&self, rename_items: &[RenameItem]) -> Result<()> {
if rename_items.is_empty() {
return Ok(());
}
let mut detector = CollisionDetector::new();
detector.scan_existing_paths(&self.config.root_dir)?;
detector.add_renames(rename_items);
let collisions = detector.detect_collisions()?;
if !collisions.is_empty() {
self.print_error("Naming collisions detected!")?;
for collision in &collisions {
match collision.collision_type {
CollisionType::SourceEqualsTarget => {
continue;
}
_ => {
self.print_error(&collision.description)?;
}
}
}
let serious_collisions: Vec<_> = collisions
.iter()
.filter(|c| c.collision_type != CollisionType::SourceEqualsTarget)
.collect();
if !serious_collisions.is_empty() {
anyhow::bail!(
"Cannot proceed due to {} naming collision(s)",
serious_collisions.len()
);
}
}
Ok(())
}
fn generate_detailed_report(
&self,
content_files: &[PathBuf],
rename_items: &[RenameItem],
) -> Result<DetailedChangeReport> {
let mut file_changes_map: HashMap<PathBuf, FileChangeReport> = HashMap::new();
let mut stats = RenameStats::default();
for file_path in content_files {
let content_count = match std::fs::read_to_string(file_path) {
Ok(content) => {
let mut total = 0;
let mut current = content;
for pair in &self.pairs {
let count = current.matches(&pair.pattern).count();
total += count;
if count > 0 {
current = current.replace(&pair.pattern, &pair.substitute);
}
}
total
}
Err(_) => 0,
};
file_changes_map.insert(
file_path.clone(),
FileChangeReport {
path: file_path.clone(),
content_changes: Some(content_count),
rename_target: None,
item_type: ItemType::File,
},
);
stats.files_with_content_changes += 1;
}
for item in rename_items {
let entry = file_changes_map
.entry(item.original_path.clone())
.or_insert_with(|| FileChangeReport {
path: item.original_path.clone(),
content_changes: None,
rename_target: None,
item_type: item.item_type.clone(),
});
entry.rename_target = Some(item.new_path.clone());
entry.item_type = item.item_type.clone();
match item.item_type {
ItemType::File => stats.files_renamed += 1,
ItemType::Directory => stats.directories_renamed += 1,
}
}
let mut file_changes: Vec<FileChangeReport> = file_changes_map.into_values().collect();
file_changes.sort_by(|a, b| a.path.cmp(&b.path));
Ok(DetailedChangeReport {
file_changes,
total_stats: stats,
})
}
fn show_summary(
&self,
content_files: &[PathBuf],
rename_items: &[RenameItem],
) -> Result<RenameStats> {
let report = self.generate_detailed_report(content_files, rename_items)?;
match self.output_format {
OutputFormat::Json => {
let json_report = serde_json::json!({
"summary": {
"content_changes": report.total_stats.files_with_content_changes,
"file_renames": report.total_stats.files_renamed,
"directory_renames": report.total_stats.directories_renamed,
"total_changes": report.total_stats.total_changes()
},
"file_changes": report.file_changes.iter().map(|fc| {
serde_json::json!({
"path": fc.path,
"content_changes": fc.content_changes,
"rename_target": fc.rename_target,
"item_type": format!("{:?}", fc.item_type)
})
}).collect::<Vec<_>>()
});
println!("{}", serde_json::to_string_pretty(&json_report)?);
}
OutputFormat::Plain => {
println!(
"Content changes: {}",
report.total_stats.files_with_content_changes
);
println!("File renames: {}", report.total_stats.files_renamed);
println!(
"Directory renames: {}",
report.total_stats.directories_renamed
);
println!("Total changes: {}", report.total_stats.total_changes());
}
OutputFormat::Human => {
self.print_info("=== PLANNED CHANGES ===")?;
self.print_info(&format!(
"Total files/directories affected: {}",
report.file_changes.len()
))?;
self.print_info(&format!(
"Content modifications: {} file(s)",
report.total_stats.files_with_content_changes
))?;
self.print_info(&format!(
"File renames: {} file(s)",
report.total_stats.files_renamed
))?;
self.print_info(&format!(
"Directory renames: {} directory(ies)",
report.total_stats.directories_renamed
))?;
self.print_info("")?;
if !report.file_changes.is_empty() {
self.print_info("=== DETAILED CHANGES BY LOCATION ===")?;
for change in &report.file_changes {
let relative_path = change
.path
.strip_prefix(&self.config.root_dir)
.unwrap_or(&change.path);
self.print_info(&format!("📁 {}", relative_path.display()))?;
if let Some(count) = change.content_changes {
if self.pairs.len() == 1 {
self.print_verbose(&format!(
" Content: {} occurrence(s) of '{}' → '{}'",
count, self.pairs[0].pattern, self.pairs[0].substitute
))?;
} else {
self.print_verbose(&format!(
" Content: {} total replacement(s) across {} pairs",
count,
self.pairs.len()
))?;
}
}
if let Some(target) = &change.rename_target {
let relative_target =
target.strip_prefix(&self.config.root_dir).unwrap_or(target);
self.print_verbose(&format!(
" Rename: {} → {}",
relative_path.display(),
relative_target.display()
))?;
}
}
}
}
}
Ok(report.total_stats)
}
fn show_diff_preview(&self, content_files: &[PathBuf]) -> Result<()> {
self.print_info("=== DIFF PREVIEW ===")?;
for (i, file_path) in content_files.iter().enumerate() {
if i >= 5 {
self.print_info(&format!("... and {} more files", content_files.len() - i))?;
break;
}
let relative_path = file_path
.strip_prefix(&self.config.root_dir)
.unwrap_or(file_path);
let content = match std::fs::read_to_string(file_path) {
Ok(content) => content,
Err(_) => {
self.print_warning(&format!(
"⚠️ Cannot preview {}: unable to read file",
relative_path.display()
))?;
continue;
}
};
self.print_info(&format!("\n⏺ Update({})", relative_path.display()))?;
let mut total_replacements = 0;
let mut simulated = content.clone();
for pair in &self.pairs {
let count = simulated.matches(&pair.pattern).count();
total_replacements += count;
if count > 0 {
simulated = simulated.replace(&pair.pattern, &pair.substitute);
}
}
self.print_verbose(&format!(
" ⎿ Updated {} with {} replacement(s)",
relative_path.display(),
total_replacements
))?;
self.show_diff_context(&content, file_path)?;
}
self.print_info("")?; Ok(())
}
fn show_diff_context(&self, content: &str, _file_path: &Path) -> Result<()> {
use colored::*;
let lines: Vec<&str> = content.lines().collect();
let mut changes_shown = 0;
const MAX_CHANGES_TO_SHOW: usize = 3;
for (i, line) in lines.iter().enumerate() {
if changes_shown >= MAX_CHANGES_TO_SHOW {
break;
}
let line_matches = self.pairs.iter().any(|p| line.contains(&p.pattern));
if !line_matches {
continue;
}
let start_context = i.saturating_sub(2);
let end_context = std::cmp::min(i + 3, lines.len());
for j in start_context..i {
let line_num = j + 1;
println!(
" {} {}",
line_num.to_string().dimmed(),
lines[j]
);
}
let line_num = i + 1;
println!(
" {} {} {}",
line_num.to_string().dimmed(),
"-".red().bold(),
line.white().on_bright_red()
);
let mut new_line = line.to_string();
for pair in &self.pairs {
new_line = new_line.replace(&pair.pattern, &pair.substitute);
}
println!(
" {} {} {}",
line_num.to_string().dimmed(),
"+".green().bold(),
new_line.white().on_bright_green()
);
for j in (i + 1)..end_context {
let line_num = j + 1;
println!(
" {} {}",
line_num.to_string().dimmed(),
lines[j]
);
}
changes_shown += 1;
if changes_shown < MAX_CHANGES_TO_SHOW && i + 3 < lines.len() {
println!(" ...");
}
}
Ok(())
}
fn confirm_changes(&self) -> Result<bool> {
if self.config.assume_yes {
return Ok(true);
}
match self.output_format {
OutputFormat::Json => Ok(true), OutputFormat::Plain | OutputFormat::Human => {
self.print_warning("This operation will modify your files and directories.")?;
let confirmation = if let Some(progress) = &self.progress {
progress.suspend(|| {
dialoguer::Confirm::new()
.with_prompt("Do you want to proceed?")
.default(false)
.interact()
})
} else {
dialoguer::Confirm::new()
.with_prompt("Do you want to proceed?")
.default(false)
.interact()
};
confirmation.with_context(|| "Failed to get user confirmation")
}
}
}
fn execute_changes(
&self,
content_files: &[PathBuf],
rename_items: &[RenameItem],
) -> Result<()> {
if !content_files.is_empty() && self.should_process_content() {
self.execute_content_changes(content_files)?;
}
if !rename_items.is_empty() && self.should_process_names() {
self.execute_renames(rename_items)?;
}
Ok(())
}
fn execute_content_changes(&self, content_files: &[PathBuf]) -> Result<()> {
self.print_info("Replacing content in files...")?;
if let Some(progress) = &self.progress {
progress.init_content_progress(content_files.len() as u64);
}
let errors = Arc::new(Mutex::new(Vec::new()));
let file_ops_ref = &self.file_ops;
let config_ref = &self.config;
let pairs_tuples: Vec<(String, String)> = self
.pairs
.iter()
.map(|p| (p.pattern.clone(), p.substitute.clone()))
.collect();
let errors_ref = Arc::clone(&errors);
if self.thread_count > 1 {
let pairs_ref = &pairs_tuples;
content_files.par_iter().for_each(|file_path| {
if !file_path.exists() {
errors_ref
.lock()
.unwrap()
.push(format!("File no longer exists: {}", file_path.display()));
return;
}
let result = file_ops_ref.replace_content_multi(file_path, pairs_ref);
match result {
Ok(_) => {}
Err(e) => {
errors_ref.lock().unwrap().push(format!(
"Failed to modify {}: {}",
file_path.display(),
e
));
}
}
});
} else {
for file_path in content_files {
if !file_path.exists() {
self.print_error(&format!("File no longer exists: {}", file_path.display()))?;
if let Some(progress) = &self.progress {
progress.update_content(&file_path.display().to_string());
}
continue;
}
let result = file_ops_ref.replace_content_multi(file_path, &pairs_tuples);
match result {
Ok(modified) => {
if modified && config_ref.verbose {
self.print_verbose(&format!("Modified: {}", file_path.display()))?;
}
}
Err(e) => {
self.print_error(&format!(
"Failed to modify {}: {}",
file_path.display(),
e
))?;
}
}
if let Some(progress) = &self.progress {
progress.update_content(&file_path.display().to_string());
}
}
}
let errors = errors.lock().unwrap();
for error in errors.iter() {
self.print_error(error)?;
}
if let Some(progress) = &self.progress {
progress.finish_content(&format!(
"Content replacement complete ({} files)",
content_files.len()
));
}
Ok(())
}
fn execute_renames(&self, rename_items: &[RenameItem]) -> Result<()> {
self.print_info("Renaming files and directories...")?;
if let Some(progress) = &self.progress {
progress.init_rename_progress(rename_items.len() as u64);
}
let mut errors = Vec::new();
let mut successful_renames = Vec::new();
for item in rename_items {
if item.original_path == item.new_path {
if let Some(progress) = &self.progress {
progress.update_rename(&item.original_path.display().to_string());
}
continue;
}
let source_exists =
item.original_path.exists() || item.original_path.symlink_metadata().is_ok();
if !source_exists {
errors.push(format!(
"Source path no longer exists: {}",
item.original_path.display()
));
if let Some(progress) = &self.progress {
progress.update_rename(&item.original_path.display().to_string());
}
continue;
}
if let Some(parent) = item.new_path.parent() {
if let Err(e) = self.file_ops.create_dir_all(parent) {
errors.push(format!(
"Failed to create parent directory for {}: {}",
item.new_path.display(),
e
));
if let Some(progress) = &self.progress {
progress.update_rename(&item.original_path.display().to_string());
}
continue;
}
}
let result = self.file_ops.move_item(&item.original_path, &item.new_path);
match result {
Ok(()) => {
successful_renames.push((item.original_path.clone(), item.new_path.clone()));
if self.config.verbose {
self.print_verbose(&format!(
"Renamed: {} → {}",
item.original_path.display(),
item.new_path.display()
))?;
}
}
Err(e) => {
errors.push(format!(
"Failed to rename {} to {}: {}",
item.original_path.display(),
item.new_path.display(),
e
));
}
}
if let Some(progress) = &self.progress {
progress.update_rename(&item.original_path.display().to_string());
}
}
for error in &errors {
self.print_error(error)?;
}
if !errors.is_empty() {
self.print_warning(&format!(
"{} rename operation(s) failed out of {}",
errors.len(),
rename_items.len()
))?;
}
if let Some(progress) = &self.progress {
progress.finish_rename(&format!(
"Rename complete ({} successful, {} failed)",
successful_renames.len(),
errors.len()
));
}
Ok(())
}
fn validate_all_operations(
&self,
content_files: &[PathBuf],
rename_items: &[RenameItem],
) -> Result<()> {
let mut validation_errors = Vec::new();
for file_path in content_files {
self.validate_content_file(file_path, &mut validation_errors);
}
for item in rename_items {
self.validate_rename_item(item, &mut validation_errors);
}
self.validate_no_empty_directories_remain(rename_items, &mut validation_errors)?;
if !validation_errors.is_empty() {
self.report_validation_errors(&validation_errors)?;
let error_types: std::collections::HashSet<&str> = validation_errors
.iter()
.map(|e| match e.error_type {
ValidationErrorType::FileNotFound => "missing files",
ValidationErrorType::PermissionDenied => "permission issues",
ValidationErrorType::EncodingError => "encoding issues",
ValidationErrorType::ReadOnlyFile => "read-only files",
ValidationErrorType::TargetExists => "target conflicts",
ValidationErrorType::ParentDirectoryError => "directory issues",
ValidationErrorType::ContentNotFound => "content issues",
ValidationErrorType::EmptyDirectoryIssue => "directory structure issues",
_ => "other issues",
})
.collect();
let error_summary = error_types.into_iter().collect::<Vec<_>>().join(", ");
anyhow::bail!(
"Operation cannot proceed due to {} validation error(s): {}",
validation_errors.len(),
error_summary
);
}
self.print_info("Validation passed: All operations can be performed safely.")?;
Ok(())
}
fn validate_content_file(
&self,
file_path: &PathBuf,
validation_errors: &mut Vec<ValidationError>,
) {
let relative_path = file_path
.strip_prefix(&self.config.root_dir)
.unwrap_or(file_path);
if !file_path.exists() {
validation_errors.push(ValidationError {
location: file_path.clone(),
error_type: ValidationErrorType::FileNotFound,
message: format!("Content file does not exist: {}", relative_path.display()),
suggestion: Some("Check if the file was moved or deleted".to_string()),
});
return;
}
if !file_path.is_file() {
validation_errors.push(ValidationError {
location: file_path.clone(),
error_type: ValidationErrorType::NotAFile,
message: format!("Content target is not a file: {}", relative_path.display()),
suggestion: Some(
"Content replacement only works on files, not directories".to_string(),
),
});
return;
}
if let Err(e) = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(file_path)
{
let error_type = if e.kind() == std::io::ErrorKind::PermissionDenied {
ValidationErrorType::PermissionDenied
} else {
ValidationErrorType::PermissionDenied
};
validation_errors.push(ValidationError {
location: file_path.clone(),
error_type,
message: format!("Cannot access file {}: {}", relative_path.display(), e),
suggestion: Some(
"Check file permissions or if file is locked by another process".to_string(),
),
});
return;
}
match self.file_ops.is_text_file(file_path) {
Ok(false) => {
if self.config.verbose {
eprintln!("WARNING: Binary file {} was included in content validation - this may indicate a discovery phase issue", relative_path.display());
}
return; }
Err(e) => {
validation_errors.push(ValidationError {
location: file_path.clone(),
error_type: ValidationErrorType::EncodingError,
message: format!(
"Cannot determine if file {} is binary or text: {}",
relative_path.display(),
e
),
suggestion: Some(
"Use --exclude patterns to exclude problematic files".to_string(),
),
});
return;
}
Ok(true) => {
}
}
let search_strings: Vec<&str> = self.pairs.iter().map(|p| p.pattern.as_str()).collect();
match self
.file_ops
.file_contains_any_string(file_path, &search_strings)
{
Ok(contains_any) => {
if !contains_any {
validation_errors.push(ValidationError {
location: file_path.clone(),
error_type: ValidationErrorType::ContentNotFound,
message: format!(
"File {} does not contain any target pattern",
relative_path.display()
),
suggestion: Some(
"File may have been modified since discovery phase".to_string(),
),
});
}
}
Err(e) => {
validation_errors.push(ValidationError {
location: file_path.clone(),
error_type: ValidationErrorType::EncodingError,
message: format!(
"Cannot read file {} with detected encoding: {}",
relative_path.display(),
e
),
suggestion: Some("Use --exclude patterns to exclude this file".to_string()),
});
}
}
}
fn validate_rename_item(
&self,
item: &RenameItem,
validation_errors: &mut Vec<ValidationError>,
) {
let relative_source = item
.original_path
.strip_prefix(&self.config.root_dir)
.unwrap_or(&item.original_path);
let relative_target = item
.new_path
.strip_prefix(&self.config.root_dir)
.unwrap_or(&item.new_path);
if item.original_path == item.new_path {
return;
}
let source_exists =
item.original_path.exists() || item.original_path.symlink_metadata().is_ok();
if !source_exists {
validation_errors.push(ValidationError {
location: item.original_path.clone(),
error_type: ValidationErrorType::FileNotFound,
message: format!(
"Rename source does not exist: {}",
relative_source.display()
),
suggestion: Some(
"Source may have been moved or deleted since discovery".to_string(),
),
});
return;
}
if item.new_path.exists() && item.new_path != item.original_path {
validation_errors.push(ValidationError {
location: item.new_path.clone(),
error_type: ValidationErrorType::TargetExists,
message: format!(
"Rename target already exists: {}",
relative_target.display()
),
suggestion: Some(
"Target was created since discovery phase, or there's a naming collision"
.to_string(),
),
});
return;
}
if let Some(parent) = item.new_path.parent() {
if !parent.exists() {
match std::fs::create_dir_all(parent) {
Ok(_) => {
let _ = std::fs::remove_dir_all(parent);
}
Err(e) => {
validation_errors.push(ValidationError {
location: parent.to_path_buf(),
error_type: ValidationErrorType::ParentDirectoryError,
message: format!(
"Cannot create parent directory for {}: {}",
relative_target.display(),
e
),
suggestion: Some("Check permissions on parent directories".to_string()),
});
return;
}
}
}
}
match item.original_path.symlink_metadata() {
Ok(metadata) => {
if metadata.permissions().readonly() {
validation_errors.push(ValidationError {
location: item.original_path.clone(),
error_type: ValidationErrorType::ReadOnlyFile,
message: format!("Source is read-only: {}", relative_source.display()),
suggestion: Some(
"Change file permissions or exclude read-only files".to_string(),
),
});
}
}
Err(e) => {
validation_errors.push(ValidationError {
location: item.original_path.clone(),
error_type: ValidationErrorType::PermissionDenied,
message: format!(
"Cannot read metadata for {}: {}",
relative_source.display(),
e
),
suggestion: Some("Check file permissions and access rights".to_string()),
});
}
}
}
fn report_validation_errors(&self, validation_errors: &[ValidationError]) -> Result<()> {
self.print_error("=== VALIDATION FAILED ===")?;
self.print_error(&format!(
"Found {} issue(s) that prevent safe execution:",
validation_errors.len()
))?;
self.print_error("")?;
let mut errors_by_type: HashMap<String, Vec<&ValidationError>> = HashMap::new();
for error in validation_errors {
let type_name = match error.error_type {
ValidationErrorType::FileNotFound => "Missing Files/Directories",
ValidationErrorType::PermissionDenied => "Permission Issues",
ValidationErrorType::EncodingError => "Encoding/Binary File Issues",
ValidationErrorType::NotAFile => "File Type Issues",
ValidationErrorType::ReadOnlyFile => "Read-Only Files",
ValidationErrorType::TargetExists => "Target Conflicts",
ValidationErrorType::ParentDirectoryError => "Directory Creation Issues",
ValidationErrorType::ContentNotFound => "Content Issues",
ValidationErrorType::EmptyDirectoryIssue => "Directory Structure Issues",
_ => "Other Issues",
};
errors_by_type
.entry(type_name.to_string())
.or_insert_with(Vec::new)
.push(error);
}
for (error_type, errors) in errors_by_type {
self.print_error(&format!("📋 {}: ({} issue(s))", error_type, errors.len()))?;
for error in errors {
let relative_path = error
.location
.strip_prefix(&self.config.root_dir)
.unwrap_or(&error.location);
self.print_error(&format!(" 📍 Location: {}", relative_path.display()))?;
self.print_error(&format!(" Problem: {}", error.message))?;
if let Some(suggestion) = &error.suggestion {
self.print_error(&format!(" Suggestion: {}", suggestion))?;
}
self.print_error("")?;
}
}
self.print_error("❌ Cannot proceed until these issues are resolved.")?;
Ok(())
}
fn validate_no_empty_directories_remain(
&self,
rename_items: &[RenameItem],
validation_errors: &mut Vec<ValidationError>,
) -> Result<()> {
use std::collections::{HashMap, HashSet};
let mut directory_contents: HashMap<PathBuf, HashSet<PathBuf>> = HashMap::new();
for entry in walkdir::WalkDir::new(&self.config.root_dir) {
let entry = entry?;
let path = entry.path();
if let Some(parent) = path.parent() {
directory_contents
.entry(parent.to_path_buf())
.or_insert_with(HashSet::new)
.insert(path.to_path_buf());
}
}
for item in rename_items {
if item.original_path == item.new_path {
continue; }
if let Some(old_parent) = item.original_path.parent() {
if let Some(contents) = directory_contents.get_mut(old_parent) {
contents.remove(&item.original_path);
}
}
if let Some(new_parent) = item.new_path.parent() {
directory_contents
.entry(new_parent.to_path_buf())
.or_insert_with(HashSet::new)
.insert(item.new_path.clone());
}
}
let renamed_dirs: HashSet<PathBuf> = rename_items
.iter()
.filter(|item| item.item_type == crate::ItemType::Directory)
.map(|item| item.original_path.clone())
.collect();
for (dir_path, contents) in &directory_contents {
if dir_path == &self.config.root_dir {
continue;
}
if renamed_dirs.contains(dir_path) {
continue;
}
if contents.is_empty() {
let relative_path = dir_path
.strip_prefix(&self.config.root_dir)
.unwrap_or(dir_path);
validation_errors.push(ValidationError {
location: dir_path.clone(),
error_type: ValidationErrorType::EmptyDirectoryIssue,
message: format!("CRITICAL: Directory would become empty after operation: {}. This indicates incorrect operation ordering.",
relative_path.display()),
suggestion: Some("This is a logic error in operation ordering - files should be processed before directories".to_string()),
});
}
}
Ok(())
}
fn show_final_report(&self, stats: &RenameStats) -> Result<()> {
match self.output_format {
OutputFormat::Json => {
let report = serde_json::json!({
"result": "success",
"stats": {
"content_changes": stats.files_with_content_changes,
"file_renames": stats.files_renamed,
"directory_renames": stats.directories_renamed,
"total_changes": stats.total_changes(),
"errors": stats.errors.len()
},
});
println!("{}", serde_json::to_string_pretty(&report)?);
}
OutputFormat::Plain => {
println!("Operation completed successfully.");
println!("Total changes: {}", stats.total_changes());
}
OutputFormat::Human => {
self.print_success("=== OPERATION COMPLETE ===")?;
self.print_success("Operation completed successfully!")?;
self.print_info(&format!("Total changes applied: {}", stats.total_changes()))?;
if !stats.errors.is_empty() {
self.print_warning(&format!("{} error(s) occurred:", stats.errors.len()))?;
for error in &stats.errors {
self.print_error(error)?;
}
}
}
}
Ok(())
}
fn print_header(&self) -> Result<()> {
if self.output_format != OutputFormat::Human {
return Ok(());
}
if self.pairs.len() == 1 {
self.print_success("=== WORKSPACE REFAC TOOL ===")?;
self.print_info(&format!("Pattern: '{}'", self.pairs[0].pattern))?;
self.print_info(&format!("Substitute: '{}'", self.pairs[0].substitute))?;
} else {
self.print_success(&format!(
"=== WORKSPACE REFAC TOOL ({} pairs) ===",
self.pairs.len()
))?;
for (i, pair) in self.pairs.iter().enumerate() {
self.print_info(&format!(
" Pair {}: '{}' → '{}'",
i + 1,
pair.pattern,
pair.substitute
))?;
}
}
self.print_info(&format!(
"Root directory: {}",
self.config.root_dir.display()
))?;
self.print_info(&format!("Mode: {:?}", self.mode))?;
if self.config.backup {
self.print_info("Backup mode: Enabled")?;
}
Ok(())
}
fn print_info(&self, message: &str) -> Result<()> {
if let Some(progress) = &self.progress {
progress.print_info(message);
} else if let Some(output) = &self.simple_output {
output.print_info(message);
}
Ok(())
}
fn print_error(&self, message: &str) -> Result<()> {
if let Some(progress) = &self.progress {
progress.print_error(message);
} else if let Some(output) = &self.simple_output {
output.print_error(message);
}
Ok(())
}
fn print_warning(&self, message: &str) -> Result<()> {
if let Some(progress) = &self.progress {
progress.print_warning(message);
} else if let Some(output) = &self.simple_output {
output.print_warning(message);
}
Ok(())
}
fn print_success(&self, message: &str) -> Result<()> {
if let Some(progress) = &self.progress {
progress.print_success(message);
} else if let Some(output) = &self.simple_output {
output.print_success(message);
}
Ok(())
}
fn print_verbose(&self, message: &str) -> Result<()> {
if let Some(progress) = &self.progress {
progress.print_verbose(message);
} else if let Some(output) = &self.simple_output {
output.print_verbose(message);
}
Ok(())
}
fn should_process_files(&self) -> bool {
self.mode.should_process_files()
}
fn should_process_dirs(&self) -> bool {
self.mode.should_process_dirs()
}
fn should_process_content(&self) -> bool {
!matches!(self.mode, Mode::NamesOnly)
}
fn should_process_names(&self) -> bool {
!matches!(self.mode, Mode::ContentOnly)
}
}
trait ModeExt {
fn should_process_files(&self) -> bool;
fn should_process_dirs(&self) -> bool;
}
impl ModeExt for Mode {
fn should_process_files(&self) -> bool {
matches!(
self,
Mode::Full | Mode::FilesOnly | Mode::ContentOnly | Mode::NamesOnly
)
}
fn should_process_dirs(&self) -> bool {
matches!(self, Mode::Full | Mode::DirsOnly | Mode::NamesOnly)
}
}