pub mod exe;
use std::path::Path;
pub use git2::{Error, Repository};
use globset::GlobSetBuilder;
pub trait FilterTree {
fn filter_by_patterns<'a>(
&'a self,
tree: &'a git2::Tree<'a>,
patterns: &[&str], ) -> Result<git2::Tree<'a>, Error>;
fn filter_by_attributes<'a>(
&'a self,
tree: &'a git2::Tree<'a>,
attributes: &[&str],
) -> Result<git2::Tree<'a>, Error>;
fn filter_by_predicate<'a, F>(
&'a self,
tree: &'a git2::Tree<'a>,
predicate: F,
) -> Result<git2::Tree<'a>, Error>
where
F: Fn(&git2::Repository, &Path) -> bool;
}
impl FilterTree for git2::Repository {
fn filter_by_patterns<'a>(
&'a self,
tree: &'a git2::Tree<'a>,
patterns: &[&str],
) -> Result<git2::Tree<'a>, Error> {
if patterns.is_empty() {
return Err(Error::from_str("At least one pattern is required"));
}
let mut glob_builder = GlobSetBuilder::new();
for pattern in patterns {
let normalized: String;
let pat = if pattern.ends_with('/') {
normalized = format!("{}**", pattern);
normalized.as_str()
} else {
pattern
};
let glob = globset::Glob::new(pat)
.map_err(|e| Error::from_str(&format!("Invalid pattern '{}': {}", pattern, e)))?;
glob_builder.add(glob);
}
let matcher = glob_builder
.build()
.map_err(|e| Error::from_str(&e.to_string()))?;
filter_tree_recursive(self, tree, None, &|_repo, path| matcher.is_match(path))
}
fn filter_by_predicate<'a, F>(
&'a self,
tree: &'a git2::Tree<'a>,
predicate: F,
) -> Result<git2::Tree<'a>, Error>
where
F: Fn(&git2::Repository, &Path) -> bool,
{
filter_tree_recursive(self, tree, None, &predicate)
}
fn filter_by_attributes<'a>(
&'a self,
tree: &'a git2::Tree<'a>,
attributes: &[&str],
) -> Result<git2::Tree<'a>, Error> {
if attributes.is_empty() {
return Err(git2::Error::from_str("at least one attribute is required"));
}
filter_tree_recursive(self, tree, None, &|repo, path| {
for attribute in attributes {
match repo.get_attr(path, attribute, git2::AttrCheckFlags::FILE_THEN_INDEX) {
Ok(Some(value)) => {
let value = git2::AttrValue::from_string(Some(value));
match value {
git2::AttrValue::Unspecified => return false,
git2::AttrValue::False => return false,
_ => {}
}
}
Ok(None) => return false,
Err(_) => return false,
}
}
true
})
}
}
fn filter_tree_recursive<'a, F>(
repo: &'a Repository,
tree: &'a git2::Tree<'a>,
prefix: Option<&str>,
predicate: &F,
) -> Result<git2::Tree<'a>, Error>
where
F: Fn(&Repository, &Path) -> bool,
{
let mut builder = repo.treebuilder(None)?;
for entry in tree.iter() {
let Some(name) = entry.name() else {
return Err(Error::from_str("name has invalid UTF-8"));
};
let git_path = match prefix {
Some(dir) => format!("{}/{}", dir, name),
None => name.to_string(),
};
let full_path = Path::new(&git_path);
match entry.kind() {
Some(git2::ObjectType::Blob) => {
if predicate(repo, &full_path) {
builder.insert(name, entry.id(), entry.filemode())?;
}
}
Some(git2::ObjectType::Tree) => {
let subtree = entry.to_object(repo)?.peel_to_tree()?;
let filtered_subtree =
filter_tree_recursive(repo, &subtree, Some(&git_path), predicate)?;
if !filtered_subtree.is_empty() {
builder.insert(name, filtered_subtree.id(), entry.filemode())?;
}
}
_ => continue,
}
}
let tree_oid = builder.write()?;
repo.find_tree(tree_oid)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;
fn setup_test_repo() -> (Repository, PathBuf) {
let thread_id = std::thread::current().id();
let temp_path = std::env::temp_dir().join(format!("git-filter-tree-test-{:?}", thread_id));
let _ = fs::remove_dir_all(&temp_path);
fs::create_dir_all(&temp_path).unwrap();
let repo = Repository::init_bare(&temp_path).unwrap();
(repo, temp_path)
}
fn cleanup_test_repo(path: PathBuf) {
let _ = fs::remove_dir_all(path);
}
fn create_test_tree<'a>(repo: &'a Repository) -> Result<git2::Tree<'a>, Error> {
let mut tree_builder = repo.treebuilder(None)?;
let blob1 = repo.blob(b"content1")?;
let blob2 = repo.blob(b"content2")?;
let blob3 = repo.blob(b"content3")?;
tree_builder.insert("file1.txt", blob1, 0o100644)?;
tree_builder.insert("file2.rs", blob2, 0o100644)?;
tree_builder.insert("test.md", blob3, 0o100644)?;
let tree_oid = tree_builder.write()?;
repo.find_tree(tree_oid)
}
#[test]
fn test_filter_single_pattern() -> Result<(), Error> {
let (repo, temp_path) = setup_test_repo();
let tree = create_test_tree(&repo)?;
assert_eq!(tree.len(), 3);
let filtered = repo.filter_by_patterns(&tree, &["*.txt"])?;
assert_eq!(filtered.len(), 1);
assert!(filtered.get_name("file1.txt").is_some());
assert!(filtered.get_name("file2.rs").is_none());
assert!(filtered.get_name("test.md").is_none());
cleanup_test_repo(temp_path);
Ok(())
}
#[test]
fn test_filter_multiple_patterns() -> Result<(), Error> {
let (repo, temp_path) = setup_test_repo();
let tree = create_test_tree(&repo)?;
let filtered = repo.filter_by_patterns(&tree, &["*.txt", "*.rs"])?;
assert_eq!(filtered.len(), 2);
assert!(filtered.get_name("file1.txt").is_some());
assert!(filtered.get_name("file2.rs").is_some());
assert!(filtered.get_name("test.md").is_none());
cleanup_test_repo(temp_path);
Ok(())
}
#[test]
fn test_filter_exact_match() -> Result<(), Error> {
let (repo, temp_path) = setup_test_repo();
let tree = create_test_tree(&repo)?;
let filtered = repo.filter_by_patterns(&tree, &["file1.txt"])?;
assert_eq!(filtered.len(), 1);
assert!(filtered.get_name("file1.txt").is_some());
cleanup_test_repo(temp_path);
Ok(())
}
#[test]
fn test_filter_wildcard_patterns() -> Result<(), Error> {
let (repo, temp_path) = setup_test_repo();
let tree = create_test_tree(&repo)?;
let filtered = repo.filter_by_patterns(&tree, &["file*"])?;
assert_eq!(filtered.len(), 2);
assert!(filtered.get_name("file1.txt").is_some());
assert!(filtered.get_name("file2.rs").is_some());
assert!(filtered.get_name("test.md").is_none());
cleanup_test_repo(temp_path);
Ok(())
}
#[test]
fn test_filter_no_matches() -> Result<(), Error> {
let (repo, temp_path) = setup_test_repo();
let tree = create_test_tree(&repo)?;
let filtered = repo.filter_by_patterns(&tree, &["*.nonexistent"])?;
assert_eq!(filtered.len(), 0);
cleanup_test_repo(temp_path);
Ok(())
}
#[test]
fn test_filter_all_matches() -> Result<(), Error> {
let (repo, temp_path) = setup_test_repo();
let tree = create_test_tree(&repo)?;
let filtered = repo.filter_by_patterns(&tree, &["*"])?;
assert_eq!(filtered.len(), 3);
cleanup_test_repo(temp_path);
Ok(())
}
#[test]
fn test_filter_empty_patterns_error() {
let (repo, temp_path) = setup_test_repo();
let tree = create_test_tree(&repo).unwrap();
let result = repo.filter_by_patterns(&tree, &[]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().message(),
"At least one pattern is required"
);
cleanup_test_repo(temp_path);
}
#[test]
fn test_filter_invalid_pattern_error() {
let (repo, temp_path) = setup_test_repo();
let tree = create_test_tree(&repo).unwrap();
let result = repo.filter_by_patterns(&tree, &["[invalid"]);
assert!(result.is_err());
cleanup_test_repo(temp_path);
}
#[test]
fn test_filter_with_nested_tree() -> Result<(), Error> {
let (repo, temp_path) = setup_test_repo();
let mut tree_builder = repo.treebuilder(None)?;
let mut subtree_builder = repo.treebuilder(None)?;
let blob = repo.blob(b"nested content")?;
subtree_builder.insert("nested.txt", blob, 0o100644)?;
let subtree_oid = subtree_builder.write()?;
let blob1 = repo.blob(b"content1")?;
tree_builder.insert("file1.txt", blob1, 0o100644)?;
tree_builder.insert("subdir", subtree_oid, 0o040000)?;
let tree_oid = tree_builder.write()?;
let tree = repo.find_tree(tree_oid)?;
let filtered = repo.filter_by_patterns(&tree, &["*"])?;
assert_eq!(filtered.len(), 2);
cleanup_test_repo(temp_path);
Ok(())
}
#[test]
fn test_filter_preserves_empty_tree() -> Result<(), Error> {
let (repo, temp_path) = setup_test_repo();
let tree_builder = repo.treebuilder(None)?;
let tree_oid = tree_builder.write()?;
let tree = repo.find_tree(tree_oid)?;
assert_eq!(tree.len(), 0);
let filtered = repo.filter_by_patterns(&tree, &["*"])?;
assert_eq!(filtered.len(), 0);
cleanup_test_repo(temp_path);
Ok(())
}
#[test]
fn test_filter_case_sensitive() -> Result<(), Error> {
let (repo, temp_path) = setup_test_repo();
let mut tree_builder = repo.treebuilder(None)?;
let blob1 = repo.blob(b"content1")?;
let blob2 = repo.blob(b"content2")?;
tree_builder.insert("File.txt", blob1, 0o100644)?;
tree_builder.insert("file.txt", blob2, 0o100644)?;
let tree_oid = tree_builder.write()?;
let tree = repo.find_tree(tree_oid)?;
let filtered = repo.filter_by_patterns(&tree, &["file.txt"])?;
assert_eq!(filtered.len(), 1);
assert!(filtered.get_name("file.txt").is_some());
cleanup_test_repo(temp_path);
Ok(())
}
#[test]
fn test_filter_complex_patterns() -> Result<(), Error> {
let (repo, temp_path) = setup_test_repo();
let mut tree_builder = repo.treebuilder(None)?;
let blob = repo.blob(b"content")?;
tree_builder.insert("test1.txt", blob, 0o100644)?;
tree_builder.insert("test2.rs", blob, 0o100644)?;
tree_builder.insert("data.json", blob, 0o100644)?;
tree_builder.insert("README.md", blob, 0o100644)?;
let tree_oid = tree_builder.write()?;
let tree = repo.find_tree(tree_oid)?;
let filtered = repo.filter_by_patterns(&tree, &["test*", "*.md"])?;
assert_eq!(filtered.len(), 3);
assert!(filtered.get_name("test1.txt").is_some());
assert!(filtered.get_name("test2.rs").is_some());
assert!(filtered.get_name("README.md").is_some());
assert!(filtered.get_name("data.json").is_none());
cleanup_test_repo(temp_path);
Ok(())
}
#[test]
fn test_filter_trailing_slash_matches_directory_contents() -> Result<(), Error> {
let (repo, temp_path) = setup_test_repo();
let blob = repo.blob(b"content")?;
let mut src_builder = repo.treebuilder(None)?;
src_builder.insert("lib.rs", blob, 0o100644)?;
let src_oid = src_builder.write()?;
let mut pyo3_builder = repo.treebuilder(None)?;
pyo3_builder.insert("Cargo.toml", blob, 0o100644)?;
pyo3_builder.insert("src", src_oid, 0o040000)?;
let pyo3_oid = pyo3_builder.write()?;
let mut root_builder = repo.treebuilder(None)?;
root_builder.insert("pyo3", pyo3_oid, 0o040000)?;
root_builder.insert("README.md", blob, 0o100644)?;
let root_oid = root_builder.write()?;
let tree = repo.find_tree(root_oid)?;
let filtered = repo.filter_by_patterns(&tree, &["pyo3/"])?;
assert_eq!(filtered.len(), 1, "only the pyo3 dir should remain");
assert!(filtered.get_name("pyo3").is_some());
assert!(filtered.get_name("README.md").is_none());
let pyo3_entry = filtered.get_name("pyo3").unwrap();
let pyo3_tree = repo.find_tree(pyo3_entry.id())?;
assert!(pyo3_tree.get_name("Cargo.toml").is_some());
assert!(pyo3_tree.get_name("src").is_some());
cleanup_test_repo(temp_path);
Ok(())
}
fn setup_attr_test_repo() -> (Repository, PathBuf) {
let thread_id = std::thread::current().id();
let temp_path = std::env::temp_dir().join(format!("git-filter-attr-test-{:?}", thread_id));
let _ = fs::remove_dir_all(&temp_path);
fs::create_dir_all(&temp_path).unwrap();
let repo = Repository::init(&temp_path).unwrap();
(repo, temp_path)
}
fn write_gitattributes(repo_path: &Path, content: &str) {
fs::write(repo_path.join(".gitattributes"), content).unwrap();
}
#[test]
fn test_filter_by_attributes_empty_returns_error() {
let (repo, temp_path) = setup_attr_test_repo();
write_gitattributes(&temp_path, "");
let tree = create_test_tree(&repo).unwrap();
let result = repo.filter_by_attributes(&tree, &[]);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().message(),
"at least one attribute is required"
);
cleanup_test_repo(temp_path);
}
#[test]
fn test_filter_by_attributes_set_attribute_includes_matching_files() -> Result<(), Error> {
let (repo, temp_path) = setup_attr_test_repo();
write_gitattributes(&temp_path, "*.txt export-ignore\n");
let blob = repo.blob(b"content")?;
let mut builder = repo.treebuilder(None)?;
builder.insert("readme.txt", blob, 0o100644)?;
builder.insert("main.rs", blob, 0o100644)?;
builder.insert("data.json", blob, 0o100644)?;
let tree = repo.find_tree(builder.write()?)?;
let filtered = repo.filter_by_attributes(&tree, &["export-ignore"])?;
assert_eq!(filtered.len(), 1);
assert!(filtered.get_name("readme.txt").is_some());
assert!(filtered.get_name("main.rs").is_none());
assert!(filtered.get_name("data.json").is_none());
cleanup_test_repo(temp_path);
Ok(())
}
#[test]
fn test_filter_by_attributes_explicitly_unset_attribute_excluded() -> Result<(), Error> {
let (repo, temp_path) = setup_attr_test_repo();
write_gitattributes(&temp_path, "*.txt custom-attr\n*.md -custom-attr\n");
let blob = repo.blob(b"content")?;
let mut builder = repo.treebuilder(None)?;
builder.insert("readme.txt", blob, 0o100644)?;
builder.insert("notes.md", blob, 0o100644)?;
builder.insert("main.rs", blob, 0o100644)?;
let tree = repo.find_tree(builder.write()?)?;
let filtered = repo.filter_by_attributes(&tree, &["custom-attr"])?;
assert_eq!(filtered.len(), 1);
assert!(filtered.get_name("readme.txt").is_some());
assert!(filtered.get_name("notes.md").is_none());
assert!(filtered.get_name("main.rs").is_none());
cleanup_test_repo(temp_path);
Ok(())
}
#[test]
fn test_filter_by_attributes_no_attributes_set_returns_empty_tree() -> Result<(), Error> {
let (repo, temp_path) = setup_attr_test_repo();
write_gitattributes(&temp_path, "");
let blob = repo.blob(b"content")?;
let mut builder = repo.treebuilder(None)?;
builder.insert("file.txt", blob, 0o100644)?;
builder.insert("file.rs", blob, 0o100644)?;
let tree = repo.find_tree(builder.write()?)?;
let filtered = repo.filter_by_attributes(&tree, &["export-ignore"])?;
assert_eq!(filtered.len(), 0);
cleanup_test_repo(temp_path);
Ok(())
}
#[test]
fn test_filter_by_attributes_multiple_attributes_all_required() -> Result<(), Error> {
let (repo, temp_path) = setup_attr_test_repo();
write_gitattributes(&temp_path, "*.txt attr-a attr-b\n*.rs attr-a\n");
let blob = repo.blob(b"content")?;
let mut builder = repo.treebuilder(None)?;
builder.insert("file.txt", blob, 0o100644)?;
builder.insert("file.rs", blob, 0o100644)?;
builder.insert("file.md", blob, 0o100644)?;
let tree = repo.find_tree(builder.write()?)?;
let filtered = repo.filter_by_attributes(&tree, &["attr-a", "attr-b"])?;
assert_eq!(filtered.len(), 1);
assert!(filtered.get_name("file.txt").is_some());
assert!(filtered.get_name("file.rs").is_none());
assert!(filtered.get_name("file.md").is_none());
cleanup_test_repo(temp_path);
Ok(())
}
#[test]
fn test_filter_by_attributes_attribute_with_value() -> Result<(), Error> {
let (repo, temp_path) = setup_attr_test_repo();
write_gitattributes(&temp_path, "*.rs linguist-language=Rust\n");
let blob = repo.blob(b"content")?;
let mut builder = repo.treebuilder(None)?;
builder.insert("main.rs", blob, 0o100644)?;
builder.insert("main.py", blob, 0o100644)?;
let tree = repo.find_tree(builder.write()?)?;
let filtered = repo.filter_by_attributes(&tree, &["linguist-language"])?;
assert_eq!(filtered.len(), 1);
assert!(filtered.get_name("main.rs").is_some());
assert!(filtered.get_name("main.py").is_none());
cleanup_test_repo(temp_path);
Ok(())
}
#[test]
fn test_filter_by_attributes_all_files_match() -> Result<(), Error> {
let (repo, temp_path) = setup_attr_test_repo();
write_gitattributes(&temp_path, "* generated\n");
let blob = repo.blob(b"content")?;
let mut builder = repo.treebuilder(None)?;
builder.insert("a.txt", blob, 0o100644)?;
builder.insert("b.rs", blob, 0o100644)?;
builder.insert("c.md", blob, 0o100644)?;
let tree = repo.find_tree(builder.write()?)?;
let filtered = repo.filter_by_attributes(&tree, &["generated"])?;
assert_eq!(filtered.len(), 3);
cleanup_test_repo(temp_path);
Ok(())
}
#[test]
fn test_filter_by_attributes_nested_tree_filters_recursively() -> Result<(), Error> {
let (repo, temp_path) = setup_attr_test_repo();
write_gitattributes(&temp_path, "*.proto linguist-generated\n");
let blob = repo.blob(b"content")?;
let mut src_builder = repo.treebuilder(None)?;
src_builder.insert("api.proto", blob, 0o100644)?;
src_builder.insert("main.rs", blob, 0o100644)?;
let src_oid = src_builder.write()?;
let mut root_builder = repo.treebuilder(None)?;
root_builder.insert("src", src_oid, 0o040000)?;
root_builder.insert("README.md", blob, 0o100644)?;
let tree = repo.find_tree(root_builder.write()?)?;
let filtered = repo.filter_by_attributes(&tree, &["linguist-generated"])?;
assert_eq!(filtered.len(), 1);
assert!(filtered.get_name("src").is_some());
assert!(filtered.get_name("README.md").is_none());
let src_entry = filtered.get_name("src").unwrap();
let src_tree = repo.find_tree(src_entry.id())?;
assert_eq!(src_tree.len(), 1);
assert!(src_tree.get_name("api.proto").is_some());
assert!(src_tree.get_name("main.rs").is_none());
cleanup_test_repo(temp_path);
Ok(())
}
#[test]
fn test_filter_by_attributes_empty_tree_stays_empty() -> Result<(), Error> {
let (repo, temp_path) = setup_attr_test_repo();
write_gitattributes(&temp_path, "* export-ignore\n");
let tree = repo.find_tree(repo.treebuilder(None)?.write()?)?;
assert_eq!(tree.len(), 0);
let filtered = repo.filter_by_attributes(&tree, &["export-ignore"])?;
assert_eq!(filtered.len(), 0);
cleanup_test_repo(temp_path);
Ok(())
}
#[test]
fn test_filter_by_attributes_subdirectory_excluded_when_all_children_unmatched()
-> Result<(), Error> {
let (repo, temp_path) = setup_attr_test_repo();
write_gitattributes(&temp_path, "*.txt export-ignore\n");
let blob = repo.blob(b"content")?;
let mut docs_builder = repo.treebuilder(None)?;
docs_builder.insert("guide.md", blob, 0o100644)?;
docs_builder.insert("api.md", blob, 0o100644)?;
let docs_oid = docs_builder.write()?;
let mut root_builder = repo.treebuilder(None)?;
root_builder.insert("docs", docs_oid, 0o040000)?;
root_builder.insert("notes.txt", blob, 0o100644)?;
let tree = repo.find_tree(root_builder.write()?)?;
let filtered = repo.filter_by_attributes(&tree, &["export-ignore"])?;
assert_eq!(filtered.len(), 1);
assert!(filtered.get_name("notes.txt").is_some());
assert!(filtered.get_name("docs").is_none());
cleanup_test_repo(temp_path);
Ok(())
}
#[test]
fn test_filter_by_predicate_always_false_returns_empty_tree() -> Result<(), Error> {
let (repo, temp_path) = setup_test_repo();
let tree = create_test_tree(&repo)?;
let filtered = repo.filter_by_predicate(&tree, |_repo, _path| false)?;
assert_eq!(filtered.len(), 0);
cleanup_test_repo(temp_path);
Ok(())
}
#[test]
fn test_filter_by_predicate_always_true_returns_full_tree() -> Result<(), Error> {
let (repo, temp_path) = setup_test_repo();
let tree = create_test_tree(&repo)?;
let filtered = repo.filter_by_predicate(&tree, |_repo, _path| true)?;
assert_eq!(filtered.len(), tree.len());
cleanup_test_repo(temp_path);
Ok(())
}
#[test]
fn test_filter_by_predicate_matches_on_path() -> Result<(), Error> {
let (repo, temp_path) = setup_test_repo();
let tree = create_test_tree(&repo)?;
let filtered = repo.filter_by_predicate(&tree, |_repo, path| {
path.to_str().is_some_and(|s| s.contains("file"))
})?;
assert_eq!(filtered.len(), 2);
assert!(filtered.get_name("file1.txt").is_some());
assert!(filtered.get_name("file2.rs").is_some());
assert!(filtered.get_name("test.md").is_none());
cleanup_test_repo(temp_path);
Ok(())
}
#[test]
fn test_filter_by_predicate_receives_full_nested_path() -> Result<(), Error> {
let (repo, temp_path) = setup_test_repo();
let blob = repo.blob(b"content")?;
let mut sub_builder = repo.treebuilder(None)?;
sub_builder.insert("deep.rs", blob, 0o100644)?;
sub_builder.insert("deep.txt", blob, 0o100644)?;
let sub_oid = sub_builder.write()?;
let mut root_builder = repo.treebuilder(None)?;
root_builder.insert("top.rs", blob, 0o100644)?;
root_builder.insert("src", sub_oid, 0o040000)?;
let tree = repo.find_tree(root_builder.write()?)?;
let seen_paths = std::cell::RefCell::new(Vec::new());
let _ = repo.filter_by_predicate(&tree, |_repo, path| {
seen_paths
.borrow_mut()
.push(path.to_str().unwrap().to_string());
true
});
let seen_paths = seen_paths.into_inner();
assert!(seen_paths.contains(&"top.rs".to_string()));
assert!(seen_paths.contains(&"src/deep.rs".to_string()));
assert!(seen_paths.contains(&"src/deep.txt".to_string()));
cleanup_test_repo(temp_path);
Ok(())
}
#[test]
fn test_filter_by_predicate_prunes_subtree_when_no_descendants_match() -> Result<(), Error> {
let (repo, temp_path) = setup_test_repo();
let blob = repo.blob(b"content")?;
let mut sub_builder = repo.treebuilder(None)?;
sub_builder.insert("a.txt", blob, 0o100644)?;
sub_builder.insert("b.txt", blob, 0o100644)?;
let sub_oid = sub_builder.write()?;
let mut root_builder = repo.treebuilder(None)?;
root_builder.insert("keep.rs", blob, 0o100644)?;
root_builder.insert("docs", sub_oid, 0o040000)?;
let tree = repo.find_tree(root_builder.write()?)?;
let filtered = repo.filter_by_predicate(&tree, |_repo, path| {
path.extension().is_some_and(|e| e == "rs")
})?;
assert_eq!(filtered.len(), 1);
assert!(filtered.get_name("keep.rs").is_some());
assert!(filtered.get_name("docs").is_none());
cleanup_test_repo(temp_path);
Ok(())
}
#[test]
fn test_filter_by_predicate_empty_tree_stays_empty() -> Result<(), Error> {
let (repo, temp_path) = setup_test_repo();
let tree = repo.find_tree(repo.treebuilder(None)?.write()?)?;
assert_eq!(tree.len(), 0);
let filtered = repo.filter_by_predicate(&tree, |_repo, _path| true)?;
assert_eq!(filtered.len(), 0);
cleanup_test_repo(temp_path);
Ok(())
}
}