1use std::collections::HashMap;
2use std::ffi::OsString;
3use std::fmt::{Display, Formatter};
4
5use anyhow::{anyhow, Context};
6use chrono::{DateTime, Datelike, Months, NaiveDateTime, Timelike, Utc, Weekday};
7use lazy_static::lazy_static;
8
9use crate::traits::CommitStatsExt;
10use crate::{
11 Author, CommitArgs, CommitArgsBuilder, CommitDetail, CommitHash, CommitStats, CommitsHeatMap, CommitsPerAuthor,
12 CommitsPerDayHour, CommitsPerMonth, CommitsPerWeekday, Detail, GlobalStat, MinimalCommitDetail, SimpleStat, SortStatsBy,
13};
14
15lazy_static! {
16 static ref AUTHOR_STR_RE: regex::Regex = regex::Regex::new("^(?:\"?([^\"]*)\"?\\s)?(?:<?(.+@[^>]+)?>?)$").unwrap();
17}
18
19impl Author {
22 pub fn new<T: Into<String>>(name: T) -> Self {
23 Author {
24 name: name.into(),
25 email: None,
26 }
27 }
28
29 pub fn with_email(mut self, email: &str) -> Self {
30 self.email = Some(email.to_string());
31 self
32 }
33
34 pub fn with_email_opt(mut self, email: Option<&str>) -> Self {
35 if let Some(email) = email {
36 self.email = Some(email.to_string());
37 } else {
38 self.email = None;
39 }
40 self
41 }
42
43 pub fn from(other: &Author) -> Self {
44 Author {
45 name: other.name.to_string(),
46 email: other.email.clone(),
47 }
48 }
49}
50
51impl<'a> TryFrom<&'a str> for Author {
52 type Error = anyhow::Error;
53
54 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
55 let find = AUTHOR_STR_RE
56 .captures(value)
57 .ok_or(anyhow!("failed to parse author string. Got {:}", value))?;
58
59 if find.len() == 3 {
60 let name = find
61 .get(1)
62 .ok_or(anyhow!("failed to extract author name from {:}", value))?
63 .as_str();
64
65 let email = find.get(2).map_or_else(|| None, |s| Some(s.as_str().to_string()));
66 Ok(Author {
67 name: name.to_string(),
68 email,
69 })
70 } else {
71 Err(anyhow!("invalid author mailmap"))
72 }
73 }
74}
75
76impl TryFrom<String> for Author {
77 type Error = anyhow::Error;
78
79 fn try_from(value: String) -> Result<Self, Self::Error> {
80 value.as_str().try_into()
81 }
82}
83
84impl PartialEq for Author {
85 fn eq(&self, other: &Self) -> bool {
86 let email_match = match &self.email {
87 Some(e1) => match &other.email {
88 Some(e2) => e1.eq_ignore_ascii_case(e2),
89 None => false,
90 },
91 None => false,
92 };
93
94 self.name.eq_ignore_ascii_case(&other.name) || email_match
95 }
96}
97
98impl Eq for Author {}
99
100impl Display for Author {
101 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
102 if let Some(email) = &self.email {
103 write!(f, "{} <{}>", self.name, email.as_str())
104 } else {
105 write!(f, "{} <>", self.name)
106 }
107 }
108}
109
110impl Display for CommitHash {
115 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
116 write!(f, "{}", self.0)
117 }
118}
119
120impl From<&str> for CommitHash {
121 fn from(value: &str) -> Self {
122 CommitHash(value.to_string())
123 }
124}
125
126impl<'a> From<&'a CommitHash> for &'a str {
127 fn from(value: &'a CommitHash) -> Self {
128 &value.0
129 }
130}
131
132impl CommitArgsBuilder {
137 pub fn since(mut self, value: i64) -> Self {
138 self.0.since = Some(value);
139 self
140 }
141
142 pub fn until(mut self, value: i64) -> Self {
143 self.0.until = Some(value);
144 self
145 }
146
147 pub fn exclude_merges(mut self, value: bool) -> Self {
148 self.0.exclude_merges = value;
149 self
150 }
151
152 pub fn target_branch(mut self, value: &str) -> Self {
153 self.0.target_branch = Some(value.to_string());
154 self
155 }
156
157 pub fn author(mut self, value: Author) -> Self {
158 self.0.author = Some(value);
159 self
160 }
161
162 pub fn exclude_author(mut self, value: String) -> Self {
163 self.0.exclude_author = Some(value);
164 self
165 }
166
167 pub fn build(self) -> anyhow::Result<CommitArgs> {
168 self.0.validate()?;
169 Ok(self.0)
170 }
171}
172
173impl CommitArgs {
174 pub fn builder() -> CommitArgsBuilder {
202 CommitArgsBuilder(Default::default())
203 }
204
205 pub(crate) fn validate(&self) -> anyhow::Result<()> {
206 if self.author.is_some() && self.exclude_author.is_some() {
207 return Err(anyhow!("cannot specify both author and exclude_author"));
208 }
209
210 if let Some(since) = self.since {
211 DateTime::from_timestamp(since, 0).context("invalid datetime specified for since")?;
212 }
213
214 if let Some(until) = self.until {
215 DateTime::from_timestamp(until, 0).context("invalid datetime specified for until")?;
216 }
217
218 return Ok(());
219 }
220}
221
222impl IntoIterator for CommitArgs {
223 type Item = OsString;
224 type IntoIter = std::vec::IntoIter<Self::Item>;
225
226 fn into_iter(self) -> Self::IntoIter {
227 let mut args: Vec<OsString> = vec![];
228
229 if let Some(target_branch) = self.target_branch {
230 args.push(target_branch.into());
231 } else {
232 args.push("--all".into());
233 }
234
235 args.push("--pretty=%H".into());
236
237 if let Some(since) = self.since {
238 let datetime = DateTime::from_timestamp(since, 0).unwrap();
239 args.push(format!("--since={:}", datetime.format("%Y-%m-%d").to_string()).into());
240 }
241
242 if let Some(until) = self.until {
243 let datetime = DateTime::from_timestamp(until, 0).unwrap();
244 args.push(format!("--until={:}", datetime.format("%Y-%m-%d").to_string()).into());
245 }
246
247 if let Some(author) = self.author.as_ref() {
248 args.push(format!("--author={:}", author.name).into());
249 }
250
251 if self.exclude_merges {
252 args.push("--no-merges".into());
253 }
254
255 if let Some(exclude_author) = self.exclude_author.as_ref() {
256 args.push("--perl-regexp".into());
257 args.push(format!("--author=^((?!{:}).*)$", exclude_author).into());
258 }
259
260 args.into_iter()
261 }
262}
263
264impl Display for CommitArgs {
265 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
266 let mut s = vec![];
267 if let Some(author) = self.author.as_ref() {
268 s.push(format!("author:{}", author));
269 }
270 if let Some(exclude_author) = self.exclude_author.as_ref() {
271 s.push(format!("exclude author:{}", exclude_author));
272 }
273
274 if self.exclude_merges {
275 s.push("exclude_merges:true".to_string());
276 }
277
278 if let Some(value) = self.target_branch.as_ref() {
279 s.push(format!("target_branch:{}", value));
280 }
281
282 if let Some(value) = self.since.as_ref() {
283 let datetime = DateTime::from_timestamp(*value, 0).unwrap();
284 s.push(format!("since={:}", datetime.format("%Y-%m-%d").to_string()).into());
285 }
286
287 if let Some(value) = self.until.as_ref() {
288 let datetime = DateTime::from_timestamp(*value, 0).unwrap();
289 s.push(format!("until:{:}", datetime.format("%Y-%m-%d").to_string()).into());
290 }
291
292 write!(f, "{}", s.join(", "))
293 }
294}
295
296impl std::ops::Add for CommitStats {
301 type Output = CommitStats;
302
303 fn add(self, rhs: Self) -> Self::Output {
304 CommitStats {
305 files_changed: self.files_changed.saturating_add(rhs.files_changed),
306 lines_added: self.lines_added.saturating_add(rhs.lines_added),
307 lines_deleted: self.lines_deleted.saturating_add(rhs.lines_deleted),
308 }
309 }
310}
311
312impl std::ops::AddAssign for CommitStats {
313 fn add_assign(&mut self, rhs: Self) {
314 self.files_changed = self.files_changed.saturating_add(rhs.files_changed);
315 self.lines_added = self.lines_added.saturating_add(rhs.lines_added);
316 self.lines_deleted = self.lines_deleted.saturating_add(rhs.lines_deleted);
317 }
318}
319
320impl Display for CommitStats {
321 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
322 write!(
323 f,
324 "files changed: {}, lines added: {}, lines deleted: {}",
325 self.files_changed, self.lines_added, self.lines_deleted
326 )
327 }
328}
329
330impl Display for GlobalStat {
335 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
336 write!(
337 f,
338 "author: {}, total commits: {}, {}",
339 self.author, self.commits_count, self.stats
340 )
341 }
342}
343
344impl SimpleStat {
349 pub fn new() -> Self {
350 SimpleStat::default()
351 }
352}
353
354impl Display for SimpleStat {
355 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
356 write!(f, "total commits: {}, {}", self.commits_count, self.stats)
357 }
358}
359
360impl std::ops::Add for SimpleStat {
361 type Output = SimpleStat;
362
363 fn add(self, rhs: Self) -> Self::Output {
364 SimpleStat {
365 commits_count: self.commits_count.saturating_add(rhs.commits_count),
366 stats: self.stats.add(rhs.stats),
367 }
368 }
369}
370
371impl std::ops::AddAssign for SimpleStat {
372 fn add_assign(&mut self, rhs: Self) {
373 self.commits_count = self.commits_count.saturating_add(rhs.commits_count);
374 self.stats = self.stats + rhs.stats;
375 }
376}
377
378impl From<CommitDetail> for SimpleStat {
379 fn from(value: CommitDetail) -> Self {
380 value.stats.into()
381 }
382}
383
384impl From<CommitStats> for SimpleStat {
385 fn from(value: CommitStats) -> Self {
386 SimpleStat {
387 commits_count: 1,
388 stats: value,
389 }
390 }
391}
392
393impl From<CommitDetail> for MinimalCommitDetail {
398 fn from(value: CommitDetail) -> Self {
399 MinimalCommitDetail {
400 hash: value.hash,
401 author_timestamp: value.author_timestamp,
402 stats: value.stats,
403 }
404 }
405}
406
407impl Display for MinimalCommitDetail {
408 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
409 write!(f, "{} {}", self.hash, self.stats)
410 }
411}
412
413impl Default for SortStatsBy {
418 fn default() -> Self {
419 SortStatsBy::Commits
420 }
421}
422
423impl CommitDetail {
428 pub fn get_author_datetime(&self) -> DateTime<Utc> {
429 let naive = NaiveDateTime::from_timestamp_opt(self.author_timestamp, 0).unwrap();
430 DateTime::from_naive_utc_and_offset(naive, Utc)
431 }
432}
433
434impl Display for CommitDetail {
435 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
436 write!(
437 f,
438 "{}, author: {}, {}, {}",
439 self.hash,
440 self.author,
441 self.get_author_datetime(),
442 self.stats
443 )
444 }
445}
446
447impl<'a> CommitStatsExt for Vec<CommitDetail> {
452 fn commits_per_author(&self) -> CommitsPerAuthor {
453 let mut hashmap: HashMap<Author, Vec<MinimalCommitDetail>> = HashMap::new();
454
455 let mut cloned = self.to_vec();
456
457 while !cloned.is_empty() {
458 let commit = cloned.remove(0);
459 let author = commit.author.to_owned();
460 let minimal_commit: MinimalCommitDetail = commit.into();
461 let mut vec: Vec<MinimalCommitDetail> = Vec::new();
462 let mut index = Some(0);
463
464 while index.is_some() {
465 index = cloned.iter().position(|c| {
466 let ca = &c.author;
467 author.eq(ca)
468 });
469
470 if let Some(index) = index {
471 let commit2 = cloned.remove(index);
472 vec.push(commit2.into());
473 }
474 }
475
476 vec.insert(0, minimal_commit);
477 hashmap.insert(author.to_owned(), vec);
478 }
479 CommitsPerAuthor(hashmap)
480 }
481
482 fn commits_per_month(mut self) -> CommitsPerMonth {
483 let mut result: HashMap<String, HashMap<Author, SimpleStat>> = HashMap::new();
484 if self.len() > 1 {
485 let last = self.last().unwrap();
486 let first = self.first().unwrap();
487 let last_date = last.get_author_datetime();
488 let mut first_date = first
489 .get_author_datetime()
490 .with_day(last_date.day())
491 .unwrap()
492 .with_hour(0)
493 .unwrap()
494 .with_minute(0)
495 .unwrap()
496 .with_second(0)
497 .unwrap()
498 .with_nanosecond(0)
499 .unwrap();
500
501 loop {
502 let date_key = first_date.with_day0(0).unwrap().format("%Y-%m").to_string();
503 let mut current_map: HashMap<Author, SimpleStat> = HashMap::new();
504
505 if self.is_empty() {
506 break;
507 }
508
509 loop {
510 if self.is_empty() {
511 break;
512 }
513
514 let commit = self.get(0).unwrap();
515 let commit_datetime = commit.get_author_datetime();
516 if commit_datetime.year() <= first_date.year() && commit_datetime.month() <= first_date.month() {
517 let removed = self.remove(0);
518 let author = removed.author.to_owned();
519 if current_map.contains_key(&author) {
520 *current_map.get_mut(&author).unwrap() += removed.into();
521 } else {
522 current_map.insert(author, removed.into());
523 }
524 } else {
525 break;
526 }
527 }
528 result.insert(date_key, current_map);
529
530 first_date = first_date.checked_add_months(Months::new(1)).unwrap();
531 if first_date > last_date {
532 break;
533 }
534 }
535 }
536 CommitsPerMonth(result)
537 }
538
539 fn commits_per_weekday(mut self) -> CommitsPerWeekday {
540 let mut final_map: HashMap<u8, HashMap<Author, SimpleStat>> = HashMap::from([
541 (Weekday::Mon.num_days_from_monday() as u8, HashMap::new()),
542 (Weekday::Tue.num_days_from_monday() as u8, HashMap::new()),
543 (Weekday::Wed.num_days_from_monday() as u8, HashMap::new()),
544 (Weekday::Thu.num_days_from_monday() as u8, HashMap::new()),
545 (Weekday::Fri.num_days_from_monday() as u8, HashMap::new()),
546 (Weekday::Sat.num_days_from_monday() as u8, HashMap::new()),
547 (Weekday::Sun.num_days_from_monday() as u8, HashMap::new()),
548 ]);
549
550 for commit in self.iter_mut() {
551 let author = commit.author.to_owned();
552 let datetime = commit.get_author_datetime();
553 let weekday = datetime.weekday().num_days_from_monday() as u8;
554 if !final_map.get(&weekday).unwrap().contains_key(&author) {
555 final_map.get_mut(&weekday).unwrap().insert(author.clone(), SimpleStat::new());
556 }
557 *final_map.get_mut(&weekday).unwrap().get_mut(&author).unwrap() += commit.to_owned().into();
558 }
559 CommitsPerWeekday(final_map)
560 }
561
562 fn commits_per_day_hour(self) -> CommitsPerDayHour {
563 let mut final_map: HashMap<u32, HashMap<Author, SimpleStat>> = HashMap::new();
564 for i in 0..24 {
565 final_map.insert(i, HashMap::new());
566 }
567
568 for commit in self.into_iter() {
569 let author = commit.author.to_owned();
570 let datetime = commit.get_author_datetime();
571 let hour = datetime.hour();
572 if !final_map.get(&hour).unwrap().contains_key(&author) {
573 final_map.get_mut(&hour).unwrap().insert(author, commit.into());
574 } else {
575 *final_map.get_mut(&hour).unwrap().get_mut(&author).unwrap() += commit.into();
576 }
577 }
578 CommitsPerDayHour(final_map)
579 }
580
581 fn commits_heatmap(self) -> CommitsHeatMap {
582 let mut final_map: HashMap<Author, Vec<Vec<SimpleStat>>> = HashMap::new();
584 for commit in self.into_iter() {
585 let author = commit.author.to_owned();
586
587 if !final_map.contains_key(&author) {
588 let mut rows = Vec::new();
589 for _weekday in 0..7 {
590 let mut row = Vec::new();
591 for _hour in 0..24 {
592 row.push(SimpleStat::new());
593 }
594 rows.push(row);
595 }
596 final_map.insert(author.clone(), rows);
597 }
598
599 let datetime = commit.get_author_datetime();
600 let weekday = datetime.weekday().num_days_from_monday() as usize;
601 let hour = datetime.hour() as usize;
602
603 *final_map
604 .get_mut(&author)
605 .unwrap()
606 .get_mut(weekday)
607 .unwrap()
608 .get_mut(hour)
609 .unwrap() += commit.into();
610 }
611
612 CommitsHeatMap(final_map)
613 }
614}
615
616impl CommitsPerWeekday {
621 pub fn detailed_stats(&self) -> &HashMap<u8, HashMap<Author, SimpleStat>> {
622 &self.0
623 }
624
625 pub fn global_stats(&self) -> HashMap<u8, SimpleStat> {
626 let mut global_map: HashMap<u8, SimpleStat> = HashMap::new();
627 for (key, value) in self.0.iter() {
628 global_map.insert(*key, SimpleStat::new());
629 for (_, stats) in value.iter() {
630 *global_map.get_mut(key).unwrap() += stats.clone();
631 }
632 }
633 global_map
634 }
635}
636
637impl CommitsPerDayHour {
642 pub fn detailed_stats(&self) -> &HashMap<u32, HashMap<Author, SimpleStat>> {
643 &self.0
644 }
645
646 pub fn global_stats(&self) -> HashMap<u32, SimpleStat> {
647 let mut global_map: HashMap<u32, SimpleStat> = HashMap::new();
648 for (key, value) in self.0.iter() {
649 global_map.insert(key.clone(), SimpleStat::new());
650 for (_, stats) in value.iter() {
651 *global_map.get_mut(key).unwrap() += stats.clone();
652 }
653 }
654 global_map
655 }
656}
657
658impl CommitsPerMonth {
663 pub fn detailed_stats(&self) -> &HashMap<String, HashMap<Author, SimpleStat>> {
664 &self.0
665 }
666
667 pub fn global_stats(&self) -> HashMap<String, SimpleStat> {
668 let mut global_map: HashMap<String, SimpleStat> = HashMap::new();
669 for (key, value) in self.0.iter() {
670 global_map.insert(key.clone(), SimpleStat::new());
671 for (_, stats) in value.iter() {
672 *global_map.get_mut(key).unwrap() += stats.clone();
673 }
674 }
675 global_map
676 }
677}
678
679impl CommitsHeatMap {
684 pub fn detailed_stats(&self) -> &HashMap<Author, Vec<Vec<SimpleStat>>> {
685 &self.0
686 }
687
688 pub fn global_stats(&self) -> Vec<Vec<SimpleStat>> {
689 let mut final_map: Vec<Vec<SimpleStat>> = Vec::new();
692 for _weekday in 0..7 {
693 let mut row = Vec::new();
694 for _hour in 0..24 {
695 row.push(SimpleStat::new());
696 }
697 final_map.push(row);
698 }
699
700 for (_author, author_stats) in self.0.iter() {
701 for (weekday, weekday_stats) in author_stats.iter().enumerate() {
702 for (hour, hour_stats) in weekday_stats.iter().enumerate() {
703 *final_map.get_mut(weekday).unwrap().get_mut(hour).unwrap() += hour_stats.clone();
704 }
705 }
706 }
707
708 final_map
709 }
710}
711
712impl CommitsPerAuthor {
717 pub fn detailed_stats(&self) -> &HashMap<Author, Vec<MinimalCommitDetail>> {
718 &self.0
719 }
720
721 pub fn global_stats(&self, sort_stats_by: SortStatsBy) -> Vec<GlobalStat> {
722 let mut global_stats = self
723 .0
724 .iter()
725 .map(|(key, value)| {
726 let stats = value.iter().map(|item| item.stats).reduce(|acc, item| acc + item).unwrap();
727 let total_commits = value.len();
728 GlobalStat {
729 author: Author::from(key),
730 commits_count: total_commits,
731 stats,
732 }
733 })
734 .collect::<Vec<_>>();
735
736 match sort_stats_by {
737 SortStatsBy::Commits => global_stats.sort_by_key(|item| item.commits_count),
738 SortStatsBy::FilesChanged => global_stats.sort_by_key(|item| item.stats.files_changed),
739 SortStatsBy::LinesAdded => global_stats.sort_by_key(|item| item.stats.lines_added),
740 SortStatsBy::LinesDeleted => global_stats.sort_by_key(|item| item.stats.lines_deleted),
741 }
742
743 global_stats.reverse();
744 global_stats
745 }
746}
747
748impl Display for Detail {
753 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
754 let mut strings = vec![];
755 strings.push(format!("size={}", self.size));
756 strings.push(format!("commits_count={}", self.commits_count));
757 if let Some(value) = self.first_commit {
758 if let Some(naive) = NaiveDateTime::from_timestamp_opt(value, 0) {
759 let datetime: DateTime<Utc> = DateTime::from_naive_utc_and_offset(naive, Utc);
760 strings.push(format!("first_commit={}", datetime));
761 }
762 }
763 if let Some(value) = self.last_commit {
764 if let Some(naive) = NaiveDateTime::from_timestamp_opt(value, 0) {
765 let datetime: DateTime<Utc> = DateTime::from_naive_utc_and_offset(naive, Utc);
766 strings.push(format!("last_commit={}", datetime));
767 }
768 }
769 write!(f, "{}", strings.join(", "))
770 }
771}
772
773