use anyhow::{Context, Result, anyhow};
use std::path::{Path, PathBuf};
use std::process::Command;
use super::untracked::{
is_untracked_and_visible, list_untracked, synthesize_untracked, synthesize_untracked_diff_text,
};
use super::{FileDiff, parse::parse_unified_diff};
pub fn diff_single_file(root: &Path, baseline_sha: &str, file_path: &Path) -> Result<String> {
let rel = file_path.strip_prefix(root).unwrap_or(file_path);
let output = Command::new("git")
.args([
"diff",
"--no-renames",
baseline_sha,
"--",
&rel.to_string_lossy(),
])
.current_dir(root)
.output()
.context("git diff single file")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("git diff single file failed: {}", stderr.trim()));
}
let raw = String::from_utf8_lossy(&output.stdout).into_owned();
if !raw.is_empty() {
return Ok(raw);
}
if is_untracked_and_visible(root, rel)? {
match synthesize_untracked_diff_text(root, rel) {
Ok(text) => return Ok(text),
Err(e) => {
let vanished = e.chain().any(|cause| {
cause
.downcast_ref::<std::io::Error>()
.is_some_and(|io| io.kind() == std::io::ErrorKind::NotFound)
});
if vanished {
return Ok(String::new());
}
return Err(e)
.with_context(|| format!("synthesizing untracked snapshot {}", rel.display()));
}
}
}
Ok(raw)
}
pub fn compute_diff(root: &Path, baseline_sha: &str) -> Result<Vec<FileDiff>> {
compute_diff_with_snapshots(root, baseline_sha).map(|(files, _)| files)
}
pub fn compute_diff_with_snapshots(
root: &Path,
baseline_sha: &str,
) -> Result<(Vec<FileDiff>, std::collections::HashMap<PathBuf, String>)> {
let output = Command::new("git")
.args(["diff", "--no-renames", baseline_sha, "--"])
.current_dir(root)
.output()
.context("failed to spawn `git diff --no-renames`")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("`git diff` failed: {}", stderr.trim()));
}
let raw = String::from_utf8_lossy(&output.stdout);
let mut files = parse_unified_diff(&raw).context("parsing git diff output")?;
let mut snapshots: std::collections::HashMap<PathBuf, String> =
split_raw_diff_by_file(&raw, &files);
for rel in list_untracked(root)? {
match synthesize_untracked(root, &rel) {
Ok(synth) => {
match synthesize_untracked_diff_text(root, &rel) {
Ok(text) => {
snapshots.insert(synth.path.clone(), text);
}
Err(e) => {
let vanished = e.chain().any(|cause| {
cause
.downcast_ref::<std::io::Error>()
.is_some_and(|io| io.kind() == std::io::ErrorKind::NotFound)
});
if !vanished {
return Err(e).with_context(|| {
format!("synthesizing untracked snapshot {}", rel.display())
});
}
}
}
files.push(synth);
}
Err(e) => {
let vanished = e.chain().any(|cause| {
cause
.downcast_ref::<std::io::Error>()
.is_some_and(|io| io.kind() == std::io::ErrorKind::NotFound)
});
if vanished {
continue;
}
return Err(e)
.with_context(|| format!("synthesizing untracked file {}", rel.display()));
}
}
}
Ok((files, snapshots))
}
fn split_raw_diff_by_file(
raw: &str,
files: &[FileDiff],
) -> std::collections::HashMap<PathBuf, String> {
let mut snapshots = std::collections::HashMap::new();
if files.is_empty() || raw.is_empty() {
return snapshots;
}
let mut file_idx = 0usize;
let mut current = String::new();
for line in raw.lines() {
if line.starts_with("diff --git ")
&& !current.is_empty()
&& let Some(file) = files.get(file_idx)
{
snapshots.insert(file.path.clone(), std::mem::take(&mut current));
file_idx += 1;
}
current.push_str(line);
current.push('\n');
}
if !current.is_empty()
&& let Some(file) = files.get(file_idx)
{
snapshots.insert(file.path.clone(), current);
}
snapshots
}