pointbreak 0.6.0

Durable terminal code review for changes humans and coding agents collaborate on together
Documentation
use crate::error::{Result, ShoreError};
use crate::model::{DiffRow, DiffRowKind, HunkId, ReviewHunk};

#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct PatchFile {
    pub old_path: Option<String>,
    pub new_path: Option<String>,
    pub old_mode: Option<String>,
    pub new_mode: Option<String>,
    pub similarity: Option<u16>,
    pub is_binary: bool,
    pub hunks: Vec<ReviewHunk>,
}

impl PatchFile {
    pub fn key(&self) -> String {
        self.new_path
            .as_ref()
            .or(self.old_path.as_ref())
            .expect("patch file has at least one path")
            .clone()
    }
}

pub(crate) fn parse_patch(patch: &str) -> Result<Vec<PatchFile>> {
    let mut files = Vec::new();
    let mut current_file: Option<PatchFile> = None;
    let mut current_hunk: Option<ReviewHunk> = None;
    let mut old_line = 0;
    let mut new_line = 0;

    for line in patch.lines() {
        if line.starts_with("diff --git ") {
            finish_hunk(&mut current_file, &mut current_hunk);
            if let Some(file) = current_file.take() {
                files.push(file);
            }
            current_file = Some(PatchFile {
                old_path: diff_git_path(line, 'a'),
                new_path: diff_git_path(line, 'b'),
                old_mode: None,
                new_mode: None,
                similarity: None,
                is_binary: false,
                hunks: Vec::new(),
            });
            continue;
        }

        if current_file.is_none() {
            continue;
        }

        if let Some(path) = line.strip_prefix("--- ") {
            current_file.as_mut().expect("current file").old_path = parse_patch_path(path);
            continue;
        }
        if let Some(path) = line.strip_prefix("+++ ") {
            current_file.as_mut().expect("current file").new_path = parse_patch_path(path);
            continue;
        }
        if let Some(mode) = line.strip_prefix("old mode ") {
            current_file.as_mut().expect("current file").old_mode = Some(mode.to_owned());
            continue;
        }
        if let Some(mode) = line.strip_prefix("new mode ") {
            current_file.as_mut().expect("current file").new_mode = Some(mode.to_owned());
            continue;
        }
        if let Some(mode) = line.strip_prefix("new file mode ") {
            current_file.as_mut().expect("current file").new_mode = Some(mode.to_owned());
            continue;
        }
        if let Some(mode) = line.strip_prefix("deleted file mode ") {
            current_file.as_mut().expect("current file").old_mode = Some(mode.to_owned());
            continue;
        }
        if let Some(index) = line.strip_prefix("similarity index ") {
            let similarity = index.trim_end_matches('%').parse().map_err(|error| {
                ShoreError::Message(format!("invalid similarity index {index}: {error}"))
            })?;
            current_file.as_mut().expect("current file").similarity = Some(similarity);
            continue;
        }
        if let Some(path) = line.strip_prefix("rename from ") {
            current_file.as_mut().expect("current file").old_path = Some(path.to_owned());
            continue;
        }
        if let Some(path) = line.strip_prefix("rename to ") {
            current_file.as_mut().expect("current file").new_path = Some(path.to_owned());
            continue;
        }
        if line.starts_with("Binary files ") || line == "GIT binary patch" {
            current_file.as_mut().expect("current file").is_binary = true;
            continue;
        }
        if line.starts_with("@@ ") {
            finish_hunk(&mut current_file, &mut current_hunk);
            let file_key = current_file.as_ref().expect("current file").key();
            let range = parse_hunk_header(line)?;
            old_line = range.old_start;
            new_line = range.new_start;
            current_hunk = Some(ReviewHunk {
                id: HunkId::new(format!(
                    "{}:{}:{}",
                    file_key, range.old_start, range.new_start
                )),
                header: line.to_owned(),
                old_start: range.old_start,
                old_lines: range.old_lines,
                new_start: range.new_start,
                new_lines: range.new_lines,
                rows: Vec::new(),
            });
            continue;
        }

        let Some(hunk) = current_hunk.as_mut() else {
            continue;
        };
        if line.starts_with('\\') {
            continue;
        }

        let Some(marker) = line.as_bytes().first().copied() else {
            continue;
        };
        let text = line[1..].to_owned();
        match marker {
            b' ' => {
                hunk.rows.push(DiffRow {
                    kind: DiffRowKind::Context,
                    old_line: Some(old_line),
                    new_line: Some(new_line),
                    text,
                });
                old_line += 1;
                new_line += 1;
            }
            b'+' => {
                hunk.rows.push(DiffRow {
                    kind: DiffRowKind::Added,
                    old_line: None,
                    new_line: Some(new_line),
                    text,
                });
                new_line += 1;
            }
            b'-' => {
                hunk.rows.push(DiffRow {
                    kind: DiffRowKind::Removed,
                    old_line: Some(old_line),
                    new_line: None,
                    text,
                });
                old_line += 1;
            }
            _ => {}
        }
    }

    finish_hunk(&mut current_file, &mut current_hunk);
    if let Some(file) = current_file {
        files.push(file);
    }

    Ok(files)
}

fn finish_hunk(file: &mut Option<PatchFile>, hunk: &mut Option<ReviewHunk>) {
    if let (Some(file), Some(hunk)) = (file.as_mut(), hunk.take()) {
        file.hunks.push(hunk);
    }
}

fn parse_patch_path(path: &str) -> Option<String> {
    match path {
        "/dev/null" => None,
        path => path
            .strip_prefix("a/")
            .or_else(|| path.strip_prefix("b/"))
            .or(Some(path))
            .map(str::to_owned),
    }
}

fn diff_git_path(line: &str, prefix: char) -> Option<String> {
    match prefix {
        'a' => line
            .strip_prefix("diff --git a/")
            .and_then(|tail| tail.split_once(" b/").map(|(path, _)| path.to_owned())),
        'b' => line.split_once(" b/").map(|(_, path)| path.to_owned()),
        _ => None,
    }
}

#[derive(Debug)]
struct HunkRange {
    old_start: u32,
    old_lines: u32,
    new_start: u32,
    new_lines: u32,
}

fn parse_hunk_header(header: &str) -> Result<HunkRange> {
    let mut parts = header.split_whitespace();
    let _at = parts.next();
    let old = parts
        .next()
        .ok_or_else(|| ShoreError::Message(format!("missing old range in hunk header {header}")))?;
    let new = parts
        .next()
        .ok_or_else(|| ShoreError::Message(format!("missing new range in hunk header {header}")))?;
    let (old_start, old_lines) = parse_range(old, '-')?;
    let (new_start, new_lines) = parse_range(new, '+')?;

    Ok(HunkRange {
        old_start,
        old_lines,
        new_start,
        new_lines,
    })
}

fn parse_range(range: &str, prefix: char) -> Result<(u32, u32)> {
    let range = range
        .strip_prefix(prefix)
        .ok_or_else(|| ShoreError::Message(format!("range {range} missing prefix {prefix}")))?;
    let (start, lines) = match range.split_once(',') {
        Some((start, lines)) => (start, lines),
        None => (range, "1"),
    };
    Ok((
        start.parse().map_err(|error| {
            ShoreError::Message(format!("invalid range start {start}: {error}"))
        })?,
        lines.parse().map_err(|error| {
            ShoreError::Message(format!("invalid range length {lines}: {error}"))
        })?,
    ))
}