git_anger_library/
repo.rs

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/// A simple representation of a git repository.
13#[derive(Debug)]
14#[cfg_attr(feature = "json", derive(Serialize))]
15pub struct Repo {
16    /// Name of the repository.
17    pub name: String,
18    /// Count of the total amount of commits in the repository.
19    pub total_commits: usize,
20    /// Count of the total amount of curses used in the commits.
21    pub total_curses: usize,
22    /// HashMap of all the naughty words used by the authors.
23    pub curses: HashMap<String, usize>,
24    /// HashMap of all the authors that have been committed.
25    pub authors: HashMap<String, Author>,
26}
27
28impl Repo {
29    /// Creates a new and empty repository.
30    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    /// Checks if an author exists and creates a new author if she/he doesn't
54    /// exist.
55    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    /// Counts all the naughty words used by authors.
66    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    /// Count total naughty authors in repository.
78    fn total_naughty_authors(&self) -> usize {
79        self.authors.values().filter(|a| a.is_naughty()).count()
80    }
81
82    /// Serialize the `Repo` struct into a JSON-object and print it.
83    #[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    /// Build a table to display naughty authors and their words.
93    #[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    /// Create a sorted `Vec` from a HashMap of curses, sorted by counts
116    #[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    /// Add headers to a table
128    #[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    /// Add separators (`----`) to a table based on word lengths.
150    #[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    /// Add all the naughty authors to the table.
171    #[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                // FIXME: use authors curses, not global curses
185
186                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    /// Sum up the total naughty count and print it.
203    #[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    /// Build a list of commits by walking the history of a repository.
225    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    /// Iterate over all commits, finding authors who have been naughty and
238    /// keep track of them.
239    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}