contributor_graphs/
model.rs1use serde::Serialize;
2
3#[derive(Debug, Clone)]
5pub struct Commit {
6 pub sha: String,
7 pub ts: i64,
8 pub name: String,
9 pub email: String,
10}
11
12#[derive(Debug, Clone, Serialize)]
15pub struct Contributor {
16 pub name: String,
17 pub login: Option<String>,
18 pub avatar: Option<String>,
19 pub url: Option<String>,
20 pub first: i64,
21 pub last: i64,
22 pub commits: u32,
23 pub bot: bool,
24 pub group: Option<String>,
25 pub members: u32,
28 #[serde(skip_serializing_if = "Vec::is_empty")]
30 pub member_names: Vec<String>,
31 pub m0: i32,
33 pub months: Vec<u32>,
35}
36
37pub fn aggregate_by_group(contributors: &[Contributor], unaffiliated: &str) -> Vec<Contributor> {
40 use std::collections::HashMap;
41
42 struct Agg {
43 commits: u32,
44 first: i64,
45 last: i64,
46 months: HashMap<i32, u32>,
47 members: Vec<(String, u32)>,
48 }
49 let mut order: Vec<String> = Vec::new();
50 let mut map: HashMap<String, Agg> = HashMap::new();
51
52 for c in contributors {
53 let key = c.group.clone().unwrap_or_else(|| unaffiliated.to_string());
54 let agg = map.entry(key.clone()).or_insert_with(|| {
55 order.push(key.clone());
56 Agg {
57 commits: 0,
58 first: i64::MAX,
59 last: i64::MIN,
60 months: HashMap::new(),
61 members: Vec::new(),
62 }
63 });
64 agg.commits += c.commits;
65 agg.first = agg.first.min(c.first);
66 agg.last = agg.last.max(c.last);
67 agg.members.push((c.name.clone(), c.commits));
68 for (i, &v) in c.months.iter().enumerate() {
69 if v > 0 {
70 *agg.months.entry(c.m0 + i as i32).or_insert(0) += v;
71 }
72 }
73 }
74
75 order
76 .into_iter()
77 .map(|key| {
78 let agg = map.remove(&key).unwrap();
79 let m0 = *agg.months.keys().min().unwrap_or(&month_index(agg.first));
80 let m1 = *agg.months.keys().max().unwrap_or(&m0);
81 let len = (m1 - m0 + 1).clamp(1, 6000) as usize;
82 let mut months = vec![0u32; len];
83 for (&m, &v) in &agg.months {
84 if let Some(slot) = months.get_mut((m - m0) as usize) {
85 *slot += v;
86 }
87 }
88 let mut members = agg.members;
89 members.sort_by_key(|(_, commits)| std::cmp::Reverse(*commits));
90 let member_count = members.len() as u32;
91 let member_names = members.into_iter().take(8).map(|(n, _)| n).collect();
92 Contributor {
93 name: key.clone(),
94 login: None,
95 avatar: None,
96 url: None,
97 first: agg.first,
98 last: agg.last,
99 commits: agg.commits,
100 bot: false,
101 group: Some(key),
102 members: member_count,
103 member_names,
104 m0,
105 months,
106 }
107 })
108 .collect()
109}
110
111#[derive(Debug, Clone, Serialize)]
112pub struct RepoMeta {
113 pub name: String,
114 pub url: Option<String>,
115 pub slug: Option<String>,
116 pub branch: String,
117 pub first: i64,
118 pub last: i64,
119 pub total_commits: u64,
120 pub total_contributors: usize,
121 pub generated: String,
122 #[serde(skip_serializing_if = "Option::is_none")]
124 pub owner_avatar: Option<String>,
125}
126
127pub fn month_index(ts: i64) -> i32 {
128 use chrono::{Datelike, TimeZone, Utc};
129 let dt = Utc.timestamp_opt(ts, 0).single().unwrap_or_default();
130 (dt.year() - 1970) * 12 + dt.month0() as i32
131}
132
133pub fn month_start_ts(mi: i32) -> i64 {
134 use chrono::{TimeZone, Utc};
135 let year = 1970 + mi.div_euclid(12);
136 let month = mi.rem_euclid(12) as u32 + 1;
137 Utc.with_ymd_and_hms(year, month, 1, 0, 0, 0)
138 .single()
139 .map(|d| d.timestamp())
140 .unwrap_or_default()
141}
142
143pub fn format_month_year(ts: i64) -> String {
144 use chrono::{TimeZone, Utc};
145 Utc.timestamp_opt(ts, 0)
146 .single()
147 .map(|d| d.format("%b %Y").to_string())
148 .unwrap_or_default()
149}
150
151pub fn thousands(n: u64) -> String {
152 let s = n.to_string();
153 let mut out = String::new();
154 for (i, c) in s.chars().enumerate() {
155 if i > 0 && (s.len() - i).is_multiple_of(3) {
156 out.push(',');
157 }
158 out.push(c);
159 }
160 out
161}