use std::path::{Path, PathBuf};
use std::process::Command;
use crate::error::{Error, Result};
pub const GIT_BIN: &str = "git";
fn git(dir: &Path, args: &[&str]) -> Result<std::process::Output> {
let output = Command::new(GIT_BIN)
.arg("-C")
.arg(dir)
.args(args)
.output()?;
Ok(output)
}
pub fn repo_root(start: &Path) -> Result<PathBuf> {
let output = git(start, &["rev-parse", "--show-toplevel"])?;
if !output.status.success() {
return Err(Error::NotAGitRepo);
}
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if path.is_empty() {
return Err(Error::NotAGitRepo);
}
Ok(PathBuf::from(path))
}
pub fn tracked_files(repo: &Path) -> Result<Vec<PathBuf>> {
let output = git(repo, &["ls-files"])?;
if !output.status.success() {
return Err(Error::ProcessFailed {
tool: GIT_BIN.to_string(),
code: output.status.code().unwrap_or(-1),
message: String::from_utf8_lossy(&output.stderr).trim().to_string(),
});
}
Ok(String::from_utf8_lossy(&output.stdout)
.lines()
.filter(|line| !line.is_empty())
.map(PathBuf::from)
.collect())
}
pub fn is_tracked(repo: &Path, path: &Path) -> Result<bool> {
let path_str = path.to_string_lossy();
let output = git(repo, &["ls-files", "--error-unmatch", &path_str])?;
Ok(output.status.success())
}
pub fn is_ignored(repo: &Path, path: &Path) -> Result<bool> {
let path_str = path.to_string_lossy();
let output = git(repo, &["check-ignore", "--quiet", &path_str])?;
match output.status.code() {
Some(0) => Ok(true),
Some(1) => Ok(false),
other => Err(Error::ProcessFailed {
tool: GIT_BIN.to_string(),
code: other.unwrap_or(-1),
message: String::from_utf8_lossy(&output.stderr).trim().to_string(),
}),
}
}
pub fn ensure_gitignored(repo: &Path, pattern: &str) -> Result<bool> {
let gitignore = repo.join(".gitignore");
let existing = match std::fs::read_to_string(&gitignore) {
Ok(contents) => contents,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => String::new(),
Err(err) => return Err(Error::Io(err)),
};
let already_present = existing
.lines()
.map(str::trim)
.any(|line| line == pattern.trim());
if already_present {
return Ok(false);
}
let mut updated = existing;
if !updated.is_empty() && !updated.ends_with('\n') {
updated.push('\n');
}
updated.push_str(pattern.trim_end());
updated.push('\n');
std::fs::write(&gitignore, updated)?;
Ok(true)
}