Skip to main content

stratum/domain/
commit.rs

1use std::cell::OnceCell;
2
3use crate::Actor;
4use crate::{Error, Repository};
5
6/// A singular git commit for the repository being inspected
7pub 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    /// Instantiate a new Commit object from a git2 commit
15    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    /// Return the commit hash
24    pub fn hash(&self) -> String {
25        self.inner.id().to_string()
26    }
27
28    /// Return the commit message if it exists
29    pub fn msg(&self) -> Option<String> {
30        self.inner.message().map(|s| s.to_string())
31    }
32
33    /// Return the commit author
34    pub fn author(&self) -> Actor {
35        Actor::new(self.inner.author())
36    }
37
38    /// Return the commit committer
39    pub fn committer(&self) -> Actor {
40        Actor::new(self.inner.committer())
41    }
42
43    /// Retrun the hashes of all commit parents
44    pub fn parents(&self) -> impl Iterator<Item = String> {
45        self.inner.parent_ids().map(|id| id.to_string())
46    }
47
48    /// Return whether the commit is a merge commit
49    pub fn is_merge(&self) -> bool {
50        self.inner.parent_count() > 1
51    }
52
53    /// The number of insertions in the commit
54    pub fn insertions(&self) -> Result<usize, Error> {
55        Ok(self.stats()?.insertions())
56    }
57
58    /// The number of deletions in the commit
59    pub fn deletions(&self) -> Result<usize, Error> {
60        Ok(self.stats()?.deletions())
61    }
62
63    /// The total number of lines modified in the commit
64    pub fn lines(&self) -> Result<usize, Error> {
65        Ok(self.insertions()? + self.deletions()?)
66    }
67
68    /// The number of files modified in the commit
69    pub fn files(&self) -> Result<usize, Error> {
70        Ok(self.stats()?.files_changed())
71    }
72
73    //TODO: Should stats also be cached?
74    /// Return the git2 Stats from the commits diff
75    fn stats(&self) -> Result<git2::DiffStats, Error> {
76        let diff = self.diff()?;
77        diff.stats().map_err(Error::Git)
78    }
79
80    /// Return the git diff for the current commit within the context of a
81    /// repository.
82    fn diff(&self) -> Result<&git2::Diff<'repo>, Error> {
83        //TODO: mv diff calc into cache get_or_init
84        // The above TODO will require https://github.com/rust-lang/rust/issues/109737
85        // to be brought into stable, i.e. the fn OnceCell::get_or_try_init is
86        // made stable. This is because stratum::Error can't implement `Clone`
87        // because of `git2::Error` :(
88        let diff = self.calculate_diff()?;
89        Ok(self.cache.get_or_init(|| diff))
90    }
91
92    /// Diff the current commit to it's parent(s) adjusting strategy based on the
93    /// number of parents
94    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            //TODO: Expose opts?
101            .diff_tree_to_tree(parent_tree.as_ref(), this_tree.as_ref(), None)
102            .map_err(Error::Git)
103    }
104
105    /// Resolve to the correct parent tree changing strategies based on number
106    /// of parents.
107    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            //TODO: Resolve merge commit process
112            _ => 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        // Won't compile if return type is bad, stat otherwise checked in insertions
260        // and deletions test functions
261        let _: git2::DiffStats = commit
262            .stats()
263            .expect("Failed to construct git2 Stats object");
264    }
265}