use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::process::Command;
pub fn get_changed_files(base: &Path) -> Vec<PathBuf> {
let git_root = match find_git_root(base) {
Some(r) => r,
None => {
eprintln!(
" ⚠ --only-changed: '{}' is not inside a git repository. \
Falling back to full scan.",
base.display()
);
return vec![];
}
};
let mut changed: HashSet<PathBuf> = HashSet::new();
collect_git_diff(
&git_root,
&["diff", "--name-only", "--cached"],
&mut changed,
);
collect_git_diff(&git_root, &["diff", "--name-only"], &mut changed);
changed.into_iter().collect()
}
fn collect_git_diff(git_root: &Path, args: &[&str], out: &mut HashSet<PathBuf>) {
let output = Command::new("git")
.args(args)
.current_dir(git_root)
.output();
let output = match output {
Ok(o) if o.status.success() || o.status.code() == Some(1) => o,
_ => return,
};
for line in String::from_utf8_lossy(&output.stdout).lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
out.insert(git_root.join(trimmed));
}
}
}
fn find_git_root(start: &Path) -> Option<PathBuf> {
let output = Command::new("git")
.args(["rev-parse", "--show-toplevel"])
.current_dir(start)
.output();
match output {
Ok(o) if o.status.success() => {
let root = String::from_utf8_lossy(&o.stdout).trim().to_string();
if root.is_empty() {
None
} else {
Some(PathBuf::from(root))
}
}
_ => {
let mut dir = if start.is_file() {
start.parent()?.to_path_buf()
} else {
start.to_path_buf()
};
loop {
if dir.join(".git").exists() {
return Some(dir);
}
if !dir.pop() {
return None;
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn find_git_root_from_workspace() {
let here = Path::new(env!("CARGO_MANIFEST_DIR"));
assert!(
find_git_root(here).is_some(),
"expected to find git root from CARGO_MANIFEST_DIR"
);
}
#[test]
fn find_git_root_not_a_repo() {
let tmp = std::env::temp_dir();
let result = find_git_root(&tmp);
let _ = result;
}
}