Skip to content

Commit df0bc6c

Browse files
pabloosabaterrgitster
authored andcommitted
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. The lookahead is not completely reliable, on graphs with filtered parents, the walker when processing the current commit it will simplify its parents by removing the ones that won't be shown, (They have the TREESAME flag when filtering by path for example), but it doesn't act for the grandparents or the next commit if it is unrelated until we move to the next. For example given A visual root B child C parent of B, visual root FILTERED D last commit We would expect A B D When processing A, for the walker and the information at the renderer, B is still a child of C, as B parent, hasn't been removed yet. This makes cascade to not trigger as the lookahead fails to detect if the next commit will be a visual root. Once at B, its parent has been removed and has become a visual root, and it just adds its indent to the one left by A. We end up with an extra indent: A B D The output isn't broken as unrelated commits are successfully separated by indentation, but an indent level should have been avoided. Create a new test file for graph indentations test called 't4218-log-graph-indentation.sh'. The filtered parents edge case is documented as a NEEDSWORK on the lookahead function and it has its own 'test_expect_failure' at 't4218'. [1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.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>
1 parent f325922 commit df0bc6c

3 files changed

Lines changed: 718 additions & 0 deletions

File tree

graph.c

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,23 @@ struct column {
6060
* index into column_colors.
6161
*/
6262
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 is_merge_parent:1;
6373
};
6474

6575
enum graph_state {
6676
GRAPH_PADDING,
6777
GRAPH_SKIP,
6878
GRAPH_PRE_COMMIT,
79+
GRAPH_PRE_ROOT,
6980
GRAPH_COMMIT,
7081
GRAPH_POST_MERGE,
7182
GRAPH_COLLAPSING
@@ -315,6 +326,48 @@ struct git_graph {
315326
* diff_output_prefix_callback().
316327
*/
317328
struct strbuf prefix_buf;
329+
330+
/*
331+
* If a commit is a visual root, we need to indent it to prevent
332+
* unrelated commits from being vertically adjacent to it.
333+
*/
334+
unsigned is_visual_root:1;
335+
336+
/*
337+
* Indentation increases for each visual root adjacent to another visual
338+
* root, making visual root commits indentation cascade.
339+
*/
340+
unsigned int visual_root_depth;
341+
342+
/*
343+
* When a visual root is adjacent to other visual roots, the first one
344+
* can avoid indentation and the rest cascades, increasing the indentation
345+
* for each one.
346+
*/
347+
unsigned visual_root_cascade:1;
348+
349+
/*
350+
* Set when the current commit was already present in graph->columns
351+
* before being processed.
352+
*/
353+
unsigned commit_in_columns:1;
354+
};
355+
356+
struct graph_lookahead_flags {
357+
/*
358+
* Set when there will be a commit after the current one that will be
359+
* rendered.
360+
*/
361+
unsigned int is_next_visible:1;
362+
/*
363+
* Set when the next visible commit is candidate to be a visual root.
364+
*/
365+
unsigned int is_next_visual_root:1;
366+
/*
367+
* Set when the next visible commit will be rendered under the current
368+
* commit.
369+
*/
370+
unsigned int next_has_column:1;
318371
};
319372

320373
static inline int graph_needs_truncation(struct git_graph *graph, int lane)
@@ -388,6 +441,8 @@ struct git_graph *graph_init(struct rev_info *opt)
388441
graph->num_columns = 0;
389442
graph->num_new_columns = 0;
390443
graph->mapping_size = 0;
444+
graph->visual_root_depth = 0;
445+
graph->visual_root_cascade = 0;
391446
/*
392447
* Start the column color at the maximum value, since we'll
393448
* always increment it for the first commit we output.
@@ -561,6 +616,11 @@ static void graph_insert_into_new_columns(struct git_graph *graph,
561616
struct commit *commit,
562617
int idx)
563618
{
619+
/*
620+
* Get the initial merge_layout before it's modified to know if this
621+
* is a merge.
622+
*/
623+
int initial_merge_layout = graph->merge_layout;
564624
int i = graph_find_new_column_by_commit(graph, commit);
565625
int mapping_idx;
566626

@@ -572,6 +632,7 @@ static void graph_insert_into_new_columns(struct git_graph *graph,
572632
i = graph->num_new_columns++;
573633
graph->new_columns[i].commit = commit;
574634
graph->new_columns[i].color = graph_find_commit_color(graph, commit);
635+
graph->new_columns[i].is_merge_parent = 0;
575636
}
576637

577638
if (graph->num_parents > 1 && idx > -1 && graph->merge_layout == -1) {
@@ -610,6 +671,12 @@ static void graph_insert_into_new_columns(struct git_graph *graph,
610671
}
611672

612673
graph->mapping[mapping_idx] = i;
674+
675+
/*
676+
* Mark non-first parents of a merge.
677+
*/
678+
if (graph->num_parents > 1 && initial_merge_layout >= 0 && idx > -1)
679+
graph->new_columns[i].is_merge_parent = 1;
613680
}
614681

615682
static void graph_update_columns(struct git_graph *graph)
@@ -701,10 +768,20 @@ static void graph_update_columns(struct git_graph *graph)
701768
if (graph->num_parents == 0)
702769
graph->width += 2;
703770
} else {
771+
int j;
704772
graph_insert_into_new_columns(graph, col_commit, -1);
773+
/*
774+
* This column is not the current commit, but we need to
775+
* propagate the flag until the commit is processed.
776+
*/
777+
j = graph_find_new_column_by_commit(graph, col_commit);
778+
if (j >= 0 && graph->columns[i].is_merge_parent)
779+
graph->new_columns[j].is_merge_parent = 1;
705780
}
706781
}
707782

