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(&current_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