use anyhow::{Context, Result};
use std::process::Command;
#[derive(Debug, Clone)]
pub struct DiffHunk {
pub file: String,
pub old_file: String,
pub new_file: String,
pub file_header: String,
pub header: String,
pub lines: Vec<String>,
pub unsupported_metadata: Option<String>,
}
const DIFF_FORMAT_ARGS: &[&str] = &[
"--no-color",
"--no-ext-diff",
"--src-prefix=a/",
"--dst-prefix=b/",
];
pub fn run_git_diff(staged: bool, file: Option<&str>) -> Result<String> {
let mut cmd = Command::new("git");
cmd.arg("diff");
cmd.args(DIFF_FORMAT_ARGS);
if staged {
cmd.arg("--cached");
}
if let Some(f) = file {
cmd.arg("--").arg(f);
}
run_git_cmd(&mut cmd)
}
pub fn run_git_diff_commit(commit: &str, file: Option<&str>) -> Result<String> {
let mut cmd = Command::new("git");
cmd.args(["show", "--pretty="]);
cmd.args(DIFF_FORMAT_ARGS);
cmd.arg(commit);
if let Some(f) = file {
cmd.arg("--").arg(f);
}
run_git_cmd(&mut cmd)
}
pub fn run_git_cmd(cmd: &mut Command) -> Result<String> {
let output = cmd.output().context("failed to run git command")?;
if !output.status.success() {
anyhow::bail!(
"git command failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
fn strip_diff_prefix(line: &str) -> &str {
line.strip_prefix("--- a/")
.or_else(|| line.strip_prefix("+++ b/"))
.or_else(|| line.strip_prefix("--- /"))
.or_else(|| line.strip_prefix("+++ /"))
.or_else(|| line.strip_prefix("+++ a/"))
.or_else(|| line.strip_prefix("--- "))
.or_else(|| line.strip_prefix("+++ "))
.unwrap_or(line)
}
const UNSUPPORTED_PREAMBLE_PREFIXES: &[&str] = &[
"rename from ",
"rename to ",
"copy from ",
"copy to ",
"old mode ",
"new mode ",
"similarity index ",
"dissimilarity index ",
];
pub fn parse_diff(input: &str) -> Vec<DiffHunk> {
let mut hunks = Vec::new();
let mut current_old_file = String::new();
let mut current_new_file = String::new();
let mut current_file_header = String::new();
let mut current_header: Option<String> = None;
let mut current_lines: Vec<String> = Vec::new();
let mut current_unsupported: Option<String> = None;
for line in input.lines() {
if line.starts_with("diff --git") {
if let Some(header) = current_header.take() {
hunks.push(DiffHunk {
file: display_file(¤t_old_file, ¤t_new_file),
old_file: current_old_file.clone(),
new_file: current_new_file.clone(),
file_header: current_file_header.clone(),
header,
lines: std::mem::take(&mut current_lines),
unsupported_metadata: current_unsupported.clone(),
});
}
current_file_header.clear();
current_old_file.clear();
current_new_file.clear();
current_unsupported = None;
} else if current_unsupported.is_none() {
if let Some(prefix) = UNSUPPORTED_PREAMBLE_PREFIXES
.iter()
.find(|p| line.starts_with(*p))
{
current_unsupported = Some(prefix.trim().to_string());
}
}
if line.starts_with("--- ") {
current_file_header = line.to_string();
current_old_file = strip_diff_prefix(line).to_string();
} else if line.starts_with("+++ ") {
current_file_header.push('\n');
current_file_header.push_str(line);
current_new_file = strip_diff_prefix(line).to_string();
} else if line.starts_with("@@ ") {
if let Some(header) = current_header.take() {
hunks.push(DiffHunk {
file: display_file(¤t_old_file, ¤t_new_file),
old_file: current_old_file.clone(),
new_file: current_new_file.clone(),
file_header: current_file_header.clone(),
header,
lines: std::mem::take(&mut current_lines),
unsupported_metadata: current_unsupported.clone(),
});
}
current_header = Some(line.to_string());
} else if current_header.is_some() {
current_lines.push(line.to_string());
}
}
if let Some(header) = current_header.take() {
hunks.push(DiffHunk {
file: display_file(¤t_old_file, ¤t_new_file),
old_file: current_old_file,
new_file: current_new_file,
file_header: current_file_header,
header,
lines: current_lines,
unsupported_metadata: current_unsupported,
});
}
hunks
}
fn display_file(old: &str, new: &str) -> String {
if new == "dev/null" || new.is_empty() {
old.to_string()
} else {
new.to_string()
}
}
pub fn check_supported(hunk: &DiffHunk, id: &str) -> Result<()> {
if let Some(ref metadata) = hunk.unsupported_metadata {
anyhow::bail!(
"hunk {} involves '{}' which is not supported for hunk-level operations",
id,
metadata
);
}
Ok(())
}