eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
#![allow(dead_code)]
use anyhow::Result;

#[derive(Debug, Default, Clone)]
pub struct DiffParseResult {
    pub files: Vec<DiffFile>,
}

#[derive(Debug, Default, Clone)]
pub struct DiffFile {
    pub path: String,
    pub hunks: Vec<DiffHunk>,
}

#[derive(Debug, Default, Clone)]
pub struct DiffHunk {
    pub header: String,
    pub old_start: i32,
    pub old_lines: i32,
    pub new_start: i32,
    pub new_lines: i32,
    pub lines: Vec<DiffLine>,
}

#[derive(Debug, Default, Clone)]
pub struct DiffLine {
    pub kind: DiffLineKind,
    pub content: String,
    pub old_lineno: Option<i32>,
    pub new_lineno: Option<i32>,
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum DiffLineKind {
    #[default]
    Context,
    Add,
    Delete,
    HunkHeader,
}

/// Parse unified diff (multi-file) as emitted by `git diff`.
/// Assumes file header lines: `diff --git a/... b/...` and hunks `@@ -a,b +c,d @@`.
pub fn parse_diff(raw: &str) -> Result<DiffParseResult> {
    let mut files = Vec::new();
    let mut current_file: Option<DiffFile> = None;
    let mut current_hunk: Option<DiffHunk> = None;
    let mut old_lineno = 0;
    let mut new_lineno = 0;

    for line in raw.lines() {
        if line.starts_with("Binary files") {
            // Skip binary diffs
            continue;
        } else if line.starts_with("diff --git ") {
            if let Some(mut f) = current_file.take() {
                if let Some(h) = current_hunk.take() {
                    f.hunks.push(h);
                }
                files.push(f);
            }
            // Extract path from the b/ side
            let path = line
                .split_whitespace()
                .nth(3)
                .unwrap_or("b/unknown")
                .trim_start_matches("b/")
                .to_string();
            current_file = Some(DiffFile {
                path,
                hunks: Vec::new(),
            });
            old_lineno = 0;
            new_lineno = 0;
        } else if line.starts_with("@@") {
            // Flush previous hunk
            if let Some(ref mut f) = current_file {
                if let Some(h) = current_hunk.take() {
                    f.hunks.push(h);
                }
            }
            // Parse header: @@ -a,b +c,d @@
            let header = line.to_string();
            let parts: Vec<&str> = line.split_whitespace().collect();
            let (mut old_start, mut old_count, mut new_start, mut new_count) = (0, 0, 0, 0);
            if parts.len() >= 3 {
                if let Some(old) = parts.get(1) {
                    let nums: Vec<&str> = old.trim_start_matches('-').split(',').collect();
                    old_start = nums.get(0).and_then(|n| n.parse::<i32>().ok()).unwrap_or(0);
                    old_count = nums.get(1).and_then(|n| n.parse::<i32>().ok()).unwrap_or(0);
                }
                if let Some(newn) = parts.get(2) {
                    let nums: Vec<&str> = newn.trim_start_matches('+').split(',').collect();
                    new_start = nums.get(0).and_then(|n| n.parse::<i32>().ok()).unwrap_or(0);
                    new_count = nums.get(1).and_then(|n| n.parse::<i32>().ok()).unwrap_or(0);
                }
            }
            old_lineno = old_start;
            new_lineno = new_start;
            current_hunk = Some(DiffHunk {
                header: header.clone(),
                old_start,
                old_lines: old_count,
                new_start,
                new_lines: new_count,
                lines: vec![DiffLine {
                    kind: DiffLineKind::HunkHeader,
                    content: header,
                    old_lineno: None,
                    new_lineno: None,
                }],
            });
        } else if line.starts_with('+') || line.starts_with('-') || line.starts_with(' ') {
            if let Some(ref mut h) = current_hunk {
                let (kind, old_no, new_no) = if line.starts_with('+') {
                    let ln = new_lineno;
                    new_lineno += 1;
                    (DiffLineKind::Add, None, Some(ln))
                } else if line.starts_with('-') {
                    let ln = old_lineno;
                    old_lineno += 1;
                    (DiffLineKind::Delete, Some(ln), None)
                } else {
                    let o = old_lineno;
                    let n = new_lineno;
                    old_lineno += 1;
                    new_lineno += 1;
                    (DiffLineKind::Context, Some(o), Some(n))
                };
                h.lines.push(DiffLine {
                    kind,
                    content: line.to_string(),
                    old_lineno: old_no,
                    new_lineno: new_no,
                });
            }
        } else {
            // ignore other metadata lines
        }
    }

    if let Some(mut f) = current_file {
        if let Some(h) = current_hunk {
            f.hunks.push(h);
        }
        files.push(f);
    }

    Ok(DiffParseResult { files })
}