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