git_commits/
ext.rs

1pub mod prelude {
2    pub use super::{CommitExt, Commits, DiffExt, RepositoryExt};
3}
4
5use std::ops::ControlFlow;
6
7use git2::{
8    Commit, Diff, DiffDelta, DiffFormat, DiffHunk, DiffLine, DiffOptions, ErrorCode, Repository,
9    Revwalk, Sort, Tree,
10};
11
12use crate::GitError;
13
14pub trait WalkOutput {
15    /// Returns `Ok(true)` to signal that the iteration should stop
16    /// without any error occurring.
17    fn finished(self) -> Result<bool, GitError>;
18}
19
20impl WalkOutput for () {
21    fn finished(self) -> Result<bool, GitError> {
22        Ok(false)
23    }
24}
25
26impl WalkOutput for bool {
27    fn finished(self) -> Result<bool, GitError> {
28        Ok(self)
29    }
30}
31
32impl WalkOutput for ControlFlow<()> {
33    fn finished(self) -> Result<bool, GitError> {
34        match self {
35            ControlFlow::Continue(()) => Ok(false),
36            ControlFlow::Break(()) => Ok(true),
37        }
38    }
39}
40
41impl<T> WalkOutput for Result<T, GitError>
42where
43    T: WalkOutput,
44{
45    fn finished(self) -> Result<bool, GitError> {
46        self?.finished()
47    }
48}
49
50pub trait RepositoryExt {
51    fn commits(&self) -> Result<Commits<'_>, GitError>;
52    fn count_commits(&self) -> Result<usize, GitError>;
53
54    fn walk_commits<T, F>(&self, mut f: F) -> Result<(), GitError>
55    where
56        F: FnMut(Commit<'_>) -> T,
57        T: WalkOutput,
58    {
59        for commit in self.commits()? {
60            if f(commit?).finished()? {
61                break;
62            }
63        }
64        Ok(())
65    }
66}
67
68impl RepositoryExt for Repository {
69    fn commits(&self) -> Result<Commits<'_>, GitError> {
70        Commits::new(self)
71    }
72
73    fn count_commits(&self) -> Result<usize, GitError> {
74        Ok(revwalk(self)?.count())
75    }
76}
77
78pub struct Commits<'a> {
79    repo: &'a Repository,
80    revwalk: Revwalk<'a>,
81}
82
83impl<'a> Commits<'a> {
84    fn new(repo: &'a Repository) -> Result<Self, GitError> {
85        let revwalk = revwalk(repo)?;
86        Ok(Self { repo, revwalk })
87    }
88}
89
90impl<'a> Iterator for Commits<'a> {
91    type Item = Result<git2::Commit<'a>, GitError>;
92
93    fn next(&mut self) -> Option<Self::Item> {
94        let oid = match self.revwalk.next()? {
95            Ok(oid) => oid,
96            Err(err) => return Some(Err(err)),
97        };
98        let commit = match self.repo.find_commit(oid) {
99            Ok(commit) => commit,
100            Err(err) => return Some(Err(err)),
101        };
102        Some(Ok(commit))
103    }
104}
105
106fn revwalk(repo: &Repository) -> Result<Revwalk<'_>, GitError> {
107    let mut revwalk = repo.revwalk()?;
108    revwalk.set_sorting(Sort::REVERSE | Sort::TIME)?;
109    revwalk.push_head()?;
110    Ok(revwalk)
111}
112
113pub trait CommitExt {
114    fn walk_diffs<T, F>(&self, repo: &Repository, f: F) -> Result<(), GitError>
115    where
116        F: FnMut(Diff<'_>) -> T,
117        T: WalkOutput;
118
119    fn walk_changes<T, F>(
120        &self,
121        repo: &Repository,
122        format: DiffFormat,
123        mut f: F,
124    ) -> Result<(), GitError>
125    where
126        F: FnMut(DiffDelta<'_>, Option<DiffHunk<'_>>, DiffLine<'_>) -> T,
127        T: WalkOutput,
128    {
129        self.walk_diffs(repo, |diff| diff.walk_changes(format, &mut f))
130    }
131}
132
133impl CommitExt for Commit<'_> {
134    fn walk_diffs<T, F>(&self, repo: &Repository, mut f: F) -> Result<(), GitError>
135    where
136        F: FnMut(Diff<'_>) -> T,
137        T: WalkOutput,
138    {
139        let new_tree = self.tree()?;
140        if self.parent_count() == 0 {
141            walk_diff(repo, None, Some(&new_tree), f)?;
142        } else {
143            for parent in self.parents() {
144                let old_tree = parent.tree()?;
145                walk_diff(repo, Some(&old_tree), Some(&new_tree), &mut f)?;
146            }
147        }
148        Ok(())
149    }
150}
151
152fn walk_diff<T, F>(
153    repo: &Repository,
154    old_tree: Option<&Tree<'_>>,
155    new_tree: Option<&Tree<'_>>,
156    f: F,
157) -> Result<(), GitError>
158where
159    F: FnOnce(Diff<'_>) -> T,
160    T: WalkOutput,
161{
162    let mut opts = DiffOptions::new();
163    opts.show_binary(true);
164
165    let mut diff = repo.diff_tree_to_tree(old_tree, new_tree, Some(&mut opts))?;
166    diff.find_similar(None)?;
167
168    f(diff).finished()?;
169
170    Ok(())
171}
172
173pub trait DiffExt {
174    fn walk_changes<T, F>(&self, format: DiffFormat, f: F) -> Result<(), GitError>
175    where
176        F: FnMut(DiffDelta<'_>, Option<DiffHunk<'_>>, DiffLine<'_>) -> T,
177        T: WalkOutput;
178}
179
180impl DiffExt for Diff<'_> {
181    fn walk_changes<T, F>(&self, format: DiffFormat, mut f: F) -> Result<(), GitError>
182    where
183        F: FnMut(DiffDelta<'_>, Option<DiffHunk<'_>>, DiffLine<'_>) -> T,
184        T: WalkOutput,
185    {
186        let mut error = None;
187        let res = self.print(format, |delta, hunk, line| {
188            match f(delta, hunk, line).finished() {
189                Ok(stop) => !stop,
190                Err(err) => {
191                    debug_assert!(error.is_none());
192                    error = Some(err);
193                    false
194                }
195            }
196        });
197        match res {
198            Ok(()) => Ok(()),
199            Err(err) if err.code() == ErrorCode::User => match error {
200                Some(err) => Err(err),
201                None => Ok(()),
202            },
203            Err(err) => {
204                debug_assert!(error.is_none());
205                Err(err)
206            }
207        }
208    }
209}