use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{bail, Context, Result};
use crate::colocated_test::Language;
pub fn stale_sources(
repo: &Path,
base: &str,
language: Language,
exempt: &BTreeSet<String>,
) -> Result<Vec<PathBuf>> {
let entries = changed_entries(repo, base)?;
let changed: BTreeSet<&str> = entries.iter().map(|(_, path)| path.as_str()).collect();
let mut stale = Vec::new();
for (status, rel) in &entries {
let path = Path::new(rel);
if !language.tracks(path) || language.is_test(path) || language.is_support(path) {
continue;
}
let is_subject = match status {
Status::Modified => {
let contents = std::fs::read_to_string(repo.join(path))
.with_context(|| format!("reading changed source `{rel}`"))?;
language.has_code(&contents)
}
Status::Deleted => true,
Status::Other => false,
};
if !is_subject || exempt.contains(rel) {
continue;
}
let expected = language
.expected_test_path(path)
.to_string_lossy()
.replace('\\', "/");
if !changed.contains(expected.as_str()) {
stale.push(path.to_path_buf());
}
}
stale.sort();
Ok(stale)
}
enum Status {
Modified,
Deleted,
Other,
}
impl Status {
fn from_code(code: &str) -> Status {
match code.chars().next() {
Some('M') => Status::Modified,
Some('D') => Status::Deleted,
_ => Status::Other,
}
}
}
fn changed_entries(repo: &Path, base: &str) -> Result<Vec<(Status, String)>> {
let range = format!("{base}...HEAD");
let output = Command::new("git")
.current_dir(repo)
.args([
"diff",
"--name-status",
"--no-renames",
"--relative",
&range,
])
.output()
.with_context(|| format!("running `git diff` in `{}`", repo.display()))?;
if !output.status.success() {
bail!(
"`git diff {range}` failed in `{}`: {}",
repo.display(),
String::from_utf8_lossy(&output.stderr).trim()
);
}
let stdout = String::from_utf8_lossy(&output.stdout);
let mut entries = Vec::new();
for line in stdout.lines() {
if let Some((status, path)) = line.split_once('\t') {
let path = path.trim_end_matches('\r').replace('\\', "/");
entries.push((Status::from_code(status), path));
}
}
Ok(entries)
}