feat(commit): 添加.gitignore文件过滤功能,自动跳过被忽略的文件

在自动暂存和`--all`模式下,检测并跳过被.gitignore规则匹配的文件,暂存完成后显示被移除的被忽略文件列表
This commit is contained in:
2026-07-20 17:33:57 +08:00
parent 19aff8a6c1
commit 995d263a48
5 changed files with 395 additions and 14 deletions

View File

@@ -533,27 +533,116 @@ impl GitRepo {
Ok(files)
}
/// Stage files
pub fn stage_files<P: AsRef<Path>>(&self, paths: &[P]) -> Result<()> {
/// Stage files, skipping paths matched by .gitignore rules
/// Returns the list of skipped paths (those matched by .gitignore)
pub fn stage_files<P: AsRef<Path>>(&self, paths: &[P]) -> Result<Vec<String>> {
let mut index = self.repo.index()?;
let mut skipped = Vec::new();
for path in paths {
let path = path.as_ref();
if path.is_absolute() {
if let Ok(rel_path) = path.strip_prefix(&self.path) {
index.add_path(rel_path)?;
let rel_path = if path.is_absolute() {
match path.strip_prefix(&self.path) {
Ok(p) => p,
Err(_) => {
// Outside repo, skip
skipped.push(path.to_string_lossy().to_string());
continue;
}
}
} else {
index.add_path(path)?;
path
};
// Check if the path is ignored by .gitignore
if self.is_path_ignored(rel_path)? {
skipped.push(rel_path.to_string_lossy().to_string());
continue;
}
index.add_path(rel_path)?;
}
index.write()?;
Ok(())
Ok(skipped)
}
/// Stage all changes including subdirectories
pub fn stage_all(&self) -> Result<()> {
/// Check if a path is ignored by .gitignore rules
pub fn is_path_ignored<P: AsRef<Path>>(&self, path: P) -> Result<bool> {
let path = path.as_ref();
// Convert to relative path if absolute
let rel_path = if path.is_absolute() {
match path.strip_prefix(&self.path) {
Ok(p) => p,
Err(_) => return Ok(false), // Outside repo, not ignored
}
} else {
path
};
let path_str = match rel_path.to_str() {
Some(s) => s,
None => return Ok(false), // Non-UTF8 path, not ignored
};
let output = std::process::Command::new("git")
.args(["check-ignore", "--quiet", "--", path_str])
.current_dir(&self.path)
.output()?;
// Exit code 0: ignored, 1: not ignored, other: error
match output.status.code() {
Some(0) => Ok(true),
Some(1) => Ok(false),
_ => Ok(false), // Treat errors as not ignored
}
}
/// Remove files from index that are tracked but should be ignored by .gitignore
pub fn remove_ignored_from_index(&self) -> Result<Vec<String>> {
let output = std::process::Command::new("git")
.args(["ls-files", "--cached", "-i", "--exclude-standard"])
.current_dir(&self.path)
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("Failed to list ignored tracked files: {}", stderr);
}
let stdout = String::from_utf8_lossy(&output.stdout);
let ignored_files: Vec<String> = stdout
.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect();
if ignored_files.is_empty() {
return Ok(Vec::new());
}
// Remove these files from the index (keep working tree files)
let mut args = vec!["rm", "--cached", "--quiet", "--"];
for file in &ignored_files {
args.push(file);
}
let rm_output = std::process::Command::new("git")
.args(&args)
.current_dir(&self.path)
.output()?;
if !rm_output.status.success() {
let stderr = String::from_utf8_lossy(&rm_output.stderr);
bail!("Failed to remove ignored files from index: {}", stderr);
}
Ok(ignored_files)
}
/// Stage all changes including subdirectories, then remove ignored tracked files
pub fn stage_all(&self) -> Result<Vec<String>> {
// Use git command for reliable staging (handles all edge cases)
let output = std::process::Command::new("git")
.args(["add", "-A"])
@@ -569,7 +658,14 @@ impl GitRepo {
// Force refresh the git2 index to pick up changes from git CLI
let _ = self.repo.index()?.write();
Ok(())
// Remove files that are tracked but should be ignored by .gitignore
match self.remove_ignored_from_index() {
Ok(removed) => Ok(removed),
Err(e) => {
eprintln!("Warning: failed to clean ignored files from index: {}", e);
Ok(Vec::new())
}
}
}
/// Unstage files