1use std::cmp::{Ordering, Reverse};
9use std::collections::{BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque};
10use std::fs;
11use std::path::Path;
12
13use crate::diff::zero_oid;
14use crate::error::{Error, Result};
15use crate::ident::{committer_unix_seconds_for_ordering, parse_signature_times};
16use crate::ignore::{parse_sparse_patterns_from_blob, path_matches_sparse_pattern_list};
17use crate::index::Index;
18use crate::objects::{parse_commit, parse_tag, parse_tree, ObjectId, ObjectKind};
19use crate::pack;
20use crate::patch_ids::compute_patch_id;
21use crate::reflog::{list_reflog_refs, read_reflog};
22use crate::refs;
23use crate::repo::Repository;
24use crate::rev_parse::{resolve_revision_for_range_end, resolve_treeish_path, split_treeish_spec};
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum OutputMode {
29 OidOnly,
31 Parents,
33 Format(String),
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum MissingAction {
40 Error,
42 Print,
44 Allow,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum FilterObjectKind {
51 Blob,
52 Tree,
53 Commit,
54 Tag,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum ObjectFilter {
60 BlobNone,
62 BlobLimit(u64),
64 TreeDepth(u64),
66 SparseOid(ObjectId),
68 ObjectType(FilterObjectKind),
70 Combine(Vec<ObjectFilter>),
72}
73
74impl ObjectFilter {
75 pub fn parse(spec: &str) -> std::result::Result<Self, String> {
77 if spec == "blob:none" {
78 return Ok(ObjectFilter::BlobNone);
79 }
80 if let Some(rest) = spec.strip_prefix("blob:limit=") {
81 let bytes = parse_size_suffix(rest)
82 .ok_or_else(|| format!("invalid blob:limit value: {rest}"))?;
83 return Ok(ObjectFilter::BlobLimit(bytes));
84 }
85 if let Some(rest) = spec.strip_prefix("tree:") {
86 let depth: u64 = rest
87 .parse()
88 .map_err(|_| format!("invalid tree depth: {rest}"))?;
89 return Ok(ObjectFilter::TreeDepth(depth));
90 }
91 if let Some(rest) = spec.strip_prefix("object:type=") {
92 let kind = match rest {
93 "blob" => FilterObjectKind::Blob,
94 "tree" => FilterObjectKind::Tree,
95 "commit" => FilterObjectKind::Commit,
96 "tag" => FilterObjectKind::Tag,
97 "" => return Err("invalid object type".to_owned()),
98 _ => return Err(format!("invalid object type: {rest}")),
99 };
100 return Ok(ObjectFilter::ObjectType(kind));
101 }
102 if let Some(rest) = spec.strip_prefix("sparse:oid=") {
103 let oid = rest
104 .parse::<ObjectId>()
105 .map_err(|_| format!("invalid sparse:oid value: {rest}"))?;
106 return Ok(ObjectFilter::SparseOid(oid));
107 }
108 if let Some(rest) = spec.strip_prefix("combine:") {
109 let parts = split_combine(rest);
110 let mut filters = Vec::new();
111 for part in parts {
112 filters.push(ObjectFilter::parse(&part)?);
113 }
114 return Ok(ObjectFilter::Combine(filters));
115 }
116 Err(format!("unsupported filter spec: {spec}"))
117 }
118
119 #[must_use]
121 pub fn merge_with(self, other: Self) -> Self {
122 match (self, other) {
123 (ObjectFilter::Combine(mut a), ObjectFilter::Combine(mut b)) => {
124 a.append(&mut b);
125 ObjectFilter::Combine(a)
126 }
127 (ObjectFilter::Combine(mut a), b) => {
128 a.push(b);
129 ObjectFilter::Combine(a)
130 }
131 (a, ObjectFilter::Combine(mut b)) => {
132 let mut v = vec![a];
133 v.append(&mut b);
134 ObjectFilter::Combine(v)
135 }
136 (a, b) => ObjectFilter::Combine(vec![a, b]),
137 }
138 }
139
140 pub fn includes_blob(&self, size: u64) -> bool {
142 match self {
143 ObjectFilter::BlobNone => false,
144 ObjectFilter::BlobLimit(limit) => size <= *limit,
145 ObjectFilter::TreeDepth(_) => true,
148 ObjectFilter::SparseOid(_) => true,
149 ObjectFilter::ObjectType(kind) => *kind == FilterObjectKind::Blob,
150 ObjectFilter::Combine(filters) => filters.iter().all(|f| f.includes_blob(size)),
151 }
152 }
153
154 #[must_use]
159 pub fn includes_blob_under_tree(&self, size: u64, parent_tree_depth: u64) -> bool {
160 match self {
161 ObjectFilter::BlobNone => false,
162 ObjectFilter::BlobLimit(limit) => size <= *limit,
163 ObjectFilter::TreeDepth(max_depth) => parent_tree_depth.saturating_add(1) < *max_depth,
164 ObjectFilter::SparseOid(_) => true,
165 ObjectFilter::ObjectType(kind) => *kind == FilterObjectKind::Blob,
166 ObjectFilter::Combine(filters) => filters
167 .iter()
168 .all(|f| f.includes_blob_under_tree(size, parent_tree_depth)),
169 }
170 }
171
172 pub fn includes_tree(&self, depth: u64) -> bool {
174 match self {
175 ObjectFilter::BlobNone => true,
176 ObjectFilter::BlobLimit(_) => true,
177 ObjectFilter::TreeDepth(max_depth) => depth < *max_depth,
178 ObjectFilter::SparseOid(_) => true,
179 ObjectFilter::ObjectType(kind) => *kind == FilterObjectKind::Tree,
180 ObjectFilter::Combine(filters) => filters.iter().all(|f| f.includes_tree(depth)),
181 }
182 }
183
184 pub fn includes_commit_or_tag_object(&self, kind: ObjectKind) -> bool {
186 let expected = match kind {
187 ObjectKind::Commit => Some(FilterObjectKind::Commit),
188 ObjectKind::Tag => Some(FilterObjectKind::Tag),
189 _ => None,
190 };
191 match self {
192 ObjectFilter::BlobNone | ObjectFilter::BlobLimit(_) => true,
193 ObjectFilter::TreeDepth(_) => true,
194 ObjectFilter::SparseOid(_) => true,
195 ObjectFilter::ObjectType(t) => expected == Some(*t),
196 ObjectFilter::Combine(filters) => filters
197 .iter()
198 .all(|f| f.includes_commit_or_tag_object(kind)),
199 }
200 }
201
202 pub fn includes_loose_object(&self, kind: ObjectKind, size: u64) -> bool {
204 match kind {
205 ObjectKind::Blob => self.includes_blob(size),
206 ObjectKind::Tree => self.includes_tree(0),
207 ObjectKind::Commit | ObjectKind::Tag => self.includes_commit_or_tag_object(kind),
208 }
209 }
210
211 #[must_use]
213 pub fn passes_for_object(&self, kind: ObjectKind, size: usize) -> bool {
214 self.includes_loose_object(kind, size as u64)
215 }
216}
217
218#[must_use]
221pub fn reachable_object_ids_for_cat_file(
222 repo: &Repository,
223 filter: Option<&ObjectFilter>,
224 filter_provided_objects: bool,
225) -> Result<Vec<ObjectId>> {
226 let opts = RevListOptions {
227 all_refs: true,
228 objects: true,
229 no_object_names: true,
230 quiet: true,
231 filter: filter.cloned(),
232 filter_provided_objects,
233 ..Default::default()
234 };
235 let result = rev_list(repo, &[], &[], &opts)?;
236 let mut set = BTreeSet::new();
237 for (i, oid) in result.commits.iter().enumerate() {
238 if result.objects_print_commit.get(i).copied().unwrap_or(true) {
239 set.insert(*oid);
240 }
241 }
242 for (oid, _) in &result.objects {
243 set.insert(*oid);
244 }
245 Ok(set.into_iter().collect())
246}
247
248#[must_use]
251pub fn object_ids_for_cat_file_filtered(
252 repo: &Repository,
253 filter: &ObjectFilter,
254) -> Result<Vec<ObjectId>> {
255 reachable_object_ids_for_cat_file(repo, Some(filter), true)
256}
257
258fn parse_size_suffix(s: &str) -> Option<u64> {
260 let s = s.trim();
261 if s.is_empty() {
262 return None;
263 }
264 let (num_str, multiplier) = match s.as_bytes().last()? {
265 b'k' | b'K' => (&s[..s.len() - 1], 1024u64),
266 b'm' | b'M' => (&s[..s.len() - 1], 1024 * 1024),
267 b'g' | b'G' => (&s[..s.len() - 1], 1024 * 1024 * 1024),
268 _ => (s, 1u64),
269 };
270 let num: u64 = num_str.parse().ok()?;
271 Some(num * multiplier)
272}
273
274fn split_combine(spec: &str) -> Vec<String> {
276 let mut parts = Vec::new();
277 let mut current = String::new();
278 let chars = spec.chars().peekable();
279 for ch in chars {
280 if ch == '+' {
281 if !current.is_empty() {
282 parts.push(url_decode(¤t));
283 current.clear();
284 }
285 } else {
286 current.push(ch);
287 }
288 }
289 if !current.is_empty() {
290 parts.push(url_decode(¤t));
291 }
292 parts
293}
294
295fn url_decode(s: &str) -> String {
297 let mut result = String::new();
298 let mut chars = s.chars();
299 while let Some(ch) = chars.next() {
300 if ch == '%' {
301 let hi = chars.next().unwrap_or('0');
302 let lo = chars.next().unwrap_or('0');
303 let byte = u8::from_str_radix(&format!("{hi}{lo}"), 16).unwrap_or(b'?');
304 result.push(byte as char);
305 } else {
306 result.push(ch);
307 }
308 }
309 result
310}
311
312#[derive(Debug, Clone, Copy, PartialEq, Eq)]
314pub enum OrderingMode {
315 Default,
317 Topo,
319 Date,
321}
322
323#[derive(Debug, Clone)]
325pub struct RevListOptions {
326 pub all_refs: bool,
328 pub first_parent: bool,
330 pub ancestry_path: bool,
332 pub ancestry_path_bottoms: Vec<ObjectId>,
334 pub simplify_by_decoration: bool,
336 pub output_mode: OutputMode,
338 pub quiet: bool,
340 pub count: bool,
342 pub skip: usize,
344 pub max_count: Option<usize>,
346 pub ordering: OrderingMode,
348 pub reverse: bool,
350 pub objects: bool,
352 pub no_object_names: bool,
354 pub boundary: bool,
356 pub left_right: bool,
358 pub left_only: bool,
360 pub right_only: bool,
362 pub cherry_mark: bool,
364 pub cherry_pick: bool,
366 pub min_parents: Option<usize>,
368 pub max_parents: Option<usize>,
370 pub symmetric_left: Option<ObjectId>,
372 pub symmetric_right: Option<ObjectId>,
374 pub paths: Vec<String>,
376 pub full_history: bool,
378 pub sparse: bool,
380 pub filter: Option<ObjectFilter>,
382 pub filter_provided_objects: bool,
384 pub filter_print_omitted: bool,
386 pub in_commit_order: bool,
388 pub no_kept_objects: bool,
390 pub missing_action: MissingAction,
392 pub use_bitmap_index: bool,
394 pub unpacked_only: bool,
396 pub bitmap_oid_only_objects: bool,
399 pub until_cutoff: Option<i64>,
401 pub since_cutoff: Option<i64>,
403 pub include_reflog_entries: bool,
405 pub include_indexed_objects: bool,
407}
408
409impl Default for RevListOptions {
410 fn default() -> Self {
411 Self {
412 all_refs: false,
413 first_parent: false,
414 ancestry_path: false,
415 ancestry_path_bottoms: Vec::new(),
416 simplify_by_decoration: false,
417 output_mode: OutputMode::OidOnly,
418 quiet: false,
419 count: false,
420 skip: 0,
421 max_count: None,
422 ordering: OrderingMode::Default,
423 reverse: false,
424 objects: false,
425 no_object_names: false,
426 boundary: false,
427 left_right: false,
428 left_only: false,
429 right_only: false,
430 cherry_mark: false,
431 cherry_pick: false,
432 min_parents: None,
433 max_parents: None,
434 symmetric_left: None,
435 symmetric_right: None,
436 paths: Vec::new(),
437 full_history: false,
438 sparse: false,
439 filter: None,
440 filter_provided_objects: false,
441 filter_print_omitted: false,
442 in_commit_order: false,
443 no_kept_objects: false,
444 missing_action: MissingAction::Error,
445 use_bitmap_index: false,
446 unpacked_only: false,
447 bitmap_oid_only_objects: false,
448 until_cutoff: None,
449 since_cutoff: None,
450 include_reflog_entries: false,
451 include_indexed_objects: false,
452 }
453 }
454}
455
456#[derive(Debug, Clone)]
458pub struct RevListResult {
459 pub commits: Vec<ObjectId>,
461 pub objects: Vec<(ObjectId, String)>,
464 pub omitted_objects: Vec<ObjectId>,
466 pub missing_objects: Vec<ObjectId>,
468 pub boundary_commits: Vec<ObjectId>,
470 pub left_right_map: HashMap<ObjectId, bool>,
472 pub cherry_equivalent: HashSet<ObjectId>,
474 pub per_commit_object_counts: Vec<usize>,
478 pub object_walk_tips: Vec<ObjectId>,
480 pub objects_print_commit: Vec<bool>,
483 pub object_segments: Vec<Vec<(ObjectId, String)>>,
486 pub bitmap_object_format: bool,
488}
489
490pub fn rev_list(
504 repo: &Repository,
505 positive_specs: &[String],
506 negative_specs: &[String],
507 options: &RevListOptions,
508) -> Result<RevListResult> {
509 let mut graph = CommitGraph::new(repo, options.first_parent);
510
511 let (mut include, object_roots) = if options.objects {
512 resolve_specs_for_objects(repo, positive_specs)?
513 } else {
514 (resolve_specs(repo, positive_specs)?, Vec::new())
515 };
516 let exclude = resolve_specs(repo, negative_specs)?;
517
518 if options.all_refs {
519 include.extend(all_ref_tips(repo)?);
520 }
521
522 if options.objects && options.include_reflog_entries {
523 include.extend(reflog_commit_tips(repo)?);
524 }
525
526 let mut index_blob_roots: Vec<RootObject> = Vec::new();
527 if options.objects && options.include_indexed_objects
528 && repo.work_tree.is_some() {
529 let index_path = repo.git_dir.join("index");
530 if index_path.is_file() {
531 let idx = Index::load(&index_path)?;
532 for e in &idx.entries {
533 if e.stage() != 0 {
534 continue;
535 }
536 let path_str = String::from_utf8_lossy(&e.path).into_owned();
537 index_blob_roots.push(RootObject {
538 oid: e.oid,
539 input: format!(":{path_str}"),
540 expected_kind: Some(ExpectedObjectKind::Blob),
541 root_path: Some(path_str),
542 });
543 }
544 }
545 }
546
547 let object_roots = if index_blob_roots.is_empty() {
548 object_roots
549 } else {
550 let mut merged = object_roots;
551 merged.extend(index_blob_roots);
552 merged
553 };
554
555 if include.is_empty() && object_roots.is_empty() {
556 return Err(Error::InvalidRef("no revisions specified".to_owned()));
557 }
558
559 let object_walk_tip_commits: Vec<ObjectId> = if options.objects {
560 include.clone()
561 } else {
562 Vec::new()
563 };
564
565 let (mut included, _discovery_order) = if include.is_empty() {
566 (HashSet::new(), Vec::new())
567 } else {
568 walk_closure_ordered(&mut graph, &include)?
569 };
570 let excluded = if exclude.is_empty() {
571 HashSet::new()
572 } else {
573 walk_closure(&mut graph, &exclude)?
574 };
575 included.retain(|oid| !excluded.contains(oid));
576
577 if options.simplify_by_decoration {
578 let decorated = all_ref_tips(repo)?;
579 included.retain(|oid| decorated.contains(oid));
580 }
581
582 if options.ancestry_path {
583 let mut bottoms = options.ancestry_path_bottoms.clone();
584 if bottoms.is_empty() {
585 bottoms.extend(exclude.iter().copied());
586 }
587 if bottoms.is_empty() {
588 return Err(Error::InvalidRef(
589 "--ancestry-path requires a range with excluded tips".to_owned(),
590 ));
591 }
592 limit_to_ancestry(&mut graph, &mut included, &bottoms)?;
593 }
594
595 if options.min_parents.is_some() || options.max_parents.is_some() {
597 let min_p = options.min_parents.unwrap_or(0);
598 let max_p = options.max_parents.unwrap_or(usize::MAX);
599 included.retain(|oid| {
600 let count = graph.parents_of(*oid).map(|p| p.len()).unwrap_or(0);
601 count >= min_p && count <= max_p
602 });
603 }
604
605 let mut ordered = match options.ordering {
606 OrderingMode::Default => {
607 let tips: Vec<ObjectId> = include
608 .iter()
609 .copied()
610 .filter(|oid| included.contains(oid))
611 .collect();
612 date_order_walk(&mut graph, &tips, &included)?
613 }
614 OrderingMode::Topo | OrderingMode::Date => topo_sort(&mut graph, &included)?,
615 };
616
617 if !options.paths.is_empty() {
619 let paths = &options.paths;
620 ordered.retain(|oid| {
621 commit_touches_paths(
622 repo,
623 &mut graph,
624 *oid,
625 paths,
626 options.full_history,
627 options.sparse,
628 )
629 .unwrap_or(false)
630 });
631 }
632
633 let mut left_right_map = HashMap::new();
635 if options.left_right
636 || options.left_only
637 || options.right_only
638 || options.cherry_mark
639 || options.cherry_pick
640 {
641 if let (Some(left_oid), Some(right_oid)) = (options.symmetric_left, options.symmetric_right)
642 {
643 let left_reach = walk_closure(&mut graph, &[left_oid])?;
648 let right_reach = walk_closure(&mut graph, &[right_oid])?;
649 for &oid in &ordered {
650 let from_left = left_reach.contains(&oid);
651 let from_right = right_reach.contains(&oid);
652 if from_left && !from_right {
653 left_right_map.insert(oid, true);
654 } else if from_right && !from_left {
655 left_right_map.insert(oid, false);
656 } else {
657 left_right_map.insert(oid, false);
658 }
659 }
660 }
661 }
662
663 let mut cherry_equivalent = HashSet::new();
666 if options.cherry_pick || options.cherry_mark {
667 let left_commits: Vec<_> = ordered
668 .iter()
669 .filter(|o| left_right_map.get(o) == Some(&true))
670 .copied()
671 .collect();
672 let right_commits: Vec<_> = ordered
673 .iter()
674 .filter(|o| left_right_map.get(o) == Some(&false))
675 .copied()
676 .collect();
677 let left_first = !left_commits.is_empty()
678 && !right_commits.is_empty()
679 && left_commits.len() < right_commits.len();
680
681 let mut by_patch: HashMap<ObjectId, ObjectId> = HashMap::new();
682 if left_first {
683 for oid in &left_commits {
684 if let Ok(Some(pid)) = compute_patch_id(&repo.odb, oid) {
685 by_patch.entry(pid).or_insert(*oid);
686 }
687 }
688 for oid in &right_commits {
689 if let Ok(Some(pid)) = compute_patch_id(&repo.odb, oid) {
690 if let Some(&other) = by_patch.get(&pid) {
691 cherry_equivalent.insert(*oid);
692 cherry_equivalent.insert(other);
693 }
694 }
695 }
696 } else {
697 for oid in &right_commits {
698 if let Ok(Some(pid)) = compute_patch_id(&repo.odb, oid) {
699 by_patch.entry(pid).or_insert(*oid);
700 }
701 }
702 for oid in &left_commits {
703 if let Ok(Some(pid)) = compute_patch_id(&repo.odb, oid) {
704 if let Some(&other) = by_patch.get(&pid) {
705 cherry_equivalent.insert(*oid);
706 cherry_equivalent.insert(other);
707 }
708 }
709 }
710 }
711 }
712
713 if options.left_only {
715 ordered.retain(|oid| left_right_map.get(oid) == Some(&true));
716 }
717 if options.right_only {
718 ordered.retain(|oid| left_right_map.get(oid) == Some(&false));
719 }
720
721 if options.cherry_pick {
723 ordered.retain(|oid| !cherry_equivalent.contains(oid));
724 }
725
726 if options.until_cutoff.is_some() || options.since_cutoff.is_some() {
727 let until = options.until_cutoff;
728 let since = options.since_cutoff;
729 ordered.retain(|oid| {
730 let ts = graph.committer_time(*oid);
731 if let Some(u) = until {
732 if ts > u {
733 return false;
734 }
735 }
736 if let Some(s) = since {
737 if ts < s {
738 return false;
739 }
740 }
741 true
742 });
743 }
744
745 if options.skip > 0 {
746 ordered = ordered.into_iter().skip(options.skip).collect();
747 }
748 if let Some(max_count) = options.max_count {
749 ordered.truncate(max_count);
750 }
751 if options.reverse {
752 ordered.reverse();
753 }
754
755 let boundary_commits = if options.boundary {
757 let included_set: HashSet<ObjectId> = ordered.iter().copied().collect();
758 let mut boundary = Vec::new();
759 let mut boundary_seen = HashSet::new();
760 for &oid in &ordered {
761 if let Ok(parents) = graph.parents_of(oid).map(|p| p.to_vec()) {
762 for parent in parents {
763 if !included_set.contains(&parent) && boundary_seen.insert(parent) {
764 boundary.push(parent);
765 }
766 }
767 }
768 }
769 boundary
770 } else {
771 Vec::new()
772 };
773
774 let kept_set = if options.no_kept_objects {
776 kept_object_ids(repo).unwrap_or_default()
777 } else {
778 HashSet::new()
779 };
780
781 if options.no_kept_objects {
782 ordered.retain(|oid| !kept_set.contains(oid));
783 }
784
785 if options.unpacked_only {
786 let packed = packed_object_set(repo);
787 ordered.retain(|oid| !packed.contains(oid));
788 }
789
790 let commit_tips_set: HashSet<ObjectId> = object_walk_tip_commits.iter().copied().collect();
791 let objects_print_commit: Vec<bool> = if options.objects {
792 ordered
793 .iter()
794 .map(|&c| {
795 let user_given = !options.filter_provided_objects && commit_tips_set.contains(&c);
796 user_given || filter_shows_commit_line_when_not_user_given(options.filter.as_ref())
797 })
798 .collect()
799 } else {
800 Vec::new()
801 };
802
803 let sparse_lines = sparse_oid_lines_from_filter(repo, options.filter.as_ref());
804 let skip_trees = skip_tree_descent_for_object_type_filter(options.filter.as_ref());
805 let bitmap_object_format = options.objects
806 && options.use_bitmap_index
807 && (options.bitmap_oid_only_objects || !object_roots.is_empty() || options.unpacked_only);
808 let omit_object_paths = bitmap_object_format;
809 let packed_set = if options.objects && options.unpacked_only {
810 Some(packed_object_set(repo))
811 } else {
812 None
813 };
814
815 let (objects, omitted_objects, missing_objects, per_commit_object_counts, object_segments) =
817 if options.objects {
818 let filter_provided = options.filter_provided_objects;
819 let (mut objs, omit, miss, counts, segments) = if options.in_commit_order {
820 let (o, om, mi, c) = collect_reachable_objects_in_commit_order(
821 repo,
822 &mut graph,
823 &ordered,
824 &object_roots,
825 options.filter.as_ref(),
826 filter_provided,
827 options.missing_action,
828 sparse_lines.as_deref(),
829 skip_trees,
830 omit_object_paths,
831 packed_set.as_ref(),
832 )?;
833 (o, om, mi, c, Vec::new())
834 } else {
835 let (o, om, mi, seg) = collect_reachable_objects_segmented(
836 repo,
837 &mut graph,
838 &ordered,
839 &object_roots,
840 options.filter.as_ref(),
841 filter_provided,
842 options.missing_action,
843 sparse_lines.as_deref(),
844 skip_trees,
845 omit_object_paths,
846 packed_set.as_ref(),
847 )?;
848 (o, om, mi, Vec::new(), seg)
849 };
850 if options.no_kept_objects {
851 objs.retain(|(oid, _)| !kept_set.contains(oid));
852 }
853 (objs, omit, miss, counts, segments)
854 } else {
855 (Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new())
856 };
857
858 Ok(RevListResult {
859 commits: ordered,
860 objects,
861 omitted_objects,
862 missing_objects,
863 boundary_commits,
864 left_right_map,
865 cherry_equivalent,
866 per_commit_object_counts,
867 object_walk_tips: object_walk_tip_commits,
868 objects_print_commit,
869 object_segments,
870 bitmap_object_format,
871 })
872}
873
874#[must_use]
881pub fn split_revision_token(token: &str) -> (Vec<String>, Vec<String>) {
882 if let Some((lhs, rhs)) = crate::rev_parse::split_double_dot_range(token) {
883 let positive = if rhs.is_empty() {
884 "HEAD".to_owned()
885 } else {
886 rhs.to_owned()
887 };
888 let negative = if lhs.is_empty() {
889 "HEAD".to_owned()
890 } else {
891 lhs.to_owned()
892 };
893 return (vec![positive], vec![negative]);
894 }
895 if let Some(rest) = token.strip_prefix('^') {
896 return (Vec::new(), vec![rest.to_owned()]);
897 }
898 (vec![token.to_owned()], Vec::new())
899}
900
901fn ansi_color_from_name(name: &str) -> String {
902 match name {
903 "red" => "\x1b[31m".to_owned(),
904 "green" => "\x1b[32m".to_owned(),
905 "yellow" => "\x1b[33m".to_owned(),
906 "blue" => "\x1b[34m".to_owned(),
907 "magenta" => "\x1b[35m".to_owned(),
908 "cyan" => "\x1b[36m".to_owned(),
909 "white" => "\x1b[37m".to_owned(),
910 "bold" => "\x1b[1m".to_owned(),
911 "dim" => "\x1b[2m".to_owned(),
912 "ul" | "underline" => "\x1b[4m".to_owned(),
913 "blink" => "\x1b[5m".to_owned(),
914 "reverse" => "\x1b[7m".to_owned(),
915 "reset" => "\x1b[m".to_owned(),
916 _ => String::new(),
917 }
918}
919
920fn color_name_to_code(name: &str) -> Option<u8> {
921 match name {
922 "black" => Some(0),
923 "red" => Some(1),
924 "green" => Some(2),
925 "yellow" => Some(3),
926 "blue" => Some(4),
927 "magenta" => Some(5),
928 "cyan" => Some(6),
929 "white" => Some(7),
930 "default" => Some(9),
931 _ => None,
932 }
933}
934
935fn ansi_color_from_spec(spec: &str) -> String {
936 if spec == "reset" {
937 return "\x1b[m".to_owned();
938 }
939 let mut codes = Vec::new();
940 let mut fg_set = false;
941 for part in spec.split_whitespace() {
942 match part {
943 "bold" => codes.push("1".to_owned()),
944 "dim" => codes.push("2".to_owned()),
945 "italic" => codes.push("3".to_owned()),
946 "ul" | "underline" => codes.push("4".to_owned()),
947 "blink" => codes.push("5".to_owned()),
948 "reverse" => codes.push("7".to_owned()),
949 "strike" => codes.push("9".to_owned()),
950 "nobold" | "nodim" => codes.push("22".to_owned()),
951 "noitalic" => codes.push("23".to_owned()),
952 "noul" | "nounderline" => codes.push("24".to_owned()),
953 "noblink" => codes.push("25".to_owned()),
954 "noreverse" => codes.push("27".to_owned()),
955 "nostrike" => codes.push("29".to_owned()),
956 _ => {
957 if let Some(code) = color_name_to_code(part) {
958 if !fg_set {
959 codes.push(format!("{}", 30 + code));
960 fg_set = true;
961 } else {
962 codes.push(format!("{}", 40 + code));
963 }
964 }
965 }
966 }
967 }
968 if codes.is_empty() {
969 String::new()
970 } else {
971 format!("\x1b[{}m", codes.join(";"))
972 }
973}
974
975fn format_relative_date(diff: i64) -> String {
976 if diff < 0 {
977 "in the future".to_owned()
978 } else if diff < 60 {
979 format!("{} seconds ago", diff)
980 } else if diff < 3600 {
981 let m = diff / 60;
982 if m == 1 {
983 "1 minute ago".to_owned()
984 } else {
985 format!("{m} minutes ago")
986 }
987 } else if diff < 86400 {
988 let h = diff / 3600;
989 if h == 1 {
990 "1 hour ago".to_owned()
991 } else {
992 format!("{h} hours ago")
993 }
994 } else if diff < 86400 * 30 {
995 let d = diff / 86400;
996 if d == 1 {
997 "1 day ago".to_owned()
998 } else {
999 format!("{d} days ago")
1000 }
1001 } else if diff < 86400 * 365 {
1002 let months = diff / (86400 * 30);
1003 if months == 1 {
1004 "1 month ago".to_owned()
1005 } else {
1006 format!("{months} months ago")
1007 }
1008 } else {
1009 let years = diff / (86400 * 365);
1010 if years == 1 {
1011 "1 year ago".to_owned()
1012 } else {
1013 format!("{years} years ago")
1014 }
1015 }
1016}
1017
1018pub fn render_commit(
1024 repo: &Repository,
1025 oid: ObjectId,
1026 mode: &OutputMode,
1027 abbrev_len: usize,
1028) -> Result<String> {
1029 render_commit_with_color(repo, oid, mode, abbrev_len, false)
1030}
1031
1032pub fn render_commit_with_color(
1034 repo: &Repository,
1035 oid: ObjectId,
1036 mode: &OutputMode,
1037 abbrev_len: usize,
1038 use_color: bool,
1039) -> Result<String> {
1040 match mode {
1041 OutputMode::OidOnly => Ok(format!("{oid}")),
1042 OutputMode::Parents => {
1043 let mut out = format!("{oid}");
1044 let commit = load_commit(repo, oid)?;
1045 for parent in commit.parents {
1046 out.push(' ');
1047 out.push_str(&parent.to_hex());
1048 }
1049 Ok(out)
1050 }
1051 OutputMode::Format(fmt) => {
1052 let commit = load_commit(repo, oid)?;
1053 let subject = commit.message.lines().next().unwrap_or_default();
1054 let hex = oid.to_hex();
1055
1056 match fmt.as_str() {
1058 "oneline" => {
1059 return Ok(format!("{} {}", hex, subject));
1060 }
1061 "short" => {
1062 fn fmt_ident(ident: &str) -> String {
1063 let name = if let Some(bracket) = ident.find('<') {
1064 ident[..bracket].trim()
1065 } else {
1066 ident.trim()
1067 };
1068 let email = if let Some(start) = ident.find('<') {
1069 if let Some(end) = ident.find('>') {
1070 &ident[start..=end]
1071 } else {
1072 ""
1073 }
1074 } else {
1075 ""
1076 };
1077 format!("{} {}", name, email)
1078 }
1079 let mut out = String::new();
1080 out.push_str(&format!("Author: {}\n", fmt_ident(&commit.author)));
1081 out.push('\n');
1082 out.push_str(&format!(" {}\n", subject));
1083 out.push('\n');
1084 return Ok(out);
1085 }
1086 "medium" => {
1087 fn extract_ident_display(ident: &str) -> String {
1088 let name = if let Some(bracket) = ident.find('<') {
1089 ident[..bracket].trim()
1090 } else {
1091 ident.trim()
1092 };
1093 let email = if let Some(start) = ident.find('<') {
1094 if let Some(end) = ident.find('>') {
1095 &ident[start..=end]
1096 } else {
1097 ""
1098 }
1099 } else {
1100 ""
1101 };
1102 format!("{} {}", name, email)
1103 }
1104 fn format_default_date(ident: &str) -> String {
1105 let parts: Vec<&str> = ident.rsplitn(3, ' ').collect();
1106 if parts.len() < 2 {
1107 return String::new();
1108 }
1109 let ts_str = parts[1];
1110 let offset_str = parts[0];
1111 let ts: i64 = match ts_str.parse() {
1112 Ok(v) => v,
1113 Err(_) => return format!("{ts_str} {offset_str}"),
1114 };
1115 let tz_bytes = offset_str.as_bytes();
1116 let tz_secs: i64 = if tz_bytes.len() >= 5 {
1117 let sign = if tz_bytes[0] == b'-' { -1i64 } else { 1i64 };
1118 let h: i64 = offset_str[1..3].parse().unwrap_or(0);
1119 let m: i64 = offset_str[3..5].parse().unwrap_or(0);
1120 sign * (h * 3600 + m * 60)
1121 } else {
1122 0
1123 };
1124 let adjusted = ts + tz_secs;
1125 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
1126 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
1127 let weekday = match dt.weekday() {
1128 time::Weekday::Monday => "Mon",
1129 time::Weekday::Tuesday => "Tue",
1130 time::Weekday::Wednesday => "Wed",
1131 time::Weekday::Thursday => "Thu",
1132 time::Weekday::Friday => "Fri",
1133 time::Weekday::Saturday => "Sat",
1134 time::Weekday::Sunday => "Sun",
1135 };
1136 let month = match dt.month() {
1137 time::Month::January => "Jan",
1138 time::Month::February => "Feb",
1139 time::Month::March => "Mar",
1140 time::Month::April => "Apr",
1141 time::Month::May => "May",
1142 time::Month::June => "Jun",
1143 time::Month::July => "Jul",
1144 time::Month::August => "Aug",
1145 time::Month::September => "Sep",
1146 time::Month::October => "Oct",
1147 time::Month::November => "Nov",
1148 time::Month::December => "Dec",
1149 };
1150 format!(
1151 "{} {} {} {:02}:{:02}:{:02} {} {}",
1152 weekday,
1153 month,
1154 dt.day(),
1155 dt.hour(),
1156 dt.minute(),
1157 dt.second(),
1158 dt.year(),
1159 offset_str
1160 )
1161 }
1162 let mut out = String::new();
1163 out.push_str(&format!(
1164 "Author: {}\n",
1165 extract_ident_display(&commit.author)
1166 ));
1167 out.push_str(&format!(
1168 "Date: {}\n",
1169 format_default_date(&commit.author)
1170 ));
1171 out.push('\n');
1172 for line in commit.message.lines() {
1173 out.push_str(&format!(" {}\n", line));
1174 }
1175 return Ok(out);
1176 }
1177 _ => {}
1178 }
1179
1180 let raw_fmt = if let Some(t) = fmt.strip_prefix("format:") {
1181 t
1182 } else if let Some(t) = fmt.strip_prefix("tformat:") {
1183 t
1184 } else {
1185 fmt.as_str()
1186 };
1187 let body = {
1189 let mut lines = commit.message.lines();
1190 lines.next(); if let Some(blank) = lines.next() {
1193 if blank.is_empty() {
1194 lines.collect::<Vec<_>>().join("\n")
1195 } else {
1196 std::iter::once(blank)
1197 .chain(lines)
1198 .collect::<Vec<_>>()
1199 .join("\n")
1200 }
1201 } else {
1202 String::new()
1203 }
1204 };
1205 let tree_hex = commit.tree.to_hex();
1206 let parent_hexes: Vec<String> = commit.parents.iter().map(|p| p.to_hex()).collect();
1207 let parent_abbrevs: Vec<String> = commit
1208 .parents
1209 .iter()
1210 .map(|p| {
1211 let hex = p.to_hex();
1212 let n = abbrev_len.clamp(4, 40).min(hex.len());
1213 hex[..n].to_string()
1214 })
1215 .collect();
1216
1217 fn extract_name(ident: &str) -> &str {
1219 if let Some(bracket) = ident.find('<') {
1220 ident[..bracket].trim()
1221 } else {
1222 ident.trim()
1223 }
1224 }
1225 fn extract_email(ident: &str) -> &str {
1226 if let Some(start) = ident.find('<') {
1227 if let Some(end) = ident.find('>') {
1228 return &ident[start + 1..end];
1229 }
1230 }
1231 ""
1232 }
1233 fn extract_timestamp(ident: &str) -> String {
1234 match parse_signature_times(ident) {
1235 Some(p) => p.unix_seconds.to_string(),
1236 None => String::new(),
1237 }
1238 }
1239 fn weekday_str(dt: &time::OffsetDateTime) -> &'static str {
1240 match dt.weekday() {
1241 time::Weekday::Monday => "Mon",
1242 time::Weekday::Tuesday => "Tue",
1243 time::Weekday::Wednesday => "Wed",
1244 time::Weekday::Thursday => "Thu",
1245 time::Weekday::Friday => "Fri",
1246 time::Weekday::Saturday => "Sat",
1247 time::Weekday::Sunday => "Sun",
1248 }
1249 }
1250 fn month_str(dt: &time::OffsetDateTime) -> &'static str {
1251 match dt.month() {
1252 time::Month::January => "Jan",
1253 time::Month::February => "Feb",
1254 time::Month::March => "Mar",
1255 time::Month::April => "Apr",
1256 time::Month::May => "May",
1257 time::Month::June => "Jun",
1258 time::Month::July => "Jul",
1259 time::Month::August => "Aug",
1260 time::Month::September => "Sep",
1261 time::Month::October => "Oct",
1262 time::Month::November => "Nov",
1263 time::Month::December => "Dec",
1264 }
1265 }
1266 fn extract_email_local(ident: &str) -> &str {
1267 let email = extract_email(ident);
1268 if let Some(at) = email.find('@') {
1269 &email[..at]
1270 } else {
1271 email
1272 }
1273 }
1274 fn extract_date_default(ident: &str) -> String {
1275 let Some(p) = parse_signature_times(ident) else {
1276 return String::new();
1277 };
1278 let offset_str = ident.get(p.tz_hhmm_range.clone()).unwrap_or("+0000");
1279 let adjusted = p.unix_seconds + p.tz_offset_secs;
1280 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
1281 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
1282 format!(
1283 "{} {} {} {:02}:{:02}:{:02} {} {}",
1284 weekday_str(&dt),
1285 month_str(&dt),
1286 dt.day(),
1287 dt.hour(),
1288 dt.minute(),
1289 dt.second(),
1290 dt.year(),
1291 offset_str
1292 )
1293 }
1294 fn extract_date_rfc2822(ident: &str) -> String {
1295 let Some(p) = parse_signature_times(ident) else {
1296 return String::new();
1297 };
1298 let offset_str = ident.get(p.tz_hhmm_range.clone()).unwrap_or("+0000");
1299 let adjusted = p.unix_seconds + p.tz_offset_secs;
1300 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
1301 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
1302 format!(
1303 "{}, {} {} {} {:02}:{:02}:{:02} {}",
1304 weekday_str(&dt),
1305 dt.day(),
1306 month_str(&dt),
1307 dt.year(),
1308 dt.hour(),
1309 dt.minute(),
1310 dt.second(),
1311 offset_str
1312 )
1313 }
1314 fn extract_date_short(ident: &str) -> String {
1315 let Some(p) = parse_signature_times(ident) else {
1316 return String::new();
1317 };
1318 let adjusted = p.unix_seconds + p.tz_offset_secs;
1319 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
1320 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
1321 format!("{:04}-{:02}-{:02}", dt.year(), dt.month() as u8, dt.day())
1322 }
1323 fn extract_date_iso(ident: &str) -> String {
1324 let Some(p) = parse_signature_times(ident) else {
1325 return String::new();
1326 };
1327 let offset_str = ident.get(p.tz_hhmm_range.clone()).unwrap_or("+0000");
1328 let adjusted = p.unix_seconds + p.tz_offset_secs;
1329 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
1330 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
1331 format!(
1332 "{:04}-{:02}-{:02} {:02}:{:02}:{:02} {}",
1333 dt.year(),
1334 dt.month() as u8,
1335 dt.day(),
1336 dt.hour(),
1337 dt.minute(),
1338 dt.second(),
1339 offset_str
1340 )
1341 }
1342
1343 #[derive(Clone, Copy)]
1345 enum Align {
1346 Left,
1347 Right,
1348 Center,
1349 }
1350 #[derive(Clone, Copy)]
1351 enum Trunc {
1352 None,
1353 Trunc,
1354 LTrunc,
1355 MTrunc,
1356 }
1357 struct ColSpec {
1358 width: usize,
1359 align: Align,
1360 trunc: Trunc,
1361 }
1362 fn apply_col(spec: &ColSpec, s: &str) -> String {
1363 let char_len = s.chars().count();
1364 if char_len > spec.width {
1365 match spec.trunc {
1366 Trunc::None => s.to_owned(),
1367 Trunc::Trunc => {
1368 let mut out: String =
1369 s.chars().take(spec.width.saturating_sub(2)).collect();
1370 out.push_str("..");
1371 out
1372 }
1373 Trunc::LTrunc => {
1374 let skip = char_len - spec.width + 2;
1375 let mut out = String::from("..");
1376 out.extend(s.chars().skip(skip));
1377 out
1378 }
1379 Trunc::MTrunc => {
1380 let keep = spec.width.saturating_sub(2);
1381 let left_half = keep / 2;
1382 let right_half = keep - left_half;
1383 let mut out: String = s.chars().take(left_half).collect();
1384 out.push_str("..");
1385 out.extend(s.chars().skip(char_len - right_half));
1386 out
1387 }
1388 }
1389 } else {
1390 let pad = spec.width - char_len;
1391 match spec.align {
1392 Align::Left => {
1393 let mut out = s.to_owned();
1394 for _ in 0..pad {
1395 out.push(' ');
1396 }
1397 out
1398 }
1399 Align::Right => {
1400 let mut out = String::new();
1401 for _ in 0..pad {
1402 out.push(' ');
1403 }
1404 out.push_str(s);
1405 out
1406 }
1407 Align::Center => {
1408 let left = pad / 2;
1409 let right = pad - left;
1410 let mut out = String::new();
1411 for _ in 0..left {
1412 out.push(' ');
1413 }
1414 out.push_str(s);
1415 for _ in 0..right {
1416 out.push(' ');
1417 }
1418 out
1419 }
1420 }
1421 }
1422 }
1423 fn parse_col_spec(
1424 chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
1425 align: Align,
1426 ) -> Option<ColSpec> {
1427 if chars.peek() != Some(&'(') {
1429 return None;
1430 }
1431 chars.next();
1432 let mut num_str = String::new();
1433 while let Some(&c) = chars.peek() {
1434 if c.is_ascii_digit() {
1435 num_str.push(c);
1436 chars.next();
1437 } else {
1438 break;
1439 }
1440 }
1441 let width: usize = num_str.parse().ok()?;
1442 let trunc = if chars.peek() == Some(&',') {
1443 chars.next(); let mut mode = String::new();
1445 while let Some(&c) = chars.peek() {
1446 if c == ')' {
1447 break;
1448 }
1449 mode.push(c);
1450 chars.next();
1451 }
1452 match mode.as_str() {
1453 "trunc" => Trunc::Trunc,
1454 "ltrunc" => Trunc::LTrunc,
1455 "mtrunc" => Trunc::MTrunc,
1456 _ => Trunc::None,
1457 }
1458 } else {
1459 Trunc::None
1460 };
1461 if chars.peek() == Some(&')') {
1463 chars.next();
1464 }
1465 Some(ColSpec {
1466 width,
1467 align,
1468 trunc,
1469 })
1470 }
1471
1472 let mut pending_col: Option<ColSpec> = None;
1473 let mut rendered = String::new();
1474 let mut chars = raw_fmt.chars().peekable();
1475 while let Some(ch) = chars.next() {
1476 if ch != '%' {
1477 rendered.push(ch);
1478 continue;
1479 }
1480 if chars.peek() == Some(&'<') {
1482 chars.next();
1483 if let Some(spec) = parse_col_spec(&mut chars, Align::Left) {
1484 pending_col = Some(spec);
1485 }
1486 continue;
1487 }
1488 if chars.peek() == Some(&'>') {
1489 chars.next();
1490 if chars.peek() == Some(&'<') {
1491 chars.next(); if let Some(spec) = parse_col_spec(&mut chars, Align::Center) {
1493 pending_col = Some(spec);
1494 }
1495 } else if chars.peek() == Some(&'>') {
1496 chars.next(); if let Some(spec) = parse_col_spec(&mut chars, Align::Right) {
1498 pending_col = Some(spec);
1499 }
1500 } else if let Some(spec) = parse_col_spec(&mut chars, Align::Right) {
1501 pending_col = Some(spec);
1502 }
1503 continue;
1504 }
1505
1506 let mut expanded = String::new();
1508 let target = if pending_col.is_some() {
1509 &mut expanded
1510 } else {
1511 &mut rendered
1512 };
1513 match chars.peek() {
1514 Some('%') => {
1515 chars.next();
1516 target.push('%');
1517 }
1518 Some('H') => {
1519 chars.next();
1520 target.push_str(&oid.to_hex());
1521 }
1522 Some('h') => {
1523 chars.next();
1524 let hex = oid.to_hex();
1525 let n = abbrev_len.clamp(4, 40).min(hex.len());
1526 target.push_str(&hex[..n]);
1527 }
1528 Some('T') => {
1529 chars.next();
1530 target.push_str(&tree_hex);
1531 }
1532 Some('t') => {
1533 chars.next();
1534 let n = abbrev_len.clamp(4, 40).min(tree_hex.len());
1535 target.push_str(&tree_hex[..n]);
1536 }
1537 Some('P') => {
1538 chars.next();
1539 target.push_str(&parent_hexes.join(" "));
1540 }
1541 Some('p') => {
1542 chars.next();
1543 target.push_str(&parent_abbrevs.join(" "));
1544 }
1545 Some('n') => {
1546 chars.next();
1547 target.push('\n');
1548 }
1549 Some('s') => {
1550 chars.next();
1551 target.push_str(subject);
1552 }
1553 Some('b') => {
1554 chars.next();
1555 target.push_str(&body);
1556 if !body.is_empty() {
1557 target.push('\n');
1558 }
1559 }
1560 Some('B') => {
1561 chars.next();
1562 target.push_str(&commit.message);
1563 }
1564 Some('a') => {
1565 chars.next();
1566 match chars.next() {
1567 Some('n') => target.push_str(extract_name(&commit.author)),
1568 Some('N') => target.push_str(extract_name(&commit.author)),
1569 Some('e') => target.push_str(extract_email(&commit.author)),
1570 Some('E') => target.push_str(extract_email(&commit.author)),
1571 Some('l') => target.push_str(extract_email_local(&commit.author)),
1572 Some('d') => target.push_str(&extract_date_default(&commit.author)),
1573 Some('D') => target.push_str(&extract_date_rfc2822(&commit.author)),
1574 Some('t') => target.push_str(&extract_timestamp(&commit.author)),
1575 Some('s') => target.push_str(&extract_date_short(&commit.author)),
1576 Some('i') => target.push_str(&extract_date_iso(&commit.author)),
1577 Some('I') => {
1578 let Some(p) = parse_signature_times(&commit.author) else {
1579 break;
1580 };
1581 let adjusted = p.unix_seconds + p.tz_offset_secs;
1582 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
1583 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
1584 let sign_ch = if p.tz_offset_secs >= 0 { '+' } else { '-' };
1585 let abs_off = p.tz_offset_secs.unsigned_abs();
1586 target.push_str(&format!(
1587 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}{:02}:{:02}",
1588 dt.year(),
1589 dt.month() as u8,
1590 dt.day(),
1591 dt.hour(),
1592 dt.minute(),
1593 dt.second(),
1594 sign_ch,
1595 abs_off / 3600,
1596 (abs_off % 3600) / 60
1597 ));
1598 }
1599 Some('r') => {
1600 let Some(p) = parse_signature_times(&commit.author) else {
1601 break;
1602 };
1603 let now = std::time::SystemTime::now()
1604 .duration_since(std::time::UNIX_EPOCH)
1605 .unwrap_or_default()
1606 .as_secs() as i64;
1607 target.push_str(&format_relative_date(now - p.unix_seconds));
1608 }
1609 Some(other) => {
1610 target.push('%');
1611 target.push('a');
1612 target.push(other);
1613 }
1614 None => {
1615 target.push('%');
1616 target.push('a');
1617 }
1618 }
1619 }
1620 Some('c') => {
1621 chars.next();
1622 match chars.next() {
1623 Some('n') => target.push_str(extract_name(&commit.committer)),
1624 Some('N') => target.push_str(extract_name(&commit.committer)),
1625 Some('e') => target.push_str(extract_email(&commit.committer)),
1626 Some('E') => target.push_str(extract_email(&commit.committer)),
1627 Some('l') => target.push_str(extract_email_local(&commit.committer)),
1628 Some('d') => target.push_str(&extract_date_default(&commit.committer)),
1629 Some('D') => target.push_str(&extract_date_rfc2822(&commit.committer)),
1630 Some('t') => target.push_str(&extract_timestamp(&commit.committer)),
1631 Some('s') => target.push_str(&extract_date_short(&commit.committer)),
1632 Some('i') => target.push_str(&extract_date_iso(&commit.committer)),
1633 Some('I') => {
1634 let Some(p) = parse_signature_times(&commit.committer) else {
1635 break;
1636 };
1637 let adjusted = p.unix_seconds + p.tz_offset_secs;
1638 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
1639 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
1640 let sign_ch = if p.tz_offset_secs >= 0 { '+' } else { '-' };
1641 let abs_off = p.tz_offset_secs.unsigned_abs();
1642 target.push_str(&format!(
1643 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}{:02}:{:02}",
1644 dt.year(),
1645 dt.month() as u8,
1646 dt.day(),
1647 dt.hour(),
1648 dt.minute(),
1649 dt.second(),
1650 sign_ch,
1651 abs_off / 3600,
1652 (abs_off % 3600) / 60
1653 ));
1654 }
1655 Some('r') => {
1656 let Some(p) = parse_signature_times(&commit.committer) else {
1657 break;
1658 };
1659 let now = std::time::SystemTime::now()
1660 .duration_since(std::time::UNIX_EPOCH)
1661 .unwrap_or_default()
1662 .as_secs() as i64;
1663 target.push_str(&format_relative_date(now - p.unix_seconds));
1664 }
1665 Some(other) => {
1666 target.push('%');
1667 target.push('c');
1668 target.push(other);
1669 }
1670 None => {
1671 target.push('%');
1672 target.push('c');
1673 }
1674 }
1675 }
1676 Some('x') => {
1677 chars.next();
1679 let mut hex_str = String::new();
1680 if let Some(&c1) = chars.peek() {
1681 if c1.is_ascii_hexdigit() {
1682 hex_str.push(c1);
1683 chars.next();
1684 }
1685 }
1686 if let Some(&c2) = chars.peek() {
1687 if c2.is_ascii_hexdigit() {
1688 hex_str.push(c2);
1689 chars.next();
1690 }
1691 }
1692 if let Ok(byte) = u8::from_str_radix(&hex_str, 16) {
1693 target.push(byte as char);
1694 }
1695 }
1696 Some('C') => {
1697 chars.next();
1698 if chars.peek() == Some(&'(') {
1699 chars.next();
1700 let mut spec = String::new();
1701 for c in chars.by_ref() {
1702 if c == ')' {
1703 break;
1704 }
1705 spec.push(c);
1706 }
1707 let (force, color_spec) =
1708 if let Some(rest) = spec.strip_prefix("always,") {
1709 (true, rest)
1710 } else if let Some(rest) = spec.strip_prefix("auto,") {
1711 (false, rest)
1712 } else if spec == "auto" {
1713 if use_color {
1714 target.push_str("\x1b[m");
1715 }
1716 continue;
1717 } else {
1718 (false, spec.as_str())
1719 };
1720 if use_color || force {
1721 target.push_str(&ansi_color_from_spec(color_spec));
1722 }
1723 } else {
1724 let remaining: String = chars.clone().collect();
1727 let known = [
1728 "reset", "red", "green", "blue", "yellow", "magenta", "cyan",
1729 "white", "bold", "dim", "ul",
1730 ];
1731 let mut matched = false;
1732 for name in &known {
1733 if remaining.starts_with(name) {
1734 for _ in 0..name.len() {
1735 chars.next();
1736 }
1737 if use_color {
1738 target.push_str(&ansi_color_from_name(name));
1739 }
1740 matched = true;
1741 break;
1742 }
1743 }
1744 if !matched {
1745 while let Some(&c) = chars.peek() {
1747 if c.is_alphanumeric() {
1748 chars.next();
1749 } else {
1750 break;
1751 }
1752 }
1753 }
1754 }
1755 }
1756 Some('w') => {
1757 chars.next();
1759 if chars.peek() == Some(&'(') {
1760 chars.next();
1761 for c in chars.by_ref() {
1762 if c == ')' {
1763 break;
1764 }
1765 }
1766 }
1767 }
1768 Some('+') => {
1769 chars.next();
1771 if chars.peek() == Some(&'%') {
1773 }
1777 let mut sub = String::new();
1779 if let Some(&nc) = chars.peek() {
1780 match nc {
1781 'b' => {
1782 chars.next();
1783 sub.push_str(&body);
1784 if !body.is_empty() {
1785 sub.push('\n');
1786 }
1787 }
1788 's' => {
1789 chars.next();
1790 sub.push_str(subject);
1791 }
1792 _ => {
1793 chars.next();
1794 sub.push('%');
1795 sub.push('+');
1796 sub.push(nc);
1797 }
1798 }
1799 }
1800 if !sub.is_empty() {
1801 target.push('\n');
1802 target.push_str(&sub);
1803 }
1804 }
1805 Some('-') => {
1806 chars.next();
1808 if let Some(&nc) = chars.peek() {
1810 match nc {
1811 'b' => {
1812 chars.next();
1813 if !body.is_empty() {
1814 target.push_str(&body);
1815 target.push('\n');
1816 }
1817 }
1818 's' => {
1819 chars.next();
1820 target.push_str(subject);
1821 }
1822 _ => {
1823 chars.next();
1824 target.push('%');
1825 target.push('-');
1826 target.push(nc);
1827 }
1828 }
1829 }
1830 }
1831 Some('d') => {
1832 chars.next();
1834 }
1835 Some('D') => {
1836 chars.next();
1838 }
1839 Some('e') => {
1840 chars.next();
1842 }
1843 Some('g') => {
1844 chars.next();
1846 if let Some(&_nc) = chars.peek() {
1847 chars.next(); }
1850 }
1851 Some(&other) => {
1852 chars.next();
1853 target.push('%');
1854 target.push(other);
1855 }
1856 None => target.push('%'),
1857 }
1858 if let Some(spec) = pending_col.take() {
1860 let formatted = apply_col(&spec, &expanded);
1861 rendered.push_str(&formatted);
1862 }
1863 }
1864 Ok(rendered)
1865 }
1866 }
1867}
1868
1869#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1870enum ExpectedObjectKind {
1871 Commit,
1872 Tree,
1873 Blob,
1874}
1875
1876impl ExpectedObjectKind {
1877 fn from_tag_type(kind: &str) -> Option<Self> {
1878 match kind {
1879 "commit" => Some(Self::Commit),
1880 "tree" => Some(Self::Tree),
1881 "blob" => Some(Self::Blob),
1882 _ => None,
1883 }
1884 }
1885
1886 fn matches(self, kind: ObjectKind) -> bool {
1887 matches!(
1888 (self, kind),
1889 (Self::Commit, ObjectKind::Commit)
1890 | (Self::Tree, ObjectKind::Tree)
1891 | (Self::Blob, ObjectKind::Blob)
1892 )
1893 }
1894
1895 fn as_str(self) -> &'static str {
1896 match self {
1897 Self::Commit => "commit",
1898 Self::Tree => "tree",
1899 Self::Blob => "blob",
1900 }
1901 }
1902}
1903
1904#[derive(Clone, Debug)]
1905struct RootObject {
1906 oid: ObjectId,
1907 input: String,
1908 expected_kind: Option<ExpectedObjectKind>,
1909 root_path: Option<String>,
1911}
1912
1913fn filter_shows_commit_line_when_not_user_given(filter: Option<&ObjectFilter>) -> bool {
1915 match filter {
1916 None => true,
1917 Some(ObjectFilter::BlobNone)
1918 | Some(ObjectFilter::BlobLimit(_))
1919 | Some(ObjectFilter::TreeDepth(_))
1920 | Some(ObjectFilter::SparseOid(_)) => true,
1921 Some(ObjectFilter::ObjectType(FilterObjectKind::Commit)) => true,
1922 Some(ObjectFilter::ObjectType(
1923 FilterObjectKind::Blob | FilterObjectKind::Tree | FilterObjectKind::Tag,
1924 )) => false,
1925 Some(ObjectFilter::Combine(parts)) => parts
1926 .iter()
1927 .all(|p| filter_shows_commit_line_when_not_user_given(Some(p))),
1928 }
1929}
1930
1931fn skip_tree_descent_for_object_type_filter(filter: Option<&ObjectFilter>) -> bool {
1932 match filter {
1933 Some(ObjectFilter::ObjectType(FilterObjectKind::Commit | FilterObjectKind::Tag)) => true,
1934 Some(ObjectFilter::Combine(parts)) => parts
1935 .iter()
1936 .any(|p| skip_tree_descent_for_object_type_filter(Some(p))),
1937 _ => false,
1938 }
1939}
1940
1941fn sparse_oid_lines_from_filter(
1942 repo: &Repository,
1943 filter: Option<&ObjectFilter>,
1944) -> Option<Vec<String>> {
1945 let f = filter?;
1946 match f {
1947 ObjectFilter::SparseOid(oid) => {
1948 let obj = repo.odb.read(oid).ok()?;
1949 if obj.kind != ObjectKind::Blob {
1950 return None;
1951 }
1952 let text = std::str::from_utf8(&obj.data).ok()?;
1953 Some(parse_sparse_patterns_from_blob(text))
1954 }
1955 ObjectFilter::Combine(parts) => {
1956 for p in parts {
1957 if let Some(lines) = sparse_oid_lines_from_filter(repo, Some(p)) {
1958 return Some(lines);
1959 }
1960 }
1961 None
1962 }
1963 _ => None,
1964 }
1965}
1966
1967fn packed_object_set(repo: &Repository) -> HashSet<ObjectId> {
1968 let mut out = HashSet::new();
1969 let objects_dir = repo.odb.objects_dir();
1970 if let Ok(indexes) = pack::read_local_pack_indexes(objects_dir) {
1971 for idx in indexes {
1972 for e in idx.entries {
1973 out.insert(e.oid);
1974 }
1975 }
1976 }
1977 out
1978}
1979
1980fn resolve_specs(repo: &Repository, specs: &[String]) -> Result<Vec<ObjectId>> {
1981 let mut out = Vec::with_capacity(specs.len());
1982 for spec in specs {
1983 let oid = resolve_revision_for_range_end(repo, spec)?;
1984 let commit_oid = peel_to_commit(repo, oid)?;
1985 out.push(commit_oid);
1986 }
1987 Ok(out)
1988}
1989
1990fn resolve_specs_for_objects(
1991 repo: &Repository,
1992 specs: &[String],
1993) -> Result<(Vec<ObjectId>, Vec<RootObject>)> {
1994 let mut commits = Vec::new();
1995 let mut roots = Vec::new();
1996
1997 for spec in specs {
1998 if let Ok(raw_oid) = spec.parse::<ObjectId>() {
1999 let raw_object = repo.odb.read(&raw_oid)?;
2000 match raw_object.kind {
2001 ObjectKind::Commit => {
2002 commits.push(raw_oid);
2003 }
2004 ObjectKind::Tag => {
2005 let tag = parse_tag(&raw_object.data)?;
2006 let expected_kind = ExpectedObjectKind::from_tag_type(&tag.object_type)
2007 .ok_or_else(|| {
2008 Error::CorruptObject(format!(
2009 "object {spec} has unsupported tag type '{}'",
2010 tag.object_type
2011 ))
2012 })?;
2013 roots.push(RootObject {
2014 oid: tag.object,
2015 input: spec.clone(),
2016 expected_kind: Some(expected_kind),
2017 root_path: None,
2018 });
2019 }
2020 ObjectKind::Tree | ObjectKind::Blob => roots.push(RootObject {
2021 oid: raw_oid,
2022 input: spec.clone(),
2023 expected_kind: None,
2024 root_path: None,
2025 }),
2026 }
2027 continue;
2028 }
2029
2030 if let Some((treeish, path)) = split_treeish_spec(spec) {
2031 if !path.is_empty() {
2032 let treeish_oid = resolve_revision_for_range_end(repo, treeish)?;
2033 let blob_oid = resolve_treeish_path(repo, treeish_oid, path)?;
2034 roots.push(RootObject {
2035 oid: blob_oid,
2036 input: spec.clone(),
2037 expected_kind: Some(ExpectedObjectKind::Blob),
2038 root_path: Some(path.to_owned()),
2039 });
2040 continue;
2041 }
2042 }
2043
2044 let oid = resolve_revision_for_range_end(repo, spec)?;
2045 match peel_to_commit(repo, oid) {
2046 Ok(commit_oid) => commits.push(commit_oid),
2047 Err(Error::CorruptObject(_)) | Err(Error::ObjectNotFound(_)) => {
2048 roots.push(RootObject {
2049 oid,
2050 input: spec.clone(),
2051 expected_kind: None,
2052 root_path: None,
2053 })
2054 }
2055 Err(err) => return Err(err),
2056 }
2057 }
2058
2059 Ok((commits, roots))
2060}
2061
2062fn peel_to_commit(repo: &Repository, mut oid: ObjectId) -> Result<ObjectId> {
2064 loop {
2065 let object = repo.odb.read(&oid)?;
2066 match object.kind {
2067 ObjectKind::Commit => return Ok(oid),
2068 ObjectKind::Tag => {
2069 let tag = parse_tag(&object.data)?;
2070 oid = tag.object;
2071 }
2072 other => {
2073 return Err(Error::CorruptObject(format!(
2074 "object {oid} is a {other:?}, not a commit"
2075 )));
2076 }
2077 }
2078 }
2079}
2080
2081fn reflog_commit_tips(repo: &Repository) -> Result<Vec<ObjectId>> {
2082 let z = zero_oid();
2083 let mut out = Vec::new();
2084 let mut seen = HashSet::new();
2085 for refname in list_reflog_refs(&repo.git_dir)? {
2086 let entries = read_reflog(&repo.git_dir, &refname)?;
2087 for e in entries {
2088 for oid in [e.old_oid, e.new_oid] {
2089 if oid == z {
2090 continue;
2091 }
2092 match peel_to_commit(repo, oid) {
2093 Ok(c) if seen.insert(c) => out.push(c),
2094 Err(_) => {}
2095 _ => {}
2096 }
2097 }
2098 }
2099 }
2100 Ok(out)
2101}
2102
2103fn all_ref_tips(repo: &Repository) -> Result<Vec<ObjectId>> {
2104 let mut raw = Vec::new();
2105 if let Ok(head) = refs::resolve_ref(&repo.git_dir, "HEAD") {
2106 raw.push(head);
2107 }
2108 raw.extend(
2109 refs::list_refs(&repo.git_dir, "refs/")?
2110 .into_iter()
2111 .map(|(_, oid)| oid),
2112 );
2113 let mut out = Vec::new();
2115 let mut seen = HashSet::new();
2116 for oid in raw {
2117 match peel_to_commit(repo, oid) {
2118 Ok(commit_oid) if seen.insert(commit_oid) => out.push(commit_oid),
2119 Err(_) => {} _ => {}
2121 }
2122 }
2123 out.sort();
2124 Ok(out)
2125}
2126
2127fn walk_closure(graph: &mut CommitGraph<'_>, starts: &[ObjectId]) -> Result<HashSet<ObjectId>> {
2128 let (seen, _) = walk_closure_ordered(graph, starts)?;
2129 Ok(seen)
2130}
2131
2132fn walk_closure_ordered(
2134 graph: &mut CommitGraph<'_>,
2135 starts: &[ObjectId],
2136) -> Result<(HashSet<ObjectId>, Vec<ObjectId>)> {
2137 let mut seen = HashSet::new();
2138 let mut order = Vec::new();
2139 let mut queue = VecDeque::new();
2140 for &start in starts {
2141 queue.push_back(start);
2142 }
2143 while let Some(oid) = queue.pop_front() {
2144 if !seen.insert(oid) {
2145 continue;
2146 }
2147 order.push(oid);
2148 for parent in graph.parents_of(oid)? {
2149 queue.push_back(parent);
2150 }
2151 }
2152 Ok((seen, order))
2153}
2154
2155fn date_order_walk(
2162 graph: &mut CommitGraph<'_>,
2163 tips: &[ObjectId],
2164 selected: &HashSet<ObjectId>,
2165) -> Result<Vec<ObjectId>> {
2166 let mut unfinished_children: HashMap<ObjectId, usize> =
2167 selected.iter().map(|&oid| (oid, 0usize)).collect();
2168 for &child in selected {
2169 for parent in graph.parents_of(child)? {
2170 if selected.contains(&parent) {
2171 if let Some(count) = unfinished_children.get_mut(&parent) {
2172 *count += 1;
2173 }
2174 }
2175 }
2176 }
2177
2178 let mut heap = BinaryHeap::new();
2179 for &tip in tips {
2180 if selected.contains(&tip) {
2181 heap.push(CommitDateKey {
2182 oid: tip,
2183 date: graph.committer_time(tip),
2184 });
2185 }
2186 }
2187
2188 let mut emitted = HashSet::new();
2189 let mut out = Vec::with_capacity(selected.len());
2190 while let Some(item) = heap.pop() {
2191 if !emitted.insert(item.oid) {
2192 continue;
2193 }
2194 out.push(item.oid);
2195 for parent in graph.parents_of(item.oid)? {
2196 if !selected.contains(&parent) {
2197 continue;
2198 }
2199 let Some(count) = unfinished_children.get_mut(&parent) else {
2200 continue;
2201 };
2202 *count = count.saturating_sub(1);
2203 if *count == 0 {
2204 heap.push(CommitDateKey {
2205 oid: parent,
2206 date: graph.committer_time(parent),
2207 });
2208 }
2209 }
2210 }
2211
2212 Ok(out)
2213}
2214
2215fn topo_sort(graph: &mut CommitGraph<'_>, selected: &HashSet<ObjectId>) -> Result<Vec<ObjectId>> {
2216 let mut child_count: HashMap<ObjectId, usize> = selected.iter().map(|&oid| (oid, 0)).collect();
2217
2218 for &oid in selected {
2219 for parent in graph.parents_of(oid)? {
2220 if !selected.contains(&parent) {
2221 continue;
2222 }
2223 if let Some(count) = child_count.get_mut(&parent) {
2224 *count += 1;
2225 }
2226 }
2227 }
2228
2229 let mut ready: BinaryHeap<Reverse<CommitDateKey>> = BinaryHeap::new();
2233 for (&oid, &count) in &child_count {
2234 if count == 0 {
2235 ready.push(Reverse(CommitDateKey {
2236 oid,
2237 date: graph.committer_time(oid),
2238 }));
2239 }
2240 }
2241
2242 let mut out = Vec::with_capacity(selected.len());
2243 while let Some(Reverse(item)) = ready.pop() {
2244 let oid = item.oid;
2245 out.push(oid);
2246 for parent in graph.parents_of(oid)? {
2247 if !selected.contains(&parent) {
2248 continue;
2249 }
2250 if let Some(count) = child_count.get_mut(&parent) {
2251 *count = count.saturating_sub(1);
2252 if *count == 0 {
2253 ready.push(Reverse(CommitDateKey {
2254 oid: parent,
2255 date: graph.committer_time(parent),
2256 }));
2257 }
2258 }
2259 }
2260 }
2261
2262 Ok(out)
2263}
2264
2265fn limit_to_ancestry(
2266 graph: &mut CommitGraph<'_>,
2267 selected: &mut HashSet<ObjectId>,
2268 bottoms: &[ObjectId],
2269) -> Result<()> {
2270 let mut keep = HashSet::new();
2271 for &bottom in bottoms {
2272 let ancestors = walk_closure(graph, &[bottom])?;
2273 keep.extend(
2274 ancestors
2275 .iter()
2276 .copied()
2277 .filter(|oid| selected.contains(oid)),
2278 );
2279
2280 for &candidate in selected.iter() {
2281 if candidate == bottom {
2282 keep.insert(candidate);
2283 continue;
2284 }
2285 let closure = walk_closure(graph, &[candidate])?;
2286 if closure.contains(&bottom) {
2287 keep.insert(candidate);
2288 }
2289 }
2290 }
2291 selected.retain(|oid| keep.contains(oid));
2292 Ok(())
2293}
2294
2295fn commit_touches_paths(
2297 repo: &Repository,
2298 graph: &mut CommitGraph<'_>,
2299 oid: ObjectId,
2300 paths: &[String],
2301 full_history: bool,
2302 sparse: bool,
2303) -> Result<bool> {
2304 let commit = load_commit(repo, oid)?;
2305 let parents = graph.parents_of(oid)?;
2306 let commit_entries = flatten_tree(repo, commit.tree, "")?;
2307 let commit_map: HashMap<String, ObjectId> = commit_entries.into_iter().collect();
2308
2309 if parents.is_empty() {
2311 if sparse {
2312 return Ok(true);
2313 }
2314 return Ok(commit_map.keys().any(|path| {
2315 paths.iter().any(|spec| {
2316 crate::pathspec::matches_pathspec_with_context(
2317 spec,
2318 path,
2319 crate::pathspec::PathspecMatchContext {
2320 is_directory: false,
2321 is_git_submodule: false,
2322 },
2323 )
2324 })
2325 }));
2326 }
2327
2328 if parents.len() == 1 {
2330 let parent = load_commit(repo, parents[0])?;
2331 let parent_map: HashMap<String, ObjectId> =
2332 flatten_tree(repo, parent.tree, "")?.into_iter().collect();
2333 let differs = path_differs_for_specs(&commit_map, &parent_map, paths);
2334 if differs {
2335 return Ok(true);
2336 }
2337 if sparse {
2338 return Ok(true);
2339 }
2340 return Ok(false);
2341 }
2342
2343 let mut treesame_parents = 0usize;
2347 let mut differs_any = false;
2348 for parent_oid in &parents {
2349 let parent = load_commit(repo, *parent_oid)?;
2350 let parent_map: HashMap<String, ObjectId> =
2351 flatten_tree(repo, parent.tree, "")?.into_iter().collect();
2352 let differs = path_differs_for_specs(&commit_map, &parent_map, paths);
2353 if differs {
2354 differs_any = true;
2355 } else {
2356 treesame_parents += 1;
2357 }
2358 }
2359
2360 if !full_history && treesame_parents == 1 {
2361 return Ok(false);
2362 }
2363
2364 if differs_any {
2365 return Ok(true);
2366 }
2367
2368 Ok(sparse)
2369}
2370
2371fn path_differs_for_specs(
2372 current: &HashMap<String, ObjectId>,
2373 parent: &HashMap<String, ObjectId>,
2374 specs: &[String],
2375) -> bool {
2376 let mut paths = std::collections::BTreeSet::new();
2377 paths.extend(current.keys().cloned());
2378 paths.extend(parent.keys().cloned());
2379
2380 for path in &paths {
2381 if !specs
2382 .iter()
2383 .any(|spec| crate::pathspec::matches_pathspec(spec, path))
2384 {
2385 continue;
2386 }
2387 if current.get(path) != parent.get(path) {
2388 return true;
2389 }
2390 }
2391 false
2392}
2393
2394fn load_commit(repo: &Repository, oid: ObjectId) -> Result<crate::objects::CommitData> {
2395 let object = repo.odb.read(&oid)?;
2396 if object.kind != ObjectKind::Commit {
2397 return Err(Error::CorruptObject(format!(
2398 "object {oid} is not a commit"
2399 )));
2400 }
2401 parse_commit(&object.data)
2402}
2403
2404pub fn collect_revision_specs_with_stdin(
2412 args_specs: &[String],
2413 read_stdin: bool,
2414) -> Result<(Vec<String>, Vec<String>, bool)> {
2415 let mut positive = Vec::new();
2416 let mut negative = Vec::new();
2417 let mut not_mode = false;
2418
2419 for spec in args_specs {
2420 let (pos, neg) = split_revision_token(spec);
2421 if not_mode {
2422 positive.extend(neg);
2423 negative.extend(pos);
2424 } else {
2425 positive.extend(pos);
2426 negative.extend(neg);
2427 }
2428 }
2429
2430 if !read_stdin {
2431 return Ok((positive, negative, false));
2432 }
2433
2434 let mut in_paths = false;
2435 let mut stdin_all_refs = false;
2436 let stdin = std::io::read_to_string(std::io::stdin()).map_err(Error::Io)?;
2437 for raw_line in stdin.lines() {
2438 let line = raw_line.trim();
2439 if line.is_empty() {
2440 continue;
2441 }
2442 if in_paths {
2443 continue;
2444 }
2445 if line == "--" {
2446 in_paths = true;
2447 continue;
2448 }
2449 if line == "--not" {
2450 not_mode = !not_mode;
2451 continue;
2452 }
2453 if line == "--all" {
2454 stdin_all_refs = true;
2455 continue;
2456 }
2457 if line.starts_with("--") {
2458 return Err(Error::InvalidRef(format!(
2459 "invalid option '{line}' in --stdin mode"
2460 )));
2461 }
2462 if line.starts_with('-') {
2463 return Err(Error::InvalidRef(format!(
2464 "invalid option '{line}' in --stdin mode"
2465 )));
2466 }
2467 let (pos, neg) = split_revision_token(line);
2468 if not_mode {
2469 positive.extend(neg);
2470 negative.extend(pos);
2471 } else {
2472 positive.extend(pos);
2473 negative.extend(neg);
2474 }
2475 }
2476
2477 Ok((positive, negative, stdin_all_refs))
2478}
2479
2480pub fn tag_targets(git_dir: &Path) -> Result<HashSet<ObjectId>> {
2482 Ok(refs::list_refs(git_dir, "refs/tags/")?
2483 .into_iter()
2484 .map(|(_, oid)| oid)
2485 .collect())
2486}
2487
2488struct CommitGraph<'r> {
2489 repo: &'r Repository,
2490 first_parent_only: bool,
2491 parents: HashMap<ObjectId, Vec<ObjectId>>,
2492 committer_time: HashMap<ObjectId, i64>,
2493 shallow_boundaries: HashSet<ObjectId>,
2494 graft_parents: HashMap<ObjectId, Vec<ObjectId>>,
2495}
2496
2497impl<'r> CommitGraph<'r> {
2498 fn new(repo: &'r Repository, first_parent_only: bool) -> Self {
2499 let shallow_boundaries = load_shallow_boundaries(&repo.git_dir);
2500 let graft_parents = load_graft_parents(&repo.git_dir);
2501 Self {
2502 repo,
2503 first_parent_only,
2504 parents: HashMap::new(),
2505 committer_time: HashMap::new(),
2506 shallow_boundaries,
2507 graft_parents,
2508 }
2509 }
2510
2511 fn parents_of(&mut self, oid: ObjectId) -> Result<Vec<ObjectId>> {
2512 self.populate(oid)?;
2513 Ok(self.parents.get(&oid).cloned().unwrap_or_default())
2514 }
2515
2516 fn committer_time(&mut self, oid: ObjectId) -> i64 {
2517 if self.populate(oid).is_err() {
2518 return 0;
2519 }
2520 self.committer_time.get(&oid).copied().unwrap_or(0)
2521 }
2522
2523 fn populate(&mut self, oid: ObjectId) -> Result<()> {
2524 if self.parents.contains_key(&oid) {
2525 return Ok(());
2526 }
2527 let commit = load_commit(self.repo, oid)?;
2528 let mut parents = if self.shallow_boundaries.contains(&oid) {
2530 Vec::new()
2531 } else {
2532 commit.parents
2533 };
2534 if let Some(graft_parents) = self.graft_parents.get(&oid) {
2535 parents = graft_parents.clone();
2536 }
2537 if self.first_parent_only && parents.len() > 1 {
2538 parents.truncate(1);
2539 }
2540 self.committer_time
2541 .insert(oid, committer_unix_seconds_for_ordering(&commit.committer));
2542 self.parents.insert(oid, parents);
2543 Ok(())
2544 }
2545}
2546
2547fn load_shallow_boundaries(git_dir: &Path) -> HashSet<ObjectId> {
2549 let shallow_path = git_dir.join("shallow");
2550 let mut set = HashSet::new();
2551 if let Ok(contents) = fs::read_to_string(&shallow_path) {
2552 for line in contents.lines() {
2553 let line = line.trim();
2554 if !line.is_empty() {
2555 if let Ok(oid) = line.parse::<ObjectId>() {
2556 set.insert(oid);
2557 }
2558 }
2559 }
2560 }
2561 set
2562}
2563
2564fn load_graft_parents(git_dir: &Path) -> HashMap<ObjectId, Vec<ObjectId>> {
2566 let graft_path = git_dir.join("info/grafts");
2567 let mut grafts = HashMap::new();
2568 let Ok(contents) = fs::read_to_string(&graft_path) else {
2569 return grafts;
2570 };
2571 for raw_line in contents.lines() {
2572 let line = raw_line.trim();
2573 if line.is_empty() || line.starts_with('#') {
2574 continue;
2575 }
2576 let mut fields = line.split_whitespace();
2577 let Some(commit_hex) = fields.next() else {
2578 continue;
2579 };
2580 let Ok(commit_oid) = commit_hex.parse::<ObjectId>() else {
2581 continue;
2582 };
2583 let mut parents = Vec::new();
2584 let mut valid = true;
2585 for parent_hex in fields {
2586 match parent_hex.parse::<ObjectId>() {
2587 Ok(parent_oid) => parents.push(parent_oid),
2588 Err(_) => {
2589 valid = false;
2590 break;
2591 }
2592 }
2593 }
2594 if valid {
2595 grafts.insert(commit_oid, parents);
2596 }
2597 }
2598 grafts
2599}
2600
2601#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2602struct CommitDateKey {
2603 oid: ObjectId,
2604 date: i64,
2605}
2606
2607impl Ord for CommitDateKey {
2608 fn cmp(&self, other: &Self) -> Ordering {
2609 match self.date.cmp(&other.date) {
2610 Ordering::Equal => self.oid.cmp(&other.oid),
2611 ord => ord,
2612 }
2613 }
2614}
2615
2616impl PartialOrd for CommitDateKey {
2617 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2618 Some(self.cmp(other))
2619 }
2620}
2621
2622pub fn read_lines(path: &Path) -> Result<Vec<String>> {
2628 let content = fs::read_to_string(path)?;
2629 Ok(content.lines().map(|line| line.to_owned()).collect())
2630}
2631
2632#[must_use]
2634pub fn is_symmetric_diff(token: &str) -> bool {
2635 token.contains("...") && !token.contains("....")
2636}
2637
2638#[must_use]
2640pub fn split_symmetric_diff(token: &str) -> Option<(String, String)> {
2641 token
2642 .split_once("...")
2643 .map(|(l, r)| (l.to_owned(), r.to_owned()))
2644}
2645
2646#[derive(Debug)]
2651enum TreeWalkState {
2652 Simple(HashSet<ObjectId>),
2653 TreeDepth(HashMap<ObjectId, u64>),
2654}
2655
2656impl TreeWalkState {
2657 fn new(filter: Option<&ObjectFilter>) -> Self {
2658 if filter_uses_tree_depth(filter) {
2659 Self::TreeDepth(HashMap::new())
2660 } else {
2661 Self::Simple(HashSet::new())
2662 }
2663 }
2664
2665 fn should_skip_tree(&mut self, oid: ObjectId, depth: u64) -> bool {
2667 match self {
2668 TreeWalkState::Simple(set) => !set.insert(oid),
2669 TreeWalkState::TreeDepth(map) => match map.get(&oid).copied() {
2670 None => {
2671 map.insert(oid, depth);
2672 false
2673 }
2674 Some(prev) if depth >= prev => true,
2675 Some(_) => {
2676 map.insert(oid, depth);
2677 false
2678 }
2679 },
2680 }
2681 }
2682}
2683
2684fn filter_uses_tree_depth(filter: Option<&ObjectFilter>) -> bool {
2685 match filter {
2686 Some(ObjectFilter::TreeDepth(_)) => true,
2687 Some(ObjectFilter::Combine(parts)) => parts.iter().any(|p| filter_uses_tree_depth(Some(p))),
2688 _ => false,
2689 }
2690}
2691
2692fn collect_tree_closure_objects(
2694 repo: &Repository,
2695 tree_oid: ObjectId,
2696 into: &mut HashSet<ObjectId>,
2697 missing_action: MissingAction,
2698 missing: &mut Vec<ObjectId>,
2699 missing_seen: &mut HashSet<ObjectId>,
2700) -> Result<()> {
2701 let mut stack = vec![tree_oid];
2702 let mut expanded_trees = HashSet::new();
2703 while let Some(t) = stack.pop() {
2704 if !expanded_trees.insert(t) {
2705 continue;
2706 }
2707 into.insert(t);
2708 let object = match repo.odb.read(&t) {
2709 Ok(o) => o,
2710 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
2711 if missing_action == MissingAction::Print && missing_seen.insert(t) {
2712 missing.push(t);
2713 }
2714 continue;
2715 }
2716 Err(e) => return Err(e),
2717 };
2718 if object.kind != ObjectKind::Tree {
2719 continue;
2720 }
2721 let entries = parse_tree(&object.data)?;
2722 for entry in entries {
2723 if entry.mode == 0o160000 {
2724 continue;
2725 }
2726 into.insert(entry.oid);
2727 if entry.mode == 0o040000 {
2728 stack.push(entry.oid);
2729 }
2730 }
2731 }
2732 Ok(())
2733}
2734
2735fn union_parent_reachable_objects(
2736 repo: &Repository,
2737 parents: &[ObjectId],
2738 missing_action: MissingAction,
2739 missing: &mut Vec<ObjectId>,
2740 missing_seen: &mut HashSet<ObjectId>,
2741) -> Result<HashSet<ObjectId>> {
2742 let mut out = HashSet::new();
2743 for &p in parents {
2744 let commit = match load_commit(repo, p) {
2745 Ok(c) => c,
2746 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
2747 if missing_action == MissingAction::Print && missing_seen.insert(p) {
2748 missing.push(p);
2749 }
2750 continue;
2751 }
2752 Err(e) => return Err(e),
2753 };
2754 collect_tree_closure_objects(
2755 repo,
2756 commit.tree,
2757 &mut out,
2758 missing_action,
2759 missing,
2760 missing_seen,
2761 )?;
2762 }
2763 Ok(out)
2764}
2765
2766#[allow(dead_code)]
2769fn collect_reachable_objects(
2770 repo: &Repository,
2771 graph: &mut CommitGraph<'_>,
2772 commits: &[ObjectId],
2773 object_roots: &[RootObject],
2774 filter: Option<&ObjectFilter>,
2775 filter_provided: bool,
2776 missing_action: MissingAction,
2777 sparse_lines: Option<&[String]>,
2778 skip_trees_for_type_filter: bool,
2779 omit_object_paths: bool,
2780 packed_set: Option<&HashSet<ObjectId>>,
2781) -> Result<(Vec<(ObjectId, String)>, Vec<ObjectId>, Vec<ObjectId>)> {
2782 let mut tree_state = TreeWalkState::new(filter);
2783 let mut emitted = HashSet::new();
2784 let mut result = Vec::new();
2785 let mut omitted = Vec::new();
2786 let mut missing = Vec::new();
2787 let mut missing_seen = HashSet::new();
2788 for &commit_oid in commits {
2789 let commit = match load_commit(repo, commit_oid) {
2790 Ok(commit) => commit,
2791 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
2792 if missing_seen.insert(commit_oid) && missing_action == MissingAction::Print {
2793 missing.push(commit_oid);
2794 }
2795 continue;
2796 }
2797 Err(err) => return Err(err),
2798 };
2799 let parents = graph.parents_of(commit_oid)?;
2800 let parent_union = union_parent_reachable_objects(
2801 repo,
2802 &parents,
2803 missing_action,
2804 &mut missing,
2805 &mut missing_seen,
2806 )?;
2807 collect_tree_objects_filtered(
2808 repo,
2809 commit.tree,
2810 "",
2811 0,
2812 false,
2813 Some(&parent_union),
2814 &mut tree_state,
2815 &mut emitted,
2816 &mut result,
2817 &mut omitted,
2818 &mut missing,
2819 &mut missing_seen,
2820 filter,
2821 filter_provided,
2822 missing_action,
2823 sparse_lines,
2824 skip_trees_for_type_filter,
2825 omit_object_paths,
2826 packed_set,
2827 )?;
2828 }
2829
2830 for root in object_roots {
2831 collect_root_object(
2832 repo,
2833 root,
2834 &mut tree_state,
2835 &mut emitted,
2836 &mut result,
2837 &mut omitted,
2838 &mut missing,
2839 &mut missing_seen,
2840 filter,
2841 filter_provided,
2842 missing_action,
2843 sparse_lines,
2844 skip_trees_for_type_filter,
2845 omit_object_paths,
2846 packed_set,
2847 )?;
2848 }
2849
2850 Ok((result, omitted, missing))
2851}
2852
2853fn collect_reachable_objects_segmented(
2859 repo: &Repository,
2860 graph: &mut CommitGraph<'_>,
2861 commits: &[ObjectId],
2862 object_roots: &[RootObject],
2863 filter: Option<&ObjectFilter>,
2864 filter_provided: bool,
2865 missing_action: MissingAction,
2866 sparse_lines: Option<&[String]>,
2867 skip_trees_for_type_filter: bool,
2868 omit_object_paths: bool,
2869 packed_set: Option<&HashSet<ObjectId>>,
2870) -> Result<(
2871 Vec<(ObjectId, String)>,
2872 Vec<ObjectId>,
2873 Vec<ObjectId>,
2874 Vec<Vec<(ObjectId, String)>>,
2875)> {
2876 let mut emitted = HashSet::new();
2877 let mut result = Vec::new();
2878 let mut omitted = Vec::new();
2879 let mut missing = Vec::new();
2880 let mut missing_seen = HashSet::new();
2881 let mut segments: Vec<Vec<(ObjectId, String)>> = Vec::with_capacity(commits.len() + 1);
2882
2883 for &commit_oid in commits {
2884 let start = result.len();
2885 let commit = match load_commit(repo, commit_oid) {
2886 Ok(commit) => commit,
2887 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
2888 if missing_action == MissingAction::Print && missing_seen.insert(commit_oid) {
2889 missing.push(commit_oid);
2890 }
2891 segments.push(Vec::new());
2892 continue;
2893 }
2894 Err(err) => return Err(err),
2895 };
2896 let mut tree_state = TreeWalkState::new(filter);
2897 let parents = graph.parents_of(commit_oid)?;
2898 let parent_union = union_parent_reachable_objects(
2899 repo,
2900 &parents,
2901 missing_action,
2902 &mut missing,
2903 &mut missing_seen,
2904 )?;
2905 collect_tree_objects_filtered(
2906 repo,
2907 commit.tree,
2908 "",
2909 0,
2910 false,
2911 Some(&parent_union),
2912 &mut tree_state,
2913 &mut emitted,
2914 &mut result,
2915 &mut omitted,
2916 &mut missing,
2917 &mut missing_seen,
2918 filter,
2919 filter_provided,
2920 missing_action,
2921 sparse_lines,
2922 skip_trees_for_type_filter,
2923 omit_object_paths,
2924 packed_set,
2925 )?;
2926 segments.push(result[start..].to_vec());
2927 }
2928
2929 let roots_start = result.len();
2930 let mut root_tree_state = TreeWalkState::new(filter);
2931 for root in object_roots {
2932 collect_root_object(
2933 repo,
2934 root,
2935 &mut root_tree_state,
2936 &mut emitted,
2937 &mut result,
2938 &mut omitted,
2939 &mut missing,
2940 &mut missing_seen,
2941 filter,
2942 filter_provided,
2943 missing_action,
2944 sparse_lines,
2945 skip_trees_for_type_filter,
2946 omit_object_paths,
2947 packed_set,
2948 )?;
2949 }
2950 segments.push(result[roots_start..].to_vec());
2951
2952 Ok((result, omitted, missing, segments))
2953}
2954
2955fn collect_root_object(
2956 repo: &Repository,
2957 root: &RootObject,
2958 tree_state: &mut TreeWalkState,
2959 emitted: &mut HashSet<ObjectId>,
2960 result: &mut Vec<(ObjectId, String)>,
2961 omitted: &mut Vec<ObjectId>,
2962 missing: &mut Vec<ObjectId>,
2963 missing_seen: &mut HashSet<ObjectId>,
2964 filter: Option<&ObjectFilter>,
2965 filter_provided: bool,
2966 missing_action: MissingAction,
2967 sparse_lines: Option<&[String]>,
2968 skip_trees_for_type_filter: bool,
2969 omit_object_paths: bool,
2970 packed_set: Option<&HashSet<ObjectId>>,
2971) -> Result<()> {
2972 let object = match repo.odb.read(&root.oid) {
2973 Ok(object) => object,
2974 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
2975 if missing_action == MissingAction::Print && missing_seen.insert(root.oid) {
2976 missing.push(root.oid);
2977 }
2978 return Ok(());
2979 }
2980 Err(err) => return Err(err),
2981 };
2982
2983 if let Some(expected) = root.expected_kind {
2984 if !expected.matches(object.kind) {
2985 return Err(Error::CorruptObject(format!(
2986 "object {} is not a {}",
2987 root.input,
2988 expected.as_str()
2989 )));
2990 }
2991 }
2992
2993 match object.kind {
2994 ObjectKind::Commit => {
2995 let commit = parse_commit(&object.data)?;
2996 let parent_union = union_parent_reachable_objects(
2997 repo,
2998 &commit.parents,
2999 missing_action,
3000 missing,
3001 missing_seen,
3002 )?;
3003 collect_tree_objects_filtered(
3004 repo,
3005 commit.tree,
3006 "",
3007 0,
3008 false,
3009 Some(&parent_union),
3010 tree_state,
3011 emitted,
3012 result,
3013 omitted,
3014 missing,
3015 missing_seen,
3016 filter,
3017 filter_provided,
3018 missing_action,
3019 sparse_lines,
3020 skip_trees_for_type_filter,
3021 omit_object_paths,
3022 packed_set,
3023 )?;
3024 }
3025 ObjectKind::Tree => {
3026 collect_tree_objects_filtered(
3027 repo,
3028 root.oid,
3029 "",
3030 0,
3031 true,
3032 None,
3033 tree_state,
3034 emitted,
3035 result,
3036 omitted,
3037 missing,
3038 missing_seen,
3039 filter,
3040 filter_provided,
3041 missing_action,
3042 sparse_lines,
3043 skip_trees_for_type_filter,
3044 omit_object_paths,
3045 packed_set,
3046 )?;
3047 }
3048 ObjectKind::Blob => {
3049 let path_for_sparse = root.root_path.as_deref().unwrap_or("");
3050 let blob_included = match filter {
3051 None => true,
3052 Some(f) => {
3053 if !filter_provided {
3054 true
3055 } else {
3056 f.includes_blob_under_tree(object.data.len() as u64, 0)
3057 }
3058 }
3059 };
3060 let blob_included = blob_included
3061 && sparse_lines.is_none_or(|lines| {
3062 path_matches_sparse_pattern_list(path_for_sparse, lines) != Some(false)
3063 });
3064 if !blob_included {
3065 omitted.push(root.oid);
3066 return Ok(());
3067 }
3068 if packed_set.is_some_and(|p| p.contains(&root.oid)) {
3069 return Ok(());
3070 }
3071 if !emitted.insert(root.oid) {
3072 return Ok(());
3073 }
3074 let out_path = if omit_object_paths {
3075 String::new()
3076 } else {
3077 path_for_sparse.to_owned()
3078 };
3079 result.push((root.oid, out_path));
3080 }
3081 ObjectKind::Tag => {
3082 let tag = parse_tag(&object.data)?;
3083 let expected_kind =
3084 ExpectedObjectKind::from_tag_type(&tag.object_type).ok_or_else(|| {
3085 Error::CorruptObject(format!(
3086 "object {} has unsupported tag type '{}'",
3087 root.input, tag.object_type
3088 ))
3089 })?;
3090 let nested = RootObject {
3091 oid: tag.object,
3092 input: root.input.clone(),
3093 expected_kind: Some(expected_kind),
3094 root_path: None,
3095 };
3096 collect_root_object(
3097 repo,
3098 &nested,
3099 tree_state,
3100 emitted,
3101 result,
3102 omitted,
3103 missing,
3104 missing_seen,
3105 filter,
3106 filter_provided,
3107 missing_action,
3108 sparse_lines,
3109 skip_trees_for_type_filter,
3110 omit_object_paths,
3111 packed_set,
3112 )?;
3113 }
3114 }
3115
3116 Ok(())
3117}
3118
3119#[allow(dead_code)]
3120fn collect_tree_objects_filtered(
3121 repo: &Repository,
3122 tree_oid: ObjectId,
3123 prefix: &str,
3124 depth: u64,
3125 explicit_root: bool,
3126 parent_union: Option<&HashSet<ObjectId>>,
3127 tree_state: &mut TreeWalkState,
3128 emitted: &mut HashSet<ObjectId>,
3129 result: &mut Vec<(ObjectId, String)>,
3130 omitted: &mut Vec<ObjectId>,
3131 missing: &mut Vec<ObjectId>,
3132 missing_seen: &mut HashSet<ObjectId>,
3133 filter: Option<&ObjectFilter>,
3134 filter_provided: bool,
3135 missing_action: MissingAction,
3136 sparse_lines: Option<&[String]>,
3137 skip_trees_for_type_filter: bool,
3138 omit_object_paths: bool,
3139 packed_set: Option<&HashSet<ObjectId>>,
3140) -> Result<()> {
3141 if tree_state.should_skip_tree(tree_oid, depth) {
3142 return Ok(());
3143 }
3144 if !explicit_root {
3145 if let Some(pu) = parent_union {
3146 if pu.contains(&tree_oid) {
3147 return Ok(());
3148 }
3149 }
3150 }
3151 let object = match repo.odb.read(&tree_oid) {
3152 Ok(object) => object,
3153 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
3154 if missing_action == MissingAction::Print && missing_seen.insert(tree_oid) {
3155 missing.push(tree_oid);
3156 }
3157 return Ok(());
3158 }
3159 Err(err) => return Err(err),
3160 };
3161 if object.kind != ObjectKind::Tree {
3162 return Err(Error::CorruptObject(format!(
3163 "object {tree_oid} is not a tree"
3164 )));
3165 }
3166 let tree_included = match filter {
3167 None => true,
3168 Some(f) => {
3169 if explicit_root && !filter_provided {
3170 true
3171 } else {
3172 f.includes_tree(depth)
3173 }
3174 }
3175 };
3176 let tree_included = tree_included
3177 && sparse_lines
3178 .is_none_or(|lines| path_matches_sparse_pattern_list(prefix, lines) != Some(false));
3179 if tree_included {
3180 if !packed_set.is_some_and(|p| p.contains(&tree_oid)) && emitted.insert(tree_oid) {
3181 let out_path = if omit_object_paths {
3182 String::new()
3183 } else {
3184 prefix.to_owned()
3185 };
3186 result.push((tree_oid, out_path));
3187 }
3188 } else {
3189 omitted.push(tree_oid);
3190 }
3191 if skip_trees_for_type_filter && depth == 0 && !explicit_root {
3192 return Ok(());
3193 }
3194 let entries = parse_tree(&object.data)?;
3195 for entry in entries {
3196 if entry.mode == 0o160000 {
3199 continue;
3200 }
3201 let name = String::from_utf8_lossy(&entry.name).to_string();
3202 let path = if prefix.is_empty() {
3203 name.clone()
3204 } else {
3205 format!("{prefix}/{name}")
3206 };
3207 let child_obj = match repo.odb.read(&entry.oid) {
3208 Ok(object) => object,
3209 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
3210 if missing_action == MissingAction::Print && missing_seen.insert(entry.oid) {
3211 missing.push(entry.oid);
3212 }
3213 continue;
3214 }
3215 Err(err) => return Err(err),
3216 };
3217 if entry.mode == 0o040000 {
3218 if child_obj.kind != ObjectKind::Tree {
3219 return Err(Error::CorruptObject(format!(
3220 "object {} is not a tree",
3221 entry.oid
3222 )));
3223 }
3224 if let Some(pu) = parent_union {
3225 if pu.contains(&entry.oid) {
3226 continue;
3227 }
3228 }
3229 let child_tree_depth = depth + 1;
3230 collect_tree_objects_filtered(
3231 repo,
3232 entry.oid,
3233 &path,
3234 child_tree_depth,
3235 false,
3236 parent_union,
3237 tree_state,
3238 emitted,
3239 result,
3240 omitted,
3241 missing,
3242 missing_seen,
3243 filter,
3244 filter_provided,
3245 missing_action,
3246 sparse_lines,
3247 skip_trees_for_type_filter,
3248 omit_object_paths,
3249 packed_set,
3250 )?;
3251 } else {
3252 if let Some(pu) = parent_union {
3253 if pu.contains(&entry.oid) {
3254 continue;
3255 }
3256 }
3257 if child_obj.kind == ObjectKind::Blob {
3258 let blob_included = match filter {
3259 None => true,
3260 Some(f) => {
3261 if explicit_root && !filter_provided {
3262 true
3263 } else {
3264 f.includes_blob_under_tree(child_obj.data.len() as u64, depth)
3265 }
3266 }
3267 };
3268 let blob_included = blob_included
3269 && sparse_lines.is_none_or(|lines| {
3270 path_matches_sparse_pattern_list(&path, lines) != Some(false)
3271 });
3272 if blob_included {
3273 if !packed_set.is_some_and(|p| p.contains(&entry.oid))
3274 && emitted.insert(entry.oid)
3275 {
3276 let out_path = if omit_object_paths {
3277 String::new()
3278 } else {
3279 path.clone()
3280 };
3281 result.push((entry.oid, out_path));
3282 }
3283 } else {
3284 omitted.push(entry.oid);
3285 }
3286 } else {
3287 if emitted.insert(entry.oid) {
3289 result.push((entry.oid, path));
3290 }
3291 }
3292 }
3293 }
3294 Ok(())
3295}
3296
3297fn collect_reachable_objects_in_commit_order(
3301 repo: &Repository,
3302 graph: &mut CommitGraph<'_>,
3303 commits: &[ObjectId],
3304 object_roots: &[RootObject],
3305 filter: Option<&ObjectFilter>,
3306 filter_provided: bool,
3307 missing_action: MissingAction,
3308 sparse_lines: Option<&[String]>,
3309 skip_trees_for_type_filter: bool,
3310 omit_object_paths: bool,
3311 packed_set: Option<&HashSet<ObjectId>>,
3312) -> Result<(
3313 Vec<(ObjectId, String)>,
3314 Vec<ObjectId>,
3315 Vec<ObjectId>,
3316 Vec<usize>,
3317)> {
3318 let mut tree_state = TreeWalkState::new(filter);
3319 let mut emitted = HashSet::new();
3320 let mut result = Vec::new();
3321 let mut omitted = Vec::new();
3322 let mut missing = Vec::new();
3323 let mut missing_seen = HashSet::new();
3324 let mut counts = Vec::with_capacity(commits.len());
3325 for &commit_oid in commits {
3326 let commit = match load_commit(repo, commit_oid) {
3327 Ok(commit) => commit,
3328 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
3329 if missing_action == MissingAction::Print && missing_seen.insert(commit_oid) {
3330 missing.push(commit_oid);
3331 }
3332 counts.push(0);
3333 continue;
3334 }
3335 Err(err) => return Err(err),
3336 };
3337 let before = result.len();
3338 let parents = graph.parents_of(commit_oid)?;
3339 let parent_union = union_parent_reachable_objects(
3340 repo,
3341 &parents,
3342 missing_action,
3343 &mut missing,
3344 &mut missing_seen,
3345 )?;
3346 collect_tree_objects_filtered(
3347 repo,
3348 commit.tree,
3349 "",
3350 0,
3351 false,
3352 Some(&parent_union),
3353 &mut tree_state,
3354 &mut emitted,
3355 &mut result,
3356 &mut omitted,
3357 &mut missing,
3358 &mut missing_seen,
3359 filter,
3360 filter_provided,
3361 missing_action,
3362 sparse_lines,
3363 skip_trees_for_type_filter,
3364 omit_object_paths,
3365 packed_set,
3366 )?;
3367 counts.push(result.len() - before);
3368 }
3369
3370 for root in object_roots {
3371 collect_root_object(
3372 repo,
3373 root,
3374 &mut tree_state,
3375 &mut emitted,
3376 &mut result,
3377 &mut omitted,
3378 &mut missing,
3379 &mut missing_seen,
3380 filter,
3381 filter_provided,
3382 missing_action,
3383 sparse_lines,
3384 skip_trees_for_type_filter,
3385 omit_object_paths,
3386 packed_set,
3387 )?;
3388 }
3389
3390 Ok((result, omitted, missing, counts))
3391}
3392
3393fn kept_object_ids(repo: &Repository) -> Result<HashSet<ObjectId>> {
3395 let pack_dir = repo.git_dir.join("objects/pack");
3396 let mut kept = HashSet::new();
3397 if !pack_dir.is_dir() {
3398 return Ok(kept);
3399 }
3400 for entry in std::fs::read_dir(&pack_dir)? {
3401 let entry = entry?;
3402 let path = entry.path();
3403 if path.extension().is_some_and(|ext| ext == "keep") {
3404 let idx_path = path.with_extension("idx");
3406 if idx_path.exists() {
3407 if let Ok(oids) = crate::pack::read_idx_object_ids(&idx_path) {
3408 kept.extend(oids);
3409 }
3410 }
3411 }
3412 }
3413 Ok(kept)
3414}
3415
3416fn flatten_tree(
3417 repo: &Repository,
3418 tree_oid: ObjectId,
3419 prefix: &str,
3420) -> Result<Vec<(String, ObjectId)>> {
3421 let mut result = Vec::new();
3422 let object = match repo.odb.read(&tree_oid) {
3423 Ok(o) => o,
3424 Err(_) => return Ok(result),
3425 };
3426 if object.kind != ObjectKind::Tree {
3427 return Ok(result);
3428 }
3429 let entries = parse_tree(&object.data)?;
3430 for entry in entries {
3431 let name = String::from_utf8_lossy(&entry.name).to_string();
3432 let path = if prefix.is_empty() {
3433 name
3434 } else {
3435 format!("{prefix}/{name}")
3436 };
3437 let child = match repo.odb.read(&entry.oid) {
3438 Ok(o) => o,
3439 Err(Error::ObjectNotFound(_)) => continue,
3440 Err(err) => return Err(err),
3441 };
3442 if child.kind == ObjectKind::Tree {
3443 result.extend(flatten_tree(repo, entry.oid, &path)?);
3444 } else {
3445 result.push((path, entry.oid));
3446 }
3447 }
3448 Ok(result)
3449}
3450
3451pub fn merge_bases(
3453 repo: &Repository,
3454 a: ObjectId,
3455 b: ObjectId,
3456 first_parent_only: bool,
3457) -> Result<Vec<ObjectId>> {
3458 let mut graph = CommitGraph::new(repo, first_parent_only);
3459 let ancestors_a = walk_closure(&mut graph, &[a])?;
3460 let ancestors_b = walk_closure(&mut graph, &[b])?;
3461 let common: HashSet<ObjectId> = ancestors_a.intersection(&ancestors_b).copied().collect();
3462 if common.is_empty() {
3463 return Ok(Vec::new());
3464 }
3465 let mut bases = Vec::new();
3467 for &c in &common {
3468 let is_dominated = common.iter().any(|&other| {
3469 if other == c {
3470 return false;
3471 }
3472 let other_anc = walk_closure(&mut graph, &[other]).unwrap_or_default();
3473 other_anc.contains(&c)
3474 });
3475 if !is_dominated {
3476 bases.push(c);
3477 }
3478 }
3479 if bases.is_empty() {
3480 let mut sorted: Vec<_> = common.into_iter().collect();
3481 sorted.sort_by_key(|b| std::cmp::Reverse(graph.committer_time(*b)));
3482 bases.push(sorted[0]);
3483 }
3484 Ok(bases)
3485}