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