securegit 0.8.5

Zero-trust git replacement with 12 built-in security scanners, LLM redteam bridge, universal undo, durable backups, and a 50-tool MCP server
Documentation
use crate::cli::UI;
use anyhow::Result;
use git2::DiffOptions;
use std::path::Path;

pub struct DiffDisplayOptions {
    pub cached: bool,
    pub stat_only: bool,
    pub name_only: bool,
    pub name_status: bool,
    pub commit_spec: Option<String>,
    pub paths: Vec<String>,
    pub ignore_whitespace: bool,
}

pub fn execute(path: &Path, opts: &DiffDisplayOptions, _ui: &UI) -> Result<()> {
    let repo = crate::ops::open_repo(path)?;

    let mut diff_opts = DiffOptions::new();
    if opts.ignore_whitespace {
        diff_opts.ignore_whitespace(true);
    }
    for p in &opts.paths {
        diff_opts.pathspec(p);
    }

    let diff = if let Some(spec) = opts.commit_spec.as_deref() {
        // Handle commit range: "A..B", "A...B", or single commit vs working tree
        if spec.contains("...") {
            let parts: Vec<&str> = spec.splitn(2, "...").collect();
            let left = repo.revparse_single(parts[0])?.peel_to_tree()?;
            let right = repo.revparse_single(parts[1])?.peel_to_tree()?;
            let merge_base_oid = repo.merge_base(
                repo.revparse_single(parts[0])?.id(),
                repo.revparse_single(parts[1])?.id(),
            )?;
            let base_tree = repo.find_commit(merge_base_oid)?.tree()?;
            // Show diff from merge-base to right side
            let _ = left; // suppress unused
            repo.diff_tree_to_tree(Some(&base_tree), Some(&right), Some(&mut diff_opts))?
        } else if spec.contains("..") {
            let parts: Vec<&str> = spec.splitn(2, "..").collect();
            let left = repo.revparse_single(parts[0])?.peel_to_tree()?;
            let right = repo.revparse_single(parts[1])?.peel_to_tree()?;
            repo.diff_tree_to_tree(Some(&left), Some(&right), Some(&mut diff_opts))?
        } else {
            // Single ref: diff that commit against working tree
            let tree = repo.revparse_single(spec)?.peel_to_tree()?;
            repo.diff_tree_to_workdir(Some(&tree), Some(&mut diff_opts))?
        }
    } else if opts.cached {
        let head_tree = repo.head().ok().and_then(|h| h.peel_to_tree().ok());
        repo.diff_tree_to_index(head_tree.as_ref(), None, Some(&mut diff_opts))?
    } else {
        repo.diff_index_to_workdir(None, Some(&mut diff_opts))?
    };

    if opts.stat_only {
        let stats = diff.stats()?;
        let buf = stats.to_buf(git2::DiffStatsFormat::FULL, 80)?;
        print!("{}", buf.as_str().unwrap_or(""));
        return Ok(());
    }

    if opts.name_only || opts.name_status {
        for delta in diff.deltas() {
            let path = delta
                .new_file()
                .path()
                .or_else(|| delta.old_file().path())
                .map(|p| p.display().to_string())
                .unwrap_or_default();

            if opts.name_status {
                let status_char = match delta.status() {
                    git2::Delta::Added => 'A',
                    git2::Delta::Deleted => 'D',
                    git2::Delta::Modified => 'M',
                    git2::Delta::Renamed => 'R',
                    git2::Delta::Copied => 'C',
                    git2::Delta::Typechange => 'T',
                    _ => '?',
                };
                println!("{}\t{}", status_char, path);
            } else {
                println!("{}", path);
            }
        }
        return Ok(());
    }

    display_diff(&diff, _ui)
}

pub fn display_diff(diff: &git2::Diff, _ui: &UI) -> Result<()> {
    diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
        let prefix = match line.origin() {
            '+' => "+",
            '-' => "-",
            ' ' => " ",
            _ => "",
        };
        let content = std::str::from_utf8(line.content()).unwrap_or("");
        print!("{}{}", prefix, strip_control_chars(content));
        true
    })?;

    Ok(())
}

