use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GitFileStatus {
New,
Modified,
}
pub fn collect(dir: &Path) -> HashMap<PathBuf, GitFileStatus> {
let output = match Command::new("git")
.args(["status", "--porcelain=v1", "-unormal"])
.current_dir(dir)
.output()
{
Ok(o) if o.status.success() => o.stdout,
_ => return HashMap::new(),
};
let Ok(text) = std::str::from_utf8(&output) else {
return HashMap::new();
};
let mut map: HashMap<PathBuf, GitFileStatus> = HashMap::new();
for line in text.lines() {
if line.len() < 4 {
continue;
}
let xy = &line[..2];
let raw_path = if let Some(arrow) = line[3..].rfind(" -> ") {
&line[3 + arrow + 4..]
} else {
&line[3..]
};
let status = xy_to_status(xy);
let abs = dir.join(raw_path);
insert_with_ancestors(&mut map, abs, status, dir);
}
map
}
fn xy_to_status(xy: &str) -> GitFileStatus {
match xy {
"??" | "A " => GitFileStatus::New,
_ => GitFileStatus::Modified,
}
}
#[allow(clippy::needless_pass_by_value)]
fn insert_with_ancestors(
map: &mut HashMap<PathBuf, GitFileStatus>,
path: PathBuf,
status: GitFileStatus,
root: &Path,
) {
map.entry(path.clone())
.and_modify(|existing| {
if status == GitFileStatus::Modified {
*existing = GitFileStatus::Modified;
}
})
.or_insert(status);
let mut current = path.as_path();
while let Some(parent) = current.parent() {
if parent == root || !parent.starts_with(root) {
break;
}
map.entry(parent.to_path_buf())
.and_modify(|existing| {
if *existing != GitFileStatus::Modified {
*existing = GitFileStatus::Modified;
}
})
.or_insert(GitFileStatus::Modified);
current = parent;
}
}