rpo 0.1.0-beta.4

Git contribution analysis: commits, file changes, and per-line authorship over time as polars DataFrames
//! Blame frame builder. See spec §4.3.

use polars::prelude::*;

use super::BlameRecord;
use crate::RpoError;

pub fn build(records: Vec<BlameRecord>) -> Result<DataFrame, RpoError> {
    let n = records.len();
    let mut snap_sha = Vec::with_capacity(n);
    let mut snap_time = Vec::with_capacity(n);
    let mut snap_label: Vec<Option<String>> = Vec::with_capacity(n);
    let mut path = Vec::with_capacity(n);
    let mut start_line = Vec::with_capacity(n);
    let mut line_count = Vec::with_capacity(n);
    let mut commit_sha = Vec::with_capacity(n);
    let mut can_an = Vec::with_capacity(n);
    let mut can_ae = Vec::with_capacity(n);
    let mut can_cn = Vec::with_capacity(n);
    let mut can_ce = Vec::with_capacity(n);
    let mut commit_time = Vec::with_capacity(n);
    let mut extension: Vec<Option<String>> = Vec::with_capacity(n);
    let mut is_generated = Vec::with_capacity(n);
    let mut is_vendored = Vec::with_capacity(n);

    for r in records {
        snap_sha.push(r.snapshot_sha);
        snap_time.push(r.snapshot_time_ms);
        snap_label.push(r.snapshot_label);
        path.push(r.path);
        start_line.push(r.start_line);
        line_count.push(r.line_count);
        commit_sha.push(r.commit_sha);
        can_an.push(r.canonical_author_name);
        can_ae.push(r.canonical_author_email);
        can_cn.push(r.canonical_committer_name);
        can_ce.push(r.canonical_committer_email);
        commit_time.push(r.commit_time_ms);
        extension.push(r.extension);
        is_generated.push(false); // v1: always false (spec §4.3)
        is_vendored.push(false);
    }

    let sn_time = Column::new("snapshot_time".into(), snap_time).cast(&DataType::Datetime(
        TimeUnit::Milliseconds,
        Some(TimeZone::UTC),
    ))?;
    let ct = Column::new("commit_time".into(), commit_time).cast(&DataType::Datetime(
        TimeUnit::Milliseconds,
        Some(TimeZone::UTC),
    ))?;

    let df = DataFrame::new_infer_height(vec![
        Column::new("snapshot_sha".into(), snap_sha),
        sn_time,
        Column::new("snapshot_label".into(), snap_label),
        Column::new("path".into(), path),
        Column::new("start_line".into(), start_line),
        Column::new("line_count".into(), line_count),
        Column::new("commit_sha".into(), commit_sha),
        Column::new("canonical_author_name".into(), can_an),
        Column::new("canonical_author_email".into(), can_ae),
        Column::new("canonical_committer_name".into(), can_cn),
        Column::new("canonical_committer_email".into(), can_ce),
        ct,
        Column::new("extension".into(), extension),
        Column::new("is_generated".into(), is_generated),
        Column::new("is_vendored".into(), is_vendored),
    ])?;
    Ok(df)
}

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

    #[test]
    fn empty_input_produces_fifteen_column_zero_row_frame() {
        let df = build(vec![]).unwrap();
        assert_eq!(df.height(), 0);
        assert_eq!(df.width(), 15);
    }
}