use crate::error::{Error, Result};
use crate::git_date::parse::DateParseError;
use crate::git_date::parse_date_cutoff;
use crate::merge_base::{
count_commits_reachable_excluding_ancestors_of, is_ancestor as merge_base_is_ancestor,
merge_bases_all, merge_bases_first_vs_rest, ReachableCountFilter,
};
use crate::objects::ObjectId;
use crate::refs::{list_refs_for_repository, list_refs_glob_for_repository};
use crate::repo::Repository;
use std::sync::Arc;
use crate::rev_list::{
open_commit_stream, rev_list, split_revision_token, CommitExtent, CommitStream,
CommitStreamItem, CommitStreamParams, OrderingMode, RevListOptions, RevListResult,
};
use crate::rev_list_error::RevListError;
use crate::rev_parse::{
is_top_level_range, peel_to_commit_for_merge_base, resolve_revision,
resolve_revision_for_range_end, resolve_walk_range_end, split_top_level_triple_dot_range,
try_peel_to_commit_for_merge_base,
};
use crate::revwalk::filter::CommitFilter;
use super::RevWalk;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum MergeFilter {
#[default]
Any,
OnlyMerges,
NoMerges,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RefSelection {
All,
Branches(Option<String>),
Tags(Option<String>),
Remotes(Option<String>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LeftRightCounts {
pub left: usize,
pub right: usize,
}
#[derive(Debug, Clone, Default)]
pub struct RevWalkCommitIterOptions {
pub skip: usize,
pub limit: Option<usize>,
pub since: Option<i64>,
pub until: Option<i64>,
pub first_parent: bool,
}
#[derive(Debug, Clone, Default)]
pub struct RevWalkListOptions {
pub limit: Option<usize>,
pub skip: usize,
pub since: Option<i64>,
pub until: Option<i64>,
pub merges: MergeFilter,
pub reverse: bool,
pub refs: Vec<RefSelection>,
pub first_parent: bool,
pub paths: Vec<String>,
pub follow: bool,
pub full_history: bool,
pub commit_filter: Option<Arc<CommitFilter>>,
pub pickaxe: Option<Arc<crate::diff::Pickaxe>>,
pub graph: bool,
pub retain_commit_data: bool,
pub retained_commit_extent: CommitExtent,
pub left_right: bool,
pub cherry_mark: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RevWalkParsedSpecs {
pub positive: Vec<String>,
pub negative: Vec<String>,
pub symmetric_left: Option<ObjectId>,
pub symmetric_right: Option<ObjectId>,
}
pub fn parse_rev_walk_tokens(repo: &Repository, tokens: &[&str]) -> Result<RevWalkParsedSpecs> {
let mut positive = Vec::new();
let mut negative = Vec::new();
let mut symmetric_left = None;
let mut symmetric_right = None;
for token in tokens {
if !is_top_level_range(token) {
match token.strip_prefix('^') {
Some(excluded) => negative.push(excluded.to_owned()),
None => positive.push((*token).to_owned()),
}
} else if let Some((left, right)) = split_top_level_triple_dot_range(token) {
let left_spec = if left.is_empty() { "HEAD" } else { left };
let right_spec = if right.is_empty() { "HEAD" } else { right };
let (pos, neg, left_oid, right_oid) =
symmetric_rev_list_specs(repo, left_spec, right_spec)?;
positive.extend(pos);
negative.extend(neg);
symmetric_left = Some(left_oid);
symmetric_right = Some(right_oid);
} else {
let (pos, neg) = split_revision_token(token);
positive.extend(pos);
negative.extend(neg);
}
}
Ok(RevWalkParsedSpecs {
positive,
negative,
symmetric_left,
symmetric_right,
})
}
fn symmetric_rev_list_specs(
repo: &Repository,
left_spec: &str,
right_spec: &str,
) -> Result<(Vec<String>, Vec<String>, ObjectId, ObjectId)> {
let left_tip = resolve_walk_range_end(repo, left_spec)?;
let right_tip = resolve_walk_range_end(repo, right_spec)?;
let left = peel_to_commit_for_merge_base(repo, left_tip)?;
let right = peel_to_commit_for_merge_base(repo, right_tip)?;
let negative = merge_bases_all(repo, &[left, right])?
.iter()
.map(ObjectId::to_hex)
.collect();
Ok((vec![left.to_hex(), right.to_hex()], negative, left, right))
}
pub(crate) fn expand_ref_selections(
repo: &Repository,
selections: &[RefSelection],
) -> Result<(bool, Vec<String>)> {
let mut all_refs = false;
let mut positive = Vec::new();
for selection in selections {
match selection {
RefSelection::All => {
all_refs = true;
}
RefSelection::Branches(pattern) => {
positive.extend(ref_glob_tips(repo, "refs/heads/", pattern)?);
}
RefSelection::Tags(pattern) => {
positive.extend(ref_glob_tips(repo, "refs/tags/", pattern)?);
}
RefSelection::Remotes(pattern) => {
positive.extend(ref_glob_tips(repo, "refs/remotes/", pattern)?);
}
}
}
Ok((all_refs, positive))
}
fn ref_glob_tips(repo: &Repository, prefix: &str, pattern: &Option<String>) -> Result<Vec<String>> {
let tips = if let Some(glob) = pattern {
let full_pattern = format!("{prefix}{glob}");
list_refs_glob_for_repository(repo, &full_pattern)?
} else {
list_refs_for_repository(repo, prefix)?
};
Ok(tips.into_iter().map(|(_, oid)| oid.to_hex()).collect())
}
fn merge_filter_to_parent_bounds(merges: MergeFilter) -> (Option<usize>, Option<usize>) {
match merges {
MergeFilter::Any => (None, None),
MergeFilter::OnlyMerges => (Some(2), None),
MergeFilter::NoMerges => (None, Some(1)),
}
}
pub fn validate_follow_pathspecs(paths: &[String]) -> Result<()> {
if paths.len() != 1 {
return Err(crate::rev_list_error::RevListError::FollowRequiresSinglePath.into());
}
use crate::pathspec::{exact_literal_pathspec_set, PathspecMatchContext};
if exact_literal_pathspec_set(paths, PathspecMatchContext::default()).is_none() {
return Err(crate::rev_list_error::RevListError::FollowRequiresLiteralPath.into());
}
Ok(())
}
pub(crate) fn rev_list_options_from_walk(
repo: &Repository,
walk: &RevWalkListOptions,
parsed: &RevWalkParsedSpecs,
all_refs: bool,
count: bool,
) -> Result<RevListOptions> {
if walk.follow {
validate_follow_pathspecs(&walk.paths)?;
}
let paths = repo.resolve_rev_walk_pathspecs(&walk.paths)?;
let (min_parents, max_parents) = merge_filter_to_parent_bounds(walk.merges);
let graph = walk.graph;
Ok(RevListOptions {
first_parent: walk.first_parent,
max_count: walk.limit,
skip: walk.skip,
since_cutoff: walk.since,
until_cutoff: walk.until,
reverse: walk.reverse,
min_parents,
max_parents,
all_refs,
paths,
count,
ordering: if graph {
OrderingMode::Topo
} else {
OrderingMode::Default
},
preserve_simplify_merges_graph_merges: graph,
path_graph_reorder: graph,
parent_rewrite: graph && !walk.paths.is_empty(),
use_commit_graph: true,
use_commit_graph_bloom: !walk.paths.is_empty(),
paths_resolved_from_argv: !walk.paths.is_empty(),
symmetric_left: parsed.symmetric_left,
symmetric_right: parsed.symmetric_right,
commit_filter: walk.commit_filter.clone(),
pickaxe: walk.pickaxe.clone(),
retain_commit_data: walk.retain_commit_data,
left_right: walk.left_right || walk.cherry_mark,
cherry_mark: walk.cherry_mark,
follow: walk.follow,
full_history: walk.full_history,
..RevListOptions::default()
})
}
pub fn committer_cutoff_from_date(
date: &str,
now_sec: i64,
) -> std::result::Result<i64, DateParseError> {
parse_date_cutoff(date.trim(), now_sec).map_err(|_| DateParseError)
}
pub fn resolve_log_walk_tips_and_hide(
repo: &Repository,
tokens: &[&str],
walk: &RevWalkListOptions,
) -> Result<(Vec<ObjectId>, Vec<ObjectId>)> {
let (parsed, positive, all_refs) = positive_specs_for_walk(repo, tokens, walk)?;
let mut include = crate::rev_list::resolve_revision_specs_to_commits(repo, &positive)?;
if all_refs {
include.extend(crate::rev_list::all_ref_commit_tips_for_walk(repo)?);
}
let exclude = crate::rev_list::resolve_revision_specs_to_commits(repo, &parsed.negative)?;
Ok((include, exclude))
}
pub fn rev_list_commits_for_author_group(
repo: &Repository,
tokens: &[&str],
walk: &RevWalkListOptions,
) -> Result<Vec<ObjectId>> {
let (parsed, positive, all_refs) = positive_specs_for_walk(repo, tokens, walk)?;
let mut opts = rev_list_options_from_walk(repo, walk, &parsed, all_refs, false)?;
opts.use_bitmap_index = true;
let result = rev_list(repo, &positive, &parsed.negative, &opts)?;
Ok(result.commits)
}
pub(crate) fn positive_specs_for_walk(
repo: &Repository,
tokens: &[&str],
walk: &RevWalkListOptions,
) -> Result<(RevWalkParsedSpecs, Vec<String>, bool)> {
let parsed = parse_rev_walk_tokens(repo, tokens)?;
let (all_refs, mut positive) = expand_ref_selections(repo, &walk.refs)?;
positive.extend(parsed.positive.iter().cloned());
Ok((parsed, positive, all_refs))
}
fn single_symmetric_range(
tokens: &[&str],
parsed: &RevWalkParsedSpecs,
) -> Result<(ObjectId, ObjectId)> {
match (tokens, parsed.symmetric_left, parsed.symmetric_right) {
([_], Some(left), Some(right)) => Ok((left, right)),
_ => Err(Error::RevList(RevListError::SymmetricRangeRequired)),
}
}
fn commit_count_from_result(result: RevListResult) -> Result<usize> {
Ok(result.commit_count())
}
fn walk_list_is_single_positive_bounded_page(walk: &RevWalkListOptions) -> bool {
walk.skip == 0
&& matches!(walk.limit, Some(1) | Some(2))
&& !walk.reverse
&& walk.merges == MergeFilter::Any
&& !walk.first_parent
&& walk.paths.is_empty()
&& walk.refs.is_empty()
&& walk.since.is_none()
&& walk.until.is_none()
&& walk.pickaxe.is_none()
&& walk.commit_filter.as_ref().is_none_or(|f| !f.is_active())
&& !walk.graph
}
fn try_single_positive_bounded_page(
repo: &Repository,
tokens: &[&str],
walk: &RevWalkListOptions,
) -> Result<Option<RevListResult>> {
if !walk_list_is_single_positive_bounded_page(walk) || tokens.len() != 1 {
return Ok(None);
}
let parsed = parse_rev_walk_tokens(repo, tokens)?;
if !parsed.negative.is_empty() || parsed.symmetric_left.is_some() || parsed.positive.len() != 1
{
return Ok(None);
}
let spec = &parsed.positive[0];
if spec.contains("..") {
return Ok(None);
}
let oid = resolve_revision_for_range_end(repo, spec)?;
let Some(commit_tip) = try_peel_to_commit_for_merge_base(repo, oid)? else {
return Ok(None);
};
let max_fetch = walk.limit.unwrap_or(1);
if max_fetch >= 2 {
let parents = crate::rev_parse::commit_parents_for_navigation(repo, commit_tip)?;
if parents.len() > 1 {
return Ok(None);
}
}
Ok(Some(
crate::rev_list::single_tip_bounded_page_with_parent_probe(
repo,
commit_tip,
max_fetch,
walk.retain_commit_data,
)?,
))
}
#[must_use]
pub fn rev_walk_list_supports_stream(walk: &RevWalkListOptions) -> bool {
!walk.graph
&& walk.paths.is_empty()
&& !walk.reverse
&& !walk.first_parent
&& walk.merges == MergeFilter::Any
&& walk.commit_filter.as_ref().is_none_or(|f| !f.is_active())
&& walk.pickaxe.is_none()
&& !walk.left_right
&& !walk.cherry_mark
}
pub type RevWalkCommitStream<'repo> = CommitStream<'repo>;
pub type RevWalkCommitStreamItem = CommitStreamItem;
impl<'repo> RevWalk<'repo> {
pub fn commits_stream(
&self,
tokens: &[&str],
walk: &RevWalkListOptions,
) -> Result<Option<RevWalkCommitStream<'repo>>> {
if !rev_walk_list_supports_stream(walk) {
return Ok(None);
}
let (tips, hide) = resolve_log_walk_tips_and_hide(self.repository(), tokens, walk)?;
let params = CommitStreamParams {
skip: walk.skip,
limit: walk.limit,
since: walk.since,
until: walk.until,
first_parent: walk.first_parent,
retain_commit_bytes: walk.retain_commit_data,
retained_commit_extent: walk.retained_commit_extent,
};
Ok(Some(open_commit_stream(
self.repository(),
&tips,
&hide,
¶ms,
)?))
}
pub fn commits(&self, tokens: &[&str], options: &RevWalkListOptions) -> Result<Vec<ObjectId>> {
Ok(self.rev_list_result(tokens, options)?.commits)
}
pub fn rev_list_result(
&self,
tokens: &[&str],
options: &RevWalkListOptions,
) -> Result<crate::rev_list::RevListResult> {
let repo = self.repository();
let sides = options.left_right || options.cherry_mark;
if !sides {
if let Some(result) = try_single_positive_bounded_page(repo, tokens, options)? {
return Ok(result);
}
}
let (parsed, positive, all_refs) = positive_specs_for_walk(repo, tokens, options)?;
if sides {
single_symmetric_range(tokens, &parsed)?;
}
let opts = rev_list_options_from_walk(repo, options, &parsed, all_refs, false)?;
rev_list(repo, &positive, &parsed.negative, &opts)
}
pub fn count(&self, tokens: &[&str], options: &RevWalkListOptions) -> Result<usize> {
let repo = self.repository();
let (parsed, positive, all_refs) = positive_specs_for_walk(repo, tokens, options)?;
if parsed.negative.is_empty()
&& positive.len() == 1
&& options.skip == 0
&& options.limit.is_some()
&& (options.since.is_some() || options.until.is_some())
&& options.paths.is_empty()
&& options.refs.is_empty()
&& !options.reverse
&& options.merges == MergeFilter::Any
&& !options.first_parent
&& options
.commit_filter
.as_ref()
.is_none_or(|f| !f.is_active())
&& options.pickaxe.is_none()
{
let tip = crate::rev_parse::resolve_revision_for_range_end(repo, &positive[0])?;
let tip = crate::rev_parse::peel_to_commit_for_merge_base(repo, tip)?;
let need = options.limit.unwrap_or(usize::MAX);
let cutoffs = crate::merge_base::ReachableWalkCommitterCutoffs {
since: options.since,
until: options.until,
};
if let Some(list) =
crate::merge_base::try_linear_chain_commits_with_cutoffs(repo, tip, need, cutoffs)?
{
return Ok(list.len());
}
}
if !parsed.negative.is_empty()
&& parsed.symmetric_left.is_none()
&& options.paths.is_empty()
&& options.refs.is_empty()
&& options.skip == 0
&& options.limit.is_none()
&& !options.reverse
&& options
.commit_filter
.as_ref()
.is_none_or(|f| !f.is_active())
&& options.pickaxe.is_none()
&& !options.graph
{
let (include, exclude) = resolve_log_walk_tips_and_hide(repo, tokens, options)?;
let (min_parents, max_parents) = merge_filter_to_parent_bounds(options.merges);
let filter = ReachableCountFilter {
min_parents,
max_parents,
first_parent_only: options.first_parent,
since: options.since,
until: options.until,
};
return count_commits_reachable_excluding_ancestors_of(
repo, &include, &exclude, filter,
);
}
let opts = rev_list_options_from_walk(repo, options, &parsed, all_refs, true)?;
let result = rev_list(repo, &positive, &parsed.negative, &opts)?;
commit_count_from_result(result)
}
pub fn symmetric_range_tips(&self, tokens: &[&str]) -> Result<(ObjectId, ObjectId)> {
if tokens.len() != 1 {
return Err(Error::RevList(RevListError::SymmetricRangeRequired));
}
let parsed = parse_rev_walk_tokens(self.repository(), tokens)?;
single_symmetric_range(tokens, &parsed)
}
pub fn left_right_counts(
&self,
tokens: &[&str],
options: &RevWalkListOptions,
) -> Result<LeftRightCounts> {
let repo = self.repository();
let (parsed, positive, all_refs) = positive_specs_for_walk(repo, tokens, options)?;
let (left, right) = single_symmetric_range(tokens, &parsed)?;
if options.paths.is_empty()
&& options.refs.is_empty()
&& options.skip == 0
&& options.limit.is_none()
&& !options.reverse
&& options
.commit_filter
.as_ref()
.is_none_or(|f| !f.is_active())
&& options.pickaxe.is_none()
{
let (min_parents, max_parents) = merge_filter_to_parent_bounds(options.merges);
let filter = ReachableCountFilter {
min_parents,
max_parents,
first_parent_only: options.first_parent,
since: options.since,
until: options.until,
};
let left_count =
count_commits_reachable_excluding_ancestors_of(repo, &[left], &[right], filter)?;
let right_count =
count_commits_reachable_excluding_ancestors_of(repo, &[right], &[left], filter)?;
return Ok(LeftRightCounts {
left: left_count,
right: right_count,
});
}
let mut opts = rev_list_options_from_walk(repo, options, &parsed, all_refs, true)?;
opts.max_count = None;
opts.left_right = true;
let left_count = {
let mut left_opts = opts.clone();
left_opts.left_only = true;
rev_list(repo, &positive, &parsed.negative, &left_opts)?.commit_count()
};
let mut right_opts = opts;
right_opts.right_only = true;
let right_count = rev_list(repo, &positive, &parsed.negative, &right_opts)?.commit_count();
Ok(LeftRightCounts {
left: left_count,
right: right_count,
})
}
pub fn merge_bases(&self, a: &str, b: &str, all: bool) -> Result<Vec<ObjectId>> {
let repo = self.repository();
let a_tip = resolve_revision(repo, a)?;
let b_tip = resolve_revision(repo, b)?;
let a_commit = peel_to_commit_for_merge_base(repo, a_tip)?;
let b_commit = peel_to_commit_for_merge_base(repo, b_tip)?;
if all {
merge_bases_all(repo, &[a_commit, b_commit])
} else {
merge_bases_first_vs_rest(repo, a_commit, &[b_commit])
}
}
pub fn is_ancestor(&self, ancestor: &str, descendant: &str) -> Result<bool> {
let repo = self.repository();
let ancestor_tip = resolve_revision(repo, ancestor)?;
let descendant_tip = resolve_revision(repo, descendant)?;
let ancestor_commit = peel_to_commit_for_merge_base(repo, ancestor_tip)?;
let descendant_commit = peel_to_commit_for_merge_base(repo, descendant_tip)?;
merge_base_is_ancestor(repo, ancestor_commit, descendant_commit)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn split_double_dot_token_without_repo() {
let (pos, neg) = crate::rev_list::split_revision_token("main..feature");
assert_eq!(pos, vec!["feature".to_owned()]);
assert_eq!(neg, vec!["main".to_owned()]);
}
#[test]
fn merge_filter_parent_bounds() {
assert_eq!(
merge_filter_to_parent_bounds(MergeFilter::OnlyMerges),
(Some(2), None)
);
assert_eq!(
merge_filter_to_parent_bounds(MergeFilter::NoMerges),
(None, Some(1))
);
}
fn repo_with_commits(count: usize) -> (tempfile::TempDir, Repository) {
use crate::ident::Signature;
use crate::porcelain::commit::{create_commit, CommitRequest};
use crate::progress::{NeverCancel, NullProgress};
use crate::repo::{InitOptions, 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.allow_empty = true;
req.author =
Some(Signature::new("A", "a@example.com", repo.identity_now()).expect("author"));
req.committer =
Some(Signature::new("C", "c@example.com", repo.identity_now()).expect("committer"));
create_commit(&repo, &req, &mut NullProgress, &NeverCancel).expect("commit");
}
(dir, repo)
}
#[test]
fn commits_stream_matches_rev_list_for_all_refs() {
let (_dir, repo) = repo_with_commits(4);
let walk = RevWalkListOptions {
refs: vec![RefSelection::All],
..RevWalkListOptions::default()
};
let tokens = ["HEAD"];
let listed = repo
.rev_walk()
.rev_list_result(&tokens, &walk)
.expect("list")
.commits;
let streamed: Vec<_> = repo
.rev_walk()
.commits_stream(&tokens, &walk)
.expect("stream")
.expect("some")
.map(|item| item.expect("walk").oid)
.collect();
assert_eq!(streamed, listed);
}
#[test]
fn commits_stream_keeps_commits_through_their_subjects() {
use crate::ident::Signature;
use crate::porcelain::commit::{create_commit, CommitRequest};
use crate::progress::{NeverCancel, NullProgress};
use crate::repo::InitOptions;
let dir = tempfile::tempdir().expect("tempdir");
let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
for i in 0..4 {
let mut req =
CommitRequest::new(format!("subject {i}\n\n{}", "body line\n".repeat(50)));
req.allow_empty = true;
req.author =
Some(Signature::new("A", "a@example.com", repo.identity_now()).expect("author"));
req.committer =
Some(Signature::new("C", "c@example.com", repo.identity_now()).expect("committer"));
create_commit(&repo, &req, &mut NullProgress, &NeverCancel).expect("commit");
}
let stream = |extent| -> Vec<(ObjectId, Vec<u8>)> {
let walk = RevWalkListOptions {
retain_commit_data: true,
retained_commit_extent: extent,
..RevWalkListOptions::default()
};
repo.rev_walk()
.commits_stream(&["HEAD"], &walk)
.expect("stream")
.expect("some")
.map(|item| {
let item = item.expect("walk");
(item.oid, item.commit_bytes.expect("kept bytes"))
})
.collect()
};
let whole = stream(CommitExtent::Whole);
let short = stream(CommitExtent::ThroughSubject);
assert_eq!(whole.len(), 4);
for ((oid, full), (short_oid, prefix)) in whole.iter().zip(&short) {
assert_eq!(oid, short_oid);
assert_eq!(*full, repo.odb().read(oid).expect("read").data);
assert!(prefix.len() < full.len() && full.starts_with(prefix));
assert_eq!(
*prefix,
repo.odb().read_commit_through_subject(oid).expect("prefix")
);
assert!(!prefix.windows(9).any(|w| w == b"body line"));
assert_eq!(
crate::objects::read_commit_subject_line(prefix).expect("subject"),
crate::objects::read_commit_subject_line(full).expect("subject")
);
}
}
#[test]
fn commits_stream_honors_limit_without_full_walk() {
let (_dir, repo) = repo_with_commits(5);
let walk = RevWalkListOptions {
limit: Some(2),
..RevWalkListOptions::default()
};
let mut stream = repo
.rev_walk()
.commits_stream(&["HEAD"], &walk)
.expect("stream")
.expect("some");
let collected: Vec<_> = stream.by_ref().take(2).map(|r| r.unwrap().oid).collect();
assert_eq!(collected.len(), 2);
drop(stream);
let all = repo
.rev_walk()
.rev_list_result(&["HEAD"], &RevWalkListOptions::default())
.expect("list")
.commits;
assert_eq!(all.len(), 5);
}
}