Skip to main content

grit_lib/revwalk/
handle.rs

1//! Repository-scoped revision parsing and walking via [`RevWalk`].
2use crate::error::Result;
3use crate::object_inspect::{ResolveError, ResolvedObject};
4use crate::objects::ObjectId;
5use crate::repo::Repository;
6use crate::rev_list::{
7    open_commit_stream, rev_list, CommitStreamParams, RevListOptions, RevListResult,
8};
9use crate::rev_list_error::RevListError;
10use crate::rev_parse::resolve_revision;
11use crate::revwalk::parse_rev_walk_tokens;
12use crate::revwalk::RevWalkCommitIterOptions;
13use std::borrow::Cow;
14
15/// Parse revision specs and walk commit history for one repository.
16#[derive(Debug, Copy, Clone)]
17pub struct RevWalk<'repo> {
18    repo: &'repo Repository,
19}
20
21impl<'repo> RevWalk<'repo> {
22    /// Bind rev-parse and rev-list to `repo`.
23    #[inline]
24    #[must_use]
25    pub fn new(repo: &'repo Repository) -> Self {
26        Self { repo }
27    }
28
29    /// Underlying repository.
30    #[inline]
31    #[must_use]
32    pub fn repository(&self) -> &'repo Repository {
33        self.repo
34    }
35
36    /// Resolve a user revision spec (for example `HEAD`, `main~2`, `v1.0`).
37    ///
38    /// # Errors
39    ///
40    /// Propagates rev-parse and ref errors.
41    ///
42    /// # Examples
43    ///
44    /// ```
45    /// # fn main() -> grit_lib::error::Result<()> {
46    /// use grit_lib::repo::{InitOptions, Repository};
47    /// use grit_lib::ident::Signature;
48    /// use grit_lib::porcelain::commit::{create_commit, CommitRequest};
49    /// use grit_lib::progress::NullProgress;
50    /// let dir = std::env::temp_dir().join("grit-revwalk-parse-doc");
51    /// let repo = Repository::init(&dir, &InitOptions::default())?;
52    /// let mut req = CommitRequest::new("doc");
53    /// req.author = Some(Signature::new("Doc", "doc@example.com", repo.identity_now())?);
54    /// req.committer = Some(Signature::new("Doc", "doc@example.com", repo.identity_now())?);
55    /// req.allow_empty = true;
56    /// let _ = create_commit(&repo, &req, &mut NullProgress, &grit_lib::progress::NeverCancel)?;
57    /// let _ = repo.rev_walk().resolve("refs/heads/main")?;
58    /// # Ok(())
59    /// # }
60    /// ```
61    #[inline]
62    pub fn resolve(&self, spec: &str) -> Result<ObjectId> {
63        resolve_revision(self.repo, spec)
64    }
65
66    /// Resolve `spec` to oid, kind, size, and optional ref name ([`ResolvedObject`]).
67    ///
68    /// # Errors
69    ///
70    /// See [`Repository::resolve`].
71    #[inline]
72    pub fn resolve_object(&self, spec: &str) -> std::result::Result<ResolvedObject, ResolveError> {
73        self.repo.resolve(spec)
74    }
75
76    /// Walk commits matching one revision `spec`: a plain tip (`main`, `HEAD~3`), an exclusion
77    /// (`^tip`), a range (`A..B`), or a symmetric difference (`A...B`, like `git rev-list A...B`).
78    /// Either side of a range may be empty and defaults to `HEAD`.
79    ///
80    /// This is the low-level entry point: it takes the [`rev_list`] engine's full
81    /// [`RevListOptions`]. For several tokens, ref selections (`--all`, `--branches`), or path
82    /// limits use [`RevWalk::commits`] / [`RevWalk::rev_list_result`] with
83    /// [`RevWalkListOptions`](crate::revwalk::RevWalkListOptions). For `A...B`,
84    /// `symmetric_left` / `symmetric_right` are filled from the range unless already set, so
85    /// `left_right`, `left_only`, and `right_only` work as in Git.
86    ///
87    /// Commit-graph acceleration and bounded `-n` early-stop apply with
88    /// [`RevListOptions::default`].
89    ///
90    /// # Errors
91    ///
92    /// - [`RevListError::UnsupportedWalkSpec`] when `spec` starts with `-` (an option or `--`)
93    ///   or holds several whitespace-separated tokens.
94    /// - Rev-parse, merge-base, and rev-list errors otherwise.
95    ///
96    /// # Examples
97    ///
98    /// See the library guide page on [revision walking](https://grit-scm.com/docs/library/revwalk/).
99    pub fn walk(&self, spec: &str, options: &RevListOptions) -> Result<RevListResult> {
100        if !is_single_walk_spec(spec) {
101            return Err(RevListError::UnsupportedWalkSpec {
102                spec: spec.to_owned(),
103            }
104            .into());
105        }
106        let parsed = parse_rev_walk_tokens(self.repo, &[spec])?;
107        let options = if parsed.symmetric_left.is_some()
108            && (options.symmetric_left.is_none() || options.symmetric_right.is_none())
109        {
110            let mut symmetric = options.clone();
111            symmetric.symmetric_left = symmetric.symmetric_left.or(parsed.symmetric_left);
112            symmetric.symmetric_right = symmetric.symmetric_right.or(parsed.symmetric_right);
113            Cow::Owned(symmetric)
114        } else {
115            Cow::Borrowed(options)
116        };
117        rev_list(self.repo, &parsed.positive, &parsed.negative, &options)
118    }
119
120    /// Lazy commit-only iterator for one revision `spec` (same single-token rules as [`Self::walk`]).
121    ///
122    /// Yields commits in default date order without building [`RevListResult`] object/cherry tables.
123    /// Use [`RevWalkCommitIterOptions`] for `skip`, `limit`, date cutoffs, and `first_parent`; for
124    /// path limits, filters, or symmetric diff metadata use [`RevWalk::commits`] with
125    /// [`crate::revwalk::RevWalkListOptions`].
126    ///
127    /// # Errors
128    ///
129    /// Same as [`Self::walk`] for invalid specs; setup errors are returned from [`RevWalkCommitIter::next`].
130    pub fn commits_iter(
131        &self,
132        spec: &str,
133        options: &RevWalkCommitIterOptions,
134    ) -> Result<RevWalkCommitIter<'repo>> {
135        if !is_single_walk_spec(spec) {
136            return Err(RevListError::UnsupportedWalkSpec {
137                spec: spec.to_owned(),
138            }
139            .into());
140        }
141        let parsed = parse_rev_walk_tokens(self.repo, &[spec])?;
142        let include =
143            crate::rev_list::resolve_specs_for_commit_stream(self.repo, &parsed.positive)?;
144        let exclude =
145            crate::rev_list::resolve_specs_for_commit_stream(self.repo, &parsed.negative)?;
146        let params = CommitStreamParams {
147            skip: options.skip,
148            limit: options.limit,
149            since: options.since,
150            until: options.until,
151            first_parent: options.first_parent,
152        };
153        Ok(RevWalkCommitIter {
154            inner: open_commit_stream(self.repo, &include, &exclude, &params)?,
155        })
156    }
157}
158
159/// Iterator over commits from [`RevWalk::commits_iter`].
160pub struct RevWalkCommitIter<'repo> {
161    inner: crate::rev_list::CommitStream<'repo>,
162}
163
164impl Iterator for RevWalkCommitIter<'_> {
165    type Item = Result<ObjectId>;
166
167    fn next(&mut self) -> Option<Self::Item> {
168        self.inner.next()
169    }
170}
171
172/// Whether `spec` can be one revision expression rather than an option or a token list.
173///
174/// Whitespace is legal inside `@{...}` / `^{...}` (reflog dates, message searches) and after a
175/// `:` (`:/message`, `rev:path`), so only whitespace outside those forms marks several tokens.
176fn is_single_walk_spec(spec: &str) -> bool {
177    if spec.starts_with('-') {
178        return false;
179    }
180    let mut brace_depth = 0usize;
181    for c in spec.chars() {
182        match c {
183            ':' if brace_depth == 0 => return true,
184            '{' => brace_depth += 1,
185            '}' => brace_depth = brace_depth.saturating_sub(1),
186            c if c.is_whitespace() && brace_depth == 0 => return false,
187            _ => {}
188        }
189    }
190    true
191}
192
193#[cfg(test)]
194mod commit_iter_tests {
195    use super::*;
196    use crate::ident::Signature;
197    use crate::porcelain::commit::{create_commit, CommitRequest};
198    use crate::progress::{NeverCancel, NullProgress};
199    use crate::repo::{InitOptions, Repository};
200
201    fn init_repo_with_commits(count: usize) -> (tempfile::TempDir, Repository) {
202        let dir = tempfile::tempdir().expect("tempdir");
203        let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
204        for i in 0..count {
205            let mut req = CommitRequest::new(format!("commit {i}"));
206            req.author = Some(
207                Signature::new("Author", "a@example.com", repo.identity_now()).expect("author"),
208            );
209            req.committer = Some(
210                Signature::new("Committer", "c@example.com", repo.identity_now())
211                    .expect("committer"),
212            );
213            req.allow_empty = true;
214            create_commit(&repo, &req, &mut NullProgress, &NeverCancel).expect("commit");
215        }
216        (dir, repo)
217    }
218
219    #[test]
220    fn commits_iter_stops_at_limit_without_full_rev_list() {
221        let (_dir, repo) = init_repo_with_commits(5);
222        let options = RevWalkCommitIterOptions {
223            limit: Some(2),
224            ..RevWalkCommitIterOptions::default()
225        };
226        let ids: Vec<_> = repo
227            .rev_walk()
228            .commits_iter("HEAD", &options)
229            .expect("iter")
230            .collect::<Result<Vec<_>>>()
231            .expect("walk");
232        assert_eq!(ids.len(), 2);
233    }
234
235    #[test]
236    fn commits_iter_since_cutoff_filters_old_commits() {
237        let (_dir, repo) = init_repo_with_commits(4);
238        let newest = repo.rev_walk().resolve("HEAD").expect("head");
239        let params = crate::rev_list::CommitStreamParams {
240            limit: Some(10),
241            ..Default::default()
242        };
243        let all: Vec<_> = open_commit_stream(&repo, std::slice::from_ref(&newest), &[], &params)
244            .expect("stream")
245            .collect::<Result<Vec<_>>>()
246            .expect("walk");
247        assert_eq!(all.len(), 4);
248
249        let future = i64::MAX;
250        let filtered: Vec<_> = open_commit_stream(
251            &repo,
252            std::slice::from_ref(&newest),
253            &[],
254            &crate::rev_list::CommitStreamParams {
255                since: Some(future),
256                ..Default::default()
257            },
258        )
259        .expect("stream")
260        .collect::<Result<Vec<_>>>()
261        .expect("walk");
262        assert!(filtered.is_empty());
263    }
264}