use crate::interner::StringInterner;
use crate::types::{ChangeType, ChangedFile, InternedString};
use std::collections::HashSet;
pub struct DirNameExtractor<'a> {
interner: &'a StringInterner,
}
impl<'a> DirNameExtractor<'a> {
pub fn new(interner: &'a StringInterner) -> Self {
Self { interner }
}
pub fn extract(
&self,
files: &[ChangedFile],
indices: &[u32],
max_depth: Option<u32>,
exclude_current_dir: bool,
include_files: Option<&[&str]>,
deleted_only_dirs: bool,
) -> Vec<InternedString> {
let mut dirs: HashSet<InternedString> = HashSet::new();
if deleted_only_dirs {
return self.extract_deleted_only_dirs(files, indices, max_depth);
}
for &idx in indices {
let file = &files[idx as usize];
if let Some(path) = self.interner.resolve(file.path) {
if let Some(patterns) = include_files {
let matches = patterns.iter().any(|p| path.contains(p));
if !matches {
continue;
}
}
if let Some(dir) = self.extract_dir(path, max_depth, exclude_current_dir) {
dirs.insert(self.interner.intern(dir));
}
}
}
dirs.into_iter().collect()
}
fn extract_dir<'b>(
&self,
path: &'b str,
max_depth: Option<u32>,
exclude_current_dir: bool,
) -> Option<&'b str> {
let dir = match path.rfind('/') {
Some(pos) => &path[..pos],
None => {
if exclude_current_dir {
return None;
}
return Some(".");
}
};
if exclude_current_dir && dir == "." {
return None;
}
if let Some(max_depth) = max_depth {
let depth = dir.matches('/').count() as u32 + 1;
if depth > max_depth {
let mut slash_count = 0;
for (i, ch) in dir.char_indices() {
if ch == '/' {
slash_count += 1;
if slash_count >= max_depth {
return Some(&dir[..i]);
}
}
}
}
}
Some(dir)
}
fn extract_deleted_only_dirs(
&self,
files: &[ChangedFile],
indices: &[u32],
max_depth: Option<u32>,
) -> Vec<InternedString> {
use std::collections::HashMap;
let mut dir_files: HashMap<String, (usize, usize)> = HashMap::new();
for &idx in indices {
let file = &files[idx as usize];
if let Some(path) = self.interner.resolve(file.path) {
let dir = match path.rfind('/') {
Some(pos) => path[..pos].to_string(),
None => ".".to_string(),
};
let entry = dir_files.entry(dir).or_insert((0, 0));
entry.0 += 1;
if file.change_type == ChangeType::Deleted {
entry.1 += 1;
}
}
}
let mut result = Vec::new();
for (dir, (total, deleted)) in dir_files {
if total == deleted && total > 0 {
let truncated = if let Some(max) = max_depth {
let parts: Vec<&str> = dir.split('/').collect();
if parts.len() > max as usize {
parts[..max as usize].join("/")
} else {
dir
}
} else {
dir
};
result.push(self.interner.intern(&truncated));
}
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::FileOrigin;
fn make_file(interner: &StringInterner, path: &str, change_type: ChangeType) -> ChangedFile {
ChangedFile {
path: interner.intern(path),
change_type,
previous_path: None,
is_symlink: false,
submodule_depth: 0,
origin: FileOrigin::default(),
}
}
#[test]
fn test_extract_basic() {
let interner = StringInterner::new();
let files = vec![
make_file(&interner, "src/main.rs", ChangeType::Modified),
make_file(&interner, "src/lib.rs", ChangeType::Added),
make_file(&interner, "tests/test.rs", ChangeType::Modified),
];
let indices: Vec<u32> = (0..files.len() as u32).collect();
let extractor = DirNameExtractor::new(&interner);
let dirs = extractor.extract(&files, &indices, None, false, None, false);
let dir_names: HashSet<String> = dirs
.iter()
.filter_map(|d| interner.resolve(*d).map(String::from))
.collect();
assert!(dir_names.contains("src"));
assert!(dir_names.contains("tests"));
}
#[test]
fn test_extract_with_depth() {
let interner = StringInterner::new();
let files = vec![make_file(&interner, "a/b/c/file.rs", ChangeType::Modified)];
let indices = vec![0u32];
let extractor = DirNameExtractor::new(&interner);
let dirs = extractor.extract(&files, &indices, Some(2), false, None, false);
let dir_names: Vec<String> = dirs
.iter()
.filter_map(|d| interner.resolve(*d).map(String::from))
.collect();
assert_eq!(dir_names.len(), 1);
assert_eq!(dir_names[0], "a/b");
}
#[test]
fn test_extract_deleted_only_dirs() {
let interner = StringInterner::new();
let files = vec![
make_file(&interner, "old/a.rs", ChangeType::Deleted),
make_file(&interner, "old/b.rs", ChangeType::Deleted),
make_file(&interner, "mixed/a.rs", ChangeType::Deleted),
make_file(&interner, "mixed/b.rs", ChangeType::Modified),
];
let indices: Vec<u32> = (0..files.len() as u32).collect();
let extractor = DirNameExtractor::new(&interner);
let dirs = extractor.extract(&files, &indices, None, false, None, true);
let dir_names: HashSet<String> = dirs
.iter()
.filter_map(|d| interner.resolve(*d).map(String::from))
.collect();
assert!(dir_names.contains("old"));
assert!(!dir_names.contains("mixed"));
}
#[test]
fn test_extract_deleted_only_dirs_with_depth() {
let interner = StringInterner::new();
let files = vec![make_file(&interner, "a/b/c/file.rs", ChangeType::Deleted)];
let indices = vec![0u32];
let extractor = DirNameExtractor::new(&interner);
let dirs = extractor.extract(&files, &indices, Some(2), false, None, true);
let dir_names: Vec<String> = dirs
.iter()
.filter_map(|d| interner.resolve(*d).map(String::from))
.collect();
assert_eq!(dir_names.len(), 1);
assert_eq!(dir_names[0], "a/b");
}
#[test]
fn test_extract_exclude_current_dir() {
let interner = StringInterner::new();
let files = vec![
make_file(&interner, "root_file.rs", ChangeType::Modified),
make_file(&interner, "src/main.rs", ChangeType::Modified),
];
let indices: Vec<u32> = (0..files.len() as u32).collect();
let extractor = DirNameExtractor::new(&interner);
let dirs = extractor.extract(&files, &indices, None, true, None, false);
let dir_names: HashSet<String> = dirs
.iter()
.filter_map(|d| interner.resolve(*d).map(String::from))
.collect();
assert!(!dir_names.contains("."));
assert!(dir_names.contains("src"));
}
#[test]
fn test_extract_include_files_filter() {
let interner = StringInterner::new();
let files = vec![
make_file(&interner, "src/main.rs", ChangeType::Modified),
make_file(&interner, "src/lib.rs", ChangeType::Modified),
make_file(&interner, "tests/test.py", ChangeType::Modified),
];
let indices: Vec<u32> = (0..files.len() as u32).collect();
let extractor = DirNameExtractor::new(&interner);
let include = vec![".rs"];
let dirs = extractor.extract(&files, &indices, None, false, Some(&include), false);
let dir_names: HashSet<String> = dirs
.iter()
.filter_map(|d| interner.resolve(*d).map(String::from))
.collect();
assert!(dir_names.contains("src"));
assert!(!dir_names.contains("tests"));
}
}