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 open_commit_stream, rev_list, split_revision_token, CommitExtent, CommitStream,
16 CommitStreamItem, CommitStreamParams, OrderingMode, RevListOptions, RevListResult,
17};
18use crate::rev_list_error::RevListError;
19use crate::rev_parse::{
20 is_top_level_range, peel_to_commit_for_merge_base, resolve_revision,
21 resolve_revision_for_range_end, resolve_walk_range_end, split_top_level_triple_dot_range,
22 try_peel_to_commit_for_merge_base,
23};
24use crate::revwalk::filter::CommitFilter;
25
26use super::RevWalk;
27
28#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
30pub enum MergeFilter {
31 #[default]
33 Any,
34 OnlyMerges,
36 NoMerges,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum RefSelection {
43 All,
45 Branches(Option<String>),
47 Tags(Option<String>),
49 Remotes(Option<String>),
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct LeftRightCounts {
56 pub left: usize,
58 pub right: usize,
60}
61
62#[derive(Debug, Clone, Default)]
67pub struct RevWalkCommitIterOptions {
68 pub skip: usize,
70 pub limit: Option<usize>,
72 pub since: Option<i64>,
74 pub until: Option<i64>,
76 pub first_parent: bool,
78}
79
80#[derive(Debug, Clone, Default)]
88pub struct RevWalkListOptions {
89 pub limit: Option<usize>,
91 pub skip: usize,
93 pub since: Option<i64>,
95 pub until: Option<i64>,
97 pub merges: MergeFilter,
99 pub reverse: bool,
101 pub refs: Vec<RefSelection>,
103 pub first_parent: bool,
106 pub paths: Vec<String>,
108 pub follow: bool,
111 pub full_history: bool,
113 pub commit_filter: Option<Arc<CommitFilter>>,
115 pub pickaxe: Option<Arc<crate::diff::Pickaxe>>,
117 pub graph: bool,
119 pub retain_commit_data: bool,
122 pub retained_commit_extent: CommitExtent,
126 pub left_right: bool,
129 pub cherry_mark: bool,
133}
134
135#[derive(Debug, Clone, Default, PartialEq, Eq)]
141pub struct RevWalkParsedSpecs {
142 pub positive: Vec<String>,
144 pub negative: Vec<String>,
146 pub symmetric_left: Option<ObjectId>,
148 pub symmetric_right: Option<ObjectId>,
150}
151
152pub fn parse_rev_walk_tokens(repo: &Repository, tokens: &[&str]) -> Result<RevWalkParsedSpecs> {
164 let mut positive = Vec::new();
165 let mut negative = Vec::new();
166 let mut symmetric_left = None;
167 let mut symmetric_right = None;
168 for token in tokens {
169 if !is_top_level_range(token) {
170 match token.strip_prefix('^') {
171 Some(excluded) => negative.push(excluded.to_owned()),
172 None => positive.push((*token).to_owned()),
173 }
174 } else if let Some((left, right)) = split_top_level_triple_dot_range(token) {
175 let left_spec = if left.is_empty() { "HEAD" } else { left };
176 let right_spec = if right.is_empty() { "HEAD" } else { right };
177 let (pos, neg, left_oid, right_oid) =
178 symmetric_rev_list_specs(repo, left_spec, right_spec)?;
179 positive.extend(pos);
180 negative.extend(neg);
181 symmetric_left = Some(left_oid);
182 symmetric_right = Some(right_oid);
183 } else {
184 let (pos, neg) = split_revision_token(token);
185 positive.extend(pos);
186 negative.extend(neg);
187 }
188 }
189 Ok(RevWalkParsedSpecs {
190 positive,
191 negative,
192 symmetric_left,
193 symmetric_right,
194 })
195}
196
197fn symmetric_rev_list_specs(
198 repo: &Repository,
199 left_spec: &str,
200 right_spec: &str,
201) -> Result<(Vec<String>, Vec<String>, ObjectId, ObjectId)> {
202 let left_tip = resolve_walk_range_end(repo, left_spec)?;
203 let right_tip = resolve_walk_range_end(repo, right_spec)?;
204 let left = peel_to_commit_for_merge_base(repo, left_tip)?;
205 let right = peel_to_commit_for_merge_base(repo, right_tip)?;
206 let negative = merge_bases_all(repo, &[left, right])?
207 .iter()
208 .map(ObjectId::to_hex)
209 .collect();
210 Ok((vec![left.to_hex(), right.to_hex()], negative, left, right))
211}
212
213pub(crate) fn expand_ref_selections(
214 repo: &Repository,
215 selections: &[RefSelection],
216) -> Result<(bool, Vec<String>)> {
217 let mut all_refs = false;
218 let mut positive = Vec::new();
219 for selection in selections {
220 match selection {
221 RefSelection::All => {
222 all_refs = true;
223 }
224 RefSelection::Branches(pattern) => {
225 positive.extend(ref_glob_tips(repo, "refs/heads/", pattern)?);
226 }
227 RefSelection::Tags(pattern) => {
228 positive.extend(ref_glob_tips(repo, "refs/tags/", pattern)?);
229 }
230 RefSelection::Remotes(pattern) => {
231 positive.extend(ref_glob_tips(repo, "refs/remotes/", pattern)?);
232 }
233 }
234 }
235 Ok((all_refs, positive))
236}
237
238fn ref_glob_tips(repo: &Repository, prefix: &str, pattern: &Option<String>) -> Result<Vec<String>> {
239 let tips = if let Some(glob) = pattern {
240 let full_pattern = format!("{prefix}{glob}");
241 list_refs_glob_for_repository(repo, &full_pattern)?
242 } else {
243 list_refs_for_repository(repo, prefix)?
244 };
245 Ok(tips.into_iter().map(|(_, oid)| oid.to_hex()).collect())
246}
247
248fn merge_filter_to_parent_bounds(merges: MergeFilter) -> (Option<usize>, Option<usize>) {
249 match merges {
250 MergeFilter::Any => (None, None),
251 MergeFilter::OnlyMerges => (Some(2), None),
252 MergeFilter::NoMerges => (None, Some(1)),
253 }
254}
255
256pub fn validate_follow_pathspecs(paths: &[String]) -> Result<()> {
262 if paths.len() != 1 {
263 return Err(crate::rev_list_error::RevListError::FollowRequiresSinglePath.into());
264 }
265 use crate::pathspec::{exact_literal_pathspec_set, PathspecMatchContext};
266 if exact_literal_pathspec_set(paths, PathspecMatchContext::default()).is_none() {
267 return Err(crate::rev_list_error::RevListError::FollowRequiresLiteralPath.into());
268 }
269 Ok(())
270}
271
272pub(crate) fn rev_list_options_from_walk(
273 repo: &Repository,
274 walk: &RevWalkListOptions,
275 parsed: &RevWalkParsedSpecs,
276 all_refs: bool,
277 count: bool,
278) -> Result<RevListOptions> {
279 if walk.follow {
280 validate_follow_pathspecs(&walk.paths)?;
281 }
282 let paths = repo.resolve_rev_walk_pathspecs(&walk.paths)?;
283 let (min_parents, max_parents) = merge_filter_to_parent_bounds(walk.merges);
284 let graph = walk.graph;
285 Ok(RevListOptions {
286 first_parent: walk.first_parent,
287 max_count: walk.limit,
288 skip: walk.skip,
289 since_cutoff: walk.since,
290 until_cutoff: walk.until,
291 reverse: walk.reverse,
292 min_parents,
293 max_parents,
294 all_refs,
295 paths,
296 count,
297 ordering: if graph {
298 OrderingMode::Topo
299 } else {
300 OrderingMode::Default
301 },
302 preserve_simplify_merges_graph_merges: graph,
303 path_graph_reorder: graph,
304 parent_rewrite: graph && !walk.paths.is_empty(),
305 use_commit_graph: true,
306 use_commit_graph_bloom: !walk.paths.is_empty(),
307 paths_resolved_from_argv: !walk.paths.is_empty(),
308 symmetric_left: parsed.symmetric_left,
309 symmetric_right: parsed.symmetric_right,
310 commit_filter: walk.commit_filter.clone(),
311 pickaxe: walk.pickaxe.clone(),
312 retain_commit_data: walk.retain_commit_data,
313 left_right: walk.left_right || walk.cherry_mark,
314 cherry_mark: walk.cherry_mark,
315 follow: walk.follow,
316 full_history: walk.full_history,
317 ..RevListOptions::default()
318 })
319}
320
321pub fn committer_cutoff_from_date(
329 date: &str,
330 now_sec: i64,
331) -> std::result::Result<i64, DateParseError> {
332 parse_date_cutoff(date.trim(), now_sec).map_err(|_| DateParseError)
333}
334
335pub fn resolve_log_walk_tips_and_hide(
343 repo: &Repository,
344 tokens: &[&str],
345 walk: &RevWalkListOptions,
346) -> Result<(Vec<ObjectId>, Vec<ObjectId>)> {
347 let (parsed, positive, all_refs) = positive_specs_for_walk(repo, tokens, walk)?;
348 let mut include = crate::rev_list::resolve_revision_specs_to_commits(repo, &positive)?;
349 if all_refs {
350 include.extend(crate::rev_list::all_ref_commit_tips_for_walk(repo)?);
351 }
352 let exclude = crate::rev_list::resolve_revision_specs_to_commits(repo, &parsed.negative)?;
353 Ok((include, exclude))
354}
355
356pub fn rev_list_commits_for_author_group(
364 repo: &Repository,
365 tokens: &[&str],
366 walk: &RevWalkListOptions,
367) -> Result<Vec<ObjectId>> {
368 let (parsed, positive, all_refs) = positive_specs_for_walk(repo, tokens, walk)?;
369 let mut opts = rev_list_options_from_walk(repo, walk, &parsed, all_refs, false)?;
370 opts.use_bitmap_index = true;
371 let result = rev_list(repo, &positive, &parsed.negative, &opts)?;
372 Ok(result.commits)
373}
374
375pub(crate) fn positive_specs_for_walk(
376 repo: &Repository,
377 tokens: &[&str],
378 walk: &RevWalkListOptions,
379) -> Result<(RevWalkParsedSpecs, Vec<String>, bool)> {
380 let parsed = parse_rev_walk_tokens(repo, tokens)?;
381 let (all_refs, mut positive) = expand_ref_selections(repo, &walk.refs)?;
382 positive.extend(parsed.positive.iter().cloned());
383 Ok((parsed, positive, all_refs))
384}
385
386fn single_symmetric_range(
387 tokens: &[&str],
388 parsed: &RevWalkParsedSpecs,
389) -> Result<(ObjectId, ObjectId)> {
390 match (tokens, parsed.symmetric_left, parsed.symmetric_right) {
391 ([_], Some(left), Some(right)) => Ok((left, right)),
392 _ => Err(Error::RevList(RevListError::SymmetricRangeRequired)),
393 }
394}
395
396fn commit_count_from_result(result: RevListResult) -> Result<usize> {
397 Ok(result.commit_count())
398}
399
400fn walk_list_is_single_positive_bounded_page(walk: &RevWalkListOptions) -> bool {
401 walk.skip == 0
402 && matches!(walk.limit, Some(1) | Some(2))
403 && !walk.reverse
404 && walk.merges == MergeFilter::Any
405 && !walk.first_parent
406 && walk.paths.is_empty()
407 && walk.refs.is_empty()
408 && walk.since.is_none()
409 && walk.until.is_none()
410 && walk.pickaxe.is_none()
411 && walk.commit_filter.as_ref().is_none_or(|f| !f.is_active())
412 && !walk.graph
413}
414
415fn try_single_positive_bounded_page(
417 repo: &Repository,
418 tokens: &[&str],
419 walk: &RevWalkListOptions,
420) -> Result<Option<RevListResult>> {
421 if !walk_list_is_single_positive_bounded_page(walk) || tokens.len() != 1 {
422 return Ok(None);
423 }
424 let parsed = parse_rev_walk_tokens(repo, tokens)?;
425 if !parsed.negative.is_empty() || parsed.symmetric_left.is_some() || parsed.positive.len() != 1
426 {
427 return Ok(None);
428 }
429 let spec = &parsed.positive[0];
430 if spec.contains("..") {
431 return Ok(None);
432 }
433 let oid = resolve_revision_for_range_end(repo, spec)?;
434 let Some(commit_tip) = try_peel_to_commit_for_merge_base(repo, oid)? else {
435 return Ok(None);
436 };
437 let max_fetch = walk.limit.unwrap_or(1);
438 if max_fetch >= 2 {
439 let parents = crate::rev_parse::commit_parents_for_navigation(repo, commit_tip)?;
440 if parents.len() > 1 {
441 return Ok(None);
442 }
443 }
444 Ok(Some(
445 crate::rev_list::single_tip_bounded_page_with_parent_probe(
446 repo,
447 commit_tip,
448 max_fetch,
449 walk.retain_commit_data,
450 )?,
451 ))
452}
453
454#[must_use]
459pub fn rev_walk_list_supports_stream(walk: &RevWalkListOptions) -> bool {
460 !walk.graph
461 && walk.paths.is_empty()
462 && !walk.reverse
463 && !walk.first_parent
464 && walk.merges == MergeFilter::Any
465 && walk.commit_filter.as_ref().is_none_or(|f| !f.is_active())
466 && walk.pickaxe.is_none()
467 && !walk.left_right
468 && !walk.cherry_mark
469}
470
471pub type RevWalkCommitStream<'repo> = CommitStream<'repo>;
474
475pub type RevWalkCommitStreamItem = CommitStreamItem;
477
478impl<'repo> RevWalk<'repo> {
479 pub fn commits_stream(
489 &self,
490 tokens: &[&str],
491 walk: &RevWalkListOptions,
492 ) -> Result<Option<RevWalkCommitStream<'repo>>> {
493 if !rev_walk_list_supports_stream(walk) {
494 return Ok(None);
495 }
496 let (tips, hide) = resolve_log_walk_tips_and_hide(self.repository(), tokens, walk)?;
497 let params = CommitStreamParams {
498 skip: walk.skip,
499 limit: walk.limit,
500 since: walk.since,
501 until: walk.until,
502 first_parent: walk.first_parent,
503 retain_commit_bytes: walk.retain_commit_data,
504 retained_commit_extent: walk.retained_commit_extent,
505 };
506 Ok(Some(open_commit_stream(
507 self.repository(),
508 &tips,
509 &hide,
510 ¶ms,
511 )?))
512 }
513
514 pub fn commits(&self, tokens: &[&str], options: &RevWalkListOptions) -> Result<Vec<ObjectId>> {
525 Ok(self.rev_list_result(tokens, options)?.commits)
526 }
527
528 pub fn rev_list_result(
538 &self,
539 tokens: &[&str],
540 options: &RevWalkListOptions,
541 ) -> Result<crate::rev_list::RevListResult> {
542 let repo = self.repository();
543 let sides = options.left_right || options.cherry_mark;
544 if !sides {
545 if let Some(result) = try_single_positive_bounded_page(repo, tokens, options)? {
546 return Ok(result);
547 }
548 }
549 let (parsed, positive, all_refs) = positive_specs_for_walk(repo, tokens, options)?;
550 if sides {
551 single_symmetric_range(tokens, &parsed)?;
552 }
553 let opts = rev_list_options_from_walk(repo, options, &parsed, all_refs, false)?;
554 rev_list(repo, &positive, &parsed.negative, &opts)
555 }
556
557 pub fn count(&self, tokens: &[&str], options: &RevWalkListOptions) -> Result<usize> {
563 let repo = self.repository();
564 let (parsed, positive, all_refs) = positive_specs_for_walk(repo, tokens, options)?;
565 if parsed.negative.is_empty()
566 && positive.len() == 1
567 && options.skip == 0
568 && options.limit.is_some()
569 && (options.since.is_some() || options.until.is_some())
570 && options.paths.is_empty()
571 && options.refs.is_empty()
572 && !options.reverse
573 && options.merges == MergeFilter::Any
574 && !options.first_parent
575 && options
576 .commit_filter
577 .as_ref()
578 .is_none_or(|f| !f.is_active())
579 && options.pickaxe.is_none()
580 {
581 let tip = crate::rev_parse::resolve_revision_for_range_end(repo, &positive[0])?;
582 let tip = crate::rev_parse::peel_to_commit_for_merge_base(repo, tip)?;
583 let need = options.limit.unwrap_or(usize::MAX);
584 let cutoffs = crate::merge_base::ReachableWalkCommitterCutoffs {
585 since: options.since,
586 until: options.until,
587 };
588 if let Some(list) =
589 crate::merge_base::try_linear_chain_commits_with_cutoffs(repo, tip, need, cutoffs)?
590 {
591 return Ok(list.len());
592 }
593 }
594 if !parsed.negative.is_empty()
595 && parsed.symmetric_left.is_none()
596 && options.paths.is_empty()
597 && options.refs.is_empty()
598 && options.skip == 0
599 && options.limit.is_none()
600 && !options.reverse
601 && options
602 .commit_filter
603 .as_ref()
604 .is_none_or(|f| !f.is_active())
605 && options.pickaxe.is_none()
606 && !options.graph
607 {
608 let (include, exclude) = resolve_log_walk_tips_and_hide(repo, tokens, options)?;
609 let (min_parents, max_parents) = merge_filter_to_parent_bounds(options.merges);
610 let filter = ReachableCountFilter {
611 min_parents,
612 max_parents,
613 first_parent_only: options.first_parent,
614 since: options.since,
615 until: options.until,
616 };
617 return count_commits_reachable_excluding_ancestors_of(
618 repo, &include, &exclude, filter,
619 );
620 }
621 let opts = rev_list_options_from_walk(repo, options, &parsed, all_refs, true)?;
622 let result = rev_list(repo, &positive, &parsed.negative, &opts)?;
623 commit_count_from_result(result)
624 }
625
626 pub fn symmetric_range_tips(&self, tokens: &[&str]) -> Result<(ObjectId, ObjectId)> {
637 if tokens.len() != 1 {
638 return Err(Error::RevList(RevListError::SymmetricRangeRequired));
639 }
640 let parsed = parse_rev_walk_tokens(self.repository(), tokens)?;
641 single_symmetric_range(tokens, &parsed)
642 }
643
644 pub fn left_right_counts(
653 &self,
654 tokens: &[&str],
655 options: &RevWalkListOptions,
656 ) -> Result<LeftRightCounts> {
657 let repo = self.repository();
658 let (parsed, positive, all_refs) = positive_specs_for_walk(repo, tokens, options)?;
659 let (left, right) = single_symmetric_range(tokens, &parsed)?;
660
661 if options.paths.is_empty()
662 && options.refs.is_empty()
663 && options.skip == 0
664 && options.limit.is_none()
665 && !options.reverse
666 && options
667 .commit_filter
668 .as_ref()
669 .is_none_or(|f| !f.is_active())
670 && options.pickaxe.is_none()
671 {
672 let (min_parents, max_parents) = merge_filter_to_parent_bounds(options.merges);
673 let filter = ReachableCountFilter {
674 min_parents,
675 max_parents,
676 first_parent_only: options.first_parent,
677 since: options.since,
678 until: options.until,
679 };
680 let left_count =
681 count_commits_reachable_excluding_ancestors_of(repo, &[left], &[right], filter)?;
682 let right_count =
683 count_commits_reachable_excluding_ancestors_of(repo, &[right], &[left], filter)?;
684 return Ok(LeftRightCounts {
685 left: left_count,
686 right: right_count,
687 });
688 }
689
690 let mut opts = rev_list_options_from_walk(repo, options, &parsed, all_refs, true)?;
691 opts.max_count = None;
694 opts.left_right = true;
695 let left_count = {
696 let mut left_opts = opts.clone();
697 left_opts.left_only = true;
698 rev_list(repo, &positive, &parsed.negative, &left_opts)?.commit_count()
699 };
700 let mut right_opts = opts;
701 right_opts.right_only = true;
702 let right_count = rev_list(repo, &positive, &parsed.negative, &right_opts)?.commit_count();
703 Ok(LeftRightCounts {
704 left: left_count,
705 right: right_count,
706 })
707 }
708
709 pub fn merge_bases(&self, a: &str, b: &str, all: bool) -> Result<Vec<ObjectId>> {
715 let repo = self.repository();
716 let a_tip = resolve_revision(repo, a)?;
717 let b_tip = resolve_revision(repo, b)?;
718 let a_commit = peel_to_commit_for_merge_base(repo, a_tip)?;
719 let b_commit = peel_to_commit_for_merge_base(repo, b_tip)?;
720 if all {
721 merge_bases_all(repo, &[a_commit, b_commit])
722 } else {
723 merge_bases_first_vs_rest(repo, a_commit, &[b_commit])
724 }
725 }
726
727 pub fn is_ancestor(&self, ancestor: &str, descendant: &str) -> Result<bool> {
733 let repo = self.repository();
734 let ancestor_tip = resolve_revision(repo, ancestor)?;
735 let descendant_tip = resolve_revision(repo, descendant)?;
736 let ancestor_commit = peel_to_commit_for_merge_base(repo, ancestor_tip)?;
737 let descendant_commit = peel_to_commit_for_merge_base(repo, descendant_tip)?;
738 merge_base_is_ancestor(repo, ancestor_commit, descendant_commit)
739 }
740}
741
742#[cfg(test)]
743mod tests {
744 use super::*;
745
746 #[test]
747 fn split_double_dot_token_without_repo() {
748 let (pos, neg) = crate::rev_list::split_revision_token("main..feature");
749 assert_eq!(pos, vec!["feature".to_owned()]);
750 assert_eq!(neg, vec!["main".to_owned()]);
751 }
752
753 #[test]
754 fn merge_filter_parent_bounds() {
755 assert_eq!(
756 merge_filter_to_parent_bounds(MergeFilter::OnlyMerges),
757 (Some(2), None)
758 );
759 assert_eq!(
760 merge_filter_to_parent_bounds(MergeFilter::NoMerges),
761 (None, Some(1))
762 );
763 }
764
765 fn repo_with_commits(count: usize) -> (tempfile::TempDir, Repository) {
766 use crate::ident::Signature;
767 use crate::porcelain::commit::{create_commit, CommitRequest};
768 use crate::progress::{NeverCancel, NullProgress};
769 use crate::repo::{InitOptions, Repository};
770 let dir = tempfile::tempdir().expect("tempdir");
771 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
772 for i in 0..count {
773 let mut req = CommitRequest::new(format!("commit {i}"));
774 req.allow_empty = true;
775 req.author =
776 Some(Signature::new("A", "a@example.com", repo.identity_now()).expect("author"));
777 req.committer =
778 Some(Signature::new("C", "c@example.com", repo.identity_now()).expect("committer"));
779 create_commit(&repo, &req, &mut NullProgress, &NeverCancel).expect("commit");
780 }
781 (dir, repo)
782 }
783
784 #[test]
785 fn commits_stream_matches_rev_list_for_all_refs() {
786 let (_dir, repo) = repo_with_commits(4);
787 let walk = RevWalkListOptions {
788 refs: vec![RefSelection::All],
789 ..RevWalkListOptions::default()
790 };
791 let tokens = ["HEAD"];
792 let listed = repo
793 .rev_walk()
794 .rev_list_result(&tokens, &walk)
795 .expect("list")
796 .commits;
797 let streamed: Vec<_> = repo
798 .rev_walk()
799 .commits_stream(&tokens, &walk)
800 .expect("stream")
801 .expect("some")
802 .map(|item| item.expect("walk").oid)
803 .collect();
804 assert_eq!(streamed, listed);
805 }
806
807 #[test]
808 fn commits_stream_keeps_commits_through_their_subjects() {
809 use crate::ident::Signature;
810 use crate::porcelain::commit::{create_commit, CommitRequest};
811 use crate::progress::{NeverCancel, NullProgress};
812 use crate::repo::InitOptions;
813 let dir = tempfile::tempdir().expect("tempdir");
814 let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
815 for i in 0..4 {
816 let mut req =
817 CommitRequest::new(format!("subject {i}\n\n{}", "body line\n".repeat(50)));
818 req.allow_empty = true;
819 req.author =
820 Some(Signature::new("A", "a@example.com", repo.identity_now()).expect("author"));
821 req.committer =
822 Some(Signature::new("C", "c@example.com", repo.identity_now()).expect("committer"));
823 create_commit(&repo, &req, &mut NullProgress, &NeverCancel).expect("commit");
824 }
825 let stream = |extent| -> Vec<(ObjectId, Vec<u8>)> {
826 let walk = RevWalkListOptions {
827 retain_commit_data: true,
828 retained_commit_extent: extent,
829 ..RevWalkListOptions::default()
830 };
831 repo.rev_walk()
832 .commits_stream(&["HEAD"], &walk)
833 .expect("stream")
834 .expect("some")
835 .map(|item| {
836 let item = item.expect("walk");
837 (item.oid, item.commit_bytes.expect("kept bytes"))
838 })
839 .collect()
840 };
841 let whole = stream(CommitExtent::Whole);
842 let short = stream(CommitExtent::ThroughSubject);
843 assert_eq!(whole.len(), 4);
844 for ((oid, full), (short_oid, prefix)) in whole.iter().zip(&short) {
845 assert_eq!(oid, short_oid);
846 assert_eq!(*full, repo.odb().read(oid).expect("read").data);
847 assert!(prefix.len() < full.len() && full.starts_with(prefix));
848 assert_eq!(
849 *prefix,
850 repo.odb().read_commit_through_subject(oid).expect("prefix")
851 );
852 assert!(!prefix.windows(9).any(|w| w == b"body line"));
853 assert_eq!(
854 crate::objects::read_commit_subject_line(prefix).expect("subject"),
855 crate::objects::read_commit_subject_line(full).expect("subject")
856 );
857 }
858 }
859
860 #[test]
861 fn commits_stream_honors_limit_without_full_walk() {
862 let (_dir, repo) = repo_with_commits(5);
863 let walk = RevWalkListOptions {
864 limit: Some(2),
865 ..RevWalkListOptions::default()
866 };
867 let mut stream = repo
868 .rev_walk()
869 .commits_stream(&["HEAD"], &walk)
870 .expect("stream")
871 .expect("some");
872 let collected: Vec<_> = stream.by_ref().take(2).map(|r| r.unwrap().oid).collect();
873 assert_eq!(collected.len(), 2);
874 drop(stream);
875 let all = repo
876 .rev_walk()
877 .rev_list_result(&["HEAD"], &RevWalkListOptions::default())
878 .expect("list")
879 .commits;
880 assert_eq!(all.len(), 5);
881 }
882}