raw
1use std::ffi::CString;
2
3use git2::{Commit, Diff, DiffOptions, DiffStats, Sort, Tree};
4
5use crate::utils::{
6 error::{Context as _, Result},
7 git::Repository,
8};
9
10impl Repository {
11 #[tracing::instrument(skip_all)]
12 pub fn commit(&self, spec: &str) -> Result<Option<Commit<'_>>> {
13 let obj = match self.inner.revparse_single(spec) {
14 Ok(obj) => obj,
15 Err(err) => {
16 tracing::warn!(err=?err, "failed to revparse commit");
17 return Ok(None);
18 }
19 };
20
21 let commit = match obj.peel_to_commit() {
22 Ok(commit) => commit,
23 Err(err) => {
24 tracing::warn!(err=?err, "failed to peel object to commit");
25 return Ok(None);
26 }
27 };
28
29 Ok(Some(commit))
30 }
31
32 #[tracing::instrument(skip_all)]
33 pub fn commit_diff(&self, commit: &Commit<'_>) -> Result<Diff<'_>> {
34 let mut options = DiffOptions::new();
35
36 let parent = commit.parents().next();
39
40 let diff = self.inner.diff_tree_to_tree(
41 parent.and_then(|parent| parent.tree().ok()).as_ref(),
42 commit.tree().ok().as_ref(),
43 Some(&mut options),
44 )?;
45
46 Ok(diff)
47 }
48
49 #[tracing::instrument(skip_all)]
50 pub fn commit_stats(&self, commit: &Commit<'_>) -> Result<DiffStats> {
51 let diff = self.commit_diff(commit)?;
52
53 let stats = diff.stats()?;
54
55 Ok(stats)
56 }
57
58 #[tracing::instrument(skip_all)]
59 pub fn commit_tree(&self, spec: &str) -> Result<Option<(Commit<'_>, Tree<'_>)>> {
60 let Some(commit) = self.commit(spec)? else {
61 return Ok(None);
62 };
63
64 let tree = commit.tree()?;
65
66 Ok(Some((commit, tree)))
67 }
68
69 #[tracing::instrument(skip_all)]
70 pub fn commits(&self, spec: &str, amount: usize) -> Result<Option<Vec<Commit<'_>>>> {
71 if self.is_shallow() {
72 return self.commits_shallow();
73 }
74
75 self.commits_full(spec, amount)
76 }
77
78 #[tracing::instrument(skip_all)]
79 pub fn commits_full(&self, spec: &str, amount: usize) -> Result<Option<Vec<Commit<'_>>>> {
80 let mut revwalk = self.inner.revwalk()?;
81
82 let Some(commit) = self.commit(spec)? else {
83 return Ok(None);
84 };
85
86 revwalk.push(commit.id())?;
87
88 revwalk.set_sorting(Sort::TIME)?;
89
90 let commits = revwalk
91 .filter_map(|oid| oid.ok().and_then(|oid| self.inner.find_commit(oid).ok()))
92 .take(amount)
93 .collect();
94
95 Ok(Some(commits))
96 }
97
98 #[tracing::instrument(skip_all)]
99 pub fn commits_for_obj(
100 &self,
101 spec: &str,
102 amount: usize,
103 obj: Option<&str>,
104 ) -> Result<Option<Vec<Commit<'_>>>> {
105 tracing::info!("is_shallow");
106 if self.is_shallow() {
107 return self
108 .commits_shallow()
109 .context("failed to get commits on shallow repo");
110 }
111
112 let mut revwalk = self.inner.revwalk().context("failed to create revwalk")?;
113
114 let Some(commit) = self.commit(spec).context("failed to get commit")? else {
115 tracing::info!("commit None");
116 return Ok(None);
117 };
118
119 revwalk
120 .push(commit.id())
121 .context("failed to set root commit for revwalk")?;
122
123 revwalk
124 .set_sorting(Sort::TIME)
125 .context("failed to set revwalk sorting mode")?;
126
127 let commits =
128 revwalk.filter_map(|oid| oid.ok().and_then(|oid| self.inner.find_commit(oid).ok()));
129
130 let Some(Ok(path)) = obj.map(CString::new) else {
131 return Ok(Some(commits.take(amount).collect()));
132 };
133
134 let mut options = DiffOptions::new();
136 options.pathspec(path);
137
138 let commits = commits
139 .into_iter()
140 .filter(|walked_commit| {
141 let old_tree = match walked_commit.tree() {
142 Ok(tree) => tree,
143 Err(err) => {
144 tracing::error!(err=?err, "failed to get commit tree");
145 return false;
146 }
147 };
148
149 walked_commit.parents().any(|parent| {
151 let new_tree = match parent.tree() {
152 Ok(tree) => tree,
153 Err(err) => {
154 tracing::error!(err=?err, "failed to get parent commit tree");
155 return false;
156 }
157 };
158
159 let diff = match self.inner.diff_tree_to_tree(
160 Some(&old_tree),
161 Some(&new_tree),
162 Some(&mut options),
163 ) {
164 Ok(diff) => diff,
165 Err(err) => {
166 tracing::error!(err=?err, "failed to diff trees");
167 return false;
168 }
169 };
170
171 let stats = match diff.stats() {
172 Ok(stats) => stats,
173 Err(err) => {
174 tracing::error!(err=?err, "failed to get diff stats");
175 return false;
176 }
177 };
178
179 stats.files_changed() > 0
180 })
181 })
182 .take(amount)
183 .collect();
184
185 Ok(Some(commits))
186 }
187
188 #[tracing::instrument(skip_all)]
189 pub fn commits_shallow(&self) -> Result<Option<Vec<Commit<'_>>>> {
190 tracing::warn!("repository {:?} is only a shallow clone", self.inner.path());
191 let commits = self
192 .inner
193 .head()?
194 .peel_to_commit()
195 .map(|commit| vec![commit])
196 .unwrap_or_default();
197
198 Ok(Some(commits))
199 }
200
201 #[tracing::instrument(skip_all)]
202 pub fn file_last_commit<P: git2::IntoCString>(
203 &self,
204 spec: &str,
205 path: P,
206 ) -> Result<Option<Commit<'_>>> {
207 let mut revwalk = self.inner.revwalk()?;
208
209 let Some(commit) = self.commit(spec)? else {
210 return Ok(None);
211 };
212
213 revwalk.push(commit.id())?;
214
215 revwalk.set_sorting(Sort::TIME)?;
216
217 let mut options = DiffOptions::new();
218 options.pathspec(path.into_c_string()?);
219
220 let last_commit = revwalk
221 .filter_map(|oid| oid.ok().and_then(|oid| self.inner.find_commit(oid).ok()))
222 .find(|walked_commit| {
223 let commit_tree = match walked_commit.tree() {
224 Ok(tree) => tree,
225 Err(err) => {
226 tracing::error!(err=?err, "failed to get commit tree");
227 return false;
228 }
229 };
230
231 let mut files_changed = |child: &Tree<'_>, parent: Option<&Tree<'_>>| {
232 let diff = match self.inner.diff_tree_to_tree(
233 parent,
234 Some(child),
235 Some(&mut options),
236 ) {
237 Ok(diff) => diff,
238 Err(err) => {
239 tracing::error!(err=?err, "failed to diff trees");
240 return false;
241 }
242 };
243
244 let stats = match diff.stats() {
245 Ok(stats) => stats,
246 Err(err) => {
247 tracing::error!(err=?err, "failed to get diff stats");
248 return false;
249 }
250 };
251
252 stats.files_changed() > 0
253 };
254
255 if walked_commit.parent_count() == 0 {
256 files_changed(&commit_tree, None)
257 } else {
258 walked_commit.parents().any(|parent| {
260 let parent_tree = match parent.tree() {
261 Ok(tree) => tree,
262 Err(err) => {
263 tracing::error!(err=?err, "failed to get parent commit tree, this should not happen");
264 return false;
265 }
266 };
267
268 files_changed(&commit_tree, Some(&parent_tree))
269 })
270 }
271 });
272
273 Ok(Some(
274 last_commit.context("file was not part of any commit")?,
275 ))
276 }
277}
278