gmap 0.3.2

Git repository analysis tool for churn and heatmap visualization
Documentation
use crate::cache::Cache;
use crate::tui::WeekStats;
use super::FileExtensionStats;
use crate::model::CommitStats;
use crate::error::{Result, GmapError};
use chrono::{DateTime, Datelike, Utc};
use std::collections::HashMap;
use std::path::Path;
use crate::model::HeatBucket;

pub fn aggregate_weeks(stats: &[CommitStats], cache: &Cache, path_prefix: Option<&str>) -> Vec<WeekStats> {
    let mut week_map: HashMap<String, (usize, usize, usize, HashMap<String, usize>, HashMap<String, FileExtensionStats>, HashMap<String, usize>)> = HashMap::new();

    for commit_stats in stats {
        let commit_info = match cache.get_commit_info(&commit_stats.commit_id) {
            Ok(Some(info)) => info,
            _ => continue,
        };

        let week_key = format!("{}-W{:02}", commit_info.timestamp.year(), commit_info.timestamp.iso_week().week());
        let mut added = 0usize;
        let mut deleted = 0usize;
        let mut has_matching_files = false;

        for file_stats in &commit_stats.files {
            if let Some(prefix) = path_prefix {
                if !file_stats.path.starts_with(prefix) {
                    continue;
                }
            }
            has_matching_files = true;
            added += file_stats.added_lines as usize;
            deleted += file_stats.deleted_lines as usize;
        }

        if has_matching_files || path_prefix.is_none() {
            let entry = week_map.entry(week_key.clone()).or_insert((0, 0, 0, HashMap::new(), HashMap::new(), HashMap::new()));
            entry.0 += 1;
            entry.1 += added;
            entry.2 += deleted;
            *entry.3.entry(commit_info.author_name.clone()).or_insert(0) += 1;

            for file_stats in &commit_stats.files {
                if let Some(prefix) = path_prefix {
                    if !file_stats.path.starts_with(prefix) {
                        continue;
                    }
                }

                let extension = Path::new(&file_stats.path)
                    .extension()
                    .and_then(|s| s.to_str())
                    .unwrap_or("")
                    .to_lowercase();

                let ext_entry = entry.4.entry(extension).or_insert(FileExtensionStats {
                    commits: 0,
                    lines_added: 0,
                    lines_deleted: 0,
                    files_changed: 0,
                });
                ext_entry.commits += 1;
                ext_entry.lines_added += file_stats.added_lines as usize;
                ext_entry.lines_deleted += file_stats.deleted_lines as usize;
                ext_entry.files_changed += 1;

                *entry.5.entry(file_stats.path.clone()).or_insert(0) += 1;
            }
        }
    }

    let mut weeks: Vec<WeekStats> = week_map.into_iter().map(|(week, (commits, added, deleted, authors, file_extensions, file_changes))| {
        let mut top_authors: Vec<_> = authors.into_iter().collect();
        top_authors.sort_by(|a, b| b.1.cmp(&a.1));
        let top_authors = top_authors.into_iter().map(|(name, _)| name).take(3).collect();

        let mut top_files: Vec<_> = file_changes.into_iter().collect();
        top_files.sort_by(|a, b| b.1.cmp(&a.1));
        let top_files = top_files.into_iter().take(10).collect();

        WeekStats {
            week,
            commits,
            lines_added: added,
            lines_deleted: deleted,
            top_authors,
            file_extensions,
            top_files,
        }
    }).collect();

    weeks.sort_by(|a, b| a.week.cmp(&b.week));
    weeks
}

pub fn compute_heat(
    stats: &[CommitStats],
    cache: &Cache,
    path_prefix: Option<&str>,
) -> Result<Vec<HeatBucket>> {
    let mut week_map: HashMap<String, (u32, u64)> = HashMap::new();

    for commit_stats in stats {
        let commit_info = cache
            .get_commit_info(&commit_stats.commit_id)?
            .ok_or_else(|| GmapError::Cache("Commit info not found".to_string()))?;

        let week_key = get_week_key(&commit_info.timestamp);

        let mut lines_changed = 0u64;
        let mut has_matching_files = false;

        for file_stats in &commit_stats.files {
            if let Some(prefix) = path_prefix {
                if !file_stats.path.starts_with(prefix) {
                    continue;
                }
            }
            has_matching_files = true;
            lines_changed += (file_stats.added_lines + file_stats.deleted_lines) as u64;
        }

        if has_matching_files || path_prefix.is_none() {
            let entry = week_map.entry(week_key).or_insert((0, 0));
            entry.0 += 1;
            entry.1 += lines_changed;
        }
    }

    let mut buckets: Vec<_> = week_map
        .into_iter()
        .map(|(week, (commit_count, lines_changed))| HeatBucket {
            week,
            commit_count,
            lines_changed,
        })
        .collect();

    buckets.sort_by(|a, b| a.week.cmp(&b.week));
    Ok(buckets)
}

fn get_week_key(timestamp: &DateTime<Utc>) -> String {
    format!("{}-W{:02}", timestamp.year(), timestamp.iso_week().week())
}