1use serde::Serialize;
2
3#[derive(Debug, Clone)]
7pub struct GroupRule {
8 pub matcher: String,
9 pub group: String,
10 pub since: Option<i64>,
12 pub until: Option<i64>,
14}
15
16impl GroupRule {
17 pub fn covers(&self, ts: i64) -> bool {
19 self.since.is_none_or(|s| ts >= s) && self.until.is_none_or(|u| ts < u)
20 }
21 pub fn dated(&self) -> bool {
22 self.since.is_some() || self.until.is_some()
23 }
24}
25
26#[derive(Debug, Clone, Default, PartialEq, Eq)]
29pub struct CommitFilter {
30 pub since: Option<String>,
31 pub until: Option<String>,
32 pub no_merges: bool,
33}
34
35#[derive(Debug, Clone)]
37pub struct Commit {
38 pub sha: String,
39 pub ts: i64,
40 pub name: String,
41 pub email: String,
42 pub coauthors: Vec<(String, String)>,
44 pub src: u32,
47}
48
49#[derive(Debug, Clone, Serialize)]
52pub struct Contributor {
53 pub name: String,
54 pub login: Option<String>,
55 pub avatar: Option<String>,
56 pub url: Option<String>,
57 pub first: i64,
58 pub last: i64,
59 pub commits: u32,
60 pub bot: bool,
61 pub group: Option<String>,
62 pub members: u32,
65 #[serde(skip_serializing_if = "Vec::is_empty")]
67 pub member_names: Vec<String>,
68 pub m0: i32,
70 pub months: Vec<u32>,
74 #[serde(skip_serializing_if = "Vec::is_empty")]
76 pub co_months: Vec<u32>,
77 #[serde(skip_serializing_if = "is_zero")]
79 pub co_commits: u32,
80 #[serde(skip_serializing_if = "Option::is_none")]
84 pub month_groups: Option<Vec<Option<String>>>,
85}
86
87fn is_zero(n: &u32) -> bool {
88 *n == 0
89}
90
91pub fn aggregate_by_group(contributors: &[Contributor], unaffiliated: &str) -> Vec<Contributor> {
94 use std::collections::HashMap;
95
96 struct Agg {
97 commits: u32,
98 co_commits: u32,
99 first: i64,
100 last: i64,
101 months: HashMap<i32, u32>,
102 co_months: HashMap<i32, u32>,
103 members: Vec<(String, u32)>,
104 }
105 let mut order: Vec<String> = Vec::new();
106 let mut map: HashMap<String, Agg> = HashMap::new();
107 macro_rules! agg {
108 ($key:expr) => {
109 map.entry($key.clone()).or_insert_with(|| {
110 order.push($key.clone());
111 Agg {
112 commits: 0,
113 co_commits: 0,
114 first: i64::MAX,
115 last: i64::MIN,
116 months: HashMap::new(),
117 co_months: HashMap::new(),
118 members: Vec::new(),
119 }
120 })
121 };
122 }
123
124 for c in contributors {
125 let default_key = c.group.clone().unwrap_or_else(|| unaffiliated.to_string());
126 match &c.month_groups {
127 None => {
129 let a = agg!(default_key);
130 a.commits += c.commits;
131 a.co_commits += c.co_commits;
132 a.first = a.first.min(c.first);
133 a.last = a.last.max(c.last);
134 a.members.push((c.name.clone(), c.commits));
135 for (i, &v) in c.months.iter().enumerate() {
136 if v > 0 {
137 *a.months.entry(c.m0 + i as i32).or_insert(0) += v;
138 }
139 }
140 for (i, &v) in c.co_months.iter().enumerate() {
141 if v > 0 {
142 *a.co_months.entry(c.m0 + i as i32).or_insert(0) += v;
143 }
144 }
145 }
146 Some(mg) => {
148 let key_at = |i: usize| {
149 mg.get(i)
150 .cloned()
151 .flatten()
152 .unwrap_or_else(|| default_key.clone())
153 };
154 let mut per_group: HashMap<String, u32> = HashMap::new();
155 for (i, &v) in c.months.iter().enumerate() {
156 if v == 0 {
157 continue;
158 }
159 let key = key_at(i);
160 let m = c.m0 + i as i32;
161 let ts = month_start_ts(m);
162 let a = agg!(key);
163 *a.months.entry(m).or_insert(0) += v;
164 a.commits += v;
165 a.first = a.first.min(ts);
166 a.last = a.last.max(ts);
167 *per_group.entry(key).or_insert(0) += v;
168 }
169 for (i, &v) in c.co_months.iter().enumerate() {
170 if v == 0 {
171 continue;
172 }
173 let key = key_at(i);
174 let a = agg!(key);
175 *a.co_months.entry(c.m0 + i as i32).or_insert(0) += v;
176 a.co_commits += v;
177 }
178 for (key, n) in per_group {
179 agg!(key).members.push((c.name.clone(), n));
180 }
181 }
182 }
183 }
184
185 order
186 .into_iter()
187 .map(|key| {
188 let agg = map.remove(&key).unwrap();
189 let m0 = *agg.months.keys().min().unwrap_or(&month_index(agg.first));
190 let m1 = *agg.months.keys().max().unwrap_or(&m0);
191 let len = (m1 - m0 + 1).clamp(1, 6000) as usize;
192 let mut months = vec![0u32; len];
193 for (&m, &v) in &agg.months {
194 if let Some(slot) = months.get_mut((m - m0) as usize) {
195 *slot += v;
196 }
197 }
198 let mut co_months = vec![0u32; if agg.co_months.is_empty() { 0 } else { len }];
199 for (&m, &v) in &agg.co_months {
200 if let Some(slot) = co_months.get_mut((m - m0) as usize) {
201 *slot += v;
202 }
203 }
204 let mut members = agg.members;
205 members.sort_by_key(|(_, commits)| std::cmp::Reverse(*commits));
206 let member_count = members.len() as u32;
207 let member_names = members.into_iter().take(8).map(|(n, _)| n).collect();
208 Contributor {
209 name: key.clone(),
210 login: None,
211 avatar: None,
212 url: None,
213 first: agg.first,
214 last: agg.last,
215 commits: agg.commits,
216 bot: false,
217 group: Some(key),
218 members: member_count,
219 member_names,
220 m0,
221 months,
222 co_months,
223 co_commits: agg.co_commits,
224 month_groups: None,
225 }
226 })
227 .collect()
228}
229
230#[derive(Debug, Clone, Serialize)]
234pub struct Release {
235 pub name: String,
236 pub ts: i64,
237}
238
239#[derive(Debug, Clone, Serialize)]
240pub struct RepoMeta {
241 pub name: String,
242 pub url: Option<String>,
243 pub slug: Option<String>,
244 pub branch: String,
245 pub first: i64,
246 pub last: i64,
247 pub total_commits: u64,
248 pub total_contributors: usize,
249 pub generated: String,
250 #[serde(skip_serializing_if = "Option::is_none")]
252 pub owner_avatar: Option<String>,
253 #[serde(skip_serializing_if = "Option::is_none")]
255 pub description: Option<String>,
256 #[serde(skip_serializing_if = "Vec::is_empty")]
259 pub releases: Vec<Release>,
260}
261
262pub fn month_index(ts: i64) -> i32 {
263 use chrono::{Datelike, TimeZone, Utc};
264 let dt = Utc.timestamp_opt(ts, 0).single().unwrap_or_default();
265 (dt.year() - 1970) * 12 + dt.month0() as i32
266}
267
268pub fn month_start_ts(mi: i32) -> i64 {
269 use chrono::{TimeZone, Utc};
270 let year = 1970 + mi.div_euclid(12);
271 let month = mi.rem_euclid(12) as u32 + 1;
272 Utc.with_ymd_and_hms(year, month, 1, 0, 0, 0)
273 .single()
274 .map(|d| d.timestamp())
275 .unwrap_or_default()
276}
277
278pub fn format_month_year(ts: i64) -> String {
279 use chrono::{TimeZone, Utc};
280 Utc.timestamp_opt(ts, 0)
281 .single()
282 .map(|d| d.format("%b %Y").to_string())
283 .unwrap_or_default()
284}
285
286pub fn thousands(n: u64) -> String {
287 let s = n.to_string();
288 let mut out = String::new();
289 for (i, c) in s.chars().enumerate() {
290 if i > 0 && (s.len() - i).is_multiple_of(3) {
291 out.push(',');
292 }
293 out.push(c);
294 }
295 out
296}