wsb 0.85.227722

Workspace - A tool suite for file operations, version management, and development workflow automation
Documentation
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;

/// Generate per-file diff files for a single git repo.
///
/// Behavior mirrors the bash `gitdiff_per_file` function:
/// - No base: diffs unstaged changes. With `--staged`, also appends staged diffs.
/// - With base: diffs against the given base ref (commit, branch, tag).
///
/// Each changed file `path/to/file.rs` gets a corresponding `outdir/path/to/file.rs.diff`.
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) => {
            // Diff against a 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 => {
            // Unstaged changes
            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());
                }
            }

            // Staged changes (appended to same .diff files)
            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(())
}

/// Fleet diff: generate per-file diffs for superproject + all submodules.
/// Each repo's diffs go into `outdir/<repo_name>/`.
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;

    // Superproject
    {
        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;
            }
        }
    }

    // Submodules
    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(())
}