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::FileStatus;

#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct RawFile {
    pub status: FileStatus,
    pub type_change: bool,
    pub old_mode: Option<String>,
    pub new_mode: Option<String>,
    pub old_oid: Option<String>,
    pub new_oid: Option<String>,
    pub similarity: Option<u16>,
    pub old_path: Option<String>,
    pub new_path: Option<String>,
}

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

    pub fn is_submodule(&self) -> bool {
        self.old_mode.as_deref() == Some("160000") || self.new_mode.as_deref() == Some("160000")
    }

    pub fn has_mode_change(&self) -> bool {
        self.status == FileStatus::Modified
            && self.old_mode != self.new_mode
            && !self.is_submodule()
    }
}

pub(crate) fn parse_raw(raw: &[u8]) -> Result<Vec<RawFile>> {
    let fields = raw
        .split(|byte| *byte == 0)
        .filter(|field| !field.is_empty())
        .map(|field| {
            std::str::from_utf8(field)
                .map(str::to_owned)
                .map_err(|error| {
                    ShoreError::Message(format!("git raw output is not utf-8: {error}"))
                })
        })
        .collect::<Result<Vec<_>>>()?;

    let mut files = Vec::new();
    let mut index = 0;
    while index < fields.len() {
        let header = &fields[index];
        index += 1;
        let parsed_header = parse_header(header)?;

        let path = fields
            .get(index)
            .ok_or_else(|| ShoreError::Message(format!("missing path for raw header {header}")))?
            .clone();
        index += 1;
        let second_path = if matches!(
            parsed_header.status,
            FileStatus::Renamed | FileStatus::Copied
        ) {
            let second_path = fields
                .get(index)
                .ok_or_else(|| {
                    ShoreError::Message(format!("missing second path for raw header {header}"))
                })?
                .clone();
            index += 1;
            Some(second_path)
        } else {
            None
        };

        let (old_path, new_path) = match parsed_header.status {
            FileStatus::Modified => (Some(path.clone()), Some(path)),
            FileStatus::Added => (None, Some(path)),
            FileStatus::Deleted => (Some(path), None),
            FileStatus::Renamed | FileStatus::Copied => (Some(path), second_path),
        };
        files.push(RawFile {
            status: parsed_header.status,
            type_change: parsed_header.type_change,
            old_mode: mode(parsed_header.old_mode),
            new_mode: mode(parsed_header.new_mode),
            old_oid: oid(parsed_header.old_oid),
            new_oid: oid(parsed_header.new_oid),
            similarity: parsed_header.similarity,
            old_path,
            new_path,
        });
    }

    Ok(files)
}

#[derive(Debug)]
struct RawHeader<'a> {
    old_mode: &'a str,
    new_mode: &'a str,
    old_oid: &'a str,
    new_oid: &'a str,
    status: FileStatus,
    type_change: bool,
    similarity: Option<u16>,
}

fn parse_header(header: &str) -> Result<RawHeader<'_>> {
    let parts = header.split_whitespace().collect::<Vec<_>>();
    if parts.len() != 5 {
        return Err(ShoreError::Message(format!(
            "expected 5 raw header fields, got {} in {header}",
            parts.len()
        )));
    }
    let status = parts[4];
    let raw_status = status.chars().next();
    let file_status = match raw_status {
        Some('M') => FileStatus::Modified,
        Some('A') => FileStatus::Added,
        Some('D') => FileStatus::Deleted,
        Some('R') => FileStatus::Renamed,
        Some('C') => FileStatus::Copied,
        Some('T') => FileStatus::Modified,
        _ => {
            return Err(ShoreError::Message(format!(
                "unsupported raw status {status}"
            )));
        }
    };
    let similarity = status
        .get(1..)
        .filter(|score| !score.is_empty())
        .map(|score| {
            score.parse().map_err(|error| {
                ShoreError::Message(format!("invalid similarity {score}: {error}"))
            })
        })
        .transpose()?;

    Ok(RawHeader {
        old_mode: parts[0].trim_start_matches(':'),
        new_mode: parts[1],
        old_oid: parts[2],
        new_oid: parts[3],
        status: file_status,
        type_change: raw_status == Some('T'),
        similarity,
    })
}

fn mode(mode: &str) -> Option<String> {
    (mode != "000000").then(|| mode.to_owned())
}

fn oid(oid: &str) -> Option<String> {
    (!oid.chars().all(|ch| ch == '0')).then(|| oid.to_owned())
}