1#![warn(missing_docs)]
7#![warn(
8 clippy::all,
9 clippy::as_conversions,
10 clippy::clone_on_ref_ptr,
11 clippy::dbg_macro
12)]
13#![allow(clippy::too_many_arguments, clippy::blocks_in_if_conditions)]
14
15use std::cmp::Ordering;
16use std::fmt::Write;
17use std::time::SystemTime;
18
19use git_branchless_invoke::CommandContext;
20use git_branchless_opts::{Revset, SmartlogArgs};
21use lib::core::config::{
22 get_hint_enabled, get_hint_string, get_smartlog_default_revset, print_hint_suppression_notice,
23 Hint,
24};
25use lib::core::repo_ext::RepoExt;
26use lib::core::rewrite::find_rewrite_target;
27use lib::util::{ExitCode, EyreExitOr};
28use tracing::instrument;
29
30use lib::core::dag::{CommitSet, Dag};
31use lib::core::effects::Effects;
32use lib::core::eventlog::{EventLogDb, EventReplayer};
33use lib::core::formatting::Pluralize;
34use lib::core::node_descriptors::{
35 BranchesDescriptor, CommitMessageDescriptor, CommitOidDescriptor,
36 DifferentialRevisionDescriptor, ObsolescenceExplanationDescriptor, Redactor,
37 RelativeTimeDescriptor,
38};
39use lib::git::{GitRunInfo, Repo};
40
41pub use graph::{make_smartlog_graph, SmartlogGraph};
42pub use render::{render_graph, SmartlogOptions};
43
44use git_branchless_revset::resolve_commits;
45
46mod graph {
47 use std::collections::HashMap;
48
49 use lib::core::gc::mark_commit_reachable;
50 use tracing::instrument;
51
52 use lib::core::dag::{CommitSet, CommitVertex, Dag};
53 use lib::core::effects::{Effects, OperationType};
54 use lib::core::eventlog::{EventCursor, EventReplayer};
55 use lib::core::node_descriptors::NodeObject;
56 use lib::git::{Commit, Time};
57 use lib::git::{NonZeroOid, Repo};
58
59 #[derive(Debug)]
60 pub struct AncestorInfo {
61 pub oid: NonZeroOid,
62 pub distance: usize,
63 }
64
65 #[derive(Clone, Debug, PartialEq, Eq, Hash)]
66 pub struct ChildInfo {
67 pub oid: NonZeroOid,
68 pub is_merge_child: bool,
69 }
70 #[derive(Debug)]
72 pub struct Node<'repo> {
73 pub object: NodeObject<'repo>,
75
76 pub parents: Vec<NonZeroOid>,
81
82 pub children: Vec<ChildInfo>,
84
85 pub ancestor_info: Option<AncestorInfo>,
88
89 pub descendants: Vec<ChildInfo>,
91
92 pub is_main: bool,
98
99 pub is_obsolete: bool,
113
114 pub num_omitted_descendants: usize,
120 }
121
122 pub struct SmartlogGraph<'repo> {
124 pub nodes: HashMap<NonZeroOid, Node<'repo>>,
126 }
127
128 impl<'repo> SmartlogGraph<'repo> {
129 pub fn get_commits(&self) -> Vec<Commit<'repo>> {
132 let mut commits = self
133 .nodes
134 .values()
135 .filter_map(|node| match &node.object {
136 NodeObject::Commit { commit } => Some(commit.clone()),
137 NodeObject::GarbageCollected { oid: _ } => None,
138 })
139 .collect::<Vec<Commit<'repo>>>();
140 commits.sort_by_key(|commit| (commit.get_committer().get_time(), commit.get_oid()));
141 commits.reverse();
142 commits
143 }
144 }
145
146 impl std::fmt::Debug for SmartlogGraph<'_> {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 write!(f, "<CommitGraph len={}>", self.nodes.len())
149 }
150 }
151
152 #[instrument]
159 fn build_graph<'repo>(
160 effects: &Effects,
161 repo: &'repo Repo,
162 dag: &Dag,
163 commits: &CommitSet,
164 ) -> eyre::Result<SmartlogGraph<'repo>> {
165 let commits_include_main =
166 !dag.set_is_empty(&dag.main_branch_commit.intersection(commits))?;
167 let mut graph: HashMap<NonZeroOid, Node> = {
168 let mut result = HashMap::new();
169 for vertex in dag.commit_set_to_vec(commits)? {
170 let vertex = CommitSet::from(vertex);
171 let merge_bases = if commits_include_main {
172 dag.query_gca_all(dag.main_branch_commit.union(&vertex))?
173 } else {
174 dag.query_gca_all(commits.union(&vertex))?
175 };
176 let vertices = vertex.union(&merge_bases);
177
178 for oid in dag.commit_set_to_vec(&vertices)? {
179 let object = match repo.find_commit(oid)? {
180 Some(commit) => NodeObject::Commit { commit },
181 None => {
182 NodeObject::GarbageCollected { oid }
184 }
185 };
186
187 result.insert(
188 oid,
189 Node {
190 object,
191 parents: Vec::new(), children: Vec::new(), ancestor_info: None,
194 descendants: Vec::new(), is_main: dag.is_public_commit(oid)?,
196 is_obsolete: dag.set_contains(&dag.query_obsolete_commits(), oid)?,
197 num_omitted_descendants: 0, },
199 );
200 }
201 }
202 result
203 };
204
205 let mut immediate_links: Vec<(NonZeroOid, NonZeroOid, bool)> = Vec::new();
206 let mut non_immediate_links: Vec<(NonZeroOid, NonZeroOid, bool)> = Vec::new();
207
208 let non_main_node_oids =
209 graph
210 .iter()
211 .filter_map(|(child_oid, node)| if !node.is_main { Some(child_oid) } else { None });
212
213 let graph_vertices: CommitSet = graph.keys().cloned().collect();
214 for child_oid in non_main_node_oids {
215 let parent_vertices = dag.query_parent_names(CommitVertex::from(*child_oid))?;
216
217 match parent_vertices.as_slice() {
219 [] => {}
220 [first_parent_vertex, merge_parent_vertices @ ..] => {
221 if dag.set_contains(&graph_vertices, first_parent_vertex.clone())? {
222 let first_parent_oid = NonZeroOid::try_from(first_parent_vertex.clone())?;
223 immediate_links.push((*child_oid, first_parent_oid, false));
224 }
225 for merge_parent_vertex in merge_parent_vertices {
226 if dag.set_contains(&graph_vertices, merge_parent_vertex.clone())? {
227 let merge_parent_oid =
228 NonZeroOid::try_from(merge_parent_vertex.clone())?;
229 immediate_links.push((*child_oid, merge_parent_oid, true));
230 }
231 }
232 }
233 }
234
235 for excluded_parent_vertex in parent_vertices {
237 if dag.set_contains(&graph_vertices, excluded_parent_vertex.clone())? {
238 continue;
239 }
240
241 let parent_set = CommitSet::from(excluded_parent_vertex);
245 let merge_base = dag.query_gca_one(dag.main_branch_commit.union(&parent_set))?;
246
247 let path_to_main_branch = match merge_base {
248 Some(merge_base) => dag.query_range(CommitSet::from(merge_base), parent_set)?,
249 None => CommitSet::empty(),
250 };
251 let nearest_branch_ancestor =
252 dag.query_heads_ancestors(path_to_main_branch.intersection(&graph_vertices))?;
253
254 let ancestor_oids = dag.commit_set_to_vec(&nearest_branch_ancestor)?;
255 for ancestor_oid in ancestor_oids.iter() {
256 non_immediate_links.push((*ancestor_oid, *child_oid, false));
257 }
258 }
259 }
260
261 for (child_oid, parent_oid, is_merge_link) in immediate_links.iter() {
262 graph.get_mut(child_oid).unwrap().parents.push(*parent_oid);
263 graph.get_mut(parent_oid).unwrap().children.push(ChildInfo {
264 oid: *child_oid,
265 is_merge_child: *is_merge_link,
266 });
267 }
268
269 for (ancestor_oid, descendent_oid, is_merge_link) in non_immediate_links.iter() {
270 let distance = dag.set_count(
271 &dag.query_range(
272 CommitSet::from(*ancestor_oid),
273 CommitSet::from(*descendent_oid),
274 )?
275 .difference(&vec![*ancestor_oid, *descendent_oid].into_iter().collect()),
276 )?;
277 graph.get_mut(descendent_oid).unwrap().ancestor_info = Some(AncestorInfo {
278 oid: *ancestor_oid,
279 distance,
280 });
281 graph
282 .get_mut(ancestor_oid)
283 .unwrap()
284 .descendants
285 .push(ChildInfo {
286 oid: *descendent_oid,
287 is_merge_child: *is_merge_link,
288 })
289 }
290
291 for (oid, node) in graph.iter_mut() {
292 let oid_set = CommitSet::from(*oid);
293 let is_main_head = !dag.set_is_empty(&dag.main_branch_commit.intersection(&oid_set))?;
294 let ancestor_of_main = node.is_main && !is_main_head;
295 let has_descendants_in_graph =
296 !node.children.is_empty() || !node.descendants.is_empty();
297
298 if ancestor_of_main || has_descendants_in_graph {
299 continue;
300 }
301
302 let descendants_not_in_graph =
305 dag.query_descendants(oid_set.clone())?.difference(&oid_set);
306 let descendants_not_in_graph = dag.filter_visible_commits(descendants_not_in_graph)?;
307
308 node.num_omitted_descendants = dag.set_count(&descendants_not_in_graph)?;
309 }
310
311 Ok(SmartlogGraph { nodes: graph })
312 }
313
314 fn sort_children(graph: &mut SmartlogGraph) {
317 let commit_times: HashMap<NonZeroOid, Option<Time>> = graph
318 .nodes
319 .iter()
320 .map(|(oid, node)| {
321 (
322 *oid,
323 match &node.object {
324 NodeObject::Commit { commit } => Some(commit.get_time()),
325 NodeObject::GarbageCollected { oid: _ } => None,
326 },
327 )
328 })
329 .collect();
330 for node in graph.nodes.values_mut() {
331 node.children.sort_by_key(
332 |ChildInfo {
333 oid,
334 is_merge_child,
335 }| (&commit_times[oid], *is_merge_child, oid.to_string()),
336 );
337 }
338 }
339
340 #[instrument]
342 pub fn make_smartlog_graph<'repo>(
343 effects: &Effects,
344 repo: &'repo Repo,
345 dag: &Dag,
346 event_replayer: &EventReplayer,
347 event_cursor: EventCursor,
348 commits: &CommitSet,
349 exact: bool,
350 ) -> eyre::Result<SmartlogGraph<'repo>> {
351 let (effects, _progress) = effects.start_operation(OperationType::MakeGraph);
352
353 let mut graph = {
354 let (effects, _progress) = effects.start_operation(OperationType::WalkCommits);
355
356 let commits = if exact {
358 commits.clone()
359 } else {
360 commits
361 .union(&dag.head_commit)
362 .union(&dag.main_branch_commit)
363 };
364
365 for oid in dag.commit_set_to_vec(&commits)? {
366 mark_commit_reachable(repo, oid)?;
367 }
368
369 build_graph(&effects, repo, dag, &commits)?
370 };
371 sort_children(&mut graph);
372 Ok(graph)
373 }
374}
375
376mod render {
377 use std::cmp::Ordering;
378 use std::collections::HashSet;
379
380 use cursive_core::theme::{BaseColor, Effect};
381 use cursive_core::utils::markup::StyledString;
382 use tracing::instrument;
383
384 use lib::core::dag::{CommitSet, Dag};
385 use lib::core::effects::Effects;
386 use lib::core::formatting::{set_effect, Pluralize};
387 use lib::core::formatting::{Glyphs, StyledStringBuilder};
388 use lib::core::node_descriptors::{render_node_descriptors, NodeDescriptor};
389 use lib::git::{NonZeroOid, Repo};
390
391 use git_branchless_opts::{ResolveRevsetOptions, Revset};
392
393 use super::graph::{AncestorInfo, ChildInfo, SmartlogGraph};
394
395 fn split_commit_graph_by_roots(
403 repo: &Repo,
404 dag: &Dag,
405 graph: &SmartlogGraph,
406 ) -> Vec<NonZeroOid> {
407 let mut root_commit_oids: Vec<NonZeroOid> = graph
408 .nodes
409 .iter()
410 .filter(|(_oid, node)| node.parents.is_empty() && node.ancestor_info.is_none())
411 .map(|(oid, _node)| oid)
412 .copied()
413 .collect();
414
415 let compare = |lhs_oid: &NonZeroOid, rhs_oid: &NonZeroOid| -> Ordering {
416 let lhs_commit = repo.find_commit(*lhs_oid);
417 let rhs_commit = repo.find_commit(*rhs_oid);
418
419 let (lhs_commit, rhs_commit) = match (lhs_commit, rhs_commit) {
420 (Ok(Some(lhs_commit)), Ok(Some(rhs_commit))) => (lhs_commit, rhs_commit),
421 _ => return lhs_oid.cmp(rhs_oid),
422 };
423
424 let merge_base_oid =
425 dag.query_gca_one(vec![*lhs_oid, *rhs_oid].into_iter().collect::<CommitSet>());
426 let merge_base_oid = match merge_base_oid {
427 Err(_) => return lhs_oid.cmp(rhs_oid),
428 Ok(None) => None,
429 Ok(Some(merge_base_oid)) => NonZeroOid::try_from(merge_base_oid).ok(),
430 };
431
432 match merge_base_oid {
433 Some(merge_base_oid) if merge_base_oid == *lhs_oid => Ordering::Less,
435 Some(merge_base_oid) if merge_base_oid == *rhs_oid => Ordering::Greater,
436
437 Some(_) | None => match lhs_commit.get_time().cmp(&rhs_commit.get_time()) {
441 result @ Ordering::Less | result @ Ordering::Greater => result,
442 Ordering::Equal => lhs_oid.cmp(rhs_oid),
443 },
444 }
445 };
446
447 root_commit_oids.sort_by(compare);
448 root_commit_oids
449 }
450
451 #[instrument(skip(commit_descriptors, graph))]
452 fn get_child_output(
453 glyphs: &Glyphs,
454 graph: &SmartlogGraph,
455 root_oids: &[NonZeroOid],
456 commit_descriptors: &mut [&mut dyn NodeDescriptor],
457 head_oid: Option<NonZeroOid>,
458 current_oid: NonZeroOid,
459 last_child_line_char: Option<&str>,
460 ) -> eyre::Result<Vec<StyledString>> {
461 let current_node = &graph.nodes[¤t_oid];
462 let is_head = Some(current_oid) == head_oid;
463
464 let mut lines = vec![];
465
466 if let Some(AncestorInfo { oid: _, distance }) = current_node.ancestor_info {
467 lines.push(
468 StyledStringBuilder::new()
469 .append_plain(glyphs.commit_omitted)
470 .append_plain(" ")
471 .append_styled(
472 Pluralize {
473 determiner: None,
474 amount: distance,
475 unit: ("omitted commit", "omitted commits"),
476 }
477 .to_string(),
478 Effect::Dim,
479 )
480 .build(),
481 );
482 lines.push(StyledString::plain(glyphs.vertical_ellipsis));
483 };
484
485 if let [_, merge_parents @ ..] = current_node.parents.as_slice() {
486 if !merge_parents.is_empty() {
487 for merge_parent_oid in merge_parents {
488 let merge_parent_node = &graph.nodes[merge_parent_oid];
489 lines.push(
490 StyledStringBuilder::new()
491 .append_plain(last_child_line_char.unwrap_or(glyphs.line))
492 .append_plain(" ")
493 .append_styled(
494 format!("{} (merge) ", glyphs.commit_merge),
495 BaseColor::Blue.dark(),
496 )
497 .append(render_node_descriptors(
498 glyphs,
499 &merge_parent_node.object,
500 commit_descriptors,
501 )?)
502 .build(),
503 );
504 }
505 lines.push(StyledString::plain(format!(
506 "{}{}",
507 glyphs.line_with_offshoot, glyphs.merge,
508 )));
509 }
510 }
511
512 lines.push({
513 let cursor = match (current_node.is_main, current_node.is_obsolete, is_head) {
514 (false, false, false) => glyphs.commit_visible,
515 (false, false, true) => glyphs.commit_visible_head,
516 (false, true, false) => glyphs.commit_obsolete,
517 (false, true, true) => glyphs.commit_obsolete_head,
518 (true, false, false) => glyphs.commit_main,
519 (true, false, true) => glyphs.commit_main_head,
520 (true, true, false) => glyphs.commit_main_obsolete,
521 (true, true, true) => glyphs.commit_main_obsolete_head,
522 };
523 let text = render_node_descriptors(glyphs, ¤t_node.object, commit_descriptors)?;
524 let first_line = StyledStringBuilder::new()
525 .append_plain(cursor)
526 .append_plain(" ")
527 .append(text)
528 .build();
529 if is_head {
530 set_effect(first_line, Effect::Bold)
531 } else {
532 first_line
533 }
534 });
535
536 if current_node.num_omitted_descendants > 0 {
537 lines.push(StyledString::plain(glyphs.vertical_ellipsis));
538 lines.push(
539 StyledStringBuilder::new()
540 .append_plain(glyphs.commit_omitted)
541 .append_plain(" ")
542 .append_styled(
543 Pluralize {
544 determiner: None,
545 amount: current_node.num_omitted_descendants,
546 unit: ("omitted descendant commit", "omitted descendant commits"),
547 }
548 .to_string(),
549 Effect::Dim,
550 )
551 .build(),
552 );
553 };
554
555 let children: Vec<ChildInfo> = current_node
556 .children
557 .iter()
558 .filter(
559 |ChildInfo {
560 oid,
561 is_merge_child: _,
562 }| graph.nodes.contains_key(oid),
563 )
564 .cloned()
565 .collect();
566 let descendants: HashSet<ChildInfo> = current_node
567 .descendants
568 .iter()
569 .filter(
570 |ChildInfo {
571 oid,
572 is_merge_child: _,
573 }| graph.nodes.contains_key(oid),
574 )
575 .cloned()
576 .collect();
577 for (child_idx, child_info) in children.iter().chain(descendants.iter()).enumerate() {
578 let ChildInfo {
579 oid: child_oid,
580 is_merge_child,
581 } = child_info;
582 if root_oids.contains(child_oid) {
583 continue;
585 }
586 if *is_merge_child {
587 lines.push(
592 StyledStringBuilder::new()
593 .append_styled(
596 format!("{} (merge) ", glyphs.commit_merge),
597 BaseColor::Blue.dark(),
598 )
599 .append(render_node_descriptors(
600 glyphs,
601 &graph.nodes[child_oid].object,
602 commit_descriptors,
603 )?)
604 .build(),
605 );
606 continue;
607 }
608
609 let is_last_child = child_idx == (children.len() + descendants.len()) - 1;
610 lines.push(StyledString::plain(
611 if !is_last_child || last_child_line_char.is_some() {
612 format!("{}{}", glyphs.line_with_offshoot, glyphs.split)
613 } else if current_node.descendants.is_empty() {
614 glyphs.line.to_string()
615 } else {
616 glyphs.vertical_ellipsis.to_string()
617 },
618 ));
619
620 let child_output = get_child_output(
621 glyphs,
622 graph,
623 root_oids,
624 commit_descriptors,
625 head_oid,
626 *child_oid,
627 None,
628 )?;
629 for child_line in child_output {
630 let line = if is_last_child {
631 match last_child_line_char {
632 Some(last_child_line_char) => StyledStringBuilder::new()
633 .append_plain(format!("{last_child_line_char} "))
634 .append(child_line)
635 .build(),
636 None => child_line,
637 }
638 } else {
639 StyledStringBuilder::new()
640 .append_plain(format!("{} ", glyphs.line))
641 .append(child_line)
642 .build()
643 };
644 lines.push(line)
645 }
646 }
647 Ok(lines)
648 }
649
650 #[instrument(skip(commit_descriptors, graph))]
652 fn get_output(
653 glyphs: &Glyphs,
654 dag: &Dag,
655 graph: &SmartlogGraph,
656 commit_descriptors: &mut [&mut dyn NodeDescriptor],
657 head_oid: Option<NonZeroOid>,
658 root_oids: &[NonZeroOid],
659 ) -> eyre::Result<Vec<StyledString>> {
660 let mut lines = Vec::new();
661
662 let has_real_parent = |oid: NonZeroOid, parent_oid: NonZeroOid| -> eyre::Result<bool> {
668 let parents = dag.query_parents(CommitSet::from(oid))?;
669 let result = dag.set_contains(&parents, parent_oid)?;
670 Ok(result)
671 };
672
673 for (root_idx, root_oid) in root_oids.iter().enumerate() {
674 if !dag.set_is_empty(&dag.query_parents(CommitSet::from(*root_oid))?)? {
675 let line = if root_idx > 0 && has_real_parent(*root_oid, root_oids[root_idx - 1])? {
676 StyledString::plain(glyphs.line.to_owned())
677 } else {
678 StyledString::plain(glyphs.vertical_ellipsis.to_owned())
679 };
680 lines.push(line);
681 } else if root_idx > 0 {
682 lines.push(StyledString::new());
685 }
686
687 let last_child_line_char = {
688 if root_idx == root_oids.len() - 1 {
689 None
690 } else if has_real_parent(root_oids[root_idx + 1], *root_oid)? {
691 Some(glyphs.line)
692 } else {
693 Some(glyphs.vertical_ellipsis)
694 }
695 };
696
697 let child_output = get_child_output(
698 glyphs,
699 graph,
700 root_oids,
701 commit_descriptors,
702 head_oid,
703 *root_oid,
704 last_child_line_char,
705 )?;
706 lines.extend(child_output.into_iter());
707 }
708
709 Ok(lines)
710 }
711
712 #[instrument(skip(commit_descriptors, graph))]
714 pub fn render_graph(
715 effects: &Effects,
716 repo: &Repo,
717 dag: &Dag,
718 graph: &SmartlogGraph,
719 head_oid: Option<NonZeroOid>,
720 commit_descriptors: &mut [&mut dyn NodeDescriptor],
721 ) -> eyre::Result<Vec<StyledString>> {
722 let root_oids = split_commit_graph_by_roots(repo, dag, graph);
723 let lines = get_output(
724 effects.get_glyphs(),
725 dag,
726 graph,
727 commit_descriptors,
728 head_oid,
729 &root_oids,
730 )?;
731 Ok(lines)
732 }
733
734 #[derive(Debug, Default)]
736 pub struct SmartlogOptions {
737 pub event_id: Option<isize>,
741
742 pub revset: Option<Revset>,
746
747 pub resolve_revset_options: ResolveRevsetOptions,
749
750 pub reverse: bool,
753
754 pub exact: bool,
756 }
757}
758
759#[instrument]
761pub fn smartlog(
762 effects: &Effects,
763 git_run_info: &GitRunInfo,
764 options: SmartlogOptions,
765) -> EyreExitOr<()> {
766 let SmartlogOptions {
767 event_id,
768 revset,
769 resolve_revset_options,
770 reverse,
771 exact,
772 } = options;
773
774 let repo = Repo::from_dir(&git_run_info.working_directory)?;
775 let head_info = repo.get_head_info()?;
776 let conn = repo.get_db_conn()?;
777 let event_log_db = EventLogDb::new(&conn)?;
778 let event_replayer = EventReplayer::from_event_log_db(effects, &repo, &event_log_db)?;
779 let (references_snapshot, event_cursor) = {
780 let default_cursor = event_replayer.make_default_cursor();
781 match event_id {
782 None => (repo.get_references_snapshot()?, default_cursor),
783 Some(event_id) => {
784 let event_cursor = match event_id.cmp(&0) {
785 Ordering::Less => event_replayer.advance_cursor(default_cursor, event_id),
786 Ordering::Equal | Ordering::Greater => event_replayer.make_cursor(event_id),
787 };
788 let references_snapshot =
789 event_replayer.get_references_snapshot(&repo, event_cursor)?;
790 (references_snapshot, event_cursor)
791 }
792 }
793 };
794 let mut dag = Dag::open_and_sync(
795 effects,
796 &repo,
797 &event_replayer,
798 event_cursor,
799 &references_snapshot,
800 )?;
801
802 let revset = match revset {
803 Some(revset) => revset,
804 None => Revset(get_smartlog_default_revset(&repo)?),
805 };
806 let commits =
807 match resolve_commits(effects, &repo, &mut dag, &[revset], &resolve_revset_options) {
808 Ok(result) => match result.as_slice() {
809 [commit_set] => commit_set.clone(),
810 other => panic!("Expected exactly 1 result from resolve commits, got: {other:?}"),
811 },
812 Err(err) => {
813 err.describe(effects)?;
814 return Ok(Err(ExitCode(1)));
815 }
816 };
817
818 let graph = make_smartlog_graph(
819 effects,
820 &repo,
821 &dag,
822 &event_replayer,
823 event_cursor,
824 &commits,
825 exact,
826 )?;
827
828 let mut lines = render_graph(
829 &effects.reverse_order(reverse),
830 &repo,
831 &dag,
832 &graph,
833 references_snapshot.head_oid,
834 &mut [
835 &mut CommitOidDescriptor::new(true)?,
836 &mut RelativeTimeDescriptor::new(&repo, SystemTime::now())?,
837 &mut ObsolescenceExplanationDescriptor::new(
838 &event_replayer,
839 event_replayer.make_default_cursor(),
840 )?,
841 &mut BranchesDescriptor::new(
842 &repo,
843 &head_info,
844 &references_snapshot,
845 &Redactor::Disabled,
846 )?,
847 &mut DifferentialRevisionDescriptor::new(&repo, &Redactor::Disabled)?,
848 &mut CommitMessageDescriptor::new(&Redactor::Disabled)?,
849 ],
850 )?
851 .into_iter();
852 while let Some(line) = if reverse {
853 lines.next_back()
854 } else {
855 lines.next()
856 } {
857 writeln!(
858 effects.get_output_stream(),
859 "{}",
860 effects.get_glyphs().render(line)?
861 )?;
862 }
863
864 if !resolve_revset_options.show_hidden_commits
865 && get_hint_enabled(&repo, Hint::SmartlogFixAbandoned)?
866 {
867 let commits_with_abandoned_children: CommitSet = graph
868 .nodes
869 .iter()
870 .filter_map(|(oid, node)| {
871 if node.is_obsolete
872 && find_rewrite_target(&event_replayer, event_cursor, *oid).is_some()
873 {
874 Some(*oid)
875 } else {
876 None
877 }
878 })
879 .collect();
880 let children = dag.query_children(commits_with_abandoned_children)?;
881 let num_abandoned_children =
882 dag.set_count(&children.difference(&dag.query_obsolete_commits()))?;
883 if num_abandoned_children > 0 {
884 writeln!(
885 effects.get_output_stream(),
886 "{}: there {} in your commit graph",
887 effects.get_glyphs().render(get_hint_string())?,
888 Pluralize {
889 determiner: Some(("is", "are")),
890 amount: num_abandoned_children,
891 unit: ("abandoned commit", "abandoned commits"),
892 },
893 )?;
894 writeln!(
895 effects.get_output_stream(),
896 "{}: to fix this, run: git restack",
897 effects.get_glyphs().render(get_hint_string())?,
898 )?;
899 print_hint_suppression_notice(effects, Hint::SmartlogFixAbandoned)?;
900 }
901 }
902
903 Ok(Ok(()))
904}
905
906#[instrument]
908pub fn command_main(ctx: CommandContext, args: SmartlogArgs) -> EyreExitOr<()> {
909 let CommandContext {
910 effects,
911 git_run_info,
912 } = ctx;
913 let SmartlogArgs {
914 event_id,
915 revset,
916 resolve_revset_options,
917 reverse,
918 exact,
919 } = args;
920
921 smartlog(
922 &effects,
923 &git_run_info,
924 SmartlogOptions {
925 event_id,
926 revset,
927 resolve_revset_options,
928 reverse,
929 exact,
930 },
931 )
932}