use super::path_passes_filters; use ignore::gitignore::Gitignore; use virtual_fs::{Stub, VirtualFS}; #[cfg(unix)] #[test] fn test_path_passes_filters_unix() { VirtualFS::test("test_path_passes_filters", |dirs, mut sandbox| { sandbox.mkdir("my_repo"); sandbox.mkdir("my_repo/.git"); sandbox.mkdir("my_repo/.git/refs"); sandbox.mkdir("my_repo/.git/refs/heads"); sandbox.mkdir("my_repo/src"); sandbox.mkdir("my_repo/target"); sandbox.mkdir("my_repo/target/debug"); sandbox.mkdir("outside_of_codebase"); sandbox.with_files(vec![ Stub::EmptyFile("my_repo/README.txt"), Stub::EmptyFile("my_repo/.git/blob.txt"), Stub::EmptyFile("my_repo/.git/HEAD"), Stub::EmptyFile("my_repo/.git/refs/heads/main"), Stub::EmptyFile("my_repo/.git/refs/heads/feature-branch"), Stub::EmptyFile("my_repo/src/main.rs"), Stub::EmptyFile("my_repo/target/debug/a.out"), Stub::EmptyFile("outside_of_codebase/text.txt"), ]); sandbox.with_files(vec![Stub::FileWithContent("my_repo/.gitignore", "target")]); let test_gitignore_entry = dirs.tests().join("my_repo/.gitignore"); let gitignores = vec![Gitignore::new(test_gitignore_entry).0]; // Do NOT ignore a file that does not exist (for deletions) assert!(path_passes_filters( dirs.tests().join("my_repo/does_not_exist.txt").as_path(), &gitignores )); assert!(path_passes_filters( dirs.tests().join("my_repo/src").as_path(), &gitignores )); assert!(path_passes_filters( dirs.tests().join("my_repo/src/main.rs").as_path(), &gitignores )); assert!(path_passes_filters( dirs.tests().join("outside_of_codebase/text.txt").as_path(), &gitignores )); // Allow .git internal files that provide useful signals assert!(path_passes_filters( dirs.tests().join("my_repo/.git/HEAD").as_path(), &gitignores )); assert!(path_passes_filters( dirs.tests().join("my_repo/.git/refs/heads").as_path(), &gitignores )); assert!(path_passes_filters( dirs.tests().join("my_repo/.git/refs/heads/main").as_path(), &gitignores )); assert!(path_passes_filters( dirs.tests() .join("my_repo/.git/refs/heads/feature-branch") .as_path(), &gitignores )); // Non-allowlisted .git/ internal files are filtered out assert!(!path_passes_filters( dirs.tests().join("my_repo/.git/index").as_path(), &gitignores )); assert!(!path_passes_filters( dirs.tests().join("my_repo/.git/blob.txt").as_path(), &gitignores )); // .git directory itself is still ignored assert!(!path_passes_filters( dirs.tests().join("my_repo/.git").as_path(), &gitignores )); // Ignore .gitignored paths and their children. assert!(!path_passes_filters( dirs.tests().join("my_repo/target/").as_path(), &gitignores )); assert!(!path_passes_filters( dirs.tests().join("my_repo/target/debug").as_path(), &gitignores )); assert!(!path_passes_filters( dirs.tests().join("my_repo/target/debug/a.out").as_path(), &gitignores )); // Ignore a .gitignored file that does not exist (for deletions) assert!(!path_passes_filters( &dirs.tests().join("my_repo/target/does_not_exist.txt"), &gitignores )); // Ensure paths are canonicalized before being matched against gitignores. assert!(path_passes_filters( dirs.tests() .join("outside_of_codebase/../my_repo/README.txt") .as_path(), &gitignores )); assert!(!path_passes_filters( dirs.tests() .join("outside_of_codebase/../my_repo/target/debug/a.out") .as_path(), &gitignores )); }); } #[cfg_attr( windows, ignore = "TODO(CODE-312): issue with Gitignore matching on Windows" )] #[cfg(windows)] #[test] fn test_path_passes_filters_windows() { VirtualFS::test("test_path_passes_filters", |dirs, mut sandbox| { sandbox.mkdir("my_repo"); sandbox.mkdir(r"my_repo\.git"); sandbox.mkdir(r"my_repo\.git\refs"); sandbox.mkdir(r"my_repo\.git\refs\heads"); sandbox.mkdir(r"my_repo\src"); sandbox.mkdir(r"my_repo\target"); sandbox.mkdir(r"my_repo\target\debug"); sandbox.mkdir("outside_of_codebase"); sandbox.with_files(vec![ Stub::EmptyFile(r"my_repo\README.txt"), Stub::EmptyFile(r"my_repo\.git\blob.txt"), Stub::EmptyFile(r"my_repo\.git\HEAD"), Stub::EmptyFile(r"my_repo\.git\refs\heads\main"), Stub::EmptyFile(r"my_repo\.git\refs\heads\feature-branch"), Stub::EmptyFile(r"my_repo\src\main.rs"), Stub::EmptyFile(r"my_repo\target\debug\a.out"), Stub::EmptyFile(r"outside_of_codebase\text.txt"), ]); sandbox.with_files(vec![Stub::FileWithContent(r"my_repo\.gitignore", "target")]); let test_gitignore_entry = dirs.tests().join(r"my_repo\.gitignore"); let gitignores = vec![Gitignore::new(test_gitignore_entry).0]; assert!(path_passes_filters( dirs.tests().join(r"my_repo\src").as_path(), &gitignores )); assert!(path_passes_filters( dirs.tests().join(r"my_repo\src\main.rs").as_path(), &gitignores )); assert!(path_passes_filters( dirs.tests().join(r"outside_of_codebase\text.txt").as_path(), &gitignores )); // Allow .git internal files that provide useful signals assert!(path_passes_filters( dirs.tests().join(r"my_repo\.git\HEAD").as_path(), &gitignores )); assert!(path_passes_filters( dirs.tests().join(r"my_repo\.git\refs\heads").as_path(), &gitignores )); assert!(path_passes_filters( dirs.tests().join(r"my_repo\.git\refs\heads\main").as_path(), &gitignores )); assert!(path_passes_filters( dirs.tests() .join(r"my_repo\.git\refs\heads\feature-branch") .as_path(), &gitignores )); // .git directory itself is still ignored assert!(!path_passes_filters( dirs.tests().join(r"my_repo\.git").as_path(), &gitignores )); // Ignore .gitignored paths and their children. assert!(!path_passes_filters( dirs.tests().join(r"my_repo\target").as_path(), &gitignores )); assert!(!path_passes_filters( dirs.tests().join(r"my_repo\target\debug").as_path(), &gitignores )); assert!(!path_passes_filters( dirs.tests().join(r"my_repo\target\debug\a.out").as_path(), &gitignores )); // Ensure paths are canonicalized before being matched against gitignores. assert!(path_passes_filters( dirs.tests() .join(r"outside_of_codebase\..\my_repo\README.txt") .as_path(), &gitignores )); assert!(!path_passes_filters( dirs.tests() .join(r"outside_of_codebase\..\my_repo\target\debug\a.out") .as_path(), &gitignores )); }); } #[test] fn test_git_path_filtering_allowlist() { use super::{is_commit_related_git_file, is_index_lock_file, should_ignore_git_path}; use std::path::Path; // Non-git paths should not be ignored assert!(!should_ignore_git_path(Path::new( "/home/user/project/src/main.rs" ))); assert!(!should_ignore_git_path(Path::new( "/home/user/project/README.md" ))); // .git directory itself should be ignored assert!(should_ignore_git_path(Path::new("/home/user/project/.git"))); // Allowlisted: commit-related files are NOT ignored assert!(!should_ignore_git_path(Path::new( "/home/user/project/.git/HEAD" ))); assert!(!should_ignore_git_path(Path::new( "/home/user/project/.git/refs/heads/main" ))); assert!(!should_ignore_git_path(Path::new( "/home/user/project/.git/refs/heads/feature-branch" ))); // Allowlisted: index.lock is NOT ignored assert!(!should_ignore_git_path(Path::new( "/home/user/project/.git/index.lock" ))); // Everything else in .git/ IS ignored assert!(should_ignore_git_path(Path::new( "/home/user/project/.git/index" ))); assert!(should_ignore_git_path(Path::new( "/home/user/project/.git/config" ))); assert!(should_ignore_git_path(Path::new( "/home/user/project/.git/COMMIT_EDITMSG" ))); assert!(should_ignore_git_path(Path::new( "/home/user/project/.git/FETCH_HEAD" ))); assert!(should_ignore_git_path(Path::new( "/home/user/project/.git/ORIG_HEAD" ))); assert!(should_ignore_git_path(Path::new( "/home/user/project/.git/refs/tags/v1.0" ))); assert!(should_ignore_git_path(Path::new( "/home/user/project/.git/refs/remotes/origin/main" ))); assert!(should_ignore_git_path(Path::new( "/home/user/project/.git/objects/abc123" ))); assert!(should_ignore_git_path(Path::new( "/home/user/project/.git/hooks/pre-commit" ))); assert!(should_ignore_git_path(Path::new( "/home/user/project/.git/logs/HEAD" ))); // Worktree paths: allowlisted patterns under .git/worktrees// assert!(!should_ignore_git_path(Path::new( "/home/user/project/.git/worktrees/my-wt/HEAD" ))); assert!(!should_ignore_git_path(Path::new( "/home/user/project/.git/worktrees/my-wt/index.lock" ))); // Non-allowlisted worktree paths are still ignored assert!(should_ignore_git_path(Path::new( "/home/user/project/.git/worktrees/my-wt/index" ))); assert!(should_ignore_git_path(Path::new( "/home/user/project/.git/worktrees/my-wt/COMMIT_EDITMSG" ))); // worktrees dir itself (no content after worktree name) is ignored assert!(should_ignore_git_path(Path::new( "/home/user/project/.git/worktrees" ))); assert!(should_ignore_git_path(Path::new( "/home/user/project/.git/worktrees/my-wt" ))); // is_commit_related_git_file assert!(is_commit_related_git_file(Path::new("/repo/.git/HEAD"))); assert!(is_commit_related_git_file(Path::new( "/repo/.git/refs/heads/main" ))); assert!(is_commit_related_git_file(Path::new( "/repo/.git/worktrees/wt/HEAD" ))); assert!(!is_commit_related_git_file(Path::new( "/repo/.git/index.lock" ))); assert!(!is_commit_related_git_file(Path::new( "/repo/.git/refs/tags/v1" ))); // is_index_lock_file assert!(is_index_lock_file(Path::new("/repo/.git/index.lock"))); assert!(is_index_lock_file(Path::new( "/repo/.git/worktrees/wt/index.lock" ))); assert!(!is_index_lock_file(Path::new("/repo/.git/HEAD"))); assert!(!is_index_lock_file(Path::new("/repo/.git/index"))); // Test Windows-style paths (only on Windows, as path parsing is platform-specific) #[cfg(windows)] { assert!(!should_ignore_git_path(Path::new( r"C:\Users\user\project\.git\HEAD" ))); assert!(!should_ignore_git_path(Path::new( r"C:\Users\user\project\.git\index.lock" ))); assert!(should_ignore_git_path(Path::new( r"C:\Users\user\project\.git\index" ))); } } #[test] fn test_is_shared_git_ref() { use super::is_shared_git_ref; use std::path::Path; // Shared refs — broadcast to all repos assert!(is_shared_git_ref(Path::new("/repo/.git/refs/heads/main"))); assert!(is_shared_git_ref(Path::new( "/repo/.git/refs/heads/feature" ))); // Repo-specific — NOT shared assert!(!is_shared_git_ref(Path::new("/repo/.git/HEAD"))); assert!(!is_shared_git_ref(Path::new("/repo/.git/index.lock"))); // Worktree paths — NOT shared assert!(!is_shared_git_ref(Path::new( "/repo/.git/worktrees/foo/HEAD" ))); assert!(!is_shared_git_ref(Path::new( "/repo/.git/worktrees/foo/refs/heads/main" ))); // Other .git internals — NOT shared assert!(!is_shared_git_ref(Path::new("/repo/.git/refs/tags/v1"))); assert!(!is_shared_git_ref(Path::new( "/repo/.git/refs/remotes/origin/main" ))); assert!(!is_shared_git_ref(Path::new("/repo/.git/config"))); // Not a git path at all assert!(!is_shared_git_ref(Path::new("/repo/src/main.rs"))); } #[test] fn test_extract_worktree_git_dir() { use super::extract_worktree_git_dir; use std::path::{Path, PathBuf}; // Standard worktree path extracts the per-worktree gitdir assert_eq!( extract_worktree_git_dir(Path::new("/repo/.git/worktrees/foo/HEAD")), Some(PathBuf::from("/repo/.git/worktrees/foo")) ); assert_eq!( extract_worktree_git_dir(Path::new("/repo/.git/worktrees/bar/index.lock")), Some(PathBuf::from("/repo/.git/worktrees/bar")) ); // Non-worktree paths return None assert_eq!(extract_worktree_git_dir(Path::new("/repo/.git/HEAD")), None); assert_eq!( extract_worktree_git_dir(Path::new("/repo/.git/refs/heads/main")), None ); assert_eq!( extract_worktree_git_dir(Path::new("/repo/src/main.rs")), None ); // Edge case: not enough depth after worktrees/ assert_eq!( extract_worktree_git_dir(Path::new("/repo/.git/worktrees")), None ); assert_eq!( extract_worktree_git_dir(Path::new("/repo/.git/worktrees/foo")), None ); }