Skip to content

Commit 3a9b7ae

Browse files
committed
test: add unit tests for attribute filtering
1 parent d761a56 commit 3a9b7ae

1 file changed

Lines changed: 253 additions & 0 deletions

File tree

  • plumbing/git-filter-tree/src

plumbing/git-filter-tree/src/lib.rs

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,4 +439,257 @@ mod tests {
439439
cleanup_test_repo(temp_path);
440440
Ok(())
441441
}
442+
443+
// -----------------------------------------------------------------------
444+
// Helpers and tests for filter_by_attributes
445+
// -----------------------------------------------------------------------
446+
447+
/// Initializes a non-bare repository so that `.gitattributes` written to
448+
/// its working directory are picked up by `repo.get_attr(…)`.
449+
fn setup_attr_test_repo() -> (Repository, PathBuf) {
450+
let thread_id = std::thread::current().id();
451+
let temp_path = std::env::temp_dir().join(format!("git-filter-attr-test-{:?}", thread_id));
452+
let _ = fs::remove_dir_all(&temp_path);
453+
fs::create_dir_all(&temp_path).unwrap();
454+
let repo = Repository::init(&temp_path).unwrap();
455+
(repo, temp_path)
456+
}
457+
458+
fn write_gitattributes(repo_path: &Path, content: &str) {
459+
fs::write(repo_path.join(".gitattributes"), content).unwrap();
460+
}
461+
462+
// --- filter_by_attributes: error cases ---------------------------------
463+
464+
#[test]
465+
fn test_filter_by_attributes_empty_returns_error() {
466+
let (repo, temp_path) = setup_attr_test_repo();
467+
write_gitattributes(&temp_path, "");
468+
469+
let tree = create_test_tree(&repo).unwrap();
470+
let result = repo.filter_by_attributes(&tree, &[]);
471+
assert!(result.is_err());
472+
assert_eq!(
473+
result.unwrap_err().message(),
474+
"at least one attribute is required"
475+
);
476+
477+
cleanup_test_repo(temp_path);
478+
}
479+
480+
// --- filter_by_attributes: single attribute ----------------------------
481+
482+
#[test]
483+
fn test_filter_by_attributes_set_attribute_includes_matching_files() -> Result<(), Error> {
484+
let (repo, temp_path) = setup_attr_test_repo();
485+
// Only .txt files carry the export-ignore attribute.
486+
write_gitattributes(&temp_path, "*.txt export-ignore\n");
487+
488+
let blob = repo.blob(b"content")?;
489+
let mut builder = repo.treebuilder(None)?;
490+
builder.insert("readme.txt", blob, 0o100644)?;
491+
builder.insert("main.rs", blob, 0o100644)?;
492+
builder.insert("data.json", blob, 0o100644)?;
493+
let tree = repo.find_tree(builder.write()?)?;
494+
495+
let filtered = repo.filter_by_attributes(&tree, &["export-ignore"])?;
496+
assert_eq!(filtered.len(), 1);
497+
assert!(filtered.get_name("readme.txt").is_some());
498+
assert!(filtered.get_name("main.rs").is_none());
499+
assert!(filtered.get_name("data.json").is_none());
500+
501+
cleanup_test_repo(temp_path);
502+
Ok(())
503+
}
504+
505+
#[test]
506+
fn test_filter_by_attributes_explicitly_unset_attribute_excluded() -> Result<(), Error> {
507+
let (repo, temp_path) = setup_attr_test_repo();
508+
// .txt gets the attribute; .md explicitly has it unset with `-`.
509+
write_gitattributes(&temp_path, "*.txt custom-attr\n*.md -custom-attr\n");
510+
511+
let blob = repo.blob(b"content")?;
512+
let mut builder = repo.treebuilder(None)?;
513+
builder.insert("readme.txt", blob, 0o100644)?;
514+
builder.insert("notes.md", blob, 0o100644)?;
515+
builder.insert("main.rs", blob, 0o100644)?;
516+
let tree = repo.find_tree(builder.write()?)?;
517+
518+
let filtered = repo.filter_by_attributes(&tree, &["custom-attr"])?;
519+
// .txt is set, .md is explicitly unset, .rs is unspecified
520+
assert_eq!(filtered.len(), 1);
521+
assert!(filtered.get_name("readme.txt").is_some());
522+
assert!(filtered.get_name("notes.md").is_none());
523+
assert!(filtered.get_name("main.rs").is_none());
524+
525+
cleanup_test_repo(temp_path);
526+
Ok(())
527+
}
528+
529+
#[test]
530+
fn test_filter_by_attributes_no_attributes_set_returns_empty_tree() -> Result<(), Error> {
531+
let (repo, temp_path) = setup_attr_test_repo();
532+
// Empty .gitattributes — nothing is attributed.
533+
write_gitattributes(&temp_path, "");
534+
535+
let blob = repo.blob(b"content")?;
536+
let mut builder = repo.treebuilder(None)?;
537+
builder.insert("file.txt", blob, 0o100644)?;
538+
builder.insert("file.rs", blob, 0o100644)?;
539+
let tree = repo.find_tree(builder.write()?)?;
540+
541+
let filtered = repo.filter_by_attributes(&tree, &["export-ignore"])?;
542+
assert_eq!(filtered.len(), 0);
543+
544+
cleanup_test_repo(temp_path);
545+
Ok(())
546+
}
547+
548+
#[test]
549+
fn test_filter_by_attributes_multiple_attributes_all_required() -> Result<(), Error> {
550+
let (repo, temp_path) = setup_attr_test_repo();
551+
// .txt has both attributes; .rs has only one.
552+
write_gitattributes(&temp_path, "*.txt attr-a attr-b\n*.rs attr-a\n");
553+
554+
let blob = repo.blob(b"content")?;
555+
let mut builder = repo.treebuilder(None)?;
556+
builder.insert("file.txt", blob, 0o100644)?;
557+
builder.insert("file.rs", blob, 0o100644)?;
558+
builder.insert("file.md", blob, 0o100644)?;
559+
let tree = repo.find_tree(builder.write()?)?;
560+
561+
// Both attributes must be present for a file to be included.
562+
let filtered = repo.filter_by_attributes(&tree, &["attr-a", "attr-b"])?;
563+
assert_eq!(filtered.len(), 1);
564+
assert!(filtered.get_name("file.txt").is_some());
565+
assert!(filtered.get_name("file.rs").is_none());
566+
assert!(filtered.get_name("file.md").is_none());
567+
568+
cleanup_test_repo(temp_path);
569+
Ok(())
570+
}
571+
572+
#[test]
573+
fn test_filter_by_attributes_attribute_with_value() -> Result<(), Error> {
574+
let (repo, temp_path) = setup_attr_test_repo();
575+
// linguist-language is set to a string value on .rs files.
576+
write_gitattributes(&temp_path, "*.rs linguist-language=Rust\n");
577+
578+
let blob = repo.blob(b"content")?;
579+
let mut builder = repo.treebuilder(None)?;
580+
builder.insert("main.rs", blob, 0o100644)?;
581+
builder.insert("main.py", blob, 0o100644)?;
582+
let tree = repo.find_tree(builder.write()?)?;
583+
584+
// An attribute with any value (including a string) counts as "set".
585+
let filtered = repo.filter_by_attributes(&tree, &["linguist-language"])?;
586+
assert_eq!(filtered.len(), 1);
587+
assert!(filtered.get_name("main.rs").is_some());
588+
assert!(filtered.get_name("main.py").is_none());
589+
590+
cleanup_test_repo(temp_path);
591+
Ok(())
592+
}
593+
594+
#[test]
595+
fn test_filter_by_attributes_all_files_match() -> Result<(), Error> {
596+
let (repo, temp_path) = setup_attr_test_repo();
597+
// Wildcard rule sets the attribute on every file.
598+
write_gitattributes(&temp_path, "* generated\n");
599+
600+
let blob = repo.blob(b"content")?;
601+
let mut builder = repo.treebuilder(None)?;
602+
builder.insert("a.txt", blob, 0o100644)?;
603+
builder.insert("b.rs", blob, 0o100644)?;
604+
builder.insert("c.md", blob, 0o100644)?;
605+
let tree = repo.find_tree(builder.write()?)?;
606+
607+
let filtered = repo.filter_by_attributes(&tree, &["generated"])?;
608+
assert_eq!(filtered.len(), 3);
609+
610+
cleanup_test_repo(temp_path);
611+
Ok(())
612+
}
613+
614+
#[test]
615+
fn test_filter_by_attributes_nested_tree_filters_recursively() -> Result<(), Error> {
616+
let (repo, temp_path) = setup_attr_test_repo();
617+
// Only .proto files carry the attribute.
618+
write_gitattributes(&temp_path, "*.proto linguist-generated\n");
619+
620+
let blob = repo.blob(b"content")?;
621+
622+
// src/api.proto and src/main.rs
623+
let mut src_builder = repo.treebuilder(None)?;
624+
src_builder.insert("api.proto", blob, 0o100644)?;
625+
src_builder.insert("main.rs", blob, 0o100644)?;
626+
let src_oid = src_builder.write()?;
627+
628+
let mut root_builder = repo.treebuilder(None)?;
629+
root_builder.insert("src", src_oid, 0o040000)?;
630+
root_builder.insert("README.md", blob, 0o100644)?;
631+
let tree = repo.find_tree(root_builder.write()?)?;
632+
633+
let filtered = repo.filter_by_attributes(&tree, &["linguist-generated"])?;
634+
635+
// Top-level README.md must be gone; src/ must survive because it has
636+
// at least one matching descendant.
637+
assert_eq!(filtered.len(), 1);
638+
assert!(filtered.get_name("src").is_some());
639+
assert!(filtered.get_name("README.md").is_none());
640+
641+
let src_entry = filtered.get_name("src").unwrap();
642+
let src_tree = repo.find_tree(src_entry.id())?;
643+
assert_eq!(src_tree.len(), 1);
644+
assert!(src_tree.get_name("api.proto").is_some());
645+
assert!(src_tree.get_name("main.rs").is_none());
646+
647+
cleanup_test_repo(temp_path);
648+
Ok(())
649+
}
650+
651+
#[test]
652+
fn test_filter_by_attributes_empty_tree_stays_empty() -> Result<(), Error> {
653+
let (repo, temp_path) = setup_attr_test_repo();
654+
write_gitattributes(&temp_path, "* export-ignore\n");
655+
656+
let tree = repo.find_tree(repo.treebuilder(None)?.write()?)?;
657+
assert_eq!(tree.len(), 0);
658+
659+
let filtered = repo.filter_by_attributes(&tree, &["export-ignore"])?;
660+
assert_eq!(filtered.len(), 0);
661+
662+
cleanup_test_repo(temp_path);
663+
Ok(())
664+
}
665+
666+
#[test]
667+
fn test_filter_by_attributes_subdirectory_excluded_when_all_children_unmatched()
668+
-> Result<(), Error> {
669+
let (repo, temp_path) = setup_attr_test_repo();
670+
// Only .txt files match; the `docs/` sub-tree contains only .md files.
671+
write_gitattributes(&temp_path, "*.txt export-ignore\n");
672+
673+
let blob = repo.blob(b"content")?;
674+
675+
let mut docs_builder = repo.treebuilder(None)?;
676+
docs_builder.insert("guide.md", blob, 0o100644)?;
677+
docs_builder.insert("api.md", blob, 0o100644)?;
678+
let docs_oid = docs_builder.write()?;
679+
680+
let mut root_builder = repo.treebuilder(None)?;
681+
root_builder.insert("docs", docs_oid, 0o040000)?;
682+
root_builder.insert("notes.txt", blob, 0o100644)?;
683+
let tree = repo.find_tree(root_builder.write()?)?;
684+
685+
let filtered = repo.filter_by_attributes(&tree, &["export-ignore"])?;
686+
687+
// `docs/` should be pruned entirely because none of its children matched.
688+
assert_eq!(filtered.len(), 1);
689+
assert!(filtered.get_name("notes.txt").is_some());
690+
assert!(filtered.get_name("docs").is_none());
691+
692+
cleanup_test_repo(temp_path);
693+
Ok(())
694+
}
442695
}

0 commit comments

Comments
 (0)