rpo 0.1.0-beta.4

Git contribution analysis: commits, file changes, and per-line authorship over time as polars DataFrames
//! File changes frame. See spec ยง4.2.

use polars::prelude::*;

use super::{FileChangeRecord, change_kind_str};
use crate::RpoError;

pub fn build(records: Vec<FileChangeRecord>) -> Result<DataFrame, RpoError> {
    let n = records.len();
    let mut sha = Vec::with_capacity(n);
    let mut commit_time = 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 path = Vec::with_capacity(n);
    let mut old_path: Vec<Option<String>> = Vec::with_capacity(n);
    let mut kind = Vec::with_capacity(n);
    let mut insertions = Vec::with_capacity(n);
    let mut deletions = 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 {
        sha.push(r.sha);
        commit_time.push(r.commit_time_ms);
        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);
        path.push(r.path);
        old_path.push(r.old_path);
        kind.push(change_kind_str(r.change_kind).to_string());
        insertions.push(r.insertions);
        deletions.push(r.deletions);
        extension.push(r.extension);
        is_generated.push(r.is_generated);
        is_vendored.push(r.is_vendored);
    }

    let ct = Column::new("commit_time".into(), commit_time).cast(&DataType::Datetime(
        TimeUnit::Milliseconds,
        Some(TimeZone::UTC),
    ))?;
    let kind_col = Column::new("change_kind".into(), kind)
        .cast(&DataType::from_categories(Categories::global()))?;

    let df = DataFrame::new_infer_height(vec![
        Column::new("sha".into(), sha),
        ct,
        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),
        Column::new("path".into(), path),
        Column::new("old_path".into(), old_path),
        kind_col,
        Column::new("insertions".into(), insertions),
        Column::new("deletions".into(), deletions),
        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::*;
    use crate::backend::ChangeKind;

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

    #[test]
    fn records_produce_rows() {
        let rec = FileChangeRecord {
            sha: "deadbeef".into(),
            commit_time_ms: 0,
            canonical_author_name: "A".into(),
            canonical_author_email: "a@a".into(),
            canonical_committer_name: "A".into(),
            canonical_committer_email: "a@a".into(),
            path: "src/lib.rs".into(),
            old_path: None,
            change_kind: ChangeKind::Added,
            insertions: 10,
            deletions: 0,
            extension: Some("rs".into()),
            is_generated: false,
            is_vendored: false,
        };
        let df = build(vec![rec]).unwrap();
        assert_eq!(df.height(), 1);
        assert_eq!(df.column("change_kind").unwrap().len(), 1);
    }
}