1use std::cell::OnceCell;
2
3use crate::Actor;
4use crate::{Error, Repository};
5
6pub struct Commit<'repo> {
8 inner: git2::Commit<'repo>,
9 ctx: &'repo Repository,
10 cache: OnceCell<git2::Diff<'repo>>,
11}
12
13impl<'repo> Commit<'repo> {
14 pub fn new(commit: git2::Commit<'repo>, repository: &'repo Repository) -> Self {
16 Self {
17 inner: commit.to_owned(),
18 ctx: repository,
19 cache: OnceCell::new(),
20 }
21 }
22
23 pub fn hash(&self) -> String {
25 self.inner.id().to_string()
26 }
27
28 pub fn msg(&self) -> Option<String> {
30 self.inner.message().map(|s| s.to_string())
31 }
32
33 pub fn author(&self) -> Actor {
35 Actor::new(self.inner.author())
36 }
37
38 pub fn committer(&self) -> Actor {
40 Actor::new(self.inner.committer())
41 }
42
43 pub fn parents(&self) -> impl Iterator<Item = String> {
45 self.inner.parent_ids().map(|id| id.to_string())
46 }
47
48 pub fn is_merge(&self) -> bool {
50 self.inner.parent_count() > 1
51 }
52
53 pub fn insertions(&self) -> Result<usize, Error> {
55 Ok(self.stats()?.insertions())
56 }
57
58 pub fn deletions(&self) -> Result<usize, Error> {
60 Ok(self.stats()?.deletions())
61 }
62
63 pub fn lines(&self) -> Result<usize, Error> {
65 Ok(self.insertions()? + self.deletions()?)
66 }
67
68 pub fn files(&self) -> Result<usize, Error> {
70 Ok(self.stats()?.files_changed())
71 }
72
73 fn stats(&self) -> Result<git2::DiffStats, Error> {
76 let diff = self.diff()?;
77 diff.stats().map_err(Error::Git)
78 }
79
80 fn diff(&self) -> Result<&git2::Diff<'repo>, Error> {
83 let diff = self.calculate_diff()?;
89 Ok(self.cache.get_or_init(|| diff))
90 }
91
92 fn calculate_diff(&self) -> Result<git2::Diff<'repo>, Error> {
95 let this_tree = self.inner.tree().ok();
96 let parent_tree = self.resolve_parent_tree()?;
97
98 self.ctx
99 .raw()
100 .diff_tree_to_tree(parent_tree.as_ref(), this_tree.as_ref(), None)
102 .map_err(Error::Git)
103 }
104
105 fn resolve_parent_tree(&self) -> Result<Option<git2::Tree<'_>>, Error> {
108 Ok(match self.inner.parent_count() {
109 0 => None,
110 1 => self.inner.parent(0).map_err(Error::Git)?.tree().ok(),
111 _ => return Err(Error::PathError("Placeholder error".to_string())),
113 })
114 }
115}
116
117#[cfg(test)]
118mod test {
119 use super::*;
120 use crate::common::{EXPECTED_ACTOR_EMAIL, EXPECTED_ACTOR_NAME, EXPECTED_MSG, init_repo};
121
122 #[test]
123 fn test_msg() {
124 let repo = init_repo();
125 let git_commit = repo
126 .raw()
127 .head()
128 .expect("Expected a valid reference")
129 .peel_to_commit()
130 .expect("Expected a valid git2 commit");
131 let commit = Commit::new(git_commit, &repo);
132
133 assert_eq!(commit.msg().unwrap(), EXPECTED_MSG.to_string());
134 }
135
136 #[test]
137 fn test_author() {
138 let repo = init_repo();
139 let git_commit = repo
140 .raw()
141 .head()
142 .expect("Expected a valid reference")
143 .peel_to_commit()
144 .expect("Expected a valid git2 commit");
145 let commit = Commit::new(git_commit, &repo);
146
147 assert_eq!(
148 commit.author().name().unwrap(),
149 EXPECTED_ACTOR_NAME.to_string()
150 );
151 assert_eq!(
152 commit.author().email().unwrap(),
153 EXPECTED_ACTOR_EMAIL.to_string()
154 );
155 }
156
157 #[test]
158 fn test_committer() {
159 let repo = init_repo();
160 let git_commit = repo
161 .raw()
162 .head()
163 .expect("Expected a valid reference")
164 .peel_to_commit()
165 .expect("Expected a valid git2 commit");
166 let commit = Commit::new(git_commit, &repo);
167
168 assert_eq!(
169 commit.committer().name().unwrap(),
170 EXPECTED_ACTOR_NAME.to_string()
171 );
172 assert_eq!(
173 commit.committer().email().unwrap(),
174 EXPECTED_ACTOR_EMAIL.to_string()
175 );
176 }
177
178 #[test]
179 fn test_parents() {
180 let repo = init_repo();
181 let git_commit = repo
182 .raw()
183 .head()
184 .expect("Expected a valid reference")
185 .peel_to_commit()
186 .expect("Expected a valid git2 commit");
187 let commit = Commit::new(git_commit, &repo);
188
189 assert_eq!(commit.parents().collect::<Vec<String>>().len(), 1);
190 }
191
192 #[test]
193 fn test_is_merge() {
194 let repo = init_repo();
195 let git_commit = repo
196 .raw()
197 .head()
198 .expect("Expected a valid reference")
199 .peel_to_commit()
200 .expect("Expected a valid git2 commit");
201 let commit = Commit::new(git_commit, &repo);
202
203 assert!(!commit.is_merge());
204 }
205
206 #[test]
207 fn test_insertions() {
208 let repo = init_repo();
209 let git_commit = repo
210 .raw()
211 .head()
212 .expect("Expected a valid reference")
213 .peel_to_commit()
214 .expect("Expected a valid git2 commit");
215 let commit = Commit::new(git_commit, &repo);
216
217 assert_eq!(commit.insertions().unwrap(), 1)
218 }
219
220 #[test]
221 fn test_deletions() {
222 let repo = init_repo();
223 let git_commit = repo
224 .raw()
225 .head()
226 .expect("Expected a valid reference")
227 .peel_to_commit()
228 .expect("Expected a valid git2 commit");
229 let commit = Commit::new(git_commit, &repo);
230
231 assert_eq!(commit.deletions().unwrap(), 0)
232 }
233
234 #[test]
235 fn test_lines() {
236 let repo = init_repo();
237 let git_commit = repo
238 .raw()
239 .head()
240 .expect("Expected a valid reference")
241 .peel_to_commit()
242 .expect("Expected a valid git2 commit");
243 let commit = Commit::new(git_commit, &repo);
244
245 assert_eq!(commit.lines().unwrap(), 1)
246 }
247
248 #[test]
249 fn test_stat() {
250 let repo = init_repo();
251 let git_commit = repo
252 .raw()
253 .head()
254 .expect("Expected a valid reference")
255 .peel_to_commit()
256 .expect("Expected a valid git2 commit");
257 let commit = Commit::new(git_commit, &repo);
258
259 let _: git2::DiffStats = commit
262 .stats()
263 .expect("Failed to construct git2 Stats object");
264 }
265}