pub fn execute_compact(
    path: &Path,
    cached: bool,
    commit_spec: Option<&str>,
    paths: &[String],
    ignore_whitespace: bool,
) -> Result<String> {
    use crate::cli::compact::CompactDiffFormatter;

    let repo = crate::ops::open_repo(path)?;

    let mut opts = DiffOptions::new();
    if ignore_whitespace {
        opts.ignore_whitespace(true);
    }
    for p in paths {
        opts.pathspec(p);
    }

    let diff = if let Some(spec) = commit_spec {
        if spec.contains("...") {
            let parts: Vec<&str> = spec.splitn(2, "...").collect();
            let right = repo.revparse_single(parts[1])?.peel_to_tree()?;
            let merge_base_oid = repo.merge_base(
                repo.revparse_single(parts[0])?.id(),
                repo.revparse_single(parts[1])?.id(),
            )?;
            let base_tree = repo.find_commit(merge_base_oid)?.tree()?;
            repo.diff_tree_to_tree(Some(&base_tree), Some(&right), Some(&mut opts))?
        } else if spec.contains("..") {
            let parts: Vec<&str> = spec.splitn(2, "..").collect();
            let left = repo.revparse_single(parts[0])?.peel_to_tree()?;
            let right = repo.revparse_single(parts[1])?.peel_to_tree()?;
            repo.diff_tree_to_tree(Some(&left), Some(&right), Some(&mut opts))?
        } else {
            let tree = repo.revparse_single(spec)?.peel_to_tree()?;
            repo.diff_tree_to_workdir(Some(&tree), Some(&mut opts))?
        }
    } else if cached {
        let head_tree = repo.head().ok().and_then(|h| h.peel_to_tree().ok());
        repo.diff_tree_to_index(head_tree.as_ref(), None, Some(&mut opts))?
    } else {
        repo.diff_index_to_workdir(None, Some(&mut opts))?
    };

    // Stat summary
    let stats = diff.stats()?;
    let stat_buf = stats.to_buf(git2::DiffStatsFormat::SHORT, 80)?;
    let mut output = stat_buf.as_str().unwrap_or("").to_string();

    // Compact diff via formatter
    let formatter = std::cell::RefCell::new(CompactDiffFormatter::new());

    diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
        let mut fmt = formatter.borrow_mut();
        let content = std::str::from_utf8(line.content()).unwrap_or("");
        match line.origin() {
            'F' => {
                // File header — extract path from delta
                if let Some(path) = _delta
                    .new_file()
                    .path()
                    .or_else(|| _delta.old_file().path())
                {
                    fmt.begin_file(&path.display().to_string());
                }
            }
            'H' => {
                fmt.begin_hunk(content);
            }
            '+' | '-' | ' ' => {
                fmt.add_line(line.origin(), content);
            }
            _ => {}
        }
        true
    })?;

    let compact_diff = formatter.into_inner().finish();
    output.push_str(&compact_diff);

    Ok(output.trim_end().to_string())
}

/// Strip ANSI escape sequences and terminal control characters from output.
fn strip_control_chars(s: &str) -> String {
    let mut result = String::with_capacity(s.len());
    let mut chars = s.chars();
    while let Some(c) = chars.next() {
        if c == '\x1b' {
            // Skip ANSI escape sequence: ESC [ ... final_byte
            if let Some(next) = chars.next() {
                if next == '[' {
                    // CSI sequence: consume until 0x40-0x7E
                    for c2 in chars.by_ref() {
                        if ('\x40'..='\x7e').contains(&c2) {
                            break;
                        }
                    }
                }
                // else: skip the single char after ESC
            }
        } else if c == '\n' || c == '\t' || c == '\r' {
            result.push(c);
        } else if c.is_control() {
            // Skip other control characters (BEL, BS, etc.)
        } else {
            result.push(c);
        }
    }
    result
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_strip_control_chars_plain() {
        assert_eq!(strip_control_chars("hello world\n"), "hello world\n");
    }

    #[test]
    fn test_strip_control_chars_ansi() {
        assert_eq!(strip_control_chars("\x1b[31mred\x1b[0m"), "red");
        assert_eq!(
            strip_control_chars("\x1b[1;32mbold green\x1b[0m"),
            "bold green"
        );
    }

    #[test]
    fn test_strip_control_chars_bel() {
        assert_eq!(strip_control_chars("hello\x07world"), "helloworld");
    }

    #[test]
    fn test_strip_control_chars_preserves_whitespace() {
        assert_eq!(strip_control_chars("a\tb\nc\r\n"), "a\tb\nc\r\n");
    }
}