gitstats/
impls.rs

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
19// region Author
20
21impl 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
110// endregion Author
111
112// region CommitHash
113
114impl 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
132// endregion CommitHash
133
134// region CommitArgs
135
136impl 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	/// Creates a new builder
175	/// # Examples:
176	/// ```rust
177	///
178	/// use chrono::{Months, Utc};
179	///
180	///
181	///
182	/// use gitstats::{Author, CommitArgs};
183	/// use gitstats::Repo;
184	///
185	///
186	///
187	/// pub fn main() {
188	/// let repo = Repo::new("/custom/path");
189	/// 	let args = CommitArgs::builder()
190	/// 		.author(Author::try_from("Alessandro Crugnola <alessandro.crugnola@gmail.com>").unwrap())
191	/// 		.since(Utc::now().checked_sub_months(Months::new(3)).unwrap().timestamp())
192	/// 		.until(Utc::now().timestamp())
193	/// 		.exclude_merges(true)
194	/// 		.target_branch("develop")
195	/// 		.build().unwrap();
196	/// 	if let Ok(result) = repo.list_commits(args) {
197	///         println!("got commits: {}", result);
198	///     }
199	/// }
200	/// ```
201	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
296// endregion CommitArgs
297
298// region CommitStats
299
300impl 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
330// endregion CommitStats
331
332// region GlobalStat
333
334impl 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
344// endregion GlobalStat
345
346// region SimpleStat
347
348impl 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
393// endregion SimpleStat
394
395// region MinimalCommitDetail
396
397impl 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
413// endregion MinimalCommitDetail
414
415// region SortStatsBy
416
417impl Default for SortStatsBy {
418	fn default() -> Self {
419		SortStatsBy::Commits
420	}
421}
422
423// endregion SortStatsBy
424
425// region CommitDetail
426
427impl 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
447// endregion CommitDetail
448
449// region CommitStatsExt
450
451impl<'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		// hashmap per author -> vec[hour] of vec[stats]
583		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
616// endregion CommitStatsExt
617
618// region CommitsPerWeekday
619
620impl 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
637// endregion CommitsPerWeekday
638
639// region CommitsPerDayHour
640
641impl 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
658// endregion CommitsPerDayHour
659
660// region CommitsPerMonth
661
662impl 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
679// endregion CommitsPerMonth
680
681// region CommitsHeatmap
682
683impl 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		// weekday x hour
690
691		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
712// endregion CommitsHeatmap
713
714// region CommitsPerAuthor
715
716impl 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
748// endregion CommitsPerAuthor
749
750// region Detail
751
752impl 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// endregion Detail