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_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, PartialEq, Eq)]
32pub enum ObjectFilter {
33 BlobNone,
35 BlobLimit(u64),
37 TreeDepth(u64),
39 Combine(Vec<ObjectFilter>),
41}
42
43impl ObjectFilter {
44 pub fn parse(spec: &str) -> std::result::Result<Self, String> {
46 if spec == "blob:none" {
47 return Ok(ObjectFilter::BlobNone);
48 }
49 if let Some(rest) = spec.strip_prefix("blob:limit=") {
50 let bytes = parse_size_suffix(rest)
51 .ok_or_else(|| format!("invalid blob:limit value: {rest}"))?;
52 return Ok(ObjectFilter::BlobLimit(bytes));
53 }
54 if let Some(rest) = spec.strip_prefix("tree:") {
55 let depth: u64 = rest
56 .parse()
57 .map_err(|_| format!("invalid tree depth: {rest}"))?;
58 return Ok(ObjectFilter::TreeDepth(depth));
59 }
60 if let Some(rest) = spec.strip_prefix("combine:") {
61 let parts = split_combine(rest);
62 let mut filters = Vec::new();
63 for part in parts {
64 filters.push(ObjectFilter::parse(&part)?);
65 }
66 return Ok(ObjectFilter::Combine(filters));
67 }
68 Err(format!("unsupported filter spec: {spec}"))
69 }
70
71 pub fn includes_blob(&self, size: u64) -> bool {
73 match self {
74 ObjectFilter::BlobNone => false,
75 ObjectFilter::BlobLimit(limit) => size <= *limit,
76 ObjectFilter::TreeDepth(_) => true,
77 ObjectFilter::Combine(filters) => {
78 filters.iter().all(|f| f.includes_blob(size))
79 }
80 }
81 }
82
83 pub fn includes_tree(&self, depth: u64) -> bool {
85 match self {
86 ObjectFilter::BlobNone => true,
87 ObjectFilter::BlobLimit(_) => true,
88 ObjectFilter::TreeDepth(max_depth) => depth < *max_depth,
89 ObjectFilter::Combine(filters) => {
90 filters.iter().all(|f| f.includes_tree(depth))
91 }
92 }
93 }
94}
95
96fn parse_size_suffix(s: &str) -> Option<u64> {
98 let s = s.trim();
99 if s.is_empty() {
100 return None;
101 }
102 let (num_str, multiplier) = match s.as_bytes().last()? {
103 b'k' | b'K' => (&s[..s.len() - 1], 1024u64),
104 b'm' | b'M' => (&s[..s.len() - 1], 1024 * 1024),
105 b'g' | b'G' => (&s[..s.len() - 1], 1024 * 1024 * 1024),
106 _ => (s, 1u64),
107 };
108 let num: u64 = num_str.parse().ok()?;
109 Some(num * multiplier)
110}
111
112fn split_combine(spec: &str) -> Vec<String> {
114 let mut parts = Vec::new();
115 let mut current = String::new();
116 let mut chars = spec.chars().peekable();
117 while let Some(ch) = chars.next() {
118 if ch == '+' {
119 if !current.is_empty() {
120 parts.push(url_decode(¤t));
121 current.clear();
122 }
123 } else {
124 current.push(ch);
125 }
126 }
127 if !current.is_empty() {
128 parts.push(url_decode(¤t));
129 }
130 parts
131}
132
133fn url_decode(s: &str) -> String {
135 let mut result = String::new();
136 let mut chars = s.chars();
137 while let Some(ch) = chars.next() {
138 if ch == '%' {
139 let hi = chars.next().unwrap_or('0');
140 let lo = chars.next().unwrap_or('0');
141 let byte = u8::from_str_radix(&format!("{hi}{lo}"), 16).unwrap_or(b'?');
142 result.push(byte as char);
143 } else {
144 result.push(ch);
145 }
146 }
147 result
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum OrderingMode {
153 Default,
155 Topo,
157 Date,
159}
160
161#[derive(Debug, Clone)]
163pub struct RevListOptions {
164 pub all_refs: bool,
166 pub first_parent: bool,
168 pub ancestry_path: bool,
170 pub ancestry_path_bottoms: Vec<ObjectId>,
172 pub simplify_by_decoration: bool,
174 pub output_mode: OutputMode,
176 pub quiet: bool,
178 pub count: bool,
180 pub skip: usize,
182 pub max_count: Option<usize>,
184 pub ordering: OrderingMode,
186 pub reverse: bool,
188 pub objects: bool,
190 pub no_object_names: bool,
192 pub boundary: bool,
194 pub left_right: bool,
196 pub left_only: bool,
198 pub right_only: bool,
200 pub cherry_mark: bool,
202 pub cherry_pick: bool,
204 pub min_parents: Option<usize>,
206 pub max_parents: Option<usize>,
208 pub symmetric_left: Option<ObjectId>,
210 pub symmetric_right: Option<ObjectId>,
212 pub paths: Vec<String>,
214 pub full_history: bool,
216 pub sparse: bool,
218 pub filter: Option<ObjectFilter>,
220 pub filter_print_omitted: bool,
222}
223
224impl Default for RevListOptions {
225 fn default() -> Self {
226 Self {
227 all_refs: false,
228 first_parent: false,
229 ancestry_path: false,
230 ancestry_path_bottoms: Vec::new(),
231 simplify_by_decoration: false,
232 output_mode: OutputMode::OidOnly,
233 quiet: false,
234 count: false,
235 skip: 0,
236 max_count: None,
237 ordering: OrderingMode::Default,
238 reverse: false,
239 objects: false,
240 no_object_names: false,
241 boundary: false,
242 left_right: false,
243 left_only: false,
244 right_only: false,
245 cherry_mark: false,
246 cherry_pick: false,
247 min_parents: None,
248 max_parents: None,
249 symmetric_left: None,
250 symmetric_right: None,
251 paths: Vec::new(),
252 full_history: false,
253 sparse: false,
254 filter: None,
255 filter_print_omitted: false,
256 }
257 }
258}
259
260#[derive(Debug, Clone)]
262pub struct RevListResult {
263 pub commits: Vec<ObjectId>,
265 pub objects: Vec<(ObjectId, String)>,
268 pub omitted_objects: Vec<ObjectId>,
270 pub boundary_commits: Vec<ObjectId>,
272 pub left_right_map: HashMap<ObjectId, bool>,
274 pub cherry_equivalent: HashSet<ObjectId>,
276}
277
278pub fn rev_list(
292 repo: &Repository,
293 positive_specs: &[String],
294 negative_specs: &[String],
295 options: &RevListOptions,
296) -> Result<RevListResult> {
297 let mut graph = CommitGraph::new(repo, options.first_parent);
298
299 let mut include = resolve_specs(repo, positive_specs)?;
300 let exclude = resolve_specs(repo, negative_specs)?;
301
302 if options.all_refs {
303 include.extend(all_ref_tips(repo)?);
304 }
305
306 if include.is_empty() {
307 return Err(Error::InvalidRef("no revisions specified".to_owned()));
308 }
309
310 let mut included = walk_closure(&mut graph, &include)?;
311 let excluded = walk_closure(&mut graph, &exclude)?;
312 included.retain(|oid| !excluded.contains(oid));
313
314 if options.simplify_by_decoration {
315 let decorated = all_ref_tips(repo)?;
316 included.retain(|oid| decorated.contains(oid));
317 }
318
319 if options.ancestry_path {
320 let mut bottoms = options.ancestry_path_bottoms.clone();
321 if bottoms.is_empty() {
322 bottoms.extend(exclude.iter().copied());
323 }
324 if bottoms.is_empty() {
325 return Err(Error::InvalidRef(
326 "--ancestry-path requires a range with excluded tips".to_owned(),
327 ));
328 }
329 limit_to_ancestry(&mut graph, &mut included, &bottoms)?;
330 }
331
332 if options.min_parents.is_some() || options.max_parents.is_some() {
334 let min_p = options.min_parents.unwrap_or(0);
335 let max_p = options.max_parents.unwrap_or(usize::MAX);
336 included.retain(|oid| {
337 let count = graph.parents_of(*oid).map(|p| p.len()).unwrap_or(0);
338 count >= min_p && count <= max_p
339 });
340 }
341
342 let mut ordered = match options.ordering {
343 OrderingMode::Default => sort_by_commit_date_desc(&mut graph, &included)?,
344 OrderingMode::Topo | OrderingMode::Date => topo_sort(&mut graph, &included)?,
345 };
346
347 if !options.paths.is_empty() && !options.sparse {
349 let paths = &options.paths;
350 ordered.retain(|oid| {
351 commit_touches_paths(repo, &mut graph, *oid, paths).unwrap_or(false)
352 });
353 }
354
355 let mut left_right_map = HashMap::new();
357 if options.left_right || options.left_only || options.right_only || options.cherry_mark || options.cherry_pick {
358 if let (Some(left_oid), Some(right_oid)) = (options.symmetric_left, options.symmetric_right) {
359 let left_closure = walk_closure(&mut graph, &[left_oid])?;
360 let right_closure = walk_closure(&mut graph, &[right_oid])?;
361 for &oid in &ordered {
362 let in_left = left_closure.contains(&oid);
363 let in_right = right_closure.contains(&oid);
364 if in_left && !in_right {
365 left_right_map.insert(oid, true);
366 } else if in_right && !in_left {
367 left_right_map.insert(oid, false);
368 } else {
369 left_right_map.insert(oid, false);
370 }
371 }
372 }
373 }
374
375 let mut cherry_equivalent = HashSet::new();
377 if options.cherry_pick || options.cherry_mark {
378 let patch_ids = compute_patch_ids(repo, &mut graph, &ordered)?;
379 let left_commits: Vec<_> = ordered.iter().filter(|o| left_right_map.get(o) == Some(&true)).copied().collect();
380 let right_commits: Vec<_> = ordered.iter().filter(|o| left_right_map.get(o) == Some(&false)).copied().collect();
381 let left_patches: HashMap<&str, ObjectId> = left_commits.iter()
382 .filter_map(|o| patch_ids.get(o).map(|p| (p.as_str(), *o)))
383 .collect();
384 let right_patches: HashMap<&str, ObjectId> = right_commits.iter()
385 .filter_map(|o| patch_ids.get(o).map(|p| (p.as_str(), *o)))
386 .collect();
387 for (&pid, &oid) in &left_patches {
388 if !pid.is_empty() && right_patches.contains_key(pid) {
389 cherry_equivalent.insert(oid);
390 cherry_equivalent.insert(right_patches[pid]);
391 }
392 }
393 for (&pid, &oid) in &right_patches {
394 if !pid.is_empty() && left_patches.contains_key(pid) {
395 cherry_equivalent.insert(oid);
396 cherry_equivalent.insert(left_patches[pid]);
397 }
398 }
399 }
400
401 if options.left_only {
403 ordered.retain(|oid| left_right_map.get(oid) == Some(&true));
404 }
405 if options.right_only {
406 ordered.retain(|oid| left_right_map.get(oid) == Some(&false));
407 }
408
409 if options.cherry_pick {
411 ordered.retain(|oid| !cherry_equivalent.contains(oid));
412 }
413
414 if options.skip > 0 {
415 ordered = ordered.into_iter().skip(options.skip).collect();
416 }
417 if let Some(max_count) = options.max_count {
418 ordered.truncate(max_count);
419 }
420 if options.reverse {
421 ordered.reverse();
422 }
423
424 let (objects, omitted_objects) = if options.objects {
426 collect_reachable_objects(repo, &mut graph, &ordered, options.filter.as_ref())?
427 } else {
428 (Vec::new(), Vec::new())
429 };
430
431 Ok(RevListResult {
432 commits: ordered,
433 objects,
434 omitted_objects,
435 boundary_commits: Vec::new(),
436 left_right_map,
437 cherry_equivalent,
438 })
439}
440
441#[must_use]
448pub fn split_revision_token(token: &str) -> (Vec<String>, Vec<String>) {
449 if let Some((lhs, rhs)) = token.split_once("..") {
450 return (vec![rhs.to_owned()], vec![lhs.to_owned()]);
451 }
452 if let Some(rest) = token.strip_prefix('^') {
453 return (Vec::new(), vec![rest.to_owned()]);
454 }
455 (vec![token.to_owned()], Vec::new())
456}
457
458pub fn render_commit(
464 repo: &Repository,
465 oid: ObjectId,
466 mode: &OutputMode,
467 abbrev_len: usize,
468) -> Result<String> {
469 match mode {
470 OutputMode::OidOnly => Ok(format!("{oid}")),
471 OutputMode::Parents => {
472 let mut out = format!("{oid}");
473 let commit = load_commit(repo, oid)?;
474 for parent in commit.parents {
475 out.push(' ');
476 out.push_str(&parent.to_hex());
477 }
478 Ok(out)
479 }
480 OutputMode::Format(fmt) => {
481 let commit = load_commit(repo, oid)?;
482 let subject = commit.message.lines().next().unwrap_or_default();
483 let mut rendered = String::new();
484 let mut chars = fmt.chars().peekable();
485 while let Some(ch) = chars.next() {
486 if ch != '%' {
487 rendered.push(ch);
488 continue;
489 }
490 match chars.next() {
491 Some('%') => rendered.push('%'),
492 Some('H') => rendered.push_str(&oid.to_hex()),
493 Some('h') => {
494 let hex = oid.to_hex();
495 let n = abbrev_len.clamp(4, 40).min(hex.len());
496 rendered.push_str(&hex[..n]);
497 }
498 Some('s') => rendered.push_str(subject),
499 Some(other) => {
500 rendered.push('%');
501 rendered.push(other);
502 }
503 None => rendered.push('%'),
504 }
505 }
506 Ok(rendered)
507 }
508 }
509}
510
511fn resolve_specs(repo: &Repository, specs: &[String]) -> Result<Vec<ObjectId>> {
512 let mut out = Vec::with_capacity(specs.len());
513 for spec in specs {
514 let oid = resolve_revision(repo, spec)?;
515 ensure_commit(repo, oid)?;
516 out.push(oid);
517 }
518 Ok(out)
519}
520
521fn all_ref_tips(repo: &Repository) -> Result<Vec<ObjectId>> {
522 let mut out = Vec::new();
523 if let Ok(head) = refs::resolve_ref(&repo.git_dir, "HEAD") {
524 out.push(head);
525 }
526 out.extend(
527 refs::list_refs(&repo.git_dir, "refs/")?
528 .into_iter()
529 .map(|(_, oid)| oid),
530 );
531 out.sort();
532 out.dedup();
533 Ok(out)
534}
535
536fn walk_closure(graph: &mut CommitGraph<'_>, starts: &[ObjectId]) -> Result<HashSet<ObjectId>> {
537 let mut seen = HashSet::new();
538 let mut queue = VecDeque::new();
539 for &start in starts {
540 queue.push_back(start);
541 }
542 while let Some(oid) = queue.pop_front() {
543 if !seen.insert(oid) {
544 continue;
545 }
546 for parent in graph.parents_of(oid)? {
547 queue.push_back(parent);
548 }
549 }
550 Ok(seen)
551}
552
553fn sort_by_commit_date_desc(
554 graph: &mut CommitGraph<'_>,
555 selected: &HashSet<ObjectId>,
556) -> Result<Vec<ObjectId>> {
557 let mut out = selected.iter().copied().collect::<Vec<_>>();
558 out.sort_by(
559 |a, b| match graph.committer_time(*b).cmp(&graph.committer_time(*a)) {
560 Ordering::Equal => b.cmp(a),
561 other => other,
562 },
563 );
564 Ok(out)
565}
566
567fn topo_sort(graph: &mut CommitGraph<'_>, selected: &HashSet<ObjectId>) -> Result<Vec<ObjectId>> {
568 let mut child_count: HashMap<ObjectId, usize> = selected.iter().map(|&oid| (oid, 0)).collect();
569
570 for &oid in selected {
571 for parent in graph.parents_of(oid)? {
572 if !selected.contains(&parent) {
573 continue;
574 }
575 if let Some(count) = child_count.get_mut(&parent) {
576 *count += 1;
577 }
578 }
579 }
580
581 let mut ready = BinaryHeap::new();
582 for (&oid, &count) in &child_count {
583 if count == 0 {
584 ready.push(CommitDateKey {
585 oid,
586 date: graph.committer_time(oid),
587 });
588 }
589 }
590
591 let mut out = Vec::with_capacity(selected.len());
592 while let Some(item) = ready.pop() {
593 let oid = item.oid;
594 out.push(oid);
595 for parent in graph.parents_of(oid)? {
596 if !selected.contains(&parent) {
597 continue;
598 }
599 if let Some(count) = child_count.get_mut(&parent) {
600 *count = count.saturating_sub(1);
601 if *count == 0 {
602 ready.push(CommitDateKey {
603 oid: parent,
604 date: graph.committer_time(parent),
605 });
606 }
607 }
608 }
609 }
610
611 Ok(out)
612}
613
614fn limit_to_ancestry(
615 graph: &mut CommitGraph<'_>,
616 selected: &mut HashSet<ObjectId>,
617 bottoms: &[ObjectId],
618) -> Result<()> {
619 let mut keep = HashSet::new();
620 for &bottom in bottoms {
621 let ancestors = walk_closure(graph, &[bottom])?;
622 keep.extend(
623 ancestors
624 .iter()
625 .copied()
626 .filter(|oid| selected.contains(oid)),
627 );
628
629 for &candidate in selected.iter() {
630 if candidate == bottom {
631 keep.insert(candidate);
632 continue;
633 }
634 let closure = walk_closure(graph, &[candidate])?;
635 if closure.contains(&bottom) {
636 keep.insert(candidate);
637 }
638 }
639 }
640 selected.retain(|oid| keep.contains(oid));
641 Ok(())
642}
643
644fn commit_touches_paths(
646 repo: &Repository,
647 graph: &mut CommitGraph<'_>,
648 oid: ObjectId,
649 paths: &[String],
650) -> Result<bool> {
651 let commit = load_commit(repo, oid)?;
652 let parents = graph.parents_of(oid)?;
653 let parent_tree = if let Some(&parent) = parents.first() {
654 Some(load_commit(repo, parent)?.tree)
655 } else {
656 None
657 };
658 let commit_entries = flatten_tree(repo, commit.tree, "")?;
659 let commit_map: HashMap<String, ObjectId> = commit_entries.into_iter().collect();
660 let parent_map: HashMap<String, ObjectId> = if let Some(pt) = parent_tree {
661 flatten_tree(repo, pt, "")?.into_iter().collect()
662 } else {
663 HashMap::new()
664 };
665 for path in paths {
666 let c_oid = commit_map.get(path.as_str());
667 let p_oid = parent_map.get(path.as_str());
668 if c_oid != p_oid {
669 return Ok(true);
670 }
671 }
672 Ok(false)
673}
674
675fn load_commit(repo: &Repository, oid: ObjectId) -> Result<crate::objects::CommitData> {
676 let object = repo.odb.read(&oid)?;
677 if object.kind != ObjectKind::Commit {
678 return Err(Error::CorruptObject(format!(
679 "object {oid} is not a commit"
680 )));
681 }
682 parse_commit(&object.data)
683}
684
685fn ensure_commit(repo: &Repository, oid: ObjectId) -> Result<()> {
686 let object = repo.odb.read(&oid)?;
687 if object.kind != ObjectKind::Commit {
688 return Err(Error::CorruptObject(format!(
689 "object {oid} is not a commit"
690 )));
691 }
692 Ok(())
693}
694
695fn parse_signature_time(sig: &str) -> i64 {
696 let mut parts = sig.split_whitespace().collect::<Vec<_>>();
697 if parts.len() < 2 {
698 return 0;
699 }
700 let ts = parts.remove(parts.len().saturating_sub(2));
701 ts.parse::<i64>().unwrap_or(0)
702}
703
704pub fn collect_revision_specs_with_stdin(
712 args_specs: &[String],
713 read_stdin: bool,
714) -> Result<(Vec<String>, Vec<String>, bool)> {
715 let mut positive = Vec::new();
716 let mut negative = Vec::new();
717 let mut not_mode = false;
718
719 for spec in args_specs {
720 let (pos, neg) = split_revision_token(spec);
721 if not_mode {
722 positive.extend(neg);
723 negative.extend(pos);
724 } else {
725 positive.extend(pos);
726 negative.extend(neg);
727 }
728 }
729
730 if !read_stdin {
731 return Ok((positive, negative, false));
732 }
733
734 let mut in_paths = false;
735 let mut stdin_all_refs = false;
736 let stdin = std::io::read_to_string(std::io::stdin()).map_err(Error::Io)?;
737 for raw_line in stdin.lines() {
738 let line = raw_line.trim();
739 if line.is_empty() {
740 continue;
741 }
742 if in_paths {
743 continue;
744 }
745 if line == "--" {
746 in_paths = true;
747 continue;
748 }
749 if line == "--not" {
750 not_mode = !not_mode;
751 continue;
752 }
753 if line == "--all" {
754 stdin_all_refs = true;
755 continue;
756 }
757 if line.starts_with("--") {
758 return Err(Error::InvalidRef(format!(
759 "invalid option '{line}' in --stdin mode"
760 )));
761 }
762 if line.starts_with('-') {
763 return Err(Error::InvalidRef(format!(
764 "invalid option '{line}' in --stdin mode"
765 )));
766 }
767 let (pos, neg) = split_revision_token(line);
768 if not_mode {
769 positive.extend(neg);
770 negative.extend(pos);
771 } else {
772 positive.extend(pos);
773 negative.extend(neg);
774 }
775 }
776
777 Ok((positive, negative, stdin_all_refs))
778}
779
780pub fn tag_targets(git_dir: &Path) -> Result<HashSet<ObjectId>> {
782 Ok(refs::list_refs(git_dir, "refs/tags/")?
783 .into_iter()
784 .map(|(_, oid)| oid)
785 .collect())
786}
787
788struct CommitGraph<'r> {
789 repo: &'r Repository,
790 first_parent_only: bool,
791 parents: HashMap<ObjectId, Vec<ObjectId>>,
792 committer_time: HashMap<ObjectId, i64>,
793}
794
795impl<'r> CommitGraph<'r> {
796 fn new(repo: &'r Repository, first_parent_only: bool) -> Self {
797 Self {
798 repo,
799 first_parent_only,
800 parents: HashMap::new(),
801 committer_time: HashMap::new(),
802 }
803 }
804
805 fn parents_of(&mut self, oid: ObjectId) -> Result<Vec<ObjectId>> {
806 self.populate(oid)?;
807 Ok(self.parents.get(&oid).cloned().unwrap_or_default())
808 }
809
810 fn committer_time(&mut self, oid: ObjectId) -> i64 {
811 if self.populate(oid).is_err() {
812 return 0;
813 }
814 self.committer_time.get(&oid).copied().unwrap_or(0)
815 }
816
817 fn populate(&mut self, oid: ObjectId) -> Result<()> {
818 if self.parents.contains_key(&oid) {
819 return Ok(());
820 }
821 let commit = load_commit(self.repo, oid)?;
822 let mut parents = commit.parents;
823 if self.first_parent_only && parents.len() > 1 {
824 parents.truncate(1);
825 }
826 self.committer_time
827 .insert(oid, parse_signature_time(&commit.committer));
828 self.parents.insert(oid, parents);
829 Ok(())
830 }
831}
832
833#[derive(Clone, Copy, Debug, Eq, PartialEq)]
834struct CommitDateKey {
835 oid: ObjectId,
836 date: i64,
837}
838
839impl Ord for CommitDateKey {
840 fn cmp(&self, other: &Self) -> Ordering {
841 match self.date.cmp(&other.date) {
842 Ordering::Equal => self.oid.cmp(&other.oid),
843 ord => ord,
844 }
845 }
846}
847
848impl PartialOrd for CommitDateKey {
849 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
850 Some(self.cmp(other))
851 }
852}
853
854pub fn read_lines(path: &Path) -> Result<Vec<String>> {
860 let content = fs::read_to_string(path)?;
861 Ok(content.lines().map(|line| line.to_owned()).collect())
862}
863
864#[must_use]
866pub fn is_symmetric_diff(token: &str) -> bool {
867 token.contains("...") && !token.contains("....")
868}
869
870#[must_use]
872pub fn split_symmetric_diff(token: &str) -> Option<(String, String)> {
873 token.split_once("...").map(|(l, r)| (l.to_owned(), r.to_owned()))
874}
875
876#[allow(dead_code)]
879fn collect_reachable_objects(
880 repo: &Repository,
881 _graph: &mut CommitGraph<'_>,
882 commits: &[ObjectId],
883 filter: Option<&ObjectFilter>,
884) -> Result<(Vec<(ObjectId, String)>, Vec<ObjectId>)> {
885 let mut seen = HashSet::new();
886 let mut result = Vec::new();
887 let mut omitted = Vec::new();
888 for &commit_oid in commits {
889 let commit = load_commit(repo, commit_oid)?;
890 collect_tree_objects_filtered(
891 repo, commit.tree, "", 0, &mut seen, &mut result, &mut omitted, filter,
892 )?;
893 }
894 Ok((result, omitted))
895}
896
897#[allow(dead_code)]
898fn collect_tree_objects_filtered(
899 repo: &Repository,
900 tree_oid: ObjectId,
901 prefix: &str,
902 depth: u64,
903 seen: &mut HashSet<ObjectId>,
904 result: &mut Vec<(ObjectId, String)>,
905 omitted: &mut Vec<ObjectId>,
906 filter: Option<&ObjectFilter>,
907) -> Result<()> {
908 if !seen.insert(tree_oid) {
909 return Ok(());
910 }
911 let tree_included = filter.map_or(true, |f| f.includes_tree(depth));
913 if tree_included {
914 result.push((tree_oid, prefix.to_owned()));
915 } else {
916 omitted.push(tree_oid);
917 }
918 let object = repo.odb.read(&tree_oid)?;
919 if object.kind != ObjectKind::Tree {
920 return Ok(());
921 }
922 let entries = parse_tree(&object.data)?;
923 for entry in entries {
924 let name = String::from_utf8_lossy(&entry.name).to_string();
925 let path = if prefix.is_empty() {
926 name.clone()
927 } else {
928 format!("{prefix}/{name}")
929 };
930 if !seen.insert(entry.oid) {
931 continue;
932 }
933 let child_obj = repo.odb.read(&entry.oid)?;
934 match child_obj.kind {
935 ObjectKind::Tree => {
936 seen.remove(&entry.oid);
938 collect_tree_objects_filtered(
939 repo,
940 entry.oid,
941 &path,
942 depth + 1,
943 seen,
944 result,
945 omitted,
946 filter,
947 )?;
948 }
949 _ => {
950 let blob_included = filter.map_or(true, |f| {
952 f.includes_blob(child_obj.data.len() as u64)
953 });
954 if blob_included {
955 result.push((entry.oid, path));
956 } else {
957 omitted.push(entry.oid);
958 }
959 }
960 }
961 }
962 Ok(())
963}
964
965#[allow(dead_code)]
967fn compute_patch_ids(
968 repo: &Repository,
969 graph: &mut CommitGraph<'_>,
970 commits: &[ObjectId],
971) -> Result<HashMap<ObjectId, String>> {
972 let mut result = HashMap::new();
973 for &oid in commits {
974 let commit = load_commit(repo, oid)?;
975 let parents = graph.parents_of(oid)?;
976 let parent_tree = if let Some(&parent) = parents.first() {
977 load_commit(repo, parent)?.tree
978 } else {
979 ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904")?
980 };
981 let patch_id = compute_tree_diff_id(repo, parent_tree, commit.tree)?;
982 result.insert(oid, patch_id);
983 }
984 Ok(result)
985}
986
987#[allow(dead_code)]
988fn compute_tree_diff_id(
989 repo: &Repository,
990 tree_a: ObjectId,
991 tree_b: ObjectId,
992) -> Result<String> {
993 use std::collections::BTreeMap;
994 let entries_a = flatten_tree(repo, tree_a, "")?;
995 let entries_b = flatten_tree(repo, tree_b, "")?;
996 let map_a: BTreeMap<_, _> = entries_a.into_iter().collect();
997 let map_b: BTreeMap<_, _> = entries_b.into_iter().collect();
998 let mut diff_parts = Vec::new();
999 for (path, oid_b) in &map_b {
1000 match map_a.get(path) {
1001 Some(oid_a) if oid_a != oid_b => {
1002 diff_parts.push(format!("+{path}:{oid_b}"));
1003 }
1004 None => {
1005 diff_parts.push(format!("A{path}:{oid_b}"));
1006 }
1007 _ => {}
1008 }
1009 }
1010 for (path, oid_a) in &map_a {
1011 if !map_b.contains_key(path) {
1012 diff_parts.push(format!("D{path}:{oid_a}"));
1013 }
1014 }
1015 diff_parts.sort();
1016 Ok(diff_parts.join("\n"))
1017}
1018
1019#[allow(dead_code)]
1020fn flatten_tree(
1021 repo: &Repository,
1022 tree_oid: ObjectId,
1023 prefix: &str,
1024) -> Result<Vec<(String, ObjectId)>> {
1025 let mut result = Vec::new();
1026 let object = match repo.odb.read(&tree_oid) {
1027 Ok(o) => o,
1028 Err(_) => return Ok(result),
1029 };
1030 if object.kind != ObjectKind::Tree {
1031 return Ok(result);
1032 }
1033 let entries = parse_tree(&object.data)?;
1034 for entry in entries {
1035 let name = String::from_utf8_lossy(&entry.name).to_string();
1036 let path = if prefix.is_empty() {
1037 name
1038 } else {
1039 format!("{prefix}/{name}")
1040 };
1041 let child = repo.odb.read(&entry.oid)?;
1042 if child.kind == ObjectKind::Tree {
1043 result.extend(flatten_tree(repo, entry.oid, &path)?);
1044 } else {
1045 result.push((path, entry.oid));
1046 }
1047 }
1048 Ok(result)
1049}
1050
1051pub fn merge_bases(
1053 repo: &Repository,
1054 a: ObjectId,
1055 b: ObjectId,
1056 first_parent_only: bool,
1057) -> Result<Vec<ObjectId>> {
1058 let mut graph = CommitGraph::new(repo, first_parent_only);
1059 let ancestors_a = walk_closure(&mut graph, &[a])?;
1060 let ancestors_b = walk_closure(&mut graph, &[b])?;
1061 let common: HashSet<ObjectId> = ancestors_a.intersection(&ancestors_b).copied().collect();
1062 if common.is_empty() {
1063 return Ok(Vec::new());
1064 }
1065 let mut bases = Vec::new();
1067 for &c in &common {
1068 let is_dominated = common.iter().any(|&other| {
1069 if other == c { return false; }
1070 let other_anc = walk_closure(&mut graph, &[other]).unwrap_or_default();
1071 other_anc.contains(&c)
1072 });
1073 if !is_dominated {
1074 bases.push(c);
1075 }
1076 }
1077 if bases.is_empty() {
1078 let mut sorted: Vec<_> = common.into_iter().collect();
1079 sorted.sort_by_key(|b| std::cmp::Reverse(graph.committer_time(*b)));
1080 bases.push(sorted[0]);
1081 }
1082 Ok(bases)
1083}