pub mod prelude;
pub mod fleet;
pub mod git_backend;
pub mod ldiff;
pub mod logging;
pub mod refac;
pub mod scrap;
pub mod search_backend;
pub mod state;
pub mod workspace_state;
pub mod workspace_update;
pub mod entities;
pub mod web;
pub mod code_analysis;
pub mod extensions;
pub mod interactive_tree;
use anyhow::{Context, Result};
use std::path::Path;
pub fn get_version() -> &'static str {
include_str!("../version.txt").trim()
}
pub use refac::cli;
pub use refac::cli::{Args, Mode};
pub use refac::patterns_file::PatternPair;
pub use refac::rename_engine::RenameEngine;
pub use scrap::scrap_common::{ScrapEntry, ScrapMetadata};
pub use scrap::{run_scrap, run_unscrap};
pub use ldiff::run_ldiff;
pub use state::{StateConfig, VersionInfo};
pub fn run_refac(args: Args) -> Result<()> {
use refac::patterns_file::{parse_patterns_file, PatternPair};
let pairs: Vec<PatternPair> = match (&args.pattern, &args.substitute, &args.patterns_file) {
(Some(p), Some(s), None) => {
vec![PatternPair {
pattern: p.clone(),
substitute: s.clone(),
}]
}
(None, None, Some(pf)) => parse_patterns_file(pf)?,
_ => {
anyhow::bail!(
"Invalid argument combination: provide PATTERN SUBSTITUTE or --patterns-file"
);
}
};
if args.should_process_names() {
let invalid_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|', '\0'];
for (idx, pair) in pairs.iter().enumerate() {
if let Some(ch) = pair.substitute.chars().find(|c| invalid_chars.contains(c)) {
anyhow::bail!(
"Pattern pair {} ('{}' -> '{}'): substitute cannot contain path-unsafe characters ({}) when processing names. Use --content-only.",
idx + 1, pair.pattern, pair.substitute, ch
);
}
}
}
let engine = RenameEngine::new_with_pairs(args, pairs)?;
engine.execute()
}
#[derive(Debug, Clone)]
pub struct RenameItem {
pub original_path: std::path::PathBuf,
pub new_path: std::path::PathBuf,
pub item_type: ItemType,
pub depth: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ItemType {
File,
Directory,
}
#[derive(Debug, Clone)]
pub struct RenameConfig {
pub root_dir: std::path::PathBuf,
pub pattern: String,
pub substitute: String,
pub assume_yes: bool,
pub verbose: bool,
pub follow_symlinks: bool,
pub backup: bool,
}
impl RenameConfig {
pub fn new<P: AsRef<Path>>(root_dir: P, pattern: String, substitute: String) -> Result<Self> {
let root_path = root_dir.as_ref().canonicalize().with_context(|| {
format!(
"Failed to resolve root directory: {}",
root_dir.as_ref().display()
)
})?;
if pattern.is_empty() {
anyhow::bail!("Pattern cannot be empty");
}
if substitute.is_empty() {
anyhow::bail!("Substitute cannot be empty");
}
Ok(Self {
root_dir: root_path,
pattern,
substitute,
assume_yes: false,
verbose: false,
follow_symlinks: false,
backup: false,
})
}
pub fn with_assume_yes(mut self, assume_yes: bool) -> Self {
self.assume_yes = assume_yes;
self
}
pub fn with_verbose(mut self, verbose: bool) -> Self {
self.verbose = verbose;
self
}
pub fn with_follow_symlinks(mut self, follow_symlinks: bool) -> Self {
self.follow_symlinks = follow_symlinks;
self
}
pub fn with_backup(mut self, backup: bool) -> Self {
self.backup = backup;
self
}
}
#[derive(Debug, Default, Clone)]
pub struct RenameStats {
pub files_with_content_changes: usize,
pub files_renamed: usize,
pub directories_renamed: usize,
pub files_processed: usize,
pub errors: Vec<String>,
}
impl RenameStats {
pub fn total_changes(&self) -> usize {
self.files_with_content_changes + self.files_renamed + self.directories_renamed
}
pub fn add_error(&mut self, error: String) {
self.errors.push(error);
}
}
pub mod utils {
use std::path::Path;
pub fn calculate_depth(path: &Path, root: &Path) -> usize {
path.strip_prefix(root)
.map(|p| p.components().count())
.unwrap_or(0)
}
pub fn contains_pattern(text: &str, pattern: &str) -> bool {
text.contains(pattern)
}
pub fn replace_all(text: &str, old: &str, new: &str) -> String {
text.replace(old, new)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_rename_config_creation() {
let temp_dir = TempDir::new().unwrap();
let config =
RenameConfig::new(temp_dir.path(), "old".to_string(), "new".to_string()).unwrap();
assert_eq!(config.pattern, "old");
assert_eq!(config.substitute, "new");
assert!(!config.assume_yes);
}
#[test]
fn test_rename_config_validation() {
let temp_dir = TempDir::new().unwrap();
assert!(RenameConfig::new(temp_dir.path(), "".to_string(), "new".to_string()).is_err());
assert!(RenameConfig::new(temp_dir.path(), "old".to_string(), "".to_string()).is_err());
assert!(
RenameConfig::new(temp_dir.path(), "old".to_string(), "new/path".to_string()).is_ok()
);
assert!(
RenameConfig::new(temp_dir.path(), "old".to_string(), "new\\path".to_string()).is_ok()
);
}
#[test]
fn test_utils_replace_all() {
assert_eq!(utils::replace_all("hello world", "l", "x"), "hexxo worxd");
assert_eq!(
utils::replace_all("test test test", "test", "demo"),
"demo demo demo"
);
assert_eq!(utils::replace_all("no match", "xyz", "abc"), "no match");
}
#[test]
fn test_utils_contains_pattern() {
assert!(utils::contains_pattern("hello world", "hello"));
assert!(utils::contains_pattern("hello world", "world"));
assert!(utils::contains_pattern("hello world", "o w"));
assert!(!utils::contains_pattern("hello world", "xyz"));
}
#[test]
fn test_get_version() {
let version = get_version();
assert!(!version.is_empty());
assert!(version.contains('.'));
let parts: Vec<&str> = version.split('.').collect();
assert!(
parts.len() >= 2,
"Version should have at least major.minor format"
);
for part in &parts {
assert!(
part.parse::<u32>().is_ok(),
"Version part '{}' should be numeric",
part
);
}
}
#[test]
fn test_version_consistency() {
let version_content =
std::fs::read_to_string("version.txt").expect("version.txt should exist");
let file_version = version_content.trim();
assert_eq!(get_version(), file_version);
}
}