Skip to content

Commit

Permalink
Add ignore pattern matching and ansi formatting
Browse files Browse the repository at this point in the history
  • Loading branch information
VaheDanielyan committed Jan 5, 2024
1 parent a736006 commit 31332a8
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 100 deletions.
34 changes: 34 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
200 changes: 144 additions & 56 deletions src/dirdiff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -42,76 +44,162 @@ impl FileInfo {
}
}

pub struct DirCmp {
path_a : PathBuf,
path_b : PathBuf,
ignore_patterns : Option<Vec<Pattern>>,
}

impl DirCmp {
pub fn new(path_a: &PathBuf, path_b: &PathBuf, ignore_patterns : &Option<Vec<Pattern>>) -> 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<PathBuf, FileInfo> = thread_a.join().unwrap().ok().unwrap();
let map2 : HashMap<PathBuf, FileInfo> = 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<Vec<Pattern>>) -> Result<HashMap<PathBuf, FileInfo>, String> {
let files : Vec<PathBuf> = 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<PathBuf, FileInfo> = 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<PathBuf>,
pub only_in_b: Vec<PathBuf>,
pub differs : Vec<PathBuf>,
}

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<String> {
let bold = Style::new().bold();
let bold_underline = bold.underline();
let mut result : Vec<String> = Vec::new();
let mut result_plain: Vec<String> = 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<PathBuf, FileInfo> = thread_a.join().unwrap().ok().unwrap();
let map2 : HashMap<PathBuf, FileInfo> = 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<HashMap<PathBuf, FileInfo>, String> {
let files : Vec<PathBuf> = 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<PathBuf, FileInfo> = 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)
}
59 changes: 15 additions & 44 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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<Vec<String>>,
ignore_patterns: Option<Vec<Pattern>>,

#[clap(long = "ignore-file", value_parser)]
ignore_file: Option<PathBuf>,

#[clap(long = "no-colors", value_parser)]
no_colors: bool,
}

impl Args {
Expand Down Expand Up @@ -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!();
}

0 comments on commit 31332a8

Please sign in to comment.