wayver's git archive


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

src/utils/git/commit.rs@375565f690b958e08f589a7fee998ad5f47a70d0

raw
Date Commit Message Author Files + -
2026-02-17 21:07 initial mvp wayverd 74 10800 0
...

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        // This is identical to getting "commit^" and on merges this will be the
37        // merged into branch before the merge.
38        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        // filter for specific file if necessary
135        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                // check that the given file was affected from any of the parents
150                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 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                    // check that the given file was affected from any of the parents
259                    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(commit.context("file was not part of any commit")?))
274    }
275}
276