1use crate::{
2 author::Author,
3 core::{naughty_word, split_into_clean_words},
4};
5use git2::{Commit, Repository};
6#[cfg(feature = "json")]
7use serde::Serialize;
8use std::{collections::HashMap, env, error::Error, io, io::Write, path::Path};
9#[cfg(feature = "table")]
10use tabwriter::TabWriter;
11
12#[derive(Debug)]
14#[cfg_attr(feature = "json", derive(Serialize))]
15pub struct Repo {
16 pub name: String,
18 pub total_commits: usize,
20 pub total_curses: usize,
22 pub curses: HashMap<String, usize>,
24 pub authors: HashMap<String, Author>,
26}
27
28impl Repo {
29 pub fn new(path: &Path) -> Result<Self, Box<dyn Error>> {
31 let repo = Repository::open(path)?;
32 let commits = Repo::commits(&repo)?;
33
34 let repo = match path.file_name() {
35 Some(path) => path.to_str().unwrap().to_owned(),
36 None => env::current_dir()?.to_str().unwrap().to_owned(),
37 };
38
39 let mut repo = Repo {
40 name: repo,
41 total_commits: 0,
42 total_curses: 0,
43 curses: HashMap::new(),
44 authors: HashMap::new(),
45 };
46
47 repo.build(commits);
48 repo.count_curses();
49
50 Ok(repo)
51 }
52
53 pub fn author(&mut self, author_name: &str) -> &mut Author {
56 if !self.authors.contains_key(author_name) {
57 self.authors
58 .entry(author_name.into())
59 .or_insert_with(|| Author::new(author_name));
60 }
61
62 self.authors.get_mut(author_name).expect("exists")
63 }
64
65 pub fn count_curses(&mut self) {
67 for author in self.authors.values() {
68 for (name, curse) in &author.curses {
69 self.curses
70 .entry(name.to_string())
71 .and_modify(|c| *c += *curse)
72 .or_insert_with(|| *curse);
73 }
74 }
75 }
76
77 fn total_naughty_authors(&self) -> usize {
79 self.authors.values().filter(|a| a.is_naughty()).count()
80 }
81
82 #[cfg(feature = "json")]
84 pub fn print_json(&self) -> Result<(), Box<dyn Error>> {
85 let serialized = serde_json::to_string(&self)?;
86 write!(io::stdout(), "{}", serialized)?;
87 io::stdout().flush()?;
88
89 Ok(())
90 }
91
92 #[cfg(feature = "table")]
94 pub fn print_list(&self) -> Result<(), Box<dyn Error>> {
95 let mut tw = TabWriter::new(vec![]);
96 let curses = Repo::sort(&self.curses);
97
98 self.table_headers(&mut tw, &curses)?;
99 self.table_separators(&mut tw, &curses)?;
100 self.table_authors(&mut tw, &curses)?;
101
102 if self.total_naughty_authors() > 1 {
103 self.table_separators(&mut tw, &curses)?;
104 self.table_total(&mut tw, &curses)?;
105 }
106
107 tw.flush()?;
108
109 write!(io::stdout(), "{}", String::from_utf8(tw.into_inner()?)?)?;
110 io::stdout().flush()?;
111
112 Ok(())
113 }
114
115 #[cfg(feature = "table")]
117 fn sort(curses: &HashMap<String, usize>) -> Vec<(String, usize)> {
118 let mut curses: Vec<(&String, &usize)> = curses.iter().collect();
119 curses.sort_by(|(a, _), (b, _)| a.cmp(b));
120 let curses: Vec<_> = curses
121 .iter()
122 .map(|(c, i)| ((*c).to_string(), **i))
123 .collect();
124 curses
125 }
126
127 #[cfg(feature = "table")]
129 fn table_headers(
130 &self,
131 tw: &mut TabWriter<Vec<u8>>,
132 curses: &[(String, usize)],
133 ) -> Result<(), Box<dyn Error>> {
134 let mut header = String::new();
135 header.push_str("Author");
136 header.push_str("\t");
137
138 curses
139 .iter()
140 .for_each(|(curse, _)| header.push_str(&[curse, "\t"].concat()));
141
142 header.push_str(&["Total", "\t"].concat());
143
144 writeln!(tw, "{}", header)?;
145
146 Ok(())
147 }
148
149 #[cfg(feature = "table")]
151 fn table_separators(
152 &self,
153 tw: &mut TabWriter<Vec<u8>>,
154 curses: &[(String, usize)],
155 ) -> Result<(), Box<dyn Error>> {
156 let mut sep = String::new();
157 sep.push_str(&[&"-".repeat("Author".len()), "\t"].concat());
158
159 curses
160 .iter()
161 .map(|(curse, _)| (curse, curse.len()))
162 .for_each(|(_, curse_len)| sep.push_str(&[&"-".repeat(curse_len), "\t"].concat()));
163
164 sep.push_str(&[&"-".repeat("Total".len()), "\t"].concat());
165
166 writeln!(tw, "{}", sep)?;
167 Ok(())
168 }
169
170 #[cfg(feature = "table")]
172 fn table_authors(
173 &self,
174 tw: &mut TabWriter<Vec<u8>>,
175 curses: &[(String, usize)],
176 ) -> Result<(), Box<dyn Error>> {
177 let mut authors: Vec<_> = self.authors.values().collect();
178 authors.sort_unstable_by_key(|a| &a.name);
179
180 for author in authors {
181 if author.is_naughty() {
182 let mut out = String::new();
183 out.push_str(&[&author.name, "\t"].concat());
184 for (curse, _) in curses {
187 if let Some(count) = author.curses.get(curse) {
188 out.push_str(&[&count.to_string(), "\t"].concat());
189 } else {
190 out.push_str("0\t");
191 }
192 }
193 out.push_str(&author.curses.values().sum::<usize>().to_string());
194
195 writeln!(tw, "{}", out)?;
196 }
197 }
198
199 Ok(())
200 }
201
202 #[cfg(feature = "table")]
204 fn table_total(
205 &self,
206 tw: &mut TabWriter<Vec<u8>>,
207 curses: &[(String, usize)],
208 ) -> Result<(), Box<dyn Error>> {
209 let mut out = String::new();
210
211 out.push_str(&["Overall", "\t"].concat());
212
213 curses
214 .iter()
215 .for_each(|(_, count)| out.push_str(&[&count.to_string(), "\t"].concat()));
216
217 out.push_str(&self.total_curses.to_string());
218
219 writeln!(tw, "{}", out)?;
220
221 Ok(())
222 }
223
224 pub fn commits(repo: &Repository) -> Result<Vec<Commit>, Box<dyn Error>> {
226 let mut revwalk = repo.revwalk()?;
227 let mut commits: Vec<Commit> = Vec::new();
228 revwalk.push_head()?;
229 for commit_id in revwalk {
230 let commit = repo.find_commit(commit_id?)?;
231 commits.push(commit);
232 }
233
234 Ok(commits)
235 }
236
237 pub fn build(&mut self, commits: Vec<Commit>) {
240 for commit in &commits {
241 if let (Some(author_name), Some(commit_message)) = (
242 commit.author().name(),
243 commit.message().map(|w| w.to_lowercase()),
244 ) {
245 let mut curses_added = 0;
246 {
247 let author = self.author(author_name);
248 author.total_commits += 1;
249 for word in split_into_clean_words(&commit_message) {
250 if naughty_word(word) {
251 author.total_curses += 1;
252 curses_added += 1;
253 author.update_occurrence(word);
254 }
255 }
256 }
257 self.total_commits += 1;
258 self.total_curses += curses_added;
259 } else {
260 eprintln!(
261 "Skipping commit {:?} because either the commit author or message is missing",
262 commit
263 );
264 }
265 }
266 }
267}