1use std::{
2 num::NonZeroUsize,
3 path::Path,
4 rc::Rc,
5 str::FromStr,
6 sync::{LazyLock, Mutex},
7};
8
9use lru::LruCache;
10use regex::Regex;
11
12use crate::domain::{CachedDiff, CachedStat, RcDiff, RcStat};
13use crate::{Actor, Error, ModifiedFile, Repository};
14
15const CACHE_KEY: u8 = 0;
16
17fn iter_co_authors(haystack: &str) -> impl Iterator<Item = &str> {
20 const CO_AUTHOR_REGEX: &str = r"(?m)^Co-authored-by: (.*) <(.*?)>$";
21 static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(CO_AUTHOR_REGEX).unwrap());
23
24 let prefix = "Co-authored-by:";
25 RE.find_iter(haystack).map(move |re_match| {
26 re_match
27 .as_str()
28 .strip_prefix(prefix)
29 .unwrap_or_default()
30 .trim()
31 })
32}
33
34pub struct Commit<'repo> {
36 inner: git2::Commit<'repo>,
37 ctx: &'repo Repository,
38 diff_cache: Mutex<CachedDiff<'repo>>,
39 stat_cache: Mutex<CachedStat>,
40}
41
42impl<'repo> Commit<'repo> {
43 pub fn new(commit: git2::Commit<'repo>, repository: &'repo Repository) -> Self {
45 let diff_cache = Mutex::new(LruCache::new(NonZeroUsize::new(1).unwrap()));
46 let stat_cache = Mutex::new(LruCache::new(NonZeroUsize::new(1).unwrap()));
47
48 Self {
49 inner: commit.to_owned(),
50 ctx: repository,
51 diff_cache,
52 stat_cache,
53 }
54 }
55
56 pub fn hash(&self) -> String {
58 self.inner.id().to_string()
59 }
60
61 pub fn msg(&self) -> Option<&str> {
63 self.inner.message()
64 }
65
66 pub fn author(&self) -> Actor {
68 Actor::new(self.inner.author())
69 }
70
71 pub fn co_authors(&self) -> impl Iterator<Item = Result<Actor, Error>> {
76 let commit_msg = self.msg().unwrap_or_default();
77 iter_co_authors(commit_msg).map(Actor::from_str)
78 }
79
80 pub fn committer(&self) -> Actor {
82 Actor::new(self.inner.committer())
83 }
84
85 pub fn branches(&self) -> Result<impl Iterator<Item = Result<String, Error>>, Error> {
91 self.branch_iterator(None)
92 }
93
94 pub fn local_branches(&self) -> Result<impl Iterator<Item = Result<String, Error>>, Error> {
100 let flag = Some(git2::BranchType::Local);
101 self.branch_iterator(flag)
102 }
103
104 pub fn remote_branches(&self) -> Result<impl Iterator<Item = Result<String, Error>>, Error> {
110 let flag = Some(git2::BranchType::Remote);
111 self.branch_iterator(flag)
112 }
113
114 pub fn parent_commits(&self) -> impl Iterator<Item = Result<Commit<'_>, Error>> {
116 self.inner.parent_ids().map(|oid| {
117 self.ctx
118 .raw()
119 .find_commit(oid)
120 .map_err(Error::Git)
121 .map(|gitc| Commit::new(gitc, self.ctx))
122 })
123 }
124
125 pub fn parents(&self) -> impl Iterator<Item = String> {
127 self.inner.parent_ids().map(|p| p.to_string())
128 }
129
130 pub fn is_merge(&self) -> bool {
132 self.inner.parent_count() > 1
133 }
134
135 pub fn in_main(&self) -> Result<bool, Error> {
137 let b = self
138 .local_branches()?
139 .collect::<Vec<Result<String, Error>>>();
140 Ok(b.contains(&Ok("main".to_string())) || b.contains(&Ok("master".to_string())))
141 }
142
143 pub fn mod_files(&self) -> Result<impl Iterator<Item = ModifiedFile<'_>>, Error> {
145 let diff = self.diff()?;
146 Ok((0..diff.deltas().len()).map(move |n| ModifiedFile::new(Rc::clone(&diff), n)))
147 }
148
149 pub fn insertions(&self) -> Result<usize, Error> {
151 Ok(self.stats()?.insertions())
152 }
153
154 pub fn deletions(&self) -> Result<usize, Error> {
156 Ok(self.stats()?.deletions())
157 }
158
159 pub fn lines(&self) -> Result<usize, Error> {
161 Ok(self.insertions()? + self.deletions()?)
162 }
163
164 pub fn files(&self) -> Result<usize, Error> {
166 Ok(self.stats()?.files_changed())
167 }
168
169 pub fn project_path(&self) -> &Path {
171 let git_folder = self.ctx.raw().path();
172 git_folder.parent().unwrap()
174 }
175
176 pub fn project_name(&self) -> Option<&str> {
178 self.project_path().file_name().and_then(|s| s.to_str())
179 }
180
181 fn stats(&self) -> Result<RcStat, Error> {
183 let diff = self.diff()?;
184
185 let mut guard = self.stat_cache.lock().map_err(|_| Error::PoisonedCache)?;
186 let data = guard.try_get_or_insert(CACHE_KEY, || {
187 let stats = diff.stats().map_err(Error::Git)?;
188 Ok::<RcStat, Error>(Rc::new(stats))
189 })?;
190
191 Ok(Rc::clone(data))
192 }
193
194 fn diff(&self) -> Result<RcDiff<'repo>, Error> {
197 let mut guard = self.diff_cache.lock().map_err(|_| Error::PoisonedCache)?;
198 let data = guard.try_get_or_insert(CACHE_KEY, || self.calculate_diff())?;
199
200 Ok(Rc::clone(data))
201 }
202
203 fn calculate_diff(&self) -> Result<RcDiff<'repo>, Error> {
206 let this_tree = self.inner.tree().ok();
207 let parent_tree = self.resolve_parent_tree()?;
208
209 let diff = self
210 .ctx
211 .raw()
212 .diff_tree_to_tree(parent_tree.as_ref(), this_tree.as_ref(), None)
214 .map_err(Error::Git)?;
215
216 Ok(Rc::new(diff))
217 }
218
219 fn resolve_parent_tree(&self) -> Result<Option<git2::Tree<'_>>, Error> {
222 Ok(match self.inner.parent_count() {
223 0 => None,
224 1 => self.inner.parent(0).map_err(Error::Git)?.tree().ok(),
225 _ => return Err(Error::PathError("Placeholder error".to_string())),
227 })
228 }
229
230 fn commit_contains_branch(&self, branch: git2::Oid, commit: git2::Oid) -> bool {
235 self.ctx.raw().graph_descendant_of(branch, commit).is_ok()
236 }
237
238 fn branch_iterator(
240 &self,
241 bt: Option<git2::BranchType>,
242 ) -> Result<impl Iterator<Item = Result<String, Error>>, Error> {
243 let commit_id = self.inner.id();
244 let branches = self.ctx.raw().branches(bt).map_err(Error::Git)?;
245
246 Ok(branches.filter_map(move |res| {
247 let branch = match res {
248 Ok(v) => v.0,
249 Err(e) => return Some(Err(Error::Git(e))),
250 };
251
252 let oid = match branch.get().target() {
256 Some(v) => v,
257 None => return None,
258 };
259
260 if !self.commit_contains_branch(oid, commit_id) {
262 return None;
263 }
264
265 match branch.name() {
266 Ok(Some(name)) => Some(Ok(name.to_string())),
267 Ok(None) => None, Err(e) => Some(Err(Error::Git(e))),
269 }
270 }))
271 }
272}
273
274#[cfg(test)]
275mod test {
276 use super::*;
277
278 #[test]
279 fn test_iter_matches() {
280 let haystack = "Co-authored-by: John <john@example.com>";
281 assert_eq!(iter_co_authors(haystack).collect::<Vec<&str>>().len(), 1);
282
283 let haystack = "No matches expected";
284 assert_eq!(iter_co_authors(haystack).collect::<Vec<&str>>().len(), 0);
285 }
286}