use crate::error::Result;
use crate::object_inspect::{ResolveError, ResolvedObject};
use crate::objects::ObjectId;
use crate::repo::Repository;
use crate::rev_list::{
open_commit_stream, rev_list, CommitStreamParams, RevListOptions, RevListResult,
};
use crate::rev_list_error::RevListError;
use crate::rev_parse::resolve_revision;
use crate::revwalk::parse_rev_walk_tokens;
use crate::revwalk::RevWalkCommitIterOptions;
use std::borrow::Cow;
#[derive(Debug, Copy, Clone)]
pub struct RevWalk<'repo> {
repo: &'repo Repository,
}
impl<'repo> RevWalk<'repo> {
#[inline]
#[must_use]
pub fn new(repo: &'repo Repository) -> Self {
Self { repo }
}
#[inline]
#[must_use]
pub fn repository(&self) -> &'repo Repository {
self.repo
}
#[inline]
pub fn resolve(&self, spec: &str) -> Result<ObjectId> {
resolve_revision(self.repo, spec)
}
#[inline]
pub fn resolve_object(&self, spec: &str) -> std::result::Result<ResolvedObject, ResolveError> {
self.repo.resolve(spec)
}
pub fn walk(&self, spec: &str, options: &RevListOptions) -> Result<RevListResult> {
if !is_single_walk_spec(spec) {
return Err(RevListError::UnsupportedWalkSpec {
spec: spec.to_owned(),
}
.into());
}
let parsed = parse_rev_walk_tokens(self.repo, &[spec])?;
let options = if parsed.symmetric_left.is_some()
&& (options.symmetric_left.is_none() || options.symmetric_right.is_none())
{
let mut symmetric = options.clone();
symmetric.symmetric_left = symmetric.symmetric_left.or(parsed.symmetric_left);
symmetric.symmetric_right = symmetric.symmetric_right.or(parsed.symmetric_right);
Cow::Owned(symmetric)
} else {
Cow::Borrowed(options)
};
rev_list(self.repo, &parsed.positive, &parsed.negative, &options)
}
pub fn commits_iter(
&self,
spec: &str,
options: &RevWalkCommitIterOptions,
) -> Result<RevWalkCommitIter<'repo>> {
if !is_single_walk_spec(spec) {
return Err(RevListError::UnsupportedWalkSpec {
spec: spec.to_owned(),
}
.into());
}
let parsed = parse_rev_walk_tokens(self.repo, &[spec])?;
let include =
crate::rev_list::resolve_specs_for_commit_stream(self.repo, &parsed.positive)?;
let exclude =
crate::rev_list::resolve_specs_for_commit_stream(self.repo, &parsed.negative)?;
let params = CommitStreamParams {
skip: options.skip,
limit: options.limit,
since: options.since,
until: options.until,
first_parent: options.first_parent,
};
Ok(RevWalkCommitIter {
inner: open_commit_stream(self.repo, &include, &exclude, ¶ms)?,
})
}
}
pub struct RevWalkCommitIter<'repo> {
inner: crate::rev_list::CommitStream<'repo>,
}
impl Iterator for RevWalkCommitIter<'_> {
type Item = Result<ObjectId>;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next()
}
}
fn is_single_walk_spec(spec: &str) -> bool {
if spec.starts_with('-') {
return false;
}
let mut brace_depth = 0usize;
for c in spec.chars() {
match c {
':' if brace_depth == 0 => return true,
'{' => brace_depth += 1,
'}' => brace_depth = brace_depth.saturating_sub(1),
c if c.is_whitespace() && brace_depth == 0 => return false,
_ => {}
}
}
true
}
#[cfg(test)]
mod commit_iter_tests {
use super::*;
use crate::ident::Signature;
use crate::porcelain::commit::{create_commit, CommitRequest};
use crate::progress::{NeverCancel, NullProgress};
use crate::repo::{InitOptions, Repository};
fn init_repo_with_commits(count: usize) -> (tempfile::TempDir, Repository) {
let dir = tempfile::tempdir().expect("tempdir");
let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
for i in 0..count {
let mut req = CommitRequest::new(format!("commit {i}"));
req.author = Some(
Signature::new("Author", "a@example.com", repo.identity_now()).expect("author"),
);
req.committer = Some(
Signature::new("Committer", "c@example.com", repo.identity_now())
.expect("committer"),
);
req.allow_empty = true;
create_commit(&repo, &req, &mut NullProgress, &NeverCancel).expect("commit");
}
(dir, repo)
}
#[test]
fn commits_iter_stops_at_limit_without_full_rev_list() {
let (_dir, repo) = init_repo_with_commits(5);
let options = RevWalkCommitIterOptions {
limit: Some(2),
..RevWalkCommitIterOptions::default()
};
let ids: Vec<_> = repo
.rev_walk()
.commits_iter("HEAD", &options)
.expect("iter")
.collect::<Result<Vec<_>>>()
.expect("walk");
assert_eq!(ids.len(), 2);
}
#[test]
fn commits_iter_since_cutoff_filters_old_commits() {
let (_dir, repo) = init_repo_with_commits(4);
let newest = repo.rev_walk().resolve("HEAD").expect("head");
let params = crate::rev_list::CommitStreamParams {
limit: Some(10),
..Default::default()
};
let all: Vec<_> = open_commit_stream(&repo, std::slice::from_ref(&newest), &[], ¶ms)
.expect("stream")
.collect::<Result<Vec<_>>>()
.expect("walk");
assert_eq!(all.len(), 4);
let future = i64::MAX;
let filtered: Vec<_> = open_commit_stream(
&repo,
std::slice::from_ref(&newest),
&[],
&crate::rev_list::CommitStreamParams {
since: Some(future),
..Default::default()
},
)
.expect("stream")
.collect::<Result<Vec<_>>>()
.expect("walk");
assert!(filtered.is_empty());
}
}