graph: indent visual root in graph

When rendering a graph, if the history contains multiple "visual roots", actual roots or commits that look like roots (i.e. have their parents filtered out) can end up being vertically adjacent to unrelated commits, falsely appearing to be related. A fix for this issue was already attempted [1] a while ago. This happens because the commits fill the space from left to right and when a visual root ends, its column becomes free for the following commit even if they are not related. Once this happens the unrelated commit is rendered below the visual root. Because there is no special character or way to identify when a visual root is rendered making the graph confusing. By indenting the visual roots when there are still commits to show the vertical adjacency can be avoided. Add is_visual_root flag to git_graph making it visible in all graph states, give graph_update() a new function, graph_is_visual_root() to know if the current commit is a visual root and set is_visual_root. The different handled cases are: - If a visual root has children: similar to GRAPH_PRE_COMMIT state when octopus merges need space, an edge row needs to be printed to connect the child with the indented visual root. A new state GRAPH_PRE_ROOT is needed to connect the child with the visual root: * child of the visual root \ GRAPH_PRE_ROOT * visual root indented - If a visual root is child-less we can skip GRAPH_PRE_ROOT state and render the indented commit directly. * visual root indented * unrelated commit - If two or more visual roots are adjacent: by having a lookahead to the next commit that will be rendered, if the next commit is also a visual root and we are on a visual root, meaning two visual root adjacent in the history, the top one can omit the indent, making the one below to indent only once, if there are more adjacent visual commits, the indentation will increase for each adjacent one, cascading. * visual root * visual root * visual root * last commit Even if the last commit is a root, because there is nothing that will be rendered below we can omit the indentation on purpose. [1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/ Helped-by: Kristofer Karlsson <krka@spotify.com> Mentored-by: Karthik Nayak <karthik.188@gmail.com> Mentored-by: Chandra Pratap <chandrapratap3519@gmail.com> Signed-off-by: Pablo Sabater <pabloosabaterr@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Pablo Sabater committed Jul 14, 2026 at 14:09 UTC a34c00d96851009e94ec7618ad5c1464f920c80d
3 files changed +759
graph.c
+244
@@ -60,12 +60,23 @@ struct column {
60 * index into column_colors.
61 */
62 unsigned short color;
63 + /*
64 + * Marks if a commit is a non-first parent of a merge. These columns are
65 + * already visually connected to the merge commit and do not need
66 + * indentation.
67 + *
68 + * The first parent is the one that inherits the column and it can need
69 + * indentation if turns out to be a visual root and there's still
70 + * commits to render.
71 + */
72 + unsigned int is_merge_parent:1;
73 };
74
75 enum graph_state {
76 GRAPH_PADDING,
77 GRAPH_SKIP,
78 GRAPH_PRE_COMMIT,
79 + GRAPH_PRE_ROOT,
80 GRAPH_COMMIT,
81 GRAPH_POST_MERGE,
82 GRAPH_COLLAPSING
@@ -323,6 +334,51 @@ struct git_graph {
334 */
335 struct commit *lookahead[2];
336 int lookahead_nr;
337 +
338 + /*
339 + * If a commit is a visual root, we need to indent it to prevent
340 + * unrelated commits from being vertically adjacent to it.
341 + */
342 + unsigned int is_visual_root:1;
343 +
344 + /*
345 + * Indentation increases for each visual root adjacent to another visual
346 + * root, making visual root commits indentation cascade.
347 + */
348 + unsigned int visual_root_depth;
349 +
350 + /*
351 + * When a visual root is adjacent to other visual roots, the first one
352 + * can avoid indentation and the rest cascades, increasing the indentation
353 + * for each one.
354 + */
355 + unsigned int visual_root_cascade:1;
356 +
357 + /*
358 + * Set when the current commit was already present in graph->columns
359 + * before being processed.
360 + */
361 + unsigned int commit_in_columns:1;
362 +};
363 +
364 +struct graph_lookahead_flags {
365 +
366 + /*
367 + * Set when there will be a commit after the current one that will be
368 + * rendered.
369 + */
370 + unsigned int is_next_visible:1;
371 +
372 + /*
373 + * Set when the next visible commit is candidate to be a visual root.
374 + */
375 + unsigned int is_next_visual_root:1;
376 +
377 + /*
378 + * Set when the next visible commit will be rendered under the current
379 + * commit.
380 + */
381 + unsigned int next_has_column:1;
382 };
383
384 static inline int graph_needs_truncation(struct git_graph *graph, int lane)
@@ -399,6 +455,8 @@ struct git_graph *graph_init(struct rev_info *opt)
455 graph->lookahead[0] = NULL;
456 graph->lookahead[1] = NULL;
457 graph->lookahead_nr = 0;
458 + graph->visual_root_depth = 0;
459 + graph->visual_root_cascade = 0;
460 /*
461 * Start the column color at the maximum value, since we'll
462 * always increment it for the first commit we output.
@@ -581,6 +639,11 @@ static void graph_insert_into_new_columns(struct git_graph *graph,
639 struct commit *commit,
640 int idx)
641 {
642 + /*
643 + * Get the initial merge_layout before it's modified to know if this
644 + * is a merge.
645 + */
646 + int initial_merge_layout = graph->merge_layout;
647 int i = graph_find_new_column_by_commit(graph, commit);
648 int mapping_idx;
649
@@ -592,6 +655,7 @@ static void graph_insert_into_new_columns(struct git_graph *graph,
655 i = graph->num_new_columns++;
656 graph->new_columns[i].commit = commit;
657 graph->new_columns[i].color = graph_find_commit_color(graph, commit);
658 + graph->new_columns[i].is_merge_parent = 0;
659 }
660
661 if (graph->num_parents > 1 && idx > -1 && graph->merge_layout == -1) {
@@ -630,6 +694,12 @@ static void graph_insert_into_new_columns(struct git_graph *graph,
694 }
695
696 graph->mapping[mapping_idx] = i;
697 +
698 + /*
699 + * Mark non-first parents of a merge.
700 + */
701 + if (graph->num_parents > 1 && initial_merge_layout >= 0 && idx > -1)
702 + graph->new_columns[i].is_merge_parent = 1;
703 }
704
705 static void graph_update_columns(struct git_graph *graph)
@@ -721,10 +791,20 @@ static void graph_update_columns(struct git_graph *graph)
791 if (graph->num_parents == 0)
792 graph->width += 2;
793 } else {
794 + int j;
795 graph_insert_into_new_columns(graph, col_commit, -1);
796 + /*
797 + * This column is not the current commit, but we need to
798 + * propagate the flag until the commit is processed.
799 + */
800 + j = graph_find_new_column_by_commit(graph, col_commit);
801 + if (j >= 0 && graph->columns[i].is_merge_parent)
802 + graph->new_columns[j].is_merge_parent = 1;
803 }
804 }
805
806 + graph->commit_in_columns = is_commit_in_columns;
807 +
808 /*
809 * If graph_max_lanes is set, cap the width
810 */
@@ -814,9 +894,113 @@ void graph_push_lookahead(struct git_graph *graph, struct commit *c)
894 graph->lookahead[graph->lookahead_nr++] = c;
895 }
896
897 +/*
898 + * A commit can be a visual root when:
899 + *
900 + * - It has no parents.
901 + *
902 + * - It has parents but they are all filtered out and
903 + * commit->parents arrives NULL.
904 + *
905 + * - Its parents are uninteresting.
906 + *
907 + * - It is not a boundary commit. Boundary commits also have no visible
908 + * parents, but they are not selected as visual roots because they cannot
909 + * cause the ambiguity of being vertically adjacent because:
910 + *
911 + * 1. A boundary only appears because an included commit is its child.
912 + * Children are always above, and the renderer draws an edge down to
913 + * the boundary from that child. Rather than starting a column like a
914 + * visual root would do, it inherits its child column.
915 + *
916 + * 2. Included commits cannot appear below a boundary. Boundaries are
917 + * ancestors of the exclusion point; if an included commit were an
918 + * ancestor of the boundary it would be excluded and not rendered.
919 + * Boundaries therefore always sink to the bottom.
920 + */
921 +static int graph_is_visual_root_candidate(struct commit *c, struct git_graph *graph)
922 +{
923 + struct commit_list *p;
924 +
925 + if (c->object.flags & BOUNDARY)
926 + return 0;
927 + for (p = c->parents; p; p = p->next)
928 + if (graph_is_interesting(graph, p->item))
929 + return 0;
930 + return 1;
931 +}
932 +
933 +static int graph_is_visual_root(struct git_graph *graph,
934 + struct graph_lookahead_flags *flags)
935 +{
936 + /*
937 + * This must be only called for the current commit as graph contains
938 + * the state for the current commit only.
939 + *
940 + * To check if a commit is a visual root, call graph_is_visual_root_candidate()
941 + * but we won't know if it is really a visual root until we get to the
942 + * next commit state.
943 + *
944 + * The current commit is an actual visual root if it is a candidate and
945 + * the commit is not a non-first parent of a merge.
946 + *
947 + * *
948 + * |\
949 + * | * <- it is a visual root candidate but it shouldn't be indented
950 + * * because it is already connected by an edge.
951 + * ^ if commit_in_columns && is_merge_parent means the commit
952 + * | was put by a merge and is connected.
953 + * |
954 + * `-------- if !is_next_visible means we're on the last commit, avoid
955 + * indentation unless the one before is a visual root, then
956 + * we need to differentiate from the one above.
957 + *
958 + * If next_has_columns means that the next commit has
959 + * already a column, so it will not be rendered below, the
960 + * current commit has to act as the last commit and omit
961 + * indentation.
962 + */
963 + return graph_is_visual_root_candidate(graph->commit, graph) &&
964 + !(graph->commit_in_columns &&
965 + graph->columns[graph->commit_index].is_merge_parent) &&
966 + flags->is_next_visible &&
967 + (!flags->next_has_column || graph->visual_root_depth > 0);
968 +}
969 +
970 +/*
971 + * Peeks the next commits via the lookahead buffer and sets the lookahead flags.
972 + */
973 +static void graph_peek_next_visible(struct git_graph *graph,
974 + struct graph_lookahead_flags *flags)
975 +{
976 + flags->is_next_visible = 0;
977 + flags->is_next_visual_root = 0;
978 + flags->next_has_column = 0;
979 +
980 + if (!graph->lookahead_nr)
981 + return;
982 +
983 + flags->is_next_visible = 1;
984 + flags->next_has_column =
985 + graph_find_new_column_by_commit(graph, graph->lookahead[0]) >= 0;
986 +
987 + if (!graph_is_visual_root_candidate(graph->lookahead[0], graph))
988 + return;
989 +
990 + if (graph->lookahead_nr >= 2)
991 + flags->is_next_visual_root = 1;
992 +}
993 +
994 +static int graph_needs_pre_root_line(struct git_graph *graph)
995 +{
996 + return graph->commit_in_columns && graph->is_visual_root &&
997 + graph->num_columns > 0 && !graph->visual_root_cascade;
998 +}
999 +
1000 void graph_update(struct git_graph *graph, struct commit *commit)
1001 {
1002 struct commit_list *parent;
1003 + struct graph_lookahead_flags flags;
1004
1005 /*
1006 * Set the new commit
@@ -847,6 +1031,23 @@ void graph_update(struct git_graph *graph, struct commit *commit)
1031 */
1032 graph_update_columns(graph);
1033
1034 + graph_peek_next_visible(graph, &flags);
1035 +
1036 + graph->is_visual_root = graph_is_visual_root(graph, &flags);
1037 +
1038 + if (graph->is_visual_root) {
1039 + /*
1040 + * If next is a visual root we can omit the indent for the first
1041 + * visual root and start cascading.
1042 + */
1043 + if (!graph->visual_root_depth && flags.is_next_visual_root)
1044 + graph->visual_root_cascade = 1;
1045 + graph->visual_root_depth++;
1046 + } else {
1047 + graph->visual_root_depth = 0;
1048 + graph->visual_root_cascade = 0;
1049 + }
1050 +
1051 graph->expansion_row = 0;
1052
1053 /*
@@ -864,11 +1065,16 @@ void graph_update(struct git_graph *graph, struct commit *commit)
1065 * room for it. We need to do this only if there is a branch row
1066 * (or more) to the right of this commit.
1067 *
1068 + * If it is a visual root, we need to print an extra row to
1069 + * connect the indentation.
1070 + *
1071 * If there are less than 3 parents, we can immediately print the
1072 * commit line.
1073 */
1074 if (graph->state != GRAPH_PADDING)
1075 graph->state = GRAPH_SKIP;
1076 + else if (graph_needs_pre_root_line(graph))
1077 + graph->state = GRAPH_PRE_ROOT;
1078 else if (graph_needs_pre_commit_line(graph))
1079 graph->state = GRAPH_PRE_COMMIT;
1080 else
@@ -1116,6 +1322,17 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line
1322
1323 if (col_commit == graph->commit) {
1324 seen_this = 1;
1325 + if (graph->is_visual_root) {
1326 + int depth = graph->visual_root_depth;
1327 + /*
1328 + * Each visual column is 2 characters wide.
1329 + * Omit the indentation for the first visual
1330 + * root in cascade mode.
1331 + */
1332 + int padding = (depth - graph->visual_root_cascade) * 2;
1333 + graph_line_addchars(line, ' ', padding);
1334 + graph->width += padding;
1335 + }
1336 graph_output_commit_char(graph, line);
1337
1338 if (graph_needs_truncation(graph, i)) {
@@ -1487,6 +1704,30 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
1704 graph_update_state(graph, GRAPH_PADDING);
1705 }
1706
1707 +static void graph_output_pre_root_line(struct git_graph *graph, struct graph_line *line)
1708 +{
1709 + /*
1710 + * This function adds a row before a visual root, to connect the
1711 + * branch to the indented commit. It must only be called on a
1712 + * visual root.
1713 + */
1714 + if (!graph->is_visual_root)
1715 + BUG("commit must be a visual root to call pre_root_line");
1716 +
1717 + for (int i = 0; i < graph->num_columns; i++) {
1718 + struct column *col = &graph->columns[i];
1719 + if (col->commit == graph->commit) {
1720 + graph_line_addch(line, ' ');
1721 + graph_line_write_column(line, col, '\\');
1722 + } else {
1723 + graph_line_write_column(line, col, '|');
1724 + }
1725 + graph_line_addch(line, ' ');
1726 + }
1727 +
1728 + graph_update_state(graph, GRAPH_COMMIT);
1729 +}
1730 +
1731 int graph_next_line(struct git_graph *graph, struct strbuf *sb)
1732 {
1733 int shown_commit_line = 0;
@@ -1512,6 +1753,9 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb)
1753 case GRAPH_PRE_COMMIT:
1754 graph_output_pre_commit_line(graph, &line);
1755 break;
1756 + case GRAPH_PRE_ROOT:
1757 + graph_output_pre_root_line(graph, &line);
1758 + break;
1759 case GRAPH_COMMIT:
1760 graph_output_commit_line(graph, &line);
1761 shown_commit_line = 1;
t/meson.build
+1
@@ -577,6 +577,7 @@ integration_tests = [
577 't4215-log-skewed-merges.sh',
578 't4216-log-bloom.sh',
579 't4217-log-limit.sh',
580 + 't4218-log-graph-indentation.sh',
581 't4219-log-follow-merge.sh',
582 't4252-am-options.sh',
583 't4253-am-keep-cr-dos.sh',
t/t4218-log-graph-indentation.sh new
+514
@@ -0,0 +1,514 @@
1 +#!/bin/sh
2 +
3 +test_description='git log --graph visual root indentations'
4 +
5 +. ./test-lib.sh
6 +. "$TEST_DIRECTORY"/lib-log-graph.sh
7 +
8 +check_graph_with_description () {
9 + cat >expect &&
10 + lib_test_cmp_graph --format="%s%ndescription%nsecond-line" "$@"
11 +}
12 +
13 +create_orphan () {
14 + git checkout --orphan "$1" &&
15 + test_might_fail git rm -rf .
16 +}
17 +
18 +# disable commit-graph topo order to have the graph to render in different
19 +# ways (used in --first-parent tests to have multiple visual roots while a
20 +# column is active at the same time).
21 +unset_commit_graph () {
22 + sane_unset GIT_TEST_COMMIT_GRAPH &&
23 + rm -f .git/objects/info/commit-graph &&
24 + rm -rf .git/objects/info/commit-graphs
25 +}
26 +
27 +test_expect_success 'single root commit is not indented' '
28 + create_orphan _1 && test_commit 1_A &&
29 + lib_test_check_graph _1 <<-\EOF
30 + * 1_A
31 + EOF
32 +'
33 +
34 +test_expect_success 'visual root indented before unrelated branch' '
35 + create_orphan _2 && test_commit 2_A && test_commit 2_B &&
36 + create_orphan _3 && test_commit 3_A &&
37 + lib_test_check_graph _2 _3 <<-\EOF
38 + * 3_A
39 + * 2_B
40 + * 2_A
41 + EOF
42 +'
43 +
44 +test_expect_success 'visual root indentation with --left-right' '
45 + lib_test_check_graph --left-right _2..._3 <<-\EOF
46 + > 3_A
47 + < 2_B
48 + < 2_A
49 + EOF
50 +'
51 +
52 +# A better case of why indentation is still needed with '--left-right' flag is
53 +# that unrelated branches can be on the same side, so it's needed to
54 +# differentiate visual roots on the same side.
55 +test_expect_success 'visual root indentation with --left-right having unrelated commits on the same side' '
56 + lib_test_check_graph --left-right _2..._3 _1 <<-\EOF
57 + > 3_A
58 + < 2_B
59 + \
60 + < 2_A
61 + > 1_A
62 + EOF
63 +'
64 +
65 +test_expect_success 'visual root indents the description also' '
66 + check_graph_with_description _2 _3 <<-\EOF
67 + * 3_A
68 + description
69 + second-line
70 + * 2_B
71 + | description
72 + | second-line
73 + * 2_A
74 + description
75 + second-line
76 + EOF
77 +'
78 +
79 +test_expect_success 'indented visual root parent gets connected to its child' '
80 + create_orphan _4 && test_commit 4_A && test_commit 4_B &&
81 + create_orphan _5 && test_commit 5_A && test_commit 5_B &&
82 + lib_test_check_graph _4 _5 <<-\EOF
83 + * 5_B
84 + \
85 + * 5_A
86 + * 4_B
87 + * 4_A
88 + EOF
89 +'
90 +
91 +test_expect_success 'indented visual root parent gets connected to its child with description' '
92 + check_graph_with_description _4 _5 <<-\EOF
93 + * 5_B
94 + | description
95 + | second-line
96 + \
97 + * 5_A
98 + description
99 + second-line
100 + * 4_B
101 + | description
102 + | second-line
103 + * 4_A
104 + description
105 + second-line
106 + EOF
107 +'
108 +
109 +test_expect_success 'visual roots cascade and last root does not' '
110 + create_orphan _7 && test_commit 7_A && test_commit 7_B &&
111 + create_orphan _8 && test_commit 8_A &&
112 + create_orphan _9 && test_commit 9_A &&
113 + create_orphan _10 && test_commit 10_A &&
114 + lib_test_check_graph _7 _8 _9 _10 <<-\EOF
115 + * 10_A
116 + * 9_A
117 + * 8_A
118 + * 7_B
119 + * 7_A
120 + EOF
121 +'
122 +
123 +test_expect_success 'last root does not cascade' '
124 + lib_test_check_graph _8 _9 _10 <<-\EOF
125 + * 10_A
126 + * 9_A
127 + * 8_A
128 + EOF
129 +'
130 +
131 +test_expect_success 'merge parents are roots between them but they do not indent' '
132 + create_orphan _11 && test_commit 11_A &&
133 + create_orphan _12 && test_commit 12_A &&
134 + create_orphan _13 && test_commit 13_A &&
135 + git checkout _11 &&
136 + TREE=$(git write-tree) &&
137 + MERGE=$(git commit-tree $TREE -p _11 -p _12 -p _13 -m 11_octopus) &&
138 + git reset --hard $MERGE &&
139 + lib_test_check_graph _11 <<-\EOF
140 + *-. 11_octopus
141 + |\ \
142 + | | * 13_A
143 + | * 12_A
144 + * 11_A
145 + EOF
146 +'
147 +
148 +# The last parent of a merge can be indented if nothing related to it needs to
149 +# be rendered after, if it's another visual root, merge parent must not get
150 +# indented but rather activate cascading.
151 +test_expect_success 'merge then unrelated visual root and unrelated branch' '
152 + create_orphan _16 && test_commit 16_A && test_commit 16_B &&
153 + create_orphan _17 && test_commit 17_A &&
154 + create_orphan _18 && test_commit 18_A &&
155 + create_orphan _19 && test_commit 19_A &&
156 + create_orphan _20 && test_commit 20_A &&
157 + git checkout _18 &&
158 + TREE=$(git write-tree) &&
159 + MERGE=$(git commit-tree $TREE -p _18 -p _19 -p _20 -m 18_octopus) &&
160 + git reset --hard $MERGE &&
161 + lib_test_check_graph _18 _17 _16 <<-\EOF
162 + *-. 18_octopus
163 + |\ \
164 + | | * 20_A
165 + | * 19_A
166 + * 18_A
167 + * 17_A
168 + * 16_B
169 + * 16_A
170 + EOF
171 +'
172 +
173 +# The last commit root does not get indented, if the next thing after the root
174 +# merge parent is the last commit, indent the merge parent.
175 +test_expect_success 'merge then unrelated root indents merge parent' '
176 + lib_test_check_graph _18 _17 <<-\EOF
177 + *-. 18_octopus
178 + |\ \
179 + | | * 20_A
180 + | * 19_A
181 + \
182 + * 18_A
183 + * 17_A
184 + EOF
185 +'
186 +
187 +test_expect_success 'merge then unrelated branch indents merge parent' '
188 + lib_test_check_graph _18 _16 <<-\EOF
189 + *-. 18_octopus
190 + |\ \
191 + | | * 20_A
192 + | * 19_A
193 + \
194 + * 18_A
195 + * 16_B
196 + * 16_A
197 + EOF
198 +'
199 +
200 +test_expect_success 'two-parent merge of orphans' '
201 + create_orphan _21 && test_commit 21_A &&
202 + create_orphan _22 && test_commit 22_A &&
203 + git checkout _21 &&
204 + TREE=$(git write-tree) &&
205 + MERGE=$(git commit-tree $TREE -p _21 -p _22 -m 21_merge) &&
206 + git reset --hard $MERGE &&
207 + lib_test_check_graph _21 <<-\EOF
208 + * 21_merge
209 + |\
210 + | * 22_A
211 + * 21_A
212 + EOF
213 +'
214 +
215 +test_expect_success 'commit with filtered parent becomes a visual root' '
216 + create_orphan _23 &&
217 + echo test >other.txt &&
218 + git add other.txt &&
219 + git commit -m "23_A" &&
220 + echo test >foo.txt &&
221 + git add foo.txt &&
222 + git commit -m "23_B" &&
223 + create_orphan _24 &&
224 + echo test >foo.txt &&
225 + git add foo.txt &&
226 + git commit -m "24_A" &&
227 + lib_test_check_graph _23 _24 -- foo.txt <<-\EOF
228 + * 23_B
229 + * 24_A
230 + EOF
231 +'
232 +
233 +test_expect_success 'filtered parent cascading edge case' '
234 + create_orphan _27 &&
235 + echo test >foo.txt &&
236 + git add foo.txt &&
237 + test_tick &&
238 + git commit -m "D (last)" &&
239 +
240 + create_orphan _25 &&
241 + echo test >other.txt &&
242 + git add other.txt &&
243 + test_tick &&
244 + git commit -m "C-filtered" &&
245 +
246 + echo test >foo.txt &&
247 + git add foo.txt &&
248 + test_tick &&
249 + git commit -m "B (child of filtered)" &&
250 +
251 + create_orphan _26 &&
252 + echo test >foo.txt &&
253 + git add foo.txt &&
254 + test_tick &&
255 + git commit -m "A (visual root)" &&
256 +
257 + lib_test_check_graph _25 _26 _27 -- foo.txt <<-\EOF
258 + * A (visual root)
259 + * B (child of filtered)
260 + * D (last)
261 + EOF
262 +'
263 +
264 +test_expect_success 'multiple filtered parents in sequence' '
265 + create_orphan _44 &&
266 + echo a >other.txt && git add other.txt && git commit -m "44_F" &&
267 + echo b >foo.txt && git add foo.txt && git commit -m "44_C" &&
268 +
269 + create_orphan _45 &&
270 + echo c >other.txt && git add other.txt && git commit -m "45_F" &&
271 + echo d >foo.txt && git add foo.txt && git commit -m "45_C" &&
272 +
273 + create_orphan _46 &&
274 + echo e >foo.txt && git add foo.txt && git commit -m "46_A" &&
275 +
276 + lib_test_check_graph _44 _45 _46 -- foo.txt <<-\EOF
277 + * 44_C
278 + * 45_C
279 + * 46_A
280 + EOF
281 +'
282 +
283 +# These tests prove why there is no need to have indentation for boundary
284 +# commits.
285 +#
286 +# Boundary commits rather than starting a column they 'inherit' the one of
287 +# its child so there will always be an edge that connects it removing the
288 +# ambiguity.
289 +test_expect_success 'unrelated boundaries are not ambiguous' '
290 + create_orphan _28 && test_commit 28_A && test_commit 28_B &&
291 + test_commit 28_C &&
292 + create_orphan _29 && test_commit 29_A && test_commit 29_B &&
293 + lib_test_check_graph --boundary 28_A.._28 29_A.._29 <<-\EOF
294 + * 29_B
295 + | * 28_C
296 + | * 28_B
297 + | o 28_A
298 + o 29_A
299 + EOF
300 +'
301 +
302 +# Same structure as t6016
303 +test_expect_success 'boundary commits big test' '
304 + # 3 commits on branch _30
305 + create_orphan _30 &&
306 + test_commit 30_A &&
307 + test_commit 30_B &&
308 + test_commit 30_C &&
309 +
310 + # 2 commits on branch _31, started from 30_A
311 + git checkout -b _31 30_A &&
312 + test_commit 31_A &&
313 + test_commit 31_B &&
314 +
315 + # 2 commits on branch _32, started from 30_B
316 + git checkout -b _32 30_B &&
317 + test_commit 32_A &&
318 + test_commit 32_B &&
319 +
320 + # Octopus merge _31 and _32 into -30
321 + git checkout _30 &&
322 + git merge _31 _32 -m 30_D &&
323 + git tag 30_D &&
324 + test_commit 30_E &&
325 +
326 + # More commits on _32, then merge _32 into _30
327 + git checkout _32 &&
328 + test_commit 32_C &&
329 + test_commit 32_D &&
330 + git checkout _30 &&
331 + git merge -s ours _32 -m 30_F &&
332 + git tag 30_F &&
333 + test_commit 30_G &&
334 + lib_test_check_graph --boundary _30 _31 _32 ^32_C <<-\EOF
335 + * 30_G
336 + * 30_F
337 + |\
338 + | * 32_D
339 + * | 30_E
340 + | |
341 + | \
342 + *-. \ 30_D
343 + |\ \ \
344 + | * | | 31_B
345 + | * | | 31_A
346 + * | | | 30_C
347 + o | | | 30_B
348 + |/ / /
349 + o / / 30_A
350 + / /
351 + | o 32_C
352 + |/
353 + o 32_B
354 + EOF
355 +'
356 +
357 +# Filter by --first-parent and then forcing the filtered parents to be shown.
358 +test_expect_success '--first-parent flag with the filtered parents' '
359 + (
360 + unset_commit_graph &&
361 + create_orphan _35 && test_commit 35_A && test_commit 35_B &&
362 + create_orphan _36 && test_commit 36_A &&
363 + create_orphan _37 && test_commit 37_A &&
364 + git checkout _35 &&
365 + TREE=$(git write-tree) &&
366 + MERGE=$(git commit-tree $TREE -p _35 -p _36 -p _37 -m 35_octopus) &&
367 + git reset --hard $MERGE &&
368 + lib_test_check_graph --first-parent _35 _36 _37 <<-\EOF
369 + * 35_octopus
370 + | * 37_A
371 + | * 36_A
372 + * 35_B
373 + * 35_A
374 + EOF
375 + )
376 +'
377 +
378 +test_expect_success '--first-parent with filtered parents but one has a child' '
379 + (
380 + unset_commit_graph &&
381 + create_orphan _38 && test_commit 38_A && test_commit 38_B &&
382 + create_orphan _39 && test_commit 39_A &&
383 + create_orphan _40 && test_commit 40_A && test_commit 40_B &&
384 + git checkout _38 &&
385 + TREE=$(git write-tree) &&
386 + MERGE=$(git commit-tree $TREE -p _38 -p _39 -p _40 -m 38_octopus) &&
387 + git reset --hard $MERGE &&
388 + lib_test_check_graph --first-parent _38 _39 _40 <<-\EOF
389 + * 38_octopus
390 + | * 40_B
391 + | * 40_A
392 + | * 39_A
393 + * 38_B
394 + * 38_A
395 + EOF
396 + )
397 +'
398 +
399 +test_expect_success '--first-parent with filtered parents but both have children' '
400 + (
401 + unset_commit_graph &&
402 + create_orphan _41 && test_commit 41_A && test_commit 41_B &&
403 + create_orphan _42 && test_commit 42_A && test_commit 42_B &&
404 + create_orphan _43 && test_commit 43_A && test_commit 43_B &&
405 + git checkout _41 &&
406 + TREE=$(git write-tree) &&
407 + MERGE=$(git commit-tree $TREE -p _41 -p _42 -p _43 -m 41_octopus) &&
408 + git reset --hard $MERGE &&
409 + lib_test_check_graph --first-parent _41 _42 _43 <<-\EOF
410 + * 41_octopus
411 + | * 43_B
412 + | \
413 + | * 43_A
414 + | * 42_B
415 + | * 42_A
416 + * 41_B
417 + * 41_A
418 + EOF
419 + )
420 +'
421 +
422 +test_expect_success 'two unrelated merges' '
423 + create_orphan _50 && test_commit 50_A &&
424 + git checkout -b _51 &&
425 + test_commit 51_A && test_commit 51_B &&
426 + git checkout _50 &&
427 + git merge --no-ff _51 -m 50_B &&
428 +
429 + create_orphan _52 && test_commit 52_A &&
430 + git checkout -b _53 &&
431 + test_commit 53_A && test_commit 53_B &&
432 + git checkout _52 &&
433 + git merge --no-ff _53 -m 52_B &&
434 +
435 + lib_test_check_graph _52 _50 <<-\EOF
436 + * 52_B
437 + |\
438 + | * 53_B
439 + | * 53_A
440 + |/
441 + \
442 + * 52_A
443 + * 50_B
444 + |\
445 + | * 51_B
446 + | * 51_A
447 + |/
448 + * 50_A
449 + EOF
450 +'
451 +
452 +test_expect_success '--max-count treats the last visible commit as the last commit' '
453 + lib_test_check_graph --max-count=2 _8 _9 _10 <<-\EOF
454 + * 10_A
455 + * 9_A
456 + EOF
457 +'
458 +
459 +test_expect_success '--max-count=1 shows a single root without indentation' '
460 + lib_test_check_graph --max-count=1 _8 _9 _10 <<-\EOF
461 + * 10_A
462 + EOF
463 +'
464 +
465 +test_expect_success '--max-count-oldest indents visual roots' '
466 + lib_test_check_graph --max-count-oldest=3 _8 _9 _10 <<-\EOF
467 + * 10_A
468 + * 9_A
469 + * 8_A
470 + EOF
471 +'
472 +
473 +# when the graph commits are filtered with regex options like --author, the
474 +# commit parents do not come NULL so it is needed to check if the parents are
475 +# interesting.
476 +test_expect_success '--author skipped parent makes a visual root' '
477 + create_orphan _55 &&
478 + test_tick &&
479 + git commit --allow-empty -m 55_A &&
480 + create_orphan _54 &&
481 + test_tick &&
482 + git commit --allow-empty --author="Other <other@example.com>" -m 54_A &&
483 + test_tick &&
484 + git commit --allow-empty -m 54_B &&
485 + test_tick &&
486 + git commit --allow-empty -m 54_C &&
487 + lib_test_check_graph --author="A U Thor" _54 _55 <<-\EOF
488 + * 54_C
489 + \
490 + * 54_B
491 + * 55_A
492 + EOF
493 +'
494 +
495 +test_expect_success '--grep skipped parent makes a visual root' '
496 + create_orphan _57 &&
497 + test_tick &&
498 + git commit --allow-empty -m 57_keep_A &&
499 + create_orphan _56 &&
500 + test_tick &&
501 + git commit --allow-empty -m 56_skip &&
502 + test_tick &&
503 + git commit --allow-empty -m 56_keep_A &&
504 + test_tick &&
505 + git commit --allow-empty -m 56_keep_B &&
506 + lib_test_check_graph --grep=keep _56 _57 <<-\EOF
507 + * 56_keep_B
508 + \
509 + * 56_keep_A
510 + * 57_keep_A
511 + EOF
512 +'
513 +
514 +test_done