feat(commit): 添加.gitignore文件过滤功能,自动跳过被忽略的文件
在自动暂存和`--all`模式下,检测并跳过被.gitignore规则匹配的文件,暂存完成后显示被移除的被忽略文件列表
This commit is contained in:
266
tests/gitignore_tests.rs
Normal file
266
tests/gitignore_tests.rs
Normal file
@@ -0,0 +1,266 @@
|
||||
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
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user