diff --git a/CHANGELOG.md b/CHANGELOG.md index f16c56d4ce0..5029fad8356 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### New features +* `jj log` output is now topologically grouped. + [#242](https://github.com/martinvonz/jj/issues/242) + ### Fixed bugs ## [0.8.0] - 2023-07-09 diff --git a/lib/src/revset_graph.rs b/lib/src/revset_graph.rs index 92cc66d3a54..1078db730a0 100644 --- a/lib/src/revset_graph.rs +++ b/lib/src/revset_graph.rs @@ -14,7 +14,8 @@ #![allow(missing_docs)] -use std::collections::HashMap; +use std::collections::{hash_map, HashMap, VecDeque}; +use std::mem; use crate::backend::CommitId; @@ -46,7 +47,6 @@ impl RevsetGraphEdge { } } - #[cfg(test)] // TODO: remove fn is_reachable(&self) -> bool { self.edge_type != RevsetGraphEdgeType::Missing } @@ -99,6 +99,147 @@ impl Iterator for ReverseRevsetGraphIterator { } } +/// Graph iterator adapter to group topological branches. +/// +/// Basic idea is to build a linear stack towards fork point (as if we were +/// doing DFS walk.) At fork point, stacks will be simply concatenated. If +/// a commit belongs to the currently-emitting branch, it will be emitted +/// instantly. Otherwise, the commit will be queued until its ancestor reaches +/// to the emitting branch. +/// +/// The branch containing the first item in the input iterator will be emitted +/// first. The first item is often the working-copy commit. +#[derive(Clone, Debug)] +pub struct TopoGroupedRevsetGraphIterator<I> { + input_iter: I, + /// Id of linear sub graph whose entries will be emitted instantly. + /// + /// `None` means either uninitialized or reached to root/missing. + emitting_root_id: Option<CommitId>, + /// `root_id: [descendants]` for each linear sub graph. + linearized: HashMap<Option<CommitId>, VecDeque<TopoGroupedGraphWorkItem>>, +} + +#[derive(Clone, Debug)] +enum TopoGroupedGraphWorkItem { + /// Graph node to be emitted. + Entry((CommitId, Vec<RevsetGraphEdge>)), + /// Other linear sub graph to be merged in. + Merge(CommitId), +} + +impl<I> TopoGroupedRevsetGraphIterator<I> +where + I: Iterator<Item = (CommitId, Vec<RevsetGraphEdge>)>, +{ + /// Wraps the given iterator to group topological branches. The input + /// iterator must be topologically ordered. + pub fn new(input_iter: I) -> Self { + TopoGroupedRevsetGraphIterator { + input_iter, + emitting_root_id: None, + linearized: HashMap::from([(None, VecDeque::new())]), + } + } + + fn is_emitting_queue_empty(&self) -> bool { + let queue = self.linearized.get(&self.emitting_root_id).unwrap(); + queue.is_empty() + } + + fn next_emittable_item(&mut self) -> Option<TopoGroupedGraphWorkItem> { + // Once the input iterator gets fully consumed, there should be exactly one + // emitting queue (plus queues to be merged in.) This iterator emits None + // when the last queue gets empty. Therefore, the input iterator doesn't + // have to be fused. + while self.is_emitting_queue_empty() { + if let Some((id, edges)) = self.input_iter.next() { + self.enqueue_one(id, edges); + } else { + break; + } + } + let queue = self.linearized.get_mut(&self.emitting_root_id).unwrap(); + if let Some(item) = queue.pop_front() { + Some(item) + } else { + assert_eq!(self.linearized.len(), 1, "items must have been emitted"); + None + } + } + + fn enqueue_one(&mut self, current_id: CommitId, edges: Vec<RevsetGraphEdge>) { + let current_root_id = Some(current_id.clone()); + let mut reachable_edges = edges.iter().filter(|edge| edge.is_reachable()).fuse(); + // If the current branch is emitting (or nothing is emitting), slide the root + // pointer down. + let next_root_id = reachable_edges.next().map(|edge| edge.target.clone()); + let was_currently_emitting = self.emitting_root_id == current_root_id; + if self.emitting_root_id.is_none() || was_currently_emitting { + self.emitting_root_id = next_root_id.clone(); + } + if reachable_edges.next().is_none() { + // Not a merge, queue up linear sub graph. + let mut queue = self.linearized.remove(¤t_root_id).unwrap_or_default(); + queue.push_back(TopoGroupedGraphWorkItem::Entry((current_id, edges))); + match self.linearized.entry(next_root_id) { + hash_map::Entry::Occupied(mut entry) => { + // At fork point, append or prepend sub graph, so each sub graph will be + // emitted contiguously. + let other = entry.get_mut(); + if was_currently_emitting { + mem::swap(other, &mut queue); + } + other.extend(queue); + } + hash_map::Entry::Vacant(entry) => { + entry.insert(queue); + } + } + } else { + // At merge point, track each ancestor separately, and link to the current + // sub graph. + for edge in edges.iter().filter(|edge| edge.is_reachable()) { + // Just append no matter if it is a fork point. We could reorder by + // was_currently_emitting, but edges across merge point wouldn't be simplified. + let next_root_id = Some(edge.target.clone()); + let queue = self.linearized.entry(next_root_id).or_default(); + queue.push_back(TopoGroupedGraphWorkItem::Merge(current_id.clone())); + } + // Current node and its descendants will be folded into one of the ancestors, + // but we don't know which one will be emitted first. + let queue = self.linearized.entry(current_root_id).or_default(); + queue.push_back(TopoGroupedGraphWorkItem::Entry((current_id, edges))); + } + } + + fn merge_in(&mut self, root_id: &Option<CommitId>) { + if let Some(mut other) = self.linearized.remove(root_id) { + let queue = self.linearized.get_mut(&self.emitting_root_id).unwrap(); + mem::swap(queue, &mut other); + queue.extend(other); + } else { + // Would have been emitted by another branch + } + } +} + +impl<I> Iterator for TopoGroupedRevsetGraphIterator<I> +where + I: Iterator<Item = (CommitId, Vec<RevsetGraphEdge>)>, +{ + type Item = (CommitId, Vec<RevsetGraphEdge>); + + fn next(&mut self) -> Option<Self::Item> { + loop { + match self.next_emittable_item()? { + TopoGroupedGraphWorkItem::Entry(next) => return Some(next), + TopoGroupedGraphWorkItem::Merge(id) => self.merge_in(&Some(id)), + } + } + } +} + #[cfg(test)] mod tests { use std::cmp::Ordering; @@ -275,4 +416,463 @@ mod tests { A "###); } + + fn topo_grouped<I>(graph_iter: I) -> TopoGroupedRevsetGraphIterator<I::IntoIter> + where + I: IntoIterator<Item = (CommitId, Vec<RevsetGraphEdge>)>, + { + TopoGroupedRevsetGraphIterator::new(graph_iter.into_iter()) + } + + #[test] + fn test_topo_grouped_multiple_roots() { + let graph = vec![ + (id('C'), vec![missing('Y')]), + (id('B'), vec![missing('X')]), + (id('A'), vec![]), + ]; + insta::assert_snapshot!(format_graph(graph.iter().cloned()), @r###" + C # missing(Y) + + B # missing(X) + + A + "###); + insta::assert_snapshot!(format_graph(topo_grouped(graph.iter().cloned())), @r###" + C # missing(Y) + + B # missing(X) + + A + "###); + + // All items can be lazy. + let mut iter = topo_grouped(graph.iter().cloned().peekable()); + assert_eq!(iter.next().unwrap().0, id('C')); + assert_eq!(iter.input_iter.peek().unwrap().0, id('B')); + assert_eq!(iter.next().unwrap().0, id('B')); + assert_eq!(iter.input_iter.peek().unwrap().0, id('A')); + } + + #[test] + fn test_topo_grouped_trivial_fork() { + let graph = vec![ + (id('E'), vec![direct('B')]), + (id('D'), vec![direct('A')]), + (id('C'), vec![direct('B')]), + (id('B'), vec![direct('A')]), + (id('A'), vec![]), + ]; + insta::assert_snapshot!(format_graph(graph.iter().cloned()), @r###" + E # direct(B) + | + | D # direct(A) + | | + | | C # direct(B) + | | * + B | # direct(A) + |/ + A + "###); + // D-A is found earlier than B-A, but B is emitted first since it belongs to the + // emitting branch. Therefore, C-B is placed closely. + insta::assert_snapshot!(format_graph(topo_grouped(graph.iter().cloned())), @r###" + E # direct(B) + | + | C # direct(B) + |/ + B # direct(A) + | + | D # direct(A) + |/ + A + "###); + + // E can be lazy, then D and C will be queued. + let mut iter = topo_grouped(graph.iter().cloned().peekable()); + assert_eq!(iter.next().unwrap().0, id('E')); + assert_eq!(iter.input_iter.peek().unwrap().0, id('D')); + assert_eq!(iter.next().unwrap().0, id('C')); + assert_eq!(iter.input_iter.peek().unwrap().0, id('B')); + assert_eq!(iter.next().unwrap().0, id('B')); + assert_eq!(iter.input_iter.peek().unwrap().0, id('A')); + assert_eq!(iter.next().unwrap().0, id('D')); + assert_eq!(iter.input_iter.peek().unwrap().0, id('A')); + } + + #[test] + fn test_topo_grouped_fork_interleaved() { + let graph = vec![ + (id('E'), vec![direct('C')]), + (id('D'), vec![direct('B')]), + (id('C'), vec![direct('A')]), + (id('B'), vec![direct('A')]), + (id('A'), vec![]), + ]; + insta::assert_snapshot!(format_graph(graph.iter().cloned()), @r###" + E # direct(C) + | + | D # direct(B) + | | + C | # direct(A) + | | + | B # direct(A) + |/ + A + "###); + insta::assert_snapshot!(format_graph(topo_grouped(graph.iter().cloned())), @r###" + E # direct(C) + | + C # direct(A) + | + | D # direct(B) + | | + | B # direct(A) + |/ + A + "###); + + // E can be lazy, then D will be queued, then B. + let mut iter = topo_grouped(graph.iter().cloned().peekable()); + assert_eq!(iter.next().unwrap().0, id('E')); + assert_eq!(iter.input_iter.peek().unwrap().0, id('D')); + assert_eq!(iter.next().unwrap().0, id('C')); + assert_eq!(iter.input_iter.peek().unwrap().0, id('B')); + assert_eq!(iter.next().unwrap().0, id('D')); + assert_eq!(iter.input_iter.peek().unwrap().0, id('A')); + } + + #[test] + fn test_topo_grouped_merge_interleaved() { + let graph = vec![ + (id('F'), vec![direct('E')]), + (id('E'), vec![direct('C'), direct('D')]), + (id('D'), vec![direct('B')]), + (id('C'), vec![direct('A')]), + (id('B'), vec![direct('A')]), + (id('A'), vec![]), + ]; + insta::assert_snapshot!(format_graph(graph.iter().cloned()), @r###" + F # direct(E) + | + E # direct(C), direct(D) + |\ + | D # direct(B) + | | + C | # direct(A) + | | + | B # direct(A) + |/ + A + "###); + insta::assert_snapshot!(format_graph(topo_grouped(graph.iter().cloned())), @r###" + F # direct(E) + | + E # direct(C), direct(D) + |\ + C | # direct(A) + | | + | D # direct(B) + | | + | B # direct(A) + |/ + A + "###); + + // F and E can be lazy, then D will be queued, then B. + let mut iter = topo_grouped(graph.iter().cloned().peekable()); + assert_eq!(iter.next().unwrap().0, id('F')); + assert_eq!(iter.input_iter.peek().unwrap().0, id('E')); + assert_eq!(iter.next().unwrap().0, id('E')); + assert_eq!(iter.input_iter.peek().unwrap().0, id('D')); + assert_eq!(iter.next().unwrap().0, id('C')); + assert_eq!(iter.input_iter.peek().unwrap().0, id('B')); + assert_eq!(iter.next().unwrap().0, id('D')); + assert_eq!(iter.input_iter.peek().unwrap().0, id('A')); + } + + #[test] + fn test_topo_grouped_merge_but_missing() { + let graph = vec![ + (id('E'), vec![direct('D')]), + (id('D'), vec![missing('Y'), direct('C')]), + (id('C'), vec![direct('B'), missing('X')]), + (id('B'), vec![direct('A')]), + (id('A'), vec![]), + ]; + insta::assert_snapshot!(format_graph(graph.iter().cloned()), @r###" + E # direct(D) + | + D # missing(Y), direct(C) + | + C # direct(B), missing(X) + | + B # direct(A) + | + A + "###); + insta::assert_snapshot!(format_graph(topo_grouped(graph.iter().cloned())), @r###" + E # direct(D) + | + D # missing(Y), direct(C) + | + C # direct(B), missing(X) + | + B # direct(A) + | + A + "###); + + // All items can be lazy. + let mut iter = topo_grouped(graph.iter().cloned().peekable()); + assert_eq!(iter.next().unwrap().0, id('E')); + assert_eq!(iter.input_iter.peek().unwrap().0, id('D')); + assert_eq!(iter.next().unwrap().0, id('D')); + assert_eq!(iter.input_iter.peek().unwrap().0, id('C')); + assert_eq!(iter.next().unwrap().0, id('C')); + assert_eq!(iter.input_iter.peek().unwrap().0, id('B')); + assert_eq!(iter.next().unwrap().0, id('B')); + assert_eq!(iter.input_iter.peek().unwrap().0, id('A')); + } + + #[test] + fn test_topo_grouped_merge_criss_cross() { + let graph = vec![ + (id('G'), vec![direct('E')]), + (id('F'), vec![direct('D')]), + (id('E'), vec![direct('B'), direct('C')]), + (id('D'), vec![direct('B'), direct('C')]), + (id('C'), vec![direct('A')]), + (id('B'), vec![direct('A')]), + (id('A'), vec![]), + ]; + insta::assert_snapshot!(format_graph(graph.iter().cloned()), @r###" + G # direct(E) + | + | F # direct(D) + | | + E | # direct(B), direct(C) + |\ \ + | | D # direct(B), direct(C) + | |/* + | C # direct(A) + | | + B | # direct(A) + |/ + A + "###); + insta::assert_snapshot!(format_graph(topo_grouped(graph.iter().cloned())), @r###" + G # direct(E) + | + E # direct(B), direct(C) + |\ + | | F # direct(D) + | | | + | | D # direct(B), direct(C) + | |/* + B | # direct(A) + | | + | C # direct(A) + |/ + A + "###); + } + + #[test] + fn test_topo_grouped_merge_descendants_interleaved() { + let graph = vec![ + (id('H'), vec![direct('F')]), + (id('G'), vec![direct('E')]), + (id('F'), vec![direct('D')]), + (id('E'), vec![direct('C')]), + (id('D'), vec![direct('C'), direct('B')]), + (id('C'), vec![direct('A')]), + (id('B'), vec![direct('A')]), + (id('A'), vec![]), + ]; + insta::assert_snapshot!(format_graph(graph.iter().cloned()), @r###" + H # direct(F) + | + | G # direct(E) + | | + F | # direct(D) + | | + | E # direct(C) + | | + D | # direct(C), direct(B) + |\ + C | # direct(A) + | | + | B # direct(A) + |/ + A + "###); + // G-E could be moved after the merge point D, but the resulting graph wouldn't + // look nicer. + insta::assert_snapshot!(format_graph(topo_grouped(graph.iter().cloned())), @r###" + H # direct(F) + | + F # direct(D) + | + | G # direct(E) + | | + | E # direct(C) + | | + D | # direct(C), direct(B) + |\ + C | # direct(A) + | | + | B # direct(A) + |/ + A + "###); + } + + #[test] + fn test_topo_grouped_merge_multiple_roots() { + let graph = vec![ + (id('D'), vec![direct('C')]), + (id('C'), vec![direct('A'), direct('B')]), + (id('B'), vec![missing('X')]), + (id('A'), vec![]), + ]; + insta::assert_snapshot!(format_graph(graph.iter().cloned()), @r###" + D # direct(C) + | + C # direct(A), direct(B) + |\ + | B # missing(X) + | + A + "###); + // A is emitted first just because it's the first parent. This doesn't simplify + // the graph, but doesn't make it worse either. + insta::assert_snapshot!(format_graph(topo_grouped(graph.iter().cloned())), @r###" + D # direct(C) + | + C # direct(A), direct(B) + |\ + A | + / + B # missing(X) + "###); + } + + #[test] + fn test_topo_grouped_merge_and_fork() { + let graph = vec![ + (id('J'), vec![direct('F')]), + (id('I'), vec![direct('E')]), + (id('H'), vec![direct('G')]), + (id('G'), vec![direct('D'), direct('E')]), + (id('F'), vec![direct('C')]), + (id('E'), vec![direct('B')]), + (id('D'), vec![direct('B')]), + (id('C'), vec![direct('A')]), + (id('B'), vec![direct('A')]), + (id('A'), vec![]), + ]; + insta::assert_snapshot!(format_graph(graph.iter().cloned()), @r###" + J # direct(F) + | + | I # direct(E) + | | + | | H # direct(G) + | | | + | | G # direct(D), direct(E) + | |/| + F | | # direct(C) + | | | + | E | # direct(B) + | | | + | | D # direct(B) + | |/ + C | # direct(A) + | | + | B # direct(A) + |/ + A + "###); + insta::assert_snapshot!(format_graph(topo_grouped(graph.iter().cloned())), @r###" + J # direct(F) + | + F # direct(C) + | + C # direct(A) + | + | I # direct(E) + | | + | | H # direct(G) + | | | + | | G # direct(D), direct(E) + | |/| + | E | # direct(B) + | | | + | | D # direct(B) + | |/ + | B # direct(A) + |/ + A + "###); + } + + #[test] + fn test_topo_grouped_merge_and_fork_multiple_roots() { + let graph = vec![ + (id('J'), vec![direct('F')]), + (id('I'), vec![direct('G')]), + (id('H'), vec![direct('E')]), + (id('G'), vec![direct('B'), direct('E')]), + (id('F'), vec![direct('D')]), + (id('E'), vec![direct('C')]), + (id('D'), vec![direct('A')]), + (id('C'), vec![direct('A')]), + (id('B'), vec![missing('X')]), + (id('A'), vec![]), + ]; + insta::assert_snapshot!(format_graph(graph.iter().cloned()), @r###" + J # direct(F) + | + | I # direct(G) + | | + | | H # direct(E) + | | | + | G | # direct(B), direct(E) + | |\| + F | | # direct(D) + | | | + | | E # direct(C) + | | | + D | | # direct(A) + | | | + | | C # direct(A) + | | * + | B # missing(X) + | + A + "###); + // Second parent of G is emitted first. If G and its descendants were folded + // into the first parent B, topological ordering wouldn't be respected. + insta::assert_snapshot!(format_graph(topo_grouped(graph.iter().cloned())), @r###" + J # direct(F) + | + F # direct(D) + | + D # direct(A) + | + | H # direct(E) + | | + | | I # direct(G) + | | | + | | G # direct(B), direct(E) + | |/| + | E | # direct(C) + | | | + | C | # direct(A) + |/ / + A | + / + B # missing(X) + "###); + } } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 00908983cc7..e682edfa6e1 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -43,7 +43,10 @@ use jj_lib::repo_path::RepoPath; use jj_lib::revset::{ RevsetAliasesMap, RevsetExpression, RevsetFilterPredicate, RevsetIteratorExt, }; -use jj_lib::revset_graph::{ReverseRevsetGraphIterator, RevsetGraphEdge, RevsetGraphEdgeType}; +use jj_lib::revset_graph::{ + ReverseRevsetGraphIterator, RevsetGraphEdge, RevsetGraphEdgeType, + TopoGroupedRevsetGraphIterator, +}; use jj_lib::rewrite::{back_out_commit, merge_commit_trees, rebase_commit, DescendantRebaser}; use jj_lib::settings::UserSettings; use jj_lib::tree::{merge_trees, Tree}; @@ -1589,9 +1592,11 @@ fn cmd_log(ui: &mut Ui, command: &CommandHelper, args: &LogArgs) -> Result<(), C let default_node_symbol = graph.default_node_symbol().to_owned(); let iter: Box<dyn Iterator<Item = (CommitId, Vec<RevsetGraphEdge>)>> = if args.reversed { + // Don't reorder. Reversed graph typically ends with many heads, and + // topo-grouped iterator would give worse result. Box::new(ReverseRevsetGraphIterator::new(revset.iter_graph())) } else { - revset.iter_graph() + Box::new(TopoGroupedRevsetGraphIterator::new(revset.iter_graph())) }; for (commit_id, edges) in iter { let mut graphlog_edges = vec![]; diff --git a/tests/test_abandon_command.rs b/tests/test_abandon_command.rs index c67c95b4b9f..4232a1c44f2 100644 --- a/tests/test_abandon_command.rs +++ b/tests/test_abandon_command.rs @@ -84,11 +84,11 @@ fn test_rebase_branch_with_merge() { "###); insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" @ + │ ◉ b + ├─╯ + ◉ a e?? │ ◉ d e?? │ ◉ c - │ │ ◉ b - ├───╯ - ◉ │ a e?? ├─╯ ◉ "###); diff --git a/tests/test_chmod_command.rs b/tests/test_chmod_command.rs index 8fc5be042f6..dac6867011c 100644 --- a/tests/test_chmod_command.rs +++ b/tests/test_chmod_command.rs @@ -149,10 +149,10 @@ fn test_chmod_file_dir_deletion_conflicts() { insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" @ file_deletion ├─╮ + ◉ │ deletion │ │ ◉ file_dir │ ╭─┤ │ │ ◉ dir - ◉ │ │ deletion ├───╯ │ ◉ file ├─╯ diff --git a/tests/test_duplicate_command.rs b/tests/test_duplicate_command.rs index 84941b370c2..538b7105499 100644 --- a/tests/test_duplicate_command.rs +++ b/tests/test_duplicate_command.rs @@ -160,12 +160,12 @@ fn test_duplicate_many() { ◉ 0f7430f2727a e ├─╮ ◉ │ 2181781b4f81 d - │ ◉ fa167d18a83a b │ │ @ 921dde6e55c0 e │ │ ├─╮ │ │ ◉ │ ebd06dba20ec d ├───╯ │ ◉ │ │ c0cb3a0b73e7 c + │ ◉ │ fa167d18a83a b ├─╯ │ │ ◉ 1394f625cbbd b ├─────╯ @@ -195,16 +195,16 @@ fn test_duplicate_many() { ◉ 9bd4389f5d47 e ├─╮ ◉ │ d94e4c55a68b d - │ │ ◉ c6f7f8c4512e a - │ │ │ @ 921dde6e55c0 e - │ ╭───┤ - │ │ │ ◉ ebd06dba20ec d - ├─────╯ - ◉ │ │ c0cb3a0b73e7 c - │ ◉ │ 1394f625cbbd b - ├─╯ │ - ◉ │ 2443ea76b0b1 a + │ │ @ 921dde6e55c0 e + │ ╭─┤ + │ │ ◉ ebd06dba20ec d ├───╯ + ◉ │ c0cb3a0b73e7 c + │ ◉ 1394f625cbbd b + ├─╯ + ◉ 2443ea76b0b1 a + │ ◉ c6f7f8c4512e a + ├─╯ ◉ 000000000000 "###); diff --git a/tests/test_git_colocated.rs b/tests/test_git_colocated.rs index 7f68053ec16..20c47de800e 100644 --- a/tests/test_git_colocated.rs +++ b/tests/test_git_colocated.rs @@ -340,8 +340,8 @@ fn test_git_colocated_external_checkout() { // be abandoned. (#1042) insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" @ 0521ce3b8c4e29aab79f3c750e2845dcbc4c3874 + ◉ a86754f975f953fa25da4265764adc0c62e9ce6b master HEAD@git A │ ◉ 66f4d1806ae41bd604f69155dece64062a0056cf B - ◉ │ a86754f975f953fa25da4265764adc0c62e9ce6b master HEAD@git A ├─╯ ◉ 0000000000000000000000000000000000000000 "###); diff --git a/tests/test_new_command.rs b/tests/test_new_command.rs index 8f6be198e68..5147b0daa60 100644 --- a/tests/test_new_command.rs +++ b/tests/test_new_command.rs @@ -139,8 +139,8 @@ fn test_new_insert_after() { ╭─┤ @ │ G ├───╮ - │ ◉ │ E ◉ │ │ D + │ ◉ │ E ├─╯ │ │ ◉ B │ ◉ A @@ -161,8 +161,8 @@ fn test_new_insert_after() { ◉ │ G ├───╮ @ │ │ H - │ ◉ │ E ◉ │ │ D + │ ◉ │ E ├─╯ │ │ ◉ B │ ◉ A @@ -203,15 +203,15 @@ fn test_new_insert_after_children() { insta::assert_snapshot!(get_short_log_output(&test_env, &repo_path), @r###" @ G ├─╮ - │ │ ◉ F - │ │ ├─╮ - │ │ ◉ │ E - │ │ │ ◉ D - │ │ ├─╯ - ◉ │ │ C - ◉ │ │ B + ◉ │ C + ◉ │ B + ├─╯ + ◉ A + │ ◉ F + │ ├─╮ + │ ◉ │ E ├─╯ │ - ◉ │ A + │ ◉ D ├───╯ ◉ root "###); @@ -290,9 +290,9 @@ fn test_new_insert_before_root_successors() { insta::assert_snapshot!(get_short_log_output(&test_env, &repo_path), @r###" ◉ F ├─╮ + ◉ │ D │ │ ◉ C │ │ ◉ B - ◉ │ │ D │ │ ◉ A ├───╯ @ │ G @@ -359,13 +359,13 @@ fn test_new_insert_before_no_root_merge() { insta::assert_snapshot!(get_short_log_output(&test_env, &repo_path), @r###" ◉ F ├─╮ + ◉ │ D │ │ ◉ C - ◉ │ │ D │ │ ◉ B ├───╯ @ │ G - │ ◉ E ◉ │ A + │ ◉ E ├─╯ ◉ root "###); diff --git a/tests/test_rebase_command.rs b/tests/test_rebase_command.rs index 2eec5350f2a..bac7cab49a7 100644 --- a/tests/test_rebase_command.rs +++ b/tests/test_rebase_command.rs @@ -290,8 +290,8 @@ fn test_rebase_single_revision() { insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" @ d ├─╮ + ◉ │ b │ │ ◉ c - ◉ │ │ b ├───╯ │ ◉ a ├─╯ @@ -333,9 +333,9 @@ fn test_rebase_single_revision_merge_parent() { insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" @ d ├─╮ + ◉ │ b │ │ ◉ c │ ├─╯ - ◉ │ b │ ◉ a ├─╯ ◉ @@ -417,8 +417,8 @@ fn test_rebase_multiple_destinations() { insta::assert_snapshot!(stdout, @""); insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" ◉ a + ◉ b │ @ c - ◉ │ b ├─╯ ◉ "###); @@ -500,8 +500,8 @@ fn test_rebase_with_descendants() { @ d │ ◉ c ├─╯ + ◉ a │ ◉ b - ◉ │ a ├─╯ ◉ "###); @@ -529,8 +529,8 @@ fn test_rebase_with_descendants() { "###); insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" ◉ c + ◉ b │ @ d - ◉ │ b ├─╯ ◉ a ◉ diff --git a/tests/test_templater.rs b/tests/test_templater.rs index 25f04086daa..70fdb8c2cca 100644 --- a/tests/test_templater.rs +++ b/tests/test_templater.rs @@ -67,9 +67,9 @@ fn test_templater_branches() { let output = test_env.jj_cmd_success(&workspace_root, &["log", "-T", template]); insta::assert_snapshot!(output, @r###" ◉ b1bb3766d584 branch3?? + │ ◉ 21c33875443e branch1* + ├─╯ │ @ a5b4d15489cc branch2* new-branch - │ │ ◉ 21c33875443e branch1* - ├───╯ │ ◉ 8476341eb395 branch2@origin ├─╯ ◉ 000000000000