diff --git a/Cargo.lock b/Cargo.lock index 9bec00b..0676460 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,6 +59,17 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -153,7 +164,9 @@ name = "dirdiff" version = "0.2.0" dependencies = [ "ansi_term", + "atty", "clap", + "glob", "md5", "rayon", "walkdir", @@ -165,12 +178,33 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "libc" +version = "0.2.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" + [[package]] name = "md5" version = "0.7.0" diff --git a/Cargo.toml b/Cargo.toml index 5902756..b986322 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,9 @@ edition = "2021" [dependencies] ansi_term = "0.12.1" +atty = "0.2.14" clap = { version = "4.4.12", features = ["cargo", "derive"] } +glob = "0.3.1" md5 = "0.7.0" rayon = "1.8.0" walkdir = "2.4.0" diff --git a/src/dirdiff.rs b/src/dirdiff.rs index c4753c9..5a45616 100644 --- a/src/dirdiff.rs +++ b/src/dirdiff.rs @@ -5,6 +5,8 @@ use std::io::Read; use walkdir::WalkDir; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use glob::Pattern; +use ansi_term::{Style, Colour::*}; use md5; #[derive(Debug, Clone)] @@ -42,76 +44,162 @@ impl FileInfo { } } +pub struct DirCmp { + path_a : PathBuf, + path_b : PathBuf, + ignore_patterns : Option>, +} + +impl DirCmp { + pub fn new(path_a: &PathBuf, path_b: &PathBuf, ignore_patterns : &Option>) -> DirCmp { + DirCmp { path_a: path_a.to_owned(), path_b: path_b.to_owned(), ignore_patterns: ignore_patterns.to_owned() } + } + pub fn compare_directories(&self) -> CmpResult { + let path_a_clone = self.path_a.clone(); let path_b_clone = self.path_b.clone(); + + let ignore_patterns_1 = self.ignore_patterns.clone(); + let ignore_patterns_2 = self.ignore_patterns.clone(); + + let thread_a = std::thread::spawn(move || { + return DirCmp::process_directory(&path_a_clone, &ignore_patterns_1); + }); + + let thread_b = std::thread::spawn(move || { + return DirCmp::process_directory(&path_b_clone, &ignore_patterns_2); + }); + + let map1 : HashMap = thread_a.join().unwrap().ok().unwrap(); + let map2 : HashMap = thread_b.join().unwrap().ok().unwrap(); + let mut result : CmpResult = CmpResult::new(&self.path_a, &self.path_b); + + for item in &map1 { + if map2.contains_key(item.0) { + let item2 = map2.get(item.0).unwrap(); + let hash1 = match item.1.get_hash() { + Hash::Valid { hash } => hash, + Hash::Invalid { error } => error, + }; + let hash2 = match item2.get_hash() { + Hash::Valid { hash } => hash, + Hash::Invalid { error } => error, + }; + if hash1 != hash2 { + result.differs.push(item.0.clone()); + } + } + else { + result.only_in_a.push(item.0.clone()); + } + } + for item in &map2 { + if !map1.contains_key(item.0) { + result.only_in_b.push(item.0.clone()); + } + } + result + + } + fn process_directory(path: &PathBuf, ignore_patterns : &Option>) -> Result, String> { + let files : Vec = WalkDir::new(path) + .into_iter() + .filter_map(|f| f.ok()) + .filter(|f| { + let mut ignore = false; + let file_name = f.file_name().to_str().unwrap(); + if let Some(patters) = ignore_patterns { + ignore = patters.iter() + .any(|patt| { + patt.matches(file_name) + }); + } + f.file_type().is_file() && !ignore + }) + .map(|f| f.path().to_owned()) + .collect(); + let result_map : HashMap = files + .par_iter() + .map(|f| { + let file_hash = Hash::new(f); + (f.strip_prefix(path).unwrap().to_owned(), FileInfo { hash: file_hash }) + }) + .collect(); + Ok(result_map) + } +} + pub struct CmpResult { + pub dir_a : PathBuf, + pub dir_b : PathBuf, pub only_in_a : Vec, pub only_in_b: Vec, pub differs : Vec, } impl CmpResult { - pub fn new() -> CmpResult { - CmpResult { only_in_a: Vec::new(), + pub fn new(dir_a : &PathBuf, dir_b : &PathBuf) -> CmpResult { + CmpResult { dir_a: dir_a.to_owned(), + dir_b: dir_b.to_owned(), + only_in_a: Vec::new(), only_in_b: Vec::new(), differs: Vec::new() } } -} + pub fn format_text(&self, ansi: bool) -> Vec { + let bold = Style::new().bold(); + let bold_underline = bold.underline(); + let mut result : Vec = Vec::new(); + let mut result_plain: Vec = Vec::new(); + + println!(); + if self.only_in_a.is_empty() && self.only_in_b.is_empty() && self.differs.is_empty() { + let message = format!("The directories appear to be the same\n"); + let styled_message = bold. + paint(&message); + result.push(styled_message.to_string()); + result_plain.push(message); + } -pub fn dirdiff(path_a: &PathBuf, path_b: &PathBuf) -> CmpResult { - let path_a_clone = path_a.clone(); - let path_b_clone = path_b.clone(); - - let thread_a = std::thread::spawn(move || { - return process_directory(&path_a_clone); - }); - - let thread_b = std::thread::spawn(move || { - return process_directory(&path_b_clone); - }); - - let map1 : HashMap = thread_a.join().unwrap().ok().unwrap(); - let map2 : HashMap = thread_b.join().unwrap().ok().unwrap(); - let mut result : CmpResult = CmpResult::new(); - - for item in &map1 { - if map2.contains_key(item.0) { - let item2 = map2.get(item.0).unwrap(); - let hash1 = match item.1.get_hash() { - Hash::Valid { hash } => hash, - Hash::Invalid { error } => error, - }; - let hash2 = match item2.get_hash() { - Hash::Valid { hash } => hash, - Hash::Invalid { error } => error, - }; - if hash1 != hash2 { - result.differs.push(item.0.clone()); + if !self.only_in_a.is_empty() { + let message = format!("Files that appear only in {}\n", self.dir_a.to_str().unwrap()); + let styled_message = bold_underline.fg(Yellow) + .paint(&message); + result.push(styled_message.to_string()); + result_plain.push(message); + for item in &self.only_in_a { + let file_message = format!("{}\n", item.to_str().unwrap()); + result.push(file_message.clone()); + result_plain.push(file_message); } + result.push("\n".to_string()); + result_plain.push("\n".to_string()); } - else { - result.only_in_a.push(item.0.clone()); + + if !self.only_in_b.is_empty() { + let message = format!("Files that appear only in {}\n", self.dir_b.to_str().unwrap()); + let styled_message = bold_underline.fg(Yellow) + .paint(&message); + result.push(styled_message.to_string()); + result_plain.push(message); + for item in &self.only_in_b { + let file_message = format!("{}\n", item.to_str().unwrap()); + result.push(file_message.clone()); + result_plain.push(file_message); + } + result.push("\n".to_string()); + result_plain.push("\n".to_string()); } - } - for item in &map2 { - if !map1.contains_key(item.0) { - result.only_in_b.push(item.0.clone()); + + if !self.differs.is_empty() { + let message = format!("Files that differ\n"); + let styled_message = bold_underline.fg(Red) + .paint(&message); + result.push(styled_message.to_string()); + result_plain.push(message); + for item in &self.differs { + let file_message = format!("{}\n", item.to_str().unwrap()); + result.push(file_message.clone()); + result_plain.push(file_message); + } } + if ansi { result } else { result_plain } } - result -} - -fn process_directory(path: &PathBuf) -> Result, String> { - let files : Vec = WalkDir::new(path) - .into_iter() - .filter_map(|f| f.ok()) - .filter(|f| f.file_type().is_file()) - .map(|f| f.path().to_owned()) - .collect(); - let result_map : HashMap = files - .par_iter() - .map(|f| { - let file_hash = Hash::new(f); - (f.strip_prefix(path).unwrap().to_owned(), FileInfo { hash: file_hash }) - }) - .collect(); - Ok(result_map) } diff --git a/src/main.rs b/src/main.rs index 03c806b..0282e0d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,8 @@ use clap::Parser; use core::panic; use std::path::PathBuf; -use ansi_term::{Style, Colour::*}; - +use atty::Stream; +use glob::Pattern; mod dirdiff; #[derive(Parser)] @@ -16,10 +16,13 @@ struct Args { dir_b: PathBuf, #[clap(long = "ignore", value_parser, use_value_delimiter(true), value_delimiter = ' ', num_args=1..)] - ignore_pattern: Option>, + ignore_patterns: Option>, #[clap(long = "ignore-file", value_parser)] ignore_file: Option, + + #[clap(long = "no-colors", value_parser)] + no_colors: bool, } impl Args { @@ -57,46 +60,14 @@ fn main() { Ok(()) => {} }; - let result = dirdiff::dirdiff(&args.dir_a, &args.dir_b); - - let bold = Style::new().bold(); - let bold_underline = bold.underline(); - - println!(); - if result.only_in_a.is_empty() && result.only_in_b.is_empty() && result.differs.is_empty() { - let styled_message = bold. - paint(format!("The directories appear to be the same")); - println!("{}", styled_message); - return; - } - - if !result.only_in_a.is_empty() { - let styled_message = bold_underline.fg(Yellow) - .paint(format!("Files that appear only in {}", args.dir_a.to_str().unwrap())); - println!("{}", styled_message); - for item in &result.only_in_a { - println!("{}", item.to_str().unwrap()); - } - println!(); - } - - if !result.only_in_b.is_empty() { - let styled_message = bold_underline.fg(Yellow) - .paint(format!("Files that appear only in {}", args.dir_b.to_str().unwrap())); - println!("{}", styled_message); - for item in &result.only_in_b { - println!("{}", item.to_str().unwrap()); - } - println!(); - } - - if !result.only_in_b.is_empty() { - let styled_message = bold_underline.fg(Red) - .paint(format!("Files that differ")); - println!("{}", styled_message); - for item in &result.differs { - println!("{}", item.to_str().unwrap()); - } + let dir_comparator = dirdiff::DirCmp::new(&args.dir_a, &args.dir_b, &args.ignore_patterns); + let result = dir_comparator.compare_directories(); + let text = if atty::is(Stream::Stdout) { + result.format_text(!args.no_colors) + } else { + result.format_text(false) + }; + for item in text { + print!("{}", item); } - println!(); }