use quicommit::git::GitRepo; use std::fs; use std::path::Path; use std::process::Command; use tempfile::TempDir; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /// Run a git command in the given directory, returning stdout as a String. /// Panics if the command fails. fn git(dir: &Path, args: &[&str]) -> String { let output = Command::new("git") .args(args) .current_dir(dir) .output() .expect("Failed to execute git command"); if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); panic!("git {:?} failed in {:?}: {}", args, dir, stderr); } String::from_utf8_lossy(&output.stdout).to_string() } /// Initialize a new git repo in the given directory and configure a local /// user identity so commits can be created. fn init_repo(dir: &Path) { git(dir, &["init"]); git(dir, &["config", "user.name", "Test User"]); git(dir, &["config", "user.email", "test@example.com"]); // Disable commit signing in case the global config enables it. git(dir, &["config", "commit.gpgsign", "false"]); } /// Write a file with the given content, creating parent directories as needed. fn write_file(dir: &Path, rel_path: &str, content: &str) { let file_path = dir.join(rel_path); if let Some(parent) = file_path.parent() { fs::create_dir_all(parent).expect("Failed to create parent directories"); } fs::write(&file_path, content).expect("Failed to write file"); } /// Get the list of files in the index as a String (one path per line). fn ls_files(dir: &Path) -> String { git(dir, &["ls-files"]) } // --------------------------------------------------------------------------- // Tests for is_path_ignored // --------------------------------------------------------------------------- #[test] fn test_is_path_ignored_with_ignored_path() { let temp_dir = TempDir::new().unwrap(); let repo_path = temp_dir.path(); init_repo(repo_path); write_file(repo_path, ".gitignore", "__pycache__/\n"); write_file(repo_path, "__pycache__/foo.pyc", "bytecode"); write_file(repo_path, "subdir/__pycache__/bar.pyc", "more bytecode"); let repo = GitRepo::open(repo_path).expect("Failed to open repo"); // Top-level ignored file assert!( repo.is_path_ignored("__pycache__/foo.pyc").unwrap(), "__pycache__/foo.pyc should be ignored" ); // Nested ignored file under a subdirectory assert!( repo.is_path_ignored("subdir/__pycache__/bar.pyc").unwrap(), "subdir/__pycache__/bar.pyc should be ignored" ); } #[test] fn test_is_path_ignored_with_non_ignored_path() { let temp_dir = TempDir::new().unwrap(); let repo_path = temp_dir.path(); init_repo(repo_path); write_file(repo_path, ".gitignore", "__pycache__/\n"); write_file(repo_path, "src/main.rs", "fn main() {}"); let repo = GitRepo::open(repo_path).expect("Failed to open repo"); assert!( !repo.is_path_ignored("src/main.rs").unwrap(), "src/main.rs should not be ignored" ); } // --------------------------------------------------------------------------- // Tests for stage_all // --------------------------------------------------------------------------- #[test] fn test_stage_all_removes_ignored_tracked_files() { let temp_dir = TempDir::new().unwrap(); let repo_path = temp_dir.path(); init_repo(repo_path); // Create and commit __pycache__/foo.pyc so it becomes a tracked file. write_file(repo_path, "__pycache__/foo.pyc", "original bytecode"); git(repo_path, &["add", "__pycache__/foo.pyc"]); git(repo_path, &["commit", "-m", "initial commit"]); // Add a .gitignore that now ignores __pycache__/. write_file(repo_path, ".gitignore", "__pycache__/\n"); // Modify the tracked file so the working tree has unstaged changes. write_file(repo_path, "__pycache__/foo.pyc", "modified bytecode"); let repo = GitRepo::open(repo_path).expect("Failed to open repo"); let removed = repo.stage_all().expect("stage_all should succeed"); assert!( removed.iter().any(|f| f == "__pycache__/foo.pyc"), "stage_all should return __pycache__/foo.pyc in removed list, got: {:?}", removed ); // Verify the file is no longer in the index. let files = ls_files(repo_path); assert!( !files.lines().any(|l| l == "__pycache__/foo.pyc"), "__pycache__/foo.pyc should no longer be in the index, got: {}", files ); } #[test] fn test_stage_all_no_ignored_files_returns_empty() { let temp_dir = TempDir::new().unwrap(); let repo_path = temp_dir.path(); init_repo(repo_path); write_file(repo_path, ".gitignore", "*.log\n"); write_file(repo_path, "src/main.rs", "fn main() {}"); let repo = GitRepo::open(repo_path).expect("Failed to open repo"); let removed = repo.stage_all().expect("stage_all should succeed"); assert!( removed.is_empty(), "stage_all should return empty Vec when no ignored files are tracked, got: {:?}", removed ); // Verify src/main.rs was staged. let files = ls_files(repo_path); assert!( files.lines().any(|l| l == "src/main.rs"), "src/main.rs should be in the index, got: {}", files ); } // --------------------------------------------------------------------------- // Tests for stage_files // --------------------------------------------------------------------------- #[test] fn test_stage_files_skips_ignored_paths() { let temp_dir = TempDir::new().unwrap(); let repo_path = temp_dir.path(); init_repo(repo_path); write_file(repo_path, ".gitignore", "__pycache__/\n"); write_file(repo_path, "__pycache__/foo.pyc", "bytecode"); write_file(repo_path, "src/main.rs", "fn main() {}"); let repo = GitRepo::open(repo_path).expect("Failed to open repo"); let skipped = repo .stage_files(&["__pycache__/foo.pyc", "src/main.rs"]) .expect("stage_files should succeed"); assert!( skipped.iter().any(|f| f == "__pycache__/foo.pyc"), "skipped list should contain __pycache__/foo.pyc, got: {:?}", skipped ); assert!( !skipped.iter().any(|f| f == "src/main.rs"), "skipped list should not contain src/main.rs, got: {:?}", skipped ); let files = ls_files(repo_path); assert!( files.lines().any(|l| l == "src/main.rs"), "src/main.rs should be staged, got: {}", files ); assert!( !files.lines().any(|l| l == "__pycache__/foo.pyc"), "__pycache__/foo.pyc should not be staged, got: {}", files ); } #[test] fn test_stage_files_all_paths_ignored() { let temp_dir = TempDir::new().unwrap(); let repo_path = temp_dir.path(); init_repo(repo_path); write_file(repo_path, ".gitignore", "__pycache__/\n"); write_file(repo_path, "__pycache__/foo.pyc", "bytecode"); let repo = GitRepo::open(repo_path).expect("Failed to open repo"); let skipped = repo .stage_files(&["__pycache__/foo.pyc"]) .expect("stage_files should succeed"); assert!( skipped.iter().any(|f| f == "__pycache__/foo.pyc"), "skipped list should contain __pycache__/foo.pyc, got: {:?}", skipped ); // Verify the index remains empty (the file was not staged). let files = ls_files(repo_path); assert!( files.trim().is_empty(), "index should be empty, got: {}", files ); } #[test] fn test_stage_files_normal_paths_unchanged_behavior() { let temp_dir = TempDir::new().unwrap(); let repo_path = temp_dir.path(); init_repo(repo_path); // No .gitignore (or one that does not match these paths). write_file(repo_path, "src/main.rs", "fn main() {}"); write_file(repo_path, "src/lib.rs", "pub fn lib() {}"); let repo = GitRepo::open(repo_path).expect("Failed to open repo"); let skipped = repo .stage_files(&["src/main.rs", "src/lib.rs"]) .expect("stage_files should succeed"); assert!( skipped.is_empty(), "skipped list should be empty, got: {:?}", skipped ); let files = ls_files(repo_path); assert!( files.lines().any(|l| l == "src/main.rs"), "src/main.rs should be staged, got: {}", files ); assert!( files.lines().any(|l| l == "src/lib.rs"), "src/lib.rs should be staged, got: {}", files ); }