rpo 0.1.0-beta.4

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

use polars::prelude::*;

use super::CommitRecord;
use crate::RpoError;

pub fn build(records: Vec<CommitRecord>) -> Result<DataFrame, RpoError> {
    let n = records.len();

    let mut sha = Vec::with_capacity(n);
    let mut short_sha = Vec::with_capacity(n);
    let mut author_name = Vec::with_capacity(n);
    let mut author_email = Vec::with_capacity(n);
    let mut canon_an = Vec::with_capacity(n);
    let mut canon_ae = Vec::with_capacity(n);
    let mut committer_name = Vec::with_capacity(n);
    let mut committer_email = Vec::with_capacity(n);
    let mut canon_cn = Vec::with_capacity(n);
    let mut canon_ce = Vec::with_capacity(n);
    let mut author_time = Vec::with_capacity(n);
    let mut commit_time = Vec::with_capacity(n);
    let mut parent_count = Vec::with_capacity(n);
    let mut is_merge = Vec::with_capacity(n);
    let mut subject = Vec::with_capacity(n);
    let mut files_changed = Vec::with_capacity(n);
    let mut insertions = Vec::with_capacity(n);
    let mut deletions = Vec::with_capacity(n);

    for r in records {
        sha.push(r.sha);
        short_sha.push(r.short_sha);
        author_name.push(r.author_name);
        author_email.push(r.author_email);
        canon_an.push(r.canonical_author_name);
        canon_ae.push(r.canonical_author_email);
        committer_name.push(r.committer_name);
        committer_email.push(r.committer_email);
        canon_cn.push(r.canonical_committer_name);
        canon_ce.push(r.canonical_committer_email);
        author_time.push(r.author_time_ms);
        commit_time.push(r.commit_time_ms);
        parent_count.push(r.parent_count);
        is_merge.push(r.is_merge);
        subject.push(r.message_subject);
        files_changed.push(r.files_changed);
        insertions.push(r.insertions);
        deletions.push(r.deletions);
    }

    let at = Column::new("author_time".into(), author_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("sha".into(), sha),
        Column::new("short_sha".into(), short_sha),
        Column::new("author_name".into(), author_name),
        Column::new("author_email".into(), author_email),
        Column::new("canonical_author_name".into(), canon_an),
        Column::new("canonical_author_email".into(), canon_ae),
        Column::new("committer_name".into(), committer_name),
        Column::new("committer_email".into(), committer_email),
        Column::new("canonical_committer_name".into(), canon_cn),
        Column::new("canonical_committer_email".into(), canon_ce),
        at,
        ct,
        Column::new("parent_count".into(), parent_count),
        Column::new("is_merge".into(), is_merge),
        Column::new("message_subject".into(), subject),
        Column::new("files_changed".into(), files_changed),
        Column::new("insertions".into(), insertions),
        Column::new("deletions".into(), deletions),
    ])?;
    Ok(df)
}

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

    fn record(sha: &str) -> CommitRecord {
        CommitRecord {
            sha: sha.into(),
            short_sha: sha[..7].into(),
            author_name: "A".into(),
            author_email: "a@a".into(),
            canonical_author_name: "A".into(),
            canonical_author_email: "a@a".into(),
            committer_name: "A".into(),
            committer_email: "a@a".into(),
            canonical_committer_name: "A".into(),
            canonical_committer_email: "a@a".into(),
            author_time_ms: 1_700_000_000_000,
            commit_time_ms: 1_700_000_000_000,
            parent_count: 1,
            is_merge: false,
            message_subject: "x".into(),
            files_changed: 2,
            insertions: 10,
            deletions: 5,
        }
    }

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

    #[test]
    fn single_record_roundtrips() {
        let df = build(vec![record("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")]).unwrap();
        assert_eq!(df.height(), 1);
        let sha_col = df.column("sha").unwrap();
        assert_eq!(
            sha_col.str().unwrap().get(0).unwrap(),
            "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
        );
    }
}