1use std::cmp::Ordering;
9use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
10use std::fs;
11use std::path::Path;
12
13use crate::error::{Error, Result};
14use crate::objects::{parse_commit, parse_tag, parse_tree, ObjectId, ObjectKind};
15use crate::refs;
16use crate::repo::Repository;
17use crate::rev_parse::resolve_revision;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum OutputMode {
22 OidOnly,
24 Parents,
26 Format(String),
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum MissingAction {
33 Error,
35 Print,
37 Allow,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum ObjectFilter {
44 BlobNone,
46 BlobLimit(u64),
48 TreeDepth(u64),
50 ObjectType(ObjectKind),
52 Combine(Vec<ObjectFilter>),
54}
55
56impl ObjectFilter {
57 pub fn parse(spec: &str) -> std::result::Result<Self, String> {
59 if spec == "blob:none" {
60 return Ok(ObjectFilter::BlobNone);
61 }
62 if let Some(rest) = spec.strip_prefix("blob:limit=") {
63 let bytes = parse_size_suffix(rest)
64 .ok_or_else(|| format!("invalid blob:limit value: {rest}"))?;
65 return Ok(ObjectFilter::BlobLimit(bytes));
66 }
67 if let Some(rest) = spec.strip_prefix("tree:") {
68 let depth: u64 = rest
69 .parse()
70 .map_err(|_| format!("invalid tree depth: {rest}"))?;
71 return Ok(ObjectFilter::TreeDepth(depth));
72 }
73 if let Some(rest) = spec.strip_prefix("object:type=") {
74 let kind = match rest {
75 "blob" => ObjectKind::Blob,
76 "tree" => ObjectKind::Tree,
77 "commit" => ObjectKind::Commit,
78 "tag" => ObjectKind::Tag,
79 _ => return Err(format!("unsupported filter spec: {spec}")),
80 };
81 return Ok(ObjectFilter::ObjectType(kind));
82 }
83 if let Some(rest) = spec.strip_prefix("combine:") {
84 let parts = split_combine(rest);
85 let mut filters = Vec::new();
86 for part in parts {
87 filters.push(ObjectFilter::parse(&part)?);
88 }
89 return Ok(ObjectFilter::Combine(filters));
90 }
91 Err(format!("unsupported filter spec: {spec}"))
92 }
93
94 #[must_use]
96 pub fn emit_blob(&self, size: u64) -> bool {
97 match self {
98 ObjectFilter::BlobNone => false,
99 ObjectFilter::BlobLimit(limit) => size <= *limit,
100 ObjectFilter::TreeDepth(_) => true,
101 ObjectFilter::ObjectType(kind) => *kind == ObjectKind::Blob,
102 ObjectFilter::Combine(filters) => filters.iter().all(|f| f.emit_blob(size)),
103 }
104 }
105
106 #[must_use]
108 pub fn emit_tree(&self, depth: u64) -> bool {
109 match self {
110 ObjectFilter::BlobNone => true,
111 ObjectFilter::BlobLimit(_) => true,
112 ObjectFilter::TreeDepth(max_depth) => depth < *max_depth,
113 ObjectFilter::ObjectType(kind) => *kind == ObjectKind::Tree,
114 ObjectFilter::Combine(filters) => filters.iter().all(|f| f.emit_tree(depth)),
115 }
116 }
117
118 pub fn includes_blob(&self, size: u64) -> bool {
120 self.emit_blob(size)
121 }
122
123 pub fn includes_tree(&self, depth: u64) -> bool {
125 self.emit_tree(depth)
126 }
127
128 #[must_use]
130 pub fn passes_for_object(&self, kind: ObjectKind, size: usize) -> bool {
131 let sz = size as u64;
132 match self {
133 ObjectFilter::BlobNone => kind != ObjectKind::Blob,
134 ObjectFilter::BlobLimit(limit) => kind != ObjectKind::Blob || sz <= *limit,
135 ObjectFilter::TreeDepth(_) => true,
136 ObjectFilter::ObjectType(expected) => kind == *expected,
137 ObjectFilter::Combine(filters) => {
138 filters.iter().all(|f| f.passes_for_object(kind, size))
139 }
140 }
141 }
142}
143
144fn parse_size_suffix(s: &str) -> Option<u64> {
146 let s = s.trim();
147 if s.is_empty() {
148 return None;
149 }
150 let (num_str, multiplier) = match s.as_bytes().last()? {
151 b'k' | b'K' => (&s[..s.len() - 1], 1024u64),
152 b'm' | b'M' => (&s[..s.len() - 1], 1024 * 1024),
153 b'g' | b'G' => (&s[..s.len() - 1], 1024 * 1024 * 1024),
154 _ => (s, 1u64),
155 };
156 let num: u64 = num_str.parse().ok()?;
157 Some(num * multiplier)
158}
159
160fn split_combine(spec: &str) -> Vec<String> {
162 let mut parts = Vec::new();
163 let mut current = String::new();
164 let chars = spec.chars().peekable();
165 for ch in chars {
166 if ch == '+' {
167 if !current.is_empty() {
168 parts.push(url_decode(¤t));
169 current.clear();
170 }
171 } else {
172 current.push(ch);
173 }
174 }
175 if !current.is_empty() {
176 parts.push(url_decode(¤t));
177 }
178 parts
179}
180
181fn url_decode(s: &str) -> String {
183 let mut result = String::new();
184 let mut chars = s.chars();
185 while let Some(ch) = chars.next() {
186 if ch == '%' {
187 let hi = chars.next().unwrap_or('0');
188 let lo = chars.next().unwrap_or('0');
189 let byte = u8::from_str_radix(&format!("{hi}{lo}"), 16).unwrap_or(b'?');
190 result.push(byte as char);
191 } else {
192 result.push(ch);
193 }
194 }
195 result
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub enum OrderingMode {
201 Default,
203 Topo,
205 Date,
207}
208
209#[derive(Debug, Clone)]
211pub struct RevListOptions {
212 pub all_refs: bool,
214 pub first_parent: bool,
216 pub ancestry_path: bool,
218 pub ancestry_path_bottoms: Vec<ObjectId>,
220 pub simplify_by_decoration: bool,
222 pub output_mode: OutputMode,
224 pub quiet: bool,
226 pub count: bool,
228 pub skip: usize,
230 pub max_count: Option<usize>,
232 pub ordering: OrderingMode,
234 pub reverse: bool,
236 pub objects: bool,
238 pub no_object_names: bool,
240 pub boundary: bool,
242 pub left_right: bool,
244 pub left_only: bool,
246 pub right_only: bool,
248 pub cherry_mark: bool,
250 pub cherry_pick: bool,
252 pub min_parents: Option<usize>,
254 pub max_parents: Option<usize>,
256 pub symmetric_left: Option<ObjectId>,
258 pub symmetric_right: Option<ObjectId>,
260 pub paths: Vec<String>,
262 pub full_history: bool,
264 pub sparse: bool,
266 pub filter: Option<ObjectFilter>,
268 pub filter_print_omitted: bool,
270 pub in_commit_order: bool,
272 pub no_kept_objects: bool,
274 pub missing_action: MissingAction,
276}
277
278impl Default for RevListOptions {
279 fn default() -> Self {
280 Self {
281 all_refs: false,
282 first_parent: false,
283 ancestry_path: false,
284 ancestry_path_bottoms: Vec::new(),
285 simplify_by_decoration: false,
286 output_mode: OutputMode::OidOnly,
287 quiet: false,
288 count: false,
289 skip: 0,
290 max_count: None,
291 ordering: OrderingMode::Default,
292 reverse: false,
293 objects: false,
294 no_object_names: false,
295 boundary: false,
296 left_right: false,
297 left_only: false,
298 right_only: false,
299 cherry_mark: false,
300 cherry_pick: false,
301 min_parents: None,
302 max_parents: None,
303 symmetric_left: None,
304 symmetric_right: None,
305 paths: Vec::new(),
306 full_history: false,
307 sparse: false,
308 filter: None,
309 filter_print_omitted: false,
310 in_commit_order: false,
311 no_kept_objects: false,
312 missing_action: MissingAction::Error,
313 }
314 }
315}
316
317#[derive(Debug, Clone)]
319pub struct RevListResult {
320 pub commits: Vec<ObjectId>,
322 pub objects: Vec<(ObjectId, String)>,
325 pub omitted_objects: Vec<ObjectId>,
327 pub missing_objects: Vec<ObjectId>,
329 pub boundary_commits: Vec<ObjectId>,
331 pub left_right_map: HashMap<ObjectId, bool>,
333 pub cherry_equivalent: HashSet<ObjectId>,
335 pub per_commit_object_counts: Vec<usize>,
339}
340
341pub fn rev_list(
355 repo: &Repository,
356 positive_specs: &[String],
357 negative_specs: &[String],
358 options: &RevListOptions,
359) -> Result<RevListResult> {
360 let mut graph = CommitGraph::new(repo, options.first_parent);
361
362 let (mut include, object_roots) = if options.objects {
363 let (commit_starts, roots) = resolve_specs_for_objects(repo, positive_specs)?;
364 (commit_starts, roots)
365 } else {
366 (resolve_specs(repo, positive_specs)?, Vec::new())
367 };
368 let exclude = resolve_specs(repo, negative_specs)?;
369
370 if options.all_refs {
371 include.extend(all_ref_tips(repo)?);
372 }
373
374 if include.is_empty() && object_roots.is_empty() {
375 return Err(Error::InvalidRef("no revisions specified".to_owned()));
376 }
377
378 let (mut included, discovery_order) = if include.is_empty() {
379 (HashSet::new(), Vec::new())
380 } else {
381 walk_closure_ordered(&mut graph, &include)?
382 };
383 let excluded = if exclude.is_empty() {
384 HashSet::new()
385 } else {
386 walk_closure(&mut graph, &exclude)?
387 };
388 included.retain(|oid| !excluded.contains(oid));
389
390 if options.simplify_by_decoration {
391 let decorated = all_ref_tips(repo)?;
392 included.retain(|oid| decorated.contains(oid));
393 }
394
395 if options.ancestry_path {
396 let mut bottoms = options.ancestry_path_bottoms.clone();
397 if bottoms.is_empty() {
398 bottoms.extend(exclude.iter().copied());
399 }
400 if bottoms.is_empty() {
401 return Err(Error::InvalidRef(
402 "--ancestry-path requires a range with excluded tips".to_owned(),
403 ));
404 }
405 limit_to_ancestry(&mut graph, &mut included, &bottoms)?;
406 }
407
408 if options.min_parents.is_some() || options.max_parents.is_some() {
410 let min_p = options.min_parents.unwrap_or(0);
411 let max_p = options.max_parents.unwrap_or(usize::MAX);
412 included.retain(|oid| {
413 let count = graph.parents_of(*oid).map(|p| p.len()).unwrap_or(0);
414 count >= min_p && count <= max_p
415 });
416 }
417
418 let mut ordered = match options.ordering {
419 OrderingMode::Default => sort_by_commit_date_desc(&mut graph, &included, &discovery_order)?,
420 OrderingMode::Topo | OrderingMode::Date => topo_sort(&mut graph, &included)?,
421 };
422
423 if !options.paths.is_empty() {
425 let paths = &options.paths;
426 ordered.retain(|oid| {
427 commit_touches_paths(
428 repo,
429 &mut graph,
430 *oid,
431 paths,
432 options.full_history,
433 options.sparse,
434 )
435 .unwrap_or(false)
436 });
437 }
438
439 let mut left_right_map = HashMap::new();
441 if options.left_right
442 || options.left_only
443 || options.right_only
444 || options.cherry_mark
445 || options.cherry_pick
446 {
447 if let (Some(left_oid), Some(right_oid)) = (options.symmetric_left, options.symmetric_right)
448 {
449 let left_closure = walk_closure(&mut graph, &[left_oid])?;
450 let right_closure = walk_closure(&mut graph, &[right_oid])?;
451 for &oid in &ordered {
452 let in_left = left_closure.contains(&oid);
453 let in_right = right_closure.contains(&oid);
454 if in_left && !in_right {
455 left_right_map.insert(oid, true);
456 } else if in_right && !in_left {
457 left_right_map.insert(oid, false);
458 } else {
459 left_right_map.insert(oid, false);
460 }
461 }
462 }
463 }
464
465 let mut cherry_equivalent = HashSet::new();
467 if options.cherry_pick || options.cherry_mark {
468 let patch_ids = compute_patch_ids(repo, &mut graph, &ordered)?;
469 let left_commits: Vec<_> = ordered
470 .iter()
471 .filter(|o| left_right_map.get(o) == Some(&true))
472 .copied()
473 .collect();
474 let right_commits: Vec<_> = ordered
475 .iter()
476 .filter(|o| left_right_map.get(o) == Some(&false))
477 .copied()
478 .collect();
479 let left_patches: HashMap<&str, ObjectId> = left_commits
480 .iter()
481 .filter_map(|o| patch_ids.get(o).map(|p| (p.as_str(), *o)))
482 .collect();
483 let right_patches: HashMap<&str, ObjectId> = right_commits
484 .iter()
485 .filter_map(|o| patch_ids.get(o).map(|p| (p.as_str(), *o)))
486 .collect();
487 for (&pid, &oid) in &left_patches {
488 if !pid.is_empty() && right_patches.contains_key(pid) {
489 cherry_equivalent.insert(oid);
490 cherry_equivalent.insert(right_patches[pid]);
491 }
492 }
493 for (&pid, &oid) in &right_patches {
494 if !pid.is_empty() && left_patches.contains_key(pid) {
495 cherry_equivalent.insert(oid);
496 cherry_equivalent.insert(left_patches[pid]);
497 }
498 }
499 }
500
501 if options.left_only {
503 ordered.retain(|oid| left_right_map.get(oid) == Some(&true));
504 }
505 if options.right_only {
506 ordered.retain(|oid| left_right_map.get(oid) == Some(&false));
507 }
508
509 if options.cherry_pick {
511 ordered.retain(|oid| !cherry_equivalent.contains(oid));
512 }
513
514 if options.skip > 0 {
515 ordered = ordered.into_iter().skip(options.skip).collect();
516 }
517 if let Some(max_count) = options.max_count {
518 ordered.truncate(max_count);
519 }
520 if options.reverse {
521 ordered.reverse();
522 }
523
524 let boundary_commits = if options.boundary {
526 let included_set: HashSet<ObjectId> = ordered.iter().copied().collect();
527 let mut boundary = Vec::new();
528 let mut boundary_seen = HashSet::new();
529 for &oid in &ordered {
530 if let Ok(parents) = graph.parents_of(oid).map(|p| p.to_vec()) {
531 for parent in parents {
532 if !included_set.contains(&parent) && boundary_seen.insert(parent) {
533 boundary.push(parent);
534 }
535 }
536 }
537 }
538 boundary
539 } else {
540 Vec::new()
541 };
542
543 let kept_set = if options.no_kept_objects {
545 kept_object_ids(repo).unwrap_or_default()
546 } else {
547 HashSet::new()
548 };
549
550 if options.no_kept_objects {
551 ordered.retain(|oid| !kept_set.contains(oid));
552 }
553
554 let (objects, omitted_objects, missing_objects, per_commit_object_counts) = if options.objects {
556 let (mut objs, omit, miss, counts) = if options.in_commit_order {
557 collect_reachable_objects_in_commit_order(
558 repo,
559 &mut graph,
560 &ordered,
561 &object_roots,
562 options.filter.as_ref(),
563 options.missing_action,
564 )?
565 } else {
566 let (objs, omit, miss) = collect_reachable_objects(
567 repo,
568 &mut graph,
569 &ordered,
570 &object_roots,
571 options.filter.as_ref(),
572 options.missing_action,
573 )?;
574 (objs, omit, miss, Vec::new())
575 };
576 if options.no_kept_objects {
577 objs.retain(|(oid, _)| !kept_set.contains(oid));
578 }
579 (objs, omit, miss, counts)
580 } else {
581 (Vec::new(), Vec::new(), Vec::new(), Vec::new())
582 };
583
584 Ok(RevListResult {
585 commits: ordered,
586 objects,
587 omitted_objects,
588 missing_objects,
589 boundary_commits,
590 left_right_map,
591 cherry_equivalent,
592 per_commit_object_counts,
593 })
594}
595
596#[must_use]
603pub fn split_revision_token(token: &str) -> (Vec<String>, Vec<String>) {
604 if let Some((lhs, rhs)) = token.split_once("..") {
605 let positive = if rhs.is_empty() {
606 "HEAD".to_owned()
607 } else {
608 rhs.to_owned()
609 };
610 let negative = if lhs.is_empty() {
611 "HEAD".to_owned()
612 } else {
613 lhs.to_owned()
614 };
615 return (vec![positive], vec![negative]);
616 }
617 if let Some(rest) = token.strip_prefix('^') {
618 return (Vec::new(), vec![rest.to_owned()]);
619 }
620 (vec![token.to_owned()], Vec::new())
621}
622
623fn ansi_color_from_name(name: &str) -> String {
624 match name {
625 "red" => "\x1b[31m".to_owned(),
626 "green" => "\x1b[32m".to_owned(),
627 "yellow" => "\x1b[33m".to_owned(),
628 "blue" => "\x1b[34m".to_owned(),
629 "magenta" => "\x1b[35m".to_owned(),
630 "cyan" => "\x1b[36m".to_owned(),
631 "white" => "\x1b[37m".to_owned(),
632 "bold" => "\x1b[1m".to_owned(),
633 "dim" => "\x1b[2m".to_owned(),
634 "ul" | "underline" => "\x1b[4m".to_owned(),
635 "blink" => "\x1b[5m".to_owned(),
636 "reverse" => "\x1b[7m".to_owned(),
637 "reset" => "\x1b[m".to_owned(),
638 _ => String::new(),
639 }
640}
641
642fn color_name_to_code(name: &str) -> Option<u8> {
643 match name {
644 "black" => Some(0),
645 "red" => Some(1),
646 "green" => Some(2),
647 "yellow" => Some(3),
648 "blue" => Some(4),
649 "magenta" => Some(5),
650 "cyan" => Some(6),
651 "white" => Some(7),
652 "default" => Some(9),
653 _ => None,
654 }
655}
656
657fn ansi_color_from_spec(spec: &str) -> String {
658 if spec == "reset" {
659 return "\x1b[m".to_owned();
660 }
661 let mut codes = Vec::new();
662 let mut fg_set = false;
663 for part in spec.split_whitespace() {
664 match part {
665 "bold" => codes.push("1".to_owned()),
666 "dim" => codes.push("2".to_owned()),
667 "italic" => codes.push("3".to_owned()),
668 "ul" | "underline" => codes.push("4".to_owned()),
669 "blink" => codes.push("5".to_owned()),
670 "reverse" => codes.push("7".to_owned()),
671 "strike" => codes.push("9".to_owned()),
672 "nobold" | "nodim" => codes.push("22".to_owned()),
673 "noitalic" => codes.push("23".to_owned()),
674 "noul" | "nounderline" => codes.push("24".to_owned()),
675 "noblink" => codes.push("25".to_owned()),
676 "noreverse" => codes.push("27".to_owned()),
677 "nostrike" => codes.push("29".to_owned()),
678 _ => {
679 if let Some(code) = color_name_to_code(part) {
680 if !fg_set {
681 codes.push(format!("{}", 30 + code));
682 fg_set = true;
683 } else {
684 codes.push(format!("{}", 40 + code));
685 }
686 }
687 }
688 }
689 }
690 if codes.is_empty() {
691 String::new()
692 } else {
693 format!("\x1b[{}m", codes.join(";"))
694 }
695}
696
697fn format_relative_date(diff: i64) -> String {
698 if diff < 0 {
699 "in the future".to_owned()
700 } else if diff < 60 {
701 format!("{} seconds ago", diff)
702 } else if diff < 3600 {
703 let m = diff / 60;
704 if m == 1 {
705 "1 minute ago".to_owned()
706 } else {
707 format!("{m} minutes ago")
708 }
709 } else if diff < 86400 {
710 let h = diff / 3600;
711 if h == 1 {
712 "1 hour ago".to_owned()
713 } else {
714 format!("{h} hours ago")
715 }
716 } else if diff < 86400 * 30 {
717 let d = diff / 86400;
718 if d == 1 {
719 "1 day ago".to_owned()
720 } else {
721 format!("{d} days ago")
722 }
723 } else if diff < 86400 * 365 {
724 let months = diff / (86400 * 30);
725 if months == 1 {
726 "1 month ago".to_owned()
727 } else {
728 format!("{months} months ago")
729 }
730 } else {
731 let years = diff / (86400 * 365);
732 if years == 1 {
733 "1 year ago".to_owned()
734 } else {
735 format!("{years} years ago")
736 }
737 }
738}
739
740pub fn render_commit(
746 repo: &Repository,
747 oid: ObjectId,
748 mode: &OutputMode,
749 abbrev_len: usize,
750) -> Result<String> {
751 render_commit_with_color(repo, oid, mode, abbrev_len, false)
752}
753
754pub fn render_commit_with_color(
756 repo: &Repository,
757 oid: ObjectId,
758 mode: &OutputMode,
759 abbrev_len: usize,
760 use_color: bool,
761) -> Result<String> {
762 match mode {
763 OutputMode::OidOnly => Ok(format!("{oid}")),
764 OutputMode::Parents => {
765 let mut out = format!("{oid}");
766 let commit = load_commit(repo, oid)?;
767 for parent in commit.parents {
768 out.push(' ');
769 out.push_str(&parent.to_hex());
770 }
771 Ok(out)
772 }
773 OutputMode::Format(fmt) => {
774 let commit = load_commit(repo, oid)?;
775 let subject = commit.message.lines().next().unwrap_or_default();
776 let hex = oid.to_hex();
777
778 match fmt.as_str() {
780 "oneline" => {
781 return Ok(format!("{} {}", hex, subject));
782 }
783 "short" => {
784 fn fmt_ident(ident: &str) -> String {
785 let name = if let Some(bracket) = ident.find('<') {
786 ident[..bracket].trim()
787 } else {
788 ident.trim()
789 };
790 let email = if let Some(start) = ident.find('<') {
791 if let Some(end) = ident.find('>') {
792 &ident[start..=end]
793 } else {
794 ""
795 }
796 } else {
797 ""
798 };
799 format!("{} {}", name, email)
800 }
801 let mut out = String::new();
802 out.push_str(&format!("Author: {}\n", fmt_ident(&commit.author)));
803 out.push('\n');
804 out.push_str(&format!(" {}\n", subject));
805 out.push('\n');
806 return Ok(out);
807 }
808 "medium" => {
809 fn extract_ident_display(ident: &str) -> String {
810 let name = if let Some(bracket) = ident.find('<') {
811 ident[..bracket].trim()
812 } else {
813 ident.trim()
814 };
815 let email = if let Some(start) = ident.find('<') {
816 if let Some(end) = ident.find('>') {
817 &ident[start..=end]
818 } else {
819 ""
820 }
821 } else {
822 ""
823 };
824 format!("{} {}", name, email)
825 }
826 fn format_default_date(ident: &str) -> String {
827 let parts: Vec<&str> = ident.rsplitn(3, ' ').collect();
828 if parts.len() < 2 {
829 return String::new();
830 }
831 let ts_str = parts[1];
832 let offset_str = parts[0];
833 let ts: i64 = match ts_str.parse() {
834 Ok(v) => v,
835 Err(_) => return format!("{ts_str} {offset_str}"),
836 };
837 let tz_bytes = offset_str.as_bytes();
838 let tz_secs: i64 = if tz_bytes.len() >= 5 {
839 let sign = if tz_bytes[0] == b'-' { -1i64 } else { 1i64 };
840 let h: i64 = offset_str[1..3].parse().unwrap_or(0);
841 let m: i64 = offset_str[3..5].parse().unwrap_or(0);
842 sign * (h * 3600 + m * 60)
843 } else {
844 0
845 };
846 let adjusted = ts + tz_secs;
847 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
848 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
849 let weekday = match dt.weekday() {
850 time::Weekday::Monday => "Mon",
851 time::Weekday::Tuesday => "Tue",
852 time::Weekday::Wednesday => "Wed",
853 time::Weekday::Thursday => "Thu",
854 time::Weekday::Friday => "Fri",
855 time::Weekday::Saturday => "Sat",
856 time::Weekday::Sunday => "Sun",
857 };
858 let month = match dt.month() {
859 time::Month::January => "Jan",
860 time::Month::February => "Feb",
861 time::Month::March => "Mar",
862 time::Month::April => "Apr",
863 time::Month::May => "May",
864 time::Month::June => "Jun",
865 time::Month::July => "Jul",
866 time::Month::August => "Aug",
867 time::Month::September => "Sep",
868 time::Month::October => "Oct",
869 time::Month::November => "Nov",
870 time::Month::December => "Dec",
871 };
872 format!(
873 "{} {} {} {:02}:{:02}:{:02} {} {}",
874 weekday,
875 month,
876 dt.day(),
877 dt.hour(),
878 dt.minute(),
879 dt.second(),
880 dt.year(),
881 offset_str
882 )
883 }
884 let mut out = String::new();
885 out.push_str(&format!(
886 "Author: {}\n",
887 extract_ident_display(&commit.author)
888 ));
889 out.push_str(&format!(
890 "Date: {}\n",
891 format_default_date(&commit.author)
892 ));
893 out.push('\n');
894 for line in commit.message.lines() {
895 out.push_str(&format!(" {}\n", line));
896 }
897 return Ok(out);
898 }
899 _ => {}
900 }
901
902 let raw_fmt = if let Some(t) = fmt.strip_prefix("format:") {
903 t
904 } else if let Some(t) = fmt.strip_prefix("tformat:") {
905 t
906 } else {
907 fmt.as_str()
908 };
909 let body = {
911 let mut lines = commit.message.lines();
912 lines.next(); if let Some(blank) = lines.next() {
915 if blank.is_empty() {
916 lines.collect::<Vec<_>>().join("\n")
917 } else {
918 std::iter::once(blank)
919 .chain(lines)
920 .collect::<Vec<_>>()
921 .join("\n")
922 }
923 } else {
924 String::new()
925 }
926 };
927 let tree_hex = commit.tree.to_hex();
928 let parent_hexes: Vec<String> = commit.parents.iter().map(|p| p.to_hex()).collect();
929 let parent_abbrevs: Vec<String> = commit
930 .parents
931 .iter()
932 .map(|p| {
933 let hex = p.to_hex();
934 let n = abbrev_len.clamp(4, 40).min(hex.len());
935 hex[..n].to_string()
936 })
937 .collect();
938
939 fn extract_name(ident: &str) -> &str {
941 if let Some(bracket) = ident.find('<') {
942 ident[..bracket].trim()
943 } else {
944 ident.trim()
945 }
946 }
947 fn extract_email(ident: &str) -> &str {
948 if let Some(start) = ident.find('<') {
949 if let Some(end) = ident.find('>') {
950 return &ident[start + 1..end];
951 }
952 }
953 ""
954 }
955 fn extract_timestamp(ident: &str) -> &str {
956 let parts: Vec<&str> = ident.rsplitn(3, ' ').collect();
957 if parts.len() >= 2 {
958 parts[1]
959 } else {
960 ""
961 }
962 }
963 fn parse_ident_date(ident: &str) -> Option<(i64, &str)> {
964 let parts: Vec<&str> = ident.rsplitn(3, ' ').collect();
965 if parts.len() < 2 {
966 return None;
967 }
968 let ts: i64 = parts[1].parse().ok()?;
969 Some((ts, parts[0]))
970 }
971 fn parse_tz(offset_str: &str) -> i64 {
972 let tz_bytes = offset_str.as_bytes();
973 if tz_bytes.len() >= 5 {
974 let sign = if tz_bytes[0] == b'-' { -1i64 } else { 1i64 };
975 let h: i64 = offset_str[1..3].parse().unwrap_or(0);
976 let m: i64 = offset_str[3..5].parse().unwrap_or(0);
977 sign * (h * 3600 + m * 60)
978 } else {
979 0
980 }
981 }
982 fn weekday_str(dt: &time::OffsetDateTime) -> &'static str {
983 match dt.weekday() {
984 time::Weekday::Monday => "Mon",
985 time::Weekday::Tuesday => "Tue",
986 time::Weekday::Wednesday => "Wed",
987 time::Weekday::Thursday => "Thu",
988 time::Weekday::Friday => "Fri",
989 time::Weekday::Saturday => "Sat",
990 time::Weekday::Sunday => "Sun",
991 }
992 }
993 fn month_str(dt: &time::OffsetDateTime) -> &'static str {
994 match dt.month() {
995 time::Month::January => "Jan",
996 time::Month::February => "Feb",
997 time::Month::March => "Mar",
998 time::Month::April => "Apr",
999 time::Month::May => "May",
1000 time::Month::June => "Jun",
1001 time::Month::July => "Jul",
1002 time::Month::August => "Aug",
1003 time::Month::September => "Sep",
1004 time::Month::October => "Oct",
1005 time::Month::November => "Nov",
1006 time::Month::December => "Dec",
1007 }
1008 }
1009 fn extract_email_local(ident: &str) -> &str {
1010 let email = extract_email(ident);
1011 if let Some(at) = email.find('@') {
1012 &email[..at]
1013 } else {
1014 email
1015 }
1016 }
1017 fn extract_date_default(ident: &str) -> String {
1018 let Some((ts, offset_str)) = parse_ident_date(ident) else {
1019 return String::new();
1020 };
1021 let adjusted = ts + parse_tz(offset_str);
1022 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
1023 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
1024 format!(
1025 "{} {} {} {:02}:{:02}:{:02} {} {}",
1026 weekday_str(&dt),
1027 month_str(&dt),
1028 dt.day(),
1029 dt.hour(),
1030 dt.minute(),
1031 dt.second(),
1032 dt.year(),
1033 offset_str
1034 )
1035 }
1036 fn extract_date_rfc2822(ident: &str) -> String {
1037 let Some((ts, offset_str)) = parse_ident_date(ident) else {
1038 return String::new();
1039 };
1040 let adjusted = ts + parse_tz(offset_str);
1041 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
1042 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
1043 format!(
1044 "{}, {} {} {} {:02}:{:02}:{:02} {}",
1045 weekday_str(&dt),
1046 dt.day(),
1047 month_str(&dt),
1048 dt.year(),
1049 dt.hour(),
1050 dt.minute(),
1051 dt.second(),
1052 offset_str
1053 )
1054 }
1055 fn extract_date_short(ident: &str) -> String {
1056 let Some((ts, offset_str)) = parse_ident_date(ident) else {
1057 return String::new();
1058 };
1059 let adjusted = ts + parse_tz(offset_str);
1060 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
1061 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
1062 format!("{:04}-{:02}-{:02}", dt.year(), dt.month() as u8, dt.day())
1063 }
1064 fn extract_date_iso(ident: &str) -> String {
1065 let Some((ts, offset_str)) = parse_ident_date(ident) else {
1066 return String::new();
1067 };
1068 let adjusted = ts + parse_tz(offset_str);
1069 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
1070 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
1071 format!(
1072 "{:04}-{:02}-{:02} {:02}:{:02}:{:02} {}",
1073 dt.year(),
1074 dt.month() as u8,
1075 dt.day(),
1076 dt.hour(),
1077 dt.minute(),
1078 dt.second(),
1079 offset_str
1080 )
1081 }
1082
1083 #[derive(Clone, Copy)]
1085 enum Align {
1086 Left,
1087 Right,
1088 Center,
1089 }
1090 #[derive(Clone, Copy)]
1091 enum Trunc {
1092 None,
1093 Trunc,
1094 LTrunc,
1095 MTrunc,
1096 }
1097 struct ColSpec {
1098 width: usize,
1099 align: Align,
1100 trunc: Trunc,
1101 }
1102 fn apply_col(spec: &ColSpec, s: &str) -> String {
1103 let char_len = s.chars().count();
1104 if char_len > spec.width {
1105 match spec.trunc {
1106 Trunc::None => s.to_owned(),
1107 Trunc::Trunc => {
1108 let mut out: String =
1109 s.chars().take(spec.width.saturating_sub(2)).collect();
1110 out.push_str("..");
1111 out
1112 }
1113 Trunc::LTrunc => {
1114 let skip = char_len - spec.width + 2;
1115 let mut out = String::from("..");
1116 out.extend(s.chars().skip(skip));
1117 out
1118 }
1119 Trunc::MTrunc => {
1120 let keep = spec.width.saturating_sub(2);
1121 let left_half = keep / 2;
1122 let right_half = keep - left_half;
1123 let mut out: String = s.chars().take(left_half).collect();
1124 out.push_str("..");
1125 out.extend(s.chars().skip(char_len - right_half));
1126 out
1127 }
1128 }
1129 } else {
1130 let pad = spec.width - char_len;
1131 match spec.align {
1132 Align::Left => {
1133 let mut out = s.to_owned();
1134 for _ in 0..pad {
1135 out.push(' ');
1136 }
1137 out
1138 }
1139 Align::Right => {
1140 let mut out = String::new();
1141 for _ in 0..pad {
1142 out.push(' ');
1143 }
1144 out.push_str(s);
1145 out
1146 }
1147 Align::Center => {
1148 let left = pad / 2;
1149 let right = pad - left;
1150 let mut out = String::new();
1151 for _ in 0..left {
1152 out.push(' ');
1153 }
1154 out.push_str(s);
1155 for _ in 0..right {
1156 out.push(' ');
1157 }
1158 out
1159 }
1160 }
1161 }
1162 }
1163 fn parse_col_spec(
1164 chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
1165 align: Align,
1166 ) -> Option<ColSpec> {
1167 if chars.peek() != Some(&'(') {
1169 return None;
1170 }
1171 chars.next();
1172 let mut num_str = String::new();
1173 while let Some(&c) = chars.peek() {
1174 if c.is_ascii_digit() {
1175 num_str.push(c);
1176 chars.next();
1177 } else {
1178 break;
1179 }
1180 }
1181 let width: usize = num_str.parse().ok()?;
1182 let trunc = if chars.peek() == Some(&',') {
1183 chars.next(); let mut mode = String::new();
1185 while let Some(&c) = chars.peek() {
1186 if c == ')' {
1187 break;
1188 }
1189 mode.push(c);
1190 chars.next();
1191 }
1192 match mode.as_str() {
1193 "trunc" => Trunc::Trunc,
1194 "ltrunc" => Trunc::LTrunc,
1195 "mtrunc" => Trunc::MTrunc,
1196 _ => Trunc::None,
1197 }
1198 } else {
1199 Trunc::None
1200 };
1201 if chars.peek() == Some(&')') {
1203 chars.next();
1204 }
1205 Some(ColSpec {
1206 width,
1207 align,
1208 trunc,
1209 })
1210 }
1211
1212 let mut pending_col: Option<ColSpec> = None;
1213 let mut rendered = String::new();
1214 let mut chars = raw_fmt.chars().peekable();
1215 while let Some(ch) = chars.next() {
1216 if ch != '%' {
1217 rendered.push(ch);
1218 continue;
1219 }
1220 if chars.peek() == Some(&'<') {
1222 chars.next();
1223 if let Some(spec) = parse_col_spec(&mut chars, Align::Left) {
1224 pending_col = Some(spec);
1225 }
1226 continue;
1227 }
1228 if chars.peek() == Some(&'>') {
1229 chars.next();
1230 if chars.peek() == Some(&'<') {
1231 chars.next(); if let Some(spec) = parse_col_spec(&mut chars, Align::Center) {
1233 pending_col = Some(spec);
1234 }
1235 } else if chars.peek() == Some(&'>') {
1236 chars.next(); if let Some(spec) = parse_col_spec(&mut chars, Align::Right) {
1238 pending_col = Some(spec);
1239 }
1240 } else if let Some(spec) = parse_col_spec(&mut chars, Align::Right) {
1241 pending_col = Some(spec);
1242 }
1243 continue;
1244 }
1245
1246 let mut expanded = String::new();
1248 let target = if pending_col.is_some() {
1249 &mut expanded
1250 } else {
1251 &mut rendered
1252 };
1253 match chars.peek() {
1254 Some('%') => {
1255 chars.next();
1256 target.push('%');
1257 }
1258 Some('H') => {
1259 chars.next();
1260 target.push_str(&oid.to_hex());
1261 }
1262 Some('h') => {
1263 chars.next();
1264 let hex = oid.to_hex();
1265 let n = abbrev_len.clamp(4, 40).min(hex.len());
1266 target.push_str(&hex[..n]);
1267 }
1268 Some('T') => {
1269 chars.next();
1270 target.push_str(&tree_hex);
1271 }
1272 Some('t') => {
1273 chars.next();
1274 let n = abbrev_len.clamp(4, 40).min(tree_hex.len());
1275 target.push_str(&tree_hex[..n]);
1276 }
1277 Some('P') => {
1278 chars.next();
1279 target.push_str(&parent_hexes.join(" "));
1280 }
1281 Some('p') => {
1282 chars.next();
1283 target.push_str(&parent_abbrevs.join(" "));
1284 }
1285 Some('n') => {
1286 chars.next();
1287 target.push('\n');
1288 }
1289 Some('s') => {
1290 chars.next();
1291 target.push_str(subject);
1292 }
1293 Some('b') => {
1294 chars.next();
1295 target.push_str(&body);
1296 if !body.is_empty() {
1297 target.push('\n');
1298 }
1299 }
1300 Some('B') => {
1301 chars.next();
1302 target.push_str(&commit.message);
1303 }
1304 Some('a') => {
1305 chars.next();
1306 match chars.next() {
1307 Some('n') => target.push_str(extract_name(&commit.author)),
1308 Some('N') => target.push_str(extract_name(&commit.author)),
1309 Some('e') => target.push_str(extract_email(&commit.author)),
1310 Some('E') => target.push_str(extract_email(&commit.author)),
1311 Some('l') => target.push_str(extract_email_local(&commit.author)),
1312 Some('d') => target.push_str(&extract_date_default(&commit.author)),
1313 Some('D') => target.push_str(&extract_date_rfc2822(&commit.author)),
1314 Some('t') => target.push_str(extract_timestamp(&commit.author)),
1315 Some('s') => target.push_str(&extract_date_short(&commit.author)),
1316 Some('i') => target.push_str(&extract_date_iso(&commit.author)),
1317 Some('I') => {
1318 let Some((ts, offset_str)) = parse_ident_date(&commit.author)
1319 else {
1320 break;
1321 };
1322 let adjusted = ts + parse_tz(offset_str);
1323 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
1324 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
1325 let sign_ch = if parse_tz(offset_str) >= 0 { '+' } else { '-' };
1326 let abs_off = parse_tz(offset_str).unsigned_abs();
1327 target.push_str(&format!(
1328 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}{:02}:{:02}",
1329 dt.year(),
1330 dt.month() as u8,
1331 dt.day(),
1332 dt.hour(),
1333 dt.minute(),
1334 dt.second(),
1335 sign_ch,
1336 abs_off / 3600,
1337 (abs_off % 3600) / 60
1338 ));
1339 }
1340 Some('r') => {
1341 let Some((ts, _)) = parse_ident_date(&commit.author) else {
1342 break;
1343 };
1344 let now = std::time::SystemTime::now()
1345 .duration_since(std::time::UNIX_EPOCH)
1346 .unwrap_or_default()
1347 .as_secs() as i64;
1348 target.push_str(&format_relative_date(now - ts));
1349 }
1350 Some(other) => {
1351 target.push('%');
1352 target.push('a');
1353 target.push(other);
1354 }
1355 None => {
1356 target.push('%');
1357 target.push('a');
1358 }
1359 }
1360 }
1361 Some('c') => {
1362 chars.next();
1363 match chars.next() {
1364 Some('n') => target.push_str(extract_name(&commit.committer)),
1365 Some('N') => target.push_str(extract_name(&commit.committer)),
1366 Some('e') => target.push_str(extract_email(&commit.committer)),
1367 Some('E') => target.push_str(extract_email(&commit.committer)),
1368 Some('l') => target.push_str(extract_email_local(&commit.committer)),
1369 Some('d') => target.push_str(&extract_date_default(&commit.committer)),
1370 Some('D') => target.push_str(&extract_date_rfc2822(&commit.committer)),
1371 Some('t') => target.push_str(extract_timestamp(&commit.committer)),
1372 Some('s') => target.push_str(&extract_date_short(&commit.committer)),
1373 Some('i') => target.push_str(&extract_date_iso(&commit.committer)),
1374 Some('I') => {
1375 let Some((ts, offset_str)) = parse_ident_date(&commit.committer)
1376 else {
1377 break;
1378 };
1379 let adjusted = ts + parse_tz(offset_str);
1380 let dt = time::OffsetDateTime::from_unix_timestamp(adjusted)
1381 .unwrap_or(time::OffsetDateTime::UNIX_EPOCH);
1382 let sign_ch = if parse_tz(offset_str) >= 0 { '+' } else { '-' };
1383 let abs_off = parse_tz(offset_str).unsigned_abs();
1384 target.push_str(&format!(
1385 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}{:02}:{:02}",
1386 dt.year(),
1387 dt.month() as u8,
1388 dt.day(),
1389 dt.hour(),
1390 dt.minute(),
1391 dt.second(),
1392 sign_ch,
1393 abs_off / 3600,
1394 (abs_off % 3600) / 60
1395 ));
1396 }
1397 Some('r') => {
1398 let Some((ts, _)) = parse_ident_date(&commit.committer) else {
1399 break;
1400 };
1401 let now = std::time::SystemTime::now()
1402 .duration_since(std::time::UNIX_EPOCH)
1403 .unwrap_or_default()
1404 .as_secs() as i64;
1405 target.push_str(&format_relative_date(now - ts));
1406 }
1407 Some(other) => {
1408 target.push('%');
1409 target.push('c');
1410 target.push(other);
1411 }
1412 None => {
1413 target.push('%');
1414 target.push('c');
1415 }
1416 }
1417 }
1418 Some('x') => {
1419 chars.next();
1421 let mut hex_str = String::new();
1422 if let Some(&c1) = chars.peek() {
1423 if c1.is_ascii_hexdigit() {
1424 hex_str.push(c1);
1425 chars.next();
1426 }
1427 }
1428 if let Some(&c2) = chars.peek() {
1429 if c2.is_ascii_hexdigit() {
1430 hex_str.push(c2);
1431 chars.next();
1432 }
1433 }
1434 if let Ok(byte) = u8::from_str_radix(&hex_str, 16) {
1435 target.push(byte as char);
1436 }
1437 }
1438 Some('C') => {
1439 chars.next();
1440 if chars.peek() == Some(&'(') {
1441 chars.next();
1442 let mut spec = String::new();
1443 for c in chars.by_ref() {
1444 if c == ')' {
1445 break;
1446 }
1447 spec.push(c);
1448 }
1449 let (force, color_spec) =
1450 if let Some(rest) = spec.strip_prefix("always,") {
1451 (true, rest)
1452 } else if let Some(rest) = spec.strip_prefix("auto,") {
1453 (false, rest)
1454 } else if spec == "auto" {
1455 if use_color {
1456 target.push_str("\x1b[m");
1457 }
1458 continue;
1459 } else {
1460 (false, spec.as_str())
1461 };
1462 if use_color || force {
1463 target.push_str(&ansi_color_from_spec(color_spec));
1464 }
1465 } else {
1466 let remaining: String = chars.clone().collect();
1469 let known = [
1470 "reset", "red", "green", "blue", "yellow", "magenta", "cyan",
1471 "white", "bold", "dim", "ul",
1472 ];
1473 let mut matched = false;
1474 for name in &known {
1475 if remaining.starts_with(name) {
1476 for _ in 0..name.len() {
1477 chars.next();
1478 }
1479 if use_color {
1480 target.push_str(&ansi_color_from_name(name));
1481 }
1482 matched = true;
1483 break;
1484 }
1485 }
1486 if !matched {
1487 while let Some(&c) = chars.peek() {
1489 if c.is_alphanumeric() {
1490 chars.next();
1491 } else {
1492 break;
1493 }
1494 }
1495 }
1496 }
1497 }
1498 Some('w') => {
1499 chars.next();
1501 if chars.peek() == Some(&'(') {
1502 chars.next();
1503 for c in chars.by_ref() {
1504 if c == ')' {
1505 break;
1506 }
1507 }
1508 }
1509 }
1510 Some('+') => {
1511 chars.next();
1513 if chars.peek() == Some(&'%') {
1515 }
1519 let mut sub = String::new();
1521 if let Some(&nc) = chars.peek() {
1522 match nc {
1523 'b' => {
1524 chars.next();
1525 sub.push_str(&body);
1526 if !body.is_empty() {
1527 sub.push('\n');
1528 }
1529 }
1530 's' => {
1531 chars.next();
1532 sub.push_str(subject);
1533 }
1534 _ => {
1535 chars.next();
1536 sub.push('%');
1537 sub.push('+');
1538 sub.push(nc);
1539 }
1540 }
1541 }
1542 if !sub.is_empty() {
1543 target.push('\n');
1544 target.push_str(&sub);
1545 }
1546 }
1547 Some('-') => {
1548 chars.next();
1550 if let Some(&nc) = chars.peek() {
1552 match nc {
1553 'b' => {
1554 chars.next();
1555 if !body.is_empty() {
1556 target.push_str(&body);
1557 target.push('\n');
1558 }
1559 }
1560 's' => {
1561 chars.next();
1562 target.push_str(subject);
1563 }
1564 _ => {
1565 chars.next();
1566 target.push('%');
1567 target.push('-');
1568 target.push(nc);
1569 }
1570 }
1571 }
1572 }
1573 Some('d') => {
1574 chars.next();
1576 }
1577 Some('D') => {
1578 chars.next();
1580 }
1581 Some('e') => {
1582 chars.next();
1584 }
1585 Some('g') => {
1586 chars.next();
1588 if let Some(&_nc) = chars.peek() {
1589 chars.next(); }
1592 }
1593 Some(&other) => {
1594 chars.next();
1595 target.push('%');
1596 target.push(other);
1597 }
1598 None => target.push('%'),
1599 }
1600 if let Some(spec) = pending_col.take() {
1602 let formatted = apply_col(&spec, &expanded);
1603 rendered.push_str(&formatted);
1604 }
1605 }
1606 Ok(rendered)
1607 }
1608 }
1609}
1610
1611#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1612enum ExpectedObjectKind {
1613 Commit,
1614 Tree,
1615 Blob,
1616}
1617
1618impl ExpectedObjectKind {
1619 fn from_tag_type(kind: &str) -> Option<Self> {
1620 match kind {
1621 "commit" => Some(Self::Commit),
1622 "tree" => Some(Self::Tree),
1623 "blob" => Some(Self::Blob),
1624 _ => None,
1625 }
1626 }
1627
1628 fn matches(self, kind: ObjectKind) -> bool {
1629 matches!(
1630 (self, kind),
1631 (Self::Commit, ObjectKind::Commit)
1632 | (Self::Tree, ObjectKind::Tree)
1633 | (Self::Blob, ObjectKind::Blob)
1634 )
1635 }
1636
1637 fn as_str(self) -> &'static str {
1638 match self {
1639 Self::Commit => "commit",
1640 Self::Tree => "tree",
1641 Self::Blob => "blob",
1642 }
1643 }
1644}
1645
1646#[derive(Clone, Debug)]
1647struct RootObject {
1648 oid: ObjectId,
1649 input: String,
1650 expected_kind: Option<ExpectedObjectKind>,
1651}
1652
1653fn resolve_specs(repo: &Repository, specs: &[String]) -> Result<Vec<ObjectId>> {
1654 let mut out = Vec::with_capacity(specs.len());
1655 for spec in specs {
1656 let oid = resolve_revision(repo, spec)?;
1657 let commit_oid = peel_to_commit(repo, oid)?;
1658 out.push(commit_oid);
1659 }
1660 Ok(out)
1661}
1662
1663fn resolve_specs_for_objects(
1664 repo: &Repository,
1665 specs: &[String],
1666) -> Result<(Vec<ObjectId>, Vec<RootObject>)> {
1667 let mut commits = Vec::new();
1668 let mut roots = Vec::new();
1669
1670 for spec in specs {
1671 if let Ok(raw_oid) = spec.parse::<ObjectId>() {
1672 let raw_object = repo.odb.read(&raw_oid)?;
1673 match raw_object.kind {
1674 ObjectKind::Commit => {
1675 commits.push(raw_oid);
1676 }
1677 ObjectKind::Tag => {
1678 let tag = parse_tag(&raw_object.data)?;
1679 let expected_kind = ExpectedObjectKind::from_tag_type(&tag.object_type)
1680 .ok_or_else(|| {
1681 Error::CorruptObject(format!(
1682 "object {spec} has unsupported tag type '{}'",
1683 tag.object_type
1684 ))
1685 })?;
1686 roots.push(RootObject {
1687 oid: tag.object,
1688 input: spec.clone(),
1689 expected_kind: Some(expected_kind),
1690 });
1691 }
1692 ObjectKind::Tree | ObjectKind::Blob => roots.push(RootObject {
1693 oid: raw_oid,
1694 input: spec.clone(),
1695 expected_kind: None,
1696 }),
1697 }
1698 continue;
1699 }
1700
1701 let oid = resolve_revision(repo, spec)?;
1702 match peel_to_commit(repo, oid) {
1703 Ok(commit_oid) => commits.push(commit_oid),
1704 Err(Error::CorruptObject(_)) | Err(Error::ObjectNotFound(_)) => {
1705 roots.push(RootObject {
1706 oid,
1707 input: spec.clone(),
1708 expected_kind: None,
1709 })
1710 }
1711 Err(err) => return Err(err),
1712 }
1713 }
1714
1715 Ok((commits, roots))
1716}
1717
1718fn peel_to_commit(repo: &Repository, mut oid: ObjectId) -> Result<ObjectId> {
1720 loop {
1721 let object = repo.odb.read(&oid)?;
1722 match object.kind {
1723 ObjectKind::Commit => return Ok(oid),
1724 ObjectKind::Tag => {
1725 let tag = parse_tag(&object.data)?;
1726 oid = tag.object;
1727 }
1728 other => {
1729 return Err(Error::CorruptObject(format!(
1730 "object {oid} is a {other:?}, not a commit"
1731 )));
1732 }
1733 }
1734 }
1735}
1736
1737fn all_ref_tips(repo: &Repository) -> Result<Vec<ObjectId>> {
1738 let mut raw = Vec::new();
1739 if let Ok(head) = refs::resolve_ref(&repo.git_dir, "HEAD") {
1740 raw.push(head);
1741 }
1742 raw.extend(
1743 refs::list_refs(&repo.git_dir, "refs/")?
1744 .into_iter()
1745 .map(|(_, oid)| oid),
1746 );
1747 let mut out = Vec::new();
1749 let mut seen = HashSet::new();
1750 for oid in raw {
1751 match peel_to_commit(repo, oid) {
1752 Ok(commit_oid) if seen.insert(commit_oid) => out.push(commit_oid),
1753 Err(_) => {} _ => {}
1755 }
1756 }
1757 out.sort();
1758 Ok(out)
1759}
1760
1761fn walk_closure(graph: &mut CommitGraph<'_>, starts: &[ObjectId]) -> Result<HashSet<ObjectId>> {
1762 let (seen, _) = walk_closure_ordered(graph, starts)?;
1763 Ok(seen)
1764}
1765
1766fn walk_closure_ordered(
1768 graph: &mut CommitGraph<'_>,
1769 starts: &[ObjectId],
1770) -> Result<(HashSet<ObjectId>, Vec<ObjectId>)> {
1771 let mut seen = HashSet::new();
1772 let mut order = Vec::new();
1773 let mut queue = VecDeque::new();
1774 for &start in starts {
1775 queue.push_back(start);
1776 }
1777 while let Some(oid) = queue.pop_front() {
1778 if !seen.insert(oid) {
1779 continue;
1780 }
1781 order.push(oid);
1782 for parent in graph.parents_of(oid)? {
1783 queue.push_back(parent);
1784 }
1785 }
1786 Ok((seen, order))
1787}
1788
1789fn sort_by_commit_date_desc(
1790 graph: &mut CommitGraph<'_>,
1791 selected: &HashSet<ObjectId>,
1792 discovery_order: &[ObjectId],
1793) -> Result<Vec<ObjectId>> {
1794 let disc_idx: HashMap<ObjectId, usize> = discovery_order
1796 .iter()
1797 .enumerate()
1798 .map(|(i, oid)| (*oid, i))
1799 .collect();
1800 let mut out: Vec<ObjectId> = selected.iter().copied().collect();
1801 out.sort_by(|a, b| {
1802 match graph.committer_time(*b).cmp(&graph.committer_time(*a)) {
1803 Ordering::Equal => {
1804 let da = disc_idx.get(a).copied().unwrap_or(usize::MAX);
1806 let db = disc_idx.get(b).copied().unwrap_or(usize::MAX);
1807 da.cmp(&db)
1808 }
1809 other => other,
1810 }
1811 });
1812 Ok(out)
1813}
1814
1815fn topo_sort(graph: &mut CommitGraph<'_>, selected: &HashSet<ObjectId>) -> Result<Vec<ObjectId>> {
1816 let mut child_count: HashMap<ObjectId, usize> = selected.iter().map(|&oid| (oid, 0)).collect();
1817
1818 for &oid in selected {
1819 for parent in graph.parents_of(oid)? {
1820 if !selected.contains(&parent) {
1821 continue;
1822 }
1823 if let Some(count) = child_count.get_mut(&parent) {
1824 *count += 1;
1825 }
1826 }
1827 }
1828
1829 let mut ready = BinaryHeap::new();
1830 for (&oid, &count) in &child_count {
1831 if count == 0 {
1832 ready.push(CommitDateKey {
1833 oid,
1834 date: graph.committer_time(oid),
1835 });
1836 }
1837 }
1838
1839 let mut out = Vec::with_capacity(selected.len());
1840 while let Some(item) = ready.pop() {
1841 let oid = item.oid;
1842 out.push(oid);
1843 for parent in graph.parents_of(oid)? {
1844 if !selected.contains(&parent) {
1845 continue;
1846 }
1847 if let Some(count) = child_count.get_mut(&parent) {
1848 *count = count.saturating_sub(1);
1849 if *count == 0 {
1850 ready.push(CommitDateKey {
1851 oid: parent,
1852 date: graph.committer_time(parent),
1853 });
1854 }
1855 }
1856 }
1857 }
1858
1859 Ok(out)
1860}
1861
1862fn limit_to_ancestry(
1863 graph: &mut CommitGraph<'_>,
1864 selected: &mut HashSet<ObjectId>,
1865 bottoms: &[ObjectId],
1866) -> Result<()> {
1867 let mut keep = HashSet::new();
1868 for &bottom in bottoms {
1869 let ancestors = walk_closure(graph, &[bottom])?;
1870 keep.extend(
1871 ancestors
1872 .iter()
1873 .copied()
1874 .filter(|oid| selected.contains(oid)),
1875 );
1876
1877 for &candidate in selected.iter() {
1878 if candidate == bottom {
1879 keep.insert(candidate);
1880 continue;
1881 }
1882 let closure = walk_closure(graph, &[candidate])?;
1883 if closure.contains(&bottom) {
1884 keep.insert(candidate);
1885 }
1886 }
1887 }
1888 selected.retain(|oid| keep.contains(oid));
1889 Ok(())
1890}
1891
1892fn commit_touches_paths(
1894 repo: &Repository,
1895 graph: &mut CommitGraph<'_>,
1896 oid: ObjectId,
1897 paths: &[String],
1898 full_history: bool,
1899 sparse: bool,
1900) -> Result<bool> {
1901 let commit = load_commit(repo, oid)?;
1902 let parents = graph.parents_of(oid)?;
1903 let commit_entries = flatten_tree(repo, commit.tree, "")?;
1904 let commit_map: HashMap<String, ObjectId> = commit_entries.into_iter().collect();
1905
1906 if parents.is_empty() {
1908 if sparse {
1909 return Ok(true);
1910 }
1911 return Ok(commit_map
1912 .keys()
1913 .any(|path| paths.iter().any(|spec| pathspec_matches(spec, path))));
1914 }
1915
1916 if parents.len() == 1 {
1918 let parent = load_commit(repo, parents[0])?;
1919 let parent_map: HashMap<String, ObjectId> =
1920 flatten_tree(repo, parent.tree, "")?.into_iter().collect();
1921 let differs = path_differs_for_specs(&commit_map, &parent_map, paths);
1922 if differs {
1923 return Ok(true);
1924 }
1925 if sparse {
1926 return Ok(true);
1927 }
1928 return Ok(false);
1929 }
1930
1931 let mut treesame_parents = 0usize;
1935 let mut differs_any = false;
1936 for parent_oid in &parents {
1937 let parent = load_commit(repo, *parent_oid)?;
1938 let parent_map: HashMap<String, ObjectId> =
1939 flatten_tree(repo, parent.tree, "")?.into_iter().collect();
1940 let differs = path_differs_for_specs(&commit_map, &parent_map, paths);
1941 if differs {
1942 differs_any = true;
1943 } else {
1944 treesame_parents += 1;
1945 }
1946 }
1947
1948 if !full_history && treesame_parents == 1 {
1949 return Ok(false);
1950 }
1951
1952 if differs_any {
1953 return Ok(true);
1954 }
1955
1956 Ok(sparse)
1957}
1958
1959fn path_differs_for_specs(
1960 current: &HashMap<String, ObjectId>,
1961 parent: &HashMap<String, ObjectId>,
1962 specs: &[String],
1963) -> bool {
1964 let mut paths = std::collections::BTreeSet::new();
1965 paths.extend(current.keys().cloned());
1966 paths.extend(parent.keys().cloned());
1967
1968 for path in &paths {
1969 if !specs.iter().any(|spec| pathspec_matches(spec, path)) {
1970 continue;
1971 }
1972 if current.get(path) != parent.get(path) {
1973 return true;
1974 }
1975 }
1976 false
1977}
1978
1979fn pathspec_matches(spec: &str, path: &str) -> bool {
1980 let normalized = spec.strip_prefix("./").unwrap_or(spec);
1981 if normalized == "." || normalized.is_empty() {
1982 return true;
1983 }
1984
1985 if normalized.contains('*') || normalized.contains('?') || normalized.contains('[') {
1986 return crate::wildmatch::wildmatch(
1987 normalized.as_bytes(),
1988 path.as_bytes(),
1989 crate::wildmatch::WM_PATHNAME,
1990 );
1991 }
1992
1993 if let Some(prefix) = normalized.strip_suffix('/') {
1994 return path == prefix || path.starts_with(&format!("{prefix}/"));
1995 }
1996
1997 path == normalized || path.starts_with(&format!("{normalized}/"))
1998}
1999
2000fn load_commit(repo: &Repository, oid: ObjectId) -> Result<crate::objects::CommitData> {
2001 let object = repo.odb.read(&oid)?;
2002 if object.kind != ObjectKind::Commit {
2003 return Err(Error::CorruptObject(format!(
2004 "object {oid} is not a commit"
2005 )));
2006 }
2007 parse_commit(&object.data)
2008}
2009
2010fn parse_signature_time(sig: &str) -> i64 {
2011 let mut parts = sig.split_whitespace().collect::<Vec<_>>();
2012 if parts.len() < 2 {
2013 return 0;
2014 }
2015 let ts = parts.remove(parts.len().saturating_sub(2));
2016 ts.parse::<i64>().unwrap_or(0)
2017}
2018
2019pub fn collect_revision_specs_with_stdin(
2027 args_specs: &[String],
2028 read_stdin: bool,
2029) -> Result<(Vec<String>, Vec<String>, bool)> {
2030 let mut positive = Vec::new();
2031 let mut negative = Vec::new();
2032 let mut not_mode = false;
2033
2034 for spec in args_specs {
2035 let (pos, neg) = split_revision_token(spec);
2036 if not_mode {
2037 positive.extend(neg);
2038 negative.extend(pos);
2039 } else {
2040 positive.extend(pos);
2041 negative.extend(neg);
2042 }
2043 }
2044
2045 if !read_stdin {
2046 return Ok((positive, negative, false));
2047 }
2048
2049 let mut in_paths = false;
2050 let mut stdin_all_refs = false;
2051 let stdin = std::io::read_to_string(std::io::stdin()).map_err(Error::Io)?;
2052 for raw_line in stdin.lines() {
2053 let line = raw_line.trim();
2054 if line.is_empty() {
2055 continue;
2056 }
2057 if in_paths {
2058 continue;
2059 }
2060 if line == "--" {
2061 in_paths = true;
2062 continue;
2063 }
2064 if line == "--not" {
2065 not_mode = !not_mode;
2066 continue;
2067 }
2068 if line == "--all" {
2069 stdin_all_refs = true;
2070 continue;
2071 }
2072 if line.starts_with("--") {
2073 return Err(Error::InvalidRef(format!(
2074 "invalid option '{line}' in --stdin mode"
2075 )));
2076 }
2077 if line.starts_with('-') {
2078 return Err(Error::InvalidRef(format!(
2079 "invalid option '{line}' in --stdin mode"
2080 )));
2081 }
2082 let (pos, neg) = split_revision_token(line);
2083 if not_mode {
2084 positive.extend(neg);
2085 negative.extend(pos);
2086 } else {
2087 positive.extend(pos);
2088 negative.extend(neg);
2089 }
2090 }
2091
2092 Ok((positive, negative, stdin_all_refs))
2093}
2094
2095pub fn tag_targets(git_dir: &Path) -> Result<HashSet<ObjectId>> {
2097 Ok(refs::list_refs(git_dir, "refs/tags/")?
2098 .into_iter()
2099 .map(|(_, oid)| oid)
2100 .collect())
2101}
2102
2103struct CommitGraph<'r> {
2104 repo: &'r Repository,
2105 first_parent_only: bool,
2106 parents: HashMap<ObjectId, Vec<ObjectId>>,
2107 committer_time: HashMap<ObjectId, i64>,
2108 shallow_boundaries: HashSet<ObjectId>,
2109 graft_parents: HashMap<ObjectId, Vec<ObjectId>>,
2110}
2111
2112impl<'r> CommitGraph<'r> {
2113 fn new(repo: &'r Repository, first_parent_only: bool) -> Self {
2114 let shallow_boundaries = load_shallow_boundaries(&repo.git_dir);
2115 let graft_parents = load_graft_parents(&repo.git_dir);
2116 Self {
2117 repo,
2118 first_parent_only,
2119 parents: HashMap::new(),
2120 committer_time: HashMap::new(),
2121 shallow_boundaries,
2122 graft_parents,
2123 }
2124 }
2125
2126 fn parents_of(&mut self, oid: ObjectId) -> Result<Vec<ObjectId>> {
2127 self.populate(oid)?;
2128 Ok(self.parents.get(&oid).cloned().unwrap_or_default())
2129 }
2130
2131 fn committer_time(&mut self, oid: ObjectId) -> i64 {
2132 if self.populate(oid).is_err() {
2133 return 0;
2134 }
2135 self.committer_time.get(&oid).copied().unwrap_or(0)
2136 }
2137
2138 fn populate(&mut self, oid: ObjectId) -> Result<()> {
2139 if self.parents.contains_key(&oid) {
2140 return Ok(());
2141 }
2142 let commit = load_commit(self.repo, oid)?;
2143 let mut parents = if self.shallow_boundaries.contains(&oid) {
2145 Vec::new()
2146 } else {
2147 commit.parents
2148 };
2149 if let Some(graft_parents) = self.graft_parents.get(&oid) {
2150 parents = graft_parents.clone();
2151 }
2152 if self.first_parent_only && parents.len() > 1 {
2153 parents.truncate(1);
2154 }
2155 self.committer_time
2156 .insert(oid, parse_signature_time(&commit.committer));
2157 self.parents.insert(oid, parents);
2158 Ok(())
2159 }
2160}
2161
2162fn load_shallow_boundaries(git_dir: &Path) -> HashSet<ObjectId> {
2164 let shallow_path = git_dir.join("shallow");
2165 let mut set = HashSet::new();
2166 if let Ok(contents) = fs::read_to_string(&shallow_path) {
2167 for line in contents.lines() {
2168 let line = line.trim();
2169 if !line.is_empty() {
2170 if let Ok(oid) = line.parse::<ObjectId>() {
2171 set.insert(oid);
2172 }
2173 }
2174 }
2175 }
2176 set
2177}
2178
2179fn load_graft_parents(git_dir: &Path) -> HashMap<ObjectId, Vec<ObjectId>> {
2181 let graft_path = git_dir.join("info/grafts");
2182 let mut grafts = HashMap::new();
2183 let Ok(contents) = fs::read_to_string(&graft_path) else {
2184 return grafts;
2185 };
2186 for raw_line in contents.lines() {
2187 let line = raw_line.trim();
2188 if line.is_empty() || line.starts_with('#') {
2189 continue;
2190 }
2191 let mut fields = line.split_whitespace();
2192 let Some(commit_hex) = fields.next() else {
2193 continue;
2194 };
2195 let Ok(commit_oid) = commit_hex.parse::<ObjectId>() else {
2196 continue;
2197 };
2198 let mut parents = Vec::new();
2199 let mut valid = true;
2200 for parent_hex in fields {
2201 match parent_hex.parse::<ObjectId>() {
2202 Ok(parent_oid) => parents.push(parent_oid),
2203 Err(_) => {
2204 valid = false;
2205 break;
2206 }
2207 }
2208 }
2209 if valid {
2210 grafts.insert(commit_oid, parents);
2211 }
2212 }
2213 grafts
2214}
2215
2216#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2217struct CommitDateKey {
2218 oid: ObjectId,
2219 date: i64,
2220}
2221
2222impl Ord for CommitDateKey {
2223 fn cmp(&self, other: &Self) -> Ordering {
2224 match self.date.cmp(&other.date) {
2225 Ordering::Equal => self.oid.cmp(&other.oid),
2226 ord => ord,
2227 }
2228 }
2229}
2230
2231impl PartialOrd for CommitDateKey {
2232 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2233 Some(self.cmp(other))
2234 }
2235}
2236
2237pub fn read_lines(path: &Path) -> Result<Vec<String>> {
2243 let content = fs::read_to_string(path)?;
2244 Ok(content.lines().map(|line| line.to_owned()).collect())
2245}
2246
2247#[must_use]
2249pub fn is_symmetric_diff(token: &str) -> bool {
2250 token.contains("...") && !token.contains("....")
2251}
2252
2253#[must_use]
2255pub fn split_symmetric_diff(token: &str) -> Option<(String, String)> {
2256 token
2257 .split_once("...")
2258 .map(|(l, r)| (l.to_owned(), r.to_owned()))
2259}
2260
2261#[allow(dead_code)]
2264fn collect_reachable_objects(
2265 repo: &Repository,
2266 _graph: &mut CommitGraph<'_>,
2267 commits: &[ObjectId],
2268 object_roots: &[RootObject],
2269 filter: Option<&ObjectFilter>,
2270 missing_action: MissingAction,
2271) -> Result<(Vec<(ObjectId, String)>, Vec<ObjectId>, Vec<ObjectId>)> {
2272 let mut seen = HashSet::new();
2273 let mut result = Vec::new();
2274 let mut omitted = Vec::new();
2275 let mut missing = Vec::new();
2276 let mut missing_seen = HashSet::new();
2277 for &commit_oid in commits {
2278 let commit = match load_commit(repo, commit_oid) {
2279 Ok(commit) => commit,
2280 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
2281 if missing_seen.insert(commit_oid) && missing_action == MissingAction::Print {
2282 missing.push(commit_oid);
2283 }
2284 continue;
2285 }
2286 Err(err) => return Err(err),
2287 };
2288 collect_tree_objects_filtered(
2289 repo,
2290 commit.tree,
2291 "",
2292 0,
2293 &mut seen,
2294 &mut result,
2295 &mut omitted,
2296 &mut missing,
2297 &mut missing_seen,
2298 filter,
2299 missing_action,
2300 )?;
2301 }
2302
2303 for root in object_roots {
2304 collect_root_object(
2305 repo,
2306 root,
2307 &mut seen,
2308 &mut result,
2309 &mut omitted,
2310 &mut missing,
2311 &mut missing_seen,
2312 filter,
2313 missing_action,
2314 )?;
2315 }
2316
2317 Ok((result, omitted, missing))
2318}
2319
2320fn collect_root_object(
2321 repo: &Repository,
2322 root: &RootObject,
2323 seen: &mut HashSet<ObjectId>,
2324 result: &mut Vec<(ObjectId, String)>,
2325 omitted: &mut Vec<ObjectId>,
2326 missing: &mut Vec<ObjectId>,
2327 missing_seen: &mut HashSet<ObjectId>,
2328 filter: Option<&ObjectFilter>,
2329 missing_action: MissingAction,
2330) -> Result<()> {
2331 let object = match repo.odb.read(&root.oid) {
2332 Ok(object) => object,
2333 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
2334 if missing_action == MissingAction::Print && missing_seen.insert(root.oid) {
2335 missing.push(root.oid);
2336 }
2337 return Ok(());
2338 }
2339 Err(err) => return Err(err),
2340 };
2341
2342 if let Some(expected) = root.expected_kind {
2343 if !expected.matches(object.kind) {
2344 return Err(Error::CorruptObject(format!(
2345 "object {} is not a {}",
2346 root.input,
2347 expected.as_str()
2348 )));
2349 }
2350 }
2351
2352 match object.kind {
2353 ObjectKind::Commit => {
2354 let commit = parse_commit(&object.data)?;
2355 collect_tree_objects_filtered(
2356 repo,
2357 commit.tree,
2358 "",
2359 0,
2360 seen,
2361 result,
2362 omitted,
2363 missing,
2364 missing_seen,
2365 filter,
2366 missing_action,
2367 )?;
2368 }
2369 ObjectKind::Tree => {
2370 seen.remove(&root.oid);
2371 collect_tree_objects_filtered(
2372 repo,
2373 root.oid,
2374 "",
2375 0,
2376 seen,
2377 result,
2378 omitted,
2379 missing,
2380 missing_seen,
2381 filter,
2382 missing_action,
2383 )?;
2384 }
2385 ObjectKind::Blob => {
2386 if !seen.insert(root.oid) {
2387 return Ok(());
2388 }
2389 let blob_included = filter.is_none_or(|f| f.includes_blob(object.data.len() as u64));
2390 if blob_included {
2391 result.push((root.oid, String::new()));
2392 } else {
2393 omitted.push(root.oid);
2394 }
2395 }
2396 ObjectKind::Tag => {
2397 let tag = parse_tag(&object.data)?;
2398 let expected_kind =
2399 ExpectedObjectKind::from_tag_type(&tag.object_type).ok_or_else(|| {
2400 Error::CorruptObject(format!(
2401 "object {} has unsupported tag type '{}'",
2402 root.input, tag.object_type
2403 ))
2404 })?;
2405 let nested = RootObject {
2406 oid: tag.object,
2407 input: root.input.clone(),
2408 expected_kind: Some(expected_kind),
2409 };
2410 collect_root_object(
2411 repo,
2412 &nested,
2413 seen,
2414 result,
2415 omitted,
2416 missing,
2417 missing_seen,
2418 filter,
2419 missing_action,
2420 )?;
2421 }
2422 }
2423
2424 Ok(())
2425}
2426
2427#[allow(dead_code)]
2428fn collect_tree_objects_filtered(
2429 repo: &Repository,
2430 tree_oid: ObjectId,
2431 prefix: &str,
2432 depth: u64,
2433 seen: &mut HashSet<ObjectId>,
2434 result: &mut Vec<(ObjectId, String)>,
2435 omitted: &mut Vec<ObjectId>,
2436 missing: &mut Vec<ObjectId>,
2437 missing_seen: &mut HashSet<ObjectId>,
2438 filter: Option<&ObjectFilter>,
2439 missing_action: MissingAction,
2440) -> Result<()> {
2441 if !seen.insert(tree_oid) {
2442 return Ok(());
2443 }
2444 let object = match repo.odb.read(&tree_oid) {
2445 Ok(object) => object,
2446 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
2447 if missing_action == MissingAction::Print && missing_seen.insert(tree_oid) {
2448 missing.push(tree_oid);
2449 }
2450 return Ok(());
2451 }
2452 Err(err) => return Err(err),
2453 };
2454 if object.kind != ObjectKind::Tree {
2455 return Err(Error::CorruptObject(format!(
2456 "object {tree_oid} is not a tree"
2457 )));
2458 }
2459 let tree_included = filter.is_none_or(|f| f.includes_tree(depth));
2461 if tree_included {
2462 result.push((tree_oid, prefix.to_owned()));
2463 } else {
2464 omitted.push(tree_oid);
2465 }
2466 let entries = parse_tree(&object.data)?;
2467 for entry in entries {
2468 if entry.mode == 0o160000 {
2471 continue;
2472 }
2473 let name = String::from_utf8_lossy(&entry.name).to_string();
2474 let path = if prefix.is_empty() {
2475 name.clone()
2476 } else {
2477 format!("{prefix}/{name}")
2478 };
2479 let seen_before = seen.contains(&entry.oid);
2480 let child_obj = match repo.odb.read(&entry.oid) {
2481 Ok(object) => object,
2482 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
2483 if missing_action == MissingAction::Print && missing_seen.insert(entry.oid) {
2484 missing.push(entry.oid);
2485 }
2486 continue;
2487 }
2488 Err(err) => return Err(err),
2489 };
2490 if entry.mode == 0o040000 {
2491 if child_obj.kind != ObjectKind::Tree {
2492 return Err(Error::CorruptObject(format!(
2493 "object {} is not a tree",
2494 entry.oid
2495 )));
2496 }
2497 if seen_before {
2498 continue;
2499 }
2500 seen.remove(&entry.oid);
2501 collect_tree_objects_filtered(
2502 repo,
2503 entry.oid,
2504 &path,
2505 depth + 1,
2506 seen,
2507 result,
2508 omitted,
2509 missing,
2510 missing_seen,
2511 filter,
2512 missing_action,
2513 )?;
2514 } else {
2515 if child_obj.kind != ObjectKind::Blob && seen_before {
2516 return Err(Error::CorruptObject(format!(
2517 "object {} is not a blob",
2518 entry.oid
2519 )));
2520 }
2521 if seen_before {
2522 continue;
2523 }
2524 seen.insert(entry.oid);
2525
2526 if child_obj.kind == ObjectKind::Blob {
2527 let blob_included =
2528 filter.is_none_or(|f| f.includes_blob(child_obj.data.len() as u64));
2529 if blob_included {
2530 result.push((entry.oid, path));
2531 } else {
2532 omitted.push(entry.oid);
2533 }
2534 } else {
2535 result.push((entry.oid, path));
2539 }
2540 }
2541 }
2542 Ok(())
2543}
2544
2545fn collect_reachable_objects_in_commit_order(
2549 repo: &Repository,
2550 _graph: &mut CommitGraph<'_>,
2551 commits: &[ObjectId],
2552 object_roots: &[RootObject],
2553 filter: Option<&ObjectFilter>,
2554 missing_action: MissingAction,
2555) -> Result<(
2556 Vec<(ObjectId, String)>,
2557 Vec<ObjectId>,
2558 Vec<ObjectId>,
2559 Vec<usize>,
2560)> {
2561 let mut seen = HashSet::new();
2562 let mut result = Vec::new();
2563 let mut omitted = Vec::new();
2564 let mut missing = Vec::new();
2565 let mut missing_seen = HashSet::new();
2566 let mut counts = Vec::with_capacity(commits.len());
2567 for &commit_oid in commits {
2568 let commit = match load_commit(repo, commit_oid) {
2569 Ok(commit) => commit,
2570 Err(Error::ObjectNotFound(_)) if missing_action != MissingAction::Error => {
2571 if missing_action == MissingAction::Print && missing_seen.insert(commit_oid) {
2572 missing.push(commit_oid);
2573 }
2574 counts.push(0);
2575 continue;
2576 }
2577 Err(err) => return Err(err),
2578 };
2579 let before = result.len();
2580 collect_tree_objects_filtered(
2581 repo,
2582 commit.tree,
2583 "",
2584 0,
2585 &mut seen,
2586 &mut result,
2587 &mut omitted,
2588 &mut missing,
2589 &mut missing_seen,
2590 filter,
2591 missing_action,
2592 )?;
2593 counts.push(result.len() - before);
2594 }
2595
2596 for root in object_roots {
2597 collect_root_object(
2598 repo,
2599 root,
2600 &mut seen,
2601 &mut result,
2602 &mut omitted,
2603 &mut missing,
2604 &mut missing_seen,
2605 filter,
2606 missing_action,
2607 )?;
2608 }
2609
2610 Ok((result, omitted, missing, counts))
2611}
2612
2613fn kept_object_ids(repo: &Repository) -> Result<HashSet<ObjectId>> {
2615 let pack_dir = repo.git_dir.join("objects/pack");
2616 let mut kept = HashSet::new();
2617 if !pack_dir.is_dir() {
2618 return Ok(kept);
2619 }
2620 for entry in std::fs::read_dir(&pack_dir)? {
2621 let entry = entry?;
2622 let path = entry.path();
2623 if path.extension().is_some_and(|ext| ext == "keep") {
2624 let idx_path = path.with_extension("idx");
2626 if idx_path.exists() {
2627 if let Ok(oids) = crate::pack::read_idx_object_ids(&idx_path) {
2628 kept.extend(oids);
2629 }
2630 }
2631 }
2632 }
2633 Ok(kept)
2634}
2635
2636#[allow(dead_code)]
2638fn compute_patch_ids(
2639 repo: &Repository,
2640 graph: &mut CommitGraph<'_>,
2641 commits: &[ObjectId],
2642) -> Result<HashMap<ObjectId, String>> {
2643 let mut result = HashMap::new();
2644 for &oid in commits {
2645 let commit = load_commit(repo, oid)?;
2646 let parents = graph.parents_of(oid)?;
2647 let parent_tree = if let Some(&parent) = parents.first() {
2648 load_commit(repo, parent)?.tree
2649 } else {
2650 ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904")?
2651 };
2652 let patch_id = compute_tree_diff_id(repo, parent_tree, commit.tree)?;
2653 result.insert(oid, patch_id);
2654 }
2655 Ok(result)
2656}
2657
2658#[allow(dead_code)]
2659fn compute_tree_diff_id(repo: &Repository, tree_a: ObjectId, tree_b: ObjectId) -> Result<String> {
2660 use std::collections::BTreeMap;
2661 let entries_a = flatten_tree(repo, tree_a, "")?;
2662 let entries_b = flatten_tree(repo, tree_b, "")?;
2663 let map_a: BTreeMap<_, _> = entries_a.into_iter().collect();
2664 let map_b: BTreeMap<_, _> = entries_b.into_iter().collect();
2665 let mut diff_parts = Vec::new();
2666 for (path, oid_b) in &map_b {
2667 match map_a.get(path) {
2668 Some(oid_a) if oid_a != oid_b => {
2669 diff_parts.push(format!("+{path}:{oid_b}"));
2670 }
2671 None => {
2672 diff_parts.push(format!("A{path}:{oid_b}"));
2673 }
2674 _ => {}
2675 }
2676 }
2677 for (path, oid_a) in &map_a {
2678 if !map_b.contains_key(path) {
2679 diff_parts.push(format!("D{path}:{oid_a}"));
2680 }
2681 }
2682 diff_parts.sort();
2683 Ok(diff_parts.join("\n"))
2684}
2685
2686#[allow(dead_code)]
2687fn flatten_tree(
2688 repo: &Repository,
2689 tree_oid: ObjectId,
2690 prefix: &str,
2691) -> Result<Vec<(String, ObjectId)>> {
2692 let mut result = Vec::new();
2693 let object = match repo.odb.read(&tree_oid) {
2694 Ok(o) => o,
2695 Err(_) => return Ok(result),
2696 };
2697 if object.kind != ObjectKind::Tree {
2698 return Ok(result);
2699 }
2700 let entries = parse_tree(&object.data)?;
2701 for entry in entries {
2702 let name = String::from_utf8_lossy(&entry.name).to_string();
2703 let path = if prefix.is_empty() {
2704 name
2705 } else {
2706 format!("{prefix}/{name}")
2707 };
2708 let child = repo.odb.read(&entry.oid)?;
2709 if child.kind == ObjectKind::Tree {
2710 result.extend(flatten_tree(repo, entry.oid, &path)?);
2711 } else {
2712 result.push((path, entry.oid));
2713 }
2714 }
2715 Ok(result)
2716}
2717
2718fn filter_requests_tag_objects(filter: &ObjectFilter) -> bool {
2719 match filter {
2720 ObjectFilter::ObjectType(k) => *k == ObjectKind::Tag,
2721 ObjectFilter::Combine(parts) => parts.iter().any(filter_requests_tag_objects),
2722 _ => false,
2723 }
2724}
2725
2726fn append_tag_ref_object_ids(repo: &Repository, oids: &mut Vec<ObjectId>) -> Result<()> {
2727 for (_, tip) in refs::list_refs(&repo.git_dir, "refs/tags/")? {
2728 let obj = match repo.odb.read(&tip) {
2729 Ok(o) => o,
2730 Err(_) => continue,
2731 };
2732 if obj.kind == ObjectKind::Tag {
2733 oids.push(tip);
2734 }
2735 }
2736 Ok(())
2737}
2738
2739pub fn object_ids_for_cat_file_filtered(
2741 repo: &Repository,
2742 merged_filter: &ObjectFilter,
2743) -> Result<Vec<ObjectId>> {
2744 let mut options = RevListOptions::default();
2745 options.all_refs = true;
2746 options.objects = true;
2747 options.no_object_names = true;
2748 options.filter = Some(merged_filter.clone());
2749 options.missing_action = MissingAction::Allow;
2750 let r = rev_list(repo, &[], &[], &options)?;
2751 let mut oids: Vec<ObjectId> = Vec::new();
2752 oids.extend(r.commits.iter().copied());
2753 oids.extend(r.objects.iter().map(|(o, _)| *o));
2754 if filter_requests_tag_objects(merged_filter) {
2755 append_tag_ref_object_ids(repo, &mut oids)?;
2756 }
2757 oids.sort_unstable();
2758 oids.dedup();
2759 Ok(oids)
2760}
2761
2762pub fn merge_bases(
2764 repo: &Repository,
2765 a: ObjectId,
2766 b: ObjectId,
2767 first_parent_only: bool,
2768) -> Result<Vec<ObjectId>> {
2769 let mut graph = CommitGraph::new(repo, first_parent_only);
2770 let ancestors_a = walk_closure(&mut graph, &[a])?;
2771 let ancestors_b = walk_closure(&mut graph, &[b])?;
2772 let common: HashSet<ObjectId> = ancestors_a.intersection(&ancestors_b).copied().collect();
2773 if common.is_empty() {
2774 return Ok(Vec::new());
2775 }
2776 let mut bases = Vec::new();
2778 for &c in &common {
2779 let is_dominated = common.iter().any(|&other| {
2780 if other == c {
2781 return false;
2782 }
2783 let other_anc = walk_closure(&mut graph, &[other]).unwrap_or_default();
2784 other_anc.contains(&c)
2785 });
2786 if !is_dominated {
2787 bases.push(c);
2788 }
2789 }
2790 if bases.is_empty() {
2791 let mut sorted: Vec<_> = common.into_iter().collect();
2792 sorted.sort_by_key(|b| std::cmp::Reverse(graph.committer_time(*b)));
2793 bases.push(sorted[0]);
2794 }
2795 Ok(bases)
2796}