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::Index;
21use crate::objects::{parse_commit, parse_tag, parse_tree, ObjectId, ObjectKind};
22use crate::pack;
23use crate::patch_ids::compute_patch_id;
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 Allow,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum FilterObjectKind {
55 Blob,
56 Tree,
57 Commit,
58 Tag,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum ObjectFilter {
64 BlobNone,
66 BlobLimit(u64),
68 TreeDepth(u64),
70 SparseOid(String),
72 ObjectType(FilterObjectKind),
74 Combine(Vec<ObjectFilter>),
76}
77
78impl ObjectFilter {
79 pub fn parse(spec: &str) -> std::result::Result<Self, String> {
81 Self::parse_inner(spec.trim(), false)
82 }
83
84 fn parse_inner(spec: &str, from_combine_subfilter: bool) -> std::result::Result<Self, String> {
85 if spec == "blob:none" {
86 return Ok(ObjectFilter::BlobNone);
87 }
88 if let Some(rest) = spec.strip_prefix("blob:limit=") {
89 let bytes = parse_size_suffix(rest)
90 .ok_or_else(|| format!("invalid blob:limit value: {rest}"))?;
91 return Ok(ObjectFilter::BlobLimit(bytes));
92 }
93 if let Some(rest) = spec.strip_prefix("tree:") {
94 if rest.is_empty() || !rest.chars().all(|c| c.is_ascii_digit()) {
95 return Err(if from_combine_subfilter {
96 "expected 'tree:<depth>'.".to_owned()
97 } else {
98 format!("invalid tree depth: {rest}")
99 });
100 }
101 let depth: u64 = rest.parse().map_err(|_| {
102 if from_combine_subfilter {
103 "expected 'tree:<depth>'.".to_owned()
104 } else {
105 format!("invalid tree depth: {rest}")
106 }
107 })?;
108 return Ok(ObjectFilter::TreeDepth(depth));
109 }
110 if let Some(rest) = spec.strip_prefix("object:type=") {
111 let kind = match rest {
112 "blob" => FilterObjectKind::Blob,
113 "tree" => FilterObjectKind::Tree,
114 "commit" => FilterObjectKind::Commit,
115 "tag" => FilterObjectKind::Tag,
116 "" => return Err("invalid object type".to_owned()),
117 _ => return Err(format!("invalid object type: {rest}")),
118 };
119 return Ok(ObjectFilter::ObjectType(kind));
120 }
121 if let Some(rest) = spec.strip_prefix("sparse:oid=") {
122 if rest.is_empty() {
123 return Err("invalid sparse:oid value: ".to_owned());
124 }
125 return Ok(ObjectFilter::SparseOid(rest.to_owned()));
126 }
127 if let Some(rest) = spec.strip_prefix("combine:") {
128 if rest.is_empty() {
129 return Err("expected something after combine:".to_owned());
130 }
131 let parts = split_combine_raw_parts(rest);
132 if parts.is_empty() {
133 return Err("expected something after combine:".to_owned());
134 }
135 let mut filters = Vec::new();
136 for part in parts {
137 filters.push(Self::parse_from_combine_subfilter(part)?);
138 }
139 return Ok(ObjectFilter::Combine(filters));
140 }
141 Err(format!("invalid filter-spec '{spec}'"))
142 }
143
144 fn parse_from_combine_subfilter(encoded: &str) -> std::result::Result<Self, String> {
145 if let Some(ch) = combine_subfilter_has_reserved(encoded) {
146 return Err(format!("must escape char in sub-filter-spec: '{ch}'"));
147 }
148 let decoded = url_decode(encoded);
149 Self::parse_inner(&decoded, true)
150 }
151
152 #[must_use]
154 pub fn merge_with(self, other: Self) -> Self {
155 match (self, other) {
156 (ObjectFilter::Combine(mut a), ObjectFilter::Combine(mut b)) => {
157 a.append(&mut b);
158 ObjectFilter::Combine(a)
159 }
160 (ObjectFilter::Combine(mut a), b) => {
161 a.push(b);
162 ObjectFilter::Combine(a)
163 }
164 (a, ObjectFilter::Combine(mut b)) => {
165 let mut v = vec![a];
166 v.append(&mut b);
167 ObjectFilter::Combine(v)
168 }
169 (a, b) => ObjectFilter::Combine(vec![a, b]),
170 }
171 }
172
173 pub fn includes_blob(&self, size: u64) -> bool {
175 match self {
176 ObjectFilter::BlobNone => false,
177 ObjectFilter::BlobLimit(limit) => size < *limit,
178 ObjectFilter::TreeDepth(_) => true,
181 ObjectFilter::SparseOid(_) => true,
182 ObjectFilter::ObjectType(kind) => *kind == FilterObjectKind::Blob,
183 ObjectFilter::Combine(filters) => filters.iter().all(|f| f.includes_blob(size)),
184 }
185 }
186
187 #[must_use]
192 pub fn includes_blob_under_tree(&self, size: u64, parent_tree_depth: u64) -> bool {
193 match self {
194 ObjectFilter::BlobNone => false,
195 ObjectFilter::BlobLimit(limit) => size < *limit,
196 ObjectFilter::TreeDepth(max_depth) => parent_tree_depth.saturating_add(1) < *max_depth,
197 ObjectFilter::SparseOid(_) => true,
198 ObjectFilter::ObjectType(kind) => *kind == FilterObjectKind::Blob,
199 ObjectFilter::Combine(filters) => filters
200 .iter()
201 .all(|f| f.includes_blob_under_tree(size, parent_tree_depth)),
202 }
203 }
204
205 pub fn includes_tree(&self, depth: u64) -> bool {
207 match self {
208 ObjectFilter::BlobNone => true,
209 ObjectFilter::BlobLimit(_) => true,
210 ObjectFilter::TreeDepth(max_depth) => depth < *max_depth,
211 ObjectFilter::SparseOid(_) => true,
212 ObjectFilter::ObjectType(kind) => *kind == FilterObjectKind::Tree,
213 ObjectFilter::Combine(filters) => filters.iter().all(|f| f.includes_tree(depth)),
214 }
215 }
216
217 pub fn includes_commit_or_tag_object(&self, kind: ObjectKind) -> bool {
219 let expected = match kind {
220 ObjectKind::Commit => Some(FilterObjectKind::Commit),
221 ObjectKind::Tag => Some(FilterObjectKind::Tag),
222 _ => None,
223 };
224 match self {
225 ObjectFilter::BlobNone | ObjectFilter::BlobLimit(_) => true,
226 ObjectFilter::TreeDepth(_) => true,
227 ObjectFilter::SparseOid(_) => true,
228 ObjectFilter::ObjectType(t) => expected == Some(*t),
229 ObjectFilter::Combine(filters) => filters
230 .iter()
231 .all(|f| f.includes_commit_or_tag_object(kind)),
232 }
233 }
234
235 pub fn includes_loose_object(&self, kind: ObjectKind, size: u64) -> bool {
237 match kind {
238 ObjectKind::Blob => self.includes_blob(size),
239 ObjectKind::Tree => self.includes_tree(0),
240 ObjectKind::Commit | ObjectKind::Tag => self.includes_commit_or_tag_object(kind),
241 }
242 }
243
244 #[must_use]
246 pub fn passes_for_object(&self, kind: ObjectKind, size: usize) -> bool {
247 self.includes_loose_object(kind, size as u64)
248 }
249}
250
251#[must_use]
254pub fn reachable_object_ids_for_cat_file(
255 repo: &Repository,
256 filter: Option<&ObjectFilter>,
257 filter_provided_objects: bool,
258) -> Result<Vec<ObjectId>> {
259 let opts = RevListOptions {
260 all_refs: true,
261 objects: true,
262 no_object_names: true,
263 quiet: true,
264 filter: filter.cloned(),
265 filter_provided_objects,
266 ..Default::default()
267 };
268 let result = rev_list(repo, &[], &[], &opts)?;
269 let mut set = BTreeSet::new();
270 for (i, oid) in result.commits.iter().enumerate() {
271 if result.objects_print_commit.get(i).copied().unwrap_or(true) {
272 set.insert(*oid);
273 }
274 }
275 for (oid, _) in &result.objects {
276 set.insert(*oid);
277 }
278 Ok(set.into_iter().collect())
279}
280
281#[must_use]
284pub fn object_ids_for_cat_file_filtered(
285 repo: &Repository,
286 filter: &ObjectFilter,
287) -> Result<Vec<ObjectId>> {
288 reachable_object_ids_for_cat_file(repo, Some(filter), true)
289}
290
291fn parse_size_suffix(s: &str) -> Option<u64> {
293 let s = s.trim();
294 if s.is_empty() {
295 return None;
296 }
297 let (num_str, multiplier) = match s.as_bytes().last()? {
298 b'k' | b'K' => (&s[..s.len() - 1], 1024u64),
299 b'm' | b'M' => (&s[..s.len() - 1], 1024 * 1024),
300 b'g' | b'G' => (&s[..s.len() - 1], 1024 * 1024 * 1024),
301 _ => (s, 1u64),
302 };
303 let num: u64 = num_str.parse().ok()?;
304 Some(num * multiplier)
305}
306
307fn split_combine_raw_parts(spec: &str) -> Vec<&str> {
309 spec.split('+').filter(|s| !s.is_empty()).collect()
310}
311
312fn combine_subfilter_has_reserved(encoded: &str) -> Option<char> {
315 const RESERVED: &str = "~`!@#$^&*()[]{}\\;'\",<>?";
316 for ch in encoded.chars() {
317 if ch.is_control() || ch.is_whitespace() {
318 return Some(ch);
319 }
320 if RESERVED.contains(ch) {
321 return Some(ch);
322 }
323 }
324 None
325}
326
327#[must_use]
329pub fn expand_object_filter_for_protocol(spec: &str) -> std::result::Result<String, String> {
330 let f = ObjectFilter::parse(spec)?;
331 match f {
332 ObjectFilter::BlobLimit(n) => Ok(format!("blob:limit={n}")),
333 _ => Ok(spec.to_owned()),
334 }
335}
336
337fn combine_filter_allow_unencoded(ch: char) -> bool {
338 if ch.is_control() || ch.is_whitespace() || ch == '%' || ch == '+' {
339 return false;
340 }
341 !"~`!@#$^&*()[]{}\\;'\",<>?".contains(ch)
342}
343
344pub fn url_encode_object_filter_subspec(raw: &str) -> String {
346 let mut out = String::new();
347 for b in raw.as_bytes() {
348 let ch = *b as char;
349 if combine_filter_allow_unencoded(ch) {
350 out.push(ch);
351 } else {
352 out.push_str(&format!("%{:02x}", b));
353 }
354 }
355 out
356}
357
358pub fn trace_combine_filter_append(encoded_segment: &str) {
360 let Ok(trace_val) = std::env::var("GIT_TRACE") else {
361 return;
362 };
363 if trace_val.is_empty() || trace_val == "0" || trace_val.eq_ignore_ascii_case("false") {
364 return;
365 }
366 let line = format!("Add to combine filter-spec: {encoded_segment}\n");
367 match trace_val.as_str() {
368 "1" | "true" | "2" => {
369 let _ = std::io::stderr().write_all(line.as_bytes());
370 }
371 path => {
372 if let Ok(mut f) = std::fs::OpenOptions::new()
373 .create(true)
374 .append(true)
375 .open(path)
376 {
377 let _ = f.write_all(line.as_bytes());
378 }
379 }
380 }
381}
382
383fn url_decode(s: &str) -> String {
385 let mut result = String::new();
386 let mut chars = s.chars();
387 while let Some(ch) = chars.next() {
388 if ch == '%' {
389 let hi = chars.next().unwrap_or('0');
390 let lo = chars.next().unwrap_or('0');
391 let byte = u8::from_str_radix(&format!("{hi}{lo}"), 16).unwrap_or(b'?');
392 result.push(byte as char);
393 } else {
394 result.push(ch);
395 }
396 }
397 result
398}
399
400#[derive(Debug, Clone, Copy, PartialEq, Eq)]
402pub enum OrderingMode {
403 Default,
405 DateOrderWalk,
407 AuthorDateWalk,
409 Topo,
411 AuthorDateTopo,
413}
414
415#[derive(Debug, Clone)]
417pub struct RevListOptions {
418 pub all_refs: bool,
420 pub first_parent: bool,
422 pub ancestry_path: bool,
424 pub ancestry_path_bottoms: Vec<ObjectId>,
426 pub simplify_by_decoration: bool,
428 pub output_mode: OutputMode,
430 pub quiet: bool,
432 pub count: bool,
434 pub skip: usize,
436 pub max_count: Option<usize>,
438 pub ordering: OrderingMode,
440 pub reverse: bool,
442 pub objects: bool,
444 pub no_object_names: bool,
446 pub boundary: bool,
448 pub left_right: bool,
450 pub left_only: bool,
452 pub right_only: bool,
454 pub cherry_mark: bool,
456 pub cherry_pick: bool,
458 pub min_parents: Option<usize>,
460 pub max_parents: Option<usize>,
462 pub symmetric_left: Option<ObjectId>,
464 pub symmetric_right: Option<ObjectId>,
466 pub paths: Vec<String>,
468 pub full_history: bool,
470 pub sparse: bool,
472 pub simplify_merges: bool,
474 pub show_pulls: bool,
476 pub exclude_first_parent_only: bool,
478 pub filter: Option<ObjectFilter>,
480 pub filter_raw_specs: Vec<String>,
482 pub filter_provided_objects: bool,
484 pub filter_print_omitted: bool,
486 pub in_commit_order: bool,
488 pub no_kept_objects: bool,
490 pub missing_action: MissingAction,
492 pub use_bitmap_index: bool,
494 pub unpacked_only: bool,
496 pub bitmap_oid_only_objects: bool,
499 pub path_graph_reorder: bool,
502 pub until_cutoff: Option<i64>,
504 pub since_cutoff: Option<i64>,
506 pub include_reflog_entries: bool,
508 pub include_indexed_objects: bool,
510 pub use_commit_graph_bloom: bool,
512 pub commit_graph_read_changed_paths: bool,
514 pub commit_graph_changed_paths_version: i32,
516 pub bloom_stats: Option<BloomWalkStatsHandle>,
518}
519
520impl Default for RevListOptions {
521 fn default() -> Self {
522 Self {
523 all_refs: false,
524 first_parent: false,
525 ancestry_path: false,
526 ancestry_path_bottoms: Vec::new(),
527 simplify_by_decoration: false,
528 output_mode: OutputMode::OidOnly,
529 quiet: false,
530 count: false,
531 skip: 0,
532 max_count: None,
533 ordering: OrderingMode::Default,
534 reverse: false,
535 objects: false,
536 no_object_names: false,
537 boundary: false,
538 left_right: false,
539 left_only: false,
540 right_only: false,
541 cherry_mark: false,
542 cherry_pick: false,
543 min_parents: None,
544 max_parents: None,
545 symmetric_left: None,
546 symmetric_right: None,
547 paths: Vec::new(),
548 full_history: false,
549 sparse: false,
550 simplify_merges: false,
551 show_pulls: false,
552 exclude_first_parent_only: false,
553 filter: None,
554 filter_raw_specs: Vec::new(),
555 filter_provided_objects: false,
556 filter_print_omitted: false,
557 in_commit_order: false,
558 no_kept_objects: false,
559 missing_action: MissingAction::Error,
560 use_bitmap_index: false,
561 unpacked_only: false,
562 bitmap_oid_only_objects: false,
563 path_graph_reorder: true,
564 until_cutoff: None,
565 since_cutoff: None,
566 include_reflog_entries: false,
567 include_indexed_objects: false,
568 use_commit_graph_bloom: false,
569 commit_graph_read_changed_paths: true,
570 commit_graph_changed_paths_version: -1,
571 bloom_stats: None,
572 }
573 }
574}
575
576#[derive(Debug, Clone)]
578pub struct RevListResult {
579 pub commits: Vec<ObjectId>,
581 pub objects: Vec<(ObjectId, String)>,
584 pub omitted_objects: Vec<ObjectId>,
586 pub missing_objects: Vec<ObjectId>,
588 pub boundary_commits: Vec<ObjectId>,
590 pub left_right_map: HashMap<ObjectId, bool>,
592 pub cherry_equivalent: HashSet<ObjectId>,
594 pub per_commit_object_counts: Vec<usize>,
598 pub object_walk_tips: Vec<ObjectId>,
600 pub objects_print_commit: Vec<bool>,
603 pub object_segments: Vec<Vec<(ObjectId, String)>>,
606 pub bitmap_object_format: bool,
608 pub tip_annotated_tag_by_commit: HashMap<ObjectId, ObjectId>,
610}
611
612pub fn rev_list(
626 repo: &Repository,
627 positive_specs: &[String],
628 negative_specs: &[String],
629 options: &RevListOptions,
630) -> Result<RevListResult> {
631 let mut graph = CommitGraph::new(repo, options.first_parent);
632
633 let (mut include, object_roots, tip_annotated_tag_by_commit) = if options.objects {
634 resolve_specs_for_objects(repo, positive_specs)?
635 } else {
636 (
637 resolve_specs(repo, positive_specs)?,
638 Vec::new(),
639 HashMap::new(),
640 )
641 };
642 let exclude = resolve_specs(repo, negative_specs)?;
643
644 if options.all_refs {
645 include.extend(all_ref_tips(repo, &RefExclusions::default())?);
646 }
647
648 if options.objects && options.include_reflog_entries {
649 include.extend(reflog_commit_tips(repo)?);
650 }
651
652 let mut index_blob_roots: Vec<RootObject> = Vec::new();
653 if options.objects && options.include_indexed_objects && repo.work_tree.is_some() {
654 let index_path = repo.git_dir.join("index");
655 if index_path.is_file() {
656 let idx = Index::load(&index_path)?;
657 for e in &idx.entries {
658 if e.stage() != 0 {
659 continue;
660 }
661 let path_str = String::from_utf8_lossy(&e.path).into_owned();
662 index_blob_roots.push(RootObject {
663 oid: e.oid,
664 input: format!(":{path_str}"),
665 expected_kind: Some(ExpectedObjectKind::Blob),
666 root_path: Some(path_str),
667 wrap_with_tag: None,
668 });
669 }
670 }
671 }
672
673 let object_roots = if index_blob_roots.is_empty() {
674 object_roots
675 } else {
676 let mut merged = object_roots;
677 merged.extend(index_blob_roots);
678 merged
679 };
680
681 if include.is_empty() && object_roots.is_empty() {
682 return Err(Error::InvalidRef("no revisions specified".to_owned()));
683 }
684
685 let object_walk_tip_commits: Vec<ObjectId> = if options.objects {
686 include.clone()
687 } else {
688 Vec::new()
689 };
690
691 let (mut included, _discovery_order) = if include.is_empty() {
692 (HashSet::new(), Vec::new())
693 } else {
694 walk_closure_ordered(&mut graph, &include)?
695 };
696 let excluded = if exclude.is_empty() {
697 HashSet::new()
698 } else if options.exclude_first_parent_only {
699 walk_closure_first_parent_only(&mut graph, &exclude)?
700 } else {
701 walk_closure(&mut graph, &exclude)?
702 };
703 included.retain(|oid| !excluded.contains(oid));
704
705 if options.simplify_by_decoration {
706 let decorated = all_ref_tips(repo, &RefExclusions::default())?;
707 included.retain(|oid| decorated.contains(oid));
708 }
709
710 if options.ancestry_path {
711 let mut bottoms = options.ancestry_path_bottoms.clone();
712 if bottoms.is_empty() {
713 bottoms.extend(exclude.iter().copied());
714 }
715 if bottoms.is_empty() {
716 return Err(Error::InvalidRef(
717 "--ancestry-path requires a range with excluded tips".to_owned(),
718 ));
719 }
720 limit_to_ancestry(&mut graph, &mut included, &bottoms)?;
721 }
722
723 let path_effective_full = options.full_history
726 || options.ancestry_path
727 || (options.simplify_merges && !options.paths.is_empty());
728
729 if options.min_parents.is_some() || options.max_parents.is_some() {
731 let min_p = options.min_parents.unwrap_or(0);
732 let max_p = options.max_parents.unwrap_or(usize::MAX);
733 included.retain(|oid| {
734 let count = graph.parents_of(*oid).map(|p| p.len()).unwrap_or(0);
735 count >= min_p && count <= max_p
736 });
737 }
738
739 let mut ordered = match options.ordering {
740 OrderingMode::Default | OrderingMode::DateOrderWalk | OrderingMode::AuthorDateWalk => {
741 let author_dates = options.ordering == OrderingMode::AuthorDateWalk;
742 let parent_count_filter_active =
743 options.min_parents.is_some() || options.max_parents.is_some();
744 if parent_count_filter_active {
745 let mut tips: Vec<ObjectId> = Vec::new();
749 let mut tip_seen = HashSet::new();
750 for &tip in &include {
751 if included.contains(&tip) {
752 if tip_seen.insert(tip) {
753 tips.push(tip);
754 }
755 continue;
756 }
757 let parents = graph.parents_of(tip).unwrap_or_default();
758 for p in parents {
759 if tip_seen.insert(p) {
760 tips.push(p);
761 }
762 }
763 }
764 date_order_walk_with_tips(&mut graph, &tips, &included, author_dates)?
765 } else {
766 date_order_walk(&mut graph, &included, author_dates)?
767 }
768 }
769 OrderingMode::Topo => topo_sort(&mut graph, &included, false)?,
770 OrderingMode::AuthorDateTopo => topo_sort(&mut graph, &included, true)?,
771 };
772
773 if !options.paths.is_empty() {
775 let paths = &options.paths;
776 let cfg = ConfigSet::load(Some(&repo.git_dir), true).unwrap_or_default();
777 let mut core_cg = match cfg.get_bool("core.commitgraph") {
778 Some(Ok(b)) => b,
779 _ => true,
780 };
781 if std::env::var("GIT_TEST_COMMIT_GRAPH").ok().as_deref() == Some("0") {
782 core_cg = false;
783 }
784 let read_paths = cfg
785 .get("commitgraph.readchangedpaths")
786 .and_then(|v| crate::config::parse_bool(&v).ok())
787 .unwrap_or(true);
788 let version = cfg
789 .get("commitgraph.changedpathsversion")
790 .and_then(|s| s.parse::<i32>().ok())
791 .unwrap_or(-1);
792
793 let use_bloom = core_cg
794 && options.use_commit_graph_bloom
795 && crate::pathspec::pathspecs_allow_bloom(paths);
796 let read_changed = read_paths && options.commit_graph_read_changed_paths;
797 let bloom_chain = if use_bloom {
798 CommitGraphChain::load(&repo.git_dir.join("objects"))
799 } else {
800 None
801 };
802 let bloom_version = if options.commit_graph_changed_paths_version != -1 {
803 options.commit_graph_changed_paths_version
804 } else {
805 version
806 };
807 let bloom_cwd = repo.bloom_pathspec_cwd();
808
809 ordered.retain(|oid| {
810 commit_touches_paths(
811 repo,
812 &mut graph,
813 *oid,
814 paths,
815 path_effective_full,
816 options.sparse,
817 options.simplify_merges,
818 options.show_pulls,
819 bloom_chain.as_ref(),
820 read_changed,
821 bloom_version,
822 options.bloom_stats.as_ref(),
823 bloom_cwd.as_deref(),
824 )
825 .unwrap_or(false)
826 });
827 }
828
829 if !options.paths.is_empty() && options.simplify_merges && !ordered.is_empty() {
830 ordered = simplify_merges_commit_list(repo, &ordered)?;
831 }
832
833 let path_needs_graph_reorder = !options.paths.is_empty()
836 && options.path_graph_reorder
837 && (!options.full_history || options.sparse);
838 if path_needs_graph_reorder && !ordered.is_empty() {
839 if options.sparse {
840 let mut dense_opts = options.clone();
841 dense_opts.sparse = false;
842 dense_opts.path_graph_reorder = false;
843 let dense_result = rev_list(repo, positive_specs, negative_specs, &dense_opts)?;
844 let dense_ordered = reorder_path_limited_graph_commits(
845 repo,
846 &dense_result.commits,
847 options.first_parent,
848 )?;
849 ordered = expand_sparse_path_limited_graph_history(repo, &dense_ordered)?;
850 } else {
851 ordered = reorder_path_limited_graph_commits(repo, &ordered, options.first_parent)?;
852 }
853 }
854
855 let mut left_right_map = HashMap::new();
857 if options.left_right
858 || options.left_only
859 || options.right_only
860 || options.cherry_mark
861 || options.cherry_pick
862 {
863 if let (Some(left_oid), Some(right_oid)) = (options.symmetric_left, options.symmetric_right)
864 {
865 let left_reach = walk_closure(&mut graph, &[left_oid])?;
870 let right_reach = walk_closure(&mut graph, &[right_oid])?;
871 for &oid in &ordered {
872 let from_left = left_reach.contains(&oid);
873 let from_right = right_reach.contains(&oid);
874 left_right_map.insert(oid, from_left && !from_right);
875 }
876 }
877 }
878
879 let mut cherry_equivalent = HashSet::new();
882 if options.cherry_pick || options.cherry_mark {
883 let left_commits: Vec<_> = ordered
884 .iter()
885 .filter(|o| left_right_map.get(o) == Some(&true))
886 .copied()
887 .collect();
888 let right_commits: Vec<_> = ordered
889 .iter()
890 .filter(|o| left_right_map.get(o) == Some(&false))
891 .copied()
892 .collect();
893 let left_first = !left_commits.is_empty()
894 && !right_commits.is_empty()
895 && left_commits.len() < right_commits.len();
896
897 let mut by_patch: HashMap<ObjectId, ObjectId> = HashMap::new();
898 if left_first {
899 for oid in &left_commits {
900 if let Ok(Some(pid)) = compute_patch_id(&repo.odb, oid) {
901 by_patch.entry(pid).or_insert(*oid);
902 }
903 }
904 for oid in &right_commits {
905 if let Ok(Some(pid)) = compute_patch_id(&repo.odb, oid) {
906 if let Some(&other) = by_patch.get(&pid) {
907 cherry_equivalent.insert(*oid);
908 cherry_equivalent.insert(other);
909 }
910 }
911 }
912 } else {
913 for oid in &right_commits {
914 if let Ok(Some(pid)) = compute_patch_id(&repo.odb, oid) {
915 by_patch.entry(pid).or_insert(*oid);
916 }
917 }
918 for oid in &left_commits {
919 if let Ok(Some(pid)) = compute_patch_id(&repo.odb, oid) {
920 if let Some(&other) = by_patch.get(&pid) {
921 cherry_equivalent.insert(*oid);
922 cherry_equivalent.insert(other);
923 }
924 }
925 }
926 }
927 }
928
929 if options.left_only {
931 ordered.retain(|oid| left_right_map.get(oid) == Some(&true));
932 }
933 if options.right_only {
934 ordered.retain(|oid| left_right_map.get(oid) == Some(&false));
935 }
936
937 if options.cherry_pick {
939 ordered.retain(|oid| !cherry_equivalent.contains(oid));
940 }
941
942 if options.until_cutoff.is_some() || options.since_cutoff.is_some() {
943 let until = options.until_cutoff;
944 let since = options.since_cutoff;
945 ordered.retain(|oid| {
946 let ts = graph.committer_time(*oid);
947 if let Some(u) = until {
948 if ts > u {
949 return false;
950 }
951 }
952 if let Some(s) = since {
953 if ts < s {
954 return false;
955 }
956 }
957 true
958 });
959 }
960
961 if options.skip > 0 {
962 ordered = ordered.into_iter().skip(options.skip).collect();
963 }
964 if let Some(max_count) = options.max_count {
965 ordered.truncate(max_count);
966 }
967 if options.reverse {
968 ordered.reverse();
969 }
970
971 let boundary_commits = if options.boundary {
973 let included_set: HashSet<ObjectId> = ordered.iter().copied().collect();
974 let mut boundary = Vec::new();
975 let mut boundary_seen = HashSet::new();
976 for &oid in &ordered {
977 if let Ok(parents) = graph.parents_of(oid).map(|p| p.to_vec()) {
978 for parent in parents {
979 if !included_set.contains(&parent) && boundary_seen.insert(parent) {
980 boundary.push(parent);
981 }
982 }
983 }
984 }
985 boundary
986 } else {
987 Vec::new()
988 };
989
990 let kept_set = if options.no_kept_objects {
992 kept_object_ids(repo).unwrap_or_default()
993 } else {
994 HashSet::new()
995 };
996
997 if options.no_kept_objects {
998 ordered.retain(|oid| !kept_set.contains(oid));
999 }
1000
1001 if options.unpacked_only {
1002 let packed = packed_object_set(repo);
1003 ordered.retain(|oid| !packed.contains(oid));
1004 }
1005
1006 let commit_tips_set: HashSet<ObjectId> = object_walk_tip_commits.iter().copied().collect();
1007 let objects_print_commit: Vec<bool> = if options.objects {
1008 ordered
1009 .iter()
1010 .map(|&c| {
1011 let user_given = !options.filter_provided_objects && commit_tips_set.contains(&c);
1012 object_walk_print_commit_line(
1013 options.filter_provided_objects,
1014 options.filter.as_ref(),
1015 user_given,
1016 )
1017 })
1018 .collect()
1019 } else {
1020 Vec::new()
1021 };
1022
1023 let sparse_lines = sparse_oid_lines_from_filter(repo, options.filter.as_ref());
1024 let skip_trees = skip_tree_descent_for_object_type_filter(options.filter.as_ref());
1025 let walk_cfg = ConfigSet::load(Some(&repo.git_dir), true).unwrap_or_default();
1026 let promisor_repo = crate::promisor::repo_treats_promisor_packs(&repo.git_dir, &walk_cfg);
1027 let object_walk_missing_action =
1028 if options.objects && options.missing_action == MissingAction::Error && promisor_repo {
1029 MissingAction::Allow
1030 } else {
1031 options.missing_action
1032 };
1033 let bitmap_object_format = options.objects
1034 && options.use_bitmap_index
1035 && (options.bitmap_oid_only_objects || !object_roots.is_empty() || options.unpacked_only);
1036 let omit_object_paths = bitmap_object_format;
1037 let packed_set = if options.objects && options.unpacked_only {
1038 Some(packed_object_set(repo))
1039 } else {
1040 None
1041 };
1042
1043 let collect_tree_omits = options.filter_print_omitted;
1045
1046 let (objects, omitted_objects, missing_objects, per_commit_object_counts, object_segments) =
1048 if options.objects {
1049 let filter_provided = options.filter_provided_objects;
1050 let (mut objs, omit, miss, counts, segments) = if options.in_commit_order {
1051 let (o, om, mi, c) = collect_reachable_objects_in_commit_order(
1052 repo,
1053 &mut graph,
1054 &ordered,
1055 &object_roots,
1056 &tip_annotated_tag_by_commit,
1057 options.filter.as_ref(),
1058 filter_provided,
1059 object_walk_missing_action,
1060 sparse_lines.as_deref(),
1061 skip_trees,
1062 omit_object_paths,
1063 packed_set.as_ref(),
1064 collect_tree_omits,
1065 )?;
1066 (o, om, mi, c, Vec::new())
1067 } else {
1068 let (o, om, mi, seg) = collect_reachable_objects_segmented(
1069 repo,
1070 &mut graph,
1071 &ordered,
1072 &object_roots,
1073 &tip_annotated_tag_by_commit,
1074 options.filter.as_ref(),
1075 filter_provided,
1076 object_walk_missing_action,
1077 sparse_lines.as_deref(),
1078 skip_trees,
1079 omit_object_paths,
1080 packed_set.as_ref(),
1081 collect_tree_omits,
1082 )?;
1083 (o, om, mi, Vec::new(), seg)
1084 };
1085 if options.no_kept_objects {
1086 objs.retain(|(oid, _)| !kept_set.contains(oid));
1087 }
1088 (objs, omit, miss, counts, segments)
1089 } else {
1090 (Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new())
1091 };
1092
1093 let omitted_objects = if omitted_objects.is_empty() {
1094 omitted_objects
1095 } else {
1096 let emitted: HashSet<ObjectId> = objects.iter().map(|(o, _)| *o).collect();
1097 omitted_objects
1098 .into_iter()
1099 .filter(|o| !emitted.contains(o))
1100 .collect::<BTreeSet<_>>()
1101 .into_iter()
1102 .collect()
1103 };
1104
1105 Ok(RevListResult {
1106 commits: ordered,
1107 objects,
1108 omitted_objects,
1109 missing_objects,
1110 boundary_commits,
1111 left_right_map,
1112 cherry_equivalent,
1113 per_commit_object_counts,
1114 object_walk_tips: object_walk_tip_commits,
1115 objects_print_commit,
1116 object_segments,
1117 bitmap_object_format,
1118 tip_annotated_tag_by_commit,
1119 })
1120}
1121
1122#[must_use]
1129pub fn split_revision_token(token: &str) -> (Vec<String>, Vec<String>) {
1130 if let Some((lhs, rhs)) = crate::rev_parse::split_double_dot_range(token) {
1131 let positive = if rhs.is_empty() {
1132 "HEAD".to_owned()
1133 } else {
1134 rhs.to_owned()
1135 };
1136 let negative = if lhs.is_empty() {
1137 "HEAD".to_owned()
1138 } else {
1139 lhs.to_owned()
1140 };
1141 return (vec![positive], vec![negative]);
1142 }
1143 if let Some(rest) = token.strip_prefix('^') {
1144 return (Vec::new(), vec![rest.to_owned()]);
1145 }
1146 (vec![token.to_owned()], Vec::new())
1147}
1148
1149fn ansi_color_from_name(name: &str) -> String {
1150 match name {
1151 "red" => "\x1b[31m".to_owned(),
1152 "green" => "\x1b[32m".to_owned(),
1153 "yellow" => "\x1b[33m".to_owned(),
1154 "blue" => "\x1b[34m".to_owned(),
1155 "magenta" => "\x1b[35m".to_owned(),
1156 "cyan" => "\x1b[36m".to_owned(),
1157 "white" => "\x1b[37m".to_owned(),
1158 "bold" => "\x1b[1m".to_owned(),
1159 "dim" => "\x1b[2m".to_owned(),
1160 "ul" | "underline" => "\x1b[4m".to_owned(),
1161 "blink" => "\x1b[5m".to_owned(),
1162 "reverse" => "\x1b[7m".to_owned(),
1163 "reset" => "\x1b[m".to_owned(),
1164 _ => String::new(),
1165 }
1166}
1167
1168fn color_name_to_code(name: &str) -> Option<u8> {
1169 match name {
1170 "black" => Some(0),
1171 "red" => Some(1),
1172 "green" => Some(2),
1173 "yellow" => Some(3),
1174 "blue" => Some(4),
1175 "magenta" => Some(5),
1176 "cyan" => Some(6),
1177 "white" => Some(7),
1178 "default" => Some(9),
1179 _ => None,
1180 }
1181}
1182
1183fn ansi_color_from_spec(spec: &str) -> String {
1184 if spec == "reset" {
1185 return "\x1b[m".to_owned();
1186 }
1187 let mut codes = Vec::new();
1188 let mut fg_set = false;
1189 for part in spec.split_whitespace() {
1190 match part {
1191 "bold" => codes.push("1".to_owned()),
1192 "dim" => codes.push("2".to_owned()),
1193 "italic" => codes.push("3".to_owned()),
1194 "ul" | "underline" => codes.push("4".to_owned()),
1195 "blink" => codes.push("5".to_owned()),
1196 "reverse" => codes.push("7".to_owned()),
1197 "strike" => codes.push("9".to_owned()),
1198 "nobold" | "nodim" => codes.push("22".to_owned()),
1199 "noitalic" => codes.push("23".to_owned()),
1200 "noul" | "nounderline" => codes.push("24".to_owned()),
1201 "noblink" => codes.push("25".to_owned()),
1202 "noreverse" => codes.push("27".to_owned()),
1203 "nostrike" => codes.push("29".to_owned()),
1204 _ => {
1205 if let Some(code) = color_name_to_code(part) {
1206 if !fg_set {
1207 codes.push(format!("{}", 30 + code));
1208 fg_set = true;
1209 } else {
1210 codes.push(format!("{}", 40 + code));
1211 }
1212 }
1213 }
1214 }
1215 }
1216 if codes.is_empty() {
1217 String::new()
1218 } else {
1219 format!("\x1b[{}m", codes.join(";"))
1220 }
1221}
1222
1223fn format_relative_date(diff: i64) -> String {
1224 if diff < 0 {
1225 "in the future".to_owned()
1226 } else if diff < 60 {
1227 format!("{} seconds ago", diff)
1228 } else if diff < 3600 {
1229 let m = diff / 60;
1230 if m == 1 {
1231 "1 minute ago".to_owned()
1232 } else {
1233 format!("{m} minutes ago")
1234 }
1235 } else if diff < 86400 {
1236 let h = diff / 3600;
1237 if h == 1 {
1238 "1 hour ago".to_owned()
1239 } else {
1240 format!("{h} hours ago")
1241 }
1242 } else if diff < 86400 * 30 {
1243 let d = diff / 86400;
1244 if d == 1 {
1245 "1 day ago".to_owned()
1246 } else {
1247 format!("{d} days ago")
1248 }
1249 } else if diff < 86400 * 365 {
1250 let months = diff / (86400 * 30);
1251 if months == 1 {
1252 "1 month ago".to_owned()
1253 } else {
1254 format!("{months} months ago")
1255 }
1256 } else {
1257 let years = diff / (86400 * 365);
1258 if years == 1 {
1259 "1 year ago".to_owned()
1260 } else {
1261 format!("{years} years ago")
1262 }
1263 }
1264}
1265
1266pub fn render_commit(
1272 repo: &Repository,
1273 oid: ObjectId,
1274 mode: &OutputMode,
1275 abbrev_len: usize,
1276) -> Result<String> {
1277 render_commit_with_color(repo, oid, mode, abbrev_len, false)
1278}
1279
1280pub fn render_commit_with_color(
1282 repo: &Repository,
1283 oid: ObjectId,
1284 mode: &OutputMode,
1285 abbrev_len: usize,
1286 use_color: bool,
1287) -> Result<String> {
1288 match mode {
1289 OutputMode::OidOnly => Ok(format!("{oid}")),
1290 OutputMode::Parents => {
1291 let mut out = format!("{oid}");
1292 let commit = load_commit(repo, oid)?;
1293 for parent in commit.parents {
1294 out.push(' ');
1295 out.push_str(&parent.to_hex());
1296 }
1297 Ok(out)
1298 }
1299 OutputMode::Format(fmt) => {
1300 let commit = load_commit(repo, oid)?;
1301 let subject = commit.message.lines().next().unwrap_or_default();
1302 let hex = oid.to_hex();
1303
1304 match fmt.as_str() {
1306 "oneline" => {
1307 return Ok(format!("{} {}", hex, subject));
1308 }
1309 "short" => {
1310 fn fmt_ident(ident: &str) -> String {
1311 let name = if let Some(bracket) = ident.find('<') {
1312 ident[..bracket].trim()
1313 } else {
1314 ident.trim()
1315 };
1316 let email = if let Some(start) = ident.find('<') {
1317 if let Some(end) = ident.find('>') {
1318 &ident[start..=end]
1319 } else {
1320 ""
1321 }
1322 } else {
1323 ""
1324 };
1325 format!("{} {}", name, email)
1326 }
1327 let mut out = String::new();
1328 out.push_str(&format!("Author: {}\n", fmt_ident(&commit.author)));
1329 out.push('\n');
1330 out.push_str(&format!(" {}\n", subject));
1331 out.push('\n');
1332 return Ok(out);
1333 }
1334 "medium" => {
1335 fn extract_ident_display(ident: &str) -> String {
1336 let name = if let Some(bracket) = ident.find('<') {
1337 ident[..bracket].trim()
1338 } else {
1339 ident.trim()
1340 };
1341 let email = if let Some(start) = ident.find('<') {
1342 if let Some(end) = ident.find('>') {
1343 &ident[start..=end]
1344 } else {
1345 ""
1346 }
1347 } else {
1348 ""
1349 };
1350 format!("{} {}", name, email)
1351 }
1352 fn format_default_date(ident: &str) -> String {
1353 let parts: Vec<&str> = ident.rsplitn(3, ' ').collect();
1354 if parts.len() < 2 {
1355 return String::new();
1356 }
1357 let ts_str = parts[1];
1358 let offset_str = parts[0];
1359 let ts: i64 = match ts_str.parse() {
1360 Ok(v) => v,
1361 Err(_) => return format!("{ts_str} {offset_str}"),
1362 };
1363 let tz_bytes = offset_str.as_bytes();
1364 let tz_secs: i64 = if tz_bytes.len() >= 5 {
1365 let sign = if tz_bytes[0] == b'-' { -1i64 } else { 1i64 };
1366 let h: i64 = offset_str[1..3].parse().unwrap_or(0);
1367 let m: i64 = offset_str[3..5].parse().unwrap_or(0);
1368 sign * (h * 3600 + m * 60)
1369 } else {
1370 0
1371 };
1372 let adjusted = ts + tz_secs;
1373 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
1374 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
1375 let weekday = match dt.weekday() {
1376 time::Weekday::Monday => "Mon",
1377 time::Weekday::Tuesday => "Tue",
1378 time::Weekday::Wednesday => "Wed",
1379 time::Weekday::Thursday => "Thu",
1380 time::Weekday::Friday => "Fri",
1381 time::Weekday::Saturday => "Sat",
1382 time::Weekday::Sunday => "Sun",
1383 };
1384 let month = match dt.month() {
1385 time::Month::January => "Jan",
1386 time::Month::February => "Feb",
1387 time::Month::March => "Mar",
1388 time::Month::April => "Apr",
1389 time::Month::May => "May",
1390 time::Month::June => "Jun",
1391 time::Month::July => "Jul",
1392 time::Month::August => "Aug",
1393 time::Month::September => "Sep",
1394 time::Month::October => "Oct",
1395 time::Month::November => "Nov",
1396 time::Month::December => "Dec",
1397 };
1398 format!(
1399 "{} {} {} {:02}:{:02}:{:02} {} {}",
1400 weekday,
1401 month,
1402 dt.day(),
1403 dt.hour(),
1404 dt.minute(),
1405 dt.second(),
1406 dt.year(),
1407 offset_str
1408 )
1409 }
1410 let mut out = String::new();
1411 out.push_str(&format!(
1412 "Author: {}\n",
1413 extract_ident_display(&commit.author)
1414 ));
1415 out.push_str(&format!(
1416 "Date: {}\n",
1417 format_default_date(&commit.author)
1418 ));
1419 out.push('\n');
1420 for line in commit.message.lines() {
1421 out.push_str(&format!(" {}\n", line));
1422 }
1423 return Ok(out);
1424 }
1425 _ => {}
1426 }
1427
1428 let raw_fmt = if let Some(t) = fmt.strip_prefix("format:") {
1429 t
1430 } else if let Some(t) = fmt.strip_prefix("tformat:") {
1431 t
1432 } else {
1433 fmt.as_str()
1434 };
1435 let body = {
1437 let mut lines = commit.message.lines();
1438 lines.next(); if let Some(blank) = lines.next() {
1441 if blank.is_empty() {
1442 lines.collect::<Vec<_>>().join("\n")
1443 } else {
1444 std::iter::once(blank)
1445 .chain(lines)
1446 .collect::<Vec<_>>()
1447 .join("\n")
1448 }
1449 } else {
1450 String::new()
1451 }
1452 };
1453 let tree_hex = commit.tree.to_hex();
1454 let parent_hexes: Vec<String> = commit.parents.iter().map(|p| p.to_hex()).collect();
1455 let parent_abbrevs: Vec<String> = commit
1456 .parents
1457 .iter()
1458 .map(|p| {
1459 let hex = p.to_hex();
1460 let n = abbrev_len.clamp(4, 40).min(hex.len());
1461 hex[..n].to_string()
1462 })
1463 .collect();
1464
1465 fn extract_name(ident: &str) -> &str {
1467 if let Some(bracket) = ident.find('<') {
1468 ident[..bracket].trim()
1469 } else {
1470 ident.trim()
1471 }
1472 }
1473 fn extract_email(ident: &str) -> &str {
1474 if let Some(start) = ident.find('<') {
1475 if let Some(end) = ident.find('>') {
1476 return &ident[start + 1..end];
1477 }
1478 }
1479 ""
1480 }
1481 fn extract_timestamp(ident: &str) -> String {
1482 match parse_signature_times(ident) {
1483 Some(p) => p.unix_seconds.to_string(),
1484 None => String::new(),
1485 }
1486 }
1487 fn weekday_str(dt: &time::OffsetDateTime) -> &'static str {
1488 match dt.weekday() {
1489 time::Weekday::Monday => "Mon",
1490 time::Weekday::Tuesday => "Tue",
1491 time::Weekday::Wednesday => "Wed",
1492 time::Weekday::Thursday => "Thu",
1493 time::Weekday::Friday => "Fri",
1494 time::Weekday::Saturday => "Sat",
1495 time::Weekday::Sunday => "Sun",
1496 }
1497 }
1498 fn month_str(dt: &time::OffsetDateTime) -> &'static str {
1499 match dt.month() {
1500 time::Month::January => "Jan",
1501 time::Month::February => "Feb",
1502 time::Month::March => "Mar",
1503 time::Month::April => "Apr",
1504 time::Month::May => "May",
1505 time::Month::June => "Jun",
1506 time::Month::July => "Jul",
1507 time::Month::August => "Aug",
1508 time::Month::September => "Sep",
1509 time::Month::October => "Oct",
1510 time::Month::November => "Nov",
1511 time::Month::December => "Dec",
1512 }
1513 }
1514 fn extract_email_local(ident: &str) -> &str {
1515 let email = extract_email(ident);
1516 if let Some(at) = email.find('@') {
1517 &email[..at]
1518 } else {
1519 email
1520 }
1521 }
1522 fn extract_date_default(ident: &str) -> String {
1523 let Some(p) = parse_signature_times(ident) else {
1524 return String::new();
1525 };
1526 let offset_str = ident.get(p.tz_hhmm_range.clone()).unwrap_or("+0000");
1527 let adjusted = p.unix_seconds + p.tz_offset_secs;
1528 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
1529 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
1530 format!(
1531 "{} {} {} {:02}:{:02}:{:02} {} {}",
1532 weekday_str(&dt),
1533 month_str(&dt),
1534 dt.day(),
1535 dt.hour(),
1536 dt.minute(),
1537 dt.second(),
1538 dt.year(),
1539 offset_str
1540 )
1541 }
1542 fn extract_date_rfc2822(ident: &str) -> String {
1543 let Some(p) = parse_signature_times(ident) else {
1544 return String::new();
1545 };
1546 let offset_str = ident.get(p.tz_hhmm_range.clone()).unwrap_or("+0000");
1547 let adjusted = p.unix_seconds + p.tz_offset_secs;
1548 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
1549 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
1550 format!(
1551 "{}, {} {} {} {:02}:{:02}:{:02} {}",
1552 weekday_str(&dt),
1553 dt.day(),
1554 month_str(&dt),
1555 dt.year(),
1556 dt.hour(),
1557 dt.minute(),
1558 dt.second(),
1559 offset_str
1560 )
1561 }
1562 fn extract_date_short(ident: &str) -> String {
1563 let Some(p) = parse_signature_times(ident) else {
1564 return String::new();
1565 };
1566 let adjusted = p.unix_seconds + p.tz_offset_secs;
1567 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
1568 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
1569 format!("{:04}-{:02}-{:02}", dt.year(), dt.month() as u8, dt.day())
1570 }
1571 fn extract_date_iso(ident: &str) -> String {
1572 let Some(p) = parse_signature_times(ident) else {
1573 return String::new();
1574 };
1575 let offset_str = ident.get(p.tz_hhmm_range.clone()).unwrap_or("+0000");
1576 let adjusted = p.unix_seconds + p.tz_offset_secs;
1577 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
1578 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
1579 format!(
1580 "{:04}-{:02}-{:02} {:02}:{:02}:{:02} {}",
1581 dt.year(),
1582 dt.month() as u8,
1583 dt.day(),
1584 dt.hour(),
1585 dt.minute(),
1586 dt.second(),
1587 offset_str
1588 )
1589 }
1590
1591 #[derive(Clone, Copy)]
1593 enum Align {
1594 Left,
1595 Right,
1596 Center,
1597 }
1598 #[derive(Clone, Copy)]
1599 enum Trunc {
1600 None,
1601 Trunc,
1602 LTrunc,
1603 MTrunc,
1604 }
1605 struct ColSpec {
1606 width: usize,
1607 align: Align,
1608 trunc: Trunc,
1609 }
1610 fn apply_col(spec: &ColSpec, s: &str) -> String {
1611 let char_len = s.chars().count();
1612 if char_len > spec.width {
1613 match spec.trunc {
1614 Trunc::None => s.to_owned(),
1615 Trunc::Trunc => {
1616 let mut out: String =
1617 s.chars().take(spec.width.saturating_sub(2)).collect();
1618 out.push_str("..");
1619 out
1620 }
1621 Trunc::LTrunc => {
1622 let skip = char_len - spec.width + 2;
1623 let mut out = String::from("..");
1624 out.extend(s.chars().skip(skip));
1625 out
1626 }
1627 Trunc::MTrunc => {
1628 let keep = spec.width.saturating_sub(2);
1629 let left_half = keep / 2;
1630 let right_half = keep - left_half;
1631 let mut out: String = s.chars().take(left_half).collect();
1632 out.push_str("..");
1633 out.extend(s.chars().skip(char_len - right_half));
1634 out
1635 }
1636 }
1637 } else {
1638 let pad = spec.width - char_len;
1639 match spec.align {
1640 Align::Left => {
1641 let mut out = s.to_owned();
1642 for _ in 0..pad {
1643 out.push(' ');
1644 }
1645 out
1646 }
1647 Align::Right => {
1648 let mut out = String::new();
1649 for _ in 0..pad {
1650 out.push(' ');
1651 }
1652 out.push_str(s);
1653 out
1654 }
1655 Align::Center => {
1656 let left = pad / 2;
1657 let right = pad - left;
1658 let mut out = String::new();
1659 for _ in 0..left {
1660 out.push(' ');
1661 }
1662 out.push_str(s);
1663 for _ in 0..right {
1664 out.push(' ');
1665 }
1666 out
1667 }
1668 }
1669 }
1670 }
1671 fn parse_col_spec(
1672 chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
1673 align: Align,
1674 ) -> Option<ColSpec> {
1675 if chars.peek() != Some(&'(') {
1677 return None;
1678 }
1679 chars.next();
1680 let mut num_str = String::new();
1681 while let Some(&c) = chars.peek() {
1682 if c.is_ascii_digit() {
1683 num_str.push(c);
1684 chars.next();
1685 } else {
1686 break;
1687 }
1688 }
1689 let width: usize = num_str.parse().ok()?;
1690 let trunc = if chars.peek() == Some(&',') {
1691 chars.next(); let mut mode = String::new();
1693 while let Some(&c) = chars.peek() {
1694 if c == ')' {
1695 break;
1696 }
1697 mode.push(c);
1698 chars.next();
1699 }
1700 match mode.as_str() {
1701 "trunc" => Trunc::Trunc,
1702 "ltrunc" => Trunc::LTrunc,
1703 "mtrunc" => Trunc::MTrunc,
1704 _ => Trunc::None,
1705 }
1706 } else {
1707 Trunc::None
1708 };
1709 if chars.peek() == Some(&')') {
1711 chars.next();
1712 }
1713 Some(ColSpec {
1714 width,
1715 align,
1716 trunc,
1717 })
1718 }
1719
1720 let mut pending_col: Option<ColSpec> = None;
1721 let mut rendered = String::new();
1722 let mut chars = raw_fmt.chars().peekable();
1723 while let Some(ch) = chars.next() {
1724 if ch != '%' {
1725 rendered.push(ch);
1726 continue;
1727 }
1728 if chars.peek() == Some(&'<') {
1730 chars.next();
1731 if let Some(spec) = parse_col_spec(&mut chars, Align::Left) {
1732 pending_col = Some(spec);
1733 }
1734 continue;
1735 }
1736 if chars.peek() == Some(&'>') {
1737 chars.next();
1738 if chars.peek() == Some(&'<') {
1739 chars.next(); if let Some(spec) = parse_col_spec(&mut chars, Align::Center) {
1741 pending_col = Some(spec);
1742 }
1743 } else if chars.peek() == Some(&'>') {
1744 chars.next(); if let Some(spec) = parse_col_spec(&mut chars, Align::Right) {
1746 pending_col = Some(spec);
1747 }
1748 } else if let Some(spec) = parse_col_spec(&mut chars, Align::Right) {
1749 pending_col = Some(spec);
1750 }
1751 continue;
1752 }
1753
1754 let mut expanded = String::new();
1756 let target = if pending_col.is_some() {
1757 &mut expanded
1758 } else {
1759 &mut rendered
1760 };
1761 match chars.peek() {
1762 Some('%') => {
1763 chars.next();
1764 target.push('%');
1765 }
1766 Some('H') => {
1767 chars.next();
1768 target.push_str(&oid.to_hex());
1769 }
1770 Some('h') => {
1771 chars.next();
1772 let hex = oid.to_hex();
1773 let n = abbrev_len.clamp(4, 40).min(hex.len());
1774 target.push_str(&hex[..n]);
1775 }
1776 Some('T') => {
1777 chars.next();
1778 target.push_str(&tree_hex);
1779 }
1780 Some('t') => {
1781 chars.next();
1782 let n = abbrev_len.clamp(4, 40).min(tree_hex.len());
1783 target.push_str(&tree_hex[..n]);
1784 }
1785 Some('P') => {
1786 chars.next();
1787 target.push_str(&parent_hexes.join(" "));
1788 }
1789 Some('p') => {
1790 chars.next();
1791 target.push_str(&parent_abbrevs.join(" "));
1792 }
1793 Some('n') => {
1794 chars.next();
1795 target.push('\n');
1796 }
1797 Some('s') => {
1798 chars.next();
1799 target.push_str(subject);
1800 }
1801 Some('b') => {
1802 chars.next();
1803 target.push_str(&body);
1804 if !body.is_empty() {
1805 target.push('\n');
1806 }
1807 }
1808 Some('B') => {
1809 chars.next();
1810 target.push_str(&commit.message);
1811 }
1812 Some('a') => {
1813 chars.next();
1814 match chars.next() {
1815 Some('n') => target.push_str(extract_name(&commit.author)),
1816 Some('N') => target.push_str(extract_name(&commit.author)),
1817 Some('e') => target.push_str(extract_email(&commit.author)),
1818 Some('E') => target.push_str(extract_email(&commit.author)),
1819 Some('l') => target.push_str(extract_email_local(&commit.author)),
1820 Some('d') => target.push_str(&extract_date_default(&commit.author)),
1821 Some('D') => target.push_str(&extract_date_rfc2822(&commit.author)),
1822 Some('t') => target.push_str(&extract_timestamp(&commit.author)),
1823 Some('s') => target.push_str(&extract_date_short(&commit.author)),
1824 Some('i') => target.push_str(&extract_date_iso(&commit.author)),
1825 Some('I') => {
1826 let Some(p) = parse_signature_times(&commit.author) else {
1827 break;
1828 };
1829 let adjusted = p.unix_seconds + p.tz_offset_secs;
1830 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
1831 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
1832 let sign_ch = if p.tz_offset_secs >= 0 { '+' } else { '-' };
1833 let abs_off = p.tz_offset_secs.unsigned_abs();
1834 target.push_str(&format!(
1835 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}{:02}:{:02}",
1836 dt.year(),
1837 dt.month() as u8,
1838 dt.day(),
1839 dt.hour(),
1840 dt.minute(),
1841 dt.second(),
1842 sign_ch,
1843 abs_off / 3600,
1844 (abs_off % 3600) / 60
1845 ));
1846 }
1847 Some('r') => {
1848 let Some(p) = parse_signature_times(&commit.author) else {
1849 break;
1850 };
1851 let now = std::time::SystemTime::now()
1852 .duration_since(std::time::UNIX_EPOCH)
1853 .unwrap_or_default()
1854 .as_secs() as i64;
1855 target.push_str(&format_relative_date(now - p.unix_seconds));
1856 }
1857 Some(other) => {
1858 target.push('%');
1859 target.push('a');
1860 target.push(other);
1861 }
1862 None => {
1863 target.push('%');
1864 target.push('a');
1865 }
1866 }
1867 }
1868 Some('c') => {
1869 chars.next();
1870 match chars.next() {
1871 Some('n') => target.push_str(extract_name(&commit.committer)),
1872 Some('N') => target.push_str(extract_name(&commit.committer)),
1873 Some('e') => target.push_str(extract_email(&commit.committer)),
1874 Some('E') => target.push_str(extract_email(&commit.committer)),
1875 Some('l') => target.push_str(extract_email_local(&commit.committer)),
1876 Some('d') => target.push_str(&extract_date_default(&commit.committer)),
1877 Some('D') => target.push_str(&extract_date_rfc2822(&commit.committer)),
1878 Some('t') => target.push_str(&extract_timestamp(&commit.committer)),
1879 Some('s') => target.push_str(&extract_date_short(&commit.committer)),
1880 Some('i') => target.push_str(&extract_date_iso(&commit.committer)),
1881 Some('I') => {
1882 let Some(p) = parse_signature_times(&commit.committer) else {
1883 break;
1884 };
1885 let adjusted = p.unix_seconds + p.tz_offset_secs;
1886 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
1887 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
1888 let sign_ch = if p.tz_offset_secs >= 0 { '+' } else { '-' };
1889 let abs_off = p.tz_offset_secs.unsigned_abs();
1890 target.push_str(&format!(
1891 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}{:02}:{:02}",
1892 dt.year(),
1893 dt.month() as u8,
1894 dt.day(),
1895 dt.hour(),
1896 dt.minute(),
1897 dt.second(),
1898 sign_ch,
1899 abs_off / 3600,
1900 (abs_off % 3600) / 60
1901 ));
1902 }
1903 Some('r') => {
1904 let Some(p) = parse_signature_times(&commit.committer) else {
1905 break;
1906 };
1907 let now = std::time::SystemTime::now()
1908 .duration_since(std::time::UNIX_EPOCH)
1909 .unwrap_or_default()
1910 .as_secs() as i64;
1911 target.push_str(&format_relative_date(now - p.unix_seconds));
1912 }
1913 Some(other) => {
1914 target.push('%');
1915 target.push('c');
1916 target.push(other);
1917 }
1918 None => {
1919 target.push('%');
1920 target.push('c');
1921 }
1922 }
1923 }
1924 Some('x') => {
1925 chars.next();
1927 let mut hex_str = String::new();
1928 if let Some(&c1) = chars.peek() {
1929 if c1.is_ascii_hexdigit() {
1930 hex_str.push(c1);
1931 chars.next();
1932 }
1933 }
1934 if let Some(&c2) = chars.peek() {
1935 if c2.is_ascii_hexdigit() {
1936 hex_str.push(c2);
1937 chars.next();
1938 }
1939 }
1940 if let Ok(byte) = u8::from_str_radix(&hex_str, 16) {
1941 target.push(byte as char);
1942 }
1943 }
1944 Some('C') => {
1945 chars.next();
1946 if chars.peek() == Some(&'(') {
1947 chars.next();
1948 let mut spec = String::new();
1949 for c in chars.by_ref() {
1950 if c == ')' {
1951 break;
1952 }
1953 spec.push(c);
1954 }
1955 let (force, color_spec) =
1956 if let Some(rest) = spec.strip_prefix("always,") {
1957 (true, rest)
1958 } else if let Some(rest) = spec.strip_prefix("auto,") {
1959 (false, rest)
1960 } else if spec == "auto" {
1961 if use_color {
1962 target.push_str("\x1b[m");
1963 }
1964 continue;
1965 } else {
1966 (false, spec.as_str())
1967 };
1968 if use_color || force {
1969 target.push_str(&ansi_color_from_spec(color_spec));
1970 }
1971 } else {
1972 let remaining: String = chars.clone().collect();
1975 let known = [
1976 "reset", "red", "green", "blue", "yellow", "magenta", "cyan",
1977 "white", "bold", "dim", "ul",
1978 ];
1979 let mut matched = false;
1980 for name in &known {
1981 if remaining.starts_with(name) {
1982 for _ in 0..name.len() {
1983 chars.next();
1984 }
1985 if use_color {
1986 target.push_str(&ansi_color_from_name(name));
1987 }
1988 matched = true;
1989 break;
1990 }
1991 }
1992 if !matched {
1993 while let Some(&c) = chars.peek() {
1995 if c.is_alphanumeric() {
1996 chars.next();
1997 } else {
1998 break;
1999 }
2000 }
2001 }
2002 }
2003 }
2004 Some('w') => {
2005 chars.next();
2007 if chars.peek() == Some(&'(') {
2008 chars.next();
2009 for c in chars.by_ref() {
2010 if c == ')' {
2011 break;
2012 }
2013 }
2014 }
2015 }
2016 Some('+') => {
2017 chars.next();
2019 if chars.peek() == Some(&'%') {
2021 }
2025 let mut sub = String::new();
2027 if let Some(&nc) = chars.peek() {
2028 match nc {
2029 'b' => {
2030 chars.next();
2031 sub.push_str(&body);
2032 if !body.is_empty() {
2033 sub.push('\n');
2034 }
2035 }
2036 's' => {
2037 chars.next();
2038 sub.push_str(subject);
2039 }
2040 _ => {
2041 chars.next();
2042 sub.push('%');
2043 sub.push('+');
2044 sub.push(nc);
2045 }
2046 }
2047 }
2048 if !sub.is_empty() {
2049 target.push('\n');
2050 target.push_str(&sub);
2051 }
2052 }
2053 Some('-') => {
2054 chars.next();
2056 if let Some(&nc) = chars.peek() {
2058 match nc {
2059 'b' => {
2060 chars.next();
2061 if !body.is_empty() {
2062 target.push_str(&body);
2063 target.push('\n');
2064 }
2065 }
2066 's' => {
2067 chars.next();
2068 target.push_str(subject);
2069 }
2070 _ => {
2071 chars.next();
2072 target.push('%');
2073 target.push('-');
2074 target.push(nc);
2075 }
2076 }
2077 }
2078 }
2079 Some('d') => {
2080 chars.next();
2082 }
2083 Some('D') => {
2084 chars.next();
2086 }
2087 Some('e') => {
2088 chars.next();
2090 }
2091 Some('g') => {
2092 chars.next();
2094 if let Some(&_nc) = chars.peek() {
2095 chars.next(); }
2098 }
2099 Some(&other) => {
2100 chars.next();
2101 target.push('%');
2102 target.push(other);
2103 }
2104 None => target.push('%'),
2105 }
2106 if let Some(spec) = pending_col.take() {
2108 let formatted = apply_col(&spec, &expanded);
2109 rendered.push_str(&formatted);
2110 }
2111 }
2112 Ok(rendered)
2113 }
2114 }
2115}
2116
2117#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2118enum ExpectedObjectKind {
2119 Commit,
2120 Tree,
2121 Blob,
2122}
2123
2124impl ExpectedObjectKind {
2125 fn from_tag_type(kind: &str) -> Option<Self> {
2126 match kind {
2127 "commit" => Some(Self::Commit),
2128 "tree" => Some(Self::Tree),
2129 "blob" => Some(Self::Blob),
2130 _ => None,
2131 }
2132 }
2133
2134 fn matches(self, kind: ObjectKind) -> bool {
2135 matches!(
2136 (self, kind),
2137 (Self::Commit, ObjectKind::Commit)
2138 | (Self::Tree, ObjectKind::Tree)
2139 | (Self::Blob, ObjectKind::Blob)
2140 )
2141 }
2142
2143 fn as_str(self) -> &'static str {
2144 match self {
2145 Self::Commit => "commit",
2146 Self::Tree => "tree",
2147 Self::Blob => "blob",
2148 }
2149 }
2150}
2151
2152#[derive(Clone, Debug)]
2154pub struct ObjectWalkRoot {
2155 pub oid: ObjectId,
2157 pub input: String,
2158 pub root_path: Option<String>,
2160}
2161
2162#[derive(Clone, Debug)]
2163struct RootObject {
2164 oid: ObjectId,
2165 input: String,
2166 expected_kind: Option<ExpectedObjectKind>,
2167 root_path: Option<String>,
2168 wrap_with_tag: Option<ObjectId>,
2170}
2171
2172fn object_walk_print_commit_line(
2173 filter_provided_objects: bool,
2174 filter: Option<&ObjectFilter>,
2175 user_given_tip: bool,
2176) -> bool {
2177 if !filter_provided_objects {
2178 return user_given_tip || filter_shows_commit_line_when_not_user_given(filter);
2179 }
2180 match filter {
2181 Some(ObjectFilter::ObjectType(FilterObjectKind::Commit)) => {
2182 user_given_tip || filter_shows_commit_line_when_not_user_given(filter)
2183 }
2184 Some(ObjectFilter::ObjectType(_)) => false,
2185 Some(ObjectFilter::Combine(parts)) => {
2186 if parts
2187 .iter()
2188 .all(|p| matches!(p, ObjectFilter::ObjectType(FilterObjectKind::Commit)))
2189 {
2190 user_given_tip || filter_shows_commit_line_when_not_user_given(filter)
2191 } else {
2192 false
2193 }
2194 }
2195 _ => user_given_tip || filter_shows_commit_line_when_not_user_given(filter),
2196 }
2197}
2198
2199fn filter_shows_commit_line_when_not_user_given(filter: Option<&ObjectFilter>) -> bool {
2201 match filter {
2202 None => true,
2203 Some(ObjectFilter::BlobNone)
2204 | Some(ObjectFilter::BlobLimit(_))
2205 | Some(ObjectFilter::TreeDepth(_))
2206 | Some(ObjectFilter::SparseOid(_)) => true,
2207 Some(ObjectFilter::ObjectType(FilterObjectKind::Commit)) => true,
2208 Some(ObjectFilter::ObjectType(
2209 FilterObjectKind::Blob | FilterObjectKind::Tree | FilterObjectKind::Tag,
2210 )) => false,
2211 Some(ObjectFilter::Combine(parts)) => parts
2212 .iter()
2213 .all(|p| filter_shows_commit_line_when_not_user_given(Some(p))),
2214 }
2215}
2216
2217fn skip_tree_descent_for_object_type_filter(filter: Option<&ObjectFilter>) -> bool {
2218 match filter {
2219 Some(ObjectFilter::ObjectType(FilterObjectKind::Commit | FilterObjectKind::Tag)) => true,
2220 Some(ObjectFilter::Combine(parts)) => parts
2221 .iter()
2222 .any(|p| skip_tree_descent_for_object_type_filter(Some(p))),
2223 _ => false,
2224 }
2225}
2226
2227fn sparse_filter_includes_path(
2228 repo: &Repository,
2229 path: &str,
2230 sparse_lines: Option<&[String]>,
2231) -> bool {
2232 sparse_lines
2233 .map(|lines| path_in_sparse_checkout(path, lines, repo.work_tree.as_deref()))
2234 .unwrap_or(true)
2235}
2236
2237fn sparse_oid_lines_from_filter(
2238 repo: &Repository,
2239 filter: Option<&ObjectFilter>,
2240) -> Option<Vec<String>> {
2241 let f = filter?;
2242 match f {
2243 ObjectFilter::SparseOid(spec) => {
2244 let blob_oid = if let Ok(oid) = spec.parse::<ObjectId>() {
2245 oid
2246 } else if let Some((treeish, path)) = split_treeish_spec(spec) {
2247 let treeish_oid = resolve_revision_for_range_end(repo, treeish).ok()?;
2248 resolve_treeish_path(repo, treeish_oid, path).ok()?
2249 } else {
2250 return None;
2251 };
2252 let obj = repo.odb.read(&blob_oid).ok()?;
2253 if obj.kind != ObjectKind::Blob {
2254 return None;
2255 }
2256 let text = std::str::from_utf8(&obj.data).ok()?;
2257 Some(parse_sparse_patterns_from_blob(text))
2258 }
2259 ObjectFilter::Combine(parts) => {
2260 for p in parts {
2261 if let Some(lines) = sparse_oid_lines_from_filter(repo, Some(p)) {
2262 return Some(lines);
2263 }
2264 }
2265 None
2266 }
2267 _ => None,
2268 }
2269}
2270
2271fn packed_object_set(repo: &Repository) -> HashSet<ObjectId> {
2272 let mut out = HashSet::new();
2273 let objects_dir = repo.odb.objects_dir();
2274 if let Ok(indexes) = pack::read_local_pack_indexes(objects_dir) {
2275 for idx in indexes {
2276 for e in idx.entries {
2277 if let Ok(oid) = ObjectId::from_bytes(&e.oid) {
2278 out.insert(oid);
2279 }
2280 }
2281 }
2282 }
2283 out
2284}
2285
2286fn resolve_specs(repo: &Repository, specs: &[String]) -> Result<Vec<ObjectId>> {
2287 let mut out = Vec::with_capacity(specs.len());
2288 for spec in specs {
2289 let oid = resolve_revision_for_range_end(repo, spec)?;
2290 let commit_oid = peel_to_commit(repo, oid)?;
2291 out.push(commit_oid);
2292 }
2293 Ok(out)
2294}
2295
2296pub fn resolve_revision_commits(repo: &Repository, specs: &[String]) -> Result<Vec<ObjectId>> {
2301 resolve_specs(repo, specs)
2302}
2303
2304pub fn resolve_revision_specs_to_commits(
2310 repo: &Repository,
2311 specs: &[String],
2312) -> Result<Vec<ObjectId>> {
2313 resolve_specs(repo, specs)
2314}
2315
2316pub fn resolve_object_walk_roots(
2317 repo: &Repository,
2318 specs: &[String],
2319) -> Result<(Vec<ObjectId>, Vec<ObjectWalkRoot>)> {
2320 let (commits, roots, _tip_annotated_tag_by_commit) = resolve_specs_for_objects(repo, specs)?;
2321 Ok((
2322 commits,
2323 roots
2324 .into_iter()
2325 .map(|r| ObjectWalkRoot {
2326 oid: r.oid,
2327 input: r.input,
2328 root_path: r.root_path,
2329 })
2330 .collect(),
2331 ))
2332}
2333
2334fn resolve_specs_for_objects(
2335 repo: &Repository,
2336 specs: &[String],
2337) -> Result<(Vec<ObjectId>, Vec<RootObject>, HashMap<ObjectId, ObjectId>)> {
2338 let mut commits = Vec::new();
2339 let mut roots = Vec::new();
2340 let mut tip_annotated_tag_by_commit: HashMap<ObjectId, ObjectId> = HashMap::new();
2341
2342 for spec in specs {
2343 if let Ok(raw_oid) = spec.parse::<ObjectId>() {
2344 let raw_object = repo.odb.read(&raw_oid)?;
2345 match raw_object.kind {
2346 ObjectKind::Commit => {
2347 commits.push(raw_oid);
2348 }
2349 ObjectKind::Tag => {
2350 let tag = parse_tag(&raw_object.data)?;
2351 let expected_kind = ExpectedObjectKind::from_tag_type(&tag.object_type)
2352 .ok_or_else(|| {
2353 Error::CorruptObject(format!(
2354 "object {spec} has unsupported tag type '{}'",
2355 tag.object_type
2356 ))
2357 })?;
2358 if expected_kind == ExpectedObjectKind::Commit {
2359 tip_annotated_tag_by_commit.insert(tag.object, raw_oid);
2360 }
2361 roots.push(RootObject {
2362 oid: tag.object,
2363 input: spec.clone(),
2364 expected_kind: Some(expected_kind),
2365 root_path: None,
2366 wrap_with_tag: Some(raw_oid),
2367 });
2368 }
2369 ObjectKind::Tree | ObjectKind::Blob => roots.push(RootObject {
2370 oid: raw_oid,
2371 input: spec.clone(),
2372 expected_kind: None,
2373 root_path: None,
2374 wrap_with_tag: None,
2375 }),
2376 }
2377 continue;
2378 }
2379
2380 if let Some((treeish, path)) = split_treeish_spec(spec) {
2381 if !path.is_empty() {
2382 let treeish_oid = resolve_revision_for_range_end(repo, treeish)?;
2383 let blob_oid = resolve_treeish_path(repo, treeish_oid, path)?;
2384 roots.push(RootObject {
2385 oid: blob_oid,
2386 input: spec.clone(),
2387 expected_kind: Some(ExpectedObjectKind::Blob),
2388 root_path: Some(path.to_owned()),
2389 wrap_with_tag: None,
2390 });
2391 continue;
2392 }
2393 }
2394
2395 let oid = resolve_revision_for_range_end(repo, spec)?;
2396 if let Ok(obj) = repo.odb.read(&oid) {
2397 if obj.kind == ObjectKind::Tag {
2398 if let Ok(commit_oid) = peel_to_commit(repo, oid) {
2399 tip_annotated_tag_by_commit.insert(commit_oid, oid);
2400 }
2401 }
2402 }
2403 match peel_to_commit(repo, oid) {
2404 Ok(commit_oid) => commits.push(commit_oid),
2405 Err(Error::CorruptObject(_)) | Err(Error::ObjectNotFound(_)) => {
2406 roots.push(RootObject {
2407 oid,
2408 input: spec.clone(),
2409 expected_kind: None,
2410 root_path: None,
2411 wrap_with_tag: None,
2412 })
2413 }
2414 Err(err) => return Err(err),
2415 }
2416 }
2417
2418 Ok((commits, roots, tip_annotated_tag_by_commit))
2419}
2420
2421fn peel_to_commit(repo: &Repository, mut oid: ObjectId) -> Result<ObjectId> {
2423 loop {
2424 let object = repo.odb.read(&oid)?;
2425 match object.kind {
2426 ObjectKind::Commit => return Ok(oid),
2427 ObjectKind::Tag => {
2428 let tag = parse_tag(&object.data)?;
2429 oid = tag.object;
2430 }
2431 other => {
2432 return Err(Error::CorruptObject(format!(
2433 "object {oid} is a {other:?}, not a commit"
2434 )));
2435 }
2436 }
2437 }
2438}
2439
2440fn reflog_commit_tips(repo: &Repository) -> Result<Vec<ObjectId>> {
2441 let z = zero_oid();
2442 let mut out = Vec::new();
2443 let mut seen = HashSet::new();
2444 for refname in list_reflog_refs(&repo.git_dir)? {
2445 let entries = read_reflog(&repo.git_dir, &refname)?;
2446 for e in entries {
2447 for oid in [e.old_oid, e.new_oid] {
2448 if oid == z {
2449 continue;
2450 }
2451 match peel_to_commit(repo, oid) {
2452 Ok(c) if seen.insert(c) => out.push(c),
2453 Err(_) => {}
2454 _ => {}
2455 }
2456 }
2457 }
2458 }
2459 Ok(out)
2460}
2461
2462pub(crate) fn walk_closure(
2463 graph: &mut CommitGraph<'_>,
2464 starts: &[ObjectId],
2465) -> Result<HashSet<ObjectId>> {
2466 let (seen, _) = walk_closure_ordered(graph, starts)?;
2467 Ok(seen)
2468}
2469
2470fn walk_closure_first_parent_only(
2473 graph: &mut CommitGraph<'_>,
2474 starts: &[ObjectId],
2475) -> Result<HashSet<ObjectId>> {
2476 let mut seen = HashSet::new();
2477 let mut queue = VecDeque::new();
2478 for &start in starts {
2479 queue.push_back(start);
2480 }
2481 while let Some(oid) = queue.pop_front() {
2482 if !seen.insert(oid) {
2483 continue;
2484 }
2485 let parents = graph.parents_of(oid)?;
2486 if let Some(&p) = parents.first() {
2487 queue.push_back(p);
2488 }
2489 }
2490 Ok(seen)
2491}
2492
2493pub(crate) fn walk_closure_ordered(
2495 graph: &mut CommitGraph<'_>,
2496 starts: &[ObjectId],
2497) -> Result<(HashSet<ObjectId>, Vec<ObjectId>)> {
2498 let mut seen = HashSet::new();
2499 let mut order = Vec::new();
2500 let mut queue = VecDeque::new();
2501 for &start in starts {
2502 queue.push_back(start);
2503 }
2504 while let Some(oid) = queue.pop_front() {
2505 if !seen.insert(oid) {
2506 continue;
2507 }
2508 order.push(oid);
2509 for parent in graph.parents_of(oid)? {
2510 queue.push_back(parent);
2511 }
2512 }
2513 Ok((seen, order))
2514}
2515
2516fn date_order_walk_with_tips(
2520 graph: &mut CommitGraph<'_>,
2521 tips: &[ObjectId],
2522 selected: &HashSet<ObjectId>,
2523 author_dates: bool,
2524) -> Result<Vec<ObjectId>> {
2525 let mut unfinished_children: HashMap<ObjectId, usize> =
2526 selected.iter().map(|&oid| (oid, 0usize)).collect();
2527 for &child in selected {
2528 for parent in graph.parents_of(child)? {
2529 if selected.contains(&parent) {
2530 if let Some(count) = unfinished_children.get_mut(&parent) {
2531 *count += 1;
2532 }
2533 }
2534 }
2535 }
2536
2537 let mut heap = BinaryHeap::new();
2538 for &tip in tips {
2539 if selected.contains(&tip) {
2540 heap.push(CommitDateKey {
2541 oid: tip,
2542 date: graph.sort_key(tip, author_dates),
2543 });
2544 }
2545 }
2546
2547 let mut emitted = HashSet::new();
2548 let mut out = Vec::with_capacity(selected.len());
2549 while let Some(item) = heap.pop() {
2550 if !emitted.insert(item.oid) {
2551 continue;
2552 }
2553 out.push(item.oid);
2554 for parent in graph.parents_of(item.oid)? {
2555 if !selected.contains(&parent) {
2556 continue;
2557 }
2558 let Some(count) = unfinished_children.get_mut(&parent) else {
2559 continue;
2560 };
2561 *count = count.saturating_sub(1);
2562 if *count == 0 {
2563 heap.push(CommitDateKey {
2564 oid: parent,
2565 date: graph.sort_key(parent, author_dates),
2566 });
2567 }
2568 }
2569 }
2570
2571 Ok(out)
2572}
2573
2574pub(crate) fn date_order_walk(
2581 graph: &mut CommitGraph<'_>,
2582 selected: &HashSet<ObjectId>,
2583 author_dates: bool,
2584) -> Result<Vec<ObjectId>> {
2585 let mut unfinished_children: HashMap<ObjectId, usize> =
2586 selected.iter().map(|&oid| (oid, 0usize)).collect();
2587 for &child in selected {
2588 for parent in graph.parents_of(child)? {
2589 if selected.contains(&parent) {
2590 if let Some(count) = unfinished_children.get_mut(&parent) {
2591 *count += 1;
2592 }
2593 }
2594 }
2595 }
2596
2597 let mut heap = BinaryHeap::new();
2603 for &oid in selected {
2604 if unfinished_children.get(&oid).copied().unwrap_or(0) == 0 {
2605 heap.push(CommitDateKey {
2606 oid,
2607 date: graph.sort_key(oid, author_dates),
2608 });
2609 }
2610 }
2611
2612 let mut emitted = HashSet::new();
2613 let mut out = Vec::with_capacity(selected.len());
2614 while let Some(item) = heap.pop() {
2615 if !emitted.insert(item.oid) {
2616 continue;
2617 }
2618 out.push(item.oid);
2619 for parent in graph.parents_of(item.oid)? {
2620 if !selected.contains(&parent) {
2621 continue;
2622 }
2623 let Some(count) = unfinished_children.get_mut(&parent) else {
2624 continue;
2625 };
2626 *count = count.saturating_sub(1);
2627 if *count == 0 {
2628 heap.push(CommitDateKey {
2629 oid: parent,
2630 date: graph.sort_key(parent, author_dates),
2631 });
2632 }
2633 }
2634 }
2635
2636 Ok(out)
2637}
2638
2639fn topo_sort(
2640 graph: &mut CommitGraph<'_>,
2641 selected: &HashSet<ObjectId>,
2642 author_dates: bool,
2643) -> Result<Vec<ObjectId>> {
2644 let mut child_count: HashMap<ObjectId, usize> = selected.iter().map(|&oid| (oid, 0)).collect();
2645
2646 for &oid in selected {
2647 for parent in graph.parents_of(oid)? {
2648 if !selected.contains(&parent) {
2649 continue;
2650 }
2651 if let Some(count) = child_count.get_mut(&parent) {
2652 *count += 1;
2653 }
2654 }
2655 }
2656
2657 let mut ready: BinaryHeap<Reverse<CommitDateKey>> = BinaryHeap::new();
2661 for (&oid, &count) in &child_count {
2662 if count == 0 {
2663 ready.push(Reverse(CommitDateKey {
2664 oid,
2665 date: graph.sort_key(oid, author_dates),
2666 }));
2667 }
2668 }
2669
2670 let mut out = Vec::with_capacity(selected.len());
2671 while let Some(Reverse(item)) = ready.pop() {
2672 let oid = item.oid;
2673 out.push(oid);
2674 for parent in graph.parents_of(oid)? {
2675 if !selected.contains(&parent) {
2676 continue;
2677 }
2678 if let Some(count) = child_count.get_mut(&parent) {
2679 *count = count.saturating_sub(1);
2680 if *count == 0 {
2681 ready.push(Reverse(CommitDateKey {
2682 oid: parent,
2683 date: graph.sort_key(parent, author_dates),
2684 }));
2685 }
2686 }
2687 }
2688 }
2689
2690 Ok(out)
2691}
2692
2693fn simplify_merges_commit_list(repo: &Repository, commits: &[ObjectId]) -> Result<Vec<ObjectId>> {
2694 let selected: HashSet<ObjectId> = commits.iter().copied().collect();
2695 let mut out = Vec::new();
2696 for oid in commits {
2697 let raw_parents = load_commit(repo, *oid)?.parents;
2698 let direct: Vec<ObjectId> = raw_parents
2699 .iter()
2700 .copied()
2701 .filter(|p| selected.contains(p))
2702 .collect();
2703 if raw_parents.len() > 1 && direct.len() <= 1 {
2704 continue;
2705 }
2706 if direct.len() <= 1 {
2707 out.push(*oid);
2708 continue;
2709 }
2710 let mut simplified = graph_simplify_parent_list_lib(repo, &selected, &direct)?;
2711 simplified.sort_unstable();
2712 simplified.dedup();
2713 if simplified.len() > 1 {
2714 out.push(*oid);
2715 }
2716 }
2717 Ok(out)
2718}
2719
2720fn graph_simplify_parent_list_lib(
2721 repo: &Repository,
2722 selected: &HashSet<ObjectId>,
2723 parents: &[ObjectId],
2724) -> Result<Vec<ObjectId>> {
2725 let mut out = Vec::new();
2726 for parent in parents {
2727 if parent_reachable_via_others_lib(repo, selected, *parent, parents)? {
2728 continue;
2729 }
2730 out.push(*parent);
2731 }
2732 Ok(out)
2733}
2734
2735fn parent_reachable_via_others_lib(
2736 repo: &Repository,
2737 selected: &HashSet<ObjectId>,
2738 target: ObjectId,
2739 parents: &[ObjectId],
2740) -> Result<bool> {
2741 for parent in parents {
2742 if *parent == target {
2743 continue;
2744 }
2745 if graph_reaches_lib(repo, selected, *parent, target)? {
2746 return Ok(true);
2747 }
2748 }
2749 Ok(false)
2750}
2751
2752fn graph_reaches_lib(
2753 repo: &Repository,
2754 selected: &HashSet<ObjectId>,
2755 start: ObjectId,
2756 target: ObjectId,
2757) -> Result<bool> {
2758 let mut stack = vec![start];
2759 let mut seen = HashSet::new();
2760 while let Some(oid) = stack.pop() {
2761 if !seen.insert(oid) {
2762 continue;
2763 }
2764 if oid == target {
2765 return Ok(true);
2766 }
2767 let mut parents = load_commit(repo, oid)?.parents;
2768 parents.retain(|p| selected.contains(p));
2769 stack.extend(parents);
2770 }
2771 Ok(false)
2772}
2773
2774fn load_raw_parents_lib(repo: &Repository, oid: ObjectId) -> Result<Vec<ObjectId>> {
2775 Ok(load_commit(repo, oid)?.parents)
2776}
2777
2778fn first_parent_of_commit_lib(repo: &Repository, oid: ObjectId) -> Result<Option<ObjectId>> {
2779 let parents = load_raw_parents_lib(repo, oid)?;
2780 Ok(parents.first().copied())
2781}
2782
2783fn first_parent_anchor_in_set_lib(
2784 repo: &Repository,
2785 start: ObjectId,
2786 anchors: &HashSet<ObjectId>,
2787) -> Result<Option<ObjectId>> {
2788 let mut seen = HashSet::new();
2789 let mut cursor = Some(start);
2790 while let Some(oid) = cursor {
2791 if !seen.insert(oid) {
2792 break;
2793 }
2794 if anchors.contains(&oid) {
2795 return Ok(Some(oid));
2796 }
2797 cursor = first_parent_of_commit_lib(repo, oid)?;
2798 }
2799 Ok(None)
2800}
2801
2802fn collect_visible_parent_for_graph_lib(
2803 repo: &Repository,
2804 candidate: ObjectId,
2805 included: &HashSet<ObjectId>,
2806 first_parent_only: bool,
2807 seen: &mut HashSet<ObjectId>,
2808 out: &mut Vec<ObjectId>,
2809) -> Result<()> {
2810 if !seen.insert(candidate) {
2811 return Ok(());
2812 }
2813 if included.contains(&candidate) {
2814 out.push(candidate);
2815 return Ok(());
2816 }
2817 let mut parents = load_raw_parents_lib(repo, candidate)?;
2818 if parents.is_empty() {
2819 return Ok(());
2820 }
2821 if parents.len() > 1 {
2822 parents.truncate(1);
2823 }
2824 for parent in parents {
2825 collect_visible_parent_for_graph_lib(repo, parent, included, first_parent_only, seen, out)?;
2826 }
2827 Ok(())
2828}
2829
2830fn visible_parents_for_graph_lib(
2831 repo: &Repository,
2832 oid: ObjectId,
2833 included: &HashSet<ObjectId>,
2834 first_parent_only: bool,
2835) -> Result<Vec<ObjectId>> {
2836 let mut direct = load_raw_parents_lib(repo, oid)?;
2837 if first_parent_only && direct.len() > 1 {
2838 direct.truncate(1);
2839 }
2840 let mut seen = HashSet::new();
2841 let mut out = Vec::new();
2842 for parent in direct {
2843 collect_visible_parent_for_graph_lib(
2844 repo,
2845 parent,
2846 included,
2847 first_parent_only,
2848 &mut seen,
2849 &mut out,
2850 )?;
2851 }
2852 let mut dedup = HashSet::new();
2853 out.retain(|parent| dedup.insert(*parent));
2854 Ok(out)
2855}
2856
2857fn reorder_path_limited_graph_commits(
2858 repo: &Repository,
2859 commits: &[ObjectId],
2860 first_parent_only: bool,
2861) -> Result<Vec<ObjectId>> {
2862 if commits.is_empty() {
2863 return Ok(Vec::new());
2864 }
2865
2866 let included: HashSet<ObjectId> = commits.iter().copied().collect();
2867 let mut chain = Vec::new();
2868 let mut chain_seen = HashSet::new();
2869 let mut cursor = Some(commits[0]);
2870 while let Some(oid) = cursor {
2871 if !included.contains(&oid) || !chain_seen.insert(oid) {
2872 break;
2873 }
2874 chain.push(oid);
2875 let visible = visible_parents_for_graph_lib(repo, oid, &included, first_parent_only)?;
2876 cursor = visible.first().copied();
2877 }
2878
2879 let chain_set: HashSet<ObjectId> = chain.iter().copied().collect();
2880 let mut grouped: HashMap<Option<ObjectId>, Vec<ObjectId>> = HashMap::new();
2881 for oid in commits {
2882 if chain_set.contains(oid) {
2883 continue;
2884 }
2885 let anchor = first_parent_anchor_in_set_lib(repo, *oid, &chain_set)?;
2886 grouped.entry(anchor).or_default().push(*oid);
2887 }
2888
2889 let mut ordered = Vec::new();
2890 for chain_oid in chain {
2891 if let Some(group) = grouped.remove(&Some(chain_oid)) {
2892 ordered.extend(group);
2893 }
2894 ordered.push(chain_oid);
2895 }
2896 if let Some(group) = grouped.remove(&None) {
2897 ordered.extend(group);
2898 }
2899 for (_anchor, group) in grouped {
2900 ordered.extend(group);
2901 }
2902 Ok(ordered)
2903}
2904
2905fn expand_sparse_path_limited_graph_history(
2906 repo: &Repository,
2907 commits: &[ObjectId],
2908) -> Result<Vec<ObjectId>> {
2909 if commits.is_empty() {
2910 return Ok(Vec::new());
2911 }
2912
2913 let mut expanded = Vec::new();
2914 let mut seen = HashSet::new();
2915 let mut push_unique = |oid: ObjectId, out: &mut Vec<ObjectId>| {
2916 if seen.insert(oid) {
2917 out.push(oid);
2918 }
2919 };
2920
2921 for window in commits.windows(2) {
2922 let from = window[0];
2923 let to = window[1];
2924 push_unique(from, &mut expanded);
2925
2926 let mut cursor = first_parent_of_commit_lib(repo, from)?;
2927 let mut chain = Vec::new();
2928 let mut found_target = false;
2929 let mut local_seen = HashSet::new();
2930 while let Some(oid) = cursor {
2931 if !local_seen.insert(oid) {
2932 break;
2933 }
2934 if oid == to {
2935 found_target = true;
2936 break;
2937 }
2938 chain.push(oid);
2939 cursor = first_parent_of_commit_lib(repo, oid)?;
2940 }
2941 if found_target {
2942 for oid in chain {
2943 push_unique(oid, &mut expanded);
2944 }
2945 }
2946 }
2947
2948 if let Some(&last) = commits.last() {
2949 push_unique(last, &mut expanded);
2950 let mut cursor = first_parent_of_commit_lib(repo, last)?;
2951 let mut tail_seen = HashSet::new();
2952 while let Some(oid) = cursor {
2953 if !tail_seen.insert(oid) {
2954 break;
2955 }
2956 push_unique(oid, &mut expanded);
2957 cursor = first_parent_of_commit_lib(repo, oid)?;
2958 }
2959 }
2960
2961 Ok(expanded)
2962}
2963
2964fn limit_to_ancestry(
2965 graph: &mut CommitGraph<'_>,
2966 selected: &mut HashSet<ObjectId>,
2967 bottoms: &[ObjectId],
2968) -> Result<()> {
2969 let mut keep = HashSet::new();
2970 for &bottom in bottoms {
2971 let ancestors = walk_closure(graph, &[bottom])?;
2972 keep.extend(
2973 ancestors
2974 .iter()
2975 .copied()
2976 .filter(|oid| selected.contains(oid)),
2977 );
2978
2979 for &candidate in selected.iter() {
2980 if candidate == bottom {
2981 keep.insert(candidate);
2982 continue;
2983 }
2984 let closure = walk_closure(graph, &[candidate])?;
2985 if closure.contains(&bottom) {
2986 keep.insert(candidate);
2987 }
2988 }
2989 }
2990 selected.retain(|oid| keep.contains(oid));
2991 Ok(())
2992}
2993
2994#[allow(clippy::too_many_arguments)]
3000fn commit_touches_paths(
3001 repo: &Repository,
3002 graph: &mut CommitGraph<'_>,
3003 oid: ObjectId,
3004 paths: &[String],
3005 full_history: bool,
3006 sparse: bool,
3007 simplify_merges: bool,
3008 show_pulls: bool,
3009 bloom_chain: Option<&CommitGraphChain>,
3010 read_changed_paths: bool,
3011 changed_paths_version: i32,
3012 bloom_stats: Option<&BloomWalkStatsHandle>,
3013 bloom_cwd: Option<&str>,
3014) -> Result<bool> {
3015 let commit = load_commit(repo, oid)?;
3016 let parents = graph.parents_of(oid)?;
3017 let commit_entries = flatten_tree(repo, commit.tree, "")?;
3018 let commit_map: HashMap<String, ObjectId> = commit_entries.into_iter().collect();
3019
3020 if parents.is_empty() {
3022 if sparse {
3023 return Ok(true);
3024 }
3025 let ctx = crate::pathspec::PathspecMatchContext {
3026 is_directory: false,
3027 is_git_submodule: false,
3028 };
3029 return Ok(commit_map
3030 .keys()
3031 .any(|path| crate::pathspec::matches_pathspec_list_with_context(path, paths, ctx)));
3032 }
3033
3034 if parents.len() == 1 {
3036 let mut bloom_ret = BloomPrecheck::Inapplicable;
3037 if let Some(chain) = bloom_chain {
3038 bloom_ret = chain.bloom_precheck_for_paths(
3039 &repo.odb,
3040 oid,
3041 paths,
3042 bloom_cwd,
3043 changed_paths_version,
3044 read_changed_paths,
3045 )?;
3046 if let Some(stats) = bloom_stats {
3047 if let Ok(mut g) = stats.lock() {
3048 g.record_precheck(bloom_ret);
3049 }
3050 }
3051 if bloom_ret == BloomPrecheck::DefinitelyNot {
3052 return Ok(false);
3053 }
3054 }
3055
3056 let parent = load_commit(repo, parents[0])?;
3057 let parent_map: HashMap<String, ObjectId> =
3058 flatten_tree(repo, parent.tree, "")?.into_iter().collect();
3059 let differs = path_differs_for_specs(&commit_map, &parent_map, paths);
3060 if bloom_ret == BloomPrecheck::Maybe && !differs {
3061 if let Some(stats) = bloom_stats {
3062 if let Ok(mut g) = stats.lock() {
3063 g.record_false_positive();
3064 }
3065 }
3066 }
3067 if differs {
3068 return Ok(true);
3069 }
3070 if sparse {
3071 return Ok(true);
3072 }
3073 return Ok(false);
3074 }
3075
3076 let mut treesame_parents = 0usize;
3078 let mut differs_any = false;
3079 let mut first_parent_differs = false;
3080 for (nth, parent_oid) in parents.iter().enumerate() {
3081 let parent = load_commit(repo, *parent_oid)?;
3082 let parent_map: HashMap<String, ObjectId> =
3083 flatten_tree(repo, parent.tree, "")?.into_iter().collect();
3084 let differs = path_differs_for_specs(&commit_map, &parent_map, paths);
3085 if nth == 0 {
3086 first_parent_differs = differs;
3087 }
3088 if differs {
3089 differs_any = true;
3090 } else {
3091 treesame_parents += 1;
3092 }
3093 }
3094
3095 if full_history && !simplify_merges && parents.len() > 1 && treesame_parents == parents.len() {
3098 return Ok(true);
3099 }
3100
3101 if !full_history && treesame_parents == 1 {
3102 return Ok(false);
3103 }
3104
3105 if full_history && simplify_merges {
3106 if treesame_parents == parents.len() {
3107 return Ok(sparse);
3108 }
3109 if treesame_parents > 0 && !differs_any {
3110 return Ok(sparse);
3111 }
3112 if show_pulls && first_parent_differs && treesame_parents > 0 {
3113 return Ok(true);
3114 }
3115 if treesame_parents == 1 {
3116 return Ok(false);
3117 }
3118 return Ok(differs_any || sparse);
3119 }
3120
3121 if differs_any {
3122 return Ok(true);
3123 }
3124
3125 Ok(sparse)
3126}
3127
3128pub fn commit_visible_for_dense_pathspecs(
3135 repo: &Repository,
3136 oid: ObjectId,
3137 paths: &[String],
3138) -> Result<bool> {
3139 if paths.is_empty() {
3140 return Ok(true);
3141 }
3142 let commit = load_commit(repo, oid)?;
3143 let parents = commit.parents.clone();
3144 let commit_entries = flatten_tree(repo, commit.tree, "")?;
3145 let commit_map: HashMap<String, ObjectId> = commit_entries.into_iter().collect();
3146
3147 if parents.is_empty() {
3148 return Ok(commit_map.keys().any(|path| {
3149 paths.iter().any(|spec| {
3150 crate::pathspec::matches_pathspec_with_context(
3151 spec,
3152 path,
3153 crate::pathspec::PathspecMatchContext {
3154 is_directory: false,
3155 is_git_submodule: false,
3156 },
3157 )
3158 })
3159 }));
3160 }
3161
3162 if parents.len() == 1 {
3163 let parent = load_commit(repo, parents[0])?;
3164 let parent_map: HashMap<String, ObjectId> =
3165 flatten_tree(repo, parent.tree, "")?.into_iter().collect();
3166 return Ok(path_differs_for_specs(&commit_map, &parent_map, paths));
3167 }
3168
3169 let mut treesame_parents = 0usize;
3170 let mut differs_any = false;
3171 for parent_oid in &parents {
3172 let parent = load_commit(repo, *parent_oid)?;
3173 let parent_map: HashMap<String, ObjectId> =
3174 flatten_tree(repo, parent.tree, "")?.into_iter().collect();
3175 let differs = path_differs_for_specs(&commit_map, &parent_map, paths);
3176 if differs {
3177 differs_any = true;
3178 } else {
3179 treesame_parents += 1;
3180 }
3181 }
3182
3183 if treesame_parents == 1 {
3184 return Ok(false);
3185 }
3186 if differs_any {
3187 return Ok(true);
3188 }
3189 Ok(false)
3190}
3191
3192fn path_differs_for_specs(
3193 current: &HashMap<String, ObjectId>,
3194 parent: &HashMap<String, ObjectId>,
3195 specs: &[String],
3196) -> bool {
3197 let mut paths = std::collections::BTreeSet::new();
3198 paths.extend(current.keys().cloned());
3199 paths.extend(parent.keys().cloned());
3200
3201 for path in &paths {
3202 if !crate::pathspec::matches_pathspec_list(path, specs) {
3203 continue;
3204 }
3205 if current.get(path) != parent.get(path) {
3206 return true;
3207 }
3208 }
3209 false
3210}
3211
3212fn load_commit(repo: &Repository, oid: ObjectId) -> Result<crate::objects::CommitData> {
3213 let object = repo.odb.read(&oid)?;
3214 if object.kind != ObjectKind::Commit {
3215 return Err(Error::CorruptObject(format!(
3216 "object {oid} is not a commit"
3217 )));
3218 }
3219 parse_commit(&object.data)
3220}
3221
3222fn extend_split_token(
3223 token: &str,
3224 not_mode: bool,
3225 positive: &mut Vec<String>,
3226 negative: &mut Vec<String>,
3227) {
3228 let (pos, neg) = split_revision_token(token);
3229 if not_mode {
3230 positive.extend(neg);
3231 negative.extend(pos);
3232 } else {
3233 positive.extend(pos);
3234 negative.extend(neg);
3235 }
3236}
3237
3238fn parse_long_opt_value(opt: &str, argv0: &str, argv1: Option<&str>) -> Option<(usize, String)> {
3243 let rest = argv0.strip_prefix("--")?;
3244 let rest = rest.strip_prefix(opt)?;
3245 if let Some(stripped) = rest.strip_prefix('=') {
3246 return Some((1, stripped.to_owned()));
3247 }
3248 if !rest.is_empty() {
3249 return None;
3250 }
3251 let Some(next) = argv1 else {
3252 return None;
3253 };
3254 Some((2, next.to_owned()))
3255}
3256
3257fn stdin_die_requires_value(opt: &str) -> Error {
3258 Error::Message(format!("fatal: Option '{opt}' requires a value"))
3259}
3260
3261fn apply_stdin_pseudo_opt(
3262 git_dir: &Path,
3263 line: &str,
3264 next_line: Option<&str>,
3265 not_mode: bool,
3266 positive: &mut Vec<String>,
3267 negative: &mut Vec<String>,
3268 stdin_all_refs: &mut bool,
3269) -> Result<Option<usize>> {
3270 if line == "--end-of-options" {
3271 return Ok(Some(1));
3272 }
3273 if line == "--all" {
3274 if not_mode {
3275 for (_, oid) in refs::list_refs(git_dir, "refs/")? {
3276 let s = oid.to_hex();
3277 negative.push(s);
3278 }
3279 if let Ok(head_oid) = refs::resolve_ref(git_dir, "HEAD") {
3280 negative.push(head_oid.to_hex());
3281 }
3282 } else {
3283 *stdin_all_refs = true;
3284 }
3285 return Ok(Some(1));
3286 }
3287 if line == "--not" {
3288 return Ok(Some(1));
3289 }
3290 if line == "--branches" {
3291 for (_, oid) in refs::list_refs(git_dir, "refs/heads/")? {
3292 let s = oid.to_hex();
3293 if not_mode {
3294 negative.push(s);
3295 } else {
3296 positive.push(s);
3297 }
3298 }
3299 return Ok(Some(1));
3300 }
3301 if line == "--tags" {
3302 for (_, oid) in refs::list_refs(git_dir, "refs/tags/")? {
3303 let s = oid.to_hex();
3304 if not_mode {
3305 negative.push(s);
3306 } else {
3307 positive.push(s);
3308 }
3309 }
3310 return Ok(Some(1));
3311 }
3312 if line == "--remotes" {
3313 for (_, oid) in refs::list_refs(git_dir, "refs/remotes/")? {
3314 let s = oid.to_hex();
3315 if not_mode {
3316 negative.push(s);
3317 } else {
3318 positive.push(s);
3319 }
3320 }
3321 return Ok(Some(1));
3322 }
3323 if let Some((consumed, pattern)) = parse_long_opt_value("branches", line, next_line) {
3324 let full_pattern = format!("refs/heads/{pattern}");
3325 for (_, oid) in refs::list_refs_glob(git_dir, &full_pattern)? {
3326 let s = oid.to_hex();
3327 if not_mode {
3328 negative.push(s);
3329 } else {
3330 positive.push(s);
3331 }
3332 }
3333 return Ok(Some(consumed));
3334 }
3335 if let Some((1, pattern)) = parse_long_opt_value("tags", line, None) {
3336 let full_pattern = format!("refs/tags/{pattern}");
3337 for (_, oid) in refs::list_refs_glob(git_dir, &full_pattern)? {
3338 let s = oid.to_hex();
3339 if not_mode {
3340 negative.push(s);
3341 } else {
3342 positive.push(s);
3343 }
3344 }
3345 return Ok(Some(1));
3346 }
3347 if let Some((consumed, pattern)) = parse_long_opt_value("tags", line, next_line) {
3348 let full_pattern = format!("refs/tags/{pattern}");
3349 for (_, oid) in refs::list_refs_glob(git_dir, &full_pattern)? {
3350 let s = oid.to_hex();
3351 if not_mode {
3352 negative.push(s);
3353 } else {
3354 positive.push(s);
3355 }
3356 }
3357 return Ok(Some(consumed));
3358 }
3359 if let Some((1, pattern)) = parse_long_opt_value("remotes", line, None) {
3360 let full_pattern = format!("refs/remotes/{pattern}");
3361 for (_, oid) in refs::list_refs_glob(git_dir, &full_pattern)? {
3362 let s = oid.to_hex();
3363 if not_mode {
3364 negative.push(s);
3365 } else {
3366 positive.push(s);
3367 }
3368 }
3369 return Ok(Some(1));
3370 }
3371 if let Some((consumed, pattern)) = parse_long_opt_value("remotes", line, next_line) {
3372 let full_pattern = format!("refs/remotes/{pattern}");
3373 for (_, oid) in refs::list_refs_glob(git_dir, &full_pattern)? {
3374 let s = oid.to_hex();
3375 if not_mode {
3376 negative.push(s);
3377 } else {
3378 positive.push(s);
3379 }
3380 }
3381 return Ok(Some(consumed));
3382 }
3383 if line == "--glob" {
3384 return Err(stdin_die_requires_value("--glob"));
3385 }
3386 if let Some((1, pattern)) = parse_long_opt_value("glob", line, None) {
3387 for (_, oid) in refs::list_refs_glob(git_dir, &pattern)? {
3388 let s = oid.to_hex();
3389 if not_mode {
3390 negative.push(s);
3391 } else {
3392 positive.push(s);
3393 }
3394 }
3395 return Ok(Some(1));
3396 }
3397 if let Some((consumed, pattern)) = parse_long_opt_value("glob", line, next_line) {
3398 for (_, oid) in refs::list_refs_glob(git_dir, &pattern)? {
3399 let s = oid.to_hex();
3400 if not_mode {
3401 negative.push(s);
3402 } else {
3403 positive.push(s);
3404 }
3405 }
3406 return Ok(Some(consumed));
3407 }
3408 if line == "--no-walk" || line.starts_with("--no-walk=") {
3409 if let Some(rest) = line.strip_prefix("--no-walk=") {
3410 if rest == "sorted" || rest == "unsorted" {
3411 return Ok(Some(1));
3412 }
3413 eprintln!("error: invalid argument to --no-walk");
3414 return Err(Error::Message(format!(
3415 "fatal: invalid option '{line}' in --stdin mode"
3416 )));
3417 }
3418 return Ok(Some(1));
3419 }
3420 if line.starts_with("--") {
3421 return Err(Error::Message(format!(
3422 "fatal: invalid option '{line}' in --stdin mode"
3423 )));
3424 }
3425 if line.starts_with('-') {
3426 return Err(Error::Message(format!(
3427 "fatal: invalid option '{line}' in --stdin mode"
3428 )));
3429 }
3430 Ok(None)
3431}
3432
3433fn read_revisions_from_stdin_lines(
3435 git_dir: &Path,
3436) -> Result<(Vec<String>, Vec<String>, bool, Vec<String>)> {
3437 let stdin = std::io::read_to_string(std::io::stdin()).map_err(Error::Io)?;
3438 let lines: Vec<String> = stdin.lines().map(std::borrow::ToOwned::to_owned).collect();
3439
3440 let mut positive = Vec::new();
3441 let mut negative = Vec::new();
3442 let mut stdin_all_refs = false;
3443 let mut stdin_not_mode = false;
3444 let mut seen_end_of_options = false;
3445 let mut i = 0usize;
3446
3447 while i < lines.len() {
3448 let line = lines[i].as_str();
3449 if line.is_empty() {
3450 break;
3451 }
3452 if line == "--" {
3453 i += 1;
3454 let paths: Vec<String> = lines[i..].to_vec();
3455 return Ok((positive, negative, stdin_all_refs, paths));
3456 }
3457
3458 if !seen_end_of_options && line.starts_with('-') {
3459 if line == "--end-of-options" {
3460 seen_end_of_options = true;
3461 i += 1;
3462 continue;
3463 }
3464 let next = lines.get(i + 1).map(|s| s.as_str());
3465 match apply_stdin_pseudo_opt(
3466 git_dir,
3467 line,
3468 next,
3469 stdin_not_mode,
3470 &mut positive,
3471 &mut negative,
3472 &mut stdin_all_refs,
3473 )? {
3474 Some(consumed) => {
3475 if line == "--not" && consumed == 1 {
3476 stdin_not_mode = !stdin_not_mode;
3477 }
3478 i += consumed;
3479 }
3480 None => {
3481 extend_split_token(line, stdin_not_mode, &mut positive, &mut negative);
3482 i += 1;
3483 }
3484 }
3485 continue;
3486 }
3487
3488 extend_split_token(line, stdin_not_mode, &mut positive, &mut negative);
3489 i += 1;
3490 }
3491
3492 Ok((positive, negative, stdin_all_refs, Vec::new()))
3493}
3494
3495pub fn collect_revision_specs_with_stdin(
3506 git_dir: &Path,
3507 args_specs: &[String],
3508 read_stdin: bool,
3509) -> Result<(Vec<String>, Vec<String>, bool, Vec<String>)> {
3510 let mut positive = Vec::new();
3511 let mut negative = Vec::new();
3512
3513 for spec in args_specs {
3514 let (pos, neg) = split_revision_token(spec);
3515 positive.extend(pos);
3516 negative.extend(neg);
3517 }
3518
3519 if !read_stdin {
3520 return Ok((positive, negative, false, Vec::new()));
3521 }
3522
3523 let (s_pos, s_neg, stdin_all_refs, stdin_paths) = read_revisions_from_stdin_lines(git_dir)?;
3524 positive.extend(s_pos);
3525 negative.extend(s_neg);
3526
3527 Ok((positive, negative, stdin_all_refs, stdin_paths))
3528}
3529
3530pub fn tag_targets(git_dir: &Path) -> Result<HashSet<ObjectId>> {
3532 Ok(refs::list_refs(git_dir, "refs/tags/")?
3533 .into_iter()
3534 .map(|(_, oid)| oid)
3535 .collect())
3536}
3537
3538pub(crate) struct CommitGraph<'r> {
3539 repo: &'r Repository,
3540 first_parent_only: bool,
3541 parents: HashMap<ObjectId, Vec<ObjectId>>,
3542 committer_time: HashMap<ObjectId, i64>,
3543 author_time: HashMap<ObjectId, i64>,
3544 shallow_boundaries: HashSet<ObjectId>,
3545 graft_parents: HashMap<ObjectId, Vec<ObjectId>>,
3546}
3547
3548impl<'r> CommitGraph<'r> {
3549 pub(crate) fn new(repo: &'r Repository, first_parent_only: bool) -> Self {
3550 let shallow_boundaries = load_shallow_boundaries(&repo.git_dir);
3551 let graft_parents = load_graft_parents(&repo.git_dir);
3552 Self {
3553 repo,
3554 first_parent_only,
3555 parents: HashMap::new(),
3556 committer_time: HashMap::new(),
3557 author_time: HashMap::new(),
3558 shallow_boundaries,
3559 graft_parents,
3560 }
3561 }
3562
3563 pub(crate) fn parents_of(&mut self, oid: ObjectId) -> Result<Vec<ObjectId>> {
3564 self.populate(oid)?;
3565 Ok(self.parents.get(&oid).cloned().unwrap_or_default())
3566 }
3567
3568 fn committer_time(&mut self, oid: ObjectId) -> i64 {
3569 if self.populate(oid).is_err() {
3570 return 0;
3571 }
3572 self.committer_time.get(&oid).copied().unwrap_or(0)
3573 }
3574
3575 fn author_time(&mut self, oid: ObjectId) -> i64 {
3576 if self.populate(oid).is_err() {
3577 return 0;
3578 }
3579 self.author_time.get(&oid).copied().unwrap_or(0)
3580 }
3581
3582 fn sort_key(&mut self, oid: ObjectId, author: bool) -> i64 {
3583 if author {
3584 self.author_time(oid)
3585 } else {
3586 self.committer_time(oid)
3587 }
3588 }
3589
3590 fn populate(&mut self, oid: ObjectId) -> Result<()> {
3591 if self.parents.contains_key(&oid) {
3592 return Ok(());
3593 }
3594 let commit = load_commit(self.repo, oid)?;
3595 let mut parents = if self.shallow_boundaries.contains(&oid) {
3597 Vec::new()
3598 } else {
3599 commit.parents
3600 };
3601 if let Some(graft_parents) = self.graft_parents.get(&oid) {
3602 parents = graft_parents.clone();
3603 }
3604 if self.first_parent_only && parents.len() > 1 {
3605 parents.truncate(1);
3606 }
3607 self.committer_time
3608 .insert(oid, committer_unix_seconds_for_ordering(&commit.committer));
3609 self.author_time
3610 .insert(oid, committer_unix_seconds_for_ordering(&commit.author));
3611 self.parents.insert(oid, parents);
3612 Ok(())
3613 }
3614}
3615
3616fn load_shallow_boundaries(git_dir: &Path) -> HashSet<ObjectId> {
3618 let shallow_path = git_dir.join("shallow");
3619 let mut set = HashSet::new();
3620 if let Ok(contents) = fs::read_to_string(&shallow_path) {
3621 for line in contents.lines() {
3622 let line = line.trim();
3623 if !line.is_empty() {
3624 if let Ok(oid) = line.parse::<ObjectId>() {
3625 set.insert(oid);
3626 }
3627 }
3628 }
3629 }
3630 set
3631}
3632
3633fn load_graft_parents(git_dir: &Path) -> HashMap<ObjectId, Vec<ObjectId>> {
3635 let graft_path = git_dir.join("info/grafts");
3636 let mut grafts = HashMap::new();
3637 let Ok(contents) = fs::read_to_string(&graft_path) else {
3638 return grafts;
3639 };
3640 for raw_line in contents.lines() {
3641 let line = raw_line.trim();
3642 if line.is_empty() || line.starts_with('#') {
3643 continue;
3644 }
3645 let mut fields = line.split_whitespace();
3646 let Some(commit_hex) = fields.next() else {
3647 continue;
3648 };
3649 let Ok(commit_oid) = commit_hex.parse::<ObjectId>() else {
3650 continue;
3651 };
3652 let mut parents = Vec::new();
3653 let mut valid = true;
3654 for parent_hex in fields {
3655 match parent_hex.parse::<ObjectId>() {
3656 Ok(parent_oid) => parents.push(parent_oid),
3657 Err(_) => {
3658 valid = false;
3659 break;
3660 }
3661 }
3662 }
3663 if valid {
3664 grafts.insert(commit_oid, parents);
3665 }
3666 }
3667 grafts
3668}
3669
3670fn commit_tips_from_ref_pairs(
3671 repo: &Repository,
3672 pairs: &[(String, ObjectId)],
3673 exclusions: &RefExclusions,
3674) -> Result<Vec<ObjectId>> {
3675 let namespace_prefix = git_namespace_prefix();
3676 let mut raw = Vec::new();
3677 for (refname, oid) in pairs {
3678 if exclusions.ref_excluded(strip_git_namespace(refname, &namespace_prefix), refname) {
3679 continue;
3680 }
3681 raw.push(*oid);
3682 }
3683 peel_ref_oids_to_unique_commits(repo, raw)
3684}
3685
3686fn peel_ref_oids_to_unique_commits(repo: &Repository, raw: Vec<ObjectId>) -> Result<Vec<ObjectId>> {
3687 let mut out = Vec::new();
3688 let mut seen = HashSet::new();
3689 for oid in raw {
3690 match peel_to_commit(repo, oid) {
3691 Ok(commit_oid) if seen.insert(commit_oid) => out.push(commit_oid),
3692 Err(_) => {}
3693 _ => {}
3694 }
3695 }
3696 out.sort();
3697 Ok(out)
3698}
3699
3700fn all_ref_tips(repo: &Repository, exclusions: &RefExclusions) -> Result<Vec<ObjectId>> {
3701 let mut pairs = Vec::new();
3702 if let Ok(head) = refs::resolve_ref(&repo.git_dir, "HEAD") {
3703 pairs.push(("HEAD".to_owned(), head));
3704 }
3705 pairs.extend(refs::list_refs(&repo.git_dir, "refs/")?);
3706 commit_tips_from_ref_pairs(repo, &pairs, exclusions)
3707}
3708
3709pub fn commit_tips_from_named_refs(
3711 repo: &Repository,
3712 pairs: &[(String, ObjectId)],
3713 exclusions: &RefExclusions,
3714) -> Result<Vec<ObjectId>> {
3715 commit_tips_from_ref_pairs(repo, pairs, exclusions)
3716}
3717
3718#[must_use]
3723pub fn shallow_boundary_oids(git_dir: &Path) -> HashSet<ObjectId> {
3724 crate::shallow::load_shallow_boundaries(git_dir)
3725}
3726
3727#[must_use]
3733pub fn shallow_borders_reachable_from_wants(
3734 repo: &Repository,
3735 wants: &[ObjectId],
3736) -> Vec<ObjectId> {
3737 let boundaries = shallow_boundary_oids(&repo.git_dir);
3738 if boundaries.is_empty() || wants.is_empty() {
3739 return Vec::new();
3740 }
3741 let mut out: Vec<ObjectId> = Vec::new();
3742 let mut seen_out = HashSet::new();
3743 let mut visited = HashSet::new();
3744 let mut q: VecDeque<ObjectId> = wants.iter().copied().collect();
3745 while let Some(oid) = q.pop_front() {
3746 if !visited.insert(oid) {
3747 continue;
3748 }
3749 if boundaries.contains(&oid) {
3750 if seen_out.insert(oid) {
3751 out.push(oid);
3752 }
3753 continue;
3754 }
3755 for p in commit_parent_ids(repo, oid) {
3756 q.push_back(p);
3757 }
3758 }
3759 out.sort();
3760 out
3761}
3762
3763#[must_use]
3776pub fn shallow_grafts_for_upload_pack_deepen(
3777 repo: &Repository,
3778 wants: &[ObjectId],
3779 client_shallow: &[ObjectId],
3780 deepen: usize,
3781) -> Vec<ObjectId> {
3782 if deepen == 0 || wants.is_empty() {
3783 return Vec::new();
3784 }
3785
3786 let server_shallow = shallow_boundary_oids(&repo.git_dir);
3787 let client_set: HashSet<ObjectId> = client_shallow.iter().copied().collect();
3788
3789 let min_hit = min_client_shallow_distance(repo, wants, &client_set, &server_shallow);
3790 let base = min_hit.unwrap_or(1);
3791 let target_depth = base.saturating_add(deepen);
3792
3793 let included = commits_within_parent_depth(repo, wants, target_depth, &server_shallow);
3794 border_commits_not_in_client_shallow(repo, &included, &client_set)
3795}
3796
3797fn commit_parent_ids(repo: &Repository, oid: ObjectId) -> Vec<ObjectId> {
3798 let Ok(obj) = repo.odb.read(&oid) else {
3799 return Vec::new();
3800 };
3801 if obj.kind != ObjectKind::Commit {
3802 return Vec::new();
3803 }
3804 parse_commit(&obj.data)
3805 .map(|c| c.parents)
3806 .unwrap_or_default()
3807}
3808
3809fn min_client_shallow_distance(
3810 repo: &Repository,
3811 wants: &[ObjectId],
3812 client_shallow: &HashSet<ObjectId>,
3813 server_shallow: &HashSet<ObjectId>,
3814) -> Option<usize> {
3815 if client_shallow.is_empty() {
3816 return None;
3817 }
3818 let mut best: Option<usize> = None;
3819 let mut dist: HashMap<ObjectId, usize> = HashMap::new();
3820 let mut q: VecDeque<(ObjectId, usize)> = VecDeque::new();
3821 for &w in wants {
3822 dist.insert(w, 0);
3823 q.push_back((w, 0));
3824 }
3825 while let Some((oid, d)) = q.pop_front() {
3826 if client_shallow.contains(&oid) {
3827 best = Some(best.map(|b| b.min(d)).unwrap_or(d));
3828 }
3829 if server_shallow.contains(&oid) {
3830 continue;
3831 }
3832 for p in commit_parent_ids(repo, oid) {
3833 let nd = d.saturating_add(1);
3834 let prev = dist.get(&p).copied().unwrap_or(usize::MAX);
3835 if nd < prev {
3836 dist.insert(p, nd);
3837 q.push_back((p, nd));
3838 }
3839 }
3840 }
3841 best
3842}
3843
3844fn commits_within_parent_depth(
3845 repo: &Repository,
3846 wants: &[ObjectId],
3847 max_depth: usize,
3848 server_shallow: &HashSet<ObjectId>,
3849) -> HashSet<ObjectId> {
3850 let mut best_depth: HashMap<ObjectId, usize> = HashMap::new();
3851 let mut q: VecDeque<(ObjectId, usize)> = VecDeque::new();
3852 for &w in wants {
3853 best_depth.insert(w, 1);
3854 q.push_back((w, 1));
3855 }
3856 while let Some((oid, depth)) = q.pop_front() {
3857 if depth > max_depth {
3858 continue;
3859 }
3860 if best_depth.get(&oid).copied() != Some(depth) {
3861 continue;
3862 }
3863 if depth == max_depth {
3864 continue;
3865 }
3866 if server_shallow.contains(&oid) {
3867 continue;
3868 }
3869 for p in commit_parent_ids(repo, oid) {
3870 let nd = depth.saturating_add(1);
3871 if nd > max_depth {
3872 continue;
3873 }
3874 let prev = best_depth.get(&p).copied().unwrap_or(usize::MAX);
3875 if nd < prev {
3876 best_depth.insert(p, nd);
3877 q.push_back((p, nd));
3878 }
3879 }
3880 }
3881 best_depth.into_keys().collect()
3882}
3883
3884fn border_commits_not_in_client_shallow(
3885 repo: &Repository,
3886 included: &HashSet<ObjectId>,
3887 client_shallow: &HashSet<ObjectId>,
3888) -> Vec<ObjectId> {
3889 let mut out = Vec::new();
3890 let mut seen_out = HashSet::new();
3891 for &c in included {
3892 if client_shallow.contains(&c) {
3893 continue;
3894 }
3895 let parents = commit_parent_ids(repo, c);
3896 let is_border = parents.iter().any(|p| !included.contains(p));
3897 if is_border && seen_out.insert(c) {
3898 out.push(c);
3899 }
3900 }
3901 out.sort();
3902 out
3903}
3904
3905pub fn shallow_grafts_for_upload_pack_rev_list(
3910 repo: &Repository,
3911 wants: &[ObjectId],
3912 client_shallow: &[ObjectId],
3913 deepen_since: Option<i64>,
3914 deepen_not: &[ObjectId],
3915) -> Result<Vec<ObjectId>> {
3916 if wants.is_empty() || (deepen_since.is_none() && deepen_not.is_empty()) {
3917 return Ok(Vec::new());
3918 }
3919
3920 let positive: Vec<String> = wants.iter().map(|o| o.to_hex()).collect();
3921 let negative: Vec<String> = deepen_not
3922 .iter()
3923 .map(|o| format!("^{}", o.to_hex()))
3924 .collect();
3925
3926 let options = RevListOptions {
3927 since_cutoff: deepen_since,
3928 missing_action: MissingAction::Allow,
3929 ..Default::default()
3930 };
3931
3932 let res = rev_list(repo, &positive, &negative, &options)?;
3933 let included: HashSet<ObjectId> = res.commits.iter().copied().collect();
3934 let client_set: HashSet<ObjectId> = client_shallow.iter().copied().collect();
3935 Ok(border_commits_not_in_client_shallow(
3936 repo,
3937 &included,
3938 &client_set,
3939 ))
3940}
3941
3942#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3943struct CommitDateKey {
3944 oid: ObjectId,
3945 date: i64,
3946}
3947
3948impl Ord for CommitDateKey {
3949 fn cmp(&self, other: &Self) -> Ordering {
3950 match self.date.cmp(&other.date) {
3951 Ordering::Equal => self.oid.cmp(&other.oid),
3952 ord => ord,
3953 }
3954 }
3955}
3956
3957impl PartialOrd for CommitDateKey {
3958 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
3959 Some(self.cmp(other))
3960 }
3961}
3962
3963pub fn read_lines(path: &Path) -> Result<Vec<String>> {
3969 let content = fs::read_to_string(path)?;
3970 Ok(content.lines().map(|line| line.to_owned()).collect())
3971}
3972
3973#[must_use]
3975pub fn is_symmetric_diff(token: &str) -> bool {
3976 token.contains("...") && !token.contains("....")
3977}
3978
3979#[must_use]
3981pub fn split_symmetric_diff(token: &str) -> Option<(String, String)> {
3982 token
3983 .split_once("...")
3984 .map(|(l, r)| (l.to_owned(), r.to_owned()))
3985}
3986
3987#[derive(Debug, Default)]
3990struct TreeWalkState {
3991 seen_at_depth: HashMap<ObjectId, u64>,
3992}
3993
3994impl TreeWalkState {
3995 fn new() -> Self {
3996 Self::default()
3997 }
3998
3999 fn should_skip_tree(&mut self, oid: ObjectId, depth: u64) -> bool {
4002 match self.seen_at_depth.get(&oid).copied() {
4003 None => {
4004 self.seen_at_depth.insert(oid, depth);
4005 false
4006 }
4007 Some(prev) if depth >= prev => true,
4008 Some(_) => {
4009 self.seen_at_depth.insert(oid, depth);
4010 false
4011 }
4012 }
4013 }
4014}
4015
4016fn collect_tree_closure_objects(
4018 repo: &Repository,
4019 tree_oid: ObjectId,
4020 into: &mut HashSet<ObjectId>,
4021 missing_action: MissingAction,
4022 missing: &mut Vec<ObjectId>,
4023 missing_seen: &mut HashSet<ObjectId>,
4024) -> Result<()> {
4025 let mut stack = vec![tree_oid];
4026 let mut expanded_trees = HashSet::new();
4027 while let Some(t) = stack.pop() {
4028 if !expanded_trees.insert(t) {
4029 continue;
4030 }
4031 into.insert(t);
4032 let object = match repo.odb.read(&t) {
4033 Ok(o) => o,
4034 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
4035 if missing_action == MissingAction::Print && missing_seen.insert(t) {
4036 missing.push(t);
4037 }
4038 continue;
4039 }
4040 Err(e) => return Err(e),
4041 };
4042 if object.kind != ObjectKind::Tree {
4043 continue;
4044 }
4045 let entries = parse_tree(&object.data)?;
4046 for entry in entries {
4047 if entry.mode == 0o160000 {
4048 continue;
4049 }
4050 into.insert(entry.oid);
4051 if entry.mode == 0o040000 {
4052 stack.push(entry.oid);
4053 }
4054 }
4055 }
4056 Ok(())
4057}
4058
4059fn union_parent_reachable_objects(
4060 repo: &Repository,
4061 parents: &[ObjectId],
4062 missing_action: MissingAction,
4063 missing: &mut Vec<ObjectId>,
4064 missing_seen: &mut HashSet<ObjectId>,
4065) -> Result<HashSet<ObjectId>> {
4066 let mut out = HashSet::new();
4067 for &p in parents {
4068 let commit = match load_commit(repo, p) {
4069 Ok(c) => c,
4070 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
4071 if missing_action == MissingAction::Print && missing_seen.insert(p) {
4072 missing.push(p);
4073 }
4074 continue;
4075 }
4076 Err(e) => return Err(e),
4077 };
4078 collect_tree_closure_objects(
4079 repo,
4080 commit.tree,
4081 &mut out,
4082 missing_action,
4083 missing,
4084 missing_seen,
4085 )?;
4086 }
4087 Ok(out)
4088}
4089
4090#[allow(dead_code)]
4093fn collect_reachable_objects(
4094 repo: &Repository,
4095 graph: &mut CommitGraph<'_>,
4096 commits: &[ObjectId],
4097 object_roots: &[RootObject],
4098 tip_annotated_tags: &HashMap<ObjectId, ObjectId>,
4099 filter: Option<&ObjectFilter>,
4100 filter_provided: bool,
4101 missing_action: MissingAction,
4102 sparse_lines: Option<&[String]>,
4103 skip_trees_for_type_filter: bool,
4104 omit_object_paths: bool,
4105 packed_set: Option<&HashSet<ObjectId>>,
4106 collect_tree_omits: bool,
4107) -> Result<(Vec<(ObjectId, String)>, Vec<ObjectId>, Vec<ObjectId>)> {
4108 let mut tree_state = TreeWalkState::new();
4109 let mut top_tree_omit =
4110 walk_needs_top_tree_omit_set(filter, collect_tree_omits).then(HashSet::<ObjectId>::new);
4111 let mut combine_states = CombineSubState::prepare_sub_states(filter, collect_tree_omits);
4112 let mut emitted = HashSet::new();
4113 let mut result = Vec::new();
4114 let mut omitted = Vec::new();
4115 let mut missing = Vec::new();
4116 let mut missing_seen = HashSet::new();
4117 for &commit_oid in commits {
4118 let commit = match load_commit(repo, commit_oid) {
4119 Ok(commit) => commit,
4120 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
4121 if missing_seen.insert(commit_oid) && missing_action == MissingAction::Print {
4122 missing.push(commit_oid);
4123 }
4124 continue;
4125 }
4126 Err(err) => return Err(err),
4127 };
4128 let parents = graph.parents_of(commit_oid)?;
4129 let parent_union = union_parent_reachable_objects(
4130 repo,
4131 &parents,
4132 missing_action,
4133 &mut missing,
4134 &mut missing_seen,
4135 )?;
4136 if let Some(&tag_oid) = tip_annotated_tags.get(&commit_oid) {
4137 if emitted.insert(tag_oid) {
4138 result.push((tag_oid, "tag".to_owned()));
4139 }
4140 }
4141 collect_tree_objects_filtered(
4142 repo,
4143 commit.tree,
4144 "",
4145 0,
4146 false,
4147 Some(&parent_union),
4148 &mut tree_state,
4149 &mut emitted,
4150 &mut result,
4151 &mut omitted,
4152 &mut missing,
4153 &mut missing_seen,
4154 filter,
4155 filter_provided,
4156 missing_action,
4157 sparse_lines,
4158 skip_trees_for_type_filter,
4159 omit_object_paths,
4160 packed_set,
4161 collect_tree_omits,
4162 &mut top_tree_omit,
4163 &mut combine_states,
4164 )?;
4165 }
4166
4167 for root in object_roots {
4168 collect_root_object(
4169 repo,
4170 root,
4171 &mut tree_state,
4172 &mut emitted,
4173 &mut result,
4174 &mut omitted,
4175 &mut missing,
4176 &mut missing_seen,
4177 filter,
4178 filter_provided,
4179 missing_action,
4180 sparse_lines,
4181 skip_trees_for_type_filter,
4182 omit_object_paths,
4183 packed_set,
4184 collect_tree_omits,
4185 &mut top_tree_omit,
4186 &mut combine_states,
4187 )?;
4188 }
4189
4190 Ok((result, omitted, missing))
4191}
4192
4193fn collect_reachable_objects_segmented(
4199 repo: &Repository,
4200 _graph: &mut CommitGraph<'_>,
4201 commits: &[ObjectId],
4202 object_roots: &[RootObject],
4203 tip_annotated_tags: &HashMap<ObjectId, ObjectId>,
4204 filter: Option<&ObjectFilter>,
4205 filter_provided: bool,
4206 missing_action: MissingAction,
4207 sparse_lines: Option<&[String]>,
4208 skip_trees_for_type_filter: bool,
4209 omit_object_paths: bool,
4210 packed_set: Option<&HashSet<ObjectId>>,
4211 collect_tree_omits: bool,
4212) -> Result<(
4213 Vec<(ObjectId, String)>,
4214 Vec<ObjectId>,
4215 Vec<ObjectId>,
4216 Vec<Vec<(ObjectId, String)>>,
4217)> {
4218 let mut emitted = HashSet::new();
4219 let mut result = Vec::new();
4220 let mut omitted = Vec::new();
4221 let mut missing = Vec::new();
4222 let mut missing_seen = HashSet::new();
4223 let mut segments: Vec<Vec<(ObjectId, String)>> = Vec::with_capacity(commits.len() + 1);
4224 let mut tree_state = TreeWalkState::new();
4225 let mut top_tree_omit =
4226 walk_needs_top_tree_omit_set(filter, collect_tree_omits).then(HashSet::<ObjectId>::new);
4227 let mut combine_states = CombineSubState::prepare_sub_states(filter, collect_tree_omits);
4228
4229 for &commit_oid in commits {
4230 let start = result.len();
4231 let commit = match load_commit(repo, commit_oid) {
4232 Ok(commit) => commit,
4233 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
4234 if missing_action == MissingAction::Print && missing_seen.insert(commit_oid) {
4235 missing.push(commit_oid);
4236 }
4237 segments.push(Vec::new());
4238 continue;
4239 }
4240 Err(err) => return Err(err),
4241 };
4242 if let Some(&tag_oid) = tip_annotated_tags.get(&commit_oid) {
4243 if emitted.insert(tag_oid) {
4244 result.push((tag_oid, "tag".to_owned()));
4245 }
4246 }
4247 collect_tree_objects_filtered(
4250 repo,
4251 commit.tree,
4252 "",
4253 0,
4254 false,
4255 None,
4256 &mut tree_state,
4257 &mut emitted,
4258 &mut result,
4259 &mut omitted,
4260 &mut missing,
4261 &mut missing_seen,
4262 filter,
4263 filter_provided,
4264 missing_action,
4265 sparse_lines,
4266 skip_trees_for_type_filter,
4267 omit_object_paths,
4268 packed_set,
4269 collect_tree_omits,
4270 &mut top_tree_omit,
4271 &mut combine_states,
4272 )?;
4273 segments.push(result[start..].to_vec());
4274 }
4275
4276 let roots_start = result.len();
4277 for root in object_roots {
4278 collect_root_object(
4279 repo,
4280 root,
4281 &mut tree_state,
4282 &mut emitted,
4283 &mut result,
4284 &mut omitted,
4285 &mut missing,
4286 &mut missing_seen,
4287 filter,
4288 filter_provided,
4289 missing_action,
4290 sparse_lines,
4291 skip_trees_for_type_filter,
4292 omit_object_paths,
4293 packed_set,
4294 collect_tree_omits,
4295 &mut top_tree_omit,
4296 &mut combine_states,
4297 )?;
4298 }
4299 segments.push(result[roots_start..].to_vec());
4300
4301 Ok((result, omitted, missing, segments))
4302}
4303
4304#[derive(Clone, Copy, Debug)]
4305struct ListFilterBits {
4306 mark_seen: bool,
4307 do_show: bool,
4308 skip_tree: bool,
4309}
4310
4311impl ListFilterBits {
4312 fn merge_combine(subs: &[ListFilterBits], sub_skipping: &[bool]) -> Self {
4313 let mut out = ListFilterBits {
4314 mark_seen: true,
4315 do_show: true,
4316 skip_tree: true,
4317 };
4318 for (sub, skipping) in subs.iter().zip(sub_skipping.iter()) {
4319 if !sub.do_show {
4320 out.do_show = false;
4321 }
4322 if !sub.mark_seen {
4323 out.mark_seen = false;
4324 }
4325 if !skipping {
4326 out.skip_tree = false;
4327 }
4328 }
4329 out
4330 }
4331}
4332
4333fn walk_needs_top_tree_omit_set(filter: Option<&ObjectFilter>, collect_omits: bool) -> bool {
4334 collect_omits && matches!(filter, Some(ObjectFilter::TreeDepth(_)))
4335}
4336
4337fn trace_skip_tree_contents(prefix: &str) {
4338 let Ok(trace_val) = std::env::var("GIT_TRACE") else {
4339 return;
4340 };
4341 if trace_val.is_empty() || trace_val == "0" || trace_val.eq_ignore_ascii_case("false") {
4342 return;
4343 }
4344 let path = if prefix.is_empty() {
4345 String::new()
4346 } else {
4347 format!("{prefix}/")
4348 };
4349 let line = format!("Skipping contents of tree {path}...\n");
4350 match trace_val.as_str() {
4351 "1" | "true" | "2" => {
4352 let _ = std::io::stderr().write_all(line.as_bytes());
4353 }
4354 path_dest => {
4355 if let Ok(mut f) = std::fs::OpenOptions::new()
4356 .create(true)
4357 .append(true)
4358 .open(path_dest)
4359 {
4360 let _ = f.write_all(line.as_bytes());
4361 }
4362 }
4363 }
4364}
4365
4366fn tree_depth_begin_tree(
4367 tree_oid: ObjectId,
4368 depth: u64,
4369 exclude_depth: u64,
4370 tree_state: &mut TreeWalkState,
4371 tree_omit_set: &mut Option<HashSet<ObjectId>>,
4372 collect_omits: bool,
4373) -> ListFilterBits {
4374 let include_it = depth < exclude_depth;
4375 let already_seen = tree_state.should_skip_tree(tree_oid, depth);
4376 if already_seen {
4377 return ListFilterBits {
4378 mark_seen: false,
4379 do_show: false,
4380 skip_tree: true,
4381 };
4382 }
4383
4384 let been_omitted = if collect_omits {
4385 if let Some(omits) = tree_omit_set.as_mut() {
4386 if include_it {
4387 omits.remove(&tree_oid);
4388 false
4389 } else {
4390 !omits.insert(tree_oid)
4391 }
4392 } else {
4393 false
4394 }
4395 } else {
4396 false
4397 };
4398
4399 let skip_tree = if include_it {
4400 false
4401 } else {
4402 !(collect_omits && !been_omitted)
4403 };
4404
4405 let do_show = include_it;
4406 ListFilterBits {
4407 mark_seen: true,
4408 do_show,
4409 skip_tree,
4410 }
4411}
4412
4413fn tree_depth_blob(
4414 oid: ObjectId,
4415 _size: u64,
4416 parent_depth: u64,
4417 exclude_depth: u64,
4418 tree_omit_set: &mut Option<HashSet<ObjectId>>,
4419 collect_omits: bool,
4420) -> ListFilterBits {
4421 let include_it = parent_depth.saturating_add(1) < exclude_depth;
4422 if collect_omits {
4423 if let Some(omits) = tree_omit_set.as_mut() {
4424 if include_it {
4425 omits.remove(&oid);
4426 } else {
4427 omits.insert(oid);
4428 }
4429 }
4430 }
4431 ListFilterBits {
4432 mark_seen: include_it,
4434 do_show: include_it,
4435 skip_tree: false,
4436 }
4437}
4438
4439fn filter_object_bits_tree_begin(
4440 f: &ObjectFilter,
4441 tree_oid: ObjectId,
4442 depth: u64,
4443 tree_state: &mut TreeWalkState,
4444 tree_omit_set: &mut Option<HashSet<ObjectId>>,
4445 collect_omits: bool,
4446 sub_states: Option<&mut [CombineSubState]>,
4447) -> ListFilterBits {
4448 match f {
4449 ObjectFilter::BlobNone | ObjectFilter::BlobLimit(_) => ListFilterBits {
4450 mark_seen: true,
4451 do_show: true,
4452 skip_tree: false,
4453 },
4454 ObjectFilter::TreeDepth(excl) => tree_depth_begin_tree(
4455 tree_oid,
4456 depth,
4457 *excl,
4458 tree_state,
4459 tree_omit_set,
4460 collect_omits,
4461 ),
4462 ObjectFilter::SparseOid(_) => ListFilterBits {
4463 mark_seen: true,
4464 do_show: true,
4465 skip_tree: false,
4466 },
4467 ObjectFilter::ObjectType(k) => {
4468 let show = *k == FilterObjectKind::Tree;
4471 let skip_tree = matches!(k, FilterObjectKind::Commit | FilterObjectKind::Tag);
4472 ListFilterBits {
4473 mark_seen: true,
4474 do_show: show,
4475 skip_tree,
4476 }
4477 }
4478 ObjectFilter::Combine(parts) => {
4479 let states = sub_states.expect("combine sub-states");
4480 debug_assert_eq!(states.len(), parts.len());
4481 let mut bits = Vec::with_capacity(parts.len());
4482 let mut skipping = Vec::with_capacity(parts.len());
4483 for (i, p) in parts.iter().enumerate() {
4484 let b = filter_object_bits_tree_begin(
4485 p,
4486 tree_oid,
4487 depth,
4488 &mut states[i].tree_state,
4489 &mut states[i].tree_omit_set,
4490 collect_omits,
4491 None,
4492 );
4493 if b.skip_tree {
4494 states[i].is_skipping_tree = true;
4495 states[i].skip_tree_oid = Some(tree_oid);
4496 } else {
4497 states[i].is_skipping_tree = false;
4498 states[i].skip_tree_oid = None;
4499 }
4500 bits.push(b);
4501 skipping.push(states[i].is_skipping_tree);
4502 }
4503 ListFilterBits::merge_combine(&bits, &skipping)
4504 }
4505 }
4506}
4507
4508fn filter_object_bits_blob(
4509 f: &ObjectFilter,
4510 oid: ObjectId,
4511 size: u64,
4512 parent_depth: u64,
4513 tree_omit_set: &mut Option<HashSet<ObjectId>>,
4514 collect_omits: bool,
4515 sub_states: Option<&mut [CombineSubState]>,
4516) -> ListFilterBits {
4517 match f {
4518 ObjectFilter::BlobNone => ListFilterBits {
4519 mark_seen: true,
4520 do_show: false,
4521 skip_tree: false,
4522 },
4523 ObjectFilter::BlobLimit(limit) => {
4524 let include = size < *limit;
4525 ListFilterBits {
4526 mark_seen: true,
4527 do_show: include,
4528 skip_tree: false,
4529 }
4530 }
4531 ObjectFilter::TreeDepth(excl) => {
4532 tree_depth_blob(oid, size, parent_depth, *excl, tree_omit_set, collect_omits)
4533 }
4534 ObjectFilter::SparseOid(_) => ListFilterBits {
4535 mark_seen: true,
4536 do_show: true,
4537 skip_tree: false,
4538 },
4539 ObjectFilter::ObjectType(k) => {
4540 let show = *k == FilterObjectKind::Blob;
4541 ListFilterBits {
4542 mark_seen: true,
4543 do_show: show,
4544 skip_tree: false,
4545 }
4546 }
4547 ObjectFilter::Combine(parts) => {
4548 let states = sub_states.expect("combine sub-states");
4549 let mut bits = Vec::with_capacity(parts.len());
4550 let mut skipping = Vec::with_capacity(parts.len());
4551 for (i, p) in parts.iter().enumerate() {
4552 let b = filter_object_bits_blob(
4553 p,
4554 oid,
4555 size,
4556 parent_depth,
4557 &mut states[i].tree_omit_set,
4558 collect_omits,
4559 None,
4560 );
4561 bits.push(b);
4562 skipping.push(states[i].is_skipping_tree);
4563 }
4564 ListFilterBits::merge_combine(&bits, &skipping)
4565 }
4566 }
4567}
4568
4569#[derive(Debug)]
4570struct CombineSubState {
4571 tree_state: TreeWalkState,
4572 tree_omit_set: Option<HashSet<ObjectId>>,
4573 is_skipping_tree: bool,
4574 skip_tree_oid: Option<ObjectId>,
4575}
4576
4577impl CombineSubState {
4578 fn new(parts_len: usize, collect_omits: bool) -> Vec<Self> {
4579 (0..parts_len)
4580 .map(|_| Self {
4581 tree_state: TreeWalkState::new(),
4582 tree_omit_set: collect_omits.then(HashSet::new),
4583 is_skipping_tree: false,
4584 skip_tree_oid: None,
4585 })
4586 .collect()
4587 }
4588
4589 fn prepare_sub_states(
4590 filter: Option<&ObjectFilter>,
4591 collect_omits: bool,
4592 ) -> Option<Vec<CombineSubState>> {
4593 match filter {
4594 Some(ObjectFilter::Combine(parts)) => {
4595 Some(CombineSubState::new(parts.len(), collect_omits))
4596 }
4597 _ => None,
4598 }
4599 }
4600}
4601
4602#[allow(dead_code)]
4603fn collect_tree_objects_filtered(
4604 repo: &Repository,
4605 tree_oid: ObjectId,
4606 prefix: &str,
4607 depth: u64,
4608 explicit_root: bool,
4609 parent_union: Option<&HashSet<ObjectId>>,
4610 tree_state: &mut TreeWalkState,
4611 emitted: &mut HashSet<ObjectId>,
4612 result: &mut Vec<(ObjectId, String)>,
4613 omitted: &mut Vec<ObjectId>,
4614 missing: &mut Vec<ObjectId>,
4615 missing_seen: &mut HashSet<ObjectId>,
4616 filter: Option<&ObjectFilter>,
4617 filter_provided: bool,
4618 missing_action: MissingAction,
4619 sparse_lines: Option<&[String]>,
4620 skip_trees_for_type_filter: bool,
4621 omit_object_paths: bool,
4622 packed_set: Option<&HashSet<ObjectId>>,
4623 collect_tree_omits: bool,
4624 tree_omit_set: &mut Option<HashSet<ObjectId>>,
4625 combine_states: &mut Option<Vec<CombineSubState>>,
4626) -> Result<()> {
4627 if !explicit_root {
4628 if let Some(pu) = parent_union {
4629 if pu.contains(&tree_oid) {
4630 return Ok(());
4631 }
4632 }
4633 }
4634 let object = match repo.odb.read(&tree_oid) {
4635 Ok(object) => object,
4636 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
4637 if missing_action == MissingAction::Print && missing_seen.insert(tree_oid) {
4638 missing.push(tree_oid);
4639 }
4640 return Ok(());
4641 }
4642 Err(err) => return Err(err),
4643 };
4644 if object.kind != ObjectKind::Tree {
4645 return Err(Error::CorruptObject(format!(
4646 "object {tree_oid} is not a tree"
4647 )));
4648 }
4649
4650 let bits = match filter {
4651 None => ListFilterBits {
4652 mark_seen: true,
4653 do_show: true,
4654 skip_tree: false,
4655 },
4656 Some(f) => {
4657 if explicit_root && !filter_provided {
4658 ListFilterBits {
4659 mark_seen: true,
4660 do_show: true,
4661 skip_tree: false,
4662 }
4663 } else {
4664 match f {
4665 ObjectFilter::Combine(_) => {
4666 let states = combine_states.as_mut().expect("combine states");
4667 filter_object_bits_tree_begin(
4668 f,
4669 tree_oid,
4670 depth,
4671 tree_state,
4672 tree_omit_set,
4673 collect_tree_omits,
4674 Some(states.as_mut_slice()),
4675 )
4676 }
4677 _ => filter_object_bits_tree_begin(
4678 f,
4679 tree_oid,
4680 depth,
4681 tree_state,
4682 tree_omit_set,
4683 collect_tree_omits,
4684 None,
4685 ),
4686 }
4687 }
4688 }
4689 };
4690
4691 let tree_included = bits.do_show;
4694 if tree_included {
4695 if !packed_set.is_some_and(|p| p.contains(&tree_oid)) && emitted.insert(tree_oid) {
4696 let out_path = if omit_object_paths {
4697 String::new()
4698 } else {
4699 prefix.to_owned()
4700 };
4701 result.push((tree_oid, out_path));
4702 }
4703 } else if bits.mark_seen {
4704 omitted.push(tree_oid);
4705 }
4706
4707 if bits.skip_tree {
4708 trace_skip_tree_contents(prefix);
4709 }
4710
4711 if skip_trees_for_type_filter && depth == 0 && !explicit_root {
4712 return Ok(());
4713 }
4714
4715 if bits.skip_tree {
4716 return Ok(());
4717 }
4718
4719 let entries = parse_tree(&object.data)?;
4720 for entry in entries {
4721 if entry.mode == 0o160000 {
4722 continue;
4723 }
4724 let name = String::from_utf8_lossy(&entry.name).to_string();
4725 let path = if prefix.is_empty() {
4726 name.clone()
4727 } else {
4728 format!("{prefix}/{name}")
4729 };
4730 let child_obj = match repo.odb.read(&entry.oid) {
4731 Ok(object) => object,
4732 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
4733 if missing_action == MissingAction::Print && missing_seen.insert(entry.oid) {
4734 missing.push(entry.oid);
4735 }
4736 continue;
4737 }
4738 Err(err) => return Err(err),
4739 };
4740 if entry.mode == 0o040000 {
4741 if child_obj.kind != ObjectKind::Tree {
4742 return Err(Error::CorruptObject(format!(
4743 "object {} is not a tree",
4744 entry.oid
4745 )));
4746 }
4747 if let Some(pu) = parent_union {
4748 if pu.contains(&entry.oid) {
4749 continue;
4750 }
4751 }
4752 let child_tree_depth = depth + 1;
4753 collect_tree_objects_filtered(
4754 repo,
4755 entry.oid,
4756 &path,
4757 child_tree_depth,
4758 false,
4759 parent_union,
4760 tree_state,
4761 emitted,
4762 result,
4763 omitted,
4764 missing,
4765 missing_seen,
4766 filter,
4767 filter_provided,
4768 missing_action,
4769 sparse_lines,
4770 skip_trees_for_type_filter,
4771 omit_object_paths,
4772 packed_set,
4773 collect_tree_omits,
4774 tree_omit_set,
4775 combine_states,
4776 )?;
4777 } else {
4778 if let Some(pu) = parent_union {
4779 if pu.contains(&entry.oid) {
4780 continue;
4781 }
4782 }
4783 if child_obj.kind == ObjectKind::Blob {
4784 let sparse_blob = sparse_filter_includes_path(repo, &path, sparse_lines);
4785 let blob_bits = match filter {
4786 None => ListFilterBits {
4787 mark_seen: true,
4788 do_show: true,
4789 skip_tree: false,
4790 },
4791 Some(f) => {
4792 if explicit_root && !filter_provided {
4793 ListFilterBits {
4794 mark_seen: true,
4795 do_show: true,
4796 skip_tree: false,
4797 }
4798 } else {
4799 match f {
4800 ObjectFilter::Combine(_) => {
4801 let states = combine_states.as_mut().unwrap();
4802 filter_object_bits_blob(
4803 f,
4804 entry.oid,
4805 child_obj.data.len() as u64,
4806 depth,
4807 tree_omit_set,
4808 collect_tree_omits,
4809 Some(states.as_mut_slice()),
4810 )
4811 }
4812 _ => filter_object_bits_blob(
4813 f,
4814 entry.oid,
4815 child_obj.data.len() as u64,
4816 depth,
4817 tree_omit_set,
4818 collect_tree_omits,
4819 None,
4820 ),
4821 }
4822 }
4823 }
4824 };
4825 let blob_included = blob_bits.do_show && sparse_blob;
4826 if !blob_included {
4827 omitted.push(entry.oid);
4828 } else if blob_bits.mark_seen
4829 && emitted.insert(entry.oid)
4830 && !packed_set.is_some_and(|p| p.contains(&entry.oid))
4831 {
4832 let out_path = if omit_object_paths {
4833 String::new()
4834 } else {
4835 path.clone()
4836 };
4837 result.push((entry.oid, out_path));
4838 }
4839 } else {
4840 if emitted.contains(&entry.oid) {
4841 return Err(Error::CorruptObject(format!(
4842 "object {} is not a blob",
4843 entry.oid
4844 )));
4845 }
4846 if emitted.insert(entry.oid) {
4847 result.push((entry.oid, path));
4848 }
4849 }
4850 }
4851 }
4852
4853 if let Some(f) = filter {
4854 if let ObjectFilter::Combine(_) = f {
4855 if let Some(states) = combine_states.as_mut() {
4856 for st in states.iter_mut() {
4857 if st.is_skipping_tree && st.skip_tree_oid == Some(tree_oid) {
4858 st.is_skipping_tree = false;
4859 st.skip_tree_oid = None;
4860 }
4861 }
4862 }
4863 }
4864 }
4865
4866 Ok(())
4867}
4868
4869fn collect_root_object(
4870 repo: &Repository,
4871 root: &RootObject,
4872 tree_state: &mut TreeWalkState,
4873 emitted: &mut HashSet<ObjectId>,
4874 result: &mut Vec<(ObjectId, String)>,
4875 omitted: &mut Vec<ObjectId>,
4876 missing: &mut Vec<ObjectId>,
4877 missing_seen: &mut HashSet<ObjectId>,
4878 filter: Option<&ObjectFilter>,
4879 filter_provided: bool,
4880 missing_action: MissingAction,
4881 sparse_lines: Option<&[String]>,
4882 skip_trees_for_type_filter: bool,
4883 omit_object_paths: bool,
4884 packed_set: Option<&HashSet<ObjectId>>,
4885 collect_tree_omits: bool,
4886 tree_omit_set: &mut Option<HashSet<ObjectId>>,
4887 combine_states: &mut Option<Vec<CombineSubState>>,
4888) -> Result<()> {
4889 if let Some(tag_oid) = root.wrap_with_tag {
4890 let show_tag = match filter {
4891 None => true,
4892 Some(f) => f.includes_commit_or_tag_object(ObjectKind::Tag),
4893 };
4894 if show_tag && emitted.insert(tag_oid) {
4895 result.push((tag_oid, "tag".to_owned()));
4896 }
4897 }
4898
4899 let object = match repo.odb.read(&root.oid) {
4900 Ok(object) => object,
4901 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
4902 if missing_action == MissingAction::Print && missing_seen.insert(root.oid) {
4903 missing.push(root.oid);
4904 }
4905 return Ok(());
4906 }
4907 Err(err) => return Err(err),
4908 };
4909
4910 if let Some(expected) = root.expected_kind {
4911 if !expected.matches(object.kind) {
4912 return Err(Error::CorruptObject(format!(
4913 "object {} is not a {}",
4914 root.input,
4915 expected.as_str()
4916 )));
4917 }
4918 }
4919
4920 match object.kind {
4921 ObjectKind::Commit => {
4922 let commit = parse_commit(&object.data)?;
4923 let parent_union = union_parent_reachable_objects(
4924 repo,
4925 &commit.parents,
4926 missing_action,
4927 missing,
4928 missing_seen,
4929 )?;
4930 collect_tree_objects_filtered(
4931 repo,
4932 commit.tree,
4933 "",
4934 0,
4935 false,
4936 Some(&parent_union),
4937 tree_state,
4938 emitted,
4939 result,
4940 omitted,
4941 missing,
4942 missing_seen,
4943 filter,
4944 filter_provided,
4945 missing_action,
4946 sparse_lines,
4947 skip_trees_for_type_filter,
4948 omit_object_paths,
4949 packed_set,
4950 collect_tree_omits,
4951 tree_omit_set,
4952 combine_states,
4953 )?;
4954 }
4955 ObjectKind::Tree => {
4956 collect_tree_objects_filtered(
4957 repo,
4958 root.oid,
4959 "",
4960 0,
4961 true,
4962 None,
4963 tree_state,
4964 emitted,
4965 result,
4966 omitted,
4967 missing,
4968 missing_seen,
4969 filter,
4970 filter_provided,
4971 missing_action,
4972 sparse_lines,
4973 skip_trees_for_type_filter,
4974 omit_object_paths,
4975 packed_set,
4976 collect_tree_omits,
4977 tree_omit_set,
4978 combine_states,
4979 )?;
4980 }
4981 ObjectKind::Blob => {
4982 let path_for_sparse = root.root_path.as_deref().unwrap_or("");
4983 let sparse_blob = sparse_filter_includes_path(repo, path_for_sparse, sparse_lines);
4984 let blob_bits = match filter {
4985 None => ListFilterBits {
4986 mark_seen: true,
4987 do_show: true,
4988 skip_tree: false,
4989 },
4990 Some(f) => {
4991 if !filter_provided {
4992 ListFilterBits {
4993 mark_seen: true,
4994 do_show: true,
4995 skip_tree: false,
4996 }
4997 } else {
4998 match f {
4999 ObjectFilter::Combine(_) => {
5000 let states = combine_states.as_mut().expect("combine states");
5001 filter_object_bits_blob(
5002 f,
5003 root.oid,
5004 object.data.len() as u64,
5005 0,
5006 tree_omit_set,
5007 collect_tree_omits,
5008 Some(states.as_mut_slice()),
5009 )
5010 }
5011 _ => filter_object_bits_blob(
5012 f,
5013 root.oid,
5014 object.data.len() as u64,
5015 0,
5016 tree_omit_set,
5017 collect_tree_omits,
5018 None,
5019 ),
5020 }
5021 }
5022 }
5023 };
5024 let blob_included = blob_bits.do_show && sparse_blob;
5025 if !blob_included {
5026 if blob_bits.mark_seen {
5027 omitted.push(root.oid);
5028 }
5029 return Ok(());
5030 }
5031 if packed_set.is_some_and(|p| p.contains(&root.oid)) {
5032 return Ok(());
5033 }
5034 if blob_bits.mark_seen && !emitted.insert(root.oid) {
5035 return Ok(());
5036 }
5037 let out_path = if omit_object_paths {
5038 String::new()
5039 } else {
5040 path_for_sparse.to_owned()
5041 };
5042 result.push((root.oid, out_path));
5043 }
5044 ObjectKind::Tag => {
5045 let tag = parse_tag(&object.data)?;
5046 let expected_kind =
5047 ExpectedObjectKind::from_tag_type(&tag.object_type).ok_or_else(|| {
5048 Error::CorruptObject(format!(
5049 "object {} has unsupported tag type '{}'",
5050 root.input, tag.object_type
5051 ))
5052 })?;
5053 let nested = RootObject {
5054 oid: tag.object,
5055 input: root.input.clone(),
5056 expected_kind: Some(expected_kind),
5057 root_path: None,
5058 wrap_with_tag: None,
5059 };
5060 collect_root_object(
5061 repo,
5062 &nested,
5063 tree_state,
5064 emitted,
5065 result,
5066 omitted,
5067 missing,
5068 missing_seen,
5069 filter,
5070 filter_provided,
5071 missing_action,
5072 sparse_lines,
5073 skip_trees_for_type_filter,
5074 omit_object_paths,
5075 packed_set,
5076 collect_tree_omits,
5077 tree_omit_set,
5078 combine_states,
5079 )?;
5080 }
5081 }
5082
5083 Ok(())
5084}
5085
5086fn collect_reachable_objects_in_commit_order(
5090 repo: &Repository,
5091 _graph: &mut CommitGraph<'_>,
5092 commits: &[ObjectId],
5093 object_roots: &[RootObject],
5094 tip_annotated_tags: &HashMap<ObjectId, ObjectId>,
5095 filter: Option<&ObjectFilter>,
5096 filter_provided: bool,
5097 missing_action: MissingAction,
5098 sparse_lines: Option<&[String]>,
5099 skip_trees_for_type_filter: bool,
5100 omit_object_paths: bool,
5101 packed_set: Option<&HashSet<ObjectId>>,
5102 collect_tree_omits: bool,
5103) -> Result<(
5104 Vec<(ObjectId, String)>,
5105 Vec<ObjectId>,
5106 Vec<ObjectId>,
5107 Vec<usize>,
5108)> {
5109 let mut tree_state = TreeWalkState::new();
5110 let mut top_tree_omit =
5111 walk_needs_top_tree_omit_set(filter, collect_tree_omits).then(HashSet::<ObjectId>::new);
5112 let mut combine_states = CombineSubState::prepare_sub_states(filter, collect_tree_omits);
5113 let mut emitted = HashSet::new();
5114 let mut result = Vec::new();
5115 let mut omitted = Vec::new();
5116 let mut missing = Vec::new();
5117 let mut missing_seen = HashSet::new();
5118 let mut counts = Vec::with_capacity(commits.len());
5119 for &commit_oid in commits {
5120 let commit = match load_commit(repo, commit_oid) {
5121 Ok(commit) => commit,
5122 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
5123 if missing_action == MissingAction::Print && missing_seen.insert(commit_oid) {
5124 missing.push(commit_oid);
5125 }
5126 counts.push(0);
5127 continue;
5128 }
5129 Err(err) => return Err(err),
5130 };
5131 let before = result.len();
5132 if let Some(&tag_oid) = tip_annotated_tags.get(&commit_oid) {
5133 if emitted.insert(tag_oid) {
5134 result.push((tag_oid, "tag".to_owned()));
5135 }
5136 }
5137 collect_tree_objects_filtered(
5141 repo,
5142 commit.tree,
5143 "",
5144 0,
5145 false,
5146 None,
5147 &mut tree_state,
5148 &mut emitted,
5149 &mut result,
5150 &mut omitted,
5151 &mut missing,
5152 &mut missing_seen,
5153 filter,
5154 filter_provided,
5155 missing_action,
5156 sparse_lines,
5157 skip_trees_for_type_filter,
5158 omit_object_paths,
5159 packed_set,
5160 collect_tree_omits,
5161 &mut top_tree_omit,
5162 &mut combine_states,
5163 )?;
5164 counts.push(result.len() - before);
5165 }
5166
5167 for root in object_roots {
5168 collect_root_object(
5169 repo,
5170 root,
5171 &mut tree_state,
5172 &mut emitted,
5173 &mut result,
5174 &mut omitted,
5175 &mut missing,
5176 &mut missing_seen,
5177 filter,
5178 filter_provided,
5179 missing_action,
5180 sparse_lines,
5181 skip_trees_for_type_filter,
5182 omit_object_paths,
5183 packed_set,
5184 collect_tree_omits,
5185 &mut top_tree_omit,
5186 &mut combine_states,
5187 )?;
5188 }
5189
5190 Ok((result, omitted, missing, counts))
5191}
5192
5193fn kept_object_ids(repo: &Repository) -> Result<HashSet<ObjectId>> {
5195 let pack_dir = repo.git_dir.join("objects/pack");
5196 let mut kept = HashSet::new();
5197 if !pack_dir.is_dir() {
5198 return Ok(kept);
5199 }
5200 for entry in std::fs::read_dir(&pack_dir)? {
5201 let entry = entry?;
5202 let path = entry.path();
5203 if path.extension().is_some_and(|ext| ext == "keep") {
5204 let idx_path = path.with_extension("idx");
5206 if idx_path.exists() {
5207 if let Ok(oids) = crate::pack::read_idx_object_ids(&idx_path) {
5208 kept.extend(oids);
5209 }
5210 }
5211 }
5212 }
5213 Ok(kept)
5214}
5215
5216fn flatten_tree(
5217 repo: &Repository,
5218 tree_oid: ObjectId,
5219 prefix: &str,
5220) -> Result<Vec<(String, ObjectId)>> {
5221 let mut result = Vec::new();
5222 let object = match repo.odb.read(&tree_oid) {
5223 Ok(o) => o,
5224 Err(_) => return Ok(result),
5225 };
5226 if object.kind != ObjectKind::Tree {
5227 return Ok(result);
5228 }
5229 let entries = parse_tree(&object.data)?;
5230 for entry in entries {
5231 let name = String::from_utf8_lossy(&entry.name).to_string();
5232 let path = if prefix.is_empty() {
5233 name
5234 } else {
5235 format!("{prefix}/{name}")
5236 };
5237 let child = match repo.odb.read(&entry.oid) {
5238 Ok(o) => o,
5239 Err(Error::ObjectNotFound(_)) => continue,
5240 Err(err) => return Err(err),
5241 };
5242 if child.kind == ObjectKind::Tree {
5243 result.extend(flatten_tree(repo, entry.oid, &path)?);
5244 } else {
5245 result.push((path, entry.oid));
5246 }
5247 }
5248 Ok(result)
5249}
5250
5251pub fn merge_bases(
5253 repo: &Repository,
5254 a: ObjectId,
5255 b: ObjectId,
5256 first_parent_only: bool,
5257) -> Result<Vec<ObjectId>> {
5258 let mut graph = CommitGraph::new(repo, first_parent_only);
5259 let ancestors_a = walk_closure(&mut graph, &[a])?;
5260 let ancestors_b = walk_closure(&mut graph, &[b])?;
5261 let common: HashSet<ObjectId> = ancestors_a.intersection(&ancestors_b).copied().collect();
5262 if common.is_empty() {
5263 return Ok(Vec::new());
5264 }
5265 let mut bases = Vec::new();
5267 for &c in &common {
5268 let is_dominated = common.iter().any(|&other| {
5269 if other == c {
5270 return false;
5271 }
5272 let other_anc = walk_closure(&mut graph, &[other]).unwrap_or_default();
5273 other_anc.contains(&c)
5274 });
5275 if !is_dominated {
5276 bases.push(c);
5277 }
5278 }
5279 if bases.is_empty() {
5280 let mut sorted: Vec<_> = common.into_iter().collect();
5281 sorted.sort_by_key(|b| std::cmp::Reverse(graph.committer_time(*b)));
5282 bases.push(sorted[0]);
5283 }
5284 Ok(bases)
5285}