1use crate::error::{Error, Result};
3use crate::git_date::parse::DateParseError;
4use crate::git_date::parse_date_cutoff;
5use crate::merge_base::{
6 count_commits_reachable_excluding_ancestors_of, is_ancestor as merge_base_is_ancestor,
7 merge_bases_all, merge_bases_first_vs_rest, ReachableCountFilter,
8};
9use crate::objects::ObjectId;
10use crate::refs::{list_refs_for_repository, list_refs_glob_for_repository};
11use crate::repo::Repository;
12use std::sync::Arc;
13
14use crate::rev_list::{
15 rev_list, split_revision_token, OrderingMode, RevListOptions, RevListResult,
16};
17use crate::rev_list_error::RevListError;
18use crate::rev_parse::{
19 is_top_level_range, peel_to_commit_for_merge_base, resolve_revision,
20 resolve_revision_for_range_end, resolve_walk_range_end, split_top_level_triple_dot_range,
21 try_peel_to_commit_for_merge_base,
22};
23use crate::revwalk::filter::CommitFilter;
24
25use super::RevWalk;
26
27#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
29pub enum MergeFilter {
30 #[default]
32 Any,
33 OnlyMerges,
35 NoMerges,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum RefSelection {
42 All,
44 Branches(Option<String>),
46 Tags(Option<String>),
48 Remotes(Option<String>),
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct LeftRightCounts {
55 pub left: usize,
57 pub right: usize,
59}
60
61#[derive(Debug, Clone, Default)]
66pub struct RevWalkCommitIterOptions {
67 pub skip: usize,
69 pub limit: Option<usize>,
71 pub since: Option<i64>,
73 pub until: Option<i64>,
75 pub first_parent: bool,
77}
78
79#[derive(Debug, Clone, Default)]
87pub struct RevWalkListOptions {
88 pub limit: Option<usize>,
90 pub skip: usize,
92 pub since: Option<i64>,
94 pub until: Option<i64>,
96 pub merges: MergeFilter,
98 pub reverse: bool,
100 pub refs: Vec<RefSelection>,
102 pub first_parent: bool,
105 pub paths: Vec<String>,
107 pub commit_filter: Option<Arc<CommitFilter>>,
109 pub pickaxe: Option<Arc<crate::diff::Pickaxe>>,
111 pub graph: bool,
113 pub retain_commit_data: bool,
116 pub left_right: bool,
119 pub cherry_mark: bool,
123}
124
125#[derive(Debug, Clone, Default, PartialEq, Eq)]
131pub struct RevWalkParsedSpecs {
132 pub positive: Vec<String>,
134 pub negative: Vec<String>,
136 pub symmetric_left: Option<ObjectId>,
138 pub symmetric_right: Option<ObjectId>,
140}
141
142pub fn parse_rev_walk_tokens(repo: &Repository, tokens: &[&str]) -> Result<RevWalkParsedSpecs> {
154 let mut positive = Vec::new();
155 let mut negative = Vec::new();
156 let mut symmetric_left = None;
157 let mut symmetric_right = None;
158 for token in tokens {
159 if !is_top_level_range(token) {
160 match token.strip_prefix('^') {
161 Some(excluded) => negative.push(excluded.to_owned()),
162 None => positive.push((*token).to_owned()),
163 }
164 } else if let Some((left, right)) = split_top_level_triple_dot_range(token) {
165 let left_spec = if left.is_empty() { "HEAD" } else { left };
166 let right_spec = if right.is_empty() { "HEAD" } else { right };
167 let (pos, neg, left_oid, right_oid) =
168 symmetric_rev_list_specs(repo, left_spec, right_spec)?;
169 positive.extend(pos);
170 negative.extend(neg);
171 symmetric_left = Some(left_oid);
172 symmetric_right = Some(right_oid);
173 } else {
174 let (pos, neg) = split_revision_token(token);
175 positive.extend(pos);
176 negative.extend(neg);
177 }
178 }
179 Ok(RevWalkParsedSpecs {
180 positive,
181 negative,
182 symmetric_left,
183 symmetric_right,
184 })
185}
186
187fn symmetric_rev_list_specs(
188 repo: &Repository,
189 left_spec: &str,
190 right_spec: &str,
191) -> Result<(Vec<String>, Vec<String>, ObjectId, ObjectId)> {
192 let left_tip = resolve_walk_range_end(repo, left_spec)?;
193 let right_tip = resolve_walk_range_end(repo, right_spec)?;
194 let left = peel_to_commit_for_merge_base(repo, left_tip)?;
195 let right = peel_to_commit_for_merge_base(repo, right_tip)?;
196 let negative = merge_bases_all(repo, &[left, right])?
197 .iter()
198 .map(ObjectId::to_hex)
199 .collect();
200 Ok((vec![left.to_hex(), right.to_hex()], negative, left, right))
201}
202
203pub(crate) fn expand_ref_selections(
204 repo: &Repository,
205 selections: &[RefSelection],
206) -> Result<(bool, Vec<String>)> {
207 let mut all_refs = false;
208 let mut positive = Vec::new();
209 for selection in selections {
210 match selection {
211 RefSelection::All => {
212 all_refs = true;
213 }
214 RefSelection::Branches(pattern) => {
215 positive.extend(ref_glob_tips(repo, "refs/heads/", pattern)?);
216 }
217 RefSelection::Tags(pattern) => {
218 positive.extend(ref_glob_tips(repo, "refs/tags/", pattern)?);
219 }
220 RefSelection::Remotes(pattern) => {
221 positive.extend(ref_glob_tips(repo, "refs/remotes/", pattern)?);
222 }
223 }
224 }
225 Ok((all_refs, positive))
226}
227
228fn ref_glob_tips(repo: &Repository, prefix: &str, pattern: &Option<String>) -> Result<Vec<String>> {
229 let tips = if let Some(glob) = pattern {
230 let full_pattern = format!("{prefix}{glob}");
231 list_refs_glob_for_repository(repo, &full_pattern)?
232 } else {
233 list_refs_for_repository(repo, prefix)?
234 };
235 Ok(tips.into_iter().map(|(_, oid)| oid.to_hex()).collect())
236}
237
238fn merge_filter_to_parent_bounds(merges: MergeFilter) -> (Option<usize>, Option<usize>) {
239 match merges {
240 MergeFilter::Any => (None, None),
241 MergeFilter::OnlyMerges => (Some(2), None),
242 MergeFilter::NoMerges => (None, Some(1)),
243 }
244}
245
246pub(crate) fn rev_list_options_from_walk(
247 repo: &Repository,
248 walk: &RevWalkListOptions,
249 parsed: &RevWalkParsedSpecs,
250 all_refs: bool,
251 count: bool,
252) -> Result<RevListOptions> {
253 let paths = repo.resolve_rev_walk_pathspecs(&walk.paths)?;
254 let (min_parents, max_parents) = merge_filter_to_parent_bounds(walk.merges);
255 let graph = walk.graph;
256 Ok(RevListOptions {
257 first_parent: walk.first_parent,
258 max_count: walk.limit,
259 skip: walk.skip,
260 since_cutoff: walk.since,
261 until_cutoff: walk.until,
262 reverse: walk.reverse,
263 min_parents,
264 max_parents,
265 all_refs,
266 paths,
267 count,
268 ordering: if graph {
269 OrderingMode::Topo
270 } else {
271 OrderingMode::Default
272 },
273 preserve_simplify_merges_graph_merges: graph,
274 path_graph_reorder: graph,
275 parent_rewrite: graph && !walk.paths.is_empty(),
276 use_commit_graph: true,
277 use_commit_graph_bloom: !walk.paths.is_empty(),
278 paths_resolved_from_argv: !walk.paths.is_empty(),
279 symmetric_left: parsed.symmetric_left,
280 symmetric_right: parsed.symmetric_right,
281 commit_filter: walk.commit_filter.clone(),
282 pickaxe: walk.pickaxe.clone(),
283 retain_commit_data: walk.retain_commit_data,
284 left_right: walk.left_right || walk.cherry_mark,
285 cherry_mark: walk.cherry_mark,
286 ..RevListOptions::default()
287 })
288}
289
290pub fn committer_cutoff_from_date(
298 date: &str,
299 now_sec: i64,
300) -> std::result::Result<i64, DateParseError> {
301 parse_date_cutoff(date.trim(), now_sec).map_err(|_| DateParseError)
302}
303
304pub fn resolve_log_walk_tips_and_hide(
312 repo: &Repository,
313 tokens: &[&str],
314 walk: &RevWalkListOptions,
315) -> Result<(Vec<ObjectId>, Vec<ObjectId>)> {
316 let (parsed, positive, all_refs) = positive_specs_for_walk(repo, tokens, walk)?;
317 let mut include = crate::rev_list::resolve_revision_specs_to_commits(repo, &positive)?;
318 if all_refs {
319 include.extend(crate::rev_list::all_ref_commit_tips_for_walk(repo)?);
320 }
321 let exclude = crate::rev_list::resolve_revision_specs_to_commits(repo, &parsed.negative)?;
322 Ok((include, exclude))
323}
324
325pub fn rev_list_commits_for_author_group(
333 repo: &Repository,
334 tokens: &[&str],
335 walk: &RevWalkListOptions,
336) -> Result<Vec<ObjectId>> {
337 let (parsed, positive, all_refs) = positive_specs_for_walk(repo, tokens, walk)?;
338 let mut opts = rev_list_options_from_walk(repo, walk, &parsed, all_refs, false)?;
339 opts.use_bitmap_index = true;
340 let result = rev_list(repo, &positive, &parsed.negative, &opts)?;
341 Ok(result.commits)
342}
343
344pub(crate) fn positive_specs_for_walk(
345 repo: &Repository,
346 tokens: &[&str],
347 walk: &RevWalkListOptions,
348) -> Result<(RevWalkParsedSpecs, Vec<String>, bool)> {
349 let parsed = parse_rev_walk_tokens(repo, tokens)?;
350 let (all_refs, mut positive) = expand_ref_selections(repo, &walk.refs)?;
351 positive.extend(parsed.positive.iter().cloned());
352 Ok((parsed, positive, all_refs))
353}
354
355fn single_symmetric_range(
356 tokens: &[&str],
357 parsed: &RevWalkParsedSpecs,
358) -> Result<(ObjectId, ObjectId)> {
359 match (tokens, parsed.symmetric_left, parsed.symmetric_right) {
360 ([_], Some(left), Some(right)) => Ok((left, right)),
361 _ => Err(Error::RevList(RevListError::SymmetricRangeRequired)),
362 }
363}
364
365fn commit_count_from_result(result: RevListResult) -> Result<usize> {
366 Ok(result.commit_count())
367}
368
369fn walk_list_is_single_positive_bounded_page(walk: &RevWalkListOptions) -> bool {
370 walk.skip == 0
371 && matches!(walk.limit, Some(1) | Some(2))
372 && !walk.reverse
373 && walk.merges == MergeFilter::Any
374 && !walk.first_parent
375 && walk.paths.is_empty()
376 && walk.refs.is_empty()
377 && walk.since.is_none()
378 && walk.until.is_none()
379 && walk.pickaxe.is_none()
380 && walk.commit_filter.as_ref().is_none_or(|f| !f.is_active())
381 && !walk.graph
382}
383
384fn try_single_positive_bounded_page(
386 repo: &Repository,
387 tokens: &[&str],
388 walk: &RevWalkListOptions,
389) -> Result<Option<RevListResult>> {
390 if !walk_list_is_single_positive_bounded_page(walk) || tokens.len() != 1 {
391 return Ok(None);
392 }
393 let parsed = parse_rev_walk_tokens(repo, tokens)?;
394 if !parsed.negative.is_empty() || parsed.symmetric_left.is_some() || parsed.positive.len() != 1
395 {
396 return Ok(None);
397 }
398 let spec = &parsed.positive[0];
399 if spec.contains("..") {
400 return Ok(None);
401 }
402 let oid = resolve_revision_for_range_end(repo, spec)?;
403 let Some(commit_tip) = try_peel_to_commit_for_merge_base(repo, oid)? else {
404 return Ok(None);
405 };
406 let max_fetch = walk.limit.unwrap_or(1);
407 if max_fetch >= 2 {
408 let parents = crate::rev_parse::commit_parents_for_navigation(repo, commit_tip)?;
409 if parents.len() > 1 {
410 return Ok(None);
411 }
412 }
413 Ok(Some(
414 crate::rev_list::single_tip_bounded_page_with_parent_probe(
415 repo,
416 commit_tip,
417 max_fetch,
418 walk.retain_commit_data,
419 )?,
420 ))
421}
422
423impl<'repo> RevWalk<'repo> {
424 pub fn commits(&self, tokens: &[&str], options: &RevWalkListOptions) -> Result<Vec<ObjectId>> {
435 Ok(self.rev_list_result(tokens, options)?.commits)
436 }
437
438 pub fn rev_list_result(
448 &self,
449 tokens: &[&str],
450 options: &RevWalkListOptions,
451 ) -> Result<crate::rev_list::RevListResult> {
452 let repo = self.repository();
453 let sides = options.left_right || options.cherry_mark;
454 if !sides {
455 if let Some(result) = try_single_positive_bounded_page(repo, tokens, options)? {
456 return Ok(result);
457 }
458 }
459 let (parsed, positive, all_refs) = positive_specs_for_walk(repo, tokens, options)?;
460 if sides {
461 single_symmetric_range(tokens, &parsed)?;
462 }
463 let opts = rev_list_options_from_walk(repo, options, &parsed, all_refs, false)?;
464 rev_list(repo, &positive, &parsed.negative, &opts)
465 }
466
467 pub fn count(&self, tokens: &[&str], options: &RevWalkListOptions) -> Result<usize> {
473 let repo = self.repository();
474 let (parsed, positive, all_refs) = positive_specs_for_walk(repo, tokens, options)?;
475 if parsed.negative.is_empty()
476 && positive.len() == 1
477 && options.skip == 0
478 && options.limit.is_some()
479 && (options.since.is_some() || options.until.is_some())
480 && options.paths.is_empty()
481 && options.refs.is_empty()
482 && !options.reverse
483 && options.merges == MergeFilter::Any
484 && !options.first_parent
485 && options
486 .commit_filter
487 .as_ref()
488 .is_none_or(|f| !f.is_active())
489 && options.pickaxe.is_none()
490 {
491 let tip = crate::rev_parse::resolve_revision_for_range_end(repo, &positive[0])?;
492 let tip = crate::rev_parse::peel_to_commit_for_merge_base(repo, tip)?;
493 let need = options.limit.unwrap_or(usize::MAX);
494 let cutoffs = crate::merge_base::ReachableWalkCommitterCutoffs {
495 since: options.since,
496 until: options.until,
497 };
498 if let Some(list) =
499 crate::merge_base::try_linear_chain_commits_with_cutoffs(repo, tip, need, cutoffs)?
500 {
501 return Ok(list.len());
502 }
503 }
504 if !parsed.negative.is_empty()
505 && parsed.symmetric_left.is_none()
506 && options.paths.is_empty()
507 && options.refs.is_empty()
508 && options.skip == 0
509 && options.limit.is_none()
510 && !options.reverse
511 && options
512 .commit_filter
513 .as_ref()
514 .is_none_or(|f| !f.is_active())
515 && options.pickaxe.is_none()
516 && !options.graph
517 {
518 let (include, exclude) = resolve_log_walk_tips_and_hide(repo, tokens, options)?;
519 let (min_parents, max_parents) = merge_filter_to_parent_bounds(options.merges);
520 let filter = ReachableCountFilter {
521 min_parents,
522 max_parents,
523 first_parent_only: options.first_parent,
524 since: options.since,
525 until: options.until,
526 };
527 return count_commits_reachable_excluding_ancestors_of(
528 repo, &include, &exclude, filter,
529 );
530 }
531 let opts = rev_list_options_from_walk(repo, options, &parsed, all_refs, true)?;
532 let result = rev_list(repo, &positive, &parsed.negative, &opts)?;
533 commit_count_from_result(result)
534 }
535
536 pub fn symmetric_range_tips(&self, tokens: &[&str]) -> Result<(ObjectId, ObjectId)> {
547 if tokens.len() != 1 {
548 return Err(Error::RevList(RevListError::SymmetricRangeRequired));
549 }
550 let parsed = parse_rev_walk_tokens(self.repository(), tokens)?;
551 single_symmetric_range(tokens, &parsed)
552 }
553
554 pub fn left_right_counts(
563 &self,
564 tokens: &[&str],
565 options: &RevWalkListOptions,
566 ) -> Result<LeftRightCounts> {
567 let repo = self.repository();
568 let (parsed, positive, all_refs) = positive_specs_for_walk(repo, tokens, options)?;
569 let (left, right) = single_symmetric_range(tokens, &parsed)?;
570
571 if options.paths.is_empty()
572 && options.refs.is_empty()
573 && options.skip == 0
574 && options.limit.is_none()
575 && !options.reverse
576 && options
577 .commit_filter
578 .as_ref()
579 .is_none_or(|f| !f.is_active())
580 && options.pickaxe.is_none()
581 {
582 let (min_parents, max_parents) = merge_filter_to_parent_bounds(options.merges);
583 let filter = ReachableCountFilter {
584 min_parents,
585 max_parents,
586 first_parent_only: options.first_parent,
587 since: options.since,
588 until: options.until,
589 };
590 let left_count =
591 count_commits_reachable_excluding_ancestors_of(repo, &[left], &[right], filter)?;
592 let right_count =
593 count_commits_reachable_excluding_ancestors_of(repo, &[right], &[left], filter)?;
594 return Ok(LeftRightCounts {
595 left: left_count,
596 right: right_count,
597 });
598 }
599
600 let mut opts = rev_list_options_from_walk(repo, options, &parsed, all_refs, true)?;
601 opts.max_count = None;
604 opts.left_right = true;
605 let left_count = {
606 let mut left_opts = opts.clone();
607 left_opts.left_only = true;
608 rev_list(repo, &positive, &parsed.negative, &left_opts)?.commit_count()
609 };
610 let mut right_opts = opts;
611 right_opts.right_only = true;
612 let right_count = rev_list(repo, &positive, &parsed.negative, &right_opts)?.commit_count();
613 Ok(LeftRightCounts {
614 left: left_count,
615 right: right_count,
616 })
617 }
618
619 pub fn merge_bases(&self, a: &str, b: &str, all: bool) -> Result<Vec<ObjectId>> {
625 let repo = self.repository();
626 let a_tip = resolve_revision(repo, a)?;
627 let b_tip = resolve_revision(repo, b)?;
628 let a_commit = peel_to_commit_for_merge_base(repo, a_tip)?;
629 let b_commit = peel_to_commit_for_merge_base(repo, b_tip)?;
630 if all {
631 merge_bases_all(repo, &[a_commit, b_commit])
632 } else {
633 merge_bases_first_vs_rest(repo, a_commit, &[b_commit])
634 }
635 }
636
637 pub fn is_ancestor(&self, ancestor: &str, descendant: &str) -> Result<bool> {
643 let repo = self.repository();
644 let ancestor_tip = resolve_revision(repo, ancestor)?;
645 let descendant_tip = resolve_revision(repo, descendant)?;
646 let ancestor_commit = peel_to_commit_for_merge_base(repo, ancestor_tip)?;
647 let descendant_commit = peel_to_commit_for_merge_base(repo, descendant_tip)?;
648 merge_base_is_ancestor(repo, ancestor_commit, descendant_commit)
649 }
650}
651
652#[cfg(test)]
653mod tests {
654 use super::*;
655
656 #[test]
657 fn split_double_dot_token_without_repo() {
658 let (pos, neg) = crate::rev_list::split_revision_token("main..feature");
659 assert_eq!(pos, vec!["feature".to_owned()]);
660 assert_eq!(neg, vec!["main".to_owned()]);
661 }
662
663 #[test]
664 fn merge_filter_parent_bounds() {
665 assert_eq!(
666 merge_filter_to_parent_bounds(MergeFilter::OnlyMerges),
667 (Some(2), None)
668 );
669 assert_eq!(
670 merge_filter_to_parent_bounds(MergeFilter::NoMerges),
671 (None, Some(1))
672 );
673 }
674}