wayver's git archive


a simple self-hosted git server
git clone https://git.wayver.dev/bile

src/git/commit.rs@1160e836ead3fbd29e297268d1009d648b68cae0

raw
Date Commit Message Author Files + -
2026-02-19 17:51 large refactoring wayverd 53 2153 1683
...

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        // This is identical to getting "commit^" and on merges this will be the
34        // merged into branch before the merge.
35        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        // filter for specific file if necessary
134        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                // check that the given file was affected from any of the parents
149                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                    // check that the given file was affected from any of the parents
258                    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