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_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 Hint, get_hint_enabled, get_hint_string, get_smartlog_default_revset, get_smartlog_reverse,
23 print_hint_suppression_notice,
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::{SmartlogGraph, make_smartlog_graph};
42pub use render::{SmartlogOptions, render_graph};
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 = graph
209 .iter()
210 .filter_map(|(child_oid, node)| if !node.is_main { Some(child_oid) } else { None });
211
212 let graph_vertices: CommitSet = graph.keys().cloned().collect();
213 for child_oid in non_main_node_oids {
214 let parent_vertices = dag.query_parent_names(CommitVertex::from(*child_oid))?;
215
216 match parent_vertices.as_slice() {
218 [] => {}
219 [first_parent_vertex, merge_parent_vertices @ ..] => {
220 if dag.set_contains(&graph_vertices, first_parent_vertex.clone())? {
221 let first_parent_oid = NonZeroOid::try_from(first_parent_vertex.clone())?;
222 immediate_links.push((*child_oid, first_parent_oid, false));
223 }
224 for merge_parent_vertex in merge_parent_vertices {
225 if dag.set_contains(&graph_vertices, merge_parent_vertex.clone())? {
226 let merge_parent_oid =
227 NonZeroOid::try_from(merge_parent_vertex.clone())?;
228 immediate_links.push((*child_oid, merge_parent_oid, true));
229 }
230 }
231 }
232 }
233
234 for excluded_parent_vertex in parent_vertices {
236 if dag.set_contains(&graph_vertices, excluded_parent_vertex.clone())? {
237 continue;
238 }
239
240 let parent_set = CommitSet::from(excluded_parent_vertex);
244 let merge_base = dag.query_gca_one(dag.main_branch_commit.union(&parent_set))?;
245
246 let path_to_main_branch = match merge_base {
247 Some(merge_base) => dag.query_range(CommitSet::from(merge_base), parent_set)?,
248 None => CommitSet::empty(),
249 };
250 let nearest_branch_ancestor =
251 dag.query_heads_ancestors(path_to_main_branch.intersection(&graph_vertices))?;
252
253 let ancestor_oids = dag.commit_set_to_vec(&nearest_branch_ancestor)?;
254 for ancestor_oid in ancestor_oids.iter() {
255 non_immediate_links.push((*ancestor_oid, *child_oid, false));
256 }
257 }
258 }
259
260 for (child_oid, parent_oid, is_merge_link) in immediate_links.iter() {
261 graph.get_mut(child_oid).unwrap().parents.push(*parent_oid);
262 graph.get_mut(parent_oid).unwrap().children.push(ChildInfo {
263 oid: *child_oid,
264 is_merge_child: *is_merge_link,
265 });
266 }
267
268 for (ancestor_oid, descendent_oid, is_merge_link) in non_immediate_links.iter() {
269 let distance = dag.set_count(
270 &dag.query_range(
271 CommitSet::from(*ancestor_oid),
272 CommitSet::from(*descendent_oid),
273 )?
274 .difference(&vec![*ancestor_oid, *descendent_oid].into_iter().collect()),
275 )?;
276 graph.get_mut(descendent_oid).unwrap().ancestor_info = Some(AncestorInfo {
277 oid: *ancestor_oid,
278 distance,
279 });
280 graph
281 .get_mut(ancestor_oid)
282 .unwrap()
283 .descendants
284 .push(ChildInfo {
285 oid: *descendent_oid,
286 is_merge_child: *is_merge_link,
287 })
288 }
289
290 for (oid, node) in graph.iter_mut() {
291 let oid_set = CommitSet::from(*oid);
292 let is_main_head = !dag.set_is_empty(&dag.main_branch_commit.intersection(&oid_set))?;
293 let ancestor_of_main = node.is_main && !is_main_head;
294 let has_descendants_in_graph =
295 !node.children.is_empty() || !node.descendants.is_empty();
296
297 if ancestor_of_main || has_descendants_in_graph {
298 continue;
299 }
300
301 let descendants_not_in_graph =
304 dag.query_descendants(oid_set.clone())?.difference(&oid_set);
305 let descendants_not_in_graph = dag.filter_visible_commits(descendants_not_in_graph)?;
306
307 node.num_omitted_descendants = dag.set_count(&descendants_not_in_graph)?;
308 }
309
310 Ok(SmartlogGraph { nodes: graph })
311 }
312
313 fn sort_children(graph: &mut SmartlogGraph) {
316 let commit_times: HashMap<NonZeroOid, Option<Time>> = graph
317 .nodes
318 .iter()
319 .map(|(oid, node)| {
320 (
321 *oid,
322 match &node.object {
323 NodeObject::Commit { commit } => Some(commit.get_time()),
324 NodeObject::GarbageCollected { oid: _ } => None,
325 },
326 )
327 })
328 .collect();
329 for node in graph.nodes.values_mut() {
330 node.children.sort_by_key(
331 |ChildInfo {
332 oid,
333 is_merge_child,
334 }| (&commit_times[oid], *is_merge_child, oid.to_string()),
335 );
336 }
337 }
338
339 #[instrument]
341 pub fn make_smartlog_graph<'repo>(
342 effects: &Effects,
343 repo: &'repo Repo,
344 dag: &Dag,
345 event_replayer: &EventReplayer,
346 event_cursor: EventCursor,
347 commits: &CommitSet,
348 exact: bool,
349 ) -> eyre::Result<SmartlogGraph<'repo>> {
350 let (effects, _progress) = effects.start_operation(OperationType::MakeGraph);
351
352 let mut graph = {
353 let (effects, _progress) = effects.start_operation(OperationType::WalkCommits);
354
355 let commits = if exact {
357 commits.clone()
358 } else {
359 commits
360 .union(&dag.head_commit)
361 .union(&dag.main_branch_commit)
362 };
363
364 for oid in dag.commit_set_to_vec(&commits)? {
365 mark_commit_reachable(repo, oid)?;
366 }
367
368 build_graph(&effects, repo, dag, &commits)?
369 };
370 sort_children(&mut graph);
371 Ok(graph)
372 }
373}
374
375mod render {
376 use std::cmp::Ordering;
377 use std::collections::HashSet;
378
379 use cursive_core::theme::{BaseColor, Effect};
380 use cursive_core::utils::markup::StyledString;
381 use tracing::instrument;
382
383 use lib::core::dag::{CommitSet, Dag};
384 use lib::core::effects::Effects;
385 use lib::core::formatting::{Glyphs, StyledStringBuilder};
386 use lib::core::formatting::{Pluralize, set_effect};
387 use lib::core::node_descriptors::{NodeDescriptor, render_node_descriptors};
388 use lib::git::{NonZeroOid, Repo};
389
390 use git_branchless_opts::{ResolveRevsetOptions, Revset};
391
392 use super::graph::{AncestorInfo, ChildInfo, SmartlogGraph};
393
394 fn split_commit_graph_by_roots(
402 repo: &Repo,
403 dag: &Dag,
404 graph: &SmartlogGraph,
405 ) -> Vec<NonZeroOid> {
406 let mut root_commit_oids: Vec<NonZeroOid> = graph
407 .nodes
408 .iter()
409 .filter(|(_oid, node)| node.parents.is_empty() && node.ancestor_info.is_none())
410 .map(|(oid, _node)| oid)
411 .copied()
412 .collect();
413
414 let compare = |lhs_oid: &NonZeroOid, rhs_oid: &NonZeroOid| -> Ordering {
415 let lhs_commit = repo.find_commit(*lhs_oid);
416 let rhs_commit = repo.find_commit(*rhs_oid);
417
418 let (lhs_commit, rhs_commit) = match (lhs_commit, rhs_commit) {
419 (Ok(Some(lhs_commit)), Ok(Some(rhs_commit))) => (lhs_commit, rhs_commit),
420 _ => return lhs_oid.cmp(rhs_oid),
421 };
422
423 let merge_base_oid =
424 dag.query_gca_one(vec![*lhs_oid, *rhs_oid].into_iter().collect::<CommitSet>());
425 let merge_base_oid = match merge_base_oid {
426 Err(_) => return lhs_oid.cmp(rhs_oid),
427 Ok(None) => None,
428 Ok(Some(merge_base_oid)) => NonZeroOid::try_from(merge_base_oid).ok(),
429 };
430
431 match merge_base_oid {
432 Some(merge_base_oid) if merge_base_oid == *lhs_oid => Ordering::Less,
434 Some(merge_base_oid) if merge_base_oid == *rhs_oid => Ordering::Greater,
435
436 Some(_) | None => match lhs_commit.get_time().cmp(&rhs_commit.get_time()) {
440 result @ Ordering::Less | result @ Ordering::Greater => result,
441 Ordering::Equal => lhs_oid.cmp(rhs_oid),
442 },
443 }
444 };
445
446 root_commit_oids.sort_by(compare);
447 root_commit_oids
448 }
449
450 #[instrument(skip(commit_descriptors, graph))]
451 fn get_child_output(
452 glyphs: &Glyphs,
453 graph: &SmartlogGraph,
454 root_oids: &[NonZeroOid],
455 commit_descriptors: &mut [&mut dyn NodeDescriptor],
456 head_oid: Option<NonZeroOid>,
457 current_oid: NonZeroOid,
458 last_child_line_char: Option<&str>,
459 ) -> eyre::Result<Vec<StyledString>> {
460 let current_node = &graph.nodes[¤t_oid];
461 let is_head = Some(current_oid) == head_oid;
462
463 let mut lines = vec![];
464
465 if let Some(AncestorInfo { oid: _, distance }) = current_node.ancestor_info {
466 lines.push(
467 StyledStringBuilder::new()
468 .append_plain(glyphs.commit_omitted)
469 .append_plain(" ")
470 .append_styled(
471 Pluralize {
472 determiner: None,
473 amount: distance,
474 unit: ("omitted commit", "omitted commits"),
475 }
476 .to_string(),
477 Effect::Dim,
478 )
479 .build(),
480 );
481 lines.push(StyledString::plain(glyphs.vertical_ellipsis));
482 };
483
484 if let [_, merge_parents @ ..] = current_node.parents.as_slice() {
485 if !merge_parents.is_empty() {
486 for merge_parent_oid in merge_parents {
487 let merge_parent_node = &graph.nodes[merge_parent_oid];
488 lines.push(
489 StyledStringBuilder::new()
490 .append_plain(last_child_line_char.unwrap_or(glyphs.line))
491 .append_plain(" ")
492 .append_styled(
493 format!("{} (merge) ", glyphs.commit_merge),
494 BaseColor::Blue.dark(),
495 )
496 .append(render_node_descriptors(
497 glyphs,
498 &merge_parent_node.object,
499 commit_descriptors,
500 )?)
501 .build(),
502 );
503 }
504 lines.push(StyledString::plain(format!(
505 "{}{}",
506 glyphs.line_with_offshoot, glyphs.merge,
507 )));
508 }
509 }
510
511 lines.push({
512 let cursor = match (current_node.is_main, current_node.is_obsolete, is_head) {
513 (false, false, false) => glyphs.commit_visible,
514 (false, false, true) => glyphs.commit_visible_head,
515 (false, true, false) => glyphs.commit_obsolete,
516 (false, true, true) => glyphs.commit_obsolete_head,
517 (true, false, false) => glyphs.commit_main,
518 (true, false, true) => glyphs.commit_main_head,
519 (true, true, false) => glyphs.commit_main_obsolete,
520 (true, true, true) => glyphs.commit_main_obsolete_head,
521 };
522 let text = render_node_descriptors(glyphs, ¤t_node.object, commit_descriptors)?;
523 let first_line = StyledStringBuilder::new()
524 .append_plain(cursor)
525 .append_plain(" ")
526 .append(text)
527 .build();
528 if is_head {
529 set_effect(first_line, Effect::Bold)
530 } else {
531 first_line
532 }
533 });
534
535 if current_node.num_omitted_descendants > 0 {
536 lines.push(StyledString::plain(glyphs.vertical_ellipsis));
537 lines.push(
538 StyledStringBuilder::new()
539 .append_plain(glyphs.commit_omitted)
540 .append_plain(" ")
541 .append_styled(
542 Pluralize {
543 determiner: None,
544 amount: current_node.num_omitted_descendants,
545 unit: ("omitted descendant commit", "omitted descendant commits"),
546 }
547 .to_string(),
548 Effect::Dim,
549 )
550 .build(),
551 );
552 };
553
554 let children: Vec<ChildInfo> = current_node
555 .children
556 .iter()
557 .filter(
558 |ChildInfo {
559 oid,
560 is_merge_child: _,
561 }| graph.nodes.contains_key(oid),
562 )
563 .cloned()
564 .collect();
565 let descendants: HashSet<ChildInfo> = current_node
566 .descendants
567 .iter()
568 .filter(
569 |ChildInfo {
570 oid,
571 is_merge_child: _,
572 }| graph.nodes.contains_key(oid),
573 )
574 .cloned()
575 .collect();
576 for (child_idx, child_info) in children.iter().chain(descendants.iter()).enumerate() {
577 let ChildInfo {
578 oid: child_oid,
579 is_merge_child,
580 } = child_info;
581 if root_oids.contains(child_oid) {
582 continue;
584 }
585 if *is_merge_child {
586 lines.push(
591 StyledStringBuilder::new()
592 .append_styled(
595 format!("{} (merge) ", glyphs.commit_merge),
596 BaseColor::Blue.dark(),
597 )
598 .append(render_node_descriptors(
599 glyphs,
600 &graph.nodes[child_oid].object,
601 commit_descriptors,
602 )?)
603 .build(),
604 );
605 continue;
606 }
607
608 let is_last_child = child_idx == (children.len() + descendants.len()) - 1;
609 lines.push(StyledString::plain(
610 if !is_last_child || last_child_line_char.is_some() {
611 format!("{}{}", glyphs.line_with_offshoot, glyphs.split)
612 } else if current_node.descendants.is_empty() {
613 glyphs.line.to_string()
614 } else {
615 glyphs.vertical_ellipsis.to_string()
616 },
617 ));
618
619 let child_output = get_child_output(
620 glyphs,
621 graph,
622 root_oids,
623 commit_descriptors,
624 head_oid,
625 *child_oid,
626 None,
627 )?;
628 for child_line in child_output {
629 let line = if is_last_child {
630 match last_child_line_char {
631 Some(last_child_line_char) => StyledStringBuilder::new()
632 .append_plain(format!("{last_child_line_char} "))
633 .append(child_line)
634 .build(),
635 None => child_line,
636 }
637 } else {
638 StyledStringBuilder::new()
639 .append_plain(format!("{} ", glyphs.line))
640 .append(child_line)
641 .build()
642 };
643 lines.push(line)
644 }
645 }
646 Ok(lines)
647 }
648
649 #[instrument(skip(commit_descriptors, graph))]
651 fn get_output(
652 glyphs: &Glyphs,
653 dag: &Dag,
654 graph: &SmartlogGraph,
655 commit_descriptors: &mut [&mut dyn NodeDescriptor],
656 head_oid: Option<NonZeroOid>,
657 root_oids: &[NonZeroOid],
658 ) -> eyre::Result<Vec<StyledString>> {
659 let mut lines = Vec::new();
660
661 let has_real_parent = |oid: NonZeroOid, parent_oid: NonZeroOid| -> eyre::Result<bool> {
667 let parents = dag.query_parents(CommitSet::from(oid))?;
668 let result = dag.set_contains(&parents, parent_oid)?;
669 Ok(result)
670 };
671
672 for (root_idx, root_oid) in root_oids.iter().enumerate() {
673 if !dag.set_is_empty(&dag.query_parents(CommitSet::from(*root_oid))?)? {
674 let line = if root_idx > 0 && has_real_parent(*root_oid, root_oids[root_idx - 1])? {
675 StyledString::plain(glyphs.line.to_owned())
676 } else {
677 StyledString::plain(glyphs.vertical_ellipsis.to_owned())
678 };
679 lines.push(line);
680 } else if root_idx > 0 {
681 lines.push(StyledString::new());
684 }
685
686 let last_child_line_char = {
687 if root_idx == root_oids.len() - 1 {
688 None
689 } else if has_real_parent(root_oids[root_idx + 1], *root_oid)? {
690 Some(glyphs.line)
691 } else {
692 Some(glyphs.vertical_ellipsis)
693 }
694 };
695
696 let child_output = get_child_output(
697 glyphs,
698 graph,
699 root_oids,
700 commit_descriptors,
701 head_oid,
702 *root_oid,
703 last_child_line_char,
704 )?;
705 lines.extend(child_output.into_iter());
706 }
707
708 Ok(lines)
709 }
710
711 #[instrument(skip(commit_descriptors, graph))]
713 pub fn render_graph(
714 effects: &Effects,
715 repo: &Repo,
716 dag: &Dag,
717 graph: &SmartlogGraph,
718 head_oid: Option<NonZeroOid>,
719 commit_descriptors: &mut [&mut dyn NodeDescriptor],
720 ) -> eyre::Result<Vec<StyledString>> {
721 let root_oids = split_commit_graph_by_roots(repo, dag, graph);
722 let lines = get_output(
723 effects.get_glyphs(),
724 dag,
725 graph,
726 commit_descriptors,
727 head_oid,
728 &root_oids,
729 )?;
730 Ok(lines)
731 }
732
733 #[derive(Debug, Default)]
735 pub struct SmartlogOptions {
736 pub event_id: Option<isize>,
740
741 pub revset: Option<Revset>,
745
746 pub resolve_revset_options: ResolveRevsetOptions,
748
749 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 reverse = if reverse {
829 writeln!(
830 effects.get_error_stream(),
831 "WARNING: The `--reverse` flag is deprecated.\nPlease use the `branchless.smartlog.reverse` configuration option."
832 )?;
833 true
834 } else {
835 get_smartlog_reverse(&repo)?
836 };
837
838 let mut lines = render_graph(
839 &effects.reverse_order(reverse),
840 &repo,
841 &dag,
842 &graph,
843 references_snapshot.head_oid,
844 &mut [
845 &mut CommitOidDescriptor::new(true)?,
846 &mut RelativeTimeDescriptor::new(&repo, SystemTime::now())?,
847 &mut ObsolescenceExplanationDescriptor::new(
848 &event_replayer,
849 event_replayer.make_default_cursor(),
850 )?,
851 &mut BranchesDescriptor::new(
852 &repo,
853 &head_info,
854 &references_snapshot,
855 &Redactor::Disabled,
856 )?,
857 &mut DifferentialRevisionDescriptor::new(&repo, &Redactor::Disabled)?,
858 &mut CommitMessageDescriptor::new(&Redactor::Disabled)?,
859 ],
860 )?
861 .into_iter();
862 while let Some(line) = if reverse {
863 lines.next_back()
864 } else {
865 lines.next()
866 } {
867 writeln!(
868 effects.get_output_stream(),
869 "{}",
870 effects.get_glyphs().render(line)?
871 )?;
872 }
873
874 if !resolve_revset_options.show_hidden_commits
875 && get_hint_enabled(&repo, Hint::SmartlogFixAbandoned)?
876 {
877 let commits_with_abandoned_children: CommitSet = graph
878 .nodes
879 .iter()
880 .filter_map(|(oid, node)| {
881 if node.is_obsolete
882 && find_rewrite_target(&event_replayer, event_cursor, *oid).is_some()
883 {
884 Some(*oid)
885 } else {
886 None
887 }
888 })
889 .collect();
890 let children = dag.query_children(commits_with_abandoned_children)?;
891 let num_abandoned_children =
892 dag.set_count(&children.difference(&dag.query_obsolete_commits()))?;
893 if num_abandoned_children > 0 {
894 writeln!(
895 effects.get_output_stream(),
896 "{}: there {} in your commit graph",
897 effects.get_glyphs().render(get_hint_string())?,
898 Pluralize {
899 determiner: Some(("is", "are")),
900 amount: num_abandoned_children,
901 unit: ("abandoned commit", "abandoned commits"),
902 },
903 )?;
904 writeln!(
905 effects.get_output_stream(),
906 "{}: to fix this, run: git restack",
907 effects.get_glyphs().render(get_hint_string())?,
908 )?;
909 print_hint_suppression_notice(effects, Hint::SmartlogFixAbandoned)?;
910 }
911 }
912
913 Ok(Ok(()))
914}
915
916#[instrument]
918pub fn command_main(ctx: CommandContext, args: SmartlogArgs) -> EyreExitOr<()> {
919 let CommandContext {
920 effects,
921 git_run_info,
922 } = ctx;
923 let SmartlogArgs {
924 event_id,
925 revset,
926 resolve_revset_options,
927 reverse,
928 exact,
929 } = args;
930
931 smartlog(
932 &effects,
933 &git_run_info,
934 SmartlogOptions {
935 event_id,
936 revset,
937 resolve_revset_options,
938 reverse,
939 exact,
940 },
941 )
942}