use crate::fleet::config::GitModules;
use crate::fleet::error::{Result, SuperGitError};
use crate::fleet::output::Formatter;
use std::fs;
use std::path::Path;
use std::process::Command;
pub fn generate_per_file_diffs(
repo_path: &Path,
base: Option<&str>,
outdir: &Path,
include_staged: bool,
) -> Result<Vec<String>> {
let mut written_files: Vec<String> = Vec::new();
match base {
Some(base_ref) => {
let names = git_diff_name_only(repo_path, &["diff", base_ref, "--name-only"])?;
for file in &names {
let diff = git_diff_file(repo_path, &["diff", base_ref, "--", file])?;
if !diff.is_empty() {
write_diff_file(outdir, file, &diff, false)?;
written_files.push(file.clone());
}
}
}
None => {
let names = git_diff_name_only(repo_path, &["diff", "--name-only"])?;
for file in &names {
let diff = git_diff_file(repo_path, &["diff", "--", file])?;
if !diff.is_empty() {
write_diff_file(outdir, file, &diff, false)?;
written_files.push(file.clone());
}
}
if include_staged {
let staged_names =
git_diff_name_only(repo_path, &["diff", "--cached", "--name-only"])?;
for file in &staged_names {
let diff = git_diff_file(repo_path, &["diff", "--cached", "--", file])?;
if !diff.is_empty() {
let is_append = written_files.contains(file);
write_diff_file(outdir, file, &diff, is_append)?;
if !written_files.contains(file) {
written_files.push(file.clone());
}
}
}
}
}
}
Ok(written_files)
}
fn git_diff_name_only(repo_path: &Path, args: &[&str]) -> Result<Vec<String>> {
let output = Command::new("git")
.args(args)
.current_dir(repo_path)
.output()
.map_err(|e| SuperGitError::Other(format!("Failed to run git diff: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(SuperGitError::Other(format!("git diff failed: {}", stderr)));
}
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(stdout
.lines()
.filter(|l| !l.is_empty())
.map(|l| l.to_string())
.collect())
}
fn git_diff_file(repo_path: &Path, args: &[&str]) -> Result<String> {
let output = Command::new("git")
.args(args)
.current_dir(repo_path)
.output()
.map_err(|e| SuperGitError::Other(format!("Failed to run git diff: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(SuperGitError::Other(format!("git diff failed: {}", stderr)));
}
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
fn write_diff_file(outdir: &Path, file: &str, diff: &str, append: bool) -> Result<()> {
let diff_path = outdir.join(format!("{}.diff", file));
if let Some(parent) = diff_path.parent() {
fs::create_dir_all(parent).map_err(|e| {
SuperGitError::Other(format!(
"Failed to create directory '{}': {}",
parent.display(),
e
))
})?;
}
if append {
use std::io::Write;
let mut f = fs::OpenOptions::new()
.create(true)
.append(true)
.open(&diff_path)
.map_err(|e| {
SuperGitError::Other(format!(
"Failed to open '{}' for appending: {}",
diff_path.display(),
e
))
})?;
f.write_all(diff.as_bytes()).map_err(|e| {
SuperGitError::Other(format!(
"Failed to write to '{}': {}",
diff_path.display(),
e
))
})?;
} else {
fs::write(&diff_path, diff).map_err(|e| {
SuperGitError::Other(format!(
"Failed to write '{}': {}",
diff_path.display(),
e
))
})?;
}
Ok(())
}
pub fn execute<P: AsRef<Path>>(
repo_root: P,
base: Option<&str>,
outdir: &str,
include_staged: bool,
verbose: bool,
) -> Result<()> {
let repo_root = repo_root.as_ref();
let gitmodules = GitModules::parse(repo_root)?;
let outdir_path = Path::new(outdir);
Formatter::print_info(&format!(
"Generating per-file diffs for superproject + {} submodule(s) into '{}'",
gitmodules.submodules.len(),
outdir
));
let mut total_files = 0;
let mut failed = 0;
{
let super_outdir = outdir_path.join("_superproject");
if verbose {
Formatter::print_submodule_header("[SUPERPROJECT]");
}
match generate_per_file_diffs(repo_root, base, &super_outdir, include_staged) {
Ok(files) => {
if files.is_empty() {
if verbose {
Formatter::print_info("No changes");
}
} else {
Formatter::print_success(&format!(
"Superproject: {} diff file(s) written",
files.len()
));
total_files += files.len();
}
}
Err(e) => {
Formatter::print_error(&format!("Superproject diff failed: {}", e));
failed += 1;
}
}
}
for submodule in &gitmodules.submodules {
let submodule_path = repo_root.join(&submodule.path);
if !submodule_path.exists() {
Formatter::print_warning(&format!(
"Submodule '{}' not initialized, skipping",
submodule.name
));
continue;
}
let sub_outdir = outdir_path.join(&submodule.name);
if verbose {
Formatter::print_submodule_header(&submodule.name);
}
match generate_per_file_diffs(&submodule_path, base, &sub_outdir, include_staged) {
Ok(files) => {
if files.is_empty() {
if verbose {
Formatter::print_info(&format!("No changes in '{}'", submodule.name));
}
} else {
Formatter::print_success(&format!(
"'{}': {} diff file(s) written",
submodule.name,
files.len()
));
total_files += files.len();
}
}
Err(e) => {
Formatter::print_error(&format!("Diff failed for '{}': {}", submodule.name, e));
failed += 1;
}
}
}
if total_files > 0 || failed == 0 {
Formatter::print_success(&format!(
"Total: {} diff file(s) written to '{}'",
total_files, outdir
));
}
if failed > 0 {
return Err(SuperGitError::Other(format!(
"{} repo(s) failed to generate diffs",
failed
)));
}
Ok(())
}