1use std::cmp::{Ordering, Reverse};
9use std::collections::{BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque};
10use std::fs;
11use std::io::Write;
12use std::path::Path;
13
14use crate::commit_graph_file::{BloomPrecheck, BloomWalkStatsHandle, CommitGraphChain};
15use crate::config::ConfigSet;
16use crate::diff::zero_oid;
17use crate::error::{Error, Result};
18use crate::ident::{committer_unix_seconds_for_ordering, parse_signature_times};
19use crate::ignore::{parse_sparse_patterns_from_blob, path_in_sparse_checkout};
20use crate::index::{CacheTreeNode, Index, MODE_GITLINK, MODE_TREE};
21use crate::objects::{parse_commit, parse_tag, parse_tree, ObjectId, ObjectKind};
22use crate::pack;
23use crate::patch_ids::{compute_patch_id, compute_patch_id_for_paths};
24use crate::ref_exclusions::{git_namespace_prefix, strip_git_namespace, RefExclusions};
25use crate::reflog::{list_reflog_refs, read_reflog};
26use crate::refs;
27use crate::repo::Repository;
28use crate::rev_parse::{resolve_revision_for_range_end, resolve_treeish_path, split_treeish_spec};
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum OutputMode {
33 OidOnly,
35 Parents,
37 Format(String),
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum MissingAction {
44 Error,
46 Print,
48 PrintInfo,
50 Allow,
52}
53
54impl MissingAction {
55 fn reports_missing(self) -> bool {
56 matches!(self, Self::Print | Self::PrintInfo)
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum FilterObjectKind {
63 Blob,
64 Tree,
65 Commit,
66 Tag,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
71pub enum ObjectFilter {
72 BlobNone,
74 BlobLimit(u64),
76 TreeDepth(u64),
78 SparseOid(String),
80 ObjectType(FilterObjectKind),
82 Combine(Vec<ObjectFilter>),
84}
85
86impl ObjectFilter {
87 pub fn parse(spec: &str) -> std::result::Result<Self, String> {
89 Self::parse_inner(spec.trim(), false)
90 }
91
92 fn parse_inner(spec: &str, from_combine_subfilter: bool) -> std::result::Result<Self, String> {
93 if spec == "blob:none" {
94 return Ok(ObjectFilter::BlobNone);
95 }
96 if let Some(rest) = spec.strip_prefix("blob:limit=") {
97 let bytes = parse_size_suffix(rest)
98 .ok_or_else(|| format!("invalid blob:limit value: {rest}"))?;
99 return Ok(ObjectFilter::BlobLimit(bytes));
100 }
101 if let Some(rest) = spec.strip_prefix("tree:") {
102 if rest.is_empty() || !rest.chars().all(|c| c.is_ascii_digit()) {
103 return Err(if from_combine_subfilter {
104 "expected 'tree:<depth>'.".to_owned()
105 } else {
106 format!("invalid tree depth: {rest}")
107 });
108 }
109 let depth: u64 = rest.parse().map_err(|_| {
110 if from_combine_subfilter {
111 "expected 'tree:<depth>'.".to_owned()
112 } else {
113 format!("invalid tree depth: {rest}")
114 }
115 })?;
116 return Ok(ObjectFilter::TreeDepth(depth));
117 }
118 if let Some(rest) = spec.strip_prefix("object:type=") {
119 let kind = match rest {
120 "blob" => FilterObjectKind::Blob,
121 "tree" => FilterObjectKind::Tree,
122 "commit" => FilterObjectKind::Commit,
123 "tag" => FilterObjectKind::Tag,
124 "" => return Err("invalid object type".to_owned()),
125 _ => return Err(format!("invalid object type: {rest}")),
126 };
127 return Ok(ObjectFilter::ObjectType(kind));
128 }
129 if let Some(rest) = spec.strip_prefix("sparse:oid=") {
130 if rest.is_empty() {
131 return Err("invalid sparse:oid value: ".to_owned());
132 }
133 return Ok(ObjectFilter::SparseOid(rest.to_owned()));
134 }
135 if let Some(rest) = spec.strip_prefix("combine:") {
136 if rest.is_empty() {
137 return Err("expected something after combine:".to_owned());
138 }
139 let parts = split_combine_raw_parts(rest);
140 if parts.is_empty() {
141 return Err("expected something after combine:".to_owned());
142 }
143 let mut filters = Vec::new();
144 for part in parts {
145 filters.push(Self::parse_from_combine_subfilter(part)?);
146 }
147 return Ok(ObjectFilter::Combine(filters));
148 }
149 Err(format!("invalid filter-spec '{spec}'"))
150 }
151
152 fn parse_from_combine_subfilter(encoded: &str) -> std::result::Result<Self, String> {
153 if let Some(ch) = combine_subfilter_has_reserved(encoded) {
154 return Err(format!("must escape char in sub-filter-spec: '{ch}'"));
155 }
156 let decoded = url_decode(encoded);
157 Self::parse_inner(&decoded, true)
158 }
159
160 #[must_use]
162 pub fn merge_with(self, other: Self) -> Self {
163 match (self, other) {
164 (ObjectFilter::Combine(mut a), ObjectFilter::Combine(mut b)) => {
165 a.append(&mut b);
166 ObjectFilter::Combine(a)
167 }
168 (ObjectFilter::Combine(mut a), b) => {
169 a.push(b);
170 ObjectFilter::Combine(a)
171 }
172 (a, ObjectFilter::Combine(mut b)) => {
173 let mut v = vec![a];
174 v.append(&mut b);
175 ObjectFilter::Combine(v)
176 }
177 (a, b) => ObjectFilter::Combine(vec![a, b]),
178 }
179 }
180
181 pub fn includes_blob(&self, size: u64) -> bool {
183 match self {
184 ObjectFilter::BlobNone => false,
185 ObjectFilter::BlobLimit(limit) => size < *limit,
186 ObjectFilter::TreeDepth(_) => true,
189 ObjectFilter::SparseOid(_) => true,
190 ObjectFilter::ObjectType(kind) => *kind == FilterObjectKind::Blob,
191 ObjectFilter::Combine(filters) => filters.iter().all(|f| f.includes_blob(size)),
192 }
193 }
194
195 #[must_use]
200 pub fn includes_blob_under_tree(&self, size: u64, parent_tree_depth: u64) -> bool {
201 match self {
202 ObjectFilter::BlobNone => false,
203 ObjectFilter::BlobLimit(limit) => size < *limit,
204 ObjectFilter::TreeDepth(max_depth) => parent_tree_depth.saturating_add(1) < *max_depth,
205 ObjectFilter::SparseOid(_) => true,
206 ObjectFilter::ObjectType(kind) => *kind == FilterObjectKind::Blob,
207 ObjectFilter::Combine(filters) => filters
208 .iter()
209 .all(|f| f.includes_blob_under_tree(size, parent_tree_depth)),
210 }
211 }
212
213 pub fn includes_tree(&self, depth: u64) -> bool {
215 match self {
216 ObjectFilter::BlobNone => true,
217 ObjectFilter::BlobLimit(_) => true,
218 ObjectFilter::TreeDepth(max_depth) => depth < *max_depth,
219 ObjectFilter::SparseOid(_) => true,
220 ObjectFilter::ObjectType(kind) => *kind == FilterObjectKind::Tree,
221 ObjectFilter::Combine(filters) => filters.iter().all(|f| f.includes_tree(depth)),
222 }
223 }
224
225 pub fn includes_commit_or_tag_object(&self, kind: ObjectKind) -> bool {
227 let expected = match kind {
228 ObjectKind::Commit => Some(FilterObjectKind::Commit),
229 ObjectKind::Tag => Some(FilterObjectKind::Tag),
230 _ => None,
231 };
232 match self {
233 ObjectFilter::BlobNone | ObjectFilter::BlobLimit(_) => true,
234 ObjectFilter::TreeDepth(_) => true,
235 ObjectFilter::SparseOid(_) => true,
236 ObjectFilter::ObjectType(t) => expected == Some(*t),
237 ObjectFilter::Combine(filters) => filters
238 .iter()
239 .all(|f| f.includes_commit_or_tag_object(kind)),
240 }
241 }
242
243 pub fn includes_loose_object(&self, kind: ObjectKind, size: u64) -> bool {
245 match kind {
246 ObjectKind::Blob => self.includes_blob(size),
247 ObjectKind::Tree => self.includes_tree(0),
248 ObjectKind::Commit | ObjectKind::Tag => self.includes_commit_or_tag_object(kind),
249 }
250 }
251
252 #[must_use]
254 pub fn passes_for_object(&self, kind: ObjectKind, size: usize) -> bool {
255 self.includes_loose_object(kind, size as u64)
256 }
257}
258
259#[must_use]
262pub fn reachable_object_ids_for_cat_file(
263 repo: &Repository,
264 filter: Option<&ObjectFilter>,
265 filter_provided_objects: bool,
266) -> Result<Vec<ObjectId>> {
267 let opts = RevListOptions {
268 all_refs: true,
269 objects: true,
270 no_object_names: true,
271 quiet: true,
272 filter: filter.cloned(),
273 filter_provided_objects,
274 ..Default::default()
275 };
276 let result = rev_list(repo, &[], &[], &opts)?;
277 let mut set = BTreeSet::new();
278 for (i, oid) in result.commits.iter().enumerate() {
279 if result.objects_print_commit.get(i).copied().unwrap_or(true) {
280 set.insert(*oid);
281 }
282 }
283 for (oid, _) in &result.objects {
284 set.insert(*oid);
285 }
286 Ok(set.into_iter().collect())
287}
288
289#[must_use]
292pub fn object_ids_for_cat_file_filtered(
293 repo: &Repository,
294 filter: &ObjectFilter,
295) -> Result<Vec<ObjectId>> {
296 reachable_object_ids_for_cat_file(repo, Some(filter), true)
297}
298
299fn parse_size_suffix(s: &str) -> Option<u64> {
301 let s = s.trim();
302 if s.is_empty() {
303 return None;
304 }
305 let (num_str, multiplier) = match s.as_bytes().last()? {
306 b'k' | b'K' => (&s[..s.len() - 1], 1024u64),
307 b'm' | b'M' => (&s[..s.len() - 1], 1024 * 1024),
308 b'g' | b'G' => (&s[..s.len() - 1], 1024 * 1024 * 1024),
309 _ => (s, 1u64),
310 };
311 let num: u64 = num_str.parse().ok()?;
312 Some(num * multiplier)
313}
314
315fn split_combine_raw_parts(spec: &str) -> Vec<&str> {
317 spec.split('+').filter(|s| !s.is_empty()).collect()
318}
319
320fn combine_subfilter_has_reserved(encoded: &str) -> Option<char> {
323 const RESERVED: &str = "~`!@#$^&*()[]{}\\;'\",<>?";
324 for ch in encoded.chars() {
325 if ch.is_control() || ch.is_whitespace() {
326 return Some(ch);
327 }
328 if RESERVED.contains(ch) {
329 return Some(ch);
330 }
331 }
332 None
333}
334
335#[must_use]
337pub fn expand_object_filter_for_protocol(spec: &str) -> std::result::Result<String, String> {
338 let f = ObjectFilter::parse(spec)?;
339 match f {
340 ObjectFilter::BlobLimit(n) => Ok(format!("blob:limit={n}")),
341 _ => Ok(spec.to_owned()),
342 }
343}
344
345fn combine_filter_allow_unencoded(ch: char) -> bool {
346 if ch.is_control() || ch.is_whitespace() || ch == '%' || ch == '+' {
347 return false;
348 }
349 !"~`!@#$^&*()[]{}\\;'\",<>?".contains(ch)
350}
351
352pub fn url_encode_object_filter_subspec(raw: &str) -> String {
354 let mut out = String::new();
355 for b in raw.as_bytes() {
356 let ch = *b as char;
357 if combine_filter_allow_unencoded(ch) {
358 out.push(ch);
359 } else {
360 out.push_str(&format!("%{:02x}", b));
361 }
362 }
363 out
364}
365
366pub fn trace_combine_filter_append(encoded_segment: &str) {
368 let Ok(trace_val) = std::env::var("GIT_TRACE") else {
369 return;
370 };
371 if trace_val.is_empty() || trace_val == "0" || trace_val.eq_ignore_ascii_case("false") {
372 return;
373 }
374 let line = format!("Add to combine filter-spec: {encoded_segment}\n");
375 match trace_val.as_str() {
376 "1" | "true" | "2" => {
377 let _ = std::io::stderr().write_all(line.as_bytes());
378 }
379 path => {
380 if let Ok(mut f) = std::fs::OpenOptions::new()
381 .create(true)
382 .append(true)
383 .open(path)
384 {
385 let _ = f.write_all(line.as_bytes());
386 }
387 }
388 }
389}
390
391fn url_decode(s: &str) -> String {
393 let mut result = String::new();
394 let mut chars = s.chars();
395 while let Some(ch) = chars.next() {
396 if ch == '%' {
397 let hi = chars.next().unwrap_or('0');
398 let lo = chars.next().unwrap_or('0');
399 let byte = u8::from_str_radix(&format!("{hi}{lo}"), 16).unwrap_or(b'?');
400 result.push(byte as char);
401 } else {
402 result.push(ch);
403 }
404 }
405 result
406}
407
408#[derive(Debug, Clone, Copy, PartialEq, Eq)]
410pub enum OrderingMode {
411 Default,
413 DateOrderWalk,
415 AuthorDateWalk,
417 Topo,
419 AuthorDateTopo,
421}
422
423#[derive(Debug, Clone)]
425pub struct RevListOptions {
426 pub all_refs: bool,
428 pub first_parent: bool,
430 pub ancestry_path: bool,
432 pub ancestry_path_bottoms: Vec<ObjectId>,
434 pub simplify_by_decoration: bool,
436 pub simplify_by_decoration_oids: HashSet<ObjectId>,
444 pub output_mode: OutputMode,
446 pub quiet: bool,
448 pub count: bool,
450 pub skip: usize,
452 pub max_count: Option<usize>,
454 pub ordering: OrderingMode,
456 pub reverse: bool,
458 pub maximal_only: bool,
460 pub objects: bool,
462 pub no_object_names: bool,
464 pub boundary: bool,
466 pub left_right: bool,
468 pub left_right_explicit: bool,
470 pub left_only: bool,
472 pub right_only: bool,
474 pub cherry_mark: bool,
476 pub cherry_pick: bool,
478 pub min_parents: Option<usize>,
480 pub max_parents: Option<usize>,
482 pub symmetric_left: Option<ObjectId>,
484 pub symmetric_right: Option<ObjectId>,
486 pub paths: Vec<String>,
488 pub full_history: bool,
490 pub parent_rewrite: bool,
492 pub sparse: bool,
494 pub simplify_merges: bool,
496 pub preserve_simplify_merges_graph_merges: bool,
498 pub show_pulls: bool,
500 pub exclude_first_parent_only: bool,
502 pub filter: Option<ObjectFilter>,
504 pub filter_raw_specs: Vec<String>,
506 pub filter_provided_objects: bool,
508 pub filter_print_omitted: bool,
510 pub in_commit_order: bool,
512 pub no_kept_objects: bool,
514 pub missing_action: MissingAction,
516 pub exclude_promisor_objects: bool,
518 pub ignore_missing: bool,
520 pub use_bitmap_index: bool,
522 pub unpacked_only: bool,
524 pub bitmap_oid_only_objects: bool,
527 pub path_graph_reorder: bool,
530 pub until_cutoff: Option<i64>,
532 pub since_cutoff: Option<i64>,
534 pub include_reflog_entries: bool,
536 pub include_indexed_objects: bool,
538 pub exclude_indexed_objects: bool,
540 pub use_commit_graph_bloom: bool,
542 pub commit_graph_read_changed_paths: bool,
544 pub commit_graph_changed_paths_version: i32,
546 pub bloom_stats: Option<BloomWalkStatsHandle>,
548}
549
550impl Default for RevListOptions {
551 fn default() -> Self {
552 Self {
553 all_refs: false,
554 first_parent: false,
555 ancestry_path: false,
556 ancestry_path_bottoms: Vec::new(),
557 simplify_by_decoration: false,
558 simplify_by_decoration_oids: HashSet::new(),
559 output_mode: OutputMode::OidOnly,
560 quiet: false,
561 count: false,
562 skip: 0,
563 max_count: None,
564 ordering: OrderingMode::Default,
565 reverse: false,
566 maximal_only: false,
567 objects: false,
568 no_object_names: false,
569 boundary: false,
570 left_right: false,
571 left_right_explicit: false,
572 left_only: false,
573 right_only: false,
574 cherry_mark: false,
575 cherry_pick: false,
576 min_parents: None,
577 max_parents: None,
578 symmetric_left: None,
579 symmetric_right: None,
580 paths: Vec::new(),
581 full_history: false,
582 parent_rewrite: false,
583 sparse: false,
584 simplify_merges: false,
585 preserve_simplify_merges_graph_merges: false,
586 show_pulls: false,
587 exclude_first_parent_only: false,
588 filter: None,
589 filter_raw_specs: Vec::new(),
590 filter_provided_objects: false,
591 filter_print_omitted: false,
592 in_commit_order: false,
593 no_kept_objects: false,
594 missing_action: MissingAction::Error,
595 exclude_promisor_objects: false,
596 ignore_missing: false,
597 use_bitmap_index: false,
598 unpacked_only: false,
599 bitmap_oid_only_objects: false,
600 path_graph_reorder: true,
601 until_cutoff: None,
602 since_cutoff: None,
603 include_reflog_entries: false,
604 include_indexed_objects: false,
605 exclude_indexed_objects: false,
606 use_commit_graph_bloom: false,
607 commit_graph_read_changed_paths: true,
608 commit_graph_changed_paths_version: -1,
609 bloom_stats: None,
610 }
611 }
612}
613
614#[derive(Debug, Clone)]
616pub struct RevListResult {
617 pub commits: Vec<ObjectId>,
619 pub objects: Vec<(ObjectId, String)>,
622 pub omitted_objects: Vec<ObjectId>,
624 pub missing_objects: Vec<ObjectId>,
626 pub boundary_commits: Vec<ObjectId>,
628 pub left_right_map: HashMap<ObjectId, bool>,
630 pub cherry_equivalent: HashSet<ObjectId>,
632 pub per_commit_object_counts: Vec<usize>,
636 pub object_walk_tips: Vec<ObjectId>,
638 pub objects_print_commit: Vec<bool>,
641 pub object_segments: Vec<Vec<(ObjectId, String)>>,
644 pub bitmap_object_format: bool,
646 pub tip_annotated_tag_by_commit: HashMap<ObjectId, ObjectId>,
648}
649
650#[derive(Debug, Clone, PartialEq, Eq)]
652pub struct BisectEntry {
653 pub oid: ObjectId,
655 pub reaches: usize,
657 pub distance: usize,
659}
660
661#[derive(Debug, Clone, PartialEq, Eq)]
663pub struct BisectSelection {
664 pub best: Option<BisectEntry>,
666 pub all: Vec<BisectEntry>,
668 pub total: usize,
670}
671
672pub fn select_bisect_commits(
688 repo: &Repository,
689 commits: &[ObjectId],
690 first_parent: bool,
691) -> Result<BisectSelection> {
692 let total = commits.len();
693 if total == 0 {
694 return Ok(BisectSelection {
695 best: None,
696 all: Vec::new(),
697 total: 0,
698 });
699 }
700
701 let candidates: HashSet<ObjectId> = commits.iter().copied().collect();
702 let mut graph = CommitGraph::new(repo, first_parent);
703 let mut entries = Vec::with_capacity(commits.len());
704 for &oid in commits {
705 let reaches = count_reachable_candidates(&mut graph, oid, &candidates)?;
706 let distance = reaches.min(total.saturating_sub(reaches));
707 entries.push(BisectEntry {
708 oid,
709 reaches,
710 distance,
711 });
712 }
713
714 let mut sorted = entries.clone();
715 sorted.sort_by(|a, b| b.distance.cmp(&a.distance).then_with(|| a.oid.cmp(&b.oid)));
716
717 let mut best = None;
718 for entry in &entries {
719 if best
720 .as_ref()
721 .is_none_or(|current: &BisectEntry| entry.distance > current.distance)
722 {
723 best = Some(entry.clone());
724 }
725 }
726 Ok(BisectSelection {
727 best,
728 all: sorted,
729 total,
730 })
731}
732
733fn count_reachable_candidates(
734 graph: &mut CommitGraph<'_>,
735 oid: ObjectId,
736 candidates: &HashSet<ObjectId>,
737) -> Result<usize> {
738 let mut stack = vec![oid];
739 let mut seen = HashSet::new();
740 while let Some(current) = stack.pop() {
741 if !seen.insert(current) {
742 continue;
743 }
744 for parent in graph.parents_of(current)? {
745 if candidates.contains(&parent) {
746 stack.push(parent);
747 }
748 }
749 }
750 Ok(seen.len())
751}
752
753fn retain_maximal_commits(
754 graph: &mut CommitGraph<'_>,
755 candidates: &mut HashSet<ObjectId>,
756) -> Result<()> {
757 if candidates.len() < 2 {
758 return Ok(());
759 }
760
761 let candidate_list: Vec<ObjectId> = candidates.iter().copied().collect();
762 let candidate_set: HashSet<ObjectId> = candidate_list.iter().copied().collect();
763 let mut reachable_from_another = HashSet::new();
764
765 for tip in candidate_list {
766 for reachable in walk_closure(graph, &[tip])? {
767 if reachable != tip && candidate_set.contains(&reachable) {
768 reachable_from_another.insert(reachable);
769 }
770 }
771 }
772
773 candidates.retain(|oid| !reachable_from_another.contains(oid));
774 Ok(())
775}
776
777pub fn rev_list(
791 repo: &Repository,
792 positive_specs: &[String],
793 negative_specs: &[String],
794 options: &RevListOptions,
795) -> Result<RevListResult> {
796 let mut graph = CommitGraph::new(repo, options.first_parent);
797
798 let (mut include, mut object_roots, tip_annotated_tag_by_commit) = if options.objects {
799 resolve_specs_for_objects_with_options(
800 repo,
801 positive_specs,
802 options.ignore_missing,
803 options.missing_action,
804 )?
805 } else {
806 (
807 resolve_specs_with_options(repo, positive_specs, options.ignore_missing)?,
808 Vec::new(),
809 HashMap::new(),
810 )
811 };
812 let (exclude, negative_object_roots) = if options.objects {
813 let (commits, roots, _) = resolve_specs_for_objects_with_options(
814 repo,
815 negative_specs,
816 options.ignore_missing,
817 options.missing_action,
818 )?;
819 (commits, roots)
820 } else {
821 (
822 resolve_specs_with_options(repo, negative_specs, options.ignore_missing)?,
823 Vec::new(),
824 )
825 };
826
827 if options.all_refs {
828 include.extend(all_ref_tips(repo, &RefExclusions::default())?);
829 if options.objects {
834 object_roots.extend(all_ref_non_commit_object_roots(repo)?);
835 }
836 }
837
838 if options.objects && options.include_reflog_entries {
839 include.extend(reflog_commit_tips(repo)?);
840 }
841
842 let index_object_roots = if options.objects
843 && (options.include_indexed_objects || options.exclude_indexed_objects)
844 {
845 indexed_object_roots(repo)?
846 } else {
847 Vec::new()
848 };
849
850 let object_roots = if !options.include_indexed_objects || index_object_roots.is_empty() {
851 object_roots
852 } else {
853 let mut merged = object_roots;
854 merged.extend(index_object_roots.iter().cloned());
855 merged
856 };
857 let negative_object_roots = if options.exclude_indexed_objects && !index_object_roots.is_empty()
858 {
859 let mut merged = negative_object_roots;
860 merged.extend(index_object_roots);
861 merged
862 } else {
863 negative_object_roots
864 };
865
866 if include.is_empty() && object_roots.is_empty() {
867 if options.ignore_missing {
868 return Ok(RevListResult {
869 commits: Vec::new(),
870 objects: Vec::new(),
871 omitted_objects: Vec::new(),
872 missing_objects: Vec::new(),
873 boundary_commits: Vec::new(),
874 left_right_map: HashMap::new(),
875 cherry_equivalent: HashSet::new(),
876 per_commit_object_counts: Vec::new(),
877 object_walk_tips: Vec::new(),
878 objects_print_commit: Vec::new(),
879 object_segments: Vec::new(),
880 bitmap_object_format: false,
881 tip_annotated_tag_by_commit: HashMap::new(),
882 });
883 }
884 return Err(Error::InvalidRef("no revisions specified".to_owned()));
885 }
886
887 let object_walk_tip_commits: Vec<ObjectId> = if options.objects {
888 include.clone()
889 } else {
890 Vec::new()
891 };
892
893 let excluded_promisor = if options.exclude_promisor_objects {
894 crate::promisor::promisor_expanded_object_ids(repo)?
895 } else {
896 HashSet::new()
897 };
898
899 let mut traversal_missing = Vec::new();
900 let mut traversal_missing_seen = HashSet::new();
901 let (mut included, discovery_order) = if include.is_empty() {
902 (HashSet::new(), Vec::new())
903 } else if options.exclude_promisor_objects {
904 walk_closure_ordered_excluding(&mut graph, &include, &excluded_promisor)?
905 } else if options.missing_action != MissingAction::Error {
906 walk_closure_ordered_with_missing(
907 &mut graph,
908 &include,
909 &HashSet::new(),
910 options.missing_action,
911 &mut traversal_missing,
912 &mut traversal_missing_seen,
913 )?
914 } else {
915 walk_closure_ordered(&mut graph, &include)?
916 };
917 let excluded = if exclude.is_empty() {
918 HashSet::new()
919 } else if options.exclude_promisor_objects {
920 walk_closure_ordered_excluding(&mut graph, &exclude, &excluded_promisor)?.0
921 } else if options.exclude_first_parent_only {
922 walk_closure_first_parent_only(&mut graph, &exclude)?
923 } else if options.missing_action != MissingAction::Error {
924 walk_closure_ordered_with_missing(
925 &mut graph,
926 &exclude,
927 &HashSet::new(),
928 options.missing_action,
929 &mut traversal_missing,
930 &mut traversal_missing_seen,
931 )?
932 .0
933 } else {
934 walk_closure(&mut graph, &exclude)?
935 };
936 included.retain(|oid| !excluded.contains(oid));
937
938 let (bloom_chain, bloom_read_changed, bloom_version, bloom_cwd) = if !options.paths.is_empty() {
955 let cfg = ConfigSet::load(Some(&repo.git_dir), true).unwrap_or_default();
956 let mut core_cg = match cfg.get_bool("core.commitgraph") {
957 Some(Ok(b)) => b,
958 _ => true,
959 };
960 if std::env::var("GIT_TEST_COMMIT_GRAPH").ok().as_deref() == Some("0") {
961 core_cg = false;
962 }
963 let read_paths = cfg
964 .get("commitgraph.readchangedpaths")
965 .and_then(|v| crate::config::parse_bool(&v).ok())
966 .unwrap_or(true);
967 let version = cfg
968 .get("commitgraph.changedpathsversion")
969 .and_then(|s| s.parse::<i32>().ok())
970 .unwrap_or(-1);
971 let use_bloom = core_cg
972 && options.use_commit_graph_bloom
973 && crate::pathspec::pathspecs_allow_bloom(&options.paths);
974 let read_changed = read_paths && options.commit_graph_read_changed_paths;
975 let chain = if use_bloom {
976 CommitGraphChain::load(&repo.git_dir.join("objects"))
977 } else {
978 None
979 };
980 let version = if options.commit_graph_changed_paths_version != -1 {
981 options.commit_graph_changed_paths_version
982 } else {
983 version
984 };
985 (chain, read_changed, version, repo.bloom_pathspec_cwd())
986 } else {
987 (None, false, -1, None)
988 };
989
990 let mut dense_path_limited = false;
991 let dense_traversable: HashSet<ObjectId> = if !options.paths.is_empty()
992 && !options.full_history
993 && !options.ancestry_path
994 && !options.simplify_merges
995 {
996 let traversable = included.clone();
997 if bloom_chain.is_some() {
1005 let bloom_ctx = DenseBloomCtx {
1006 chain: bloom_chain.as_ref(),
1007 read_changed_paths: bloom_read_changed,
1008 changed_paths_version: bloom_version,
1009 stats: options.bloom_stats.as_ref(),
1010 cwd: bloom_cwd.as_deref(),
1011 };
1012 consult_dense_bloom_filters(
1013 repo,
1014 &mut graph,
1015 &traversable,
1016 &options.paths,
1017 &bloom_ctx,
1018 )?;
1019 }
1020 included = walk_dense_path_limited_closure(
1021 repo,
1022 &mut graph,
1023 &include,
1024 &excluded,
1025 &options.paths,
1026 options.sparse,
1027 options.show_pulls,
1028 )?;
1029 dense_path_limited = true;
1030 traversable
1031 } else {
1032 HashSet::new()
1033 };
1034
1035 if options.simplify_by_decoration && options.simplify_by_decoration_oids.is_empty() {
1036 let decorated = all_ref_tips(repo, &RefExclusions::default())?;
1041 included.retain(|oid| decorated.contains(oid));
1042 }
1043
1044 let mut effective_ancestry_path_bottoms = Vec::new();
1045 if options.ancestry_path {
1046 let mut bottoms = options.ancestry_path_bottoms.clone();
1047 if bottoms.is_empty() {
1048 bottoms.extend(exclude.iter().copied());
1049 }
1050 if bottoms.is_empty() {
1051 return Err(Error::InvalidRef(
1052 "--ancestry-path requires a range with excluded tips".to_owned(),
1053 ));
1054 }
1055 limit_to_ancestry(&mut graph, &mut included, &bottoms)?;
1056 effective_ancestry_path_bottoms = bottoms;
1057 }
1058
1059 let path_effective_full = options.full_history
1062 || options.ancestry_path
1063 || (options.simplify_merges && !options.paths.is_empty());
1064
1065 let parent_count_filter_active = options.min_parents.is_some() || options.max_parents.is_some();
1069 let traversable = if parent_count_filter_active {
1070 if dense_path_limited {
1077 dense_traversable.clone()
1078 } else {
1079 included.clone()
1080 }
1081 } else {
1082 HashSet::new()
1083 };
1084 if parent_count_filter_active {
1085 let min_p = options.min_parents.unwrap_or(0);
1086 let max_p = options.max_parents.unwrap_or(usize::MAX);
1087 included.retain(|oid| {
1088 let count = graph.parents_of(*oid).map(|p| p.len()).unwrap_or(0);
1089 count >= min_p && count <= max_p
1090 });
1091 }
1092
1093 if options.maximal_only {
1094 retain_maximal_commits(&mut graph, &mut included)?;
1095 }
1096
1097 let mut ordered = match options.ordering {
1098 OrderingMode::Default | OrderingMode::DateOrderWalk | OrderingMode::AuthorDateWalk => {
1099 let author_dates = options.ordering == OrderingMode::AuthorDateWalk;
1100 if parent_count_filter_active {
1101 date_order_walk_through_dropped(
1108 &mut graph,
1109 &include,
1110 &traversable,
1111 &included,
1112 author_dates,
1113 )?
1114 } else if dense_path_limited && !include.is_empty() {
1115 date_order_walk_through_dropped(
1119 &mut graph,
1120 &include,
1121 &dense_traversable,
1122 &included,
1123 author_dates,
1124 )?
1125 } else {
1126 date_order_walk(&mut graph, &included, &include, author_dates)?
1127 }
1128 }
1129 OrderingMode::Topo => graph_order_topo_sort(&mut graph, &included, &discovery_order)?,
1130 OrderingMode::AuthorDateTopo => topo_sort(&mut graph, &included, true)?,
1131 };
1132
1133 if !options.paths.is_empty() {
1135 let paths = &options.paths;
1136 let retain_stats = if dense_path_limited {
1143 None
1144 } else {
1145 options.bloom_stats.as_ref()
1146 };
1147 ordered.retain(|oid| {
1148 commit_touches_paths(
1149 repo,
1150 &mut graph,
1151 *oid,
1152 paths,
1153 path_effective_full,
1154 options.sparse,
1155 options.simplify_merges,
1156 options.preserve_simplify_merges_graph_merges,
1157 options.show_pulls,
1158 options.parent_rewrite,
1159 options.ancestry_path,
1160 &effective_ancestry_path_bottoms,
1161 &excluded,
1162 bloom_chain.as_ref(),
1163 bloom_read_changed,
1164 bloom_version,
1165 retain_stats,
1166 bloom_cwd.as_deref(),
1167 )
1168 .unwrap_or(false)
1169 });
1170 }
1171
1172 if !options.paths.is_empty() && options.simplify_merges && !ordered.is_empty() {
1173 ordered = simplify_merges_commit_list(
1174 repo,
1175 &ordered,
1176 &options.paths,
1177 &excluded,
1178 &effective_ancestry_path_bottoms,
1179 options.ordering,
1180 options.show_pulls,
1181 options.preserve_simplify_merges_graph_merges,
1182 )?;
1183 }
1184
1185 let path_needs_graph_reorder = !options.paths.is_empty()
1188 && options.path_graph_reorder
1189 && !options.simplify_merges
1190 && (!options.full_history || options.sparse);
1191 if path_needs_graph_reorder && !ordered.is_empty() {
1192 if options.sparse {
1193 let mut dense_opts = options.clone();
1194 dense_opts.sparse = false;
1195 dense_opts.path_graph_reorder = false;
1196 let dense_result = rev_list(repo, positive_specs, negative_specs, &dense_opts)?;
1197 let dense_ordered = reorder_path_limited_graph_commits(
1198 repo,
1199 &dense_result.commits,
1200 options.first_parent,
1201 )?;
1202 ordered = expand_sparse_path_limited_graph_history(repo, &dense_ordered)?;
1203 } else {
1204 ordered = reorder_path_limited_graph_commits(repo, &ordered, options.first_parent)?;
1205 }
1206 }
1207
1208 if matches!(
1209 options.ordering,
1210 OrderingMode::Topo | OrderingMode::AuthorDateTopo
1211 ) && !options.left_right
1212 && !options.left_only
1213 && !options.right_only
1214 && !options.cherry_mark
1215 && !options.cherry_pick
1216 {
1217 if let (Some(left_oid), Some(right_oid)) = (options.symmetric_left, options.symmetric_right)
1218 {
1219 reorder_symmetric_topo_right_first(&mut graph, &mut ordered, left_oid, right_oid)?;
1220 }
1221 }
1222
1223 let mut left_right_map = HashMap::new();
1225 if options.left_right
1226 || options.left_only
1227 || options.right_only
1228 || options.cherry_mark
1229 || options.cherry_pick
1230 {
1231 if let (Some(left_oid), Some(right_oid)) = (options.symmetric_left, options.symmetric_right)
1232 {
1233 let left_reach = walk_closure(&mut graph, &[left_oid])?;
1238 let right_reach = walk_closure(&mut graph, &[right_oid])?;
1239 for &oid in &ordered {
1240 let from_left = left_reach.contains(&oid);
1241 let from_right = right_reach.contains(&oid);
1242 left_right_map.insert(oid, from_left && !from_right);
1243 }
1244 }
1245 }
1246
1247 let mut cherry_equivalent = HashSet::new();
1250 if options.cherry_pick || options.cherry_mark {
1251 let left_commits: Vec<_> = ordered
1252 .iter()
1253 .filter(|o| left_right_map.get(o) == Some(&true))
1254 .copied()
1255 .collect();
1256 let right_commits: Vec<_> = ordered
1257 .iter()
1258 .filter(|o| left_right_map.get(o) == Some(&false))
1259 .copied()
1260 .collect();
1261 let left_first = !left_commits.is_empty()
1262 && !right_commits.is_empty()
1263 && left_commits.len() < right_commits.len();
1264
1265 let mut by_patch: HashMap<ObjectId, Vec<ObjectId>> = HashMap::new();
1266 if left_first {
1267 for oid in &left_commits {
1268 if let Ok(Some(pid)) = compute_cherry_patch_id(repo, oid, &options.paths) {
1269 by_patch.entry(pid).or_default().push(*oid);
1270 }
1271 }
1272 for oid in &right_commits {
1273 if let Ok(Some(pid)) = compute_cherry_patch_id(repo, oid, &options.paths) {
1274 if let Some(others) = by_patch.get(&pid) {
1275 cherry_equivalent.insert(*oid);
1276 cherry_equivalent.extend(others.iter().copied());
1277 }
1278 }
1279 }
1280 } else {
1281 for oid in &right_commits {
1282 if let Ok(Some(pid)) = compute_cherry_patch_id(repo, oid, &options.paths) {
1283 by_patch.entry(pid).or_default().push(*oid);
1284 }
1285 }
1286 for oid in &left_commits {
1287 if let Ok(Some(pid)) = compute_cherry_patch_id(repo, oid, &options.paths) {
1288 if let Some(others) = by_patch.get(&pid) {
1289 cherry_equivalent.insert(*oid);
1290 cherry_equivalent.extend(others.iter().copied());
1291 }
1292 }
1293 }
1294 }
1295 }
1296
1297 if options.left_only {
1299 ordered.retain(|oid| left_right_map.get(oid) == Some(&true));
1300 }
1301 if options.right_only {
1302 ordered.retain(|oid| left_right_map.get(oid) == Some(&false));
1303 }
1304
1305 if options.cherry_pick {
1307 ordered.retain(|oid| !cherry_equivalent.contains(oid));
1308 }
1309
1310 if options.until_cutoff.is_some() || options.since_cutoff.is_some() {
1311 let until = options.until_cutoff;
1312 let since = options.since_cutoff;
1313 ordered.retain(|oid| {
1314 let ts = graph.committer_time(*oid);
1315 if let Some(u) = until {
1316 if ts > u {
1317 return false;
1318 }
1319 }
1320 if let Some(s) = since {
1321 if ts < s {
1322 return false;
1323 }
1324 }
1325 true
1326 });
1327 }
1328
1329 if options.simplify_by_decoration && !options.simplify_by_decoration_oids.is_empty() {
1335 let keep = compute_simplify_by_decoration_keep_set(
1336 &mut graph,
1337 &ordered,
1338 &options.simplify_by_decoration_oids,
1339 )?;
1340 ordered.retain(|oid| keep.contains(oid));
1341 }
1342
1343 if options.skip > 0 {
1344 ordered = ordered.into_iter().skip(options.skip).collect();
1345 }
1346 if let Some(max_count) = options.max_count {
1347 ordered.truncate(max_count);
1348 }
1349 if options.reverse {
1350 ordered.reverse();
1351 }
1352
1353 let boundary_commits = if options.boundary {
1355 let included_set: HashSet<ObjectId> = ordered.iter().copied().collect();
1356 let mut boundary = Vec::new();
1357 let mut boundary_seen = HashSet::new();
1358 for &oid in &ordered {
1359 if let Ok(parents) = graph.parents_of(oid).map(|p| p.to_vec()) {
1360 for parent in parents {
1361 if !included_set.contains(&parent) && boundary_seen.insert(parent) {
1362 boundary.push(parent);
1363 }
1364 }
1365 }
1366 }
1367 boundary
1368 } else {
1369 Vec::new()
1370 };
1371
1372 let kept_set = if options.no_kept_objects {
1374 kept_object_ids(repo).unwrap_or_default()
1375 } else {
1376 HashSet::new()
1377 };
1378
1379 if options.no_kept_objects {
1380 ordered.retain(|oid| !kept_set.contains(oid));
1381 }
1382
1383 if options.unpacked_only {
1384 let packed = packed_object_set(repo);
1385 ordered.retain(|oid| !packed.contains(oid));
1386 }
1387
1388 let commit_tips_set: HashSet<ObjectId> = object_walk_tip_commits.iter().copied().collect();
1389 let objects_print_commit: Vec<bool> = if options.objects {
1390 ordered
1391 .iter()
1392 .map(|&c| {
1393 let user_given = !options.filter_provided_objects && commit_tips_set.contains(&c);
1394 object_walk_print_commit_line(
1395 options.filter_provided_objects,
1396 options.filter.as_ref(),
1397 user_given,
1398 )
1399 })
1400 .collect()
1401 } else {
1402 Vec::new()
1403 };
1404
1405 let sparse_lines = sparse_oid_lines_from_filter(repo, options.filter.as_ref())?;
1406 let skip_trees = skip_tree_descent_for_object_type_filter(options.filter.as_ref());
1407 let walk_cfg = ConfigSet::load(Some(&repo.git_dir), true).unwrap_or_default();
1408 let promisor_repo = crate::promisor::repo_treats_promisor_packs(&repo.git_dir, &walk_cfg);
1409 let object_walk_missing_action = if options.objects
1410 && options.missing_action == MissingAction::Error
1411 && (promisor_repo || options.exclude_promisor_objects)
1412 {
1413 MissingAction::Allow
1414 } else {
1415 options.missing_action
1416 };
1417 let bitmap_object_format = options.objects
1422 && options.use_bitmap_index
1423 && (options.bitmap_oid_only_objects
1424 || !object_roots.is_empty()
1425 || options.unpacked_only
1426 || (pack_bitmap_present(repo)
1427 && !filter_forces_bitmap_fallback(options.filter.as_ref())));
1428 let omit_object_paths = bitmap_object_format;
1429 let packed_set = None;
1433
1434 let collect_tree_omits = options.filter_print_omitted;
1436
1437 let (objects, omitted_objects, missing_objects, per_commit_object_counts, object_segments) =
1439 if options.objects {
1440 let excluded_object_ids = excluded_object_root_ids(
1441 repo,
1442 &negative_object_roots,
1443 object_walk_missing_action,
1444 sparse_lines.as_deref(),
1445 skip_trees,
1446 omit_object_paths,
1447 collect_tree_omits,
1448 )?;
1449 let filter_provided = options.filter_provided_objects;
1450 let (mut objs, mut omit, mut miss, mut counts, mut segments) =
1451 if options.in_commit_order {
1452 let (o, om, mi, c) = collect_reachable_objects_in_commit_order(
1453 repo,
1454 &mut graph,
1455 &ordered,
1456 &object_roots,
1457 &tip_annotated_tag_by_commit,
1458 options.filter.as_ref(),
1459 filter_provided,
1460 object_walk_missing_action,
1461 sparse_lines.as_deref(),
1462 skip_trees,
1463 omit_object_paths,
1464 packed_set.as_ref(),
1465 collect_tree_omits,
1466 )?;
1467 (o, om, mi, c, Vec::new())
1468 } else {
1469 let (o, om, mi, seg) = collect_reachable_objects_segmented(
1470 repo,
1471 &mut graph,
1472 &ordered,
1473 &object_roots,
1474 &tip_annotated_tag_by_commit,
1475 options.filter.as_ref(),
1476 filter_provided,
1477 object_walk_missing_action,
1478 sparse_lines.as_deref(),
1479 skip_trees,
1480 omit_object_paths,
1481 packed_set.as_ref(),
1482 collect_tree_omits,
1483 )?;
1484 (o, om, mi, Vec::new(), seg)
1485 };
1486 if !excluded_object_ids.is_empty() {
1487 if !counts.is_empty() {
1488 let mut offset = 0usize;
1489 for count in &mut counts {
1490 let end = offset.saturating_add(*count);
1491 *count = objs[offset..end]
1492 .iter()
1493 .filter(|(oid, _)| !excluded_object_ids.contains(oid))
1494 .count();
1495 offset = end;
1496 }
1497 }
1498 objs.retain(|(oid, _)| !excluded_object_ids.contains(oid));
1499 omit.retain(|oid| !excluded_object_ids.contains(oid));
1500 miss.retain(|oid| !excluded_object_ids.contains(oid));
1501 for segment in &mut segments {
1502 segment.retain(|(oid, _)| !excluded_object_ids.contains(oid));
1503 }
1504 }
1505 if options.no_kept_objects {
1506 objs.retain(|(oid, _)| !kept_set.contains(oid));
1507 }
1508 if options.exclude_promisor_objects {
1509 objs.retain(|(oid, _)| !excluded_promisor.contains(oid));
1510 for segment in &mut segments {
1511 segment.retain(|(oid, _)| !excluded_promisor.contains(oid));
1512 }
1513 }
1514 if !options.paths.is_empty() && !omit_object_paths {
1515 retain_objects_matching_pathspecs(&mut objs, &options.paths);
1516 let mut seen_oids: HashSet<ObjectId> = objs.iter().map(|(oid, _)| *oid).collect();
1517 for (oid, path) in
1518 collect_pathspec_matching_tree_objects(repo, &ordered, &options.paths)?
1519 {
1520 if seen_oids.insert(oid) {
1521 objs.push((oid, path));
1522 }
1523 }
1524 for segment in &mut segments {
1525 retain_objects_matching_pathspecs(segment, &options.paths);
1526 }
1527 if !counts.is_empty() {
1528 counts = segments.iter().map(Vec::len).collect();
1529 }
1530 }
1531 (objs, omit, miss, counts, segments)
1532 } else {
1533 (Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new())
1534 };
1535
1536 let omitted_objects = if omitted_objects.is_empty() {
1537 omitted_objects
1538 } else {
1539 let emitted: HashSet<ObjectId> = objects.iter().map(|(o, _)| *o).collect();
1540 omitted_objects
1541 .into_iter()
1542 .filter(|o| !emitted.contains(o))
1543 .collect::<BTreeSet<_>>()
1544 .into_iter()
1545 .collect()
1546 };
1547
1548 let missing_objects = if traversal_missing.is_empty() {
1549 missing_objects
1550 } else {
1551 let mut seen = HashSet::new();
1552 traversal_missing
1553 .into_iter()
1554 .chain(missing_objects)
1555 .filter(|oid| seen.insert(*oid))
1556 .collect()
1557 };
1558
1559 Ok(RevListResult {
1560 commits: ordered,
1561 objects,
1562 omitted_objects,
1563 missing_objects,
1564 boundary_commits,
1565 left_right_map,
1566 cherry_equivalent,
1567 per_commit_object_counts,
1568 object_walk_tips: object_walk_tip_commits,
1569 objects_print_commit,
1570 object_segments,
1571 bitmap_object_format,
1572 tip_annotated_tag_by_commit,
1573 })
1574}
1575
1576#[must_use]
1583pub fn split_revision_token(token: &str) -> (Vec<String>, Vec<String>) {
1584 if let Some((lhs, rhs)) = crate::rev_parse::split_double_dot_range(token) {
1585 let positive = if rhs.is_empty() {
1586 "HEAD".to_owned()
1587 } else {
1588 rhs.to_owned()
1589 };
1590 let negative = if lhs.is_empty() {
1591 "HEAD".to_owned()
1592 } else {
1593 lhs.to_owned()
1594 };
1595 return (vec![positive], vec![negative]);
1596 }
1597 if let Some(rest) = token.strip_prefix('^') {
1598 return (Vec::new(), vec![rest.to_owned()]);
1599 }
1600 (vec![token.to_owned()], Vec::new())
1601}
1602
1603fn ansi_color_from_name(name: &str) -> String {
1604 match name {
1605 "red" => "\x1b[31m".to_owned(),
1606 "green" => "\x1b[32m".to_owned(),
1607 "yellow" => "\x1b[33m".to_owned(),
1608 "blue" => "\x1b[34m".to_owned(),
1609 "magenta" => "\x1b[35m".to_owned(),
1610 "cyan" => "\x1b[36m".to_owned(),
1611 "white" => "\x1b[37m".to_owned(),
1612 "bold" => "\x1b[1m".to_owned(),
1613 "dim" => "\x1b[2m".to_owned(),
1614 "ul" | "underline" => "\x1b[4m".to_owned(),
1615 "blink" => "\x1b[5m".to_owned(),
1616 "reverse" => "\x1b[7m".to_owned(),
1617 "reset" => "\x1b[m".to_owned(),
1618 _ => String::new(),
1619 }
1620}
1621
1622fn color_name_to_code(name: &str) -> Option<u8> {
1623 match name {
1624 "black" => Some(0),
1625 "red" => Some(1),
1626 "green" => Some(2),
1627 "yellow" => Some(3),
1628 "blue" => Some(4),
1629 "magenta" => Some(5),
1630 "cyan" => Some(6),
1631 "white" => Some(7),
1632 "default" => Some(9),
1633 _ => None,
1634 }
1635}
1636
1637fn ansi_color_from_spec(spec: &str) -> String {
1638 if spec == "reset" {
1639 return "\x1b[m".to_owned();
1640 }
1641 let mut attrs = Vec::new();
1642 let mut fg = None;
1643 let mut bg = None;
1644 for part in spec.split_whitespace() {
1645 match part {
1646 "bold" => attrs.push("1".to_owned()),
1647 "dim" => attrs.push("2".to_owned()),
1648 "italic" => attrs.push("3".to_owned()),
1649 "ul" | "underline" => attrs.push("4".to_owned()),
1650 "blink" => attrs.push("5".to_owned()),
1651 "reverse" => attrs.push("7".to_owned()),
1652 "strike" => attrs.push("9".to_owned()),
1653 "nobold" | "nodim" => attrs.push("22".to_owned()),
1654 "noitalic" => attrs.push("23".to_owned()),
1655 "noul" | "nounderline" => attrs.push("24".to_owned()),
1656 "noblink" => attrs.push("25".to_owned()),
1657 "noreverse" => attrs.push("27".to_owned()),
1658 "nostrike" => attrs.push("29".to_owned()),
1659 _ => {
1660 if let Some(code) = color_name_to_code(part) {
1661 if fg.is_none() {
1662 fg = Some(format!("{}", 30 + code));
1663 } else {
1664 bg = Some(format!("{}", 40 + code));
1665 }
1666 }
1667 }
1668 }
1669 }
1670 let mut codes = attrs;
1671 if let Some(code) = fg {
1672 codes.push(code);
1673 }
1674 if let Some(code) = bg {
1675 codes.push(code);
1676 }
1677 if codes.is_empty() {
1678 String::new()
1679 } else {
1680 format!("\x1b[{}m", codes.join(";"))
1681 }
1682}
1683
1684fn format_relative_date(diff: i64) -> String {
1685 if diff < 0 {
1686 "in the future".to_owned()
1687 } else if diff < 60 {
1688 format!("{} seconds ago", diff)
1689 } else if diff < 3600 {
1690 let m = diff / 60;
1691 if m == 1 {
1692 "1 minute ago".to_owned()
1693 } else {
1694 format!("{m} minutes ago")
1695 }
1696 } else if diff < 86400 {
1697 let h = diff / 3600;
1698 if h == 1 {
1699 "1 hour ago".to_owned()
1700 } else {
1701 format!("{h} hours ago")
1702 }
1703 } else if diff < 86400 * 30 {
1704 let d = diff / 86400;
1705 if d == 1 {
1706 "1 day ago".to_owned()
1707 } else {
1708 format!("{d} days ago")
1709 }
1710 } else if diff < 86400 * 365 {
1711 let months = diff / (86400 * 30);
1712 if months == 1 {
1713 "1 month ago".to_owned()
1714 } else {
1715 format!("{months} months ago")
1716 }
1717 } else {
1718 let years = diff / (86400 * 365);
1719 if years == 1 {
1720 "1 year ago".to_owned()
1721 } else {
1722 format!("{years} years ago")
1723 }
1724 }
1725}
1726
1727pub fn render_commit(
1733 repo: &Repository,
1734 oid: ObjectId,
1735 mode: &OutputMode,
1736 abbrev_len: usize,
1737) -> Result<String> {
1738 render_commit_with_color(repo, oid, mode, abbrev_len, false)
1739}
1740
1741pub fn render_commit_with_color(
1743 repo: &Repository,
1744 oid: ObjectId,
1745 mode: &OutputMode,
1746 abbrev_len: usize,
1747 use_color: bool,
1748) -> Result<String> {
1749 match mode {
1750 OutputMode::OidOnly => Ok(format!("{oid}")),
1751 OutputMode::Parents => {
1752 let mut out = format!("{oid}");
1753 let commit = load_commit(repo, oid)?;
1754 for parent in commit.parents {
1755 out.push(' ');
1756 out.push_str(&parent.to_hex());
1757 }
1758 Ok(out)
1759 }
1760 OutputMode::Format(fmt) => {
1761 let commit = load_commit(repo, oid)?;
1762 let subject = commit.message.lines().next().unwrap_or_default();
1763 let hex = oid.to_hex();
1764
1765 match fmt.as_str() {
1767 "oneline" => {
1768 return Ok(format!("{} {}", hex, subject));
1769 }
1770 "short" => {
1771 fn fmt_ident(ident: &str) -> String {
1772 let name = if let Some(bracket) = ident.find('<') {
1773 ident[..bracket].trim()
1774 } else {
1775 ident.trim()
1776 };
1777 let email = if let Some(start) = ident.find('<') {
1778 if let Some(end) = ident.find('>') {
1779 &ident[start..=end]
1780 } else {
1781 ""
1782 }
1783 } else {
1784 ""
1785 };
1786 format!("{} {}", name, email)
1787 }
1788 let mut out = String::new();
1789 out.push_str(&format!("Author: {}\n", fmt_ident(&commit.author)));
1790 out.push('\n');
1791 out.push_str(&format!(" {}\n", subject));
1792 out.push('\n');
1793 return Ok(out);
1794 }
1795 "medium" => {
1796 fn extract_ident_display(ident: &str) -> String {
1797 let name = if let Some(bracket) = ident.find('<') {
1798 ident[..bracket].trim()
1799 } else {
1800 ident.trim()
1801 };
1802 let email = if let Some(start) = ident.find('<') {
1803 if let Some(end) = ident.find('>') {
1804 &ident[start..=end]
1805 } else {
1806 ""
1807 }
1808 } else {
1809 ""
1810 };
1811 format!("{} {}", name, email)
1812 }
1813 fn format_default_date(ident: &str) -> String {
1814 let parts: Vec<&str> = ident.rsplitn(3, ' ').collect();
1815 if parts.len() < 2 {
1816 return String::new();
1817 }
1818 let ts_str = parts[1];
1819 let offset_str = parts[0];
1820 let ts: i64 = match ts_str.parse() {
1821 Ok(v) => v,
1822 Err(_) => return format!("{ts_str} {offset_str}"),
1823 };
1824 let tz_bytes = offset_str.as_bytes();
1825 let tz_secs: i64 = if tz_bytes.len() >= 5 {
1826 let sign = if tz_bytes[0] == b'-' { -1i64 } else { 1i64 };
1827 let h: i64 = offset_str[1..3].parse().unwrap_or(0);
1828 let m: i64 = offset_str[3..5].parse().unwrap_or(0);
1829 sign * (h * 3600 + m * 60)
1830 } else {
1831 0
1832 };
1833 let adjusted = ts + tz_secs;
1834 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
1835 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
1836 let weekday = match dt.weekday() {
1837 time::Weekday::Monday => "Mon",
1838 time::Weekday::Tuesday => "Tue",
1839 time::Weekday::Wednesday => "Wed",
1840 time::Weekday::Thursday => "Thu",
1841 time::Weekday::Friday => "Fri",
1842 time::Weekday::Saturday => "Sat",
1843 time::Weekday::Sunday => "Sun",
1844 };
1845 let month = match dt.month() {
1846 time::Month::January => "Jan",
1847 time::Month::February => "Feb",
1848 time::Month::March => "Mar",
1849 time::Month::April => "Apr",
1850 time::Month::May => "May",
1851 time::Month::June => "Jun",
1852 time::Month::July => "Jul",
1853 time::Month::August => "Aug",
1854 time::Month::September => "Sep",
1855 time::Month::October => "Oct",
1856 time::Month::November => "Nov",
1857 time::Month::December => "Dec",
1858 };
1859 format!(
1860 "{} {} {} {:02}:{:02}:{:02} {} {}",
1861 weekday,
1862 month,
1863 dt.day(),
1864 dt.hour(),
1865 dt.minute(),
1866 dt.second(),
1867 dt.year(),
1868 offset_str
1869 )
1870 }
1871 let mut out = String::new();
1872 out.push_str(&format!(
1873 "Author: {}\n",
1874 extract_ident_display(&commit.author)
1875 ));
1876 out.push_str(&format!(
1877 "Date: {}\n",
1878 format_default_date(&commit.author)
1879 ));
1880 out.push('\n');
1881 for line in commit.message.lines() {
1882 out.push_str(&format!(" {}\n", line));
1883 }
1884 return Ok(out);
1885 }
1886 _ => {}
1887 }
1888
1889 let raw_fmt = if let Some(t) = fmt.strip_prefix("format:") {
1890 t
1891 } else if let Some(t) = fmt.strip_prefix("tformat:") {
1892 t
1893 } else {
1894 fmt.as_str()
1895 };
1896
1897 let signature: Option<crate::signing::SignatureCheck> = if raw_fmt.contains("%G") {
1900 repo.odb.read(&oid).ok().map(|obj| {
1901 let config = ConfigSet::load(Some(&repo.git_dir), true).unwrap_or_default();
1902 match crate::signing::GpgConfig::from_config(&config) {
1903 Ok(cfg) => crate::signing::verify_commit(&cfg, &obj.data)
1904 .unwrap_or_else(|_| crate::signing::SignatureCheck::default_none()),
1905 Err(_) => crate::signing::SignatureCheck::default_none(),
1906 }
1907 })
1908 } else {
1909 None
1910 };
1911 let body = {
1913 let mut lines = commit.message.lines();
1914 lines.next(); if let Some(blank) = lines.next() {
1917 if blank.is_empty() {
1918 lines.collect::<Vec<_>>().join("\n")
1919 } else {
1920 std::iter::once(blank)
1921 .chain(lines)
1922 .collect::<Vec<_>>()
1923 .join("\n")
1924 }
1925 } else {
1926 String::new()
1927 }
1928 };
1929 let tree_hex = commit.tree.to_hex();
1930 let parent_hexes: Vec<String> = commit.parents.iter().map(|p| p.to_hex()).collect();
1931 let parent_abbrevs: Vec<String> = commit
1932 .parents
1933 .iter()
1934 .map(|p| {
1935 let hex = p.to_hex();
1936 let n = abbrev_len.clamp(4, 40).min(hex.len());
1937 hex[..n].to_string()
1938 })
1939 .collect();
1940
1941 fn extract_name(ident: &str) -> &str {
1943 if let Some(bracket) = ident.find('<') {
1944 ident[..bracket].trim()
1945 } else {
1946 ident.trim()
1947 }
1948 }
1949 fn extract_email(ident: &str) -> &str {
1950 if let Some(start) = ident.find('<') {
1951 if let Some(end) = ident.find('>') {
1952 return &ident[start + 1..end];
1953 }
1954 }
1955 ""
1956 }
1957 fn extract_timestamp(ident: &str) -> String {
1958 match parse_signature_times(ident) {
1959 Some(p) => p.unix_seconds.to_string(),
1960 None => String::new(),
1961 }
1962 }
1963 fn weekday_str(dt: &time::OffsetDateTime) -> &'static str {
1964 match dt.weekday() {
1965 time::Weekday::Monday => "Mon",
1966 time::Weekday::Tuesday => "Tue",
1967 time::Weekday::Wednesday => "Wed",
1968 time::Weekday::Thursday => "Thu",
1969 time::Weekday::Friday => "Fri",
1970 time::Weekday::Saturday => "Sat",
1971 time::Weekday::Sunday => "Sun",
1972 }
1973 }
1974 fn month_str(dt: &time::OffsetDateTime) -> &'static str {
1975 match dt.month() {
1976 time::Month::January => "Jan",
1977 time::Month::February => "Feb",
1978 time::Month::March => "Mar",
1979 time::Month::April => "Apr",
1980 time::Month::May => "May",
1981 time::Month::June => "Jun",
1982 time::Month::July => "Jul",
1983 time::Month::August => "Aug",
1984 time::Month::September => "Sep",
1985 time::Month::October => "Oct",
1986 time::Month::November => "Nov",
1987 time::Month::December => "Dec",
1988 }
1989 }
1990 fn extract_email_local(ident: &str) -> &str {
1991 let email = extract_email(ident);
1992 if let Some(at) = email.find('@') {
1993 &email[..at]
1994 } else {
1995 email
1996 }
1997 }
1998 fn extract_date_default(ident: &str) -> String {
1999 let Some(p) = parse_signature_times(ident) else {
2000 return String::new();
2001 };
2002 let offset_str = ident.get(p.tz_hhmm_range.clone()).unwrap_or("+0000");
2003 let adjusted = p.unix_seconds + p.tz_offset_secs;
2004 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
2005 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
2006 format!(
2007 "{} {} {} {:02}:{:02}:{:02} {} {}",
2008 weekday_str(&dt),
2009 month_str(&dt),
2010 dt.day(),
2011 dt.hour(),
2012 dt.minute(),
2013 dt.second(),
2014 dt.year(),
2015 offset_str
2016 )
2017 }
2018 fn extract_date_rfc2822(ident: &str) -> String {
2019 let Some(p) = parse_signature_times(ident) else {
2020 return String::new();
2021 };
2022 let offset_str = ident.get(p.tz_hhmm_range.clone()).unwrap_or("+0000");
2023 let adjusted = p.unix_seconds + p.tz_offset_secs;
2024 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
2025 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
2026 format!(
2027 "{}, {} {} {} {:02}:{:02}:{:02} {}",
2028 weekday_str(&dt),
2029 dt.day(),
2030 month_str(&dt),
2031 dt.year(),
2032 dt.hour(),
2033 dt.minute(),
2034 dt.second(),
2035 offset_str
2036 )
2037 }
2038 fn extract_date_short(ident: &str) -> String {
2039 let Some(p) = parse_signature_times(ident) else {
2040 return String::new();
2041 };
2042 let adjusted = p.unix_seconds + p.tz_offset_secs;
2043 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
2044 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
2045 format!("{:04}-{:02}-{:02}", dt.year(), dt.month() as u8, dt.day())
2046 }
2047 fn extract_date_iso(ident: &str) -> String {
2048 let Some(p) = parse_signature_times(ident) else {
2049 return String::new();
2050 };
2051 let offset_str = ident.get(p.tz_hhmm_range.clone()).unwrap_or("+0000");
2052 let adjusted = p.unix_seconds + p.tz_offset_secs;
2053 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
2054 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
2055 format!(
2056 "{:04}-{:02}-{:02} {:02}:{:02}:{:02} {}",
2057 dt.year(),
2058 dt.month() as u8,
2059 dt.day(),
2060 dt.hour(),
2061 dt.minute(),
2062 dt.second(),
2063 offset_str
2064 )
2065 }
2066
2067 #[derive(Clone, Copy)]
2069 enum Align {
2070 Left,
2071 Right,
2072 Center,
2073 }
2074 #[derive(Clone, Copy)]
2075 enum Trunc {
2076 None,
2077 Trunc,
2078 LTrunc,
2079 MTrunc,
2080 }
2081 struct ColSpec {
2082 width: usize,
2083 align: Align,
2084 trunc: Trunc,
2085 }
2086 fn apply_col(spec: &ColSpec, s: &str) -> String {
2087 let char_len = s.chars().count();
2088 if char_len > spec.width {
2089 match spec.trunc {
2090 Trunc::None => s.to_owned(),
2091 Trunc::Trunc => {
2092 let mut out: String =
2093 s.chars().take(spec.width.saturating_sub(2)).collect();
2094 out.push_str("..");
2095 out
2096 }
2097 Trunc::LTrunc => {
2098 let skip = char_len - spec.width + 2;
2099 let mut out = String::from("..");
2100 out.extend(s.chars().skip(skip));
2101 out
2102 }
2103 Trunc::MTrunc => {
2104 let keep = spec.width.saturating_sub(2);
2105 let left_half = keep / 2;
2106 let right_half = keep - left_half;
2107 let mut out: String = s.chars().take(left_half).collect();
2108 out.push_str("..");
2109 out.extend(s.chars().skip(char_len - right_half));
2110 out
2111 }
2112 }
2113 } else {
2114 let pad = spec.width - char_len;
2115 match spec.align {
2116 Align::Left => {
2117 let mut out = s.to_owned();
2118 for _ in 0..pad {
2119 out.push(' ');
2120 }
2121 out
2122 }
2123 Align::Right => {
2124 let mut out = String::new();
2125 for _ in 0..pad {
2126 out.push(' ');
2127 }
2128 out.push_str(s);
2129 out
2130 }
2131 Align::Center => {
2132 let left = pad / 2;
2133 let right = pad - left;
2134 let mut out = String::new();
2135 for _ in 0..left {
2136 out.push(' ');
2137 }
2138 out.push_str(s);
2139 for _ in 0..right {
2140 out.push(' ');
2141 }
2142 out
2143 }
2144 }
2145 }
2146 }
2147 fn parse_col_spec(
2148 chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
2149 align: Align,
2150 ) -> Option<ColSpec> {
2151 if chars.peek() != Some(&'(') {
2153 return None;
2154 }
2155 chars.next();
2156 let mut num_str = String::new();
2157 while let Some(&c) = chars.peek() {
2158 if c.is_ascii_digit() {
2159 num_str.push(c);
2160 chars.next();
2161 } else {
2162 break;
2163 }
2164 }
2165 let width: usize = num_str.parse().ok()?;
2166 let trunc = if chars.peek() == Some(&',') {
2167 chars.next(); let mut mode = String::new();
2169 while let Some(&c) = chars.peek() {
2170 if c == ')' {
2171 break;
2172 }
2173 mode.push(c);
2174 chars.next();
2175 }
2176 match mode.as_str() {
2177 "trunc" => Trunc::Trunc,
2178 "ltrunc" => Trunc::LTrunc,
2179 "mtrunc" => Trunc::MTrunc,
2180 _ => Trunc::None,
2181 }
2182 } else {
2183 Trunc::None
2184 };
2185 if chars.peek() == Some(&')') {
2187 chars.next();
2188 }
2189 Some(ColSpec {
2190 width,
2191 align,
2192 trunc,
2193 })
2194 }
2195
2196 let mut pending_col: Option<ColSpec> = None;
2197 let mut rendered = String::new();
2198 let mut chars = raw_fmt.chars().peekable();
2199 while let Some(ch) = chars.next() {
2200 if ch != '%' {
2201 rendered.push(ch);
2202 continue;
2203 }
2204 if chars.peek() == Some(&'<') {
2206 chars.next();
2207 if let Some(spec) = parse_col_spec(&mut chars, Align::Left) {
2208 pending_col = Some(spec);
2209 }
2210 continue;
2211 }
2212 if chars.peek() == Some(&'>') {
2213 chars.next();
2214 if chars.peek() == Some(&'<') {
2215 chars.next(); if let Some(spec) = parse_col_spec(&mut chars, Align::Center) {
2217 pending_col = Some(spec);
2218 }
2219 } else if chars.peek() == Some(&'>') {
2220 chars.next(); if let Some(spec) = parse_col_spec(&mut chars, Align::Right) {
2222 pending_col = Some(spec);
2223 }
2224 } else if let Some(spec) = parse_col_spec(&mut chars, Align::Right) {
2225 pending_col = Some(spec);
2226 }
2227 continue;
2228 }
2229
2230 let mut expanded = String::new();
2232 let target = if pending_col.is_some() {
2233 &mut expanded
2234 } else {
2235 &mut rendered
2236 };
2237 match chars.peek() {
2238 Some('%') => {
2239 chars.next();
2240 target.push('%');
2241 }
2242 Some('H') => {
2243 chars.next();
2244 target.push_str(&oid.to_hex());
2245 }
2246 Some('h') => {
2247 chars.next();
2248 let hex = oid.to_hex();
2249 let n = abbrev_len.clamp(4, 40).min(hex.len());
2250 target.push_str(&hex[..n]);
2251 }
2252 Some('T') => {
2253 chars.next();
2254 target.push_str(&tree_hex);
2255 }
2256 Some('t') => {
2257 chars.next();
2258 let n = abbrev_len.clamp(4, 40).min(tree_hex.len());
2259 target.push_str(&tree_hex[..n]);
2260 }
2261 Some('P') => {
2262 chars.next();
2263 target.push_str(&parent_hexes.join(" "));
2264 }
2265 Some('p') => {
2266 chars.next();
2267 target.push_str(&parent_abbrevs.join(" "));
2268 }
2269 Some('n') => {
2270 chars.next();
2271 target.push('\n');
2272 }
2273 Some('s') => {
2274 chars.next();
2275 target.push_str(subject);
2276 }
2277 Some('b') => {
2278 chars.next();
2279 target.push_str(&body);
2280 if !body.is_empty() {
2281 target.push('\n');
2282 }
2283 }
2284 Some('B') => {
2285 chars.next();
2286 target.push_str(&commit.message);
2287 }
2288 Some('a') => {
2289 chars.next();
2290 match chars.next() {
2291 Some('n') => target.push_str(extract_name(&commit.author)),
2292 Some('N') => target.push_str(extract_name(&commit.author)),
2293 Some('e') => target.push_str(extract_email(&commit.author)),
2294 Some('E') => target.push_str(extract_email(&commit.author)),
2295 Some('l') => target.push_str(extract_email_local(&commit.author)),
2296 Some('d') => target.push_str(&extract_date_default(&commit.author)),
2297 Some('D') => target.push_str(&extract_date_rfc2822(&commit.author)),
2298 Some('t') => target.push_str(&extract_timestamp(&commit.author)),
2299 Some('s') => target.push_str(&extract_date_short(&commit.author)),
2300 Some('i') => target.push_str(&extract_date_iso(&commit.author)),
2301 Some('I') => {
2302 let Some(p) = parse_signature_times(&commit.author) else {
2303 break;
2304 };
2305 let adjusted = p.unix_seconds + p.tz_offset_secs;
2306 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
2307 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
2308 let sign_ch = if p.tz_offset_secs >= 0 { '+' } else { '-' };
2309 let abs_off = p.tz_offset_secs.unsigned_abs();
2310 target.push_str(&format!(
2311 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}{:02}:{:02}",
2312 dt.year(),
2313 dt.month() as u8,
2314 dt.day(),
2315 dt.hour(),
2316 dt.minute(),
2317 dt.second(),
2318 sign_ch,
2319 abs_off / 3600,
2320 (abs_off % 3600) / 60
2321 ));
2322 }
2323 Some('r') => {
2324 let Some(p) = parse_signature_times(&commit.author) else {
2325 break;
2326 };
2327 let now = std::time::SystemTime::now()
2328 .duration_since(std::time::UNIX_EPOCH)
2329 .unwrap_or_default()
2330 .as_secs() as i64;
2331 target.push_str(&format_relative_date(now - p.unix_seconds));
2332 }
2333 Some(other) => {
2334 target.push('%');
2335 target.push('a');
2336 target.push(other);
2337 }
2338 None => {
2339 target.push('%');
2340 target.push('a');
2341 }
2342 }
2343 }
2344 Some('c') => {
2345 chars.next();
2346 match chars.next() {
2347 Some('n') => target.push_str(extract_name(&commit.committer)),
2348 Some('N') => target.push_str(extract_name(&commit.committer)),
2349 Some('e') => target.push_str(extract_email(&commit.committer)),
2350 Some('E') => target.push_str(extract_email(&commit.committer)),
2351 Some('l') => target.push_str(extract_email_local(&commit.committer)),
2352 Some('d') => target.push_str(&extract_date_default(&commit.committer)),
2353 Some('D') => target.push_str(&extract_date_rfc2822(&commit.committer)),
2354 Some('t') => target.push_str(&extract_timestamp(&commit.committer)),
2355 Some('s') => target.push_str(&extract_date_short(&commit.committer)),
2356 Some('i') => target.push_str(&extract_date_iso(&commit.committer)),
2357 Some('I') => {
2358 let Some(p) = parse_signature_times(&commit.committer) else {
2359 break;
2360 };
2361 let adjusted = p.unix_seconds + p.tz_offset_secs;
2362 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
2363 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
2364 let sign_ch = if p.tz_offset_secs >= 0 { '+' } else { '-' };
2365 let abs_off = p.tz_offset_secs.unsigned_abs();
2366 target.push_str(&format!(
2367 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}{:02}:{:02}",
2368 dt.year(),
2369 dt.month() as u8,
2370 dt.day(),
2371 dt.hour(),
2372 dt.minute(),
2373 dt.second(),
2374 sign_ch,
2375 abs_off / 3600,
2376 (abs_off % 3600) / 60
2377 ));
2378 }
2379 Some('r') => {
2380 let Some(p) = parse_signature_times(&commit.committer) else {
2381 break;
2382 };
2383 let now = std::time::SystemTime::now()
2384 .duration_since(std::time::UNIX_EPOCH)
2385 .unwrap_or_default()
2386 .as_secs() as i64;
2387 target.push_str(&format_relative_date(now - p.unix_seconds));
2388 }
2389 Some(other) => {
2390 target.push('%');
2391 target.push('c');
2392 target.push(other);
2393 }
2394 None => {
2395 target.push('%');
2396 target.push('c');
2397 }
2398 }
2399 }
2400 Some('x') => {
2401 chars.next();
2403 let mut hex_str = String::new();
2404 if let Some(&c1) = chars.peek() {
2405 if c1.is_ascii_hexdigit() {
2406 hex_str.push(c1);
2407 chars.next();
2408 }
2409 }
2410 if let Some(&c2) = chars.peek() {
2411 if c2.is_ascii_hexdigit() {
2412 hex_str.push(c2);
2413 chars.next();
2414 }
2415 }
2416 if let Ok(byte) = u8::from_str_radix(&hex_str, 16) {
2417 target.push(byte as char);
2418 }
2419 }
2420 Some('C') => {
2421 chars.next();
2422 if chars.peek() == Some(&'(') {
2423 chars.next();
2424 let mut spec = String::new();
2425 for c in chars.by_ref() {
2426 if c == ')' {
2427 break;
2428 }
2429 spec.push(c);
2430 }
2431 let (force, color_spec) =
2432 if let Some(rest) = spec.strip_prefix("always,") {
2433 (true, rest)
2434 } else if let Some(rest) = spec.strip_prefix("auto,") {
2435 (false, rest)
2436 } else if spec == "auto" {
2437 if use_color {
2438 target.push_str("\x1b[m");
2439 }
2440 continue;
2441 } else {
2442 (false, spec.as_str())
2443 };
2444 if use_color || force {
2445 target.push_str(&ansi_color_from_spec(color_spec));
2446 }
2447 } else {
2448 let remaining: String = chars.clone().collect();
2451 let known = [
2452 "reset", "red", "green", "blue", "yellow", "magenta", "cyan",
2453 "white", "bold", "dim", "ul",
2454 ];
2455 let mut matched = false;
2456 for name in &known {
2457 if remaining.starts_with(name) {
2458 for _ in 0..name.len() {
2459 chars.next();
2460 }
2461 if use_color {
2462 target.push_str(&ansi_color_from_name(name));
2463 }
2464 matched = true;
2465 break;
2466 }
2467 }
2468 if !matched {
2469 while let Some(&c) = chars.peek() {
2471 if c.is_alphanumeric() {
2472 chars.next();
2473 } else {
2474 break;
2475 }
2476 }
2477 }
2478 }
2479 }
2480 Some('w') => {
2481 chars.next();
2483 if chars.peek() == Some(&'(') {
2484 chars.next();
2485 for c in chars.by_ref() {
2486 if c == ')' {
2487 break;
2488 }
2489 }
2490 }
2491 }
2492 Some('+') => {
2493 chars.next();
2495 if chars.peek() == Some(&'%') {
2497 }
2501 let mut sub = String::new();
2503 if let Some(&nc) = chars.peek() {
2504 match nc {
2505 'b' => {
2506 chars.next();
2507 sub.push_str(&body);
2508 if !body.is_empty() {
2509 sub.push('\n');
2510 }
2511 }
2512 's' => {
2513 chars.next();
2514 sub.push_str(subject);
2515 }
2516 _ => {
2517 chars.next();
2518 sub.push('%');
2519 sub.push('+');
2520 sub.push(nc);
2521 }
2522 }
2523 }
2524 if !sub.is_empty() {
2525 target.push('\n');
2526 target.push_str(&sub);
2527 }
2528 }
2529 Some('-') => {
2530 chars.next();
2532 if let Some(&nc) = chars.peek() {
2534 match nc {
2535 'b' => {
2536 chars.next();
2537 if !body.is_empty() {
2538 target.push_str(&body);
2539 target.push('\n');
2540 }
2541 }
2542 's' => {
2543 chars.next();
2544 target.push_str(subject);
2545 }
2546 _ => {
2547 chars.next();
2548 target.push('%');
2549 target.push('-');
2550 target.push(nc);
2551 }
2552 }
2553 }
2554 }
2555 Some('d') => {
2556 chars.next();
2558 }
2559 Some('D') => {
2560 chars.next();
2562 }
2563 Some('e') => {
2564 chars.next();
2566 if let Some(encoding) = &commit.encoding {
2567 target.push_str(encoding);
2568 }
2569 }
2570 Some('g') => {
2571 chars.next();
2573 if let Some(&_nc) = chars.peek() {
2574 chars.next(); }
2577 }
2578 Some('G') => {
2579 use crate::signing::{SignatureCheck, TrustLevel};
2583 let default_sig;
2584 let sig: &SignatureCheck = match &signature {
2585 Some(s) => s,
2586 None => {
2587 default_sig = SignatureCheck::default_none();
2588 &default_sig
2589 }
2590 };
2591 let mut lookahead = chars.clone();
2592 lookahead.next(); let sub = lookahead.peek().copied();
2594 let handled = match sub {
2595 Some('?') => {
2596 let ch = if sig.result == 'G'
2597 && matches!(
2598 sig.trust_level,
2599 TrustLevel::Undefined | TrustLevel::Never
2600 ) {
2601 'U'
2602 } else {
2603 sig.result
2604 };
2605 target.push(ch);
2606 true
2607 }
2608 Some('S') => {
2609 target.push_str(sig.signer.as_deref().unwrap_or(""));
2610 true
2611 }
2612 Some('K') => {
2613 target.push_str(sig.key.as_deref().unwrap_or(""));
2614 true
2615 }
2616 Some('F') => {
2617 target.push_str(sig.fingerprint.as_deref().unwrap_or(""));
2618 true
2619 }
2620 Some('P') => {
2621 target
2622 .push_str(sig.primary_key_fingerprint.as_deref().unwrap_or(""));
2623 true
2624 }
2625 Some('T') => {
2626 target.push_str(sig.trust_level.display_key());
2627 true
2628 }
2629 Some('G') => {
2630 target.push_str(&sig.output);
2631 true
2632 }
2633 _ => false,
2634 };
2635 if handled {
2636 chars.next(); chars.next(); } else {
2639 target.push('%');
2640 }
2641 }
2642 Some(&other) => {
2643 chars.next();
2644 target.push('%');
2645 target.push(other);
2646 }
2647 None => target.push('%'),
2648 }
2649 if let Some(spec) = pending_col.take() {
2651 let formatted = apply_col(&spec, &expanded);
2652 rendered.push_str(&formatted);
2653 }
2654 }
2655 Ok(rendered)
2656 }
2657 }
2658}
2659
2660#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2661enum ExpectedObjectKind {
2662 Commit,
2663 Tree,
2664 Blob,
2665 Tag,
2666}
2667
2668impl ExpectedObjectKind {
2669 fn from_object_kind(kind: ObjectKind) -> Option<Self> {
2670 match kind {
2671 ObjectKind::Commit => Some(Self::Commit),
2672 ObjectKind::Tree => Some(Self::Tree),
2673 ObjectKind::Blob => Some(Self::Blob),
2674 ObjectKind::Tag => Some(Self::Tag),
2675 }
2676 }
2677
2678 fn from_tag_type(kind: &str) -> Option<Self> {
2679 match kind {
2680 "commit" => Some(Self::Commit),
2681 "tree" => Some(Self::Tree),
2682 "blob" => Some(Self::Blob),
2683 "tag" => Some(Self::Tag),
2684 _ => None,
2685 }
2686 }
2687
2688 fn matches(self, kind: ObjectKind) -> bool {
2689 matches!(
2690 (self, kind),
2691 (Self::Commit, ObjectKind::Commit)
2692 | (Self::Tree, ObjectKind::Tree)
2693 | (Self::Blob, ObjectKind::Blob)
2694 | (Self::Tag, ObjectKind::Tag)
2695 )
2696 }
2697
2698 fn as_str(self) -> &'static str {
2699 match self {
2700 Self::Commit => "commit",
2701 Self::Tree => "tree",
2702 Self::Blob => "blob",
2703 Self::Tag => "tag",
2704 }
2705 }
2706}
2707
2708#[derive(Clone, Debug)]
2710pub struct ObjectWalkRoot {
2711 pub oid: ObjectId,
2713 pub input: String,
2714 pub root_path: Option<String>,
2716}
2717
2718#[derive(Clone, Debug)]
2719struct RootObject {
2720 oid: ObjectId,
2721 input: String,
2722 expected_kind: Option<ExpectedObjectKind>,
2723 root_path: Option<String>,
2724 wrap_with_tag: Option<ObjectId>,
2726}
2727
2728fn indexed_object_roots(repo: &Repository) -> Result<Vec<RootObject>> {
2729 let Some(_) = &repo.work_tree else {
2730 return Ok(Vec::new());
2731 };
2732 let index_path = repo.git_dir.join("index");
2733 if !index_path.is_file() {
2734 return Ok(Vec::new());
2735 }
2736
2737 let idx = Index::load(&index_path)?;
2738 let mut roots = Vec::new();
2739 for e in &idx.entries {
2740 if e.stage() != 0 || e.mode == MODE_GITLINK || e.mode == MODE_TREE {
2741 continue;
2742 }
2743 let path_str = String::from_utf8_lossy(&e.path).into_owned();
2744 roots.push(RootObject {
2745 oid: e.oid,
2746 input: format!(":{path_str}"),
2747 expected_kind: Some(ExpectedObjectKind::Blob),
2748 root_path: Some(path_str),
2749 wrap_with_tag: None,
2750 });
2751 }
2752
2753 if let Some(cache_tree) = &idx.cache_tree {
2754 push_index_cache_tree_child_roots(&mut roots, cache_tree, "");
2755 }
2756
2757 Ok(roots)
2758}
2759
2760fn push_index_cache_tree_child_roots(
2761 roots: &mut Vec<RootObject>,
2762 node: &CacheTreeNode,
2763 parent_path: &str,
2764) {
2765 for child in &node.children {
2766 let name = String::from_utf8_lossy(&child.name);
2767 let path = if parent_path.is_empty() {
2768 name.into_owned()
2769 } else {
2770 format!("{parent_path}/{name}")
2771 };
2772
2773 if child.entry_count >= 0 {
2774 if let Some(oid) = child.oid {
2775 roots.push(RootObject {
2776 oid,
2777 input: format!(":{path}"),
2778 expected_kind: Some(ExpectedObjectKind::Tree),
2779 root_path: Some(path.clone()),
2780 wrap_with_tag: None,
2781 });
2782 }
2783 }
2784
2785 push_index_cache_tree_child_roots(roots, child, &path);
2786 }
2787}
2788
2789fn object_walk_print_commit_line(
2790 filter_provided_objects: bool,
2791 filter: Option<&ObjectFilter>,
2792 user_given_tip: bool,
2793) -> bool {
2794 if !filter_provided_objects {
2795 return user_given_tip || filter_shows_commit_line_when_not_user_given(filter);
2796 }
2797 match filter {
2798 Some(ObjectFilter::ObjectType(FilterObjectKind::Commit)) => {
2799 user_given_tip || filter_shows_commit_line_when_not_user_given(filter)
2800 }
2801 Some(ObjectFilter::ObjectType(_)) => false,
2802 Some(ObjectFilter::Combine(parts)) => {
2803 if parts
2804 .iter()
2805 .all(|p| matches!(p, ObjectFilter::ObjectType(FilterObjectKind::Commit)))
2806 {
2807 user_given_tip || filter_shows_commit_line_when_not_user_given(filter)
2808 } else {
2809 false
2810 }
2811 }
2812 _ => user_given_tip || filter_shows_commit_line_when_not_user_given(filter),
2813 }
2814}
2815
2816fn filter_shows_commit_line_when_not_user_given(filter: Option<&ObjectFilter>) -> bool {
2818 match filter {
2819 None => true,
2820 Some(ObjectFilter::BlobNone)
2821 | Some(ObjectFilter::BlobLimit(_))
2822 | Some(ObjectFilter::TreeDepth(_))
2823 | Some(ObjectFilter::SparseOid(_)) => true,
2824 Some(ObjectFilter::ObjectType(FilterObjectKind::Commit)) => true,
2825 Some(ObjectFilter::ObjectType(
2826 FilterObjectKind::Blob | FilterObjectKind::Tree | FilterObjectKind::Tag,
2827 )) => false,
2828 Some(ObjectFilter::Combine(parts)) => parts
2829 .iter()
2830 .all(|p| filter_shows_commit_line_when_not_user_given(Some(p))),
2831 }
2832}
2833
2834fn skip_tree_descent_for_object_type_filter(filter: Option<&ObjectFilter>) -> bool {
2835 match filter {
2836 Some(ObjectFilter::ObjectType(FilterObjectKind::Commit | FilterObjectKind::Tag)) => true,
2837 Some(ObjectFilter::Combine(parts)) => parts
2838 .iter()
2839 .any(|p| skip_tree_descent_for_object_type_filter(Some(p))),
2840 _ => false,
2841 }
2842}
2843
2844fn sparse_filter_includes_path(
2845 repo: &Repository,
2846 path: &str,
2847 sparse_lines: Option<&[String]>,
2848) -> bool {
2849 sparse_lines
2850 .map(|lines| path_in_sparse_checkout(path, lines, repo.work_tree.as_deref()))
2851 .unwrap_or(true)
2852}
2853
2854fn sparse_oid_lines_from_filter(
2855 repo: &Repository,
2856 filter: Option<&ObjectFilter>,
2857) -> Result<Option<Vec<String>>> {
2858 let Some(f) = filter else {
2859 return Ok(None);
2860 };
2861 match f {
2862 ObjectFilter::SparseOid(spec) => {
2863 let blob_oid = if let Ok(oid) = spec.parse::<ObjectId>() {
2867 oid
2868 } else if let Some((treeish, path)) = split_treeish_spec(spec) {
2869 let treeish_oid = resolve_revision_for_range_end(repo, treeish)
2870 .map_err(|_| sparse_blob_access_error(spec))?;
2871 resolve_treeish_path(repo, treeish_oid, path)
2872 .map_err(|_| sparse_blob_access_error(spec))?
2873 } else {
2874 resolve_revision_for_range_end(repo, spec)
2879 .map_err(|_| sparse_blob_access_error(spec))?
2880 };
2881 let obj = repo
2882 .odb
2883 .read(&blob_oid)
2884 .map_err(|_| sparse_blob_access_error(spec))?;
2885 if obj.kind != ObjectKind::Blob {
2888 return Err(Error::Message(format!(
2889 "fatal: unable to parse sparse filter data in {}",
2890 blob_oid.to_hex()
2891 )));
2892 }
2893 let text = std::str::from_utf8(&obj.data).map_err(|_| {
2894 Error::Message(format!(
2895 "fatal: unable to parse sparse filter data in {}",
2896 blob_oid.to_hex()
2897 ))
2898 })?;
2899 Ok(Some(parse_sparse_patterns_from_blob(text)))
2900 }
2901 ObjectFilter::Combine(parts) => {
2902 for p in parts {
2903 if let Some(lines) = sparse_oid_lines_from_filter(repo, Some(p))? {
2904 return Ok(Some(lines));
2905 }
2906 }
2907 Ok(None)
2908 }
2909 _ => Ok(None),
2910 }
2911}
2912
2913fn sparse_blob_access_error(spec: &str) -> Error {
2915 Error::Message(format!("fatal: unable to access sparse blob in '{spec}'"))
2916}
2917
2918fn packed_object_set(repo: &Repository) -> HashSet<ObjectId> {
2919 let mut out = HashSet::new();
2920 let objects_dir = repo.odb.objects_dir();
2921 if let Ok(indexes) = pack::read_local_pack_indexes(objects_dir) {
2922 for idx in indexes {
2923 for e in idx.entries {
2924 if let Ok(oid) = ObjectId::from_bytes(&e.oid) {
2925 out.insert(oid);
2926 }
2927 }
2928 }
2929 }
2930 out
2931}
2932
2933fn resolve_specs(repo: &Repository, specs: &[String]) -> Result<Vec<ObjectId>> {
2934 resolve_specs_with_options(repo, specs, false)
2935}
2936
2937fn resolve_specs_with_options(
2938 repo: &Repository,
2939 specs: &[String],
2940 ignore_missing: bool,
2941) -> Result<Vec<ObjectId>> {
2942 let mut out = Vec::with_capacity(specs.len());
2943 for spec in specs {
2944 let oid = match resolve_revision_for_range_end(repo, spec) {
2945 Ok(oid) => oid,
2946 Err(Error::ObjectNotFound(_) | Error::InvalidRef(_)) if ignore_missing => continue,
2947 Err(err) => return Err(err),
2948 };
2949 match peel_to_commit(repo, oid) {
2950 Ok(commit_oid) => out.push(commit_oid),
2951 Err(Error::CorruptObject(_)) if object_peels_to_non_commit(repo, oid) => {}
2958 Err(Error::ObjectNotFound(_) | Error::InvalidRef(_)) if ignore_missing => {}
2959 Err(err) => return Err(err),
2960 }
2961 }
2962 Ok(out)
2963}
2964
2965pub fn resolve_revision_commits(repo: &Repository, specs: &[String]) -> Result<Vec<ObjectId>> {
2970 resolve_specs(repo, specs)
2971}
2972
2973pub fn resolve_revision_specs_to_commits(
2979 repo: &Repository,
2980 specs: &[String],
2981) -> Result<Vec<ObjectId>> {
2982 resolve_specs(repo, specs)
2983}
2984
2985pub fn resolve_object_walk_roots(
2986 repo: &Repository,
2987 specs: &[String],
2988) -> Result<(Vec<ObjectId>, Vec<ObjectWalkRoot>)> {
2989 let (commits, roots, _tip_annotated_tag_by_commit) = resolve_specs_for_objects(repo, specs)?;
2990 Ok((
2991 commits,
2992 roots
2993 .into_iter()
2994 .map(|r| ObjectWalkRoot {
2995 oid: r.oid,
2996 input: r.input,
2997 root_path: r.root_path,
2998 })
2999 .collect(),
3000 ))
3001}
3002
3003fn resolve_specs_for_objects(
3004 repo: &Repository,
3005 specs: &[String],
3006) -> Result<(Vec<ObjectId>, Vec<RootObject>, HashMap<ObjectId, ObjectId>)> {
3007 resolve_specs_for_objects_with_options(repo, specs, false, MissingAction::Error)
3008}
3009
3010fn resolve_specs_for_objects_with_options(
3011 repo: &Repository,
3012 specs: &[String],
3013 ignore_missing: bool,
3014 missing_action: MissingAction,
3015) -> Result<(Vec<ObjectId>, Vec<RootObject>, HashMap<ObjectId, ObjectId>)> {
3016 let mut commits = Vec::new();
3017 let mut roots = Vec::new();
3018 let mut tip_annotated_tag_by_commit: HashMap<ObjectId, ObjectId> = HashMap::new();
3019
3020 for spec in specs {
3021 if let Ok(raw_oid) = spec.parse::<ObjectId>() {
3022 let raw_object = match repo.odb.read(&raw_oid) {
3023 Ok(obj) => obj,
3024 Err(Error::ObjectNotFound(_)) if ignore_missing => continue,
3025 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
3026 roots.push(RootObject {
3027 oid: raw_oid,
3028 input: spec.clone(),
3029 expected_kind: None,
3030 root_path: None,
3031 wrap_with_tag: None,
3032 });
3033 continue;
3034 }
3035 Err(err) => return Err(err),
3036 };
3037 match raw_object.kind {
3038 ObjectKind::Commit => {
3039 commits.push(raw_oid);
3040 }
3041 ObjectKind::Tag => {
3042 let tag = parse_tag(&raw_object.data)?;
3043 let expected_kind = ExpectedObjectKind::from_tag_type(&tag.object_type)
3044 .ok_or_else(|| {
3045 Error::CorruptObject(format!(
3046 "object {spec} has unsupported tag type '{}'",
3047 tag.object_type
3048 ))
3049 })?;
3050 if expected_kind == ExpectedObjectKind::Commit {
3051 tip_annotated_tag_by_commit.insert(tag.object, raw_oid);
3057 commits.push(tag.object);
3058 } else {
3059 roots.push(RootObject {
3060 oid: tag.object,
3061 input: spec.clone(),
3062 expected_kind: Some(expected_kind),
3063 root_path: None,
3064 wrap_with_tag: Some(raw_oid),
3065 });
3066 }
3067 }
3068 ObjectKind::Tree | ObjectKind::Blob => roots.push(RootObject {
3069 oid: raw_oid,
3070 input: spec.clone(),
3071 expected_kind: None,
3072 root_path: None,
3073 wrap_with_tag: None,
3074 }),
3075 }
3076 continue;
3077 }
3078
3079 if let Some((treeish, path)) = split_treeish_spec(spec) {
3080 if !path.is_empty() {
3081 let treeish_oid = match resolve_revision_for_range_end(repo, treeish) {
3082 Ok(oid) => oid,
3083 Err(Error::ObjectNotFound(_) | Error::InvalidRef(_)) if ignore_missing => {
3084 continue;
3085 }
3086 Err(err) => return Err(err),
3087 };
3088 let object_oid = match resolve_treeish_path(repo, treeish_oid, path) {
3089 Ok(oid) => oid,
3090 Err(Error::ObjectNotFound(_) | Error::InvalidRef(_)) if ignore_missing => {
3091 continue;
3092 }
3093 Err(err) => return Err(err),
3094 };
3095 let expected_kind = match repo.odb.read(&object_oid) {
3096 Ok(object) => ExpectedObjectKind::from_object_kind(object.kind),
3097 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => None,
3098 Err(Error::ObjectNotFound(_)) if ignore_missing => continue,
3099 Err(err) => return Err(err),
3100 };
3101 roots.push(RootObject {
3102 oid: object_oid,
3103 input: spec.clone(),
3104 expected_kind,
3105 root_path: Some(path.to_owned()),
3106 wrap_with_tag: None,
3107 });
3108 continue;
3109 }
3110 }
3111
3112 let oid = match resolve_revision_for_range_end(repo, spec) {
3113 Ok(oid) => oid,
3114 Err(Error::ObjectNotFound(_) | Error::InvalidRef(_)) if ignore_missing => continue,
3115 Err(err) => return Err(err),
3116 };
3117 if let Ok(obj) = repo.odb.read(&oid) {
3118 if obj.kind == ObjectKind::Tag {
3119 if let Ok(commit_oid) = peel_to_commit(repo, oid) {
3120 tip_annotated_tag_by_commit.insert(commit_oid, oid);
3121 }
3122 }
3123 }
3124 match peel_to_commit(repo, oid) {
3125 Ok(commit_oid) => commits.push(commit_oid),
3126 Err(Error::CorruptObject(_)) if ignore_missing => {}
3127 Err(Error::ObjectNotFound(_)) if ignore_missing => {}
3128 Err(Error::CorruptObject(_)) | Err(Error::ObjectNotFound(_)) => {
3129 roots.push(RootObject {
3130 oid,
3131 input: spec.clone(),
3132 expected_kind: None,
3133 root_path: None,
3134 wrap_with_tag: None,
3135 })
3136 }
3137 Err(err) => return Err(err),
3138 }
3139 }
3140
3141 Ok((commits, roots, tip_annotated_tag_by_commit))
3142}
3143
3144fn object_peels_to_non_commit(repo: &Repository, mut oid: ObjectId) -> bool {
3148 loop {
3149 let Ok(object) = repo.odb.read(&oid) else {
3150 return false;
3151 };
3152 match object.kind {
3153 ObjectKind::Commit => return false,
3154 ObjectKind::Blob | ObjectKind::Tree => return true,
3155 ObjectKind::Tag => match parse_tag(&object.data) {
3156 Ok(tag) => oid = tag.object,
3157 Err(_) => return false,
3158 },
3159 }
3160 }
3161}
3162
3163fn peel_to_commit(repo: &Repository, mut oid: ObjectId) -> Result<ObjectId> {
3165 loop {
3166 let object = repo.odb.read(&oid)?;
3167 match object.kind {
3168 ObjectKind::Commit => return Ok(oid),
3169 ObjectKind::Tag => {
3170 let tag = parse_tag(&object.data)?;
3171 oid = tag.object;
3172 }
3173 other => {
3174 return Err(Error::CorruptObject(format!(
3175 "object {oid} is a {other:?}, not a commit"
3176 )));
3177 }
3178 }
3179 }
3180}
3181
3182fn reflog_commit_tips(repo: &Repository) -> Result<Vec<ObjectId>> {
3183 let z = zero_oid();
3184 let mut out = Vec::new();
3185 let mut seen = HashSet::new();
3186 for refname in list_reflog_refs(&repo.git_dir)? {
3187 let entries = read_reflog(&repo.git_dir, &refname)?;
3188 for e in entries {
3189 for oid in [e.old_oid, e.new_oid] {
3190 if oid == z {
3191 continue;
3192 }
3193 match peel_to_commit(repo, oid) {
3194 Ok(c) if seen.insert(c) => out.push(c),
3195 Err(_) => {}
3196 _ => {}
3197 }
3198 }
3199 }
3200 }
3201 Ok(out)
3202}
3203
3204pub(crate) fn walk_closure(
3205 graph: &mut CommitGraph<'_>,
3206 starts: &[ObjectId],
3207) -> Result<HashSet<ObjectId>> {
3208 let (seen, _) = walk_closure_ordered(graph, starts)?;
3209 Ok(seen)
3210}
3211
3212fn walk_closure_first_parent_only(
3215 graph: &mut CommitGraph<'_>,
3216 starts: &[ObjectId],
3217) -> Result<HashSet<ObjectId>> {
3218 let mut seen = HashSet::new();
3219 let mut queue = VecDeque::new();
3220 for &start in starts {
3221 queue.push_back(start);
3222 }
3223 while let Some(oid) = queue.pop_front() {
3224 if !seen.insert(oid) {
3225 continue;
3226 }
3227 let parents = graph.parents_of(oid)?;
3228 if let Some(&p) = parents.first() {
3229 queue.push_back(p);
3230 }
3231 }
3232 Ok(seen)
3233}
3234
3235pub(crate) fn walk_closure_ordered(
3237 graph: &mut CommitGraph<'_>,
3238 starts: &[ObjectId],
3239) -> Result<(HashSet<ObjectId>, Vec<ObjectId>)> {
3240 walk_closure_ordered_excluding(graph, starts, &HashSet::new())
3241}
3242
3243fn walk_closure_ordered_excluding(
3244 graph: &mut CommitGraph<'_>,
3245 starts: &[ObjectId],
3246 excluded: &HashSet<ObjectId>,
3247) -> Result<(HashSet<ObjectId>, Vec<ObjectId>)> {
3248 let mut seen = HashSet::new();
3249 let mut order = Vec::new();
3250 let mut queue = VecDeque::new();
3251 for &start in starts {
3252 queue.push_back(start);
3253 }
3254 while let Some(oid) = queue.pop_front() {
3255 if excluded.contains(&oid) {
3256 continue;
3257 }
3258 if !seen.insert(oid) {
3259 continue;
3260 }
3261 order.push(oid);
3262 for parent in graph.parents_of(oid)? {
3263 queue.push_back(parent);
3264 }
3265 }
3266 Ok((seen, order))
3267}
3268
3269fn walk_closure_ordered_with_missing(
3270 graph: &mut CommitGraph<'_>,
3271 starts: &[ObjectId],
3272 excluded: &HashSet<ObjectId>,
3273 missing_action: MissingAction,
3274 missing: &mut Vec<ObjectId>,
3275 missing_seen: &mut HashSet<ObjectId>,
3276) -> Result<(HashSet<ObjectId>, Vec<ObjectId>)> {
3277 let mut seen = HashSet::new();
3278 let mut order = Vec::new();
3279 let mut queue = VecDeque::new();
3280 for &start in starts {
3281 queue.push_back(start);
3282 }
3283 while let Some(oid) = queue.pop_front() {
3284 if excluded.contains(&oid) || seen.contains(&oid) {
3285 continue;
3286 }
3287 let parents = match graph.parents_of(oid) {
3288 Ok(parents) => parents,
3289 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
3290 if missing_action.reports_missing() && missing_seen.insert(oid) {
3291 missing.push(oid);
3292 }
3293 continue;
3294 }
3295 Err(err) => return Err(err),
3296 };
3297 seen.insert(oid);
3298 order.push(oid);
3299 for parent in parents {
3300 queue.push_back(parent);
3301 }
3302 }
3303 Ok((seen, order))
3304}
3305
3306fn date_order_walk_through_dropped(
3313 graph: &mut CommitGraph<'_>,
3314 tips: &[ObjectId],
3315 traversable: &HashSet<ObjectId>,
3316 emit: &HashSet<ObjectId>,
3317 author_dates: bool,
3318) -> Result<Vec<ObjectId>> {
3319 let mut heap = BinaryHeap::new();
3320 let mut queued: HashSet<ObjectId> = HashSet::new();
3321 let mut next_seq: u64 = 0;
3322 for &tip in tips {
3325 if traversable.contains(&tip) && queued.insert(tip) {
3326 heap.push(CommitDateKey {
3327 oid: tip,
3328 date: graph.sort_key(tip, author_dates),
3329 seq: next_seq,
3330 });
3331 next_seq += 1;
3332 }
3333 }
3334
3335 let mut emitted = HashSet::new();
3336 let mut out = Vec::with_capacity(emit.len());
3337 while let Some(item) = heap.pop() {
3338 if emit.contains(&item.oid) && emitted.insert(item.oid) {
3339 out.push(item.oid);
3340 }
3341 for parent in graph.parents_of(item.oid)? {
3342 if !traversable.contains(&parent) {
3343 continue;
3344 }
3345 if queued.insert(parent) {
3346 heap.push(CommitDateKey {
3347 oid: parent,
3348 date: graph.sort_key(parent, author_dates),
3349 seq: next_seq,
3350 });
3351 next_seq += 1;
3352 }
3353 }
3354 }
3355
3356 Ok(out)
3357}
3358
3359pub(crate) fn date_order_walk(
3366 graph: &mut CommitGraph<'_>,
3367 selected: &HashSet<ObjectId>,
3368 tip_order: &[ObjectId],
3369 author_dates: bool,
3370) -> Result<Vec<ObjectId>> {
3371 let mut unfinished_children: HashMap<ObjectId, usize> =
3372 selected.iter().map(|&oid| (oid, 0usize)).collect();
3373 for &child in selected {
3374 for parent in graph.parents_of(child)? {
3375 if selected.contains(&parent) {
3376 if let Some(count) = unfinished_children.get_mut(&parent) {
3377 *count += 1;
3378 }
3379 }
3380 }
3381 }
3382
3383 let mut heap = BinaryHeap::new();
3384 let ordered_tip_count = tip_order.len() as u64;
3389 let mut seeded: HashSet<ObjectId> = HashSet::new();
3398 if tip_order.is_empty() {
3399 for &oid in selected {
3400 if unfinished_children.get(&oid).copied().unwrap_or(0) == 0 {
3401 heap.push(CommitDateKey {
3402 oid,
3403 date: graph.sort_key(oid, author_dates),
3404 seq: 0,
3405 });
3406 seeded.insert(oid);
3407 }
3408 }
3409 } else {
3410 for (i, &oid) in tip_order.iter().enumerate() {
3411 if selected.contains(&oid) && seeded.insert(oid) {
3412 heap.push(CommitDateKey {
3413 oid,
3414 date: graph.sort_key(oid, author_dates),
3415 seq: i as u64,
3416 });
3417 }
3418 }
3419 }
3420
3421 let mut next_seq: u64 = ordered_tip_count + 1;
3422 let mut emitted = HashSet::new();
3423 let mut out = Vec::with_capacity(selected.len());
3424 while let Some(item) = heap.pop() {
3425 if !emitted.insert(item.oid) {
3426 continue;
3427 }
3428 out.push(item.oid);
3429 for parent in graph.parents_of(item.oid)? {
3430 if !selected.contains(&parent) {
3431 continue;
3432 }
3433 let Some(count) = unfinished_children.get_mut(&parent) else {
3434 continue;
3435 };
3436 *count = count.saturating_sub(1);
3437 if *count == 0 && !seeded.contains(&parent) {
3441 heap.push(CommitDateKey {
3442 oid: parent,
3443 date: graph.sort_key(parent, author_dates),
3444 seq: next_seq,
3445 });
3446 next_seq += 1;
3447 }
3448 }
3449 }
3450
3451 Ok(out)
3452}
3453
3454fn topo_sort(
3455 graph: &mut CommitGraph<'_>,
3456 selected: &HashSet<ObjectId>,
3457 author_dates: bool,
3458) -> Result<Vec<ObjectId>> {
3459 let mut child_count: HashMap<ObjectId, usize> = selected.iter().map(|&oid| (oid, 0)).collect();
3460
3461 for &oid in selected {
3462 for parent in graph.parents_of(oid)? {
3463 if !selected.contains(&parent) {
3464 continue;
3465 }
3466 if let Some(count) = child_count.get_mut(&parent) {
3467 *count += 1;
3468 }
3469 }
3470 }
3471
3472 let mut ready: BinaryHeap<Reverse<CommitDateKey>> = BinaryHeap::new();
3476 for (&oid, &count) in &child_count {
3477 if count == 0 {
3478 ready.push(Reverse(CommitDateKey {
3479 oid,
3480 date: graph.sort_key(oid, author_dates),
3481 seq: 0,
3482 }));
3483 }
3484 }
3485
3486 let mut out = Vec::with_capacity(selected.len());
3487 while let Some(Reverse(item)) = ready.pop() {
3488 let oid = item.oid;
3489 out.push(oid);
3490 for parent in graph.parents_of(oid)? {
3491 if !selected.contains(&parent) {
3492 continue;
3493 }
3494 if let Some(count) = child_count.get_mut(&parent) {
3495 *count = count.saturating_sub(1);
3496 if *count == 0 {
3497 ready.push(Reverse(CommitDateKey {
3498 oid: parent,
3499 date: graph.sort_key(parent, author_dates),
3500 seq: 0,
3501 }));
3502 }
3503 }
3504 }
3505 }
3506
3507 reorder_adjacent_merge_parent_blocks(graph, &mut out)?;
3508 Ok(out)
3509}
3510
3511fn graph_order_topo_sort(
3512 graph: &mut CommitGraph<'_>,
3513 selected: &HashSet<ObjectId>,
3514 discovery_order: &[ObjectId],
3515) -> Result<Vec<ObjectId>> {
3516 let mut child_count: HashMap<ObjectId, usize> = selected.iter().map(|&oid| (oid, 0)).collect();
3517
3518 for &oid in selected {
3519 for parent in graph.parents_of(oid)? {
3520 if !selected.contains(&parent) {
3521 continue;
3522 }
3523 if let Some(count) = child_count.get_mut(&parent) {
3524 *count += 1;
3525 }
3526 }
3527 }
3528
3529 let discovery_rank: HashMap<ObjectId, usize> = discovery_order
3530 .iter()
3531 .copied()
3532 .enumerate()
3533 .map(|(rank, oid)| (oid, rank))
3534 .collect();
3535 let mut ready: Vec<ObjectId> = child_count
3536 .iter()
3537 .filter_map(|(&oid, &count)| (count == 0).then_some(oid))
3538 .collect();
3539
3540 ready.sort_by(|&a, &b| {
3541 graph
3542 .committer_time(a)
3543 .cmp(&graph.committer_time(b))
3544 .then_with(|| {
3545 discovery_rank
3546 .get(&b)
3547 .copied()
3548 .unwrap_or(usize::MAX)
3549 .cmp(&discovery_rank.get(&a).copied().unwrap_or(usize::MAX))
3550 })
3551 .then_with(|| a.cmp(&b))
3552 });
3553
3554 let mut out = Vec::with_capacity(selected.len());
3555 while let Some(oid) = ready.pop() {
3556 out.push(oid);
3557 for parent in graph.parents_of(oid)? {
3558 if !selected.contains(&parent) {
3559 continue;
3560 }
3561 if let Some(count) = child_count.get_mut(&parent) {
3562 *count = count.saturating_sub(1);
3563 if *count == 0 {
3564 ready.push(parent);
3565 }
3566 }
3567 }
3568 }
3569
3570 reorder_adjacent_merge_parent_blocks(graph, &mut out)?;
3571 Ok(out)
3572}
3573
3574fn reorder_adjacent_merge_parent_blocks(
3575 graph: &mut CommitGraph<'_>,
3576 ordered: &mut [ObjectId],
3577) -> Result<()> {
3578 for index in 0..ordered.len() {
3579 let parents = graph.parents_of(ordered[index])?;
3580 if parents.len() <= 1 || index + parents.len() >= ordered.len() {
3581 continue;
3582 }
3583 let parent_set: HashSet<ObjectId> = parents.iter().copied().collect();
3584 let end = index + 1 + parents.len();
3585 if ordered[index + 1..end]
3586 .iter()
3587 .all(|oid| parent_set.contains(oid))
3588 {
3589 let rank: HashMap<ObjectId, usize> = parents
3590 .iter()
3591 .rev()
3592 .copied()
3593 .enumerate()
3594 .map(|(rank, oid)| (oid, rank))
3595 .collect();
3596 ordered[index + 1..end].sort_by_key(|oid| rank.get(oid).copied().unwrap_or(usize::MAX));
3597 }
3598 }
3599 Ok(())
3600}
3601
3602fn reorder_symmetric_topo_right_first(
3603 graph: &mut CommitGraph<'_>,
3604 ordered: &mut Vec<ObjectId>,
3605 left_oid: ObjectId,
3606 right_oid: ObjectId,
3607) -> Result<()> {
3608 if ordered.len() < 2 {
3609 return Ok(());
3610 }
3611
3612 let left_reach = walk_closure(graph, &[left_oid])?;
3613 let right_reach = walk_closure(graph, &[right_oid])?;
3614 let mut right_only = Vec::new();
3615 let mut rest = Vec::new();
3616
3617 for oid in ordered.drain(..) {
3618 if right_reach.contains(&oid) && !left_reach.contains(&oid) {
3619 right_only.push(oid);
3620 } else {
3621 rest.push(oid);
3622 }
3623 }
3624
3625 right_only.extend(rest);
3626 *ordered = right_only;
3627 Ok(())
3628}
3629
3630fn simplify_merges_commit_list(
3631 repo: &Repository,
3632 commits: &[ObjectId],
3633 paths: &[String],
3634 excluded: &HashSet<ObjectId>,
3635 ancestry_path_bottoms: &[ObjectId],
3636 ordering: OrderingMode,
3637 show_pulls: bool,
3638 preserve_graph_merges: bool,
3639) -> Result<Vec<ObjectId>> {
3640 let selected: HashSet<ObjectId> = commits.iter().copied().collect();
3641 let mut out = Vec::new();
3642 let mut simplified_parents: HashMap<ObjectId, Vec<ObjectId>> = HashMap::new();
3643 for oid in commits {
3644 let raw_parents = load_commit(repo, *oid)?.parents;
3645 let rewritten = visible_parents_for_graph_lib(repo, *oid, &selected, false)?;
3646 let path_survives =
3647 path_merge_survives_simplify(repo, *oid, paths, excluded, ancestry_path_bottoms)?;
3648 let pull_merge = show_pulls
3649 && raw_parents.len() > 1
3650 && path_merge_survives_simplify(repo, *oid, paths, excluded, &[])?;
3651 if raw_parents.len() > 1 && rewritten.len() <= 1 {
3652 if rewritten.is_empty() || pull_merge || path_survives {
3653 simplified_parents.insert(*oid, rewritten);
3654 out.push(*oid);
3655 }
3656 continue;
3657 }
3658 if rewritten.len() <= 1 {
3659 simplified_parents.insert(*oid, rewritten);
3660 out.push(*oid);
3661 continue;
3662 }
3663 let mut simplified = if preserve_graph_merges {
3664 let treesame_rewritten = path_treesame_parents(repo, *oid, paths, &rewritten)?;
3665 let all_rewritten_treesame =
3666 !rewritten.is_empty() && treesame_rewritten.len() == rewritten.len();
3667 if all_rewritten_treesame {
3668 vec![rewritten[0]]
3669 } else {
3670 graph_simplify_parent_list_lib(repo, &selected, &rewritten, &treesame_rewritten)?
3671 }
3672 } else {
3673 graph_simplify_parent_list_lib(repo, &selected, &rewritten, &HashSet::new())?
3674 };
3675 simplified.sort_unstable();
3676 simplified.dedup();
3677 if simplified.len() > 1 || pull_merge {
3678 simplified_parents.insert(*oid, simplified);
3679 out.push(*oid);
3680 }
3681 }
3682 if ordering == OrderingMode::AuthorDateTopo {
3683 return simplify_merges_author_date_order(repo, &out, &simplified_parents);
3684 }
3685 Ok(out)
3686}
3687
3688fn simplify_merges_author_date_order(
3689 repo: &Repository,
3690 commits: &[ObjectId],
3691 simplified_parents: &HashMap<ObjectId, Vec<ObjectId>>,
3692) -> Result<Vec<ObjectId>> {
3693 let selected: HashSet<ObjectId> = commits.iter().copied().collect();
3694 let mut child_count: HashMap<ObjectId, usize> =
3695 commits.iter().copied().map(|oid| (oid, 0)).collect();
3696 for parents in simplified_parents.values() {
3697 for parent in parents {
3698 if selected.contains(parent) {
3699 if let Some(count) = child_count.get_mut(parent) {
3700 *count += 1;
3701 }
3702 }
3703 }
3704 }
3705
3706 let mut graph = CommitGraph::new(repo, false);
3707 let mut ready = BinaryHeap::new();
3708 for oid in commits {
3709 if child_count.get(oid).copied().unwrap_or_default() == 0 {
3710 ready.push(CommitDateKey {
3711 oid: *oid,
3712 date: graph.sort_key(*oid, true),
3713 seq: 0,
3714 });
3715 }
3716 }
3717
3718 let mut out = Vec::with_capacity(commits.len());
3719 let mut emitted = HashSet::new();
3720 while let Some(item) = ready.pop() {
3721 if !emitted.insert(item.oid) {
3722 continue;
3723 }
3724 out.push(item.oid);
3725 if let Some(parents) = simplified_parents.get(&item.oid) {
3726 for parent in parents {
3727 if !selected.contains(parent) {
3728 continue;
3729 }
3730 let Some(count) = child_count.get_mut(parent) else {
3731 continue;
3732 };
3733 *count = count.saturating_sub(1);
3734 if *count == 0 {
3735 ready.push(CommitDateKey {
3736 oid: *parent,
3737 date: graph.sort_key(*parent, true),
3738 seq: 0,
3739 });
3740 }
3741 }
3742 }
3743 }
3744 Ok(out)
3745}
3746
3747fn path_merge_survives_simplify(
3748 repo: &Repository,
3749 oid: ObjectId,
3750 paths: &[String],
3751 excluded: &HashSet<ObjectId>,
3752 ancestry_path_bottoms: &[ObjectId],
3753) -> Result<bool> {
3754 if paths.is_empty() {
3755 return Ok(false);
3756 }
3757 let commit = load_commit(repo, oid)?;
3758 if commit.parents.len() <= 1 {
3759 return Ok(false);
3760 }
3761 let commit_map: HashMap<String, (ObjectId, u32)> =
3762 flatten_tree_with_mode(repo, commit.tree, "")?
3763 .into_iter()
3764 .collect();
3765 let mut treesame_parents = 0usize;
3766 let mut first_parent_differs = false;
3767 let mut differing_parent_is_excluded = false;
3768 let mut differing_parent_oids = Vec::new();
3769 for (nth, parent_oid) in commit.parents.iter().enumerate() {
3770 let parent = load_commit(repo, *parent_oid)?;
3771 let parent_map: HashMap<String, (ObjectId, u32)> =
3772 flatten_tree_with_mode(repo, parent.tree, "")?
3773 .into_iter()
3774 .collect();
3775 let differs = path_differs_for_specs(&commit_map, &parent_map, paths);
3776 if differs {
3777 differing_parent_oids.push(*parent_oid);
3778 if nth == 0 {
3779 first_parent_differs = true;
3780 }
3781 if excluded.contains(parent_oid) {
3782 differing_parent_is_excluded = true;
3783 }
3784 } else {
3785 treesame_parents += 1;
3786 }
3787 }
3788 if treesame_parents != 1 {
3789 return Ok(false);
3790 }
3791 if first_parent_differs || differing_parent_is_excluded {
3792 return Ok(true);
3793 }
3794 if !ancestry_path_bottoms.is_empty() {
3795 let mut graph = CommitGraph::new(repo, false);
3796 return treesame_parent_is_on_ancestry_bottom_side(
3797 &mut graph,
3798 &differing_parent_oids,
3799 ancestry_path_bottoms,
3800 );
3801 }
3802 Ok(false)
3803}
3804
3805fn graph_simplify_parent_list_lib(
3806 repo: &Repository,
3807 selected: &HashSet<ObjectId>,
3808 parents: &[ObjectId],
3809 protected_treesame: &HashSet<ObjectId>,
3810) -> Result<Vec<ObjectId>> {
3811 let mut out = Vec::new();
3812 let protected_count = parents
3813 .iter()
3814 .filter(|parent| protected_treesame.contains(parent))
3815 .count();
3816 for parent in parents {
3817 if parent_reachable_via_others_lib(repo, selected, *parent, parents)? {
3818 if protected_count == 1 && protected_treesame.contains(parent) {
3819 out.push(*parent);
3820 }
3821 continue;
3822 }
3823 out.push(*parent);
3824 }
3825 Ok(out)
3826}
3827
3828fn path_treesame_parents(
3829 repo: &Repository,
3830 oid: ObjectId,
3831 paths: &[String],
3832 parents: &[ObjectId],
3833) -> Result<HashSet<ObjectId>> {
3834 let mut treesame = HashSet::new();
3835 if paths.is_empty() || parents.is_empty() {
3836 return Ok(treesame);
3837 }
3838
3839 let commit = load_commit(repo, oid)?;
3840 let commit_map: HashMap<String, (ObjectId, u32)> =
3841 flatten_tree_with_mode(repo, commit.tree, "")?
3842 .into_iter()
3843 .collect();
3844 for parent_oid in parents {
3845 let parent = load_commit(repo, *parent_oid)?;
3846 let parent_map: HashMap<String, (ObjectId, u32)> =
3847 flatten_tree_with_mode(repo, parent.tree, "")?
3848 .into_iter()
3849 .collect();
3850 if !path_differs_for_specs(&commit_map, &parent_map, paths) {
3851 treesame.insert(*parent_oid);
3852 }
3853 }
3854 Ok(treesame)
3855}
3856
3857fn parent_reachable_via_others_lib(
3858 repo: &Repository,
3859 selected: &HashSet<ObjectId>,
3860 target: ObjectId,
3861 parents: &[ObjectId],
3862) -> Result<bool> {
3863 for parent in parents {
3864 if *parent == target {
3865 continue;
3866 }
3867 if graph_reaches_lib(repo, selected, *parent, target)? {
3868 return Ok(true);
3869 }
3870 }
3871 Ok(false)
3872}
3873
3874fn graph_reaches_lib(
3875 repo: &Repository,
3876 selected: &HashSet<ObjectId>,
3877 start: ObjectId,
3878 target: ObjectId,
3879) -> Result<bool> {
3880 let mut stack = vec![start];
3881 let mut seen = HashSet::new();
3882 while let Some(oid) = stack.pop() {
3883 if !seen.insert(oid) {
3884 continue;
3885 }
3886 if oid == target {
3887 return Ok(true);
3888 }
3889 let mut parents = load_commit(repo, oid)?.parents;
3890 parents.retain(|p| selected.contains(p));
3891 stack.extend(parents);
3892 }
3893 Ok(false)
3894}
3895
3896fn load_raw_parents_lib(repo: &Repository, oid: ObjectId) -> Result<Vec<ObjectId>> {
3897 Ok(load_commit(repo, oid)?.parents)
3898}
3899
3900fn load_navigation_parents_lib(
3901 repo: &Repository,
3902 oid: ObjectId,
3903 grafts: &HashMap<ObjectId, Vec<ObjectId>>,
3904) -> Result<Vec<ObjectId>> {
3905 if let Some(grafted) = grafts.get(&oid) {
3906 return Ok(grafted.clone());
3907 }
3908 load_raw_parents_lib(repo, oid)
3909}
3910
3911fn first_parent_of_commit_lib(
3912 repo: &Repository,
3913 oid: ObjectId,
3914 grafts: &HashMap<ObjectId, Vec<ObjectId>>,
3915) -> Result<Option<ObjectId>> {
3916 let parents = load_navigation_parents_lib(repo, oid, grafts)?;
3917 Ok(parents.first().copied())
3918}
3919
3920fn first_parent_anchor_in_set_lib(
3921 repo: &Repository,
3922 start: ObjectId,
3923 anchors: &HashSet<ObjectId>,
3924 grafts: &HashMap<ObjectId, Vec<ObjectId>>,
3925) -> Result<Option<ObjectId>> {
3926 let mut seen = HashSet::new();
3927 let mut cursor = Some(start);
3928 while let Some(oid) = cursor {
3929 if !seen.insert(oid) {
3930 break;
3931 }
3932 if anchors.contains(&oid) {
3933 return Ok(Some(oid));
3934 }
3935 cursor = first_parent_of_commit_lib(repo, oid, grafts)?;
3936 }
3937 Ok(None)
3938}
3939
3940fn collect_visible_parent_for_graph_lib(
3941 repo: &Repository,
3942 candidate: ObjectId,
3943 included: &HashSet<ObjectId>,
3944 first_parent_only: bool,
3945 grafts: &HashMap<ObjectId, Vec<ObjectId>>,
3946 seen: &mut HashSet<ObjectId>,
3947 out: &mut Vec<ObjectId>,
3948) -> Result<()> {
3949 if !seen.insert(candidate) {
3950 return Ok(());
3951 }
3952 if included.contains(&candidate) {
3953 out.push(candidate);
3954 return Ok(());
3955 }
3956 let mut parents = load_navigation_parents_lib(repo, candidate, grafts)?;
3957 if parents.is_empty() {
3958 return Ok(());
3959 }
3960 if parents.len() > 1 {
3961 parents.truncate(1);
3962 }
3963 for parent in parents {
3964 collect_visible_parent_for_graph_lib(
3965 repo,
3966 parent,
3967 included,
3968 first_parent_only,
3969 grafts,
3970 seen,
3971 out,
3972 )?;
3973 }
3974 Ok(())
3975}
3976
3977fn visible_parents_for_graph_lib(
3978 repo: &Repository,
3979 oid: ObjectId,
3980 included: &HashSet<ObjectId>,
3981 first_parent_only: bool,
3982) -> Result<Vec<ObjectId>> {
3983 let grafts = crate::rev_parse::load_graft_parents(&repo.git_dir);
3984 let mut direct = load_navigation_parents_lib(repo, oid, &grafts)?;
3985 if first_parent_only && direct.len() > 1 {
3986 direct.truncate(1);
3987 }
3988 let mut seen = HashSet::new();
3989 let mut out = Vec::new();
3990 for parent in direct {
3991 collect_visible_parent_for_graph_lib(
3992 repo,
3993 parent,
3994 included,
3995 first_parent_only,
3996 &grafts,
3997 &mut seen,
3998 &mut out,
3999 )?;
4000 }
4001 let mut dedup = HashSet::new();
4002 out.retain(|parent| dedup.insert(*parent));
4003 Ok(out)
4004}
4005
4006fn reorder_path_limited_graph_commits(
4007 repo: &Repository,
4008 commits: &[ObjectId],
4009 first_parent_only: bool,
4010) -> Result<Vec<ObjectId>> {
4011 if commits.is_empty() {
4012 return Ok(Vec::new());
4013 }
4014
4015 if !first_parent_only {
4023 return Ok(commits.to_vec());
4024 }
4025
4026 let included: HashSet<ObjectId> = commits.iter().copied().collect();
4027 let grafts = crate::rev_parse::load_graft_parents(&repo.git_dir);
4028 let mut chain = Vec::new();
4029 let mut chain_seen = HashSet::new();
4030 let mut cursor = Some(commits[0]);
4031 while let Some(oid) = cursor {
4032 if !included.contains(&oid) || !chain_seen.insert(oid) {
4033 break;
4034 }
4035 chain.push(oid);
4036 let visible = visible_parents_for_graph_lib(repo, oid, &included, first_parent_only)?;
4037 cursor = visible.first().copied();
4038 }
4039
4040 let chain_set: HashSet<ObjectId> = chain.iter().copied().collect();
4041 let mut grouped: HashMap<Option<ObjectId>, Vec<ObjectId>> = HashMap::new();
4042 for oid in commits {
4043 if chain_set.contains(oid) {
4044 continue;
4045 }
4046 let anchor = first_parent_anchor_in_set_lib(repo, *oid, &chain_set, &grafts)?;
4047 grouped.entry(anchor).or_default().push(*oid);
4048 }
4049
4050 let mut ordered = Vec::new();
4051 for chain_oid in chain {
4052 if let Some(group) = grouped.remove(&Some(chain_oid)) {
4053 ordered.extend(group);
4054 }
4055 ordered.push(chain_oid);
4056 }
4057 if let Some(group) = grouped.remove(&None) {
4058 ordered.extend(group);
4059 }
4060 for (_anchor, group) in grouped {
4061 ordered.extend(group);
4062 }
4063 Ok(ordered)
4064}
4065
4066fn expand_sparse_path_limited_graph_history(
4067 repo: &Repository,
4068 commits: &[ObjectId],
4069) -> Result<Vec<ObjectId>> {
4070 if commits.is_empty() {
4071 return Ok(Vec::new());
4072 }
4073
4074 let mut expanded = Vec::new();
4075 let mut seen = HashSet::new();
4076 let grafts = crate::rev_parse::load_graft_parents(&repo.git_dir);
4077 let mut push_unique = |oid: ObjectId, out: &mut Vec<ObjectId>| {
4078 if seen.insert(oid) {
4079 out.push(oid);
4080 }
4081 };
4082
4083 for window in commits.windows(2) {
4084 let from = window[0];
4085 let to = window[1];
4086 push_unique(from, &mut expanded);
4087
4088 let mut cursor = first_parent_of_commit_lib(repo, from, &grafts)?;
4089 let mut chain = Vec::new();
4090 let mut found_target = false;
4091 let mut local_seen = HashSet::new();
4092 while let Some(oid) = cursor {
4093 if !local_seen.insert(oid) {
4094 break;
4095 }
4096 if oid == to {
4097 found_target = true;
4098 break;
4099 }
4100 chain.push(oid);
4101 cursor = first_parent_of_commit_lib(repo, oid, &grafts)?;
4102 }
4103 if found_target {
4104 for oid in chain {
4105 push_unique(oid, &mut expanded);
4106 }
4107 }
4108 }
4109
4110 if let Some(&last) = commits.last() {
4111 push_unique(last, &mut expanded);
4112 let mut cursor = first_parent_of_commit_lib(repo, last, &grafts)?;
4113 let mut tail_seen = HashSet::new();
4114 while let Some(oid) = cursor {
4115 if !tail_seen.insert(oid) {
4116 break;
4117 }
4118 push_unique(oid, &mut expanded);
4119 cursor = first_parent_of_commit_lib(repo, oid, &grafts)?;
4120 }
4121 }
4122
4123 Ok(expanded)
4124}
4125
4126fn limit_to_ancestry(
4127 graph: &mut CommitGraph<'_>,
4128 selected: &mut HashSet<ObjectId>,
4129 bottoms: &[ObjectId],
4130) -> Result<()> {
4131 let mut keep = HashSet::new();
4132
4133 for &bottom in bottoms {
4134 let ancestors = walk_closure(graph, &[bottom])?;
4135 keep.extend(
4136 ancestors
4137 .iter()
4138 .copied()
4139 .filter(|oid| selected.contains(oid)),
4140 );
4141 }
4142
4143 let mut descendants: HashSet<ObjectId> = bottoms.iter().copied().collect();
4144 loop {
4145 let mut made_progress = false;
4146 for &candidate in selected.iter() {
4147 if descendants.contains(&candidate) {
4148 continue;
4149 }
4150
4151 let parents = graph.parents_of(candidate)?;
4152 if parents.iter().any(|parent| descendants.contains(parent)) {
4153 descendants.insert(candidate);
4154 made_progress = true;
4155 }
4156 }
4157
4158 if !made_progress {
4159 break;
4160 }
4161 }
4162
4163 keep.extend(
4164 descendants
4165 .iter()
4166 .copied()
4167 .filter(|oid| selected.contains(oid)),
4168 );
4169 selected.retain(|oid| keep.contains(oid));
4170 Ok(())
4171}
4172
4173#[allow(clippy::too_many_arguments)]
4179fn commit_touches_paths(
4180 repo: &Repository,
4181 graph: &mut CommitGraph<'_>,
4182 oid: ObjectId,
4183 paths: &[String],
4184 full_history: bool,
4185 sparse: bool,
4186 simplify_merges: bool,
4187 preserve_simplify_merges_graph_merges: bool,
4188 show_pulls: bool,
4189 parent_rewrite: bool,
4190 ancestry_path: bool,
4191 ancestry_path_bottoms: &[ObjectId],
4192 excluded: &HashSet<ObjectId>,
4193 bloom_chain: Option<&CommitGraphChain>,
4194 read_changed_paths: bool,
4195 changed_paths_version: i32,
4196 bloom_stats: Option<&BloomWalkStatsHandle>,
4197 bloom_cwd: Option<&str>,
4198) -> Result<bool> {
4199 let commit = load_commit(repo, oid)?;
4200 let parents = graph.parents_of(oid)?;
4201 let commit_entries = flatten_tree_with_mode(repo, commit.tree, "")?;
4202 let commit_map: HashMap<String, (ObjectId, u32)> = commit_entries.into_iter().collect();
4203
4204 if parents.is_empty() {
4206 if sparse {
4207 return Ok(true);
4208 }
4209 let ctx = crate::pathspec::PathspecMatchContext {
4210 is_directory: false,
4211 is_git_submodule: false,
4212 };
4213 return Ok(commit_map
4214 .keys()
4215 .any(|path| crate::pathspec::matches_pathspec_list_with_context(path, paths, ctx)));
4216 }
4217
4218 if parents.len() == 1 {
4220 let mut bloom_ret = BloomPrecheck::Inapplicable;
4221 if let Some(chain) = bloom_chain {
4222 bloom_ret = chain.bloom_precheck_for_paths(
4223 &repo.odb,
4224 oid,
4225 paths,
4226 bloom_cwd,
4227 changed_paths_version,
4228 read_changed_paths,
4229 )?;
4230 if let Some(stats) = bloom_stats {
4231 if let Ok(mut g) = stats.lock() {
4232 g.record_precheck(bloom_ret);
4233 }
4234 }
4235 if bloom_ret == BloomPrecheck::DefinitelyNot {
4236 return Ok(false);
4237 }
4238 }
4239
4240 let parent = load_commit(repo, parents[0])?;
4241 let parent_map: HashMap<String, (ObjectId, u32)> =
4242 flatten_tree_with_mode(repo, parent.tree, "")?
4243 .into_iter()
4244 .collect();
4245 let differs = path_differs_for_specs(&commit_map, &parent_map, paths);
4246 if bloom_ret == BloomPrecheck::Maybe && !differs {
4247 if let Some(stats) = bloom_stats {
4248 if let Ok(mut g) = stats.lock() {
4249 g.record_false_positive();
4250 }
4251 }
4252 }
4253 if differs {
4254 return Ok(true);
4255 }
4256 if sparse {
4257 return Ok(true);
4258 }
4259 return Ok(false);
4260 }
4261
4262 let mut treesame_parents = 0usize;
4264 let mut treesame_parent_oids = Vec::new();
4265 let mut differing_parent_oids = Vec::new();
4266 let mut differs_any = false;
4267 let mut first_parent_differs = false;
4268 for (nth, parent_oid) in parents.iter().enumerate() {
4269 let mut bloom_ret = BloomPrecheck::Inapplicable;
4275 if nth == 0 {
4276 if let Some(chain) = bloom_chain {
4277 bloom_ret = chain.bloom_precheck_for_paths(
4278 &repo.odb,
4279 oid,
4280 paths,
4281 bloom_cwd,
4282 changed_paths_version,
4283 read_changed_paths,
4284 )?;
4285 if let Some(stats) = bloom_stats {
4286 if let Ok(mut g) = stats.lock() {
4287 g.record_precheck(bloom_ret);
4288 }
4289 }
4290 }
4291 }
4292
4293 let differs = if bloom_ret == BloomPrecheck::DefinitelyNot {
4294 false
4295 } else {
4296 let parent = load_commit(repo, *parent_oid)?;
4297 let parent_map: HashMap<String, (ObjectId, u32)> =
4298 flatten_tree_with_mode(repo, parent.tree, "")?
4299 .into_iter()
4300 .collect();
4301 path_differs_for_specs(&commit_map, &parent_map, paths)
4302 };
4303 if nth == 0 {
4304 first_parent_differs = differs;
4305 if bloom_ret == BloomPrecheck::Maybe && !differs {
4306 if let Some(stats) = bloom_stats {
4307 if let Ok(mut g) = stats.lock() {
4308 g.record_false_positive();
4309 }
4310 }
4311 }
4312 }
4313 if differs {
4314 differs_any = true;
4315 differing_parent_oids.push(*parent_oid);
4316 } else {
4317 treesame_parents += 1;
4318 treesame_parent_oids.push(*parent_oid);
4319 }
4320 }
4321
4322 if ancestry_path
4323 && !ancestry_path_bottoms.is_empty()
4324 && if parent_rewrite {
4325 treesame_parents != parents.len()
4326 && treesame_parent_is_ancestry_bottom_or_ancestor(
4327 graph,
4328 &treesame_parent_oids,
4329 ancestry_path_bottoms,
4330 )?
4331 } else {
4332 treesame_parent_is_on_ancestry_bottom_side(
4333 graph,
4334 &treesame_parent_oids,
4335 ancestry_path_bottoms,
4336 )?
4337 }
4338 {
4339 return Ok(sparse);
4340 }
4341
4342 if full_history && !simplify_merges && parents.len() > 1 && treesame_parents == parents.len() {
4346 return Ok(parent_rewrite || sparse);
4347 }
4348
4349 if show_pulls && first_parent_differs && treesame_parents > 0 {
4350 return Ok(true);
4351 }
4352
4353 if !full_history && treesame_parents == 1 {
4354 return Ok(false);
4355 }
4356
4357 if full_history && simplify_merges {
4358 if preserve_simplify_merges_graph_merges {
4359 return Ok(true);
4360 }
4361 if treesame_parents == parents.len() {
4362 return Ok(true);
4363 }
4364 if treesame_parents > 0 && !differs_any {
4365 return Ok(true);
4366 }
4367 if show_pulls && first_parent_differs && treesame_parents > 0 {
4368 return Ok(true);
4369 }
4370 if treesame_parents == 1 {
4371 if ancestry_path
4372 && !ancestry_path_bottoms.is_empty()
4373 && treesame_parent_is_on_ancestry_bottom_side(
4374 graph,
4375 &differing_parent_oids,
4376 ancestry_path_bottoms,
4377 )?
4378 {
4379 return Ok(true);
4380 }
4381 return Ok(first_parent_differs
4382 || differing_parent_oids
4383 .iter()
4384 .any(|parent| excluded.contains(parent)));
4385 }
4386 return Ok(differs_any || sparse);
4387 }
4388
4389 if differs_any {
4390 return Ok(true);
4391 }
4392
4393 Ok(sparse)
4394}
4395
4396fn treesame_parent_is_on_ancestry_bottom_side(
4397 graph: &mut CommitGraph<'_>,
4398 treesame_parents: &[ObjectId],
4399 bottoms: &[ObjectId],
4400) -> Result<bool> {
4401 if treesame_parents.is_empty() {
4402 return Ok(false);
4403 }
4404 let treesame: HashSet<ObjectId> = treesame_parents.iter().copied().collect();
4405 for bottom in bottoms {
4406 if treesame.contains(bottom) {
4407 return Ok(true);
4408 }
4409 for parent in treesame_parents {
4410 let parent_closure = walk_closure(graph, &[*parent])?;
4411 if parent_closure.contains(bottom) {
4412 return Ok(true);
4413 }
4414 }
4415 }
4416 Ok(false)
4417}
4418
4419fn treesame_parent_is_ancestry_bottom_or_ancestor(
4420 graph: &mut CommitGraph<'_>,
4421 treesame_parents: &[ObjectId],
4422 bottoms: &[ObjectId],
4423) -> Result<bool> {
4424 if treesame_parents.is_empty() {
4425 return Ok(false);
4426 }
4427 let treesame: HashSet<ObjectId> = treesame_parents.iter().copied().collect();
4428 for bottom in bottoms {
4429 if treesame.contains(bottom) {
4430 return Ok(true);
4431 }
4432 let bottom_closure = walk_closure(graph, &[*bottom])?;
4433 if bottom_closure.iter().any(|oid| treesame.contains(oid)) {
4434 return Ok(true);
4435 }
4436 }
4437 Ok(false)
4438}
4439
4440#[derive(Clone, Copy)]
4445struct DenseBloomCtx<'a> {
4446 chain: Option<&'a CommitGraphChain>,
4447 read_changed_paths: bool,
4448 changed_paths_version: i32,
4449 stats: Option<&'a BloomWalkStatsHandle>,
4450 cwd: Option<&'a str>,
4451}
4452
4453#[allow(clippy::too_many_arguments)]
4454fn walk_dense_path_limited_closure(
4455 repo: &Repository,
4456 graph: &mut CommitGraph<'_>,
4457 tips: &[ObjectId],
4458 excluded: &HashSet<ObjectId>,
4459 paths: &[String],
4460 sparse: bool,
4461 show_pulls: bool,
4462) -> Result<HashSet<ObjectId>> {
4463 let mut walked = HashSet::new();
4464 let mut selected = HashSet::new();
4465 let mut queue: VecDeque<ObjectId> = tips.iter().copied().collect();
4466
4467 while let Some(oid) = queue.pop_front() {
4468 if excluded.contains(&oid) || !walked.insert(oid) {
4469 continue;
4470 }
4471
4472 let action = dense_path_limited_action(repo, graph, oid, paths, sparse, show_pulls)?;
4473 if action.visible {
4474 selected.insert(oid);
4475 }
4476
4477 for parent in action.parents_to_walk {
4478 if !excluded.contains(&parent) && !walked.contains(&parent) {
4479 queue.push_back(parent);
4480 }
4481 }
4482 }
4483
4484 Ok(selected)
4485}
4486
4487fn consult_dense_bloom_filters(
4505 repo: &Repository,
4506 graph: &mut CommitGraph<'_>,
4507 reachable: &HashSet<ObjectId>,
4508 paths: &[String],
4509 bloom: &DenseBloomCtx<'_>,
4510) -> Result<()> {
4511 let Some(chain) = bloom.chain else {
4512 return Ok(());
4513 };
4514 for oid in reachable {
4515 let parents = graph.parents_of(*oid)?;
4516 let Some(first_parent) = parents.first().copied() else {
4519 continue;
4520 };
4521 let pre = chain.bloom_precheck_for_paths(
4522 &repo.odb,
4523 *oid,
4524 paths,
4525 bloom.cwd,
4526 bloom.changed_paths_version,
4527 bloom.read_changed_paths,
4528 )?;
4529 if let Some(stats) = bloom.stats {
4530 if let Ok(mut g) = stats.lock() {
4531 g.record_precheck(pre);
4532 }
4533 }
4534 if pre == BloomPrecheck::Maybe {
4538 if let Some(stats) = bloom.stats {
4539 let commit = load_commit(repo, *oid)?;
4540 let commit_map: HashMap<String, (ObjectId, u32)> =
4541 flatten_tree_with_mode(repo, commit.tree, "")?
4542 .into_iter()
4543 .collect();
4544 let parent = load_commit(repo, first_parent)?;
4545 let parent_map: HashMap<String, (ObjectId, u32)> =
4546 flatten_tree_with_mode(repo, parent.tree, "")?
4547 .into_iter()
4548 .collect();
4549 if !path_differs_for_specs(&commit_map, &parent_map, paths) {
4550 if let Ok(mut g) = stats.lock() {
4551 g.record_false_positive();
4552 }
4553 }
4554 }
4555 }
4556 }
4557 Ok(())
4558}
4559
4560struct DensePathAction {
4561 visible: bool,
4562 parents_to_walk: Vec<ObjectId>,
4563}
4564
4565fn dense_path_limited_action(
4566 repo: &Repository,
4567 graph: &mut CommitGraph<'_>,
4568 oid: ObjectId,
4569 paths: &[String],
4570 sparse: bool,
4571 show_pulls: bool,
4572) -> Result<DensePathAction> {
4573 let commit = load_commit(repo, oid)?;
4574 let parents = graph.parents_of(oid)?;
4575 let commit_map: HashMap<String, (ObjectId, u32)> =
4576 flatten_tree_with_mode(repo, commit.tree, "")?
4577 .into_iter()
4578 .collect();
4579
4580 if parents.is_empty() {
4581 let ctx = crate::pathspec::PathspecMatchContext {
4582 is_directory: false,
4583 is_git_submodule: false,
4584 };
4585 let visible = sparse
4586 || commit_map
4587 .keys()
4588 .any(|path| crate::pathspec::matches_pathspec_list_with_context(path, paths, ctx));
4589 return Ok(DensePathAction {
4590 visible,
4591 parents_to_walk: Vec::new(),
4592 });
4593 }
4594
4595 let mut treesame = Vec::new();
4596 let mut differs_any = false;
4597 let mut first_parent_differs = false;
4598 for (nth, parent_oid) in parents.iter().enumerate() {
4599 let parent = load_commit(repo, *parent_oid)?;
4600 let parent_map: HashMap<String, (ObjectId, u32)> =
4601 flatten_tree_with_mode(repo, parent.tree, "")?
4602 .into_iter()
4603 .collect();
4604 if path_differs_for_specs(&commit_map, &parent_map, paths) {
4605 if nth == 0 {
4606 first_parent_differs = true;
4607 }
4608 differs_any = true;
4609 } else {
4610 treesame.push(*parent_oid);
4611 }
4612 }
4613
4614 let parents_to_walk = if parents.len() > 1 {
4615 match treesame.first().copied() {
4616 Some(parent) => vec![parent],
4617 None => parents.clone(),
4618 }
4619 } else {
4620 parents.clone()
4621 };
4622
4623 let visible = if sparse {
4624 true
4625 } else if parents.len() > 1 {
4626 treesame.is_empty() || (show_pulls && first_parent_differs && !treesame.is_empty())
4627 } else {
4628 differs_any
4629 };
4630
4631 Ok(DensePathAction {
4632 visible,
4633 parents_to_walk,
4634 })
4635}
4636
4637pub fn commit_visible_for_dense_pathspecs(
4644 repo: &Repository,
4645 oid: ObjectId,
4646 paths: &[String],
4647) -> Result<bool> {
4648 if paths.is_empty() {
4649 return Ok(true);
4650 }
4651 let commit = load_commit(repo, oid)?;
4652 let parents = commit.parents.clone();
4653 let commit_entries = flatten_tree_with_mode(repo, commit.tree, "")?;
4654 let commit_map: HashMap<String, (ObjectId, u32)> = commit_entries.into_iter().collect();
4655
4656 if parents.is_empty() {
4657 return Ok(commit_map.keys().any(|path| {
4658 paths.iter().any(|spec| {
4659 crate::pathspec::matches_pathspec_with_context(
4660 spec,
4661 path,
4662 crate::pathspec::PathspecMatchContext {
4663 is_directory: false,
4664 is_git_submodule: false,
4665 },
4666 )
4667 })
4668 }));
4669 }
4670
4671 if parents.len() == 1 {
4672 let parent = load_commit(repo, parents[0])?;
4673 let parent_map: HashMap<String, (ObjectId, u32)> =
4674 flatten_tree_with_mode(repo, parent.tree, "")?
4675 .into_iter()
4676 .collect();
4677 return Ok(path_differs_for_specs(&commit_map, &parent_map, paths));
4678 }
4679
4680 let mut treesame_parents = 0usize;
4681 let mut differs_any = false;
4682 for parent_oid in &parents {
4683 let parent = load_commit(repo, *parent_oid)?;
4684 let parent_map: HashMap<String, (ObjectId, u32)> =
4685 flatten_tree_with_mode(repo, parent.tree, "")?
4686 .into_iter()
4687 .collect();
4688 let differs = path_differs_for_specs(&commit_map, &parent_map, paths);
4689 if differs {
4690 differs_any = true;
4691 } else {
4692 treesame_parents += 1;
4693 }
4694 }
4695
4696 if treesame_parents == 1 {
4697 return Ok(false);
4698 }
4699 if differs_any {
4700 return Ok(true);
4701 }
4702 Ok(false)
4703}
4704
4705fn compute_cherry_patch_id(
4706 repo: &Repository,
4707 oid: &ObjectId,
4708 paths: &[String],
4709) -> Result<Option<ObjectId>> {
4710 if paths.is_empty() {
4711 compute_patch_id(&repo.odb, oid)
4712 } else {
4713 compute_patch_id_for_paths(&repo.odb, oid, paths)
4714 }
4715}
4716
4717fn path_differs_for_specs(
4718 current: &HashMap<String, (ObjectId, u32)>,
4719 parent: &HashMap<String, (ObjectId, u32)>,
4720 specs: &[String],
4721) -> bool {
4722 let mut paths = std::collections::BTreeSet::new();
4723 paths.extend(current.keys().cloned());
4724 paths.extend(parent.keys().cloned());
4725
4726 for path in &paths {
4727 if !crate::pathspec::matches_pathspec_list(path, specs) {
4728 continue;
4729 }
4730 if current.get(path) != parent.get(path) {
4731 return true;
4732 }
4733 }
4734 false
4735}
4736
4737fn load_commit(repo: &Repository, oid: ObjectId) -> Result<crate::objects::CommitData> {
4738 let object = repo.odb.read(&oid)?;
4739 if object.kind != ObjectKind::Commit {
4740 return Err(Error::CorruptObject(format!(
4741 "object {oid} is not a commit"
4742 )));
4743 }
4744 parse_commit(&object.data)
4745}
4746
4747fn extend_split_token(
4748 token: &str,
4749 not_mode: bool,
4750 positive: &mut Vec<String>,
4751 negative: &mut Vec<String>,
4752) {
4753 let (pos, neg) = split_revision_token(token);
4754 if not_mode {
4755 positive.extend(neg);
4756 negative.extend(pos);
4757 } else {
4758 positive.extend(pos);
4759 negative.extend(neg);
4760 }
4761}
4762
4763fn parse_long_opt_value(opt: &str, argv0: &str, argv1: Option<&str>) -> Option<(usize, String)> {
4768 let rest = argv0.strip_prefix("--")?;
4769 let rest = rest.strip_prefix(opt)?;
4770 if let Some(stripped) = rest.strip_prefix('=') {
4771 return Some((1, stripped.to_owned()));
4772 }
4773 if !rest.is_empty() {
4774 return None;
4775 }
4776 let Some(next) = argv1 else {
4777 return None;
4778 };
4779 Some((2, next.to_owned()))
4780}
4781
4782fn stdin_die_requires_value(opt: &str) -> Error {
4783 Error::Message(format!("fatal: Option '{opt}' requires a value"))
4784}
4785
4786fn apply_stdin_pseudo_opt(
4787 git_dir: &Path,
4788 line: &str,
4789 next_line: Option<&str>,
4790 not_mode: bool,
4791 positive: &mut Vec<String>,
4792 negative: &mut Vec<String>,
4793 stdin_all_refs: &mut bool,
4794) -> Result<Option<usize>> {
4795 if line == "--end-of-options" {
4796 return Ok(Some(1));
4797 }
4798 if line == "--all" {
4799 if not_mode {
4800 for (_, oid) in refs::list_refs(git_dir, "refs/")? {
4801 let s = oid.to_hex();
4802 negative.push(s);
4803 }
4804 if let Ok(head_oid) = refs::resolve_ref(git_dir, "HEAD") {
4805 negative.push(head_oid.to_hex());
4806 }
4807 } else {
4808 *stdin_all_refs = true;
4809 }
4810 return Ok(Some(1));
4811 }
4812 if line == "--not" {
4813 return Ok(Some(1));
4814 }
4815 if line == "--branches" {
4816 for (_, oid) in refs::list_refs(git_dir, "refs/heads/")? {
4817 let s = oid.to_hex();
4818 if not_mode {
4819 negative.push(s);
4820 } else {
4821 positive.push(s);
4822 }
4823 }
4824 return Ok(Some(1));
4825 }
4826 if line == "--tags" {
4827 for (_, oid) in refs::list_refs(git_dir, "refs/tags/")? {
4828 let s = oid.to_hex();
4829 if not_mode {
4830 negative.push(s);
4831 } else {
4832 positive.push(s);
4833 }
4834 }
4835 return Ok(Some(1));
4836 }
4837 if line == "--remotes" {
4838 for (_, oid) in refs::list_refs(git_dir, "refs/remotes/")? {
4839 let s = oid.to_hex();
4840 if not_mode {
4841 negative.push(s);
4842 } else {
4843 positive.push(s);
4844 }
4845 }
4846 return Ok(Some(1));
4847 }
4848 if let Some((consumed, pattern)) = parse_long_opt_value("branches", line, next_line) {
4849 let full_pattern = format!("refs/heads/{pattern}");
4850 for (_, oid) in refs::list_refs_glob(git_dir, &full_pattern)? {
4851 let s = oid.to_hex();
4852 if not_mode {
4853 negative.push(s);
4854 } else {
4855 positive.push(s);
4856 }
4857 }
4858 return Ok(Some(consumed));
4859 }
4860 if let Some((1, pattern)) = parse_long_opt_value("tags", line, None) {
4861 let full_pattern = format!("refs/tags/{pattern}");
4862 for (_, oid) in refs::list_refs_glob(git_dir, &full_pattern)? {
4863 let s = oid.to_hex();
4864 if not_mode {
4865 negative.push(s);
4866 } else {
4867 positive.push(s);
4868 }
4869 }
4870 return Ok(Some(1));
4871 }
4872 if let Some((consumed, pattern)) = parse_long_opt_value("tags", line, next_line) {
4873 let full_pattern = format!("refs/tags/{pattern}");
4874 for (_, oid) in refs::list_refs_glob(git_dir, &full_pattern)? {
4875 let s = oid.to_hex();
4876 if not_mode {
4877 negative.push(s);
4878 } else {
4879 positive.push(s);
4880 }
4881 }
4882 return Ok(Some(consumed));
4883 }
4884 if let Some((1, pattern)) = parse_long_opt_value("remotes", line, None) {
4885 let full_pattern = format!("refs/remotes/{pattern}");
4886 for (_, oid) in refs::list_refs_glob(git_dir, &full_pattern)? {
4887 let s = oid.to_hex();
4888 if not_mode {
4889 negative.push(s);
4890 } else {
4891 positive.push(s);
4892 }
4893 }
4894 return Ok(Some(1));
4895 }
4896 if let Some((consumed, pattern)) = parse_long_opt_value("remotes", line, next_line) {
4897 let full_pattern = format!("refs/remotes/{pattern}");
4898 for (_, oid) in refs::list_refs_glob(git_dir, &full_pattern)? {
4899 let s = oid.to_hex();
4900 if not_mode {
4901 negative.push(s);
4902 } else {
4903 positive.push(s);
4904 }
4905 }
4906 return Ok(Some(consumed));
4907 }
4908 if line == "--glob" {
4909 return Err(stdin_die_requires_value("--glob"));
4910 }
4911 if let Some((1, pattern)) = parse_long_opt_value("glob", line, None) {
4912 for (_, oid) in refs::list_refs_glob(git_dir, &pattern)? {
4913 let s = oid.to_hex();
4914 if not_mode {
4915 negative.push(s);
4916 } else {
4917 positive.push(s);
4918 }
4919 }
4920 return Ok(Some(1));
4921 }
4922 if let Some((consumed, pattern)) = parse_long_opt_value("glob", line, next_line) {
4923 for (_, oid) in refs::list_refs_glob(git_dir, &pattern)? {
4924 let s = oid.to_hex();
4925 if not_mode {
4926 negative.push(s);
4927 } else {
4928 positive.push(s);
4929 }
4930 }
4931 return Ok(Some(consumed));
4932 }
4933 if line == "--no-walk" || line.starts_with("--no-walk=") {
4934 if let Some(rest) = line.strip_prefix("--no-walk=") {
4935 if rest == "sorted" || rest == "unsorted" {
4936 return Ok(Some(1));
4937 }
4938 eprintln!("error: invalid argument to --no-walk");
4939 return Err(Error::Message(format!(
4940 "fatal: invalid option '{line}' in --stdin mode"
4941 )));
4942 }
4943 return Ok(Some(1));
4944 }
4945 if line.starts_with("--") {
4946 return Err(Error::Message(format!(
4947 "fatal: invalid option '{line}' in --stdin mode"
4948 )));
4949 }
4950 if line.starts_with('-') {
4951 return Err(Error::Message(format!(
4952 "fatal: invalid option '{line}' in --stdin mode"
4953 )));
4954 }
4955 Ok(None)
4956}
4957
4958fn read_revisions_from_stdin_lines(
4960 git_dir: &Path,
4961) -> Result<(Vec<String>, Vec<String>, bool, Vec<String>)> {
4962 let stdin = std::io::read_to_string(std::io::stdin()).map_err(Error::Io)?;
4963 let lines: Vec<String> = stdin.lines().map(std::borrow::ToOwned::to_owned).collect();
4964
4965 let mut positive = Vec::new();
4966 let mut negative = Vec::new();
4967 let mut stdin_all_refs = false;
4968 let mut stdin_not_mode = false;
4969 let mut seen_end_of_options = false;
4970 let mut i = 0usize;
4971
4972 while i < lines.len() {
4973 let line = lines[i].as_str();
4974 if line.is_empty() {
4975 break;
4976 }
4977 if line == "--" {
4978 i += 1;
4979 let paths: Vec<String> = lines[i..].to_vec();
4980 return Ok((positive, negative, stdin_all_refs, paths));
4981 }
4982
4983 if !seen_end_of_options && line.starts_with('-') {
4984 if line == "--end-of-options" {
4985 seen_end_of_options = true;
4986 i += 1;
4987 continue;
4988 }
4989 let next = lines.get(i + 1).map(|s| s.as_str());
4990 match apply_stdin_pseudo_opt(
4991 git_dir,
4992 line,
4993 next,
4994 stdin_not_mode,
4995 &mut positive,
4996 &mut negative,
4997 &mut stdin_all_refs,
4998 )? {
4999 Some(consumed) => {
5000 if line == "--not" && consumed == 1 {
5001 stdin_not_mode = !stdin_not_mode;
5002 }
5003 i += consumed;
5004 }
5005 None => {
5006 extend_split_token(line, stdin_not_mode, &mut positive, &mut negative);
5007 i += 1;
5008 }
5009 }
5010 continue;
5011 }
5012
5013 extend_split_token(line, stdin_not_mode, &mut positive, &mut negative);
5014 i += 1;
5015 }
5016
5017 Ok((positive, negative, stdin_all_refs, Vec::new()))
5018}
5019
5020pub fn collect_revision_specs_with_stdin(
5031 git_dir: &Path,
5032 args_specs: &[String],
5033 read_stdin: bool,
5034) -> Result<(Vec<String>, Vec<String>, bool, Vec<String>)> {
5035 let mut positive = Vec::new();
5036 let mut negative = Vec::new();
5037
5038 for spec in args_specs {
5039 let (pos, neg) = split_revision_token(spec);
5040 positive.extend(pos);
5041 negative.extend(neg);
5042 }
5043
5044 if !read_stdin {
5045 return Ok((positive, negative, false, Vec::new()));
5046 }
5047
5048 let (s_pos, s_neg, stdin_all_refs, stdin_paths) = read_revisions_from_stdin_lines(git_dir)?;
5049 positive.extend(s_pos);
5050 negative.extend(s_neg);
5051
5052 Ok((positive, negative, stdin_all_refs, stdin_paths))
5053}
5054
5055pub fn tag_targets(git_dir: &Path) -> Result<HashSet<ObjectId>> {
5057 Ok(refs::list_refs(git_dir, "refs/tags/")?
5058 .into_iter()
5059 .map(|(_, oid)| oid)
5060 .collect())
5061}
5062
5063pub(crate) struct CommitGraph<'r> {
5064 repo: &'r Repository,
5065 first_parent_only: bool,
5066 parents: HashMap<ObjectId, Vec<ObjectId>>,
5067 committer_time: HashMap<ObjectId, i64>,
5068 author_time: HashMap<ObjectId, i64>,
5069 shallow_boundaries: HashSet<ObjectId>,
5070 graft_parents: HashMap<ObjectId, Vec<ObjectId>>,
5071}
5072
5073impl<'r> CommitGraph<'r> {
5074 pub(crate) fn new(repo: &'r Repository, first_parent_only: bool) -> Self {
5075 let shallow_boundaries = load_shallow_boundaries(&repo.git_dir);
5076 let graft_parents = crate::rev_parse::load_graft_parents(&repo.git_dir);
5077 Self {
5078 repo,
5079 first_parent_only,
5080 parents: HashMap::new(),
5081 committer_time: HashMap::new(),
5082 author_time: HashMap::new(),
5083 shallow_boundaries,
5084 graft_parents,
5085 }
5086 }
5087
5088 pub(crate) fn parents_of(&mut self, oid: ObjectId) -> Result<Vec<ObjectId>> {
5089 self.populate(oid)?;
5090 Ok(self.parents.get(&oid).cloned().unwrap_or_default())
5091 }
5092
5093 fn committer_time(&mut self, oid: ObjectId) -> i64 {
5094 if self.populate(oid).is_err() {
5095 return 0;
5096 }
5097 self.committer_time.get(&oid).copied().unwrap_or(0)
5098 }
5099
5100 fn author_time(&mut self, oid: ObjectId) -> i64 {
5101 if self.populate(oid).is_err() {
5102 return 0;
5103 }
5104 self.author_time.get(&oid).copied().unwrap_or(0)
5105 }
5106
5107 fn sort_key(&mut self, oid: ObjectId, author: bool) -> i64 {
5108 if author {
5109 self.author_time(oid)
5110 } else {
5111 self.committer_time(oid)
5112 }
5113 }
5114
5115 fn populate(&mut self, oid: ObjectId) -> Result<()> {
5116 if self.parents.contains_key(&oid) {
5117 return Ok(());
5118 }
5119 let commit = load_commit(self.repo, oid)?;
5120 let mut parents = if self.shallow_boundaries.contains(&oid) {
5122 Vec::new()
5123 } else {
5124 commit.parents
5125 };
5126 if let Some(graft_parents) = self.graft_parents.get(&oid) {
5127 parents = graft_parents.clone();
5128 }
5129 if self.first_parent_only && parents.len() > 1 {
5130 parents.truncate(1);
5131 }
5132 self.committer_time
5133 .insert(oid, committer_unix_seconds_for_ordering(&commit.committer));
5134 self.author_time
5135 .insert(oid, committer_unix_seconds_for_ordering(&commit.author));
5136 self.parents.insert(oid, parents);
5137 Ok(())
5138 }
5139}
5140
5141fn load_shallow_boundaries(git_dir: &Path) -> HashSet<ObjectId> {
5143 let shallow_path = git_dir.join("shallow");
5144 let mut set = HashSet::new();
5145 if let Ok(contents) = fs::read_to_string(&shallow_path) {
5146 for line in contents.lines() {
5147 let line = line.trim();
5148 if !line.is_empty() {
5149 if let Ok(oid) = line.parse::<ObjectId>() {
5150 set.insert(oid);
5151 }
5152 }
5153 }
5154 }
5155 set
5156}
5157
5158fn commit_tips_from_ref_pairs(
5159 repo: &Repository,
5160 pairs: &[(String, ObjectId)],
5161 exclusions: &RefExclusions,
5162) -> Result<Vec<ObjectId>> {
5163 let namespace_prefix = git_namespace_prefix();
5164 let mut raw = Vec::new();
5165 for (refname, oid) in pairs {
5166 if exclusions.ref_excluded(strip_git_namespace(refname, &namespace_prefix), refname) {
5167 continue;
5168 }
5169 raw.push(*oid);
5170 }
5171 peel_ref_oids_to_unique_commits(repo, raw)
5172}
5173
5174fn peel_ref_oids_to_unique_commits(repo: &Repository, raw: Vec<ObjectId>) -> Result<Vec<ObjectId>> {
5175 let mut out = Vec::new();
5176 let mut seen = HashSet::new();
5177 for oid in raw {
5178 match peel_to_commit(repo, oid) {
5179 Ok(commit_oid) if seen.insert(commit_oid) => out.push(commit_oid),
5180 Err(_) => {}
5181 _ => {}
5182 }
5183 }
5184 out.sort();
5185 Ok(out)
5186}
5187
5188fn compute_simplify_by_decoration_keep_set(
5203 graph: &mut CommitGraph,
5204 ordered: &[ObjectId],
5205 decorated: &HashSet<ObjectId>,
5206) -> Result<HashSet<ObjectId>> {
5207 let in_walk: HashSet<ObjectId> = ordered.iter().copied().collect();
5208 let mut rewritten: HashMap<ObjectId, Vec<ObjectId>> = HashMap::new();
5212 let mut keep: HashSet<ObjectId> = HashSet::new();
5213
5214 for &oid in ordered.iter().rev() {
5216 let raw_parents: Vec<ObjectId> = graph
5217 .parents_of(oid)?
5218 .into_iter()
5219 .filter(|p| in_walk.contains(p))
5220 .collect();
5221
5222 let mut relevant: Vec<ObjectId> = Vec::new();
5225 let mut seen = HashSet::new();
5226 for parent in &raw_parents {
5227 if keep.contains(parent) {
5228 if seen.insert(*parent) {
5229 relevant.push(*parent);
5230 }
5231 } else if let Some(reps) = rewritten.get(parent) {
5232 for &rep in reps {
5233 if seen.insert(rep) {
5234 relevant.push(rep);
5235 }
5236 }
5237 }
5238 }
5239
5240 if relevant.len() > 1 {
5243 relevant = remove_redundant_relevant_parents(graph, &relevant, &keep)?;
5244 }
5245
5246 let is_root = raw_parents.is_empty();
5247 let kept = decorated.contains(&oid) || is_root || relevant.len() >= 2;
5248 if kept {
5249 keep.insert(oid);
5250 rewritten.insert(oid, vec![oid]);
5252 } else {
5253 rewritten.insert(oid, relevant);
5255 }
5256 }
5257
5258 Ok(keep)
5259}
5260
5261fn remove_redundant_relevant_parents(
5264 graph: &mut CommitGraph,
5265 relevant: &[ObjectId],
5266 keep: &HashSet<ObjectId>,
5267) -> Result<Vec<ObjectId>> {
5268 let mut out = Vec::new();
5269 for (i, &candidate) in relevant.iter().enumerate() {
5270 let mut redundant = false;
5271 for (j, &other) in relevant.iter().enumerate() {
5272 if i == j {
5273 continue;
5274 }
5275 if relevant_parent_reaches(graph, other, candidate, keep)? {
5276 redundant = true;
5277 break;
5278 }
5279 }
5280 if !redundant {
5281 out.push(candidate);
5282 }
5283 }
5284 Ok(out)
5285}
5286
5287fn relevant_parent_reaches(
5290 graph: &mut CommitGraph,
5291 start: ObjectId,
5292 target: ObjectId,
5293 _keep: &HashSet<ObjectId>,
5294) -> Result<bool> {
5295 if start == target {
5296 return Ok(false);
5298 }
5299 let mut stack = vec![start];
5300 let mut seen = HashSet::new();
5301 while let Some(oid) = stack.pop() {
5302 if !seen.insert(oid) {
5303 continue;
5304 }
5305 for parent in graph.parents_of(oid)? {
5306 if parent == target {
5307 return Ok(true);
5308 }
5309 stack.push(parent);
5310 }
5311 }
5312 Ok(false)
5313}
5314
5315fn all_ref_tips(repo: &Repository, exclusions: &RefExclusions) -> Result<Vec<ObjectId>> {
5316 let mut pairs = Vec::new();
5317 if let Ok(head) = refs::resolve_ref(&repo.git_dir, "HEAD") {
5318 pairs.push(("HEAD".to_owned(), head));
5319 }
5320 pairs.extend(refs::list_refs(&repo.git_dir, "refs/")?);
5321 commit_tips_from_ref_pairs(repo, &pairs, exclusions)
5322}
5323
5324fn filter_forces_bitmap_fallback(filter: Option<&ObjectFilter>) -> bool {
5331 match filter {
5332 None => false,
5333 Some(ObjectFilter::SparseOid(_)) | Some(ObjectFilter::TreeDepth(_)) => true,
5334 Some(ObjectFilter::Combine(parts)) => {
5335 parts.iter().any(|p| filter_forces_bitmap_fallback(Some(p)))
5336 }
5337 _ => false,
5338 }
5339}
5340
5341fn pack_bitmap_present(repo: &Repository) -> bool {
5345 let pack_dir = repo.odb.objects_dir().join("pack");
5346 let Ok(rd) = std::fs::read_dir(&pack_dir) else {
5347 return false;
5348 };
5349 rd.filter_map(|e| e.ok()).any(|e| {
5350 e.path()
5351 .extension()
5352 .and_then(|s| s.to_str())
5353 .is_some_and(|ext| ext == "bitmap")
5354 })
5355}
5356
5357fn all_ref_non_commit_object_roots(repo: &Repository) -> Result<Vec<RootObject>> {
5364 let mut roots = Vec::new();
5365 let mut seen: HashSet<ObjectId> = HashSet::new();
5366 for (refname, oid) in refs::list_refs(&repo.git_dir, "refs/")? {
5367 let Ok(object) = repo.odb.read(&oid) else {
5368 continue;
5369 };
5370 match object.kind {
5371 ObjectKind::Commit => continue,
5372 ObjectKind::Tree | ObjectKind::Blob => {
5373 if seen.insert(oid) {
5374 roots.push(RootObject {
5375 oid,
5376 input: refname,
5377 expected_kind: None,
5378 root_path: None,
5379 wrap_with_tag: None,
5380 });
5381 }
5382 }
5383 ObjectKind::Tag => {
5384 let Ok(tag) = parse_tag(&object.data) else {
5385 continue;
5386 };
5387 let Some(expected_kind) = ExpectedObjectKind::from_tag_type(&tag.object_type)
5388 else {
5389 continue;
5390 };
5391 if expected_kind == ExpectedObjectKind::Commit {
5393 continue;
5394 }
5395 if seen.insert(tag.object) {
5396 roots.push(RootObject {
5397 oid: tag.object,
5398 input: refname,
5399 expected_kind: Some(expected_kind),
5400 root_path: None,
5401 wrap_with_tag: Some(oid),
5402 });
5403 }
5404 }
5405 }
5406 }
5407 Ok(roots)
5408}
5409
5410pub fn commit_tips_from_named_refs(
5412 repo: &Repository,
5413 pairs: &[(String, ObjectId)],
5414 exclusions: &RefExclusions,
5415) -> Result<Vec<ObjectId>> {
5416 commit_tips_from_ref_pairs(repo, pairs, exclusions)
5417}
5418
5419#[must_use]
5424pub fn shallow_boundary_oids(git_dir: &Path) -> HashSet<ObjectId> {
5425 crate::shallow::load_shallow_boundaries(git_dir)
5426}
5427
5428#[must_use]
5434pub fn shallow_borders_reachable_from_wants(
5435 repo: &Repository,
5436 wants: &[ObjectId],
5437) -> Vec<ObjectId> {
5438 let boundaries = shallow_boundary_oids(&repo.git_dir);
5439 if boundaries.is_empty() || wants.is_empty() {
5440 return Vec::new();
5441 }
5442 let mut out: Vec<ObjectId> = Vec::new();
5443 let mut seen_out = HashSet::new();
5444 let mut visited = HashSet::new();
5445 let mut q: VecDeque<ObjectId> = wants.iter().copied().collect();
5446 while let Some(oid) = q.pop_front() {
5447 if !visited.insert(oid) {
5448 continue;
5449 }
5450 if boundaries.contains(&oid) {
5451 if seen_out.insert(oid) {
5452 out.push(oid);
5453 }
5454 continue;
5455 }
5456 for p in commit_parent_ids(repo, oid) {
5457 q.push_back(p);
5458 }
5459 }
5460 out.sort();
5461 out
5462}
5463
5464#[must_use]
5477pub fn shallow_grafts_for_upload_pack_deepen(
5478 repo: &Repository,
5479 wants: &[ObjectId],
5480 client_shallow: &[ObjectId],
5481 deepen: usize,
5482) -> Vec<ObjectId> {
5483 if deepen == 0 || wants.is_empty() {
5484 return Vec::new();
5485 }
5486
5487 let server_shallow = shallow_boundary_oids(&repo.git_dir);
5488 let client_set: HashSet<ObjectId> = client_shallow.iter().copied().collect();
5489
5490 let min_hit = min_client_shallow_distance(repo, wants, &client_set, &server_shallow);
5498 let base = min_hit.unwrap_or(0);
5499 let target_depth = base.saturating_add(deepen);
5500
5501 let included = commits_within_parent_depth(repo, wants, target_depth, &server_shallow);
5502 border_commits_not_in_client_shallow(repo, &included, &client_set)
5503}
5504
5505fn commit_parent_ids(repo: &Repository, oid: ObjectId) -> Vec<ObjectId> {
5506 let Ok(obj) = repo.odb.read(&oid) else {
5507 return Vec::new();
5508 };
5509 if obj.kind != ObjectKind::Commit {
5510 return Vec::new();
5511 }
5512 parse_commit(&obj.data)
5513 .map(|c| c.parents)
5514 .unwrap_or_default()
5515}
5516
5517fn min_client_shallow_distance(
5518 repo: &Repository,
5519 wants: &[ObjectId],
5520 client_shallow: &HashSet<ObjectId>,
5521 server_shallow: &HashSet<ObjectId>,
5522) -> Option<usize> {
5523 if client_shallow.is_empty() {
5524 return None;
5525 }
5526 let mut best: Option<usize> = None;
5527 let mut dist: HashMap<ObjectId, usize> = HashMap::new();
5528 let mut q: VecDeque<(ObjectId, usize)> = VecDeque::new();
5529 for &w in wants {
5530 dist.insert(w, 0);
5531 q.push_back((w, 0));
5532 }
5533 while let Some((oid, d)) = q.pop_front() {
5534 if client_shallow.contains(&oid) {
5535 best = Some(best.map(|b| b.min(d)).unwrap_or(d));
5536 }
5537 if server_shallow.contains(&oid) {
5538 continue;
5539 }
5540 for p in commit_parent_ids(repo, oid) {
5541 let nd = d.saturating_add(1);
5542 let prev = dist.get(&p).copied().unwrap_or(usize::MAX);
5543 if nd < prev {
5544 dist.insert(p, nd);
5545 q.push_back((p, nd));
5546 }
5547 }
5548 }
5549 best
5550}
5551
5552fn commits_within_parent_depth(
5553 repo: &Repository,
5554 wants: &[ObjectId],
5555 max_depth: usize,
5556 server_shallow: &HashSet<ObjectId>,
5557) -> HashSet<ObjectId> {
5558 let mut best_depth: HashMap<ObjectId, usize> = HashMap::new();
5559 let mut q: VecDeque<(ObjectId, usize)> = VecDeque::new();
5560 for &w in wants {
5561 best_depth.insert(w, 1);
5562 q.push_back((w, 1));
5563 }
5564 while let Some((oid, depth)) = q.pop_front() {
5565 if depth > max_depth {
5566 continue;
5567 }
5568 if best_depth.get(&oid).copied() != Some(depth) {
5569 continue;
5570 }
5571 if depth == max_depth {
5572 continue;
5573 }
5574 if server_shallow.contains(&oid) {
5575 continue;
5576 }
5577 for p in commit_parent_ids(repo, oid) {
5578 let nd = depth.saturating_add(1);
5579 if nd > max_depth {
5580 continue;
5581 }
5582 let prev = best_depth.get(&p).copied().unwrap_or(usize::MAX);
5583 if nd < prev {
5584 best_depth.insert(p, nd);
5585 q.push_back((p, nd));
5586 }
5587 }
5588 }
5589 best_depth.into_keys().collect()
5590}
5591
5592fn border_commits_not_in_client_shallow(
5593 repo: &Repository,
5594 included: &HashSet<ObjectId>,
5595 client_shallow: &HashSet<ObjectId>,
5596) -> Vec<ObjectId> {
5597 let mut out = Vec::new();
5598 let mut seen_out = HashSet::new();
5599 for &c in included {
5600 if client_shallow.contains(&c) {
5601 continue;
5602 }
5603 let parents = commit_parent_ids(repo, c);
5604 let is_border = parents.iter().any(|p| !included.contains(p));
5605 if is_border && seen_out.insert(c) {
5606 out.push(c);
5607 }
5608 }
5609 out.sort();
5610 out
5611}
5612
5613pub fn shallow_grafts_for_upload_pack_rev_list(
5618 repo: &Repository,
5619 wants: &[ObjectId],
5620 client_shallow: &[ObjectId],
5621 deepen_since: Option<i64>,
5622 deepen_not: &[ObjectId],
5623) -> Result<Vec<ObjectId>> {
5624 if wants.is_empty() || (deepen_since.is_none() && deepen_not.is_empty()) {
5625 return Ok(Vec::new());
5626 }
5627
5628 let positive: Vec<String> = wants.iter().map(|o| o.to_hex()).collect();
5629 let negative: Vec<String> = deepen_not
5630 .iter()
5631 .map(|o| format!("^{}", o.to_hex()))
5632 .collect();
5633
5634 let options = RevListOptions {
5635 since_cutoff: deepen_since,
5636 missing_action: MissingAction::Allow,
5637 ..Default::default()
5638 };
5639
5640 let res = rev_list(repo, &positive, &negative, &options)?;
5641 let included: HashSet<ObjectId> = res.commits.iter().copied().collect();
5642 let client_set: HashSet<ObjectId> = client_shallow.iter().copied().collect();
5643 Ok(border_commits_not_in_client_shallow(
5644 repo,
5645 &included,
5646 &client_set,
5647 ))
5648}
5649
5650#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5651struct CommitDateKey {
5652 oid: ObjectId,
5653 date: i64,
5654 seq: u64,
5660}
5661
5662impl Ord for CommitDateKey {
5663 fn cmp(&self, other: &Self) -> Ordering {
5664 match self.date.cmp(&other.date) {
5665 Ordering::Equal => match other.seq.cmp(&self.seq) {
5667 Ordering::Equal => self.oid.cmp(&other.oid),
5668 ord => ord,
5669 },
5670 ord => ord,
5671 }
5672 }
5673}
5674
5675impl PartialOrd for CommitDateKey {
5676 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
5677 Some(self.cmp(other))
5678 }
5679}
5680
5681pub fn read_lines(path: &Path) -> Result<Vec<String>> {
5687 let content = fs::read_to_string(path)?;
5688 Ok(content.lines().map(|line| line.to_owned()).collect())
5689}
5690
5691#[must_use]
5693pub fn is_symmetric_diff(token: &str) -> bool {
5694 token.contains("...") && !token.contains("....")
5695}
5696
5697#[must_use]
5699pub fn split_symmetric_diff(token: &str) -> Option<(String, String)> {
5700 token
5701 .split_once("...")
5702 .map(|(l, r)| (l.to_owned(), r.to_owned()))
5703}
5704
5705#[derive(Debug, Default)]
5708struct TreeWalkState {
5709 seen_at_depth: HashMap<ObjectId, u64>,
5710}
5711
5712impl TreeWalkState {
5713 fn new() -> Self {
5714 Self::default()
5715 }
5716
5717 fn should_skip_tree(&mut self, oid: ObjectId, depth: u64) -> bool {
5720 match self.seen_at_depth.get(&oid).copied() {
5721 None => {
5722 self.seen_at_depth.insert(oid, depth);
5723 false
5724 }
5725 Some(prev) if depth >= prev => true,
5726 Some(_) => {
5727 self.seen_at_depth.insert(oid, depth);
5728 false
5729 }
5730 }
5731 }
5732}
5733
5734fn collect_tree_closure_objects(
5736 repo: &Repository,
5737 tree_oid: ObjectId,
5738 into: &mut HashSet<ObjectId>,
5739 missing_action: MissingAction,
5740 missing: &mut Vec<ObjectId>,
5741 missing_seen: &mut HashSet<ObjectId>,
5742) -> Result<()> {
5743 let mut stack = vec![tree_oid];
5744 let mut expanded_trees = HashSet::new();
5745 while let Some(t) = stack.pop() {
5746 if !expanded_trees.insert(t) {
5747 continue;
5748 }
5749 into.insert(t);
5750 let object = match repo.odb.read(&t) {
5751 Ok(o) => o,
5752 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
5753 if missing_action.reports_missing() && missing_seen.insert(t) {
5754 missing.push(t);
5755 }
5756 continue;
5757 }
5758 Err(e) => return Err(e),
5759 };
5760 if object.kind != ObjectKind::Tree {
5761 continue;
5762 }
5763 let entries = parse_tree(&object.data)?;
5764 for entry in entries {
5765 if entry.mode == 0o160000 {
5766 continue;
5767 }
5768 into.insert(entry.oid);
5769 if entry.mode == 0o040000 {
5770 stack.push(entry.oid);
5771 }
5772 }
5773 }
5774 Ok(())
5775}
5776
5777fn union_parent_reachable_objects(
5778 repo: &Repository,
5779 parents: &[ObjectId],
5780 missing_action: MissingAction,
5781 missing: &mut Vec<ObjectId>,
5782 missing_seen: &mut HashSet<ObjectId>,
5783) -> Result<HashSet<ObjectId>> {
5784 let mut out = HashSet::new();
5785 for &p in parents {
5786 let commit = match load_commit(repo, p) {
5787 Ok(c) => c,
5788 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
5789 if missing_action.reports_missing() && missing_seen.insert(p) {
5790 missing.push(p);
5791 }
5792 continue;
5793 }
5794 Err(e) => return Err(e),
5795 };
5796 collect_tree_closure_objects(
5797 repo,
5798 commit.tree,
5799 &mut out,
5800 missing_action,
5801 missing,
5802 missing_seen,
5803 )?;
5804 }
5805 Ok(out)
5806}
5807
5808#[allow(dead_code)]
5811fn collect_reachable_objects(
5812 repo: &Repository,
5813 graph: &mut CommitGraph<'_>,
5814 commits: &[ObjectId],
5815 object_roots: &[RootObject],
5816 tip_annotated_tags: &HashMap<ObjectId, ObjectId>,
5817 filter: Option<&ObjectFilter>,
5818 filter_provided: bool,
5819 missing_action: MissingAction,
5820 sparse_lines: Option<&[String]>,
5821 skip_trees_for_type_filter: bool,
5822 omit_object_paths: bool,
5823 packed_set: Option<&HashSet<ObjectId>>,
5824 collect_tree_omits: bool,
5825) -> Result<(Vec<(ObjectId, String)>, Vec<ObjectId>, Vec<ObjectId>)> {
5826 let mut tree_state = TreeWalkState::new();
5827 let mut top_tree_omit =
5828 walk_needs_top_tree_omit_set(filter, collect_tree_omits).then(HashSet::<ObjectId>::new);
5829 let mut combine_states = CombineSubState::prepare_sub_states(filter, collect_tree_omits);
5830 let mut emitted = HashSet::new();
5831 let mut result = Vec::new();
5832 let mut omitted = Vec::new();
5833 let mut missing = Vec::new();
5834 let mut missing_seen = HashSet::new();
5835 for &commit_oid in commits {
5836 let commit = match load_commit(repo, commit_oid) {
5837 Ok(commit) => commit,
5838 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
5839 if missing_seen.insert(commit_oid) && missing_action.reports_missing() {
5840 missing.push(commit_oid);
5841 }
5842 continue;
5843 }
5844 Err(err) => return Err(err),
5845 };
5846 let parents = graph.parents_of(commit_oid)?;
5847 let parent_union = union_parent_reachable_objects(
5848 repo,
5849 &parents,
5850 missing_action,
5851 &mut missing,
5852 &mut missing_seen,
5853 )?;
5854 if let Some(&tag_oid) = tip_annotated_tags.get(&commit_oid) {
5855 if emitted.insert(tag_oid) {
5856 result.push((tag_oid, "tag".to_owned()));
5857 }
5858 }
5859 collect_tree_objects_filtered(
5860 repo,
5861 commit.tree,
5862 "",
5863 0,
5864 false,
5865 Some(&parent_union),
5866 &mut tree_state,
5867 &mut emitted,
5868 &mut result,
5869 &mut omitted,
5870 &mut missing,
5871 &mut missing_seen,
5872 filter,
5873 filter_provided,
5874 missing_action,
5875 sparse_lines,
5876 skip_trees_for_type_filter,
5877 omit_object_paths,
5878 packed_set,
5879 collect_tree_omits,
5880 &mut top_tree_omit,
5881 &mut combine_states,
5882 )?;
5883 }
5884
5885 for root in object_roots {
5886 collect_root_object(
5887 repo,
5888 root,
5889 &mut tree_state,
5890 &mut emitted,
5891 &mut result,
5892 &mut omitted,
5893 &mut missing,
5894 &mut missing_seen,
5895 filter,
5896 filter_provided,
5897 missing_action,
5898 sparse_lines,
5899 skip_trees_for_type_filter,
5900 omit_object_paths,
5901 packed_set,
5902 collect_tree_omits,
5903 &mut top_tree_omit,
5904 &mut combine_states,
5905 )?;
5906 }
5907
5908 Ok((result, omitted, missing))
5909}
5910
5911fn excluded_object_root_ids(
5912 repo: &Repository,
5913 object_roots: &[RootObject],
5914 missing_action: MissingAction,
5915 sparse_lines: Option<&[String]>,
5916 skip_trees_for_type_filter: bool,
5917 omit_object_paths: bool,
5918 collect_tree_omits: bool,
5919) -> Result<HashSet<ObjectId>> {
5920 if object_roots.is_empty() {
5921 return Ok(HashSet::new());
5922 }
5923
5924 let mut tree_state = TreeWalkState::new();
5925 let mut emitted = HashSet::new();
5926 let mut objects = Vec::new();
5927 let mut omitted = Vec::new();
5928 let mut missing = Vec::new();
5929 let mut missing_seen = HashSet::new();
5930 let mut top_tree_omit = None;
5931 let mut combine_states = None;
5932
5933 for root in object_roots {
5934 collect_root_object(
5935 repo,
5936 root,
5937 &mut tree_state,
5938 &mut emitted,
5939 &mut objects,
5940 &mut omitted,
5941 &mut missing,
5942 &mut missing_seen,
5943 None,
5944 false,
5945 missing_action,
5946 sparse_lines,
5947 skip_trees_for_type_filter,
5948 omit_object_paths,
5949 None,
5950 collect_tree_omits,
5951 &mut top_tree_omit,
5952 &mut combine_states,
5953 )?;
5954 }
5955
5956 Ok(objects.into_iter().map(|(oid, _)| oid).collect())
5957}
5958
5959fn retain_objects_matching_pathspecs(objects: &mut Vec<(ObjectId, String)>, pathspecs: &[String]) {
5960 objects.retain(|(_, path)| {
5961 path.is_empty()
5962 || crate::pathspec::matches_pathspec_list(path, pathspecs)
5963 || pathspecs
5964 .iter()
5965 .any(|spec| crate::pathspec::pathspec_wants_descent_into_tree(spec, path))
5966 });
5967}
5968
5969fn collect_pathspec_matching_tree_objects(
5970 repo: &Repository,
5971 commits: &[ObjectId],
5972 pathspecs: &[String],
5973) -> Result<Vec<(ObjectId, String)>> {
5974 let mut seen = HashSet::new();
5975 let mut out = Vec::new();
5976 for commit_oid in commits {
5977 let commit = load_commit(repo, *commit_oid)?;
5978 collect_pathspec_matching_tree_objects_inner(
5979 repo,
5980 commit.tree,
5981 "",
5982 pathspecs,
5983 &mut seen,
5984 &mut out,
5985 )?;
5986 }
5987 Ok(out)
5988}
5989
5990fn collect_pathspec_matching_tree_objects_inner(
5991 repo: &Repository,
5992 tree_oid: ObjectId,
5993 prefix: &str,
5994 pathspecs: &[String],
5995 seen: &mut HashSet<ObjectId>,
5996 out: &mut Vec<(ObjectId, String)>,
5997) -> Result<()> {
5998 let object = repo.odb.read(&tree_oid)?;
5999 if object.kind != ObjectKind::Tree {
6000 return Err(Error::CorruptObject(format!(
6001 "object {tree_oid} is not a tree"
6002 )));
6003 }
6004 let entries = parse_tree(&object.data)?;
6005 for entry in entries {
6006 if entry.mode == 0o160000 {
6007 continue;
6008 }
6009 let name = String::from_utf8_lossy(&entry.name);
6010 let path = if prefix.is_empty() {
6011 name.into_owned()
6012 } else {
6013 format!("{prefix}/{name}")
6014 };
6015 let is_tree = entry.mode == 0o040000;
6016 let matches = crate::pathspec::matches_pathspec_list(&path, pathspecs);
6017 let wants_descent = pathspecs
6018 .iter()
6019 .any(|spec| crate::pathspec::pathspec_wants_descent_into_tree(spec, &path));
6020 if (matches || (is_tree && wants_descent)) && seen.insert(entry.oid) {
6021 out.push((entry.oid, path.clone()));
6022 }
6023 if is_tree && wants_descent {
6024 collect_pathspec_matching_tree_objects_inner(
6025 repo, entry.oid, &path, pathspecs, seen, out,
6026 )?;
6027 }
6028 }
6029 Ok(())
6030}
6031
6032fn collect_reachable_objects_segmented(
6038 repo: &Repository,
6039 graph: &mut CommitGraph<'_>,
6040 commits: &[ObjectId],
6041 object_roots: &[RootObject],
6042 tip_annotated_tags: &HashMap<ObjectId, ObjectId>,
6043 filter: Option<&ObjectFilter>,
6044 filter_provided: bool,
6045 missing_action: MissingAction,
6046 sparse_lines: Option<&[String]>,
6047 skip_trees_for_type_filter: bool,
6048 omit_object_paths: bool,
6049 packed_set: Option<&HashSet<ObjectId>>,
6050 collect_tree_omits: bool,
6051) -> Result<(
6052 Vec<(ObjectId, String)>,
6053 Vec<ObjectId>,
6054 Vec<ObjectId>,
6055 Vec<Vec<(ObjectId, String)>>,
6056)> {
6057 let mut emitted = HashSet::new();
6058 let mut result = Vec::new();
6059 let mut omitted = Vec::new();
6060 let mut missing = Vec::new();
6061 let mut missing_seen = HashSet::new();
6062 let mut segments: Vec<Vec<(ObjectId, String)>> = Vec::with_capacity(commits.len() + 1);
6063 let mut tree_state = TreeWalkState::new();
6064 let mut top_tree_omit =
6065 walk_needs_top_tree_omit_set(filter, collect_tree_omits).then(HashSet::<ObjectId>::new);
6066 let mut combine_states = CombineSubState::prepare_sub_states(filter, collect_tree_omits);
6067
6068 for &commit_oid in commits {
6069 let start = result.len();
6070 let commit = match load_commit(repo, commit_oid) {
6071 Ok(commit) => commit,
6072 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
6073 if missing_action.reports_missing() && missing_seen.insert(commit_oid) {
6074 missing.push(commit_oid);
6075 }
6076 segments.push(Vec::new());
6077 continue;
6078 }
6079 Err(err) => return Err(err),
6080 };
6081 if let Some(&tag_oid) = tip_annotated_tags.get(&commit_oid) {
6082 if emitted.insert(tag_oid) {
6083 result.push((tag_oid, "tag".to_owned()));
6084 }
6085 }
6086 let parents = graph.parents_of(commit_oid)?;
6087 let parent_union = if filter.is_some() {
6094 None
6095 } else {
6096 Some(union_parent_reachable_objects(
6097 repo,
6098 &parents,
6099 missing_action,
6100 &mut missing,
6101 &mut missing_seen,
6102 )?)
6103 };
6104 collect_tree_objects_filtered(
6105 repo,
6106 commit.tree,
6107 "",
6108 0,
6109 false,
6110 parent_union.as_ref(),
6111 &mut tree_state,
6112 &mut emitted,
6113 &mut result,
6114 &mut omitted,
6115 &mut missing,
6116 &mut missing_seen,
6117 filter,
6118 filter_provided,
6119 missing_action,
6120 sparse_lines,
6121 skip_trees_for_type_filter,
6122 omit_object_paths,
6123 packed_set,
6124 collect_tree_omits,
6125 &mut top_tree_omit,
6126 &mut combine_states,
6127 )?;
6128 segments.push(result[start..].to_vec());
6129 }
6130
6131 let roots_start = result.len();
6132 for root in object_roots {
6133 collect_root_object(
6134 repo,
6135 root,
6136 &mut tree_state,
6137 &mut emitted,
6138 &mut result,
6139 &mut omitted,
6140 &mut missing,
6141 &mut missing_seen,
6142 filter,
6143 filter_provided,
6144 missing_action,
6145 sparse_lines,
6146 skip_trees_for_type_filter,
6147 omit_object_paths,
6148 packed_set,
6149 collect_tree_omits,
6150 &mut top_tree_omit,
6151 &mut combine_states,
6152 )?;
6153 }
6154 segments.push(result[roots_start..].to_vec());
6155
6156 Ok((result, omitted, missing, segments))
6157}
6158
6159#[derive(Clone, Copy, Debug)]
6160struct ListFilterBits {
6161 mark_seen: bool,
6162 do_show: bool,
6163 skip_tree: bool,
6164}
6165
6166impl ListFilterBits {
6167 fn merge_combine(subs: &[ListFilterBits], sub_skipping: &[bool]) -> Self {
6168 let mut out = ListFilterBits {
6169 mark_seen: true,
6170 do_show: true,
6171 skip_tree: true,
6172 };
6173 for (sub, skipping) in subs.iter().zip(sub_skipping.iter()) {
6174 if !sub.do_show {
6175 out.do_show = false;
6176 }
6177 if !sub.mark_seen {
6178 out.mark_seen = false;
6179 }
6180 if !skipping {
6181 out.skip_tree = false;
6182 }
6183 }
6184 out
6185 }
6186}
6187
6188fn walk_needs_top_tree_omit_set(filter: Option<&ObjectFilter>, collect_omits: bool) -> bool {
6189 collect_omits && matches!(filter, Some(ObjectFilter::TreeDepth(_)))
6190}
6191
6192fn trace_skip_tree_contents(prefix: &str) {
6193 let Ok(trace_val) = std::env::var("GIT_TRACE") else {
6194 return;
6195 };
6196 if trace_val.is_empty() || trace_val == "0" || trace_val.eq_ignore_ascii_case("false") {
6197 return;
6198 }
6199 let path = if prefix.is_empty() {
6200 String::new()
6201 } else {
6202 format!("{prefix}/")
6203 };
6204 let line = format!("Skipping contents of tree {path}...\n");
6205 match trace_val.as_str() {
6206 "1" | "true" | "2" => {
6207 let _ = std::io::stderr().write_all(line.as_bytes());
6208 }
6209 path_dest => {
6210 if let Ok(mut f) = std::fs::OpenOptions::new()
6211 .create(true)
6212 .append(true)
6213 .open(path_dest)
6214 {
6215 let _ = f.write_all(line.as_bytes());
6216 }
6217 }
6218 }
6219}
6220
6221fn tree_depth_begin_tree(
6222 tree_oid: ObjectId,
6223 depth: u64,
6224 exclude_depth: u64,
6225 tree_state: &mut TreeWalkState,
6226 tree_omit_set: &mut Option<HashSet<ObjectId>>,
6227 collect_omits: bool,
6228) -> ListFilterBits {
6229 let include_it = depth < exclude_depth;
6230 let already_seen = tree_state.should_skip_tree(tree_oid, depth);
6231 if already_seen {
6232 return ListFilterBits {
6233 mark_seen: false,
6234 do_show: false,
6235 skip_tree: true,
6236 };
6237 }
6238
6239 let been_omitted = if collect_omits {
6240 if let Some(omits) = tree_omit_set.as_mut() {
6241 if include_it {
6242 omits.remove(&tree_oid);
6243 false
6244 } else {
6245 !omits.insert(tree_oid)
6246 }
6247 } else {
6248 false
6249 }
6250 } else {
6251 false
6252 };
6253
6254 let skip_tree = if include_it {
6255 false
6256 } else {
6257 !(collect_omits && !been_omitted)
6258 };
6259
6260 let do_show = include_it;
6261 ListFilterBits {
6262 mark_seen: true,
6263 do_show,
6264 skip_tree,
6265 }
6266}
6267
6268fn tree_depth_blob(
6269 oid: ObjectId,
6270 _size: u64,
6271 parent_depth: u64,
6272 exclude_depth: u64,
6273 tree_omit_set: &mut Option<HashSet<ObjectId>>,
6274 collect_omits: bool,
6275) -> ListFilterBits {
6276 let include_it = parent_depth.saturating_add(1) < exclude_depth;
6277 if collect_omits {
6278 if let Some(omits) = tree_omit_set.as_mut() {
6279 if include_it {
6280 omits.remove(&oid);
6281 } else {
6282 omits.insert(oid);
6283 }
6284 }
6285 }
6286 ListFilterBits {
6287 mark_seen: include_it,
6289 do_show: include_it,
6290 skip_tree: false,
6291 }
6292}
6293
6294fn filter_object_bits_tree_begin(
6295 f: &ObjectFilter,
6296 tree_oid: ObjectId,
6297 depth: u64,
6298 tree_state: &mut TreeWalkState,
6299 tree_omit_set: &mut Option<HashSet<ObjectId>>,
6300 collect_omits: bool,
6301 sub_states: Option<&mut [CombineSubState]>,
6302) -> ListFilterBits {
6303 match f {
6304 ObjectFilter::BlobNone | ObjectFilter::BlobLimit(_) => ListFilterBits {
6305 mark_seen: true,
6306 do_show: true,
6307 skip_tree: false,
6308 },
6309 ObjectFilter::TreeDepth(excl) => tree_depth_begin_tree(
6310 tree_oid,
6311 depth,
6312 *excl,
6313 tree_state,
6314 tree_omit_set,
6315 collect_omits,
6316 ),
6317 ObjectFilter::SparseOid(_) => ListFilterBits {
6318 mark_seen: true,
6319 do_show: true,
6320 skip_tree: false,
6321 },
6322 ObjectFilter::ObjectType(k) => {
6323 let show = *k == FilterObjectKind::Tree;
6326 let skip_tree = matches!(k, FilterObjectKind::Commit | FilterObjectKind::Tag);
6327 ListFilterBits {
6328 mark_seen: true,
6329 do_show: show,
6330 skip_tree,
6331 }
6332 }
6333 ObjectFilter::Combine(parts) => {
6334 let Some(states) = sub_states else {
6337 return ListFilterBits::merge_combine(&[], &[]);
6338 };
6339 debug_assert_eq!(states.len(), parts.len());
6340 let mut bits = Vec::with_capacity(parts.len());
6341 let mut skipping = Vec::with_capacity(parts.len());
6342 for (i, p) in parts.iter().enumerate() {
6343 let b = filter_object_bits_tree_begin(
6344 p,
6345 tree_oid,
6346 depth,
6347 &mut states[i].tree_state,
6348 &mut states[i].tree_omit_set,
6349 collect_omits,
6350 None,
6351 );
6352 if b.skip_tree {
6353 states[i].is_skipping_tree = true;
6354 states[i].skip_tree_oid = Some(tree_oid);
6355 } else {
6356 states[i].is_skipping_tree = false;
6357 states[i].skip_tree_oid = None;
6358 }
6359 bits.push(b);
6360 skipping.push(states[i].is_skipping_tree);
6361 }
6362 ListFilterBits::merge_combine(&bits, &skipping)
6363 }
6364 }
6365}
6366
6367fn filter_object_bits_blob(
6368 f: &ObjectFilter,
6369 oid: ObjectId,
6370 size: u64,
6371 parent_depth: u64,
6372 tree_omit_set: &mut Option<HashSet<ObjectId>>,
6373 collect_omits: bool,
6374 sub_states: Option<&mut [CombineSubState]>,
6375) -> ListFilterBits {
6376 match f {
6377 ObjectFilter::BlobNone => ListFilterBits {
6378 mark_seen: true,
6379 do_show: false,
6380 skip_tree: false,
6381 },
6382 ObjectFilter::BlobLimit(limit) => {
6383 let include = size < *limit;
6384 ListFilterBits {
6385 mark_seen: true,
6386 do_show: include,
6387 skip_tree: false,
6388 }
6389 }
6390 ObjectFilter::TreeDepth(excl) => {
6391 tree_depth_blob(oid, size, parent_depth, *excl, tree_omit_set, collect_omits)
6392 }
6393 ObjectFilter::SparseOid(_) => ListFilterBits {
6394 mark_seen: true,
6395 do_show: true,
6396 skip_tree: false,
6397 },
6398 ObjectFilter::ObjectType(k) => {
6399 let show = *k == FilterObjectKind::Blob;
6400 ListFilterBits {
6401 mark_seen: true,
6402 do_show: show,
6403 skip_tree: false,
6404 }
6405 }
6406 ObjectFilter::Combine(parts) => {
6407 let Some(states) = sub_states else {
6410 return ListFilterBits::merge_combine(&[], &[]);
6411 };
6412 let mut bits = Vec::with_capacity(parts.len());
6413 let mut skipping = Vec::with_capacity(parts.len());
6414 for (i, p) in parts.iter().enumerate() {
6415 let b = filter_object_bits_blob(
6416 p,
6417 oid,
6418 size,
6419 parent_depth,
6420 &mut states[i].tree_omit_set,
6421 collect_omits,
6422 None,
6423 );
6424 bits.push(b);
6425 skipping.push(states[i].is_skipping_tree);
6426 }
6427 ListFilterBits::merge_combine(&bits, &skipping)
6428 }
6429 }
6430}
6431
6432#[derive(Debug)]
6433struct CombineSubState {
6434 tree_state: TreeWalkState,
6435 tree_omit_set: Option<HashSet<ObjectId>>,
6436 is_skipping_tree: bool,
6437 skip_tree_oid: Option<ObjectId>,
6438}
6439
6440impl CombineSubState {
6441 fn new(parts_len: usize, collect_omits: bool) -> Vec<Self> {
6442 (0..parts_len)
6443 .map(|_| Self {
6444 tree_state: TreeWalkState::new(),
6445 tree_omit_set: collect_omits.then(HashSet::new),
6446 is_skipping_tree: false,
6447 skip_tree_oid: None,
6448 })
6449 .collect()
6450 }
6451
6452 fn prepare_sub_states(
6453 filter: Option<&ObjectFilter>,
6454 collect_omits: bool,
6455 ) -> Option<Vec<CombineSubState>> {
6456 match filter {
6457 Some(ObjectFilter::Combine(parts)) => {
6458 Some(CombineSubState::new(parts.len(), collect_omits))
6459 }
6460 _ => None,
6461 }
6462 }
6463}
6464
6465#[allow(dead_code)]
6466fn collect_tree_objects_filtered(
6467 repo: &Repository,
6468 tree_oid: ObjectId,
6469 prefix: &str,
6470 depth: u64,
6471 explicit_root: bool,
6472 parent_union: Option<&HashSet<ObjectId>>,
6473 tree_state: &mut TreeWalkState,
6474 emitted: &mut HashSet<ObjectId>,
6475 result: &mut Vec<(ObjectId, String)>,
6476 omitted: &mut Vec<ObjectId>,
6477 missing: &mut Vec<ObjectId>,
6478 missing_seen: &mut HashSet<ObjectId>,
6479 filter: Option<&ObjectFilter>,
6480 filter_provided: bool,
6481 missing_action: MissingAction,
6482 sparse_lines: Option<&[String]>,
6483 skip_trees_for_type_filter: bool,
6484 omit_object_paths: bool,
6485 packed_set: Option<&HashSet<ObjectId>>,
6486 collect_tree_omits: bool,
6487 tree_omit_set: &mut Option<HashSet<ObjectId>>,
6488 combine_states: &mut Option<Vec<CombineSubState>>,
6489) -> Result<()> {
6490 if !explicit_root {
6491 if let Some(pu) = parent_union {
6492 if pu.contains(&tree_oid) {
6493 return Ok(());
6494 }
6495 }
6496 }
6497 let object = match repo.odb.read(&tree_oid) {
6498 Ok(object) => object,
6499 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
6500 if missing_action.reports_missing() && missing_seen.insert(tree_oid) {
6501 missing.push(tree_oid);
6502 }
6503 return Ok(());
6504 }
6505 Err(err) => return Err(err),
6506 };
6507 if object.kind != ObjectKind::Tree {
6508 return Err(Error::CorruptObject(format!(
6509 "object {tree_oid} is not a tree"
6510 )));
6511 }
6512
6513 let bits = match filter {
6514 None => ListFilterBits {
6515 mark_seen: true,
6516 do_show: true,
6517 skip_tree: false,
6518 },
6519 Some(f) => {
6520 if explicit_root && matches!(f, ObjectFilter::TreeDepth(0)) {
6521 ListFilterBits {
6522 mark_seen: true,
6523 do_show: true,
6524 skip_tree: true,
6525 }
6526 } else if explicit_root && !filter_provided {
6527 ListFilterBits {
6528 mark_seen: true,
6529 do_show: true,
6530 skip_tree: false,
6531 }
6532 } else {
6533 match f {
6534 ObjectFilter::Combine(_) => filter_object_bits_tree_begin(
6535 f,
6536 tree_oid,
6537 depth,
6538 tree_state,
6539 tree_omit_set,
6540 collect_tree_omits,
6541 combine_states.as_mut().map(Vec::as_mut_slice),
6542 ),
6543 _ => filter_object_bits_tree_begin(
6544 f,
6545 tree_oid,
6546 depth,
6547 tree_state,
6548 tree_omit_set,
6549 collect_tree_omits,
6550 None,
6551 ),
6552 }
6553 }
6554 }
6555 };
6556
6557 let tree_included = bits.do_show;
6560 if tree_included {
6561 if !packed_set.is_some_and(|p| p.contains(&tree_oid)) && emitted.insert(tree_oid) {
6562 let out_path = if omit_object_paths {
6563 String::new()
6564 } else {
6565 prefix.to_owned()
6566 };
6567 result.push((tree_oid, out_path));
6568 }
6569 } else if bits.mark_seen {
6570 omitted.push(tree_oid);
6571 }
6572
6573 if bits.skip_tree {
6574 trace_skip_tree_contents(prefix);
6575 }
6576
6577 if skip_trees_for_type_filter && depth == 0 && !explicit_root {
6578 return Ok(());
6579 }
6580
6581 if bits.skip_tree {
6582 return Ok(());
6583 }
6584
6585 let entries = parse_tree(&object.data)?;
6586 for entry in entries {
6587 if entry.mode == 0o160000 {
6588 continue;
6589 }
6590 let name = String::from_utf8_lossy(&entry.name).to_string();
6591 let path = if prefix.is_empty() {
6592 name.clone()
6593 } else {
6594 format!("{prefix}/{name}")
6595 };
6596 let child_obj = match repo.odb.read(&entry.oid) {
6597 Ok(object) => object,
6598 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
6599 if missing_action.reports_missing() && missing_seen.insert(entry.oid) {
6600 missing.push(entry.oid);
6601 }
6602 continue;
6603 }
6604 Err(err) => return Err(err),
6605 };
6606 if entry.mode == 0o040000 {
6607 if child_obj.kind != ObjectKind::Tree {
6608 return Err(Error::CorruptObject(format!(
6609 "object {} is not a tree",
6610 entry.oid
6611 )));
6612 }
6613 if let Some(pu) = parent_union {
6614 if pu.contains(&entry.oid) {
6615 continue;
6616 }
6617 }
6618 let child_tree_depth = depth + 1;
6619 collect_tree_objects_filtered(
6620 repo,
6621 entry.oid,
6622 &path,
6623 child_tree_depth,
6624 false,
6625 parent_union,
6626 tree_state,
6627 emitted,
6628 result,
6629 omitted,
6630 missing,
6631 missing_seen,
6632 filter,
6633 filter_provided,
6634 missing_action,
6635 sparse_lines,
6636 skip_trees_for_type_filter,
6637 omit_object_paths,
6638 packed_set,
6639 collect_tree_omits,
6640 tree_omit_set,
6641 combine_states,
6642 )?;
6643 } else {
6644 if let Some(pu) = parent_union {
6645 if pu.contains(&entry.oid) {
6646 continue;
6647 }
6648 }
6649 if child_obj.kind == ObjectKind::Blob {
6650 let sparse_blob = sparse_filter_includes_path(repo, &path, sparse_lines);
6651 let blob_bits = match filter {
6652 None => ListFilterBits {
6653 mark_seen: true,
6654 do_show: true,
6655 skip_tree: false,
6656 },
6657 Some(f) => {
6658 if explicit_root && !filter_provided {
6659 ListFilterBits {
6660 mark_seen: true,
6661 do_show: true,
6662 skip_tree: false,
6663 }
6664 } else {
6665 match f {
6666 ObjectFilter::Combine(_) => filter_object_bits_blob(
6667 f,
6668 entry.oid,
6669 child_obj.data.len() as u64,
6670 depth,
6671 tree_omit_set,
6672 collect_tree_omits,
6673 combine_states.as_mut().map(Vec::as_mut_slice),
6674 ),
6675 _ => filter_object_bits_blob(
6676 f,
6677 entry.oid,
6678 child_obj.data.len() as u64,
6679 depth,
6680 tree_omit_set,
6681 collect_tree_omits,
6682 None,
6683 ),
6684 }
6685 }
6686 }
6687 };
6688 let blob_included = blob_bits.do_show && sparse_blob;
6689 if !blob_included {
6690 omitted.push(entry.oid);
6691 } else if blob_bits.mark_seen
6692 && emitted.insert(entry.oid)
6693 && !packed_set.is_some_and(|p| p.contains(&entry.oid))
6694 {
6695 let out_path = if omit_object_paths {
6696 String::new()
6697 } else {
6698 path.clone()
6699 };
6700 result.push((entry.oid, out_path));
6701 }
6702 } else {
6703 if emitted.contains(&entry.oid) {
6704 return Err(Error::CorruptObject(format!(
6705 "object {} is not a blob",
6706 entry.oid
6707 )));
6708 }
6709 if emitted.insert(entry.oid) {
6710 result.push((entry.oid, path));
6711 }
6712 }
6713 }
6714 }
6715
6716 if let Some(f) = filter {
6717 if let ObjectFilter::Combine(_) = f {
6718 if let Some(states) = combine_states.as_mut() {
6719 for st in states.iter_mut() {
6720 if st.is_skipping_tree && st.skip_tree_oid == Some(tree_oid) {
6721 st.is_skipping_tree = false;
6722 st.skip_tree_oid = None;
6723 }
6724 }
6725 }
6726 }
6727 }
6728
6729 Ok(())
6730}
6731
6732fn collect_root_object(
6733 repo: &Repository,
6734 root: &RootObject,
6735 tree_state: &mut TreeWalkState,
6736 emitted: &mut HashSet<ObjectId>,
6737 result: &mut Vec<(ObjectId, String)>,
6738 omitted: &mut Vec<ObjectId>,
6739 missing: &mut Vec<ObjectId>,
6740 missing_seen: &mut HashSet<ObjectId>,
6741 filter: Option<&ObjectFilter>,
6742 filter_provided: bool,
6743 missing_action: MissingAction,
6744 sparse_lines: Option<&[String]>,
6745 skip_trees_for_type_filter: bool,
6746 omit_object_paths: bool,
6747 packed_set: Option<&HashSet<ObjectId>>,
6748 collect_tree_omits: bool,
6749 tree_omit_set: &mut Option<HashSet<ObjectId>>,
6750 combine_states: &mut Option<Vec<CombineSubState>>,
6751) -> Result<()> {
6752 if let Some(tag_oid) = root.wrap_with_tag {
6753 if root.expected_kind != Some(ExpectedObjectKind::Commit) {
6754 let show_tag = match filter {
6755 None => true,
6756 Some(f) => f.includes_commit_or_tag_object(ObjectKind::Tag),
6757 };
6758 if show_tag && emitted.insert(tag_oid) {
6759 result.push((tag_oid, "tag".to_owned()));
6760 }
6761 }
6762 }
6763
6764 let object = match repo.odb.read(&root.oid) {
6765 Ok(object) => object,
6766 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
6767 if missing_action.reports_missing() && missing_seen.insert(root.oid) {
6768 missing.push(root.oid);
6769 }
6770 return Ok(());
6771 }
6772 Err(err) => return Err(err),
6773 };
6774
6775 if let Some(expected) = root.expected_kind {
6776 if !expected.matches(object.kind) {
6777 return Err(Error::CorruptObject(format!(
6778 "object {} is not a {}",
6779 root.input,
6780 expected.as_str()
6781 )));
6782 }
6783 }
6784
6785 match object.kind {
6786 ObjectKind::Commit => {
6787 if emitted.insert(root.oid) {
6788 result.push((root.oid, "\0".to_owned()));
6789 }
6790 if let Some(tag_oid) = root.wrap_with_tag {
6791 let show_tag = match filter {
6792 None => true,
6793 Some(f) => f.includes_commit_or_tag_object(ObjectKind::Tag),
6794 };
6795 if show_tag && emitted.insert(tag_oid) {
6796 result.push((tag_oid, "tag".to_owned()));
6797 }
6798 }
6799 let commit = parse_commit(&object.data)?;
6800 let parent_union = if filter.is_some() {
6804 None
6805 } else {
6806 Some(union_parent_reachable_objects(
6807 repo,
6808 &commit.parents,
6809 missing_action,
6810 missing,
6811 missing_seen,
6812 )?)
6813 };
6814 collect_tree_objects_filtered(
6815 repo,
6816 commit.tree,
6817 "",
6818 0,
6819 false,
6820 parent_union.as_ref(),
6821 tree_state,
6822 emitted,
6823 result,
6824 omitted,
6825 missing,
6826 missing_seen,
6827 filter,
6828 filter_provided,
6829 missing_action,
6830 sparse_lines,
6831 skip_trees_for_type_filter,
6832 omit_object_paths,
6833 packed_set,
6834 collect_tree_omits,
6835 tree_omit_set,
6836 combine_states,
6837 )?;
6838 }
6839 ObjectKind::Tree => {
6840 let root_path = root.root_path.as_deref().unwrap_or("");
6841 collect_tree_objects_filtered(
6842 repo,
6843 root.oid,
6844 root_path,
6845 0,
6846 true,
6847 None,
6848 tree_state,
6849 emitted,
6850 result,
6851 omitted,
6852 missing,
6853 missing_seen,
6854 filter,
6855 filter_provided,
6856 missing_action,
6857 sparse_lines,
6858 skip_trees_for_type_filter,
6859 omit_object_paths,
6860 packed_set,
6861 collect_tree_omits,
6862 tree_omit_set,
6863 combine_states,
6864 )?;
6865 }
6866 ObjectKind::Blob => {
6867 let path_for_sparse = root.root_path.as_deref().unwrap_or("");
6868 let sparse_blob = sparse_filter_includes_path(repo, path_for_sparse, sparse_lines);
6869 let blob_bits = match filter {
6870 None => ListFilterBits {
6871 mark_seen: true,
6872 do_show: true,
6873 skip_tree: false,
6874 },
6875 Some(f) => {
6876 if !filter_provided {
6877 ListFilterBits {
6878 mark_seen: true,
6879 do_show: true,
6880 skip_tree: false,
6881 }
6882 } else {
6883 match f {
6884 ObjectFilter::Combine(_) => filter_object_bits_blob(
6885 f,
6886 root.oid,
6887 object.data.len() as u64,
6888 0,
6889 tree_omit_set,
6890 collect_tree_omits,
6891 combine_states.as_mut().map(Vec::as_mut_slice),
6892 ),
6893 _ => filter_object_bits_blob(
6894 f,
6895 root.oid,
6896 object.data.len() as u64,
6897 0,
6898 tree_omit_set,
6899 collect_tree_omits,
6900 None,
6901 ),
6902 }
6903 }
6904 }
6905 };
6906 let blob_included = blob_bits.do_show && sparse_blob;
6907 if !blob_included {
6908 if blob_bits.mark_seen {
6909 omitted.push(root.oid);
6910 }
6911 return Ok(());
6912 }
6913 if packed_set.is_some_and(|p| p.contains(&root.oid)) {
6914 return Ok(());
6915 }
6916 if blob_bits.mark_seen && !emitted.insert(root.oid) {
6917 return Ok(());
6918 }
6919 let out_path = if omit_object_paths {
6920 String::new()
6921 } else {
6922 path_for_sparse.to_owned()
6923 };
6924 result.push((root.oid, out_path));
6925 }
6926 ObjectKind::Tag => {
6927 let tag = parse_tag(&object.data)?;
6928 let expected_kind =
6929 ExpectedObjectKind::from_tag_type(&tag.object_type).ok_or_else(|| {
6930 Error::CorruptObject(format!(
6931 "object {} has unsupported tag type '{}'",
6932 root.input, tag.object_type
6933 ))
6934 })?;
6935 let nested = RootObject {
6936 oid: tag.object,
6937 input: root.input.clone(),
6938 expected_kind: Some(expected_kind),
6939 root_path: None,
6940 wrap_with_tag: None,
6941 };
6942 collect_root_object(
6943 repo,
6944 &nested,
6945 tree_state,
6946 emitted,
6947 result,
6948 omitted,
6949 missing,
6950 missing_seen,
6951 filter,
6952 filter_provided,
6953 missing_action,
6954 sparse_lines,
6955 skip_trees_for_type_filter,
6956 omit_object_paths,
6957 packed_set,
6958 collect_tree_omits,
6959 tree_omit_set,
6960 combine_states,
6961 )?;
6962 }
6963 }
6964
6965 Ok(())
6966}
6967
6968fn collect_reachable_objects_in_commit_order(
6972 repo: &Repository,
6973 _graph: &mut CommitGraph<'_>,
6974 commits: &[ObjectId],
6975 object_roots: &[RootObject],
6976 tip_annotated_tags: &HashMap<ObjectId, ObjectId>,
6977 filter: Option<&ObjectFilter>,
6978 filter_provided: bool,
6979 missing_action: MissingAction,
6980 sparse_lines: Option<&[String]>,
6981 skip_trees_for_type_filter: bool,
6982 omit_object_paths: bool,
6983 packed_set: Option<&HashSet<ObjectId>>,
6984 collect_tree_omits: bool,
6985) -> Result<(
6986 Vec<(ObjectId, String)>,
6987 Vec<ObjectId>,
6988 Vec<ObjectId>,
6989 Vec<usize>,
6990)> {
6991 let mut tree_state = TreeWalkState::new();
6992 let mut top_tree_omit =
6993 walk_needs_top_tree_omit_set(filter, collect_tree_omits).then(HashSet::<ObjectId>::new);
6994 let mut combine_states = CombineSubState::prepare_sub_states(filter, collect_tree_omits);
6995 let mut emitted = HashSet::new();
6996 let mut result = Vec::new();
6997 let mut omitted = Vec::new();
6998 let mut missing = Vec::new();
6999 let mut missing_seen = HashSet::new();
7000 let mut counts = Vec::with_capacity(commits.len());
7001 for &commit_oid in commits {
7002 let commit = match load_commit(repo, commit_oid) {
7003 Ok(commit) => commit,
7004 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
7005 if missing_action.reports_missing() && missing_seen.insert(commit_oid) {
7006 missing.push(commit_oid);
7007 }
7008 counts.push(0);
7009 continue;
7010 }
7011 Err(err) => return Err(err),
7012 };
7013 let before = result.len();
7014 if let Some(&tag_oid) = tip_annotated_tags.get(&commit_oid) {
7015 if emitted.insert(tag_oid) {
7016 result.push((tag_oid, "tag".to_owned()));
7017 }
7018 }
7019 collect_tree_objects_filtered(
7023 repo,
7024 commit.tree,
7025 "",
7026 0,
7027 false,
7028 None,
7029 &mut tree_state,
7030 &mut emitted,
7031 &mut result,
7032 &mut omitted,
7033 &mut missing,
7034 &mut missing_seen,
7035 filter,
7036 filter_provided,
7037 missing_action,
7038 sparse_lines,
7039 skip_trees_for_type_filter,
7040 omit_object_paths,
7041 packed_set,
7042 collect_tree_omits,
7043 &mut top_tree_omit,
7044 &mut combine_states,
7045 )?;
7046 counts.push(result.len() - before);
7047 }
7048
7049 for root in object_roots {
7050 collect_root_object(
7051 repo,
7052 root,
7053 &mut tree_state,
7054 &mut emitted,
7055 &mut result,
7056 &mut omitted,
7057 &mut missing,
7058 &mut missing_seen,
7059 filter,
7060 filter_provided,
7061 missing_action,
7062 sparse_lines,
7063 skip_trees_for_type_filter,
7064 omit_object_paths,
7065 packed_set,
7066 collect_tree_omits,
7067 &mut top_tree_omit,
7068 &mut combine_states,
7069 )?;
7070 }
7071
7072 Ok((result, omitted, missing, counts))
7073}
7074
7075fn kept_object_ids(repo: &Repository) -> Result<HashSet<ObjectId>> {
7077 let pack_dir = repo.git_dir.join("objects/pack");
7078 let mut kept = HashSet::new();
7079 if !pack_dir.is_dir() {
7080 return Ok(kept);
7081 }
7082 for entry in std::fs::read_dir(&pack_dir)? {
7083 let entry = entry?;
7084 let path = entry.path();
7085 if path.extension().is_some_and(|ext| ext == "keep") {
7086 let idx_path = path.with_extension("idx");
7088 if idx_path.exists() {
7089 if let Ok(oids) = crate::pack::read_idx_object_ids(&idx_path) {
7090 kept.extend(oids);
7091 }
7092 }
7093 }
7094 }
7095 Ok(kept)
7096}
7097
7098fn flatten_tree_with_mode(
7103 repo: &Repository,
7104 tree_oid: ObjectId,
7105 prefix: &str,
7106) -> Result<Vec<(String, (ObjectId, u32))>> {
7107 let mut result = Vec::new();
7108 let object = match repo.odb.read(&tree_oid) {
7109 Ok(o) => o,
7110 Err(_) => return Ok(result),
7111 };
7112 if object.kind != ObjectKind::Tree {
7113 return Ok(result);
7114 }
7115 let entries = parse_tree(&object.data)?;
7116 for entry in entries {
7117 let name = String::from_utf8_lossy(&entry.name).to_string();
7118 let path = if prefix.is_empty() {
7119 name
7120 } else {
7121 format!("{prefix}/{name}")
7122 };
7123 let child = match repo.odb.read(&entry.oid) {
7124 Ok(o) => o,
7125 Err(Error::ObjectNotFound(_)) => continue,
7126 Err(err) => return Err(err),
7127 };
7128 if child.kind == ObjectKind::Tree {
7129 result.extend(flatten_tree_with_mode(repo, entry.oid, &path)?);
7130 } else {
7131 result.push((path, (entry.oid, canon_tree_mode(entry.mode))));
7132 }
7133 }
7134 Ok(result)
7135}
7136
7137fn canon_tree_mode(mode: u32) -> u32 {
7141 const S_IFMT: u32 = 0o170000;
7142 const S_IFREG: u32 = 0o100000;
7143 const S_IFLNK: u32 = 0o120000;
7144 const S_IFGITLINK: u32 = 0o160000;
7145 match mode & S_IFMT {
7146 S_IFREG => {
7147 if mode & 0o111 != 0 {
7148 0o100755
7149 } else {
7150 0o100644
7151 }
7152 }
7153 S_IFLNK => 0o120000,
7154 S_IFGITLINK => 0o160000,
7155 _ => mode,
7156 }
7157}
7158
7159pub fn merge_bases(
7161 repo: &Repository,
7162 a: ObjectId,
7163 b: ObjectId,
7164 first_parent_only: bool,
7165) -> Result<Vec<ObjectId>> {
7166 let mut graph = CommitGraph::new(repo, first_parent_only);
7167 let ancestors_a = walk_closure(&mut graph, &[a])?;
7168 let ancestors_b = walk_closure(&mut graph, &[b])?;
7169 let common: HashSet<ObjectId> = ancestors_a.intersection(&ancestors_b).copied().collect();
7170 if common.is_empty() {
7171 return Ok(Vec::new());
7172 }
7173 let mut bases = Vec::new();
7175 for &c in &common {
7176 let is_dominated = common.iter().any(|&other| {
7177 if other == c {
7178 return false;
7179 }
7180 let other_anc = walk_closure(&mut graph, &[other]).unwrap_or_default();
7181 other_anc.contains(&c)
7182 });
7183 if !is_dominated {
7184 bases.push(c);
7185 }
7186 }
7187 if bases.is_empty() {
7188 let mut sorted: Vec<_> = common.into_iter().collect();
7189 sorted.sort_by_key(|b| std::cmp::Reverse(graph.committer_time(*b)));
7190 bases.push(sorted[0]);
7191 }
7192 Ok(bases)
7193}