1use std::cell::OnceCell;
2
3use crate::{Actor, Error, ModifiedFile, Repository};
4
5pub struct Commit<'repo> {
7 inner: git2::Commit<'repo>,
8 ctx: &'repo Repository,
9 cache: OnceCell<git2::Diff<'repo>>,
10}
11
12impl<'repo> Commit<'repo> {
13 pub fn new(commit: git2::Commit<'repo>, repository: &'repo Repository) -> Self {
15 Self {
16 inner: commit.to_owned(),
17 ctx: repository,
18 cache: OnceCell::new(),
19 }
20 }
21
22 pub fn hash(&self) -> String {
24 self.inner.id().to_string()
25 }
26
27 pub fn msg(&self) -> Option<String> {
29 self.inner.message().map(|s| s.to_string())
30 }
31
32 pub fn author(&self) -> Actor {
34 Actor::new(self.inner.author())
35 }
36
37 pub fn committer(&self) -> Actor {
39 Actor::new(self.inner.committer())
40 }
41
42 pub fn parents(&self) -> impl Iterator<Item = String> {
44 self.inner.parent_ids().map(|id| id.to_string())
45 }
46
47 pub fn is_merge(&self) -> bool {
49 self.inner.parent_count() > 1
50 }
51
52 pub fn mod_files(&self) -> Result<impl Iterator<Item = ModifiedFile<'_>>, Error> {
54 let diff = self.diff()?;
55
56 Ok((0..diff.deltas().len()).map(move |n| ModifiedFile::new(diff, n)))
57 }
58
59 pub fn insertions(&self) -> Result<usize, Error> {
61 Ok(self.stats()?.insertions())
62 }
63
64 pub fn deletions(&self) -> Result<usize, Error> {
66 Ok(self.stats()?.deletions())
67 }
68
69 pub fn lines(&self) -> Result<usize, Error> {
71 Ok(self.insertions()? + self.deletions()?)
72 }
73
74 pub fn files(&self) -> Result<usize, Error> {
76 Ok(self.stats()?.files_changed())
77 }
78
79 fn stats(&self) -> Result<git2::DiffStats, Error> {
82 let diff = self.diff()?;
83 diff.stats().map_err(Error::Git)
84 }
85
86 fn diff(&self) -> Result<&git2::Diff<'repo>, Error> {
90 let diff = self.calculate_diff()?;
91 Ok(self.cache.get_or_init(|| diff))
92 }
93
94 fn calculate_diff(&self) -> Result<git2::Diff<'repo>, Error> {
97 let this_tree = self.inner.tree().ok();
98 let parent_tree = self.resolve_parent_tree()?;
99
100 self.ctx
101 .raw()
102 .diff_tree_to_tree(parent_tree.as_ref(), this_tree.as_ref(), None)
104 .map_err(Error::Git)
105 }
106
107 fn resolve_parent_tree(&self) -> Result<Option<git2::Tree<'_>>, Error> {
110 Ok(match self.inner.parent_count() {
111 0 => None,
112 1 => self.inner.parent(0).map_err(Error::Git)?.tree().ok(),
113 _ => return Err(Error::PathError("Placeholder error".to_string())),
115 })
116 }
117}
118
119#[cfg(test)]
120mod test {
121 use super::*;
122 use crate::{
123 Local, Repository,
124 common::{EXPECTED_ACTOR_EMAIL, EXPECTED_ACTOR_NAME, EXPECTED_MSG, init_repo},
125 };
126
127 fn commit_fixture<F, R>(f: F) -> R
128 where
129 F: FnOnce(&Repository<Local>, &Commit) -> R,
130 {
131 let repo = init_repo();
132
133 let repo = Repository::<Local>::from_repository(repo);
134 let commit = repo.head().expect("Failed to get HEAD");
135
136 f(&repo, &commit)
137 }
138
139 #[test]
140 fn test_msg() {
141 commit_fixture(|_, commit| {
142 assert_eq!(commit.msg(), Some(EXPECTED_MSG.to_owned()));
144 });
145 }
146
147 #[test]
148 fn test_author() {
149 commit_fixture(|_, commit| {
150 assert_eq!(
151 commit.author().name().unwrap(),
152 EXPECTED_ACTOR_NAME.to_string()
153 );
154 assert_eq!(
155 commit.author().email().unwrap(),
156 EXPECTED_ACTOR_EMAIL.to_string()
157 );
158 });
159 }
160
161 #[test]
162 fn test_committer() {
163 commit_fixture(|_, commit| {
164 assert_eq!(
165 commit.committer().name().unwrap(),
166 EXPECTED_ACTOR_NAME.to_string()
167 );
168 assert_eq!(
169 commit.committer().email().unwrap(),
170 EXPECTED_ACTOR_EMAIL.to_string()
171 );
172 });
173 }
174
175 #[test]
176 fn test_parents() {
177 commit_fixture(|_, commit| {
178 assert_eq!(commit.parents().collect::<Vec<String>>().len(), 1);
179 });
180 }
181
182 #[test]
183 fn test_is_merge() {
184 commit_fixture(|_, commit| {
185 assert!(!commit.is_merge());
186 });
187 }
188
189 #[test]
190 fn test_insertions() {
191 commit_fixture(|_, commit| {
192 assert_eq!(commit.insertions().unwrap(), 1);
193 });
194 }
195
196 #[test]
197 fn test_deletions() {
198 commit_fixture(|_, commit| {
199 assert_eq!(commit.deletions().unwrap(), 0);
200 });
201 }
202
203 #[test]
204 fn test_lines() {
205 commit_fixture(|_, commit| {
206 assert_eq!(commit.lines().unwrap(), 1);
207 });
208 }
209
210 #[test]
211 fn test_stat() {
212 commit_fixture(|_, commit| {
213 let _: git2::DiffStats = commit
216 .stats()
217 .expect("Failed to construct git2 Stats object");
218 });
219 }
220}