git_anger_management/
repo.rs

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/// A simple representation of a git repository.
11#[derive(Debug, Serialize)]
12pub struct Repo {
13    /// Name of the repository.
14    pub name: String,
15    /// Count of the total amount of commits in the repository.
16    pub total_commits: usize,
17    /// Count of the total amount of curses used in the commits.
18    pub total_curses: usize,
19    /// HashMap of all the naughty words used by the authors.
20    pub curses: HashMap<String, usize>,
21    /// HashMap of all the authors that have been committed.
22    pub authors: HashMap<String, Author>,
23}
24
25impl Repo {
26    /// Creates a new and empty repository.
27    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    /// Checks if an author exists and creates a new author if she/he doesn't
51    /// exist.
52    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    /// Counts all the naughty words used by authors.
63    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    /// Count total naughty authors in repository.
75    fn total_naughty_authors(&self) -> usize {
76        self.authors.values().filter(|a| a.is_naughty()).count()
77    }
78
79    /// Serialize the `Repo` struct into a JSON-object and print it.
80    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    /// Build a table to display naughty authors and their words.
89    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    /// Create a sorted `Vec` from a HashMap of curses, sorted by counts
111    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    /// Add headers to a table
122    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    /// Add separators (`----`) to a table based on word lengths.
143    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    /// Add all the naughty authors to the table.
163    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                // FIXME: use authors curses, not global curses
176
177                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    /// Sum up the total naughty count and print it.
194    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    /// Build a list of commits by walking the history of a repository.
215    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    /// Iterate over all commits, finding authors who have been naughty and
228    /// keep track of them.
229    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}