use crate::Result;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::process::Command;
#[derive(Debug, clap::Args)]
pub struct CheckCaseConflict {
#[clap(required = true)]
pub files: Vec<PathBuf>,
}
impl CheckCaseConflict {
pub async fn run(&self) -> Result<()> {
let repo_files = get_repo_files()?;
let all_files: Vec<PathBuf> = repo_files
.into_iter()
.chain(self.files.iter().cloned())
.collect::<HashSet<_>>()
.into_iter()
.collect();
let conflicts = find_case_conflicts(&all_files);
if !conflicts.is_empty() {
for conflict_group in conflicts {
println!("Case conflict:");
for file in conflict_group {
println!(" {}", file.display());
}
}
return Err(eyre::eyre!("Case conflicts found in file paths"));
}
Ok(())
}
}
fn get_repo_files() -> Result<Vec<PathBuf>> {
let output = Command::new("git").args(["ls-files"]).output();
match output {
Ok(output) if output.status.success() => {
let files = String::from_utf8_lossy(&output.stdout)
.lines()
.map(PathBuf::from)
.collect();
Ok(files)
}
_ => {
Ok(vec![])
}
}
}
fn find_case_conflicts(files: &[PathBuf]) -> Vec<Vec<PathBuf>> {
let mut case_map: HashMap<String, Vec<PathBuf>> = HashMap::new();
for file in files {
let lowercase = file.to_string_lossy().to_lowercase();
case_map.entry(lowercase).or_default().push(file.clone());
}
case_map
.into_values()
.filter(|group| group.len() > 1)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_no_conflicts() {
let files = vec![
PathBuf::from("file1.txt"),
PathBuf::from("file2.txt"),
PathBuf::from("dir/file3.txt"),
];
let conflicts = find_case_conflicts(&files);
assert!(conflicts.is_empty());
}
#[test]
fn test_simple_conflict() {
let files = vec![PathBuf::from("README.md"), PathBuf::from("readme.md")];
let conflicts = find_case_conflicts(&files);
assert_eq!(conflicts.len(), 1);
assert_eq!(conflicts[0].len(), 2);
}
#[test]
fn test_multiple_conflicts() {
let files = vec![
PathBuf::from("File1.txt"),
PathBuf::from("file1.txt"),
PathBuf::from("FILE1.TXT"),
PathBuf::from("Other.md"),
PathBuf::from("other.md"),
];
let conflicts = find_case_conflicts(&files);
assert_eq!(conflicts.len(), 2);
let sizes: Vec<usize> = conflicts.iter().map(|g| g.len()).collect();
assert!(sizes.contains(&3));
assert!(sizes.contains(&2));
}
#[test]
fn test_path_with_directory() {
let files = vec![
PathBuf::from("src/Main.rs"),
PathBuf::from("src/main.rs"),
PathBuf::from("src/lib.rs"),
];
let conflicts = find_case_conflicts(&files);
assert_eq!(conflicts.len(), 1);
assert_eq!(conflicts[0].len(), 2);
}
#[test]
fn test_no_conflict_different_dirs() {
let files = vec![
PathBuf::from("dir1/file.txt"),
PathBuf::from("dir2/file.txt"),
];
let conflicts = find_case_conflicts(&files);
assert!(conflicts.is_empty());
}
#[test]
fn test_conflict_different_extensions() {
let files = vec![PathBuf::from("File.txt"), PathBuf::from("file.TXT")];
let conflicts = find_case_conflicts(&files);
assert_eq!(conflicts.len(), 1);
assert_eq!(conflicts[0].len(), 2);
}
}