783+
graph->commit_in_columns = is_commit_in_columns;
784+
708785
/*
709786
* If graph_max_lanes is set, cap the width
710787
*/
@@ -763,9 +840,135 @@ static int graph_needs_pre_commit_line(struct git_graph *graph)
763840
graph->expansion_row < graph_num_expansion_rows(graph);
764841
}
765842

843+
/*
844+
* A commit can be a visual root when:
845+
* - It has no parents.
846+
*
847+
* - It has parents but they are all filtered out and
848+
* commit->parents arrives NULL.
849+
*
850+
* - It is not a boundary commit. Boundary commits also have no visible
851+
* parents, but they are not selected as visual roots because they cannot
852+
*. cause the ambiguity of being vertically adjacent because:
853+
*
854+
* 1. A boundary only appears because an included commit is its child.
855+
* Children are always above, and the renderer draws an edge down to
856+
* the boundary from that child. Rather than starting a column like a
857+
* visual root would do, it inherits its child column.
858+
*
859+
* 2. Included commits cannot appear below a boundary. Boundaries are
860+
* ancestors of the exclusion point; if an included commit were an
861+
* ancestor of the boundary it would be excluded and not rendered.
862+
* Boundaries therefore always sink to the bottom.
863+
*/
864+
static int graph_is_visual_root_candidate(struct commit *c)
865+
{
866+
return c->parents == NULL && !(c->object.flags & BOUNDARY);
867+
}
868+
869+
static int graph_is_visual_root(struct git_graph *graph,
870+
struct graph_lookahead_flags *flags)
871+
{
872+
/*
873+
* This must be only called for the current commit as graph contains
874+
* the state for the current commit only.
875+
*
876+
* To check if a commit is a visual root, call graph_is_visual_root_candidate()
877+
* but we won't know if it is really a visual root until we get to the
878+
* next commit state.
879+
*
880+
* The current commit is an actual visual root if it is a candidate and
881+
* the commit is not a non-first parent of a merge.
882+
*
883+
* *
884+
* |\
885+
* | * <- it is a visual root candidate but it shouldn't be indented
886+
* * because it is already connected by an edge.
887+
* ^ if commit_in_columns && is_merge_parent means the commit
888+
* | was put by a merge and is connected.
889+
* |
890+
* `-------- if !is_next_visible means we're on the last commit, avoid
891+
* indentation unless the one before is a visual root, then
892+
* we need to differentiate from the one above.
893+
*
894+
* If next_has_columns means that the next commit has
895+
* already a column, so it will not be rendered below, the
896+
* current commit has to act as the last commit and omit
897+
* indentation.
898+
*/
899+
return graph_is_visual_root_candidate(graph->commit) &&
900+
!(graph->commit_in_columns &&
901+
graph->columns[graph->commit_index].is_merge_parent) &&
902+
flags->is_next_visible &&
903+
(!flags->next_has_column || graph->visual_root_depth > 0);
904+
}
905+
906+
/*
907+
* Iterates the commits queue searching for the next visible commit, once found
908+
* sets visibleness and visual-root flags.
909+
* Knowing if the next commit is also a visual root avoids redundant indentations
910+
*
911+
* NEEDSWORK: The queue is actively being modified by the walker, for each commit
912+
* its parents and itself get simplified and their flags set, but for the next
913+
* unrelated commit or the grandparents they are not simplified yet, which means
914+
* that a commit whose parents are all filtered will not be marked as a visual
915+
* root candidate at the lookahead.
916+
* This causes the lookahead to fail, failing to set the cascade flag to avoid
917+
* redundant indentations.
918+
* See 'test_expect_failure' at t4218-log-graph-indentation.sh.
919+
*/
920+
static void graph_peek_next_visible(struct git_graph *graph,
921+
struct graph_lookahead_flags *flags)
922+
{
923+
struct commit_list *cl;
924+
925+
flags->is_next_visible = 0;
926+
flags->is_next_visual_root = 0;
927+
flags->next_has_column = 0;
928+
929+
for (cl = graph->revs->commits; cl; cl = cl->next) {
930+
if (get_commit_action(graph->revs, cl->item) != commit_show)
931+
continue;
932+
flags->is_next_visible = 1;
933+
flags->next_has_column = graph_find_new_column_by_commit(graph, cl->item) >= 0;
934+
/*
935+
* We do not need graph->commit_in_columns or is_merge_parent,
936+
* because we only need to know whether the next one might be a
937+
* visual root, affecting the current commit where the cascade
938+
* would have to be set and the first visual root not indented.
939+
*
940+
* It will set next_is_visual_root to true for merge parents that
941+
* graph_is_visual_root() would return false, but if the next is
942+
* a merge parent, the current commit is the child and cannot
943+
* be a visual root and therefore having no effect.
944+
*/
945+
if (!graph_is_visual_root_candidate(cl->item))
946+
return;
947+
948+
/*
949+
* The next visible commit is a visual root candidate, but
950+
* only set cascade if it's not the last commit to be rendered.
951+
*/
952+
for (cl = cl->next; cl; cl = cl->next) {
953+
if (get_commit_action(graph->revs, cl->item) != commit_show)
954+
continue;
955+
flags->is_next_visual_root = 1;
956+
return;
957+
}
958+
return;
959+
}
960+
}
961+
962+
static int graph_needs_pre_root_line(struct git_graph *graph)
963+
{
964+
return graph->commit_in_columns && graph->is_visual_root &&
965+
graph->num_columns > 0 && !graph->visual_root_cascade;
966+
}
967+
766968
void graph_update(struct git_graph *graph, struct commit *commit)
767969
{
768970
struct commit_list *parent;
971+
struct graph_lookahead_flags flags;
769972

770973
/*
771974
* Set the new commit
@@ -796,6 +999,23 @@ void graph_update(struct git_graph *graph, struct commit *commit)
796999
*/
7971000
graph_update_columns(graph);
7981001

1002+
graph_peek_next_visible(graph, &flags);
1003+
1004+
graph->is_visual_root = graph_is_visual_root(graph, &flags);
1005+
1006+
if (graph->is_visual_root) {
1007+
/*
1008+
* If next is a visual root we can omit the indent for the first
1009+
* visual root and start cascading.
1010+
*/
1011+
if (!graph->visual_root_depth && flags.is_next_visual_root)
1012+
graph->visual_root_cascade = 1;
1013+
graph->visual_root_depth++;
1014+
} else {
1015+
graph->visual_root_depth = 0;
1016+
graph->visual_root_cascade = 0;
1017+
}
1018+
7991019
graph->expansion_row = 0;
8001020

8011021
/*
@@ -813,11 +1033,16 @@ void graph_update(struct git_graph *graph, struct commit *commit)
8131033
* room for it. We need to do this only if there is a branch row
8141034
* (or more) to the right of this commit.
8151035
*
1036+
* If it is a visual root, we need to print an extra row to
1037+
* connect the indentation.
1038+
*
8161039
* If there are less than 3 parents, we can immediately print the
8171040
* commit line.
8181041
*/
8191042
if (graph->state != GRAPH_PADDING)
8201043
graph->state = GRAPH_SKIP;
1044+
else if (graph_needs_pre_root_line(graph))
1045+
graph->state = GRAPH_PRE_ROOT;
8211046
else if (graph_needs_pre_commit_line(graph))
8221047
graph->state = GRAPH_PRE_COMMIT;
8231048
else
@@ -1065,6 +1290,17 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line
10651290

10661291
if (col_commit == graph->commit) {
10671292
seen_this = 1;
1293+
if (graph->is_visual_root) {
1294+
int depth = graph->visual_root_depth;
1295+
/*
1296+
* Each visual column is 2 characters wide.
1297+
* Omit the indentation for the first visual
1298+
* root in cascade mode.
1299+
*/
1300+
int padding = (depth - graph->visual_root_cascade) * 2;
1301+
graph_line_addchars(line, ' ', padding);
1302+
graph->width += padding;
1303+
}
10681304
graph_output_commit_char(graph, line);
10691305

10701306
if (graph_needs_truncation(graph, i)) {
@@ -1436,6 +1672,29 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
14361672
graph_update_state(graph, GRAPH_PADDING);
14371673
}
14381674

1675+
static void graph_output_pre_root_line(struct git_graph *graph, struct graph_line *line)
1676+
{
1677+
/*
1678+
* This function adds a row before a visual root, to connect the
1679+
* branch to the indented commit. It should only be called on a
1680+
* visual root.
1681+
*/
1682+
assert(graph->is_visual_root);
1683+
1684+
for (size_t i = 0; i < graph->num_columns; i++) {
1685+
struct column *col = &graph->columns[i];
1686+
if (col->commit == graph->commit) {
1687+
graph_line_addch(line, ' ');
1688+
graph_line_write_column(line, col, '\\');
1689+
} else {
1690+
graph_line_write_column(line, col, '|');
1691+
}
1692+
graph_line_addch(line, ' ');
1693+
}
1694+
1695+
graph_update_state(graph, GRAPH_COMMIT);
1696+
}
1697+
14391698
int graph_next_line(struct git_graph *graph, struct strbuf *sb)
14401699
{
14411700
int shown_commit_line = 0;
@@ -1461,6 +1720,9 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb)
14611720
case GRAPH_PRE_COMMIT:
14621721
graph_output_pre_commit_line(graph, &line);
14631722
break;
1723+
case GRAPH_PRE_ROOT:
1724+
graph_output_pre_root_line(graph, &line);
1725+
break;
14641726
case GRAPH_COMMIT:
14651727
graph_output_commit_line(graph, &line);
14661728
shown_commit_line = 1;

t/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,7 @@ integration_tests = [
576576
't4215-log-skewed-merges.sh',
577577
't4216-log-bloom.sh',
578578
't4217-log-limit.sh',
579+
't4218-log-graph-indentation.sh',
579580
't4252-am-options.sh',
580581
't4253-am-keep-cr-dos.sh',
581582
't4254-am-corrupt.sh',

0 commit comments

Comments
 (0)