{"thread":{"id":"65419","subject":"[GSoC RFC PATCH 1/1] graph: add indentation for commits preceded by a root","startedAt":"2026-04-02T21:17:25Z","lastAt":"2026-07-15T08:59:38Z","messageCount":119,"participants":["Pablo Sabater","Junio C Hamano","Pablo","Jeff King","Phillip Wood","Chandra Pratap","Kristofer Karlsson","Mirko Faina"],"isPatch":true,"patchVersion":1,"patchTotal":1},"messages":[{"id":"540796","messageId":"20260402211717.3604688-1-pabloosabaterr@gmail.com","threadId":"65419","inReplyTo":null,"subject":"[GSoC RFC PATCH 0/1] graph: add indentation for commits preceded by a root","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-04-02T21:17:16Z","receivedAt":"2026-04-02T21:17:23Z","isPatch":true,"body":"When having a history with multiple root commits and drawing the history\nnear the roots, the graphing engine renders the commit one below the other,\nseeming that they are related, which makes the graph confusing.\n\nThis issue was reported by Junio at:\n  https://lore.kernel.org/git/xmqqikaawrpx.fsf@gitster.g/\n\ne.g.:\n\n  * root-B\n  * child-A2\n  * child-A1\n  * root-A\n\nThis happens because the engine prints left to right from the first free\ncolumn and the root has no parents thus for the next row, its column\nbecomes free and the engine fills that gap with the next commit (child-A2)\nseeming that root-B and child-A2 are related when they are not.\n\nThe actual implementation is very minimal.\nThis patch makes the roots to be kept alive at least one more row to avoid\nthat and indent the next commit to the next column and then clean the mapping\nletting the indented commit to naturally collapse to the column where the\nroot was.\n\ne.g.:\n\n  * root-B\n    * child-A2\n   /\n  * child-A1\n  * root-A\n\nThis is done by adding a is_placeholder flag to the columns, the root commit\nis actually there but marked as a placeholder\n\ne.g.:\n\n   * root-B\n  (B) * child-A2\n    /\n   * child-A1\n   * root-A\n\n(B) would be root-B column with the placeholder flag active.\n\nThen teaching the rendering function to print a padding ' ' when meeting a \nplaceholder column outputs the second example.\n\nThere could also be the case where there are multiple roots\n\nwithout the patch:\n\n  * A root\n  * B root\n  * C root\n  * D1 child\n  * D root\n\nwith the patch, the indentation cascades:\n\n  * A root\n    * B root\n      * C root\n        * D1 child\n     _ /\n    /\n   /\n  * D root\n\nthe _ / might look weird but that's how the collapsing rendering does it\nfor big gaps, this case being from the 4th column to the 0th column.\nAnother patch could change the collapsing rendering for placeholders ?\nI haven't done it to keep it minimal, but a follow up could make it \nto be straight '/'. This would make it bigger but easier for the eye to follow.\nIMO is not worth it, but opinions are welcome.\n\nThe patch also adds tests for different cases like a root preceding multiple\nparents merges and the examples above.\n\nThere could be some edge cases still so any testing is very welcome.\n\nPablo Sabater (1):\n  graph: add indentation for commits preceded by a root\n\n graph.c                      |  68 ++++++++++++++++--\n t/t4215-log-skewed-merges.sh | 136 +++++++++++++++++++++++++++++++++++\n 2 files changed, 198 insertions(+), 6 deletions(-)\n\n\nbase-commit: 256554692df0685b45e60778b08802b720880c50\n-- \n2.43.0\n\n"},{"id":"540795","messageId":"20260402211717.3604688-2-pabloosabaterr@gmail.com","threadId":"65419","inReplyTo":"20260402211717.3604688-1-pabloosabaterr@gmail.com","subject":"[GSoC RFC PATCH 1/1] graph: add indentation for commits preceded by a root","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-04-02T21:17:17Z","receivedAt":"2026-04-02T21:17:25Z","isPatch":true,"body":"When having a history with multiple root commits and\ndrawing the history near the roots the graphing engine\nrenders the commit one below the other, seeming that\nthey are related.\n\nThis issue was reported by Junio at:\n  https://lore.kernel.org/git/xmqqikaawrpx.fsf@gitster.g/\n\nThis happens because the root has no parents thus\nfor the next row printing, the column becomes free\nand the engine prints from the first free columns\nleft to right.\n\nKeep the root commit at least one row more to avoid\nhaving the column empty but hide it, therefore\nmaking the next unrelated commit to live in the next\ncolumn (column means even positions where edges live:\n0, 2, 4), then clean that \"placeholder\" column and let\nthe unrelated commit to naturally collapse to the column\nwhere the root commit was.\n\nAdd is_placeholder to the struct column to mark if a column\nis acting as a placeholder for the padding.\n\nWhen the column is a root, add a column with the root\ncommit data to prevent segfaults when 'column->commit' and\nmark it as a placeholder.\n\nAfter, unless the next commit is also a root (then we\nneed to keep cascading the indentation) clean the mapping\nand the columns from the placeholder to allow it to\ncollapse naturally.\n\nTeach rendering functions to print a padding\n' ' when a placeholder column is met.\n\nAdd tests for different cases.\n\nbefore this patch:\n\n* root-B\n* child-A2\n* child-A1\n* root-A\n\nafter this patch:\n\n* root-B\n  * child-A2\n /\n* child-A1\n* root-A\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c                      |  68 ++++++++++++++++--\n t/t4215-log-skewed-merges.sh | 136 +++++++++++++++++++++++++++++++++++\n 2 files changed, 198 insertions(+), 6 deletions(-)\n\ndiff --git a/graph.c b/graph.c\nindex 26f6fbf000..736c4a0a0c 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -60,6 +60,13 @@ struct column {\n \t * index into column_colors.\n \t */\n \tunsigned short color;\n+\t/*\n+\t * A placeholder column keeps the column of the root filled for one\n+\t * extra row, avoiding a next unrelated commit to be printed in the\n+\t * same column. Placeholder columns don't propagate to the following\n+\t * commit.\n+\t */\n+\tunsigned is_placeholder:1;\n };\n \n enum graph_state {\n@@ -563,6 +570,7 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t\ti = graph->num_new_columns++;\n \t\tgraph->new_columns[i].commit = commit;\n \t\tgraph->new_columns[i].color = graph_find_commit_color(graph, commit);\n+\t\tgraph->new_columns[i].is_placeholder = 0;\n \t}\n \n \tif (graph->num_parents > 1 && idx > -1 && graph->merge_layout == -1) {\n@@ -607,7 +615,7 @@ static void graph_update_columns(struct git_graph *graph)\n {\n \tstruct commit_list *parent;\n \tint max_new_columns;\n-\tint i, seen_this, is_commit_in_columns;\n+\tint i, seen_this, is_commit_in_columns, is_root;\n \n \t/*\n \t * Swap graph->columns with graph->new_columns\n@@ -654,6 +662,9 @@ static void graph_update_columns(struct git_graph *graph)\n \t */\n \tseen_this = 0;\n \tis_commit_in_columns = 1;\n+\tis_root = graph->num_parents == 0 &&\n+\t\t  !graph->commit->parents &&\n+\t\t  !(graph->commit->object.flags & BOUNDARY);\n \tfor (i = 0; i <= graph->num_columns; i++) {\n \t\tstruct commit *col_commit;\n \t\tif (i == graph->num_columns) {\n@@ -688,11 +699,40 @@ static void graph_update_columns(struct git_graph *graph)\n \t\t\t * least 2, even if it has no interesting parents.\n \t\t\t * The current commit always takes up at least 2\n \t\t\t * spaces.\n+\t\t\t *\n+\t\t\t * Check for the commit to be a root, no parents\n+\t\t\t * and that it is not a boundary commit. If so, add a\n+\t\t\t * placeholder to keep that column filled for\n+\t\t\t * at least one row.\n+\t\t\t *\n+\t\t\t * Prevents the next commit from being inserted\n+\t\t\t * just below and making the graph confusing.\n \t\t\t */\n-\t\t\tif (graph->num_parents == 0)\n+\t\t\tif (is_root) {\n+\t\t\t\tgraph_insert_into_new_columns(graph, graph->commit, i);\n+\t\t\t\tgraph->new_columns[graph->num_new_columns - 1]\n+\t\t\t\t\t\t\t    .is_placeholder = 1;\n+\t\t\t} else if (graph->num_parents == 0) {\n \t\t\t\tgraph->width += 2;\n+\t\t\t}\n \t\t} else {\n-\t\t\tgraph_insert_into_new_columns(graph, col_commit, -1);\n+\t\t\tif (graph->columns[i].is_placeholder) {\n+\t\t\t\t/*\n+\t\t\t\t * Keep the placeholders if the next commit is\n+\t\t\t\t * a root also, making the indentation cascade.\n+\t\t\t\t */\n+\t\t\t\tif (!seen_this && is_root) {\n+\t\t\t\t\tgraph_insert_into_new_columns(graph,\n+\t\t\t\t\t\t\tgraph->columns[i].commit, i);\n+\t\t\t\t\tgraph->new_columns[graph->num_new_columns - 1]\n+\t\t\t\t\t\t\t.is_placeholder = 1;\n+\t\t\t\t} else if (!seen_this) {\n+\t\t\t\t\tgraph->mapping[graph->width] = -1;\n+\t\t\t\t\tgraph->width += 2;\n+\t\t\t\t}\n+\t\t\t} else {\n+\t\t\t\tgraph_insert_into_new_columns(graph, col_commit, -1);\n+\t\t\t}\n \t\t}\n \t}\n \n@@ -846,7 +886,10 @@ static void graph_output_padding_line(struct git_graph *graph,\n \t * Output a padding row, that leaves all branch lines unchanged\n \t */\n \tfor (i = 0; i < graph->num_new_columns; i++) {\n-\t\tgraph_line_write_column(line, &graph->new_columns[i], '|');\n+\t\tif (graph->new_columns[i].is_placeholder)\n+\t\t\tgraph_line_write_column(line, &graph->new_columns[i], ' ');\n+\t\telse\n+\t\t\tgraph_line_write_column(line, &graph->new_columns[i], '|');\n \t\tgraph_line_addch(line, ' ');\n \t}\n }\n@@ -1058,7 +1101,13 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \t\t\t   graph->mapping[2 * i] < i) {\n \t\t\tgraph_line_write_column(line, col, '/');\n \t\t} else {\n-\t\t\tgraph_line_write_column(line, col, '|');\n+\t\t\tif (col->is_placeholder) {\n+\t\t\t\tif (seen_this)\n+\t\t\t\t\tcontinue;\n+\t\t\t\tgraph_line_write_column(line, col, ' ');\n+\t\t\t} else {\n+\t\t\t\tgraph_line_write_column(line, col, '|');\n+\t\t\t}\n \t\t}\n \t\tgraph_line_addch(line, ' ');\n \t}\n@@ -1135,7 +1184,14 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n \t\t\t\tgraph_line_write_column(line, col, '|');\n \t\t\tgraph_line_addch(line, ' ');\n \t\t} else {\n-\t\t\tgraph_line_write_column(line, col, '|');\n+\t\t\tif (col->is_placeholder) {\n+\t\t\t\tif (seen_this)\n+\t\t\t\t\tcontinue;\n+\t\t\t\tgraph_line_write_column(line, col, ' ');\n+\t\t\t} else {\n+\t\t\t\tgraph_line_write_column(line, col, '|');\n+\t\t\t}\n+\n \t\t\tif (graph->merge_layout != 0 || i != graph->commit_index - 1) {\n \t\t\t\tif (parent_col)\n \t\t\t\t\tgraph_line_write_column(\ndiff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh\nindex 28d0779a8c..0333fea95a 100755\n--- a/t/t4215-log-skewed-merges.sh\n+++ b/t/t4215-log-skewed-merges.sh\n@@ -370,4 +370,140 @@ test_expect_success 'log --graph with multiple tips' '\n \tEOF\n '\n \n+test_expect_success 'log --graph with root commit' '\n+\tgit checkout --orphan 8_a &&\n+\ttest_commit 8_A &&\n+\ttest_commit 8_A1 &&\n+\tgit checkout --orphan 8_b &&\n+\ttest_commit 8_B &&\n+\n+\tcheck_graph 8_b 8_a <<-\\EOF\n+\t* 8_B\n+\t  * 8_A1\n+\t /\n+\t* 8_A\n+\tEOF\n+'\n+\n+test_expect_success 'log --graph with multiple root commits' '\n+\ttest_commit 8_B1 &&\n+\tgit checkout --orphan 8_c &&\n+\ttest_commit 8_C &&\n+\n+\tcheck_graph 8_c 8_b 8_a <<-\\EOF\n+\t* 8_C\n+\t  * 8_B1\n+\t /\n+\t* 8_B\n+\t  * 8_A1\n+\t /\n+\t* 8_A\n+\tEOF\n+'\n+\n+test_expect_success 'log --graph commit from a two parent merge shifted' '\n+\tgit checkout --orphan 9_b &&\n+\ttest_commit 9_B &&\n+\tgit checkout --orphan 9_c &&\n+\ttest_commit 9_C &&\n+\tgit checkout 9_b &&\n+\tgit merge 9_c --allow-unrelated-histories -m 9_M &&\n+\tgit checkout --orphan 9_a &&\n+\ttest_commit 9_A &&\n+\ttest_commit 9_A1 &&\n+\ttest_commit 9_A2 &&\n+\n+\tcheck_graph 9_a 9_b <<-\\EOF\n+\t* 9_A2\n+\t* 9_A1\n+\t* 9_A\n+\t  * 9_M\n+\t /|\n+\t| * 9_C\n+\t* 9_B\n+\tEOF\n+'\n+\n+test_expect_success 'log --graph commit from a three parent merge shifted' '\n+\tgit checkout --orphan 10_b &&\n+\ttest_commit 10_B &&\n+\tgit checkout --orphan 10_c &&\n+\ttest_commit 10_C &&\n+\tgit checkout --orphan 10_d &&\n+\ttest_commit 10_D &&\n+\tgit checkout 10_b &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p 10_b -p 10_c -p 10_d -m 10_M) &&\n+\tgit reset --hard $MERGE &&\n+\tgit checkout --orphan 10_a &&\n+\ttest_commit 10_A &&\n+\ttest_commit 10_A1 &&\n+\ttest_commit 10_A2 &&\n+\n+\tcheck_graph 10_a 10_b <<-\\EOF\n+\t* 10_A2\n+\t* 10_A1\n+\t* 10_A\n+\t  *   10_M\n+\t /|\\\n+\t| | * 10_D\n+\t| * 10_C\n+\t* 10_B\n+\tEOF\n+'\n+\n+test_expect_success 'log --graph commit from a four parent merge shifted' '\n+\tgit checkout --orphan 11_b &&\n+\ttest_commit 11_B &&\n+\tgit checkout --orphan 11_c &&\n+\ttest_commit 11_C &&\n+\tgit checkout --orphan 11_d &&\n+\ttest_commit 11_D &&\n+\tgit checkout --orphan 11_e &&\n+\ttest_commit 11_E &&\n+\tgit checkout 11_b &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p 11_b -p 11_c -p 11_d -p 11_e -m 11_M) &&\n+\tgit reset --hard $MERGE &&\n+\tgit checkout --orphan 11_a &&\n+\ttest_commit 11_A &&\n+\ttest_commit 11_A1 &&\n+\ttest_commit 11_A2 &&\n+\n+\tcheck_graph 11_a 11_b <<-\\EOF\n+\t* 11_A2\n+\t* 11_A1\n+\t* 11_A\n+\t  *-.   11_M\n+\t /|\\ \\\n+\t| | | * 11_E\n+\t| | * 11_D\n+\t| * 11_C\n+\t* 11_B\n+\tEOF\n+'\n+\n+test_expect_success 'log --graph disconnected three roots cascading' '\n+\tgit checkout --orphan 12_d &&\n+\ttest_commit 12_D &&\n+\ttest_commit 12_D1 &&\n+\tgit checkout --orphan 12_c &&\n+\ttest_commit 12_C &&\n+\tgit checkout --orphan 12_b &&\n+\ttest_commit 12_B &&\n+\tgit checkout --orphan 12_a &&\n+\ttest_commit 12_A &&\n+\n+\tcheck_graph 12_a 12_b 12_c 12_d <<-\\EOF\n+\t* 12_A\n+\t  * 12_B\n+\t    * 12_C\n+\t      * 12_D1\n+\t   _ /\n+\t  /\n+\t /\n+\t* 12_D\n+\tEOF\n+'\n+\n test_done\n-- \n2.43.0\n\n"},{"id":"540815","messageId":"xmqqpl4gocrj.fsf@gitster.g","threadId":"65419","inReplyTo":"20260402211717.3604688-1-pabloosabaterr@gmail.com","subject":"Re: [GSoC RFC PATCH 0/1] graph: add indentation for commits preceded by a root","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-03T05:04:32Z","receivedAt":"2026-04-03T05:04:35Z","isPatch":true,"body":"Pablo Sabater <pabloosabaterr@gmail.com> writes:\n\n> This issue was reported by Junio at:\n>   https://lore.kernel.org/git/xmqqikaawrpx.fsf@gitster.g/\n\nYou are giving me too much credit.  I just knew about previous\nattempts and the gotchas.\n\nOne thing that we may want to make sure your solution gets right is\nthe issue depicated in two graphs in the footnote of this message:\n\n  https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n\n    Stepping back a bit, I think concentrating too much on \"is it\n    root?\" is a wrong way to think about the problem.  Suppose you\n    have two histories, e.g. (time flows from left to right; A and X\n    are roots)\n\n            A---B\n                 \\\n          X---Y---Z\n\n    and doing \"git log --graph --oneline Z\" would show A, B, X, Y\n    and Z.\n\n    But in a slightly modified graph:\n\n          C\n         /\n        O---A---B\n                 \\\n          X---Y---Z\n\n    if you do \"git log --graph --oneline C..Z\", you should see the\n    same commits listed as above (A, B, X, Y and Z), and most likely\n    in the same order.\n\nThe way we draw A and make sure one raw below A in the same lane is\nvacant (to avoid something that is not an ancestor of A steals that\nspot) is applicable to both graphs.  The reason why we try to keep\none row below A vacant is not because it is a root, but because in\nthe graph being drawn, none of A's parent will appear.  Obviously if\nA is root, none of A's parent will appear in the graph, but in the\nlatter topology where we are drawing C..Z, none of A's parent will\nappear not because A is root, but because all the parents of A is\nexcluded.\n\n"},{"id":"540843","messageId":"CAN5EUNQZLHDSyLB=Z6RarfD1re3d=+tsUHCrL6QrjjU7eObRSQ@mail.gmail.com","threadId":"65419","inReplyTo":"xmqqpl4gocrj.fsf@gitster.g","subject":"Re: [GSoC RFC PATCH 0/1] graph: add indentation for commits preceded by a root","fromName":"Pablo","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-04-03T08:25:57Z","receivedAt":"2026-04-03T08:26:09Z","isPatch":true,"body":"El vie, 3 abr 2026 a las 7:04, Junio C Hamano (<gitster@pobox.com>) escribió:\n>\n> Pablo Sabater <pabloosabaterr@gmail.com> writes:\n>\n> > This issue was reported by Junio at:\n> >   https://lore.kernel.org/git/xmqqikaawrpx.fsf@gitster.g/\n>\n> You are giving me too much credit.  I just knew about previous\n> attempts and the gotchas.\n\nShould I mention that thread instead ?\n\n>\n> One thing that we may want to make sure your solution gets right is\n> the issue depicated in two graphs in the footnote of this message:\n>\n>   https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n>\n>     Stepping back a bit, I think concentrating too much on \"is it\n>     root?\" is a wrong way to think about the problem.  Suppose you\n>     have two histories, e.g. (time flows from left to right; A and X\n>     are roots)\n>\n>             A---B\n>                  \\\n>           X---Y---Z\n>\n>     and doing \"git log --graph --oneline Z\" would show A, B, X, Y\n>     and Z.\n>\n>     But in a slightly modified graph:\n>\n>           C\n>          /\n>         O---A---B\n>                  \\\n>           X---Y---Z\n>\n>     if you do \"git log --graph --oneline C..Z\", you should see the\n>     same commits listed as above (A, B, X, Y and Z), and most likely\n>     in the same order.\n\nI can't find the issue with the graph above, it would be shown:\n\n            A---B\n                 \\\n          X---Y---Z\n\nbut we shouldn't want indentation here tho\n\n  *   Z\n  |\\\n  | * B\n  | * A\n  * Y\n  * X\n\nB is the parent of A and there is no commit on a third branch that\ncould try to get below A.\n\nBut I do find the issue with focusing on: is a root ? for example with\nthis graph:\n\n  O---A\n\n  X---Y\n\nIf we O..A Y, it shows A, Y, X but because I only look for roots it\nends up looking like:\n\n  * A <- not a root but O is excluded\n  * Y <- no indentation\n  * X\n\nThen it's more something like 'seems_root' rather than 'is_root', and\nit should look like\n\n  * A\n    * Y\n   /\n  * X\n\nThis would make on your last graph to have indentation at the right of\nY, below A, even if not needed, it would protect A's spot the row\nbelow, which I think is desirable and what you meant with the\nexamples.\n\n>\n> The way we draw A and make sure one raw below A in the same lane is\n> vacant (to avoid something that is not an ancestor of A steals that\n> spot) is applicable to both graphs.  The reason why we try to keep\n> one row below A vacant is not because it is a root, but because in\n> the graph being drawn, none of A's parent will appear.  Obviously if\n> A is root, none of A's parent will appear in the graph, but in the\n> latter topology where we are drawing C..Z, none of A's parent will\n> appear not because A is root, but because all the parents of A is\n> excluded.\n>\n\nThanks for the feedback\nPablo\n"},{"id":"540861","messageId":"xmqqv7e8lyhf.fsf@gitster.g","threadId":"65419","inReplyTo":"20260402211717.3604688-2-pabloosabaterr@gmail.com","subject":"Re: [GSoC RFC PATCH 1/1] graph: add indentation for commits preceded by a root","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-03T17:55:56Z","receivedAt":"2026-04-03T17:55:58Z","isPatch":true,"body":"Pablo Sabater <pabloosabaterr@gmail.com> writes:\n\n> diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh\n> index 28d0779a8c..0333fea95a 100755\n> --- a/t/t4215-log-skewed-merges.sh\n> +++ b/t/t4215-log-skewed-merges.sh\n> @@ -370,4 +370,140 @@ test_expect_success 'log --graph with multiple tips' '\n>  \tEOF\n>  '\n>  \n> +test_expect_success 'log --graph with root commit' '\n> +\tgit checkout --orphan 8_a &&\n> +\ttest_commit 8_A &&\n> +\ttest_commit 8_A1 &&\n> +\tgit checkout --orphan 8_b &&\n> +\ttest_commit 8_B &&\n\nOn case challenged filesystems, you cannot have a commit \"8_a\" and\n\"8_A\" without being ambiguous.  The CI failures from last night are\nall from Windows and macOS X.\n\n\n\n"},{"id":"540862","messageId":"CAN5EUNRA-AAh2sKEV6Gff6tHpv=MANgZ4MdmH7kdXhUJh_sRVw@mail.gmail.com","threadId":"65419","inReplyTo":"xmqqv7e8lyhf.fsf@gitster.g","subject":"Re: [GSoC RFC PATCH 1/1] graph: add indentation for commits preceded by a root","fromName":"Pablo","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-04-03T18:07:33Z","receivedAt":"2026-04-03T18:07:46Z","isPatch":true,"body":"El vie, 3 abr 2026 a las 19:55, Junio C Hamano (<gitster@pobox.com>) escribió:\n>\n> Pablo Sabater <pabloosabaterr@gmail.com> writes:\n>\n> > diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh\n> > index 28d0779a8c..0333fea95a 100755\n> > --- a/t/t4215-log-skewed-merges.sh\n> > +++ b/t/t4215-log-skewed-merges.sh\n> > @@ -370,4 +370,140 @@ test_expect_success 'log --graph with multiple tips' '\n> >       EOF\n> >  '\n> >\n> > +test_expect_success 'log --graph with root commit' '\n> > +     git checkout --orphan 8_a &&\n> > +     test_commit 8_A &&\n> > +     test_commit 8_A1 &&\n> > +     git checkout --orphan 8_b &&\n> > +     test_commit 8_B &&\n>\n> On case challenged filesystems, you cannot have a commit \"8_a\" and\n> \"8_A\" without being ambiguous.  The CI failures from last night are\n> all from Windows and macOS X.\n>\n>\n>\n\nOkay I'll send a v2 with the \"seems_root\" change and fix the names,\nhadn't thought about it , I'll make sure that CI passes, sorry.\n"},{"id":"540883","messageId":"20260404092425.550346-1-pabloosabaterr@gmail.com","threadId":"65419","inReplyTo":"20260402211717.3604688-1-pabloosabaterr@gmail.com","subject":"[GSoC RFC PATCH v2 0/1] graph: add indentation for commits preceded by a parentless commit","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-04-04T09:24:24Z","receivedAt":"2026-04-04T09:24:30Z","isPatch":true,"body":"When having a history with multiple root commits or commits\nthat act like roots (they have excluded parents), let's call\nthem parentless, and drawing the history near them, the\ngraphing engine renders the commits one below the other, seeming\nthat they are related.\n\ne.g.:\n\n  * parentless-B\n  * child-A2\n  * child-A1\n  * parentless-A\n\nThis issue has been attempted multiple times:\n  https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n\nThis happens because the engine prints left to right from the first free\ncolumn and these parentless commits for the next row, their column\nbecomes empty and the engine fills that gap with the next commit (child-A2)\nseeming that parentless-B and child-A2 are related when they are not.\n\nThe actual implementation is very minimal.\nThis patch makes the parentless commits to be kept alive at least one more row to avoid\nthat, indenting the next commit to the next column and then clean the mapping\nletting the indented commit to naturally collapse to the column where the\nparentless commit was.\n\ne.g.:\n\n  * parentless-B\n    * child-A2\n   /\n  * child-A1\n  * parentless-A\n\nThis is done by adding a is_placeholder flag to the columns, the parentless\ncommit is actually there but marked as a placeholder\n\ne.g.:\n\n   * parentless-B\n  (B) * child-A2\n    /\n   * child-A1\n   * parentless-A\n\n(B) would be parentless-B column with the placeholder flag active.\n\nBy teaching the rendering function to print a padding ' ' when meeting a\nplaceholder column hides them, printing the second example.\n\nThere could also be the case where there are multiple parentless commits\n\nwithout the patch:\n\n  * A parentless\n  * B parentless\n  * C parentless\n  * D1 child\n  * D parentless\n\nwith the patch, the indentation cascades:\n\n  * A parentless\n    * B parentless\n      * C parentless\n        * D1 child\n     _ /\n    /\n   /\n  * D parentless\n\nthe _ / might look weird but that's how the collapsing rendering does it\nfor big gaps, this case being from the 4th column to the 0th column.\n\nAnother patch could change the collapsing rendering for placeholders?\nI haven't done it to keep it minimal, but a follow up could make it\nto be straight '/'. This would make it bigger but easier for the eye to follow.\nIMO is not worth it, but opinions are welcome.\n\nThe patch also adds tests for different cases like a parentless commit\npreceding multiple parents merges and the examples above.\n\nThere could be some edge cases still so any testing is very welcome.\n\nPSA: the tests are on t4215-log-skewed-merges.sh, which is not very related,\n     but other graph related tests have +140 tests, and this one has less than\n     20 and some of them are also not very related and differ in style.\n     A cleanup patch before this renaming the file and style of the tests is fine?\n\nChanges from v1:\n\n- Changed to parentless commits instead of root commits to make it more generic\n- Fixed the branch names to pass CI and fixed tests style.\n\nPablo Sabater (1):\n  graph: add indentation for commits preceded by a parentless commit\n\n graph.c                      |  70 ++++++++++++++++++--\n t/t4215-log-skewed-merges.sh | 124 +++++++++++++++++++++++++++++++++++\n 2 files changed, 188 insertions(+), 6 deletions(-)\n\n\nbase-commit: 8de2f1b07a8053d7f1aad70dc1131d6afcf5a28a\n-- \n2.43.0\n\n"},{"id":"540884","messageId":"20260404092425.550346-2-pabloosabaterr@gmail.com","threadId":"65419","inReplyTo":"20260404092425.550346-1-pabloosabaterr@gmail.com","subject":"[GSoC RFC PATCH v2 1/1] graph: add indentation for commits preceded by a parentless commit","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-04-04T09:24:25Z","receivedAt":"2026-04-04T09:24:32Z","isPatch":true,"body":"When having a history with multiple root commits or commits\nthat act like roots (they have excluded parents), let's call\nthem parentless, and drawing the history near them, the\ngraphing engine renders the commits one below the other, seeming\nthat they are related.\n\nThis issue has been attempted multiple times:\n  https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n\nThis happens because for these parentless commits, in the next\nrow the column becomes empty and the engine prints from left\nto right from the first empty column, filling the gap below\nthese parentless commits.\n\nKeep a parentless commit for at least one row more to avoid\nhaving the column empty but hide it as indentation,\ntherefore making the next unrelated commit live in\nthe next column (column means even positions where edges live:\n0, 2, 4), then clean that \"placeholder\" column and let\nthe unrelated commit to naturally collapse to the column\nwhere the parentless commit was.\n\nAdd is_placeholder to the struct column to mark if a column\nis acting as a placeholder for the padding.\n\nWhen a column is parentless, add a column with the parentless\ncommit data to prevent segfaults when 'column->commit' and\nmark it as a placeholder.\n\nTeach rendering functions to print a padding ' ' instead of\nan edge when a placeholder column is met.\n\nThen, unless the next commit is also parentless (then we\nneed to keep cascading the indentation) clean the mapping\nand columns from the placeholder to allow it to\ncollapse naturally.\n\nAdd tests for different cases.\n\nbefore this patch:\n\n* parentless-B\n* child-A2\n* child-A1\n* parentless-A\n\nafter this patch:\n\n* parentless-B\n  * child-A2\n /\n* child-A1\n* parentless-A\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c                      |  70 ++++++++++++++++++--\n t/t4215-log-skewed-merges.sh | 124 +++++++++++++++++++++++++++++++++++\n 2 files changed, 188 insertions(+), 6 deletions(-)\n\ndiff --git a/graph.c b/graph.c\nindex 26f6fbf000..e2b7516651 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -60,6 +60,12 @@ struct column {\n \t * index into column_colors.\n \t */\n \tunsigned short color;\n+\t/*\n+\t * A placeholder column keeps the column of a parentless commit filled \n+\t * for one extra row, avoiding a next unrelated commit to be printed\n+\t * in the same column.\n+\t */\n+\tunsigned is_placeholder:1;\n };\n \n enum graph_state {\n@@ -563,6 +569,7 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t\ti = graph->num_new_columns++;\n \t\tgraph->new_columns[i].commit = commit;\n \t\tgraph->new_columns[i].color = graph_find_commit_color(graph, commit);\n+\t\tgraph->new_columns[i].is_placeholder = 0;\n \t}\n \n \tif (graph->num_parents > 1 && idx > -1 && graph->merge_layout == -1) {\n@@ -607,7 +614,7 @@ static void graph_update_columns(struct git_graph *graph)\n {\n \tstruct commit_list *parent;\n \tint max_new_columns;\n-\tint i, seen_this, is_commit_in_columns;\n+\tint i, seen_this, is_commit_in_columns, seems_root;\n \n \t/*\n \t * Swap graph->columns with graph->new_columns\n@@ -654,6 +661,12 @@ static void graph_update_columns(struct git_graph *graph)\n \t */\n \tseen_this = 0;\n \tis_commit_in_columns = 1;\n+\t/*\n+\t * num_parents == 0 means that there are no parents flagged as\n+\t * interesting to being shown.\n+\t */\n+\tseems_root = graph->num_parents == 0 &&\n+\t\t     !(graph->commit->object.flags & BOUNDARY);\n \tfor (i = 0; i <= graph->num_columns; i++) {\n \t\tstruct commit *col_commit;\n \t\tif (i == graph->num_columns) {\n@@ -688,11 +701,40 @@ static void graph_update_columns(struct git_graph *graph)\n \t\t\t * least 2, even if it has no interesting parents.\n \t\t\t * The current commit always takes up at least 2\n \t\t\t * spaces.\n+\t\t\t *\n+\t\t\t * Check for the commit to seem like a root, no parents\n+\t\t\t * rendered and that it is not a boundary commit. If so,\n+\t\t\t * add a placeholder to keep that column filled for\n+\t\t\t * at least one row.\n+\t\t\t *\n+\t\t\t * Prevents the next commit from being inserted\n+\t\t\t * just below and making the graph confusing.\n \t\t\t */\n-\t\t\tif (graph->num_parents == 0)\n+\t\t\tif (seems_root) {\n+\t\t\t\tgraph_insert_into_new_columns(graph, graph->commit, i);\n+\t\t\t\tgraph->new_columns[graph->num_new_columns - 1]\n+\t\t\t\t\t\t\t    .is_placeholder = 1;\n+\t\t\t} else if (graph->num_parents == 0) {\n \t\t\t\tgraph->width += 2;\n+\t\t\t}\n \t\t} else {\n-\t\t\tgraph_insert_into_new_columns(graph, col_commit, -1);\n+\t\t\tif (graph->columns[i].is_placeholder) {\n+\t\t\t\t/*\n+\t\t\t\t * Keep the placeholders if the next commit is\n+\t\t\t\t * parentless also, making the indentation cascade.\n+\t\t\t\t */\n+\t\t\t\tif (!seen_this && seems_root) {\n+\t\t\t\t\tgraph_insert_into_new_columns(graph,\n+\t\t\t\t\t\t\tgraph->columns[i].commit, i);\n+\t\t\t\t\tgraph->new_columns[graph->num_new_columns - 1]\n+\t\t\t\t\t\t\t.is_placeholder = 1;\n+\t\t\t\t} else if (!seen_this) {\n+\t\t\t\t\tgraph->mapping[graph->width] = -1;\n+\t\t\t\t\tgraph->width += 2;\n+\t\t\t\t}\n+\t\t\t} else {\n+\t\t\t\tgraph_insert_into_new_columns(graph, col_commit, -1);\n+\t\t\t}\n \t\t}\n \t}\n \n@@ -846,7 +888,10 @@ static void graph_output_padding_line(struct git_graph *graph,\n \t * Output a padding row, that leaves all branch lines unchanged\n \t */\n \tfor (i = 0; i < graph->num_new_columns; i++) {\n-\t\tgraph_line_write_column(line, &graph->new_columns[i], '|');\n+\t\tif (graph->new_columns[i].is_placeholder)\n+\t\t\tgraph_line_write_column(line, &graph->new_columns[i], ' ');\n+\t\telse\n+\t\t\tgraph_line_write_column(line, &graph->new_columns[i], '|');\n \t\tgraph_line_addch(line, ' ');\n \t}\n }\n@@ -1058,7 +1103,13 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \t\t\t   graph->mapping[2 * i] < i) {\n \t\t\tgraph_line_write_column(line, col, '/');\n \t\t} else {\n-\t\t\tgraph_line_write_column(line, col, '|');\n+\t\t\tif (col->is_placeholder) {\n+\t\t\t\tif (seen_this)\n+\t\t\t\t\tcontinue;\n+\t\t\t\tgraph_line_write_column(line, col, ' ');\n+\t\t\t} else {\n+\t\t\t\tgraph_line_write_column(line, col, '|');\n+\t\t\t}\n \t\t}\n \t\tgraph_line_addch(line, ' ');\n \t}\n@@ -1135,7 +1186,14 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n \t\t\t\tgraph_line_write_column(line, col, '|');\n \t\t\tgraph_line_addch(line, ' ');\n \t\t} else {\n-\t\t\tgraph_line_write_column(line, col, '|');\n+\t\t\tif (col->is_placeholder) {\n+\t\t\t\tif (seen_this)\n+\t\t\t\t\tcontinue;\n+\t\t\t\tgraph_line_write_column(line, col, ' ');\n+\t\t\t} else {\n+\t\t\t\tgraph_line_write_column(line, col, '|');\n+\t\t\t}\n+\n \t\t\tif (graph->merge_layout != 0 || i != graph->commit_index - 1) {\n \t\t\t\tif (parent_col)\n \t\t\t\t\tgraph_line_write_column(\ndiff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh\nindex 28d0779a8c..0f6f95a6b5 100755\n--- a/t/t4215-log-skewed-merges.sh\n+++ b/t/t4215-log-skewed-merges.sh\n@@ -370,4 +370,128 @@ test_expect_success 'log --graph with multiple tips' '\n \tEOF\n '\n \n+test_expect_success 'log --graph with root commit' '\n+\tgit checkout --orphan 8_1 && test_commit 8_A && test_commit 8_A1 &&\n+\tgit checkout --orphan 8_2 && test_commit 8_B &&\n+\n+\tcheck_graph 8_2 8_1 <<-\\EOF\n+\t* 8_B\n+\t  * 8_A1\n+\t /\n+\t* 8_A\n+\tEOF\n+'\n+\n+test_expect_success 'log --graph with multiple root commits' '\n+\ttest_commit 8_B1 &&\n+\tgit checkout --orphan 8_3 && test_commit 8_C &&\n+\n+\tcheck_graph 8_3 8_2 8_1 <<-\\EOF\n+\t* 8_C\n+\t  * 8_B1\n+\t /\n+\t* 8_B\n+\t  * 8_A1\n+\t /\n+\t* 8_A\n+\tEOF\n+'\n+\n+test_expect_success 'log --graph commit from a two parent merge shifted' '\n+\tgit checkout --orphan 9_1 && test_commit 9_B &&\n+\tgit checkout --orphan 9_2 && test_commit 9_C &&\n+\tgit checkout 9_1 &&\n+\tgit merge 9_2 --allow-unrelated-histories -m 9_M &&\n+\tgit checkout --orphan 9_3 &&\n+\ttest_commit 9_A && test_commit 9_A1 && test_commit 9_A2 &&\n+\n+\tcheck_graph 9_3 9_1 <<-\\EOF\n+\t* 9_A2\n+\t* 9_A1\n+\t* 9_A\n+\t  * 9_M\n+\t /|\n+\t| * 9_C\n+\t* 9_B\n+\tEOF\n+'\n+\n+test_expect_success 'log --graph commit from a three parent merge shifted' '\n+\tgit checkout --orphan 10_1 && test_commit 10_B &&\n+\tgit checkout --orphan 10_2 && test_commit 10_C &&\n+\tgit checkout --orphan 10_3 && test_commit 10_D &&\n+\tgit checkout 10_1 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p 10_1 -p 10_2 -p 10_3 -m 10_M) &&\n+\tgit reset --hard $MERGE &&\n+\tgit checkout --orphan 10_4 &&\n+\ttest_commit 10_A && test_commit 10_A1 && test_commit 10_A2 &&\n+\n+\tcheck_graph 10_4 10_1 <<-\\EOF\n+\t* 10_A2\n+\t* 10_A1\n+\t* 10_A\n+\t  *   10_M\n+\t /|\\\n+\t| | * 10_D\n+\t| * 10_C\n+\t* 10_B\n+\tEOF\n+'\n+\n+test_expect_success 'log --graph commit from a four parent merge shifted' '\n+\tgit checkout --orphan 11_1 && test_commit 11_B &&\n+\tgit checkout --orphan 11_2 && test_commit 11_C &&\n+\tgit checkout --orphan 11_3 && test_commit 11_D &&\n+\tgit checkout --orphan 11_4 && test_commit 11_E &&\n+\tgit checkout 11_1 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p 11_1 -p 11_2 -p 11_3 -p 11_4 -m 11_M) &&\n+\tgit reset --hard $MERGE &&\n+\tgit checkout --orphan 11_5 &&\n+\ttest_commit 11_A && test_commit 11_A1 && test_commit 11_A2 &&\n+\n+\tcheck_graph 11_5 11_1 <<-\\EOF\n+\t* 11_A2\n+\t* 11_A1\n+\t* 11_A\n+\t  *-.   11_M\n+\t /|\\ \\\n+\t| | | * 11_E\n+\t| | * 11_D\n+\t| * 11_C\n+\t* 11_B\n+\tEOF\n+'\n+\n+test_expect_success 'log --graph disconnected three roots cascading' '\n+\tgit checkout --orphan 12_1 && test_commit 12_D && test_commit 12_D1 &&\n+\tgit checkout --orphan 12_2 && test_commit 12_C &&\n+\tgit checkout --orphan 12_3 && test_commit 12_B &&\n+\tgit checkout --orphan 12_4 && test_commit 12_A &&\n+\n+\tcheck_graph 12_4 12_3 12_2 12_1 <<-\\EOF\n+\t* 12_A\n+\t  * 12_B\n+\t    * 12_C\n+\t      * 12_D1\n+\t   _ /\n+\t  /\n+\t /\n+\t* 12_D\n+\tEOF\n+'\n+\n+test_expect_success 'log --graph with excluded parent (not a root)' '\n+\tgit checkout --orphan 13_1 && test_commit 13_X && test_commit 13_Y &&\n+\tgit checkout --orphan 13_2 && test_commit 13_O && test_commit 13_A &&\n+\n+\tcheck_graph 13_O..13_A 13_1 <<-\\EOF\n+\t* 13_A\n+\t  * 13_Y\n+\t /\n+\t* 13_X\n+\tEOF\n+'\n+\n test_done\n-- \n2.43.0\n\n"},{"id":"541388","messageId":"CAN5EUNSEt+W4kQsoTfLVJQ+KFYkcPCx3_=YTSwh8zhBMFDttEw@mail.gmail.com","threadId":"65419","inReplyTo":"20260404092425.550346-1-pabloosabaterr@gmail.com","subject":"Re: [GSoC RFC PATCH v2 0/1] graph: add indentation for commits preceded by a parentless commit","fromName":"Pablo","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-04-10T16:25:03Z","receivedAt":"2026-04-10T16:25:19Z","isPatch":true,"body":"El sáb, 4 abr 2026 a las 11:24, Pablo Sabater\n(<pabloosabaterr@gmail.com>) escribió:\n>\n> When having a history with multiple root commits or commits\n> that act like roots (they have excluded parents), let's call\n> them parentless, and drawing the history near them, the\n> graphing engine renders the commits one below the other, seeming\n> that they are related.\n>\n> e.g.:\n>\n>   * parentless-B\n>   * child-A2\n>   * child-A1\n>   * parentless-A\n>\n> This issue has been attempted multiple times:\n>   https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n>\n> This happens because the engine prints left to right from the first free\n> column and these parentless commits for the next row, their column\n> becomes empty and the engine fills that gap with the next commit (child-A2)\n> seeming that parentless-B and child-A2 are related when they are not.\n>\n> The actual implementation is very minimal.\n> This patch makes the parentless commits to be kept alive at least one more row to avoid\n> that, indenting the next commit to the next column and then clean the mapping\n> letting the indented commit to naturally collapse to the column where the\n> parentless commit was.\n>\n> e.g.:\n>\n>   * parentless-B\n>     * child-A2\n>    /\n>   * child-A1\n>   * parentless-A\n>\n> This is done by adding a is_placeholder flag to the columns, the parentless\n> commit is actually there but marked as a placeholder\n>\n> e.g.:\n>\n>    * parentless-B\n>   (B) * child-A2\n>     /\n>    * child-A1\n>    * parentless-A\n>\n> (B) would be parentless-B column with the placeholder flag active.\n>\n> By teaching the rendering function to print a padding ' ' when meeting a\n> placeholder column hides them, printing the second example.\n>\n> There could also be the case where there are multiple parentless commits\n>\n> without the patch:\n>\n>   * A parentless\n>   * B parentless\n>   * C parentless\n>   * D1 child\n>   * D parentless\n>\n> with the patch, the indentation cascades:\n>\n>   * A parentless\n>     * B parentless\n>       * C parentless\n>         * D1 child\n>      _ /\n>     /\n>    /\n>   * D parentless\n>\n> the _ / might look weird but that's how the collapsing rendering does it\n> for big gaps, this case being from the 4th column to the 0th column.\n>\n> Another patch could change the collapsing rendering for placeholders?\n> I haven't done it to keep it minimal, but a follow up could make it\n> to be straight '/'. This would make it bigger but easier for the eye to follow.\n> IMO is not worth it, but opinions are welcome.\n>\n> The patch also adds tests for different cases like a parentless commit\n> preceding multiple parents merges and the examples above.\n>\n> There could be some edge cases still so any testing is very welcome.\n>\n> PSA: the tests are on t4215-log-skewed-merges.sh, which is not very related,\n>      but other graph related tests have +140 tests, and this one has less than\n>      20 and some of them are also not very related and differ in style.\n>      A cleanup patch before this renaming the file and style of the tests is fine?\n>\n> Changes from v1:\n>\n> - Changed to parentless commits instead of root commits to make it more generic\n> - Fixed the branch names to pass CI and fixed tests style.\n>\n> Pablo Sabater (1):\n>   graph: add indentation for commits preceded by a parentless commit\n>\n>  graph.c                      |  70 ++++++++++++++++++--\n>  t/t4215-log-skewed-merges.sh | 124 +++++++++++++++++++++++++++++++++++\n>  2 files changed, 188 insertions(+), 6 deletions(-)\n>\n>\n> base-commit: 8de2f1b07a8053d7f1aad70dc1131d6afcf5a28a\n> --\n> 2.43.0\n>\n\nHi,\nI'm sending this because I think it has fallen through.\nSorry about the ping,\nPablo\n"},{"id":"541392","messageId":"xmqq5x5ypxhq.fsf@gitster.g","threadId":"65419","inReplyTo":"CAN5EUNSEt+W4kQsoTfLVJQ+KFYkcPCx3_=YTSwh8zhBMFDttEw@mail.gmail.com","subject":"Re: [GSoC RFC PATCH v2 0/1] graph: add indentation for commits preceded by a parentless commit","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-10T16:54:09Z","receivedAt":"2026-04-10T16:54:14Z","isPatch":true,"body":"Pablo <pabloosabaterr@gmail.com> writes:\n\n> El sáb, 4 abr 2026 a las 11:24, Pablo Sabater\n> (<pabloosabaterr@gmail.com>) escribió:\n>>\n>> When having a history with multiple root commits or commits\n>> that act like roots (they have excluded parents), let's call\n> ...\n> Hi,\n> I'm sending this because I think it has fallen through.\n> Sorry about the ping,\n> Pablo\n\nPinging is good than no pinging, so no need to say sorry.\n\nI think this topic (the one on April 04) has been on the \"What's\ncooking\" report since issue #02 of this month, waiting for comments\nand responses to them.  I haven't seen problems in it but that may\nbe because I do not view commits near the root commit very often.\n\nThanks.\n"},{"id":"542353","messageId":"20260427102838.44867-1-pabloosabaterr@gmail.com","threadId":"65419","inReplyTo":"20260404092425.550346-1-pabloosabaterr@gmail.com","subject":"[GSoC PATCH v3 0/1] graph: add indentation for commits preceded by a parentless commit","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-04-27T10:28:37Z","receivedAt":"2026-04-27T10:28:45Z","isPatch":true,"body":"When having a history with multiple root commits or commits\nthat act like roots (they have excluded parents), let's call\nthem parentless, and drawing the history near them, the\ngraphing engine renders the commits one below the other, seeming\nthat they are related:\n\n  * parentless A\n  * child B\n  * parentless B\n\nThis issue has been attempted multiple times:\n  https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n\nThis happens because the engine prints left to right from the first free\ncolumn and their column of these parentless commits for the next row\nbecomes empty and the engine fills that gap with the next commit (child B)\nseeming that parentless A and child B are related when they are not.\n\nThe actual implementation is very minimal.\nThis patch makes the parentless commits to be kept alive at least one more row\nto avoid that, indenting the next commit to the next column and then clean\nthe mapping letting the indented commit to naturally collapse to the column\nwhere the parentless commit was:\n\n  * parentless A\n    * child B\n   /\n  * parentless B\n\nThis is done by adding a is_placeholder flag to the columns, the parentless\ncommit is actually there but marked as a placeholder:\n\n   * parentless A\n  (A) * child B\n    /\n   * parentless B\n\n(A) would be \"parentless A\" column with the placeholder flag active.\n\nBy teaching the rendering function to print a padding ' ' when meeting a\nplaceholder column hides them, printing the second example.\n\nThere could also be the case where there are multiple parentless commits\n\nwithout the patch:\n\n  * parentless A\n  * parentless B\n  * parentless C\n  * child D\n  * parentless D\n\nwith the patch, the indentation cascades:\n\n  * parentless A\n    * parentless B\n      * parentless C\n        * child D\n     _ /\n    /\n   /\n  * parentless D\n\nthe _ / might look weird but that's how the collapsing rendering does it\nfor big gaps, this case being from the 4th column to the 0th column.\n\nA follow-up could change the collapsing rendering for placeholders?\nI haven't done it to keep it minimal, but a follow up could make it\nto be straight '/'. This would make it bigger but easier for the eye to follow.\nIs not worth it IMO, but opinions are welcome.\n\nThe patch also adds tests for different cases like a parentless commit\npreceding multiple parents merges and the examples above.\n\nPSA: the tests are on t4215-log-skewed-merges.sh, which is not very related,\n     but other graph related tests have +140 tests, and this one has less than\n     20 and some of them are also not very related and differ in style.\n     A cleanup patch before this renaming the file and style of the tests is fine?\n\nChanges from v2:\n\n- Removed trailing whitespace.\n- Added more comments to make it more clear an reviewable.\n- Changed is_root to is_parentless to follow the name at the cover letter and\n  commit.\n- simplified cover letter and commit ascii graphs.\n\nPablo Sabater (1):\n  graph: add indentation for commits preceded by a parentless commit\n\n graph.c                      | 115 ++++++++++++++++++++++++++++++--\n t/t4215-log-skewed-merges.sh | 124 +++++++++++++++++++++++++++++++++++\n 2 files changed, 233 insertions(+), 6 deletions(-)\n\n\nbase-commit: 94f057755b7941b321fd11fec1b2e3ca5313a4e0\n--\n2.43.0\n\n"},{"id":"542354","messageId":"20260427102838.44867-2-pabloosabaterr@gmail.com","threadId":"65419","inReplyTo":"20260427102838.44867-1-pabloosabaterr@gmail.com","subject":"[GSoC PATCH v3 1/1] graph: add indentation for commits preceded by a parentless commit","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-04-27T10:28:38Z","receivedAt":"2026-04-27T10:28:47Z","isPatch":true,"body":"When having a history with multiple root commits or commits\nthat act like roots (they have excluded parents), let's call\nthem parentless, and drawing the history near them, the\ngraphing engine renders the commits one below the other, seeming\nthat they are related.\n\nThis issue has been attempted multiple times:\n  https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n\nThis happens because for these parentless commits, in the next\nrow the column becomes empty and the engine prints from left\nto right from the first empty column, filling the gap below\nthese parentless commits.\n\nKeep a parentless commit for at least one row more to avoid\nhaving the column empty but hide it as indentation,\ntherefore making the next unrelated commit live in\nthe next column (column means even positions where edges live:\n0, 2, 4), then clean that \"placeholder\" column and let\nthe unrelated commit to naturally collapse to the column\nwhere the parentless commit was.\n\nAdd is_placeholder to the struct column to mark if a column\nis acting as a placeholder for the padding.\n\nWhen a column is parentless, add a column with the parentless\ncommit data to prevent segfaults when 'column->commit' and\nmark it as a placeholder.\n\nTeach rendering functions to print a padding ' ' instead of\nan edge when a placeholder column is met.\n\nThen, unless the next commit is also parentless (then we\nneed to keep cascading the indentation) clean the mapping\nand columns from the placeholder to allow it to\ncollapse naturally.\n\nAdd tests for different cases.\n\nbefore this patch:\n\n* parentless A\n* child B\n* parentless B\n\nafter this patch:\n\n* parentless A\n  * child B\n /\n* parentless B\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c                      | 115 ++++++++++++++++++++++++++++++--\n t/t4215-log-skewed-merges.sh | 124 +++++++++++++++++++++++++++++++++++\n 2 files changed, 233 insertions(+), 6 deletions(-)\n\ndiff --git a/graph.c b/graph.c\nindex 26f6fbf000..97292df998 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -60,6 +60,12 @@ struct column {\n \t * index into column_colors.\n \t */\n \tunsigned short color;\n+\t/*\n+\t * A placeholder column keeps the column of a parentless commit filled\n+\t * for one extra row, avoiding a next unrelated commit to be printed\n+\t * in the same column.\n+\t */\n+\tunsigned is_placeholder:1;\n };\n\n enum graph_state {\n@@ -563,6 +569,7 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t\ti = graph->num_new_columns++;\n \t\tgraph->new_columns[i].commit = commit;\n \t\tgraph->new_columns[i].color = graph_find_commit_color(graph, commit);\n+\t\tgraph->new_columns[i].is_placeholder = 0;\n \t}\n\n \tif (graph->num_parents > 1 && idx > -1 && graph->merge_layout == -1) {\n@@ -607,7 +614,7 @@ static void graph_update_columns(struct git_graph *graph)\n {\n \tstruct commit_list *parent;\n \tint max_new_columns;\n-\tint i, seen_this, is_commit_in_columns;\n+\tint i, seen_this, is_commit_in_columns, is_parentless;\n\n \t/*\n \t * Swap graph->columns with graph->new_columns\n@@ -654,6 +661,26 @@ static void graph_update_columns(struct git_graph *graph)\n \t */\n \tseen_this = 0;\n \tis_commit_in_columns = 1;\n+\t/*\n+\t * A commit is \"parentless\" (is a visual root that starts a new column)\n+\t * only if has no visible parents AND it's not a boundary commit.\n+\t *\n+\t * Boundary commits also have no visible parents, but they are\n+\t * NOT a visual root:\n+\t *\n+\t * 1. A boundary only appears in the output because an included commit\n+\t *    is its child. Children are always above, and the renderer draws an\n+\t *    edge down to the boundary from that child. Rather than starting\n+\t *    a column like a visual root would do, it \"inherits\" its child\n+\t *    column.\n+\t *\n+\t * 2. Included commit CAN'T appear below a boundary. Boundaries are\n+\t *    ancestors of the exclusion point; if an included commit were an\n+\t *    ancestor of the boundary it would be excluded and not rendered.\n+\t *    Boundaries therefore always sink to the bottom.\n+\t */\n+\tis_parentless = graph->num_parents == 0 &&\n+\t\t\t!(graph->commit->object.flags & BOUNDARY);\n \tfor (i = 0; i <= graph->num_columns; i++) {\n \t\tstruct commit *col_commit;\n \t\tif (i == graph->num_columns) {\n@@ -688,11 +715,46 @@ static void graph_update_columns(struct git_graph *graph)\n \t\t\t * least 2, even if it has no interesting parents.\n \t\t\t * The current commit always takes up at least 2\n \t\t\t * spaces.\n+\t\t\t *\n+\t\t\t * Check for the commit to seem like a root, no parents\n+\t\t\t * rendered and that it is not a boundary commit. If so,\n+\t\t\t * add a placeholder to keep that column filled for\n+\t\t\t * at least one row.\n+\t\t\t *\n+\t\t\t * Prevents the next commit from being inserted\n+\t\t\t * just below and making the graph confusing.\n \t\t\t */\n-\t\t\tif (graph->num_parents == 0)\n+\t\t\tif (is_parentless) {\n+\t\t\t\tgraph_insert_into_new_columns(graph, graph->commit, i);\n+\t\t\t\tgraph->new_columns[graph->num_new_columns - 1]\n+\t\t\t\t\t\t\t    .is_placeholder = 1;\n+\t\t\t} else if (graph->num_parents == 0) {\n \t\t\t\tgraph->width += 2;\n+\t\t\t}\n \t\t} else {\n-\t\t\tgraph_insert_into_new_columns(graph, col_commit, -1);\n+\t\t\tif (graph->columns[i].is_placeholder) {\n+\t\t\t\t/*\n+\t\t\t\t * Keep the placeholders if the next commit is\n+\t\t\t\t * parentless also, making the indentation cascade.\n+\t\t\t\t */\n+\t\t\t\tif (!seen_this && is_parentless) {\n+\t\t\t\t\tgraph_insert_into_new_columns(graph,\n+\t\t\t\t\t\t\tgraph->columns[i].commit, i);\n+\t\t\t\t\tgraph->new_columns[graph->num_new_columns - 1]\n+\t\t\t\t\t\t\t.is_placeholder = 1;\n+\t\t\t\t} else if (!seen_this) {\n+\t\t\t\t\tgraph->mapping[graph->width] = -1;\n+\t\t\t\t\tgraph->width += 2;\n+\t\t\t\t}\n+\t\t\t\t/*\n+\t\t\t\t * seen_this && is_placeholder means that this\n+\t\t\t\t * line is the one after the indented one, the\n+\t\t\t\t * placeholder is no longer needed, gets\n+\t\t\t\t * dropped and the columns collapses naturally.\n+\t\t\t\t */\n+\t\t\t} else {\n+\t\t\t\tgraph_insert_into_new_columns(graph, col_commit, -1);\n+\t\t\t}\n \t\t}\n \t}\n\n@@ -846,7 +908,10 @@ static void graph_output_padding_line(struct git_graph *graph,\n \t * Output a padding row, that leaves all branch lines unchanged\n \t */\n \tfor (i = 0; i < graph->num_new_columns; i++) {\n-\t\tgraph_line_write_column(line, &graph->new_columns[i], '|');\n+\t\tif (graph->new_columns[i].is_placeholder)\n+\t\t\tgraph_line_write_column(line, &graph->new_columns[i], ' ');\n+\t\telse\n+\t\t\tgraph_line_write_column(line, &graph->new_columns[i], '|');\n \t\tgraph_line_addch(line, ' ');\n \t}\n }\n@@ -1058,7 +1123,34 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \t\t\t   graph->mapping[2 * i] < i) {\n \t\t\tgraph_line_write_column(line, col, '/');\n \t\t} else {\n-\t\t\tgraph_line_write_column(line, col, '|');\n+\t\t\tif (col->is_placeholder) {\n+\t\t\t\t/*\n+\t\t\t\t * When the indented commit is a merge commit,\n+\t\t\t\t * the placeholder column adds unwanted padding\n+\t\t\t\t * between the commit and its subject.\n+\t\t\t\t *\n+\t\t\t\t *   * parentless commit\n+\t\t\t\t *     * merge commit\n+\t\t\t\t *    /|\n+\t\t\t\t *   | * parent A\n+\t\t\t\t *   *   parent B\n+\t\t\t\t *     ^^ unwanted padding\n+\t\t\t\t *\n+\t\t\t\t * Once the current commit has been seen, don't\n+\t\t\t\t * let placeholder columns to be rendered:\n+\t\t\t\t *\n+\t\t\t\t *   * parentless commit\n+\t\t\t\t *     * merge commit\n+\t\t\t\t *    /|\n+\t\t\t\t *   | * parent A\n+\t\t\t\t *   * parent B\n+\t\t\t\t */\n+\t\t\t\tif (seen_this)\n+\t\t\t\t\tcontinue;\n+\t\t\t\tgraph_line_write_column(line, col, ' ');\n+\t\t\t} else {\n+\t\t\t\tgraph_line_write_column(line, col, '|');\n+\t\t\t}\n \t\t}\n \t\tgraph_line_addch(line, ' ');\n \t}\n@@ -1135,7 +1227,18 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n \t\t\t\tgraph_line_write_column(line, col, '|');\n \t\t\tgraph_line_addch(line, ' ');\n \t\t} else {\n-\t\t\tgraph_line_write_column(line, col, '|');\n+\t\t\tif (col->is_placeholder) {\n+\t\t\t\t/*\n+\t\t\t\t * Same placeholder handling as in\n+\t\t\t\t * graph_output_commit_line().\n+\t\t\t\t */\n+\t\t\t\tif (seen_this)\n+\t\t\t\t\tcontinue;\n+\t\t\t\tgraph_line_write_column(line, col, ' ');\n+\t\t\t} else {\n+\t\t\t\tgraph_line_write_column(line, col, '|');\n+\t\t\t}\n+\n \t\t\tif (graph->merge_layout != 0 || i != graph->commit_index - 1) {\n \t\t\t\tif (parent_col)\n \t\t\t\t\tgraph_line_write_column(\ndiff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh\nindex 28d0779a8c..0f6f95a6b5 100755\n--- a/t/t4215-log-skewed-merges.sh\n+++ b/t/t4215-log-skewed-merges.sh\n@@ -370,4 +370,128 @@ test_expect_success 'log --graph with multiple tips' '\n \tEOF\n '\n\n+test_expect_success 'log --graph with root commit' '\n+\tgit checkout --orphan 8_1 && test_commit 8_A && test_commit 8_A1 &&\n+\tgit checkout --orphan 8_2 && test_commit 8_B &&\n+\n+\tcheck_graph 8_2 8_1 <<-\\EOF\n+\t* 8_B\n+\t  * 8_A1\n+\t /\n+\t* 8_A\n+\tEOF\n+'\n+\n+test_expect_success 'log --graph with multiple root commits' '\n+\ttest_commit 8_B1 &&\n+\tgit checkout --orphan 8_3 && test_commit 8_C &&\n+\n+\tcheck_graph 8_3 8_2 8_1 <<-\\EOF\n+\t* 8_C\n+\t  * 8_B1\n+\t /\n+\t* 8_B\n+\t  * 8_A1\n+\t /\n+\t* 8_A\n+\tEOF\n+'\n+\n+test_expect_success 'log --graph commit from a two parent merge shifted' '\n+\tgit checkout --orphan 9_1 && test_commit 9_B &&\n+\tgit checkout --orphan 9_2 && test_commit 9_C &&\n+\tgit checkout 9_1 &&\n+\tgit merge 9_2 --allow-unrelated-histories -m 9_M &&\n+\tgit checkout --orphan 9_3 &&\n+\ttest_commit 9_A && test_commit 9_A1 && test_commit 9_A2 &&\n+\n+\tcheck_graph 9_3 9_1 <<-\\EOF\n+\t* 9_A2\n+\t* 9_A1\n+\t* 9_A\n+\t  * 9_M\n+\t /|\n+\t| * 9_C\n+\t* 9_B\n+\tEOF\n+'\n+\n+test_expect_success 'log --graph commit from a three parent merge shifted' '\n+\tgit checkout --orphan 10_1 && test_commit 10_B &&\n+\tgit checkout --orphan 10_2 && test_commit 10_C &&\n+\tgit checkout --orphan 10_3 && test_commit 10_D &&\n+\tgit checkout 10_1 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p 10_1 -p 10_2 -p 10_3 -m 10_M) &&\n+\tgit reset --hard $MERGE &&\n+\tgit checkout --orphan 10_4 &&\n+\ttest_commit 10_A && test_commit 10_A1 && test_commit 10_A2 &&\n+\n+\tcheck_graph 10_4 10_1 <<-\\EOF\n+\t* 10_A2\n+\t* 10_A1\n+\t* 10_A\n+\t  *   10_M\n+\t /|\\\n+\t| | * 10_D\n+\t| * 10_C\n+\t* 10_B\n+\tEOF\n+'\n+\n+test_expect_success 'log --graph commit from a four parent merge shifted' '\n+\tgit checkout --orphan 11_1 && test_commit 11_B &&\n+\tgit checkout --orphan 11_2 && test_commit 11_C &&\n+\tgit checkout --orphan 11_3 && test_commit 11_D &&\n+\tgit checkout --orphan 11_4 && test_commit 11_E &&\n+\tgit checkout 11_1 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p 11_1 -p 11_2 -p 11_3 -p 11_4 -m 11_M) &&\n+\tgit reset --hard $MERGE &&\n+\tgit checkout --orphan 11_5 &&\n+\ttest_commit 11_A && test_commit 11_A1 && test_commit 11_A2 &&\n+\n+\tcheck_graph 11_5 11_1 <<-\\EOF\n+\t* 11_A2\n+\t* 11_A1\n+\t* 11_A\n+\t  *-.   11_M\n+\t /|\\ \\\n+\t| | | * 11_E\n+\t| | * 11_D\n+\t| * 11_C\n+\t* 11_B\n+\tEOF\n+'\n+\n+test_expect_success 'log --graph disconnected three roots cascading' '\n+\tgit checkout --orphan 12_1 && test_commit 12_D && test_commit 12_D1 &&\n+\tgit checkout --orphan 12_2 && test_commit 12_C &&\n+\tgit checkout --orphan 12_3 && test_commit 12_B &&\n+\tgit checkout --orphan 12_4 && test_commit 12_A &&\n+\n+\tcheck_graph 12_4 12_3 12_2 12_1 <<-\\EOF\n+\t* 12_A\n+\t  * 12_B\n+\t    * 12_C\n+\t      * 12_D1\n+\t   _ /\n+\t  /\n+\t /\n+\t* 12_D\n+\tEOF\n+'\n+\n+test_expect_success 'log --graph with excluded parent (not a root)' '\n+\tgit checkout --orphan 13_1 && test_commit 13_X && test_commit 13_Y &&\n+\tgit checkout --orphan 13_2 && test_commit 13_O && test_commit 13_A &&\n+\n+\tcheck_graph 13_O..13_A 13_1 <<-\\EOF\n+\t* 13_A\n+\t  * 13_Y\n+\t /\n+\t* 13_X\n+\tEOF\n+'\n+\n test_done\n--\n2.43.0\n\n"},{"id":"542355","messageId":"CAN5EUNR=paCXY9-pQ=78LF0cQak2rLu4eeY905LbE6d3zeygrA@mail.gmail.com","threadId":"65419","inReplyTo":"20260427102838.44867-1-pabloosabaterr@gmail.com","subject":"Re: [GSoC PATCH v3 0/1] graph: add indentation for commits preceded by a parentless commit","fromName":"Pablo","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-04-27T10:35:53Z","receivedAt":"2026-04-27T10:36:06Z","isPatch":true,"body":"El lun, 27 abr 2026 a las 12:28, Pablo Sabater\n(<pabloosabaterr@gmail.com>) escribió:\n>\n> Pablo Sabater (1):\n>   graph: add indentation for commits preceded by a parentless commit\n>\n>  graph.c                      | 115 ++++++++++++++++++++++++++++++--\n>  t/t4215-log-skewed-merges.sh | 124 +++++++++++++++++++++++++++++++++++\n>  2 files changed, 233 insertions(+), 6 deletions(-)\n>\n>\n> base-commit: 94f057755b7941b321fd11fec1b2e3ca5313a4e0\n\nHi!\nThis patch seems to have a problem to get reviewed, I improved the comments\nat the code and simplified the example graphs at the cover letter and\npatch to try\nto make it easier to review.\n\nLet me know if any clarification is needed,\n--\nPablo\n"},{"id":"543285","messageId":"20260513230216.GA1378627@coredump.intra.peff.net","threadId":"65419","inReplyTo":"20260427102838.44867-2-pabloosabaterr@gmail.com","subject":"Re: [GSoC PATCH v3 1/1] graph: add indentation for commits preceded by a parentless commit","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-05-13T23:02:16Z","receivedAt":"2026-05-13T23:02:18Z","isPatch":true,"body":"On Mon, Apr 27, 2026 at 12:28:38PM +0200, Pablo Sabater wrote:\n\n> @@ -1135,7 +1227,18 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n>  \t\t\t\tgraph_line_write_column(line, col, '|');\n>  \t\t\tgraph_line_addch(line, ' ');\n>  \t\t} else {\n> -\t\t\tgraph_line_write_column(line, col, '|');\n> +\t\t\tif (col->is_placeholder) {\n> +\t\t\t\t/*\n> +\t\t\t\t * Same placeholder handling as in\n> +\t\t\t\t * graph_output_commit_line().\n> +\t\t\t\t */\n> +\t\t\t\tif (seen_this)\n> +\t\t\t\t\tcontinue;\n> +\t\t\t\tgraph_line_write_column(line, col, ' ');\n> +\t\t\t} else {\n> +\t\t\t\tgraph_line_write_column(line, col, '|');\n> +\t\t\t}\n\nI haven't looked closely at the patch, but Coverity complained that\nthe \"if (seen_this)\" check here is dead code, because this whole else\nblock follows:\n\n                  } else if (seen_this) {\n                          if (graph->edges_added > 0)\n                                  graph_line_write_column(line, col, '\\\\');\n                          else\n                                  graph_line_write_column(line, col, '|');\n                          graph_line_addch(line, ' ');\n                  } else {\n\t\t\t...the code above...\n\nI don't know if that just means the continue here is redundant and can\nbe removed, or if it's a sign of a larger logic error.\n\n-Peff\n"},{"id":"543293","messageId":"CAN5EUNSxyT5EyTf8b4evbW+JbDeRms91zQEn_JgiinOgvpe6mQ@mail.gmail.com","threadId":"65419","inReplyTo":"20260513230216.GA1378627@coredump.intra.peff.net","subject":"Re: [GSoC PATCH v3 1/1] graph: add indentation for commits preceded by a parentless commit","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-05-14T10:19:01Z","receivedAt":"2026-05-14T10:19:17Z","isPatch":true,"body":"El jue, 14 may 2026 a las 1:02, Jeff King (<peff@peff.net>) escribió:\n>\n> On Mon, Apr 27, 2026 at 12:28:38PM +0200, Pablo Sabater wrote:\n>\n> > @@ -1135,7 +1227,18 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n> >                               graph_line_write_column(line, col, '|');\n> >                       graph_line_addch(line, ' ');\n> >               } else {\n> > -                     graph_line_write_column(line, col, '|');\n> > +                     if (col->is_placeholder) {\n> > +                             /*\n> > +                              * Same placeholder handling as in\n> > +                              * graph_output_commit_line().\n> > +                              */\n> > +                             if (seen_this)\n> > +                                     continue;\n> > +                             graph_line_write_column(line, col, ' ');\n> > +                     } else {\n> > +                             graph_line_write_column(line, col, '|');\n> > +                     }\n>\n> I haven't looked closely at the patch, but Coverity complained that\n> the \"if (seen_this)\" check here is dead code, because this whole else\n> block follows:\n>\n>                   } else if (seen_this) {\n>                           if (graph->edges_added > 0)\n>                                   graph_line_write_column(line, col, '\\\\');\n>                           else\n>                                   graph_line_write_column(line, col, '|');\n>                           graph_line_addch(line, ' ');\n>                   } else {\n>                         ...the code above...\n>\n> I don't know if that just means the continue here is redundant and can\n> be removed, or if it's a sign of a larger logic error.\n>\n> -Peff\n\nIt is dead code. The behaviour for placeholder at\n\"graph_output_commit_line()\" and \"graph_output_post_merge_line()\" is\nthe same, if it's a placeholder print a padding instead of an edge,\nbut I didn't give it a second thought, graph_output_commit_line() can\nhave a placeholder at its right (that's why it needs the continue to\navoid extra padding) but post merge can't and as it is dead code I\ndidn't notice.\nI'll drop the dead code.\n\nThanks,\n\n--\nPablo\n"},{"id":"543328","messageId":"26d887d2-6ec2-4af1-b0bd-8e9b017bb4dd@gmail.com","threadId":"65419","inReplyTo":"20260402211717.3604688-1-pabloosabaterr@gmail.com","subject":"Re: [GSoC RFC PATCH 0/1] graph: add indentation for commits preceded by a root","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-05-14T15:15:28Z","receivedAt":"2026-05-14T15:15:32Z","isPatch":true,"body":"Hi Pablo\n\nOn 02/04/2026 22:17, Pablo Sabater wrote:\n> When having a history with multiple root commits and drawing the history\n> near the roots, the graphing engine renders the commit one below the other,\n> seeming that they are related, which makes the graph confusing.\n> \n> This issue was reported by Junio at:\n>    https://lore.kernel.org/git/xmqqikaawrpx.fsf@gitster.g/\n> \n> e.g.:\n> \n>    * root-B\n>    * child-A2\n>    * child-A1\n>    * root-A\n> \n> [...]\n >\n>    * root-B\n>      * child-A2\n>     /\n>    * child-A1\n>    * root-A\n\nI'm rather late to the party here, but personally I find the indentation \na bit confusing, it would be clearer to me if we had a blank line after \na root commit\n\n     * root-B\n\n     * child-A2\n     * child-A1\n     * root-A\n\nIt takes the same amount of vertical space but keeps the children of \nroot-A together.\n\nThanks\n\nPhillip\n\n> This is done by adding a is_placeholder flag to the columns, the root commit\n> is actually there but marked as a placeholder\n> \n> e.g.:\n> \n>     * root-B\n>    (B) * child-A2\n>      /\n>     * child-A1\n>     * root-A\n> \n> (B) would be root-B column with the placeholder flag active.\n> \n> Then teaching the rendering function to print a padding ' ' when meeting a\n> placeholder column outputs the second example.\n> \n> There could also be the case where there are multiple roots\n> \n> without the patch:\n> \n>    * A root\n>    * B root\n>    * C root\n>    * D1 child\n>    * D root\n> \n> with the patch, the indentation cascades:\n> \n>    * A root\n>      * B root\n>        * C root\n>          * D1 child\n>       _ /\n>      /\n>     /\n>    * D root\n> \n> the _ / might look weird but that's how the collapsing rendering does it\n> for big gaps, this case being from the 4th column to the 0th column.\n> Another patch could change the collapsing rendering for placeholders ?\n> I haven't done it to keep it minimal, but a follow up could make it\n> to be straight '/'. This would make it bigger but easier for the eye to follow.\n> IMO is not worth it, but opinions are welcome.\n> \n> The patch also adds tests for different cases like a root preceding multiple\n> parents merges and the examples above.\n> \n> There could be some edge cases still so any testing is very welcome.\n> \n> Pablo Sabater (1):\n>    graph: add indentation for commits preceded by a root\n> \n>   graph.c                      |  68 ++++++++++++++++--\n>   t/t4215-log-skewed-merges.sh | 136 +++++++++++++++++++++++++++++++++++\n>   2 files changed, 198 insertions(+), 6 deletions(-)\n> \n> \n> base-commit: 256554692df0685b45e60778b08802b720880c50\n\n"},{"id":"543343","messageId":"CAN5EUNQCsKD0CJqDi43i2JVBQQChAZVt_THQ1wGpdeydNHHCFw@mail.gmail.com","threadId":"65419","inReplyTo":"26d887d2-6ec2-4af1-b0bd-8e9b017bb4dd@gmail.com","subject":"Re: [GSoC RFC PATCH 0/1] graph: add indentation for commits preceded by a root","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-05-14T17:45:31Z","receivedAt":"2026-05-14T17:45:47Z","isPatch":true,"body":"El jue, 14 may 2026 a las 17:15, Phillip Wood\n(<phillip.wood123@gmail.com>) escribió:\n>\n> Hi Pablo\n>\n> On 02/04/2026 22:17, Pablo Sabater wrote:\n> > When having a history with multiple root commits and drawing the history\n> > near the roots, the graphing engine renders the commit one below the other,\n> > seeming that they are related, which makes the graph confusing.\n> >\n> > This issue was reported by Junio at:\n> >    https://lore.kernel.org/git/xmqqikaawrpx.fsf@gitster.g/\n> >\n> > e.g.:\n> >\n> >    * root-B\n> >    * child-A2\n> >    * child-A1\n> >    * root-A\n> >\n> > [...]\n>  >\n> >    * root-B\n> >      * child-A2\n> >     /\n> >    * child-A1\n> >    * root-A\n>\n> I'm rather late to the party here, but personally I find the indentation\n> a bit confusing, it would be clearer to me if we had a blank line after\n> a root commit\n\nHi,\n\n>\n>      * root-B\n>\n>      * child-A2\n>      * child-A1\n>      * root-A\n>\n> It takes the same amount of vertical space but keeps the children of\n> root-A together.\n\nI have mixed feelings about which approach to choose.\nThe idea of a blank line was thought at\nhttps://lore.kernel.org/git/xmqq8s8vvw9m.fsf@gitster.c.googlers.com/\nbut Junio argued against it for having an extra row because the\nindentation he proposed didn't collapse, however I find indentation +\nno collapse the most confusing one.\nI'd say that I'm fine with both approaches, blank line or indentation\n+ collapse.\n\n> > without the patch:\n> >\n> >    * A root\n> >    * B root\n> >    * C root\n> >    * D1 child\n> >    * D root\n> >\n> > with the patch, the indentation cascades:\n> >\n> >    * A root\n> >      * B root\n> >        * C root\n> >          * D1 child\n> >       _ /\n> >      /\n> >     /\n> >    * D root\n\n  * A root\n\n  * B root\n\n  * C root\n\n  * D1 child\n\n  * D root\n\nHere I think a blank line looks worse, too much space for just 5\ncommits and becomes one extra line which if this were like up to 7 or\nmore parentless commits one after the other would be more noticeable.\nBut there are cases that blank line might be better:\n\n  * 10_A2\n  * 10_A1\n  * 10_A\n    *   10_M\n   /|\\\n  | | * 10_D\n  | * 10_C\n  * 10_B\n\nFeels like a shower of commits instead of an indented merge.\n\nPro to the blank line, the parentless check is the same and it's just\nprinting a '\\n' at the right spot, while indent i'm mimicking like if\nthere was a commit there.\nAnyways, I think in the majority of the cases the indentation +\ncollapsing looks better.\nSorry for the brief reply, I'm busy today.\n\nRegards,\n\n--\nPablo\n\n>\n> Thanks\n>\n> Phillip\n>\n> > This is done by adding a is_placeholder flag to the columns, the root commit\n> > is actually there but marked as a placeholder\n> >\n> > e.g.:\n> >\n> >     * root-B\n> >    (B) * child-A2\n> >      /\n> >     * child-A1\n> >     * root-A\n> >\n> > (B) would be root-B column with the placeholder flag active.\n> >\n> > Then teaching the rendering function to print a padding ' ' when meeting a\n> > placeholder column outputs the second example.\n> >\n> > There could also be the case where there are multiple roots\n> >\n> > without the patch:\n> >\n> >    * A root\n> >    * B root\n> >    * C root\n> >    * D1 child\n> >    * D root\n> >\n> > with the patch, the indentation cascades:\n> >\n> >    * A root\n> >      * B root\n> >        * C root\n> >          * D1 child\n> >       _ /\n> >      /\n> >     /\n> >    * D root\n> >\n> > the _ / might look weird but that's how the collapsing rendering does it\n> > for big gaps, this case being from the 4th column to the 0th column.\n> > Another patch could change the collapsing rendering for placeholders ?\n> > I haven't done it to keep it minimal, but a follow up could make it\n> > to be straight '/'. This would make it bigger but easier for the eye to follow.\n> > IMO is not worth it, but opinions are welcome.\n> >\n> > The patch also adds tests for different cases like a root preceding multiple\n> > parents merges and the examples above.\n> >\n> > There could be some edge cases still so any testing is very welcome.\n> >\n> > Pablo Sabater (1):\n> >    graph: add indentation for commits preceded by a root\n> >\n> >   graph.c                      |  68 ++++++++++++++++--\n> >   t/t4215-log-skewed-merges.sh | 136 +++++++++++++++++++++++++++++++++++\n> >   2 files changed, 198 insertions(+), 6 deletions(-)\n> >\n> >\n> > base-commit: 256554692df0685b45e60778b08802b720880c50\n>\n"},{"id":"543391","messageId":"2e8b9b1b-6a69-4e94-95ea-7f587435bfce@gmail.com","threadId":"65419","inReplyTo":"CAN5EUNQCsKD0CJqDi43i2JVBQQChAZVt_THQ1wGpdeydNHHCFw@mail.gmail.com","subject":"Re: [GSoC RFC PATCH 0/1] graph: add indentation for commits preceded by a root","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-05-15T09:33:28Z","receivedAt":"2026-05-15T09:33:32Z","isPatch":true,"body":"On 14/05/2026 18:45, Pablo Sabater wrote:\n> El jue, 14 may 2026 a las 17:15, Phillip Wood\n> (<phillip.wood123@gmail.com>) escribió:\n>> On 02/04/2026 22:17, Pablo Sabater wrote:\n>>> When having a history with multiple root commits and drawing the history\n>>> near the roots, the graphing engine renders the commit one below the other,\n>>> seeming that they are related, which makes the graph confusing.\n>>>\n>>> This issue was reported by Junio at:\n>>>     https://lore.kernel.org/git/xmqqikaawrpx.fsf@gitster.g/\n>>>\n>>> e.g.:\n>>>\n>>>     * root-B\n>>>     * child-A2\n>>>     * child-A1\n>>>     * root-A\n>>>\n>>> [...]\n>>   >\n>>>     * root-B\n>>>       * child-A2\n>>>      /\n>>>     * child-A1\n>>>     * root-A\n>>\n>> I'm rather late to the party here, but personally I find the indentation\n>> a bit confusing, it would be clearer to me if we had a blank line after\n>> a root commit\n> \n> Hi,\n> \n>>\n>>       * root-B\n>>\n>>       * child-A2\n>>       * child-A1\n>>       * root-A\n>>\n>> It takes the same amount of vertical space but keeps the children of\n>> root-A together.\n> \n> I have mixed feelings about which approach to choose.\n> The idea of a blank line was thought at\n> https://lore.kernel.org/git/xmqq8s8vvw9m.fsf@gitster.c.googlers.com/\n> but Junio argued against it for having an extra row because the\n> indentation he proposed didn't collapse, however I find indentation +\n> no collapse the most confusing one.\n> I'd say that I'm fine with both approaches, blank line or indentation\n> + collapse.\n\nI'm afraid I don't understand this - what does it mean for the \nindentation to collapse, or not collapse. Looking at the examples Junio \ngave they look quite nice to me, though I'd find it clearer if\n\n\n  | | *  12345678 2021-01-14 merge xxxxx@xxxx into the history\n  | | |\\\n  | | | \\\n  | | *  \\  23456789 2021-01-12 merge citest into the main history\n  | | |\\  * 5505e019c2 2014-07-09 initial xxxxxx@xxxx\n  | | | *  3e658f4085 2019-09-10 (wiki/wip-citest, origin/wip-citest) \nAdded defau\n  | | | *  ad148aafe6 2019-09-10 Added default CI/CD Jenkinsfile (from \nf7daf088)\n\nwas rendered as\n\n\n  | | *  12345678 2021-01-14 merge xxxxx@xxxx into the history\n  | | |\\\n  | | | *  5505e019c2 2014-07-09 initial xxxxxx@xxxx\n  | | *    23456789 2021-01-12 merge citest into the main history\n  | | |\\\n  | | | *  3e658f4085 2019-09-10 (wiki/wip-citest, origin/wip-citest) \nAdded defau\n  | | | *  ad148aafe6 2019-09-10 Added default CI/CD Jenkinsfile (from \nf7daf088)\n\n>>> without the patch:\n>>>\n>>>     * A root\n>>>     * B root\n>>>     * C root\n>>>     * D1 child\n>>>     * D root\n>>>\n>>> with the patch, the indentation cascades:\n>>>\n>>>     * A root\n>>>       * B root\n>>>         * C root\n>>>           * D1 child\n>>>        _ /\n>>>       /\n>>>      /\n>>>     * D root\n> \n>    * A root\n> \n>    * B root\n> \n>    * C root\n> \n>    * D1 child\n> \n>    * D root\n> \n> Here I think a blank line looks worse, too much space for just 5\n> commits and becomes one extra line which if this were like up to 7 or\n> more parentless commits one after the other would be more noticeable.\n\nBut there shouldn't be a blank line between D and D1 so the two \nalternatives take up the same amount of vertical space, the main \ndifference being whether D1 appears next to D\n\n     * A root     * A root\n                    * B root\n     * B root         * C root\n                        * D1 child\n     * C root         _/\n                    /\n     * D1 child    /\n     * D root     * D root\n\nOf course if the indentation was smarter it would take up less room and \nlook better than having blank lines\n\n     * A root\n       * B root\n         * C root\n     * D1 child\n     * D root\n\n> But there are cases that blank line might be better:\n> \n>    * 10_A2\n>    * 10_A1\n>    * 10_A\n>      *   10_M\n>     /|\\\n>    | | * 10_D\n>    | * 10_C\n>    * 10_B\n> \n> Feels like a shower of commits instead of an indented merge.\n\nYes, that is a bit confusing. I think the thing I find confusing with \nthis approach is that we're treating the commit rendered below the root \ncommit specially, rather than treating the root commit itself specially. \nTo me it is the root commit that's the odd one out because it does not \nhave any parents, but we treat the commit that's rendered below as the \nodd one by indenting it relative to its parents.\n\n> Pro to the blank line, the parentless check is the same and it's just\n> printing a '\\n' at the right spot, while indent i'm mimicking like if\n> there was a commit there.\n> Anyways, I think in the majority of the cases the indentation +\n> collapsing looks better.\n> Sorry for the brief reply, I'm busy today.\n\nNo need to apologize, it seemed quite comprehensive to me\n\nThanks\n\nPhillip\n\n> Regards,\n> \n> --\n> Pablo\n> \n>>\n>> Thanks\n>>\n>> Phillip\n>>\n>>> This is done by adding a is_placeholder flag to the columns, the root commit\n>>> is actually there but marked as a placeholder\n>>>\n>>> e.g.:\n>>>\n>>>      * root-B\n>>>     (B) * child-A2\n>>>       /\n>>>      * child-A1\n>>>      * root-A\n>>>\n>>> (B) would be root-B column with the placeholder flag active.\n>>>\n>>> Then teaching the rendering function to print a padding ' ' when meeting a\n>>> placeholder column outputs the second example.\n>>>\n>>> There could also be the case where there are multiple roots\n>>>\n>>> without the patch:\n>>>\n>>>     * A root\n>>>     * B root\n>>>     * C root\n>>>     * D1 child\n>>>     * D root\n>>>\n>>> with the patch, the indentation cascades:\n>>>\n>>>     * A root\n>>>       * B root\n>>>         * C root\n>>>           * D1 child\n>>>        _ /\n>>>       /\n>>>      /\n>>>     * D root\n>>>\n>>> the _ / might look weird but that's how the collapsing rendering does it\n>>> for big gaps, this case being from the 4th column to the 0th column.\n>>> Another patch could change the collapsing rendering for placeholders ?\n>>> I haven't done it to keep it minimal, but a follow up could make it\n>>> to be straight '/'. This would make it bigger but easier for the eye to follow.\n>>> IMO is not worth it, but opinions are welcome.\n>>>\n>>> The patch also adds tests for different cases like a root preceding multiple\n>>> parents merges and the examples above.\n>>>\n>>> There could be some edge cases still so any testing is very welcome.\n>>>\n>>> Pablo Sabater (1):\n>>>     graph: add indentation for commits preceded by a root\n>>>\n>>>    graph.c                      |  68 ++++++++++++++++--\n>>>    t/t4215-log-skewed-merges.sh | 136 +++++++++++++++++++++++++++++++++++\n>>>    2 files changed, 198 insertions(+), 6 deletions(-)\n>>>\n>>>\n>>> base-commit: 256554692df0685b45e60778b08802b720880c50\n>>\n\n"},{"id":"543472","messageId":"CA+J6zkTGgeNuH0eusTy+t8LO3bjygSz4svJB=K4R5ASmBdd0uQ@mail.gmail.com","threadId":"65419","inReplyTo":"2e8b9b1b-6a69-4e94-95ea-7f587435bfce@gmail.com","subject":"Re: [GSoC RFC PATCH 0/1] graph: add indentation for commits preceded by a root","fromName":"Chandra Pratap","fromEmail":"chandrapratap3519@gmail.com","sentAt":"2026-05-17T06:31:57Z","receivedAt":"2026-05-17T06:32:26Z","isPatch":true,"body":"Hi all,\n\nOn Fri, 15 May 2026 at 15:03, Phillip Wood <phillip.wood123@gmail.com> wrote:\n>\n> On 14/05/2026 18:45, Pablo Sabater wrote:\n> > El jue, 14 may 2026 a las 17:15, Phillip Wood\n> > (<phillip.wood123@gmail.com>) escribió:\n> >> On 02/04/2026 22:17, Pablo Sabater wrote:\n> >>> When having a history with multiple root commits and drawing the history\n> >>> near the roots, the graphing engine renders the commit one below the other,\n> >>> seeming that they are related, which makes the graph confusing.\n> >>>\n> >>> This issue was reported by Junio at:\n> >>>     https://lore.kernel.org/git/xmqqikaawrpx.fsf@gitster.g/\n> >>>\n> >>> e.g.:\n> >>>\n> >>>     * root-B\n> >>>     * child-A2\n> >>>     * child-A1\n> >>>     * root-A\n> >>>\n> >>> [...]\n> >>   >\n> >>>     * root-B\n> >>>       * child-A2\n> >>>      /\n> >>>     * child-A1\n> >>>     * root-A\n> >>\n> >> I'm rather late to the party here, but personally I find the indentation\n> >> a bit confusing, it would be clearer to me if we had a blank line after\n> >> a root commit\n> >\n> > Hi,\n> >\n> >>\n> >>       * root-B\n> >>\n> >>       * child-A2\n> >>       * child-A1\n> >>       * root-A\n> >>\n> >> It takes the same amount of vertical space but keeps the children of\n> >> root-A together.\n> >\n> > I have mixed feelings about which approach to choose.\n> > The idea of a blank line was thought at\n> > https://lore.kernel.org/git/xmqq8s8vvw9m.fsf@gitster.c.googlers.com/\n> > but Junio argued against it for having an extra row because the\n> > indentation he proposed didn't collapse, however I find indentation +\n> > no collapse the most confusing one.\n> > I'd say that I'm fine with both approaches, blank line or indentation\n> > + collapse.\n>\n> I'm afraid I don't understand this - what does it mean for the\n> indentation to collapse, or not collapse. Looking at the examples Junio\n> gave they look quite nice to me, though I'd find it clearer if\n>\n>\n>   | | *  12345678 2021-01-14 merge xxxxx@xxxx into the history\n>   | | |\\\n>   | | | \\\n>   | | *  \\  23456789 2021-01-12 merge citest into the main history\n>   | | |\\  * 5505e019c2 2014-07-09 initial xxxxxx@xxxx\n>   | | | *  3e658f4085 2019-09-10 (wiki/wip-citest, origin/wip-citest)\n> Added defau\n>   | | | *  ad148aafe6 2019-09-10 Added default CI/CD Jenkinsfile (from\n> f7daf088)\n>\n> was rendered as\n>\n>\n>   | | *  12345678 2021-01-14 merge xxxxx@xxxx into the history\n>   | | |\\\n>   | | | *  5505e019c2 2014-07-09 initial xxxxxx@xxxx\n>   | | *    23456789 2021-01-12 merge citest into the main history\n>   | | |\\\n>   | | | *  3e658f4085 2019-09-10 (wiki/wip-citest, origin/wip-citest)\n> Added defau\n>   | | | *  ad148aafe6 2019-09-10 Added default CI/CD Jenkinsfile (from\n> f7daf088)\n\nIt probably *does* look clearer here, but I have the same reservations\nagainst this as Junio: the break won't be as noticeable when --graph is\n*not* used with --oneline.\n\n> >>> without the patch:\n> >>>\n> >>>     * A root\n> >>>     * B root\n> >>>     * C root\n> >>>     * D1 child\n> >>>     * D root\n> >>>\n> >>> with the patch, the indentation cascades:\n> >>>\n> >>>     * A root\n> >>>       * B root\n> >>>         * C root\n> >>>           * D1 child\n> >>>        _ /\n> >>>       /\n> >>>      /\n> >>>     * D root\n> >\n> >    * A root\n> >\n> >    * B root\n> >\n> >    * C root\n> >\n> >    * D1 child\n> >\n> >    * D root\n> >\n> > Here I think a blank line looks worse, too much space for just 5\n> > commits and becomes one extra line which if this were like up to 7 or\n> > more parentless commits one after the other would be more noticeable.\n>\n> But there shouldn't be a blank line between D and D1 so the two\n> alternatives take up the same amount of vertical space, the main\n> difference being whether D1 appears next to D\n>\n>      * A root     * A root\n>                     * B root\n>      * B root         * C root\n>                         * D1 child\n>      * C root         _/\n>                     /\n>      * D1 child    /\n>      * D root     * D root\n>\n> Of course if the indentation was smarter it would take up less room and\n> look better than having blank lines\n>\n>      * A root\n>        * B root\n>          * C root\n>      * D1 child\n>      * D root\n\nRight, this would be ideal but that would require too much change to the\nexisting graphing logic, and should be its own patch.\n\n> > But there are cases that blank line might be better:\n> >\n> >    * 10_A2\n> >    * 10_A1\n> >    * 10_A\n> >      *   10_M\n> >     /|\\\n> >    | | * 10_D\n> >    | * 10_C\n> >    * 10_B\n> >\n> > Feels like a shower of commits instead of an indented merge.\n>\n> Yes, that is a bit confusing. I think the thing I find confusing with\n> this approach is that we're treating the commit rendered below the root\n> commit specially, rather than treating the root commit itself specially.\n> To me it is the root commit that's the odd one out because it does not\n> have any parents, but we treat the commit that's rendered below as the\n> odd one by indenting it relative to its parents.\n\nI guess that would make the examples look something like this:\n\n  * A root\n  * B root\n  * C root\n* D1 child\n* D root\n\nNo cascading, and no need for that massive _ / collapse line.\n\n* 10_A2\n* 10_A1\n \\\n  * 10_A\n*   10_M\n| \\ \\\n| | * 10_D\n| * 10_C\n* 10_B\n\nI say it looks better than the alternatives, but I'm not sure if this will\nbe easy to implement. The diagonal connection line (\\) will need to\nbe printed before printing the actual root commit, which will require\nlookahead logic.\n\nI'd prefer to avoid major surgery on the codebase.\n\n> > Pro to the blank line, the parentless check is the same and it's just\n> > printing a '\\n' at the right spot, while indent i'm mimicking like if\n> > there was a commit there.\n> > Anyways, I think in the majority of the cases the indentation +\n> > collapsing looks better.\n> > Sorry for the brief reply, I'm busy today.\n>\n> No need to apologize, it seemed quite comprehensive to me\n>\n> Thanks\n>\n> Phillip\n>\n> > Regards,\n> >\n> > --\n> > Pablo\n> >\n> >>\n> >> Thanks\n> >>\n> >> Phillip\n> >>\n> >>> This is done by adding a is_placeholder flag to the columns, the root commit\n> >>> is actually there but marked as a placeholder\n> >>>\n> >>> e.g.:\n> >>>\n> >>>      * root-B\n> >>>     (B) * child-A2\n> >>>       /\n> >>>      * child-A1\n> >>>      * root-A\n> >>>\n> >>> (B) would be root-B column with the placeholder flag active.\n> >>>\n> >>> Then teaching the rendering function to print a padding ' ' when meeting a\n> >>> placeholder column outputs the second example.\n> >>>\n> >>> There could also be the case where there are multiple roots\n> >>>\n> >>> without the patch:\n> >>>\n> >>>     * A root\n> >>>     * B root\n> >>>     * C root\n> >>>     * D1 child\n> >>>     * D root\n> >>>\n> >>> with the patch, the indentation cascades:\n> >>>\n> >>>     * A root\n> >>>       * B root\n> >>>         * C root\n> >>>           * D1 child\n> >>>        _ /\n> >>>       /\n> >>>      /\n> >>>     * D root\n> >>>\n> >>> the _ / might look weird but that's how the collapsing rendering does it\n> >>> for big gaps, this case being from the 4th column to the 0th column.\n> >>> Another patch could change the collapsing rendering for placeholders ?\n> >>> I haven't done it to keep it minimal, but a follow up could make it\n> >>> to be straight '/'. This would make it bigger but easier for the eye to follow.\n> >>> IMO is not worth it, but opinions are welcome.\n> >>>\n> >>> The patch also adds tests for different cases like a root preceding multiple\n> >>> parents merges and the examples above.\n> >>>\n> >>> There could be some edge cases still so any testing is very welcome.\n> >>>\n> >>> Pablo Sabater (1):\n> >>>     graph: add indentation for commits preceded by a root\n> >>>\n> >>>    graph.c                      |  68 ++++++++++++++++--\n> >>>    t/t4215-log-skewed-merges.sh | 136 +++++++++++++++++++++++++++++++++++\n> >>>    2 files changed, 198 insertions(+), 6 deletions(-)\n> >>>\n> >>>\n> >>> base-commit: 256554692df0685b45e60778b08802b720880c50\n> >>\n>\n\nThanks,\nChandra.\n"},{"id":"543533","messageId":"CAN5EUNQoKRqt3FGLmzRGpPU1nO5jCAogP8Wm9gBZXuPbMNbQAw@mail.gmail.com","threadId":"65419","inReplyTo":"CA+J6zkTGgeNuH0eusTy+t8LO3bjygSz4svJB=K4R5ASmBdd0uQ@mail.gmail.com","subject":"Re: [GSoC RFC PATCH 0/1] graph: add indentation for commits preceded by a root","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-05-18T13:26:45Z","receivedAt":"2026-05-18T13:27:01Z","isPatch":true,"body":"Hi Chandra, Phillip,\n\n> > >\n> > > I have mixed feelings about which approach to choose.\n> > > The idea of a blank line was thought at\n> > > https://lore.kernel.org/git/xmqq8s8vvw9m.fsf@gitster.c.googlers.com/\n> > > but Junio argued against it for having an extra row because the\n> > > indentation he proposed didn't collapse, however I find indentation +\n> > > no collapse the most confusing one.\n> > > I'd say that I'm fine with both approaches, blank line or indentation\n> > > + collapse.\n> >\n> > I'm afraid I don't understand this - what does it mean for the\n> > indentation to collapse, or not collapse.\n\nCollapsing would be when branches move to the left, eg:\n\n  *\n  |\\     <- merge\n  | *\n  |/     <- collapse\n  *\n> > Looking at the examples Junio\n> > gave they look quite nice to me, though I'd find it clearer if\n> >\n> >\n> >   | | *  12345678 2021-01-14 merge xxxxx@xxxx into the history\n> >   | | |\\\n> >   | | | \\\n> >   | | *  \\  23456789 2021-01-12 merge citest into the main history\n> >   | | |\\  * 5505e019c2 2014-07-09 initial xxxxxx@xxxx\n> >   | | | *  3e658f4085 2019-09-10 (wiki/wip-citest, origin/wip-citest)\n> > Added defau\n> >   | | | *  ad148aafe6 2019-09-10 Added default CI/CD Jenkinsfile (from\n> > f7daf088)\n> >\n> > was rendered as\n> >\n> >\n> >   | | *  12345678 2021-01-14 merge xxxxx@xxxx into the history\n> >   | | |\\\n> >   | | | *  5505e019c2 2014-07-09 initial xxxxxx@xxxx\n> >   | | *    23456789 2021-01-12 merge citest into the main history\n> >   | | |\\\n> >   | | | *  3e658f4085 2019-09-10 (wiki/wip-citest, origin/wip-citest)\n> > Added defau\n> >   | | | *  ad148aafe6 2019-09-10 Added default CI/CD Jenkinsfile (from\n> > f7daf088)\n>\n> It probably *does* look clearer here, but I have the same reservations\n> against this as Junio: the break won't be as noticeable when --graph is\n> *not* used with --oneline.\n>\n> > >>> without the patch:\n> > >>>\n> > >>>     * A root\n> > >>>     * B root\n> > >>>     * C root\n> > >>>     * D1 child\n> > >>>     * D root\n> > >>>\n> > >>> with the patch, the indentation cascades:\n> > >>>\n> > >>>     * A root\n> > >>>       * B root\n> > >>>         * C root\n> > >>>           * D1 child\n> > >>>        _ /\n> > >>>       /\n> > >>>      /\n> > >>>     * D root\n> > >\n> > >    * A root\n> > >\n> > >    * B root\n> > >\n> > >    * C root\n> > >\n> > >    * D1 child\n> > >\n> > >    * D root\n> > >\n> > > Here I think a blank line looks worse, too much space for just 5\n> > > commits and becomes one extra line which if this were like up to 7 or\n> > > more parentless commits one after the other would be more noticeable.\n> >\n> > But there shouldn't be a blank line between D and D1 so the two\n> > alternatives take up the same amount of vertical space, the main\n> > difference being whether D1 appears next to D\n> >\n> >      * A root     * A root\n> >                     * B root\n> >      * B root         * C root\n> >                         * D1 child\n> >      * C root         _/\n> >                     /\n> >      * D1 child    /\n> >      * D root     * D root\n> >\n> > Of course if the indentation was smarter it would take up less room and\n> > look better than having blank lines\n> >\n> >      * A root\n> >        * B root\n> >          * C root\n> >      * D1 child\n> >      * D root\n>\n> Right, this would be ideal but that would require too much change to the\n> existing graphing logic, and should be its own patch.\n\nFor the examples I'll use the term parentless instead of root, as\nboundary commits are excluded even if they are roots.\nBy having is_parentless as a flag in 'git_graph' that every stage can\naccess we could modify the rendering and maybe completely drop the\ncommit placeholders, working on it for v4 but currently renders like\nthis\n\n    * A parentless\n      * B parentless\n        * C parentless\n  * D1 child\n  * D parentless\n\n(A has indentation when it could not have, but that would require a\nlookahead if the next commit is also parentless)\nBut definitely a step forward.\n\nDo we want cascading or just a fixed indentation?\n\n    * A parentless\n    * B parentless\n    * C parentless\n  * D1 child\n  * D parentless\n\nBy being indented it indicates that it is parentless and that the one\nbelow doesn't relate to it, but cascading looks clearer.\n\n>\n> > > But there are cases that blank line might be better:\n> > >\n> > >    * 10_A2\n> > >    * 10_A1\n> > >    * 10_A\n> > >      *   10_M\n> > >     /|\\\n> > >    | | * 10_D\n> > >    | * 10_C\n> > >    * 10_B\n> > >\n> > > Feels like a shower of commits instead of an indented merge.\n> >\n> > Yes, that is a bit confusing. I think the thing I find confusing with\n> > this approach is that we're treating the commit rendered below the root\n> > commit specially, rather than treating the root commit itself specially.\n> > To me it is the root commit that's the odd one out because it does not\n> > have any parents, but we treat the commit that's rendered below as the\n> > odd one by indenting it relative to its parents.\n>\n> I guess that would make the examples look something like this:\n>\n>   * A root\n>   * B root\n>   * C root\n> * D1 child\n> * D root\n>\n> No cascading, and no need for that massive _ / collapse line.\n>\n> * 10_A2\n> * 10_A1\n>  \\\n>   * 10_A\n> *   10_M\n> | \\ \\\n> | | * 10_D\n> | * 10_C\n> * 10_B\n>\n> I say it looks better than the alternatives, but I'm not sure if this will\n> be easy to implement. The diagonal connection line (\\) will need to\n> be printed before printing the actual root commit, which will require\n> lookahead logic.\n>\n> I'd prefer to avoid major surgery on the codebase.\n\nOctopus merges need a pre-commit phase where an additional row\nincreases the space around a commit with multiple parents to make room\nfor it.\nA new phase can be created similarly to pre-commit as pre-root where\nthe connection edge (\\) can be printed before the indented commit.\n\nSo far this is the comparison:\n\nindentation at root:\n\n    * A parentless\n  * B1 child\n   \\\n    * B parentless\n  * C1 child\n  * C parentless\n\nindentation AFTER the root (current v3):\n\n  * A parentless\n    * B1 child\n   /\n  * B parentless\n    * C1 child\n   /\n  * C parentless\n\nKarthik mentioned that by indenting the parentless, we lose the\nconsistency of having the roots on their real column and now some are\nindented and some are not.\nThe biggest winner having the parentless indented are the merge commits:\n\n  * A child\n  * A child\n    \\\n      * A parentless\n  *-.   B child\n  | \\ \\\n  | |  * C parentless\n  | * D parentless\n  * E parentless\n\nwhich IMO looks clearer than the commit shower:\n\n  * A child\n  * A child\n  * A parentless\n    *   B child\n   /|\\\n  | | * C parentless\n  | * D parentless\n  * E parentless\n>\n>\n> Thanks,\n> Chandra.\n\nRegards\n\n--\nPablo\n"},{"id":"543567","messageId":"xmqq8q9gb704.fsf@gitster.g","threadId":"65419","inReplyTo":"CAN5EUNQoKRqt3FGLmzRGpPU1nO5jCAogP8Wm9gBZXuPbMNbQAw@mail.gmail.com","subject":"Re: [GSoC RFC PATCH 0/1] graph: add indentation for commits preceded by a root","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-05-19T00:03:23Z","receivedAt":"2026-05-19T00:03:26Z","isPatch":true,"body":"Pablo Sabater <pabloosabaterr@gmail.com> writes:\n\n> By having is_parentless as a flag in 'git_graph' that every stage can\n> access we could modify the rendering and maybe completely drop the\n> commit placeholders, working on it for v4 but currently renders like\n> this\n>\n>     * A parentless\n>       * B parentless\n>         * C parentless\n>   * D1 child\n>   * D parentless\n>\n> (A has indentation when it could not have, but that would require a\n> lookahead if the next commit is also parentless)\n> But definitely a step forward.\n>\n> Do we want cascading or just a fixed indentation?\n>\n>     * A parentless\n>     * B parentless\n>     * C parentless\n>   * D1 child\n>   * D parentless\n\nI am late to the party, but I cannot get how the latter is viable.\nIf \"A\" had parent \"B\" whose parent was \"C\" that is root, wouldn't we\nsee the same output?  Or are we adding \" parentless\" at the end of\nthe one-liner log message?\n\nThe former, with the understanding that \"two '*' commit marks\nvertically adjacent have parent-child relationship, otherwise we\ndraw line between '*' to connect them if they have parent-child\nrelationship\", does not have such a problem.\n"},{"id":"543589","messageId":"CAN5EUNSFBC0+aoW1ceGjEiKWBRjzuzUEUjg8Xys5O9rDsJdkjg@mail.gmail.com","threadId":"65419","inReplyTo":"xmqq8q9gb704.fsf@gitster.g","subject":"Re: [GSoC RFC PATCH 0/1] graph: add indentation for commits preceded by a root","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-05-19T05:59:43Z","receivedAt":"2026-05-19T06:00:00Z","isPatch":true,"body":"El mar, 19 may 2026 a las 2:03, Junio C Hamano (<gitster@pobox.com>) escribió:\n>\n> Pablo Sabater <pabloosabaterr@gmail.com> writes:\n>\n> > By having is_parentless as a flag in 'git_graph' that every stage can\n> > access we could modify the rendering and maybe completely drop the\n> > commit placeholders, working on it for v4 but currently renders like\n> > this\n> >\n> >     * A parentless\n> >       * B parentless\n> >         * C parentless\n> >   * D1 child\n> >   * D parentless\n> >\n> > (A has indentation when it could not have, but that would require a\n> > lookahead if the next commit is also parentless)\n> > But definitely a step forward.\n> >\n> > Do we want cascading or just a fixed indentation?\n> >\n> >     * A parentless\n> >     * B parentless\n> >     * C parentless\n> >   * D1 child\n> >   * D parentless\n>\n> I am late to the party, but I cannot get how the latter is viable.\n> If \"A\" had parent \"B\" whose parent was \"C\" that is root, wouldn't we\n> see the same output?  Or are we adding \" parentless\" at the end of\n> the one-liner log message?\n\nWe wouldn't see the same output because A and B wouldn't get padded in\nthat case. Vertical adjacency between indented commits doesn't imply\nrelation because indentation means that they are \"parentless\",\nambiguity happens when there's no indentation, you can't know whether\nthey are related or not, but knowing that every indented commit is a\n\"parentless\" eliminates the ambiguity.\n\n* A child\n* B child\n \\\n  * C parentless\n* D1 child\n* D parentless\n\nSome different cases:\n\nA child\n \\\n  B parentless\nC parentless\n\n  A parentless\n  B parentless\nC parentless\n\nC has no indentation because if there's nothing to render below,\nindentation is disabled.\n\n  A parentless\nB child\nC parentless\n\nAnyways, having more than 2 \"parentless\" commits one after the other\nis strange. Cascading is just having a depth counter and printing the\npadding depth times, so I'll keep it as it is more intuitive.\n>\n> The former, with the understanding that \"two '*' commit marks\n> vertically adjacent have parent-child relationship, otherwise we\n> draw line between '*' to connect them if they have parent-child\n> relationship\", does not have such a problem.\n"},{"id":"543636","messageId":"CA+J6zkSj+Bfa70h-wW8JRcWtUbFiYJyrdpdLJZ16fY7u7gwECg@mail.gmail.com","threadId":"65419","inReplyTo":"CAN5EUNQoKRqt3FGLmzRGpPU1nO5jCAogP8Wm9gBZXuPbMNbQAw@mail.gmail.com","subject":"Re: [GSoC RFC PATCH 0/1] graph: add indentation for commits preceded by a root","fromName":"Chandra Pratap","fromEmail":"chandrapratap3519@gmail.com","sentAt":"2026-05-19T10:39:46Z","receivedAt":"2026-05-19T10:40:16Z","isPatch":true,"body":"On Mon, 18 May 2026 at 18:57, Pablo Sabater <pabloosabaterr@gmail.com> wrote:\n>\n> Hi Chandra, Phillip,\n>\n> > > >\n> > > > I have mixed feelings about which approach to choose.\n> > > > The idea of a blank line was thought at\n> > > > https://lore.kernel.org/git/xmqq8s8vvw9m.fsf@gitster.c.googlers.com/\n> > > > but Junio argued against it for having an extra row because the\n> > > > indentation he proposed didn't collapse, however I find indentation +\n> > > > no collapse the most confusing one.\n> > > > I'd say that I'm fine with both approaches, blank line or indentation\n> > > > + collapse.\n> > >\n> > > I'm afraid I don't understand this - what does it mean for the\n> > > indentation to collapse, or not collapse.\n>\n> Collapsing would be when branches move to the left, eg:\n>\n>   *\n>   |\\     <- merge\n>   | *\n>   |/     <- collapse\n>   *\n> > > Looking at the examples Junio\n> > > gave they look quite nice to me, though I'd find it clearer if\n> > >\n> > >\n> > >   | | *  12345678 2021-01-14 merge xxxxx@xxxx into the history\n> > >   | | |\\\n> > >   | | | \\\n> > >   | | *  \\  23456789 2021-01-12 merge citest into the main history\n> > >   | | |\\  * 5505e019c2 2014-07-09 initial xxxxxx@xxxx\n> > >   | | | *  3e658f4085 2019-09-10 (wiki/wip-citest, origin/wip-citest)\n> > > Added defau\n> > >   | | | *  ad148aafe6 2019-09-10 Added default CI/CD Jenkinsfile (from\n> > > f7daf088)\n> > >\n> > > was rendered as\n> > >\n> > >\n> > >   | | *  12345678 2021-01-14 merge xxxxx@xxxx into the history\n> > >   | | |\\\n> > >   | | | *  5505e019c2 2014-07-09 initial xxxxxx@xxxx\n> > >   | | *    23456789 2021-01-12 merge citest into the main history\n> > >   | | |\\\n> > >   | | | *  3e658f4085 2019-09-10 (wiki/wip-citest, origin/wip-citest)\n> > > Added defau\n> > >   | | | *  ad148aafe6 2019-09-10 Added default CI/CD Jenkinsfile (from\n> > > f7daf088)\n> >\n> > It probably *does* look clearer here, but I have the same reservations\n> > against this as Junio: the break won't be as noticeable when --graph is\n> > *not* used with --oneline.\n> >\n> > > >>> without the patch:\n> > > >>>\n> > > >>>     * A root\n> > > >>>     * B root\n> > > >>>     * C root\n> > > >>>     * D1 child\n> > > >>>     * D root\n> > > >>>\n> > > >>> with the patch, the indentation cascades:\n> > > >>>\n> > > >>>     * A root\n> > > >>>       * B root\n> > > >>>         * C root\n> > > >>>           * D1 child\n> > > >>>        _ /\n> > > >>>       /\n> > > >>>      /\n> > > >>>     * D root\n> > > >\n> > > >    * A root\n> > > >\n> > > >    * B root\n> > > >\n> > > >    * C root\n> > > >\n> > > >    * D1 child\n> > > >\n> > > >    * D root\n> > > >\n> > > > Here I think a blank line looks worse, too much space for just 5\n> > > > commits and becomes one extra line which if this were like up to 7 or\n> > > > more parentless commits one after the other would be more noticeable.\n> > >\n> > > But there shouldn't be a blank line between D and D1 so the two\n> > > alternatives take up the same amount of vertical space, the main\n> > > difference being whether D1 appears next to D\n> > >\n> > >      * A root     * A root\n> > >                     * B root\n> > >      * B root         * C root\n> > >                         * D1 child\n> > >      * C root         _/\n> > >                     /\n> > >      * D1 child    /\n> > >      * D root     * D root\n> > >\n> > > Of course if the indentation was smarter it would take up less room and\n> > > look better than having blank lines\n> > >\n> > >      * A root\n> > >        * B root\n> > >          * C root\n> > >      * D1 child\n> > >      * D root\n> >\n> > Right, this would be ideal but that would require too much change to the\n> > existing graphing logic, and should be its own patch.\n>\n> For the examples I'll use the term parentless instead of root, as\n> boundary commits are excluded even if they are roots.\n> By having is_parentless as a flag in 'git_graph' that every stage can\n> access we could modify the rendering and maybe completely drop the\n> commit placeholders, working on it for v4 but currently renders like\n> this\n>\n>     * A parentless\n>       * B parentless\n>         * C parentless\n>   * D1 child\n>   * D parentless\n>\n> (A has indentation when it could not have, but that would require a\n> lookahead if the next commit is also parentless)\n> But definitely a step forward.\n>\n> Do we want cascading or just a fixed indentation?\n>\n>     * A parentless\n>     * B parentless\n>     * C parentless\n>   * D1 child\n>   * D parentless\n>\n> By being indented it indicates that it is parentless and that the one\n> below doesn't relate to it, but cascading looks clearer.\n\nAgreed, let's keep the cascading.\n\n> >\n> > > > But there are cases that blank line might be better:\n> > > >\n> > > >    * 10_A2\n> > > >    * 10_A1\n> > > >    * 10_A\n> > > >      *   10_M\n> > > >     /|\\\n> > > >    | | * 10_D\n> > > >    | * 10_C\n> > > >    * 10_B\n> > > >\n> > > > Feels like a shower of commits instead of an indented merge.\n> > >\n> > > Yes, that is a bit confusing. I think the thing I find confusing with\n> > > this approach is that we're treating the commit rendered below the root\n> > > commit specially, rather than treating the root commit itself specially.\n> > > To me it is the root commit that's the odd one out because it does not\n> > > have any parents, but we treat the commit that's rendered below as the\n> > > odd one by indenting it relative to its parents.\n> >\n> > I guess that would make the examples look something like this:\n> >\n> >   * A root\n> >   * B root\n> >   * C root\n> > * D1 child\n> > * D root\n> >\n> > No cascading, and no need for that massive _ / collapse line.\n> >\n> > * 10_A2\n> > * 10_A1\n> >  \\\n> >   * 10_A\n> > *   10_M\n> > | \\ \\\n> > | | * 10_D\n> > | * 10_C\n> > * 10_B\n> >\n> > I say it looks better than the alternatives, but I'm not sure if this will\n> > be easy to implement. The diagonal connection line (\\) will need to\n> > be printed before printing the actual root commit, which will require\n> > lookahead logic.\n> >\n> > I'd prefer to avoid major surgery on the codebase.\n>\n> Octopus merges need a pre-commit phase where an additional row\n> increases the space around a commit with multiple parents to make room\n> for it.\n> A new phase can be created similarly to pre-commit as pre-root where\n> the connection edge (\\) can be printed before the indented commit.\n>\n> So far this is the comparison:\n>\n> indentation at root:\n>\n>     * A parentless\n>   * B1 child\n>    \\\n>     * B parentless\n>   * C1 child\n>   * C parentless\n>\n> indentation AFTER the root (current v3):\n>\n>   * A parentless\n>     * B1 child\n>    /\n>   * B parentless\n>     * C1 child\n>    /\n>   * C parentless\n>\n> Karthik mentioned that by indenting the parentless, we lose the\n> consistency of having the roots on their real column and now some are\n> indented and some are not.\n> The biggest winner having the parentless indented are the merge commits:\n>\n>   * A child\n>   * A child\n>     \\\n>       * A parentless\n>   *-.   B child\n>   | \\ \\\n>   | |  * C parentless\n>   | * D parentless\n>   * E parentless\n>\n> which IMO looks clearer than the commit shower:\n>\n>   * A child\n>   * A child\n>   * A parentless\n>     *   B child\n>    /|\\\n>   | | * C parentless\n>   | * D parentless\n>   * E parentless\n\nI guess we're deciding between cleaner root (parentless) commits and\ncleaner merge commits. I favour merge commits because they appear\nmuch more frequently in most codebases.\n\n> >\n> >\n> > Thanks,\n> > Chandra.\n>\n> Regards\n>\n> --\n> Pablo\n"},{"id":"545164","messageId":"xmqqcxxyxvyo.fsf@gitster.g","threadId":"65419","inReplyTo":"CAN5EUNSFBC0+aoW1ceGjEiKWBRjzuzUEUjg8Xys5O9rDsJdkjg@mail.gmail.com","subject":"Re: [GSoC RFC PATCH 0/1] graph: add indentation for commits preceded by a root","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-10T15:21:19Z","receivedAt":"2026-06-10T15:21:22Z","isPatch":true,"body":"Pablo Sabater <pabloosabaterr@gmail.com> writes:\n\n>> > Do we want cascading or just a fixed indentation?\n>> >\n>> >     * A parentless\n>> >     * B parentless\n>> >     * C parentless\n>> >   * D1 child\n>> >   * D parentless\n>>\n>> I am late to the party, but I cannot get how the latter is viable.\n>> If \"A\" had parent \"B\" whose parent was \"C\" that is root, wouldn't we\n>> see the same output?  Or are we adding \" parentless\" at the end of\n>> the one-liner log message?\n>\n> We wouldn't see the same output because A and B wouldn't get padded in\n> that case. Vertical adjacency between indented commits doesn't imply\n> relation because indentation means that they are \"parentless\",\n\nHmph, I guess such \"the first column is special in that two commits\non consecutive lines with the asterisk on the same column, if only\nthat is on the first column, are parent-child, but it does not hold\nin all other columns\" was beyond my imagination. And that was why I\nsaid I am late to the party.  Do others find such a rule intuitive?\nI didn't (and that is what led me to ask the question).\n\n> Anyways, having more than 2 \"parentless\" commits one after the other\n> is strange. Cascading is just having a depth counter and printing the\n> padding depth times, so I'll keep it as it is more intuitive.\n\nIs everbody happy with this version, or will we see an updated final\nreroll to tie any loose ends?  For example, do we need the above\n\"vertically adjacent commits are in parent-child relationship only\nwhen they appear on the first column\" given as a new instruction in\nthe documentation to help users read and understand what the graph\noutput is trying to tell them?\n\nThanks.\n"},{"id":"545165","messageId":"CAN5EUNS4o_SN61UHrGM-4eXDNpkHzYkVKtJbw_CBJGmfbA-Hgw@mail.gmail.com","threadId":"65419","inReplyTo":"xmqqcxxyxvyo.fsf@gitster.g","subject":"Re: [GSoC RFC PATCH 0/1] graph: add indentation for commits preceded by a root","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-06-10T15:28:12Z","receivedAt":"2026-06-10T15:28:24Z","isPatch":true,"body":"El mié, 10 jun 2026 a las 17:21, Junio C Hamano (<gitster@pobox.com>) escribió:\n>\n> Pablo Sabater <pabloosabaterr@gmail.com> writes:\n>\n> >> > Do we want cascading or just a fixed indentation?\n> >> >\n> >> >     * A parentless\n> >> >     * B parentless\n> >> >     * C parentless\n> >> >   * D1 child\n> >> >   * D parentless\n> >>\n> >> I am late to the party, but I cannot get how the latter is viable.\n> >> If \"A\" had parent \"B\" whose parent was \"C\" that is root, wouldn't we\n> >> see the same output?  Or are we adding \" parentless\" at the end of\n> >> the one-liner log message?\n> >\n> > We wouldn't see the same output because A and B wouldn't get padded in\n> > that case. Vertical adjacency between indented commits doesn't imply\n> > relation because indentation means that they are \"parentless\",\n>\n> Hmph, I guess such \"the first column is special in that two commits\n> on consecutive lines with the asterisk on the same column, if only\n> that is on the first column, are parent-child, but it does not hold\n> in all other columns\" was beyond my imagination. And that was why I\n> said I am late to the party.  Do others find such a rule intuitive?\n> I didn't (and that is what led me to ask the question).\n>\n> > Anyways, having more than 2 \"parentless\" commits one after the other\n> > is strange. Cascading is just having a depth counter and printing the\n> > padding depth times, so I'll keep it as it is more intuitive.\n>\n> Is everbody happy with this version, or will we see an updated final\n> reroll to tie any loose ends?  For example, do we need the above\n> \"vertically adjacent commits are in parent-child relationship only\n> when they appear on the first column\" given as a new instruction in\n> the documentation to help users read and understand what the graph\n> output is trying to tell them?\n>\n> Thanks.\n\nHi!\nNo, it is not ready yet, sorry, I have to send the next version but I\ncannot get some tests to work, I should have it by this week.\n\nThanks,\nPablo.\n"},{"id":"545377","messageId":"20260612-ps-pre-commit-indent-v4-0-e8492037ebae@gmail.com","threadId":"65419","inReplyTo":"20260427102838.44867-1-pabloosabaterr@gmail.com","subject":"[PATCH v4 0/2] graph: indent visual roots in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-06-12T13:48:29Z","receivedAt":"2026-06-12T13:48:37Z","isPatch":true,"body":"When rendering a graph, if the history contains multiple \"visual roots\",\nactual roots or commits that look like roots (i.e. have their parents\nfiltered out) can end up being vertically adjacent to unrelated commits,\nfalsely appearing to be related.\n\nA fix for this issue was already attempted [1] a while ago.\n\nThis series adds indentation to the visual root commits, so they cannot be\nvertically adjacent anymore making it easier to identify them.\n\nbefore indentation:\n\n\t* A\n\t* B1\n\t* B2\n\t* C1\n\t* C2\n\nafter indentation:\n\n\t  * A\n\t* B1\n\t \\\n\t  * B2\n\t* C1\n\t* C2\n\nIndents the visual root commits that have still commits to show after them, and\nif they have children it connects them with an edge at a new row.\n\nIf there are multiple visual roots adjacent in history, the indentation starts\nwith the second one, avoiding redundant indentation of the first one and cascades\nafter the second.\n\n\t* A\n\t  * B\n\t    * C\n\t* D1\n\t* D2\n\nThis series first commit is a cleanup that brings a common function from t4215\nand t6016 to a graph functions file which they both use, so the new test file\nfor indentation, t4218, can use it as well.\n\nThe lookahead used to set the cascading and avoid extra indentation is not\ncompletely reliable, as the walker goes through the commits it simplifies the\nhistory of the current commit and its parents, but it doesn't simplify it\nfor the next unrelated or the grandparents. When the walker simplifies the\nhistory, it removes filtered commits from the history and sets its flags.\nWhen the next commit is an unrelated commit and its parents will be filtered\nout, for the lookahead the commit is still a child of, it cannot know that the\nnext commit once simplified (advancing the walker) it will become a visual root.\nThis makes the lookahead fail, failing to set the cascading and starting it\nwith the first visual root, carrying an extra indent for the cascade.\n\ngiven:\n\n\t* A unrelated (visual root)\n\t* B child of C\n\t* C visual root WILL BE FILTERED OUT\n\t* D unrelated (visual root)\n\nthe actual output is:\n\n\t  * A\n\t    * B\n\t* D\n\nA test has been added to t4218 and a NEEDSWORK to the lookahead function to\ndocument this edge case but I'm not that familiar with revision.c. Maybe there's\na better way to make the lookahead more reliable.\n\n[1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n\nV3 DIFF:\n\n - Completly changes the approach to indent the visual roots instead of the\n   commits after the visual roots.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\nPablo Sabater (2):\n      lib-log-graph: move check_graph function\n      graph: indent visual root in graph\n\n graph.c                                    | 262 +++++++++++++++++\n t/lib-log-graph.sh                         |   5 +\n t/meson.build                              |   1 +\n t/t4215-log-skewed-merges.sh               |  33 +--\n t/t4218-log-graph-indentation.sh           | 455 +++++++++++++++++++++++++++++\n t/t6016-rev-list-graph-simplify-history.sh |  25 +-\n 6 files changed, 747 insertions(+), 34 deletions(-)\n---\nbase-commit: 3e65291872de10c3f0bf05ea8c24187e7a71ebf0\nchange-id: 20260612-ps-pre-commit-indent-39ca72816382\n\nBest regards,\n--  \nPablo Sabater <pabloosabaterr@gmail.com>\n"},{"id":"545378","messageId":"20260612-ps-pre-commit-indent-v4-1-e8492037ebae@gmail.com","threadId":"65419","inReplyTo":"20260612-ps-pre-commit-indent-v4-0-e8492037ebae@gmail.com","subject":"[PATCH v4 1/2] lib-log-graph: move check_graph function","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-06-12T13:48:30Z","receivedAt":"2026-06-12T13:48:38Z","isPatch":true,"body":"check_graph is a function shared in the test files t4215 and t6016 used\nto format the output graph, but instead of being in a file called by\nboth test, the function code is repeated in each file.\n\nMove check_graph to lib-log-graph.sh file which both tests already\nimport graph functions from, renaming it to lib_test_check_graph.\n\nThis function is needed for the following commit which includes graph\ntests in a new file and requires check_graph.\n\nMentored-by: Karthik Nayak <karthik.188@gmail.com>\nMentored-by: Chandra Pratap <chandrapratap3519@gmail.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n t/lib-log-graph.sh                         |  5 +++++\n t/t4215-log-skewed-merges.sh               | 33 +++++++++++++-----------------\n t/t6016-rev-list-graph-simplify-history.sh | 25 +++++++++-------------\n 3 files changed, 29 insertions(+), 34 deletions(-)\n\ndiff --git a/t/lib-log-graph.sh b/t/lib-log-graph.sh\nindex bf952ef920..1eae8f60c2 100644\n--- a/t/lib-log-graph.sh\n+++ b/t/lib-log-graph.sh\n@@ -26,3 +26,8 @@ lib_test_cmp_colored_graph () {\n \ttest_decode_color <output.colors.raw | sed \"s/ *\\$//\" >output.colors &&\n \ttest_cmp expect.colors output.colors\n }\n+\n+lib_test_check_graph () {\n+\tcat >expect &&\n+\tlib_test_cmp_graph --format=%s \"$@\"\n+}\ndiff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh\nindex 1612f05f1b..eebab71039 100755\n--- a/t/t4215-log-skewed-merges.sh\n+++ b/t/t4215-log-skewed-merges.sh\n@@ -5,11 +5,6 @@ test_description='git log --graph of skewed merges'\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-log-graph.sh\n \n-check_graph () {\n-\tcat >expect &&\n-\tlib_test_cmp_graph --format=%s \"$@\"\n-}\n-\n test_expect_success 'log --graph with merge fusing with its left and right neighbors' '\n \tgit checkout --orphan _p &&\n \ttest_commit A &&\n@@ -21,7 +16,7 @@ test_expect_success 'log --graph with merge fusing with its left and right neigh\n \tgit checkout _p && git merge --no-ff _r -m G &&\n \tgit checkout @^^ && git merge --no-ff _p -m H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   H\n \t|\\\n \t| *   G\n@@ -49,7 +44,7 @@ test_expect_success 'log --graph with left-skewed merge' '\n \tgit checkout 0_p && git merge --no-ff 0_s -m 0_G &&\n \tgit checkout @^ && git merge --no-ff 0_q 0_r 0_t 0_p -m 0_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*-----.   0_H\n \t|\\ \\ \\ \\\n \t| | | | * 0_G\n@@ -83,7 +78,7 @@ test_expect_success 'log --graph with nested left-skewed merge' '\n \tgit checkout 1_p && git merge --no-ff 1_r -m 1_G &&\n \tgit checkout @^^ && git merge --no-ff 1_p -m 1_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   1_H\n \t|\\\n \t| *   1_G\n@@ -115,7 +110,7 @@ test_expect_success 'log --graph with nested left-skewed merge following normal\n \tgit checkout -b 2_s @^^ && git merge --no-ff 2_q -m 2_J &&\n \tgit checkout 2_p && git merge --no-ff 2_s -m 2_K &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   2_K\n \t|\\\n \t| *   2_J\n@@ -151,7 +146,7 @@ test_expect_success 'log --graph with nested right-skewed merge following left-s\n \tgit checkout 3_p && git merge --no-ff 3_r -m 3_H &&\n \tgit checkout @^^ && git merge --no-ff 3_p -m 3_J &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   3_J\n \t|\\\n \t| *   3_H\n@@ -182,7 +177,7 @@ test_expect_success 'log --graph with right-skewed merge following a left-skewed\n \tgit merge --no-ff 4_p -m 4_G &&\n \tgit checkout @^^ && git merge --no-ff 4_s -m 4_H &&\n \n-\tcheck_graph --date-order <<-\\EOF\n+\tlib_test_check_graph --date-order <<-\\EOF\n \t*   4_H\n \t|\\\n \t| *   4_G\n@@ -218,7 +213,7 @@ test_expect_success 'log --graph with octopus merge with column joining its penu\n \tgit checkout 5_r &&\n \tgit merge --no-ff 5_s -m 5_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   5_H\n \t|\\\n \t| *-.   5_G\n@@ -257,7 +252,7 @@ test_expect_success 'log --graph with multiple tips' '\n \tgit checkout 6_1 &&\n \tgit merge --no-ff 6_2 -m 6_I &&\n \n-\tcheck_graph 6_1 6_3 6_5 <<-\\EOF\n+\tlib_test_check_graph 6_1 6_3 6_5 <<-\\EOF\n \t*   6_I\n \t|\\\n \t| | *   6_H\n@@ -334,7 +329,7 @@ test_expect_success 'log --graph with multiple tips' '\n \tgit checkout -b M_7 7_1 &&\n \tgit merge --no-ff 7_2 7_3 -m 7_M4 &&\n \n-\tcheck_graph M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -371,7 +366,7 @@ test_expect_success 'log --graph with multiple tips' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' '\n-\tcheck_graph --graph-lane-limit=2 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=2 M_7 <<-\\EOF\n \t*-.   7_M4\n \t|\\ \\\n \t| | * 7_G\n@@ -388,7 +383,7 @@ test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=1 truncate mid octopus merge' '\n-\tcheck_graph --graph-lane-limit=1 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=1 M_7 <<-\\EOF\n \t*-~  7_M4\n \t|\\~\n \t| ~ 7_G\n@@ -405,7 +400,7 @@ test_expect_success 'log --graph --graph-lane-limit=1 truncate mid octopus merge\n '\n \n test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' '\n-\tcheck_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -441,7 +436,7 @@ test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=6 check if it only shows first of 3 parent merge' '\n-\tcheck_graph --graph-lane-limit=6 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=6 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -478,7 +473,7 @@ test_expect_success 'log --graph --graph-lane-limit=6 check if it only shows fir\n '\n \n test_expect_success 'log --graph --graph-lane-limit=7 check if it shows all 3 parent merge' '\n-\tcheck_graph --graph-lane-limit=7 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=7 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\ndiff --git a/t/t6016-rev-list-graph-simplify-history.sh b/t/t6016-rev-list-graph-simplify-history.sh\nindex 54b0a6f5f8..e0d9c3c1ac 100755\n--- a/t/t6016-rev-list-graph-simplify-history.sh\n+++ b/t/t6016-rev-list-graph-simplify-history.sh\n@@ -13,11 +13,6 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-log-graph.sh\n \n-check_graph () {\n-\tcat >expect &&\n-\tlib_test_cmp_graph --format=%s \"$@\"\n-}\n-\n test_expect_success 'set up rev-list --graph test' '\n \t# 3 commits on branch A\n \ttest_commit A1 foo.txt &&\n@@ -54,7 +49,7 @@ test_expect_success 'set up rev-list --graph test' '\n '\n \n test_expect_success '--graph --all' '\n-\tcheck_graph --all <<-\\EOF\n+\tlib_test_check_graph --all <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -82,7 +77,7 @@ test_expect_success '--graph --all' '\n # that undecorated merges are interesting, even with --simplify-by-decoration\n test_expect_success '--graph --simplify-by-decoration' '\n \tgit tag -d A4 &&\n-\tcheck_graph --all --simplify-by-decoration <<-\\EOF\n+\tlib_test_check_graph --all --simplify-by-decoration <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -114,7 +109,7 @@ test_expect_success 'setup: get rid of decorations on B' '\n \n # Graph with branch B simplified away\n test_expect_success '--graph --simplify-by-decoration prune branch B' '\n-\tcheck_graph --simplify-by-decoration --all <<-\\EOF\n+\tlib_test_check_graph --simplify-by-decoration --all <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -133,7 +128,7 @@ test_expect_success '--graph --simplify-by-decoration prune branch B' '\n '\n \n test_expect_success '--graph --full-history -- bar.txt' '\n-\tcheck_graph --full-history --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --full-history --all -- bar.txt <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -148,7 +143,7 @@ test_expect_success '--graph --full-history -- bar.txt' '\n '\n \n test_expect_success '--graph --full-history --simplify-merges -- bar.txt' '\n-\tcheck_graph --full-history --simplify-merges --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --full-history --simplify-merges --all -- bar.txt <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -161,7 +156,7 @@ test_expect_success '--graph --full-history --simplify-merges -- bar.txt' '\n '\n \n test_expect_success '--graph -- bar.txt' '\n-\tcheck_graph --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --all -- bar.txt <<-\\EOF\n \t* A7\n \t* A5\n \t* A3\n@@ -172,7 +167,7 @@ test_expect_success '--graph -- bar.txt' '\n '\n \n test_expect_success '--graph --sparse -- bar.txt' '\n-\tcheck_graph --sparse --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --sparse --all -- bar.txt <<-\\EOF\n \t* A7\n \t* A6\n \t* A5\n@@ -189,7 +184,7 @@ test_expect_success '--graph --sparse -- bar.txt' '\n '\n \n test_expect_success '--graph ^C4' '\n-\tcheck_graph --all ^C4 <<-\\EOF\n+\tlib_test_check_graph --all ^C4 <<-\\EOF\n \t* A7\n \t* A6\n \t* A5\n@@ -202,7 +197,7 @@ test_expect_success '--graph ^C4' '\n '\n \n test_expect_success '--graph ^C3' '\n-\tcheck_graph --all ^C3 <<-\\EOF\n+\tlib_test_check_graph --all ^C3 <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -220,7 +215,7 @@ test_expect_success '--graph ^C3' '\n # that important, but this test depends on it.  If the ordering ever changes\n # in the code, we'll need to update this test.\n test_expect_success '--graph --boundary ^C3' '\n-\tcheck_graph --boundary --all ^C3 <<-\\EOF\n+\tlib_test_check_graph --boundary --all ^C3 <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n\n-- \n2.54.0\n"},{"id":"545379","messageId":"20260612-ps-pre-commit-indent-v4-2-e8492037ebae@gmail.com","threadId":"65419","inReplyTo":"20260612-ps-pre-commit-indent-v4-0-e8492037ebae@gmail.com","subject":"[PATCH v4 2/2] graph: indent visual root in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-06-12T13:48:31Z","receivedAt":"2026-06-12T13:48:40Z","isPatch":true,"body":"When rendering a graph, if the history contains multiple \"visual roots\",\nactual roots or commits that look like roots (i.e. have their parents\nfiltered out) can end up being vertically adjacent to unrelated commits,\nfalsely appearing to be related.\n\nA fix for this issue was already attempted [1] a while ago.\n\nThis happens because the commits fill the space from left to right and\nwhen a visual root ends, its column becomes free for the following\ncommit even if they are not related. Once this happens the unrelated\ncommit is rendered below the visual root. Because there is no special\ncharacter or way to identify when a visual root is rendered making the\ngraph confusing.\n\nBy indenting the visual roots when there are still commits to show the\nvertical adjacency can be avoided.\n\nAdd is_visual_root flag to git_graph making it visible in all graph states,\ngive graph_update() a new function, graph_is_visual_root() to know if the\ncurrent commit is a visual root and set is_visual_root.\nThe different handled cases are:\n\n- If a visual root has children: similar to GRAPH_PRE_COMMIT state when\n  octopus merges need space, an edge row needs to be printed to connect\n  the child with the indented visual root. A new state GRAPH_PRE_ROOT is\n  needed to connect the child with the visual root:\n\n    * child of the visual root\n     \\ GRAPH_PRE_ROOT\n      * visual root indented\n\n- If a visual root is child-less we can skip GRAPH_PRE_ROOT state and\n  render the indented commit directly.\n\n      * visual root indented\n    * unrelated commit\n\n- If two or more visual roots are adjacent: by having a lookahead to the\n  next commit that will be rendered, if the next commit is also a visual\n  root and we are on a visual root, meaning two visual root adjacent in\n  the history, the top one can omit the indent, making the one below to\n  indent only once, if there are more adjacent visual commits, the\n  indentation will increase for each adjacent one, cascading.\n\n    * visual root\n      * visual root\n        * visual root\n    * last commit\n\n  Even if the last commit is a root, because there is nothing that will be\n  rendered below we can omit the indentation on purpose.\n\nThe lookahead is not completely reliable, on graphs with filtered parents,\nthe walker when processing the current commit it will simplify its\nparents by removing the ones that won't be shown, (They have the\nTREESAME flag when filtering by path for example), but it doesn't act\nfor the grandparents or the next commit if it is unrelated until we move\nto the next.\n\nFor example given\n\n  A visual root\n  B child\n  C parent of B, visual root FILTERED\n  D last commit\n\nWe would expect\n\n  A\n    B\n  D\n\nWhen processing A, for the walker and the information at the renderer, B\nis still a child of C, as B parent, hasn't been removed yet. This makes\ncascade to not trigger as the lookahead fails to detect if the next\ncommit will be a visual root.\nOnce at B, its parent has been removed and has become a visual root, and\nit just adds its indent to the one left by A. We end up with an extra\nindent:\n\n    A\n      B\n  D\n\nThe output isn't broken as unrelated commits are successfully separated\nby indentation, but an indent level should have been avoided.\n\nCreate a new test file for graph indentations test called\n't4218-log-graph-indentation.sh'.\n\nThe filtered parents edge case is documented as a NEEDSWORK on the\nlookahead function and it has its own 'test_expect_failure' at 't4218'.\n\n[1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n\nMentored-by: Karthik Nayak <karthik.188@gmail.com>\nMentored-by: Chandra Pratap <chandrapratap3519@gmail.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c                          | 262 ++++++++++++++++++++++\n t/meson.build                    |   1 +\n t/t4218-log-graph-indentation.sh | 455 +++++++++++++++++++++++++++++++++++++++\n 3 files changed, 718 insertions(+)\n\ndiff --git a/graph.c b/graph.c\nindex 842282685f..e0d1e2a510 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -60,12 +60,23 @@ struct column {\n \t * index into column_colors.\n \t */\n \tunsigned short color;\n+\t/*\n+\t * Marks if a commit is a non-first parent of a merge. These columns are\n+\t * already visually connected to the merge commit and do not need\n+\t * indentation.\n+\t *\n+\t * The first parent is the one that inherits the column and it can need\n+\t * indentation if turns out to be a visual root and there's still\n+\t * commits to render.\n+\t */\n+\tunsigned is_merge_parent:1;\n };\n \n enum graph_state {\n \tGRAPH_PADDING,\n \tGRAPH_SKIP,\n \tGRAPH_PRE_COMMIT,\n+\tGRAPH_PRE_ROOT,\n \tGRAPH_COMMIT,\n \tGRAPH_POST_MERGE,\n \tGRAPH_COLLAPSING\n@@ -315,6 +326,48 @@ struct git_graph {\n \t * diff_output_prefix_callback().\n \t */\n \tstruct strbuf prefix_buf;\n+\n+\t/*\n+\t * If a commit is a visual root, we need to indent it to prevent\n+\t * unrelated commits from being vertically adjacent to it.\n+\t */\n+\tunsigned is_visual_root:1;\n+\n+\t/*\n+\t * Indentation increases for each visual root adjacent to another visual\n+\t * root, making visual root commits indentation cascade.\n+\t */\n+\tunsigned int visual_root_depth;\n+\n+\t/*\n+\t * When a visual root is adjacent to other visual roots, the first one\n+\t * can avoid indentation and the rest cascades, increasing the indentation\n+\t * for each one.\n+\t */\n+\tunsigned visual_root_cascade:1;\n+\n+\t/*\n+\t * Set when the current commit was already present in graph->columns\n+\t * before being processed.\n+\t */\n+\tunsigned commit_in_columns:1;\n+};\n+\n+struct graph_lookahead_flags {\n+\t/*\n+\t * Set when there will be a commit after the current one that will be\n+\t * rendered.\n+\t */\n+\tunsigned int is_next_visible:1;\n+\t/*\n+\t * Set when the next visible commit is candidate to be a visual root.\n+\t */\n+\tunsigned int is_next_visual_root:1;\n+\t/*\n+\t * Set when the next visible commit will be rendered under the current\n+\t * commit.\n+\t */\n+\tunsigned int next_has_column:1;\n };\n \n static inline int graph_needs_truncation(struct git_graph *graph, int lane)\n@@ -388,6 +441,8 @@ struct git_graph *graph_init(struct rev_info *opt)\n \tgraph->num_columns = 0;\n \tgraph->num_new_columns = 0;\n \tgraph->mapping_size = 0;\n+\tgraph->visual_root_depth = 0;\n+\tgraph->visual_root_cascade = 0;\n \t/*\n \t * Start the column color at the maximum value, since we'll\n \t * always increment it for the first commit we output.\n@@ -561,6 +616,11 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t\t\t\t\t  struct commit *commit,\n \t\t\t\t\t  int idx)\n {\n+\t/*\n+\t * Get the initial merge_layout before it's modified to know if this\n+\t * is a merge.\n+\t */\n+\tint initial_merge_layout = graph->merge_layout;\n \tint i = graph_find_new_column_by_commit(graph, commit);\n \tint mapping_idx;\n \n@@ -572,6 +632,7 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t\ti = graph->num_new_columns++;\n \t\tgraph->new_columns[i].commit = commit;\n \t\tgraph->new_columns[i].color = graph_find_commit_color(graph, commit);\n+\t\tgraph->new_columns[i].is_merge_parent = 0;\n \t}\n \n \tif (graph->num_parents > 1 && idx > -1 && graph->merge_layout == -1) {\n@@ -610,6 +671,12 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t}\n \n \tgraph->mapping[mapping_idx] = i;\n+\n+\t/*\n+\t * Mark non-first parents of a merge.\n+\t */\n+\tif (graph->num_parents > 1 && initial_merge_layout >= 0 && idx > -1)\n+\t\tgraph->new_columns[i].is_merge_parent = 1;\n }\n \n static void graph_update_columns(struct git_graph *graph)\n@@ -701,10 +768,20 @@ static void graph_update_columns(struct git_graph *graph)\n \t\t\tif (graph->num_parents == 0)\n \t\t\t\tgraph->width += 2;\n \t\t} else {\n+\t\t\tint j;\n \t\t\tgraph_insert_into_new_columns(graph, col_commit, -1);\n+\t\t\t/*\n+\t\t\t * This column is not the current commit, but we need to\n+\t\t\t * propagate the flag until the commit is processed.\n+\t\t\t */\n+\t\t\tj = graph_find_new_column_by_commit(graph, col_commit);\n+\t\t\tif (j >= 0 && graph->columns[i].is_merge_parent)\n+\t\t\t\tgraph->new_columns[j].is_merge_parent = 1;\n \t\t}\n \t}\n \n+\tgraph->commit_in_columns = is_commit_in_columns;\n+\n \t/*\n \t * If graph_max_lanes is set, cap the width\n \t */\n@@ -763,9 +840,135 @@ static int graph_needs_pre_commit_line(struct git_graph *graph)\n \t       graph->expansion_row < graph_num_expansion_rows(graph);\n }\n \n+/*\n+ * A commit can be a visual root when:\n+ * - It has no parents.\n+ *\n+ * - It has parents but they are all filtered out and\n+ *   commit->parents arrives NULL.\n+ *\n+ * - It is not a boundary commit. Boundary commits also have no visible\n+ *   parents, but they are not selected as visual roots because they cannot\n+ *.  cause the ambiguity of being vertically adjacent because:\n+ *\n+ *   1. A boundary only appears because an included commit is its child.\n+ *      Children are always above, and the renderer draws an edge down to\n+ *      the boundary from that child. Rather than starting a column like a\n+ *      visual root would do, it inherits its child column.\n+ *\n+ *   2. Included commits cannot appear below a boundary. Boundaries are\n+ *      ancestors of the exclusion point; if an included commit were an\n+ *      ancestor of the boundary it would be excluded and not rendered.\n+ *      Boundaries therefore always sink to the bottom.\n+ */\n+static int graph_is_visual_root_candidate(struct commit *c)\n+{\n+\treturn c->parents == NULL && !(c->object.flags & BOUNDARY);\n+}\n+\n+static int graph_is_visual_root(struct git_graph *graph,\n+\t\t\t\tstruct graph_lookahead_flags *flags)\n+{\n+\t/*\n+\t * This must be only called for the current commit as graph contains\n+\t * the state for the current commit only.\n+\t *\n+\t * To check if a commit is a visual root, call graph_is_visual_root_candidate()\n+\t * but we won't know if it is really a visual root until we get to the\n+\t * next commit state.\n+\t *\n+\t * The current commit is an actual visual root if it is a candidate and\n+\t * the commit is not a non-first parent of a merge.\n+\t *\n+\t *   *\n+\t *   |\\\n+\t *   | *    <- it is a visual root candidate but it shouldn't be indented\n+\t *   *         because it is already connected by an edge.\n+\t *   ^         if commit_in_columns && is_merge_parent means the commit\n+\t *   |         was put by a merge and is connected.\n+\t *   |\n+\t *   `-------- if !is_next_visible means we're on the last commit, avoid\n+\t *             indentation unless the one before is a visual root, then\n+\t *             we need to differentiate from the one above.\n+\t *\n+\t * If next_has_columns means that the next commit has\n+\t * already a column, so it will not be rendered below, the\n+\t * current commit has to act as the last commit and omit\n+\t * indentation.\n+\t */\n+\treturn graph_is_visual_root_candidate(graph->commit) &&\n+\t       !(graph->commit_in_columns &&\n+\t\t graph->columns[graph->commit_index].is_merge_parent) &&\n+\t       flags->is_next_visible &&\n+\t       (!flags->next_has_column || graph->visual_root_depth > 0);\n+}\n+\n+/*\n+ * Iterates the commits queue searching for the next visible commit, once found\n+ * sets visibleness and visual-root flags.\n+ * Knowing if the next commit is also a visual root avoids redundant indentations\n+ *\n+ * NEEDSWORK: The queue is actively being modified by the walker, for each commit\n+ * its parents and itself get simplified and their flags set, but for the next\n+ * unrelated commit or the grandparents they are not simplified yet, which means\n+ * that a commit whose parents are all filtered will not be marked as a visual\n+ * root candidate at the lookahead.\n+ * This causes the lookahead to fail, failing to set the cascade flag to avoid\n+ * redundant indentations.\n+ * See 'test_expect_failure' at t4218-log-graph-indentation.sh.\n+ */\n+static void graph_peek_next_visible(struct git_graph *graph,\n+\t\t\t\t    struct graph_lookahead_flags *flags)\n+{\n+\tstruct commit_list *cl;\n+\n+\tflags->is_next_visible = 0;\n+\tflags->is_next_visual_root = 0;\n+\tflags->next_has_column = 0;\n+\n+\tfor (cl = graph->revs->commits; cl; cl = cl->next) {\n+\t\tif (get_commit_action(graph->revs, cl->item) != commit_show)\n+\t\t\tcontinue;\n+\t\tflags->is_next_visible = 1;\n+\t\tflags->next_has_column = graph_find_new_column_by_commit(graph, cl->item) >= 0;\n+\t\t/*\n+\t\t * We do not need graph->commit_in_columns or is_merge_parent,\n+\t\t * because we only need to know whether the next one might be a\n+\t\t * visual root, affecting the current commit where the cascade\n+\t\t * would have to be set and the first visual root not indented.\n+\t\t *\n+\t\t * It will set next_is_visual_root to true for merge parents that\n+\t\t * graph_is_visual_root() would return false, but if the next is\n+\t\t * a merge parent, the current commit is the child and cannot\n+\t\t * be a visual root and therefore having no effect.\n+\t\t */\n+\t\tif (!graph_is_visual_root_candidate(cl->item))\n+\t\t\treturn;\n+\n+\t\t/*\n+\t\t * The next visible commit is a visual root candidate, but\n+\t\t * only set cascade if it's not the last commit to be rendered.\n+\t\t */\n+\t\tfor (cl = cl->next; cl; cl = cl->next) {\n+\t\t\tif (get_commit_action(graph->revs, cl->item) != commit_show)\n+\t\t\t\tcontinue;\n+\t\t\tflags->is_next_visual_root = 1;\n+\t\t\treturn;\n+\t\t}\n+\t\treturn;\n+\t}\n+}\n+\n+static int graph_needs_pre_root_line(struct git_graph *graph)\n+{\n+\treturn graph->commit_in_columns && graph->is_visual_root &&\n+\t       graph->num_columns > 0 && !graph->visual_root_cascade;\n+}\n+\n void graph_update(struct git_graph *graph, struct commit *commit)\n {\n \tstruct commit_list *parent;\n+\tstruct graph_lookahead_flags flags;\n \n \t/*\n \t * Set the new commit\n@@ -796,6 +999,23 @@ void graph_update(struct git_graph *graph, struct commit *commit)\n \t */\n \tgraph_update_columns(graph);\n \n+\tgraph_peek_next_visible(graph, &flags);\n+\n+\tgraph->is_visual_root = graph_is_visual_root(graph, &flags);\n+\n+\tif (graph->is_visual_root) {\n+\t\t/*\n+\t\t * If next is a visual root we can omit the indent for the first\n+\t\t * visual root and start cascading.\n+\t\t */\n+\t\tif (!graph->visual_root_depth && flags.is_next_visual_root)\n+\t\t\tgraph->visual_root_cascade = 1;\n+\t\tgraph->visual_root_depth++;\n+\t} else {\n+\t\tgraph->visual_root_depth = 0;\n+\t\tgraph->visual_root_cascade = 0;\n+\t}\n+\n \tgraph->expansion_row = 0;\n \n \t/*\n@@ -813,11 +1033,16 @@ void graph_update(struct git_graph *graph, struct commit *commit)\n \t * room for it.  We need to do this only if there is a branch row\n \t * (or more) to the right of this commit.\n \t *\n+\t * If it is a visual root, we need to print an extra row to\n+\t * connect the indentation.\n+\t *\n \t * If there are less than 3 parents, we can immediately print the\n \t * commit line.\n \t */\n \tif (graph->state != GRAPH_PADDING)\n \t\tgraph->state = GRAPH_SKIP;\n+\telse if (graph_needs_pre_root_line(graph))\n+\t\tgraph->state = GRAPH_PRE_ROOT;\n \telse if (graph_needs_pre_commit_line(graph))\n \t\tgraph->state = GRAPH_PRE_COMMIT;\n \telse\n@@ -1065,6 +1290,17 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \n \t\tif (col_commit == graph->commit) {\n \t\t\tseen_this = 1;\n+\t\t\tif (graph->is_visual_root) {\n+\t\t\t\tint depth = graph->visual_root_depth;\n+\t\t\t\t/*\n+\t\t\t\t * Each visual column is 2 characters wide.\n+\t\t\t\t * Omit the indentation for the first visual\n+\t\t\t\t * root in cascade mode.\n+\t\t\t\t */\n+\t\t\t\tint padding = (depth - graph->visual_root_cascade) * 2;\n+\t\t\t\tgraph_line_addchars(line, ' ', padding);\n+\t\t\t\tgraph->width += padding;\n+\t\t\t}\n \t\t\tgraph_output_commit_char(graph, line);\n \n \t\t\tif (graph_needs_truncation(graph, i)) {\n@@ -1436,6 +1672,29 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n \t\tgraph_update_state(graph, GRAPH_PADDING);\n }\n \n+static void graph_output_pre_root_line(struct git_graph *graph, struct graph_line *line)\n+{\n+\t/*\n+\t * This function adds a row before a visual root, to connect the\n+\t * branch to the indented commit. It should only be called on a\n+\t * visual root.\n+\t */\n+\tassert(graph->is_visual_root);\n+\n+\tfor (size_t i = 0; i < graph->num_columns; i++) {\n+\t\tstruct column *col = &graph->columns[i];\n+\t\tif (col->commit == graph->commit) {\n+\t\t\tgraph_line_addch(line, ' ');\n+\t\t\tgraph_line_write_column(line, col, '\\\\');\n+\t\t} else {\n+\t\t\tgraph_line_write_column(line, col, '|');\n+\t\t}\n+\t\tgraph_line_addch(line, ' ');\n+\t}\n+\n+\tgraph_update_state(graph, GRAPH_COMMIT);\n+}\n+\n int graph_next_line(struct git_graph *graph, struct strbuf *sb)\n {\n \tint shown_commit_line = 0;\n@@ -1461,6 +1720,9 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb)\n \tcase GRAPH_PRE_COMMIT:\n \t\tgraph_output_pre_commit_line(graph, &line);\n \t\tbreak;\n+\tcase GRAPH_PRE_ROOT:\n+\t\tgraph_output_pre_root_line(graph, &line);\n+\t\tbreak;\n \tcase GRAPH_COMMIT:\n \t\tgraph_output_commit_line(graph, &line);\n \t\tshown_commit_line = 1;\ndiff --git a/t/meson.build b/t/meson.build\nindex c5832fee05..17037a8465 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -576,6 +576,7 @@ integration_tests = [\n   't4215-log-skewed-merges.sh',\n   't4216-log-bloom.sh',\n   't4217-log-limit.sh',\n+  't4218-log-graph-indentation.sh',\n   't4252-am-options.sh',\n   't4253-am-keep-cr-dos.sh',\n   't4254-am-corrupt.sh',\ndiff --git a/t/t4218-log-graph-indentation.sh b/t/t4218-log-graph-indentation.sh\nnew file mode 100755\nindex 0000000000..f1c9584ba5\n--- /dev/null\n+++ b/t/t4218-log-graph-indentation.sh\n@@ -0,0 +1,455 @@\n+#!/bin/sh\n+\n+test_description='git log --graph visual root indentations'\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-log-graph.sh\n+\n+check_graph_with_description () {\n+\tcat >expect &&\n+\tlib_test_cmp_graph --format=\"%s%ndescription%nsecond-line\" \"$@\"\n+}\n+\n+create_orphan () {\n+\tgit checkout --orphan \"$1\" &&\n+\t{ git rm -rf . || true; }\n+}\n+\n+test_expect_success 'single root commit is not indented' '\n+\tcreate_orphan _1 && test_commit 1_A &&\n+\tlib_test_check_graph _1 <<-\\EOF\n+\t* 1_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indented before unrelated branch' '\n+\tcreate_orphan _2 && test_commit 2_A && test_commit 2_B &&\n+\tcreate_orphan _3 && test_commit 3_A &&\n+\tlib_test_check_graph _2 _3 <<-\\EOF\n+\t  * 3_A\n+\t* 2_B\n+\t* 2_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indentation with --left-right' '\n+\tlib_test_check_graph --left-right _2..._3 <<-\\EOF\n+\t  > 3_A\n+\t< 2_B\n+\t< 2_A\n+\tEOF\n+'\n+\n+# A better case of why indentation is still needed with '--left-right' flag is\n+# that unrelated branches can be on the same side, so it's needed to\n+# differentiate visual roots on the same side.\n+test_expect_success 'visual root indentation with --left-right having unrelated commits on the same side' '\n+\tlib_test_check_graph --left-right _2..._3 _1 <<-\\EOF\n+\t  > 3_A\n+\t< 2_B\n+\t \\\n+\t  < 2_A\n+\t> 1_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indents the description also' '\n+\tcheck_graph_with_description _2 _3 <<-\\EOF\n+\t  * 3_A\n+\t    description\n+\t    second-line\n+\t* 2_B\n+\t| description\n+\t| second-line\n+\t* 2_A\n+\t  description\n+\t  second-line\n+\tEOF\n+'\n+\n+test_expect_success 'indented visual root parent gets connected to its child' '\n+\tcreate_orphan _4 && test_commit 4_A && test_commit 4_B &&\n+\tcreate_orphan _5 && test_commit 5_A && test_commit 5_B &&\n+\tlib_test_check_graph _4 _5<<-\\EOF\n+\t* 5_B\n+\t \\\n+\t  * 5_A\n+\t* 4_B\n+\t* 4_A\n+\tEOF\n+'\n+\n+test_expect_success 'indented visual root parent gets connected to its child with description' '\n+\tcheck_graph_with_description _4 _5 <<-\\EOF\n+\t* 5_B\n+\t| description\n+\t| second-line\n+\t \\\n+\t  * 5_A\n+\t    description\n+\t    second-line\n+\t* 4_B\n+\t| description\n+\t| second-line\n+\t* 4_A\n+\t  description\n+\t  second-line\n+\tEOF\n+'\n+\n+test_expect_success 'visual roots cascade and last root does not' '\n+\tcreate_orphan _7 && test_commit 7_A && test_commit 7_B &&\n+\tcreate_orphan _8 && test_commit 8_A &&\n+\tcreate_orphan _9 && test_commit 9_A &&\n+\tcreate_orphan _10 && test_commit 10_A &&\n+\tlib_test_check_graph _7 _8 _9 _10  <<-\\EOF\n+\t* 10_A\n+\t  * 9_A\n+\t    * 8_A\n+\t* 7_B\n+\t* 7_A\n+\tEOF\n+'\n+\n+test_expect_success 'last root does not cascade' '\n+\tlib_test_check_graph _8 _9 _10 <<-\\EOF\n+\t* 10_A\n+\t  * 9_A\n+\t* 8_A\n+\tEOF\n+'\n+\n+test_expect_success 'merge parents are roots between them but they do not indent' '\n+\tcreate_orphan _11 && test_commit 11_A &&\n+\tcreate_orphan _12 && test_commit 12_A &&\n+\tcreate_orphan _13 && test_commit 13_A &&\n+\tgit checkout _11 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _11 -p _12 -p _13 -m 11_octopus) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _11 <<-\\EOF\n+\t*-.   11_octopus\n+\t|\\ \\\n+\t| | * 13_A\n+\t| * 12_A\n+\t* 11_A\n+\tEOF\n+'\n+\n+# The last parent of a merge can be indented if nothing related to it needs to\n+# be rendered after, if it's another visual root, merge parent must not get\n+# indented but rather activate cascading.\n+test_expect_success 'merge then unrelated visual root and unrelated branch' '\n+\tcreate_orphan _16 && test_commit 16_A && test_commit 16_B &&\n+\tcreate_orphan _17 && test_commit 17_A &&\n+\tcreate_orphan _18 && test_commit 18_A &&\n+\tcreate_orphan _19 && test_commit 19_A &&\n+\tcreate_orphan _20 && test_commit 20_A &&\n+\tgit checkout _18 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _18 -p _19 -p _20 -m 18_octopus) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _18 _17 _16 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t* 18_A\n+\t  * 17_A\n+\t* 16_B\n+\t* 16_A\n+\tEOF\n+'\n+\n+# The last commit root does not get indented, if the next thing after the root\n+# merge parent is the last commit, indent the merge parent.\n+test_expect_success 'merge then unrelated root indents merge parent' '\n+\tlib_test_check_graph _18 _17 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t \\\n+\t  * 18_A\n+\t* 17_A\n+\tEOF\n+'\n+\n+test_expect_success 'merge then unrelated branch indents merge parent' '\n+\tlib_test_check_graph _18 _16 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t \\\n+\t  * 18_A\n+\t* 16_B\n+\t* 16_A\n+\tEOF\n+'\n+\n+test_expect_success 'two-parent merge of orphans' '\n+\tcreate_orphan _21 && test_commit 21_A &&\n+\tcreate_orphan _22 && test_commit 22_A &&\n+\tgit checkout _21 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _21 -p _22 -m 21_merge) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _21 <<-\\EOF\n+\t*   21_merge\n+\t|\\\n+\t| * 22_A\n+\t* 21_A\n+\tEOF\n+'\n+\n+test_expect_success 'commit with filtered parent becomes a visual root' '\n+\tcreate_orphan _23 &&\n+\techo test >other.txt &&\n+\tgit add other.txt &&\n+\tgit commit -m \"23_A\" &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"23_B\" &&\n+\tcreate_orphan _24 &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"24_A\" &&\n+\tlib_test_check_graph _23 _24 -- foo.txt <<-\\EOF\n+\t  * 23_B\n+\t* 24_A\n+\tEOF\n+'\n+\n+# The walker simplifies the commit for the current one and its parents, removing\n+# the filtered parents, but it doesn't go one step ahead, this causes some edge\n+# cases with the lookahead.\n+# Given A (orphan), the walker only processes A, and when we lookahead for B\n+# (child of C) even tho C will be filtered, it hasn't been simplified yet, so we\n+# don't see B as a visual root, therefore cascade indentation isn't applied to A.\n+# (cascade indentation starts the indentation at the second visual root, to avoid\n+# redundant indentation). So A gets an extra indent, and once B is processed,\n+# when rendering it, C has been removed, B is a visual root and as the last commit\n+# isn't considered a visual root as it cannot have unrelated commits below it,\n+# cascading isn't also applied, giving B another indent.\n+#\n+# The final result is an extra indent for A and B:\n+#\n+#\t  A\n+#\t    B\n+#\tD\n+#\n+# This will happen for any case where we find ourselves with the next commit\n+# being a unrelated child of a parent the will be filtered.\n+#\n+# instead of the expected:\n+test_expect_failure 'filtered parent cascading edge case' '\n+\tcreate_orphan _25 &&\n+\tgit rm -rf . &&\n+\techo test >other.txt &&\n+\tgit add other.txt &&\n+\tgit commit -m \"C-filtered\" &&\n+\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"B (child of filtered)\" &&\n+\n+\tcreate_orphan _26 &&\n+\tgit rm -rf . &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"A (visual root)\" &&\n+\n+\tcreate_orphan _27 &&\n+\tgit rm -rf . &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"D (last)\" &&\n+\n+\tlib_test_check_graph _25 _26 _27 -- foo.txt <<-\\EOF\n+\t* A (visual root)\n+\t  * B (child of filtered)\n+\t* D (last)\n+\tEOF\n+'\n+\n+test_expect_failure 'multiple filtered parents in sequence' '\n+\tcreate_orphan _44 &&\n+\tgit rm -rf . &&\n+\techo a >other.txt && git add other.txt && git commit -m \"44_F\" &&\n+\techo b >foo.txt && git add foo.txt && git commit -m \"44_C\" &&\n+\n+\tcreate_orphan _45 &&\n+\tgit rm -rf . &&\n+\techo c >other.txt && git add other.txt && git commit -m \"45_F\" &&\n+\techo d >foo.txt && git add foo.txt && git commit -m \"45_C\" &&\n+\n+\tcreate_orphan _46 &&\n+\tgit rm -rf . &&\n+\techo e >foo.txt && git add foo.txt && git commit -m \"46_A\" &&\n+\n+\tlib_test_check_graph _44 _45 _46 -- foo.txt <<-\\EOF\n+\t* 46_A\n+\t  * 45_C\n+\t* 44_C\n+\tEOF\n+'\n+\n+test_expect_failure 'real orphan root followed by child of filtered parent' '\n+\tcreate_orphan _47 &&\n+\tgit rm -rf . &&\n+\techo a >foo.txt && git add foo.txt && git commit -m \"47_A\" &&\n+\n+\tcreate_orphan _48 &&\n+\tgit rm -rf . &&\n+\techo b >other.txt && git add other.txt && git commit -m \"48_filtered\" &&\n+\techo c >foo.txt && git add foo.txt && git commit -m \"48_B\" &&\n+\n+\tcreate_orphan _49 &&\n+\tgit rm -rf . &&\n+\techo d >foo.txt && git add foo.txt && git commit -m \"49_last\" &&\n+\n+\tlib_test_check_graph _47 _48 _49 -- foo.txt <<-\\EOF\n+\t* 47_A\n+\t  * 48_B\n+\t* 49_last\n+\tEOF\n+'\n+\n+# This tests prove why there is no need to have indentation for boundary\n+# commits.\n+#\n+# Boundary commits rather than starting a column they 'inherit' the one of\n+# its child so there will always be an edge that connects it removing the\n+# ambiguity.\n+test_expect_success 'unrelated boundaries are not ambiguous' '\n+\tcreate_orphan _28 && test_commit 28_A && test_commit 28_B &&\n+\ttest_commit 28_C &&\n+\tcreate_orphan _29 && test_commit 29_A && test_commit 29_B &&\n+\tlib_test_check_graph --boundary 28_A.._28 29_A.._29 <<-\\EOF\n+\t* 29_B\n+\t| * 28_C\n+\t| * 28_B\n+\t| o 28_A\n+\to 29_A\n+\tEOF\n+'\n+\n+# Same structure as t6016\n+test_expect_success 'boundary commits big test' '\n+\t# 3 commits on branch _30\n+\tcreate_orphan _30 &&\n+\ttest_commit 30_A &&\n+\ttest_commit 30_B &&\n+\ttest_commit 30_C &&\n+\n+\t# 2 commits on branch _31, started from 30_A\n+\tgit checkout -b _31 30_A &&\n+\ttest_commit 31_A &&\n+\ttest_commit 31_B &&\n+\n+\t# 2 commits on branch _32, started from 30_B\n+\tgit checkout -b _32 30_B &&\n+\ttest_commit 32_A &&\n+\ttest_commit 32_B &&\n+\n+\t# Octopus merge _31 and _32 into -30\n+\tgit checkout _30 &&\n+\tgit merge _31 _32 -m 30_D &&\n+\tgit tag 30_D &&\n+\ttest_commit 30_E &&\n+\n+\t# More commits on _32, then merge _32 into _30\n+\tgit checkout _32 &&\n+\ttest_commit 32_C &&\n+\ttest_commit 32_D &&\n+\tgit checkout _30 &&\n+\tgit merge -s ours _32 -m 30_F &&\n+\tgit tag 30_F &&\n+\ttest_commit 30_G &&\n+\tlib_test_check_graph --boundary _30 _31 _32 ^32_C <<-\\EOF\n+\t* 30_G\n+\t*   30_F\n+\t|\\\n+\t| * 32_D\n+\t* | 30_E\n+\t| |\n+\t|  \\\n+\t*-. \\   30_D\n+\t|\\ \\ \\\n+\t| * | | 31_B\n+\t| * | | 31_A\n+\t* | | | 30_C\n+\to | | | 30_B\n+\t|/ / /\n+\to / / 30_A\n+\t / /\n+\t| o 32_C\n+\t|/\n+\to 32_B\n+\tEOF\n+'\n+\n+# Filter by --first-parent and then forcing the filtered parents to be shown.\n+test_expect_success '--first-parent flag with the filtered parents' '\n+\t(\n+\tcreate_orphan _35 && test_commit 35_A && test_commit 35_B &&\n+\tcreate_orphan _36 && test_commit 36_A &&\n+\tcreate_orphan _37 && test_commit 37_A &&\n+\tgit checkout _35 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _35 -p _36 -p _37 -m 35_octopus) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph --first-parent _35 _36 _37 <<-\\EOF\n+\t* 35_octopus\n+\t| * 37_A\n+\t|   * 36_A\n+\t* 35_B\n+\t* 35_A\n+\tEOF\n+\t)\n+'\n+\n+test_expect_success '--first-parent with filtered parents but one has a child' '\n+\t(\n+\tcreate_orphan _38 && test_commit 38_A && test_commit 38_B &&\n+\tcreate_orphan _39 && test_commit 39_A &&\n+\tcreate_orphan _40 && test_commit 40_A && test_commit 40_B &&\n+\tgit checkout _38 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _38 -p _39 -p _40 -m 38_octopus) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph --first-parent _38 _39 _40 <<-\\EOF\n+\t* 38_octopus\n+\t| * 40_B\n+\t| * 40_A\n+\t|   * 39_A\n+\t* 38_B\n+\t* 38_A\n+\tEOF\n+\t)\n+'\n+\n+test_expect_success '--first-parent with filtered parents but both have childs' '\n+\t(\n+\tcreate_orphan _41 && test_commit 41_A && test_commit 41_B &&\n+\tcreate_orphan _42 && test_commit 42_A && test_commit 42_B &&\n+\tcreate_orphan _43 && test_commit 43_A && test_commit 43_B &&\n+\tgit checkout _41 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _41 -p _42 -p _43 -m 41_octopus) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph --first-parent _41 _42 _43 <<-\\EOF\n+\t* 41_octopus\n+\t| * 43_B\n+\t|  \\\n+\t|   * 43_A\n+\t| * 42_B\n+\t| * 42_A\n+\t* 41_B\n+\t* 41_A\n+\tEOF\n+\t)\n+'\n+\n+test_done\n\n-- \n2.54.0\n"},{"id":"545430","messageId":"xmqqwlw3f8ir.fsf@gitster.g","threadId":"65419","inReplyTo":"20260612-ps-pre-commit-indent-v4-0-e8492037ebae@gmail.com","subject":"Re: [PATCH v4 0/2] graph: indent visual roots in graph","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-13T03:01:48Z","receivedAt":"2026-06-13T03:01:52Z","isPatch":true,"body":"Pablo Sabater <pabloosabaterr@gmail.com> writes:\n\n> When rendering a graph, if the history contains multiple \"visual roots\",\n> actual roots or commits that look like roots (i.e. have their parents\n> filtered out) can end up being vertically adjacent to unrelated commits,\n> falsely appearing to be related.\n> ...\n> [1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n>\n> V3 DIFF:\n>\n>  - Completly changes the approach to indent the visual roots instead of the\n>    commits after the visual roots.\n>\n> Signed-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n> ---\n> Pablo Sabater (2):\n>       lib-log-graph: move check_graph function\n>       graph: indent visual root in graph\n\nThe new tests added here does not seem to play well with\nlinux-TEST-vars CI job when merged to 'seen' or 'jch' with other\ntopics in flight.\n\n  https://github.com/git/git/actions/runs/27445164550/job/81128391150#step:10:1779\n  https://github.com/git/git/actions/runs/27445164037/job/81128386244#step:10:1778\n\nI suspect that these tests are failing the same way even standalone.\n\n  https://github.com/git/git/actions/runs/27447146727/job/81134594264#step:9:2136\n\n\n"},{"id":"545467","messageId":"20260613-ps-pre-commit-indent-v5-0-8d308efea63d@gmail.com","threadId":"65419","inReplyTo":"20260612-ps-pre-commit-indent-v4-0-e8492037ebae@gmail.com","subject":"[PATCH v5 0/2] graph: indent visual roots in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-06-13T19:09:14Z","receivedAt":"2026-06-13T19:09:23Z","isPatch":true,"body":"When rendering a graph, if the history contains multiple \"visual roots\",\nactual roots or commits that look like roots (i.e. have their parents\nfiltered out) can end up being vertically adjacent to unrelated commits,\nfalsely appearing to be related.\n\nA fix for this issue was already attempted [1] a while ago.\n\nThis series adds indentation to the visual root commits, so they cannot be\nvertically adjacent anymore making it easier to identify them.\n\nbefore indentation:\n\n\t* A\n\t* B1\n\t* B2\n\t* C1\n\t* C2\n\nafter indentation:\n\n\t  * A\n\t* B1\n\t \\\n\t  * B2\n\t* C1\n\t* C2\n\nIndents the visual root commits that have still commits to show after them, and\nif they have children it connects them with an edge at a new row.\n\nIf there are multiple visual roots adjacent in history, the indentation starts\nwith the second one, avoiding redundant indentation of the first one and cascades\nafter the second.\n\n\t* A\n\t  * B\n\t    * C\n\t* D1\n\t* D2\n\nThis series first commit is a cleanup that brings a common function from t4215\nand t6016 to a graph functions file which they both use, so the new test file\nfor indentation, t4218, can use it as well.\n\nThe lookahead used to set the cascading and avoid extra indentation is not\ncompletely reliable, as the walker goes through the commits it simplifies the\nhistory of the current commit and its parents, but it doesn't simplify it\nfor the next unrelated or the grandparents. When the walker simplifies the\nhistory, it removes filtered commits from the history and sets its flags.\nWhen the next commit is an unrelated commit and its parents will be filtered\nout, for the lookahead the commit is still a child of, it cannot know that the\nnext commit once simplified (advancing the walker) it will become a visual root.\nThis makes the lookahead fail, failing to set the cascading and starting it\nwith the first visual root, carrying an extra indent for the cascade.\n\ngiven:\n\n\t* A unrelated (visual root)\n\t* B child of C\n\t* C visual root WILL BE FILTERED OUT\n\t* D unrelated (visual root)\n\nthe actual output is:\n\n\t  * A\n\t    * B\n\t* D\n\nA test has been added to t4218 and a NEEDSWORK to the lookahead function to\ndocument this edge case but I'm not that familiar with revision.c. Maybe there's\na better way to make the lookahead more reliable.\n\n[1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n\nV4 DIFF:\n\n- Fixed test to be shown as expected by unsetting COMMIT_GRAPH\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\nPablo Sabater (2):\n      lib-log-graph: move check_graph function\n      graph: indent visual root in graph\n\n graph.c                                    | 262 ++++++++++++++++\n t/lib-log-graph.sh                         |   5 +\n t/meson.build                              |   1 +\n t/t4215-log-skewed-merges.sh               |  33 +-\n t/t4218-log-graph-indentation.sh           | 467 +++++++++++++++++++++++++++++\n t/t6016-rev-list-graph-simplify-history.sh |  25 +-\n 6 files changed, 759 insertions(+), 34 deletions(-)\n---\nbase-commit: 3e65291872de10c3f0bf05ea8c24187e7a71ebf0\nchange-id: 20260612-ps-pre-commit-indent-39ca72816382\n\nBest regards,\n--  \nPablo Sabater <pabloosabaterr@gmail.com>\n"},{"id":"545468","messageId":"20260613-ps-pre-commit-indent-v5-1-8d308efea63d@gmail.com","threadId":"65419","inReplyTo":"20260613-ps-pre-commit-indent-v5-0-8d308efea63d@gmail.com","subject":"[PATCH v5 1/2] lib-log-graph: move check_graph function","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-06-13T19:09:15Z","receivedAt":"2026-06-13T19:09:25Z","isPatch":true,"body":"check_graph is a function shared in the test files t4215 and t6016 used\nto format the output graph, but instead of being in a file called by\nboth test, the function code is repeated in each file.\n\nMove check_graph to lib-log-graph.sh file which both tests already\nimport graph functions from, renaming it to lib_test_check_graph.\n\nThis function is needed for the following commit which includes graph\ntests in a new file and requires check_graph.\n\nMentored-by: Karthik Nayak <karthik.188@gmail.com>\nMentored-by: Chandra Pratap <chandrapratap3519@gmail.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n t/lib-log-graph.sh                         |  5 +++++\n t/t4215-log-skewed-merges.sh               | 33 +++++++++++++-----------------\n t/t6016-rev-list-graph-simplify-history.sh | 25 +++++++++-------------\n 3 files changed, 29 insertions(+), 34 deletions(-)\n\ndiff --git a/t/lib-log-graph.sh b/t/lib-log-graph.sh\nindex bf952ef920..1eae8f60c2 100644\n--- a/t/lib-log-graph.sh\n+++ b/t/lib-log-graph.sh\n@@ -26,3 +26,8 @@ lib_test_cmp_colored_graph () {\n \ttest_decode_color <output.colors.raw | sed \"s/ *\\$//\" >output.colors &&\n \ttest_cmp expect.colors output.colors\n }\n+\n+lib_test_check_graph () {\n+\tcat >expect &&\n+\tlib_test_cmp_graph --format=%s \"$@\"\n+}\ndiff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh\nindex 1612f05f1b..eebab71039 100755\n--- a/t/t4215-log-skewed-merges.sh\n+++ b/t/t4215-log-skewed-merges.sh\n@@ -5,11 +5,6 @@ test_description='git log --graph of skewed merges'\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-log-graph.sh\n \n-check_graph () {\n-\tcat >expect &&\n-\tlib_test_cmp_graph --format=%s \"$@\"\n-}\n-\n test_expect_success 'log --graph with merge fusing with its left and right neighbors' '\n \tgit checkout --orphan _p &&\n \ttest_commit A &&\n@@ -21,7 +16,7 @@ test_expect_success 'log --graph with merge fusing with its left and right neigh\n \tgit checkout _p && git merge --no-ff _r -m G &&\n \tgit checkout @^^ && git merge --no-ff _p -m H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   H\n \t|\\\n \t| *   G\n@@ -49,7 +44,7 @@ test_expect_success 'log --graph with left-skewed merge' '\n \tgit checkout 0_p && git merge --no-ff 0_s -m 0_G &&\n \tgit checkout @^ && git merge --no-ff 0_q 0_r 0_t 0_p -m 0_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*-----.   0_H\n \t|\\ \\ \\ \\\n \t| | | | * 0_G\n@@ -83,7 +78,7 @@ test_expect_success 'log --graph with nested left-skewed merge' '\n \tgit checkout 1_p && git merge --no-ff 1_r -m 1_G &&\n \tgit checkout @^^ && git merge --no-ff 1_p -m 1_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   1_H\n \t|\\\n \t| *   1_G\n@@ -115,7 +110,7 @@ test_expect_success 'log --graph with nested left-skewed merge following normal\n \tgit checkout -b 2_s @^^ && git merge --no-ff 2_q -m 2_J &&\n \tgit checkout 2_p && git merge --no-ff 2_s -m 2_K &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   2_K\n \t|\\\n \t| *   2_J\n@@ -151,7 +146,7 @@ test_expect_success 'log --graph with nested right-skewed merge following left-s\n \tgit checkout 3_p && git merge --no-ff 3_r -m 3_H &&\n \tgit checkout @^^ && git merge --no-ff 3_p -m 3_J &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   3_J\n \t|\\\n \t| *   3_H\n@@ -182,7 +177,7 @@ test_expect_success 'log --graph with right-skewed merge following a left-skewed\n \tgit merge --no-ff 4_p -m 4_G &&\n \tgit checkout @^^ && git merge --no-ff 4_s -m 4_H &&\n \n-\tcheck_graph --date-order <<-\\EOF\n+\tlib_test_check_graph --date-order <<-\\EOF\n \t*   4_H\n \t|\\\n \t| *   4_G\n@@ -218,7 +213,7 @@ test_expect_success 'log --graph with octopus merge with column joining its penu\n \tgit checkout 5_r &&\n \tgit merge --no-ff 5_s -m 5_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   5_H\n \t|\\\n \t| *-.   5_G\n@@ -257,7 +252,7 @@ test_expect_success 'log --graph with multiple tips' '\n \tgit checkout 6_1 &&\n \tgit merge --no-ff 6_2 -m 6_I &&\n \n-\tcheck_graph 6_1 6_3 6_5 <<-\\EOF\n+\tlib_test_check_graph 6_1 6_3 6_5 <<-\\EOF\n \t*   6_I\n \t|\\\n \t| | *   6_H\n@@ -334,7 +329,7 @@ test_expect_success 'log --graph with multiple tips' '\n \tgit checkout -b M_7 7_1 &&\n \tgit merge --no-ff 7_2 7_3 -m 7_M4 &&\n \n-\tcheck_graph M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -371,7 +366,7 @@ test_expect_success 'log --graph with multiple tips' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' '\n-\tcheck_graph --graph-lane-limit=2 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=2 M_7 <<-\\EOF\n \t*-.   7_M4\n \t|\\ \\\n \t| | * 7_G\n@@ -388,7 +383,7 @@ test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=1 truncate mid octopus merge' '\n-\tcheck_graph --graph-lane-limit=1 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=1 M_7 <<-\\EOF\n \t*-~  7_M4\n \t|\\~\n \t| ~ 7_G\n@@ -405,7 +400,7 @@ test_expect_success 'log --graph --graph-lane-limit=1 truncate mid octopus merge\n '\n \n test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' '\n-\tcheck_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -441,7 +436,7 @@ test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=6 check if it only shows first of 3 parent merge' '\n-\tcheck_graph --graph-lane-limit=6 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=6 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -478,7 +473,7 @@ test_expect_success 'log --graph --graph-lane-limit=6 check if it only shows fir\n '\n \n test_expect_success 'log --graph --graph-lane-limit=7 check if it shows all 3 parent merge' '\n-\tcheck_graph --graph-lane-limit=7 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=7 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\ndiff --git a/t/t6016-rev-list-graph-simplify-history.sh b/t/t6016-rev-list-graph-simplify-history.sh\nindex 54b0a6f5f8..e0d9c3c1ac 100755\n--- a/t/t6016-rev-list-graph-simplify-history.sh\n+++ b/t/t6016-rev-list-graph-simplify-history.sh\n@@ -13,11 +13,6 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-log-graph.sh\n \n-check_graph () {\n-\tcat >expect &&\n-\tlib_test_cmp_graph --format=%s \"$@\"\n-}\n-\n test_expect_success 'set up rev-list --graph test' '\n \t# 3 commits on branch A\n \ttest_commit A1 foo.txt &&\n@@ -54,7 +49,7 @@ test_expect_success 'set up rev-list --graph test' '\n '\n \n test_expect_success '--graph --all' '\n-\tcheck_graph --all <<-\\EOF\n+\tlib_test_check_graph --all <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -82,7 +77,7 @@ test_expect_success '--graph --all' '\n # that undecorated merges are interesting, even with --simplify-by-decoration\n test_expect_success '--graph --simplify-by-decoration' '\n \tgit tag -d A4 &&\n-\tcheck_graph --all --simplify-by-decoration <<-\\EOF\n+\tlib_test_check_graph --all --simplify-by-decoration <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -114,7 +109,7 @@ test_expect_success 'setup: get rid of decorations on B' '\n \n # Graph with branch B simplified away\n test_expect_success '--graph --simplify-by-decoration prune branch B' '\n-\tcheck_graph --simplify-by-decoration --all <<-\\EOF\n+\tlib_test_check_graph --simplify-by-decoration --all <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -133,7 +128,7 @@ test_expect_success '--graph --simplify-by-decoration prune branch B' '\n '\n \n test_expect_success '--graph --full-history -- bar.txt' '\n-\tcheck_graph --full-history --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --full-history --all -- bar.txt <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -148,7 +143,7 @@ test_expect_success '--graph --full-history -- bar.txt' '\n '\n \n test_expect_success '--graph --full-history --simplify-merges -- bar.txt' '\n-\tcheck_graph --full-history --simplify-merges --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --full-history --simplify-merges --all -- bar.txt <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -161,7 +156,7 @@ test_expect_success '--graph --full-history --simplify-merges -- bar.txt' '\n '\n \n test_expect_success '--graph -- bar.txt' '\n-\tcheck_graph --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --all -- bar.txt <<-\\EOF\n \t* A7\n \t* A5\n \t* A3\n@@ -172,7 +167,7 @@ test_expect_success '--graph -- bar.txt' '\n '\n \n test_expect_success '--graph --sparse -- bar.txt' '\n-\tcheck_graph --sparse --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --sparse --all -- bar.txt <<-\\EOF\n \t* A7\n \t* A6\n \t* A5\n@@ -189,7 +184,7 @@ test_expect_success '--graph --sparse -- bar.txt' '\n '\n \n test_expect_success '--graph ^C4' '\n-\tcheck_graph --all ^C4 <<-\\EOF\n+\tlib_test_check_graph --all ^C4 <<-\\EOF\n \t* A7\n \t* A6\n \t* A5\n@@ -202,7 +197,7 @@ test_expect_success '--graph ^C4' '\n '\n \n test_expect_success '--graph ^C3' '\n-\tcheck_graph --all ^C3 <<-\\EOF\n+\tlib_test_check_graph --all ^C3 <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -220,7 +215,7 @@ test_expect_success '--graph ^C3' '\n # that important, but this test depends on it.  If the ordering ever changes\n # in the code, we'll need to update this test.\n test_expect_success '--graph --boundary ^C3' '\n-\tcheck_graph --boundary --all ^C3 <<-\\EOF\n+\tlib_test_check_graph --boundary --all ^C3 <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n\n-- \n2.54.0\n"},{"id":"545469","messageId":"20260613-ps-pre-commit-indent-v5-2-8d308efea63d@gmail.com","threadId":"65419","inReplyTo":"20260613-ps-pre-commit-indent-v5-0-8d308efea63d@gmail.com","subject":"[PATCH v5 2/2] graph: indent visual root in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-06-13T19:09:16Z","receivedAt":"2026-06-13T19:09:27Z","isPatch":true,"body":"When rendering a graph, if the history contains multiple \"visual roots\",\nactual roots or commits that look like roots (i.e. have their parents\nfiltered out) can end up being vertically adjacent to unrelated commits,\nfalsely appearing to be related.\n\nA fix for this issue was already attempted [1] a while ago.\n\nThis happens because the commits fill the space from left to right and\nwhen a visual root ends, its column becomes free for the following\ncommit even if they are not related. Once this happens the unrelated\ncommit is rendered below the visual root. Because there is no special\ncharacter or way to identify when a visual root is rendered making the\ngraph confusing.\n\nBy indenting the visual roots when there are still commits to show the\nvertical adjacency can be avoided.\n\nAdd is_visual_root flag to git_graph making it visible in all graph states,\ngive graph_update() a new function, graph_is_visual_root() to know if the\ncurrent commit is a visual root and set is_visual_root.\nThe different handled cases are:\n\n- If a visual root has children: similar to GRAPH_PRE_COMMIT state when\n  octopus merges need space, an edge row needs to be printed to connect\n  the child with the indented visual root. A new state GRAPH_PRE_ROOT is\n  needed to connect the child with the visual root:\n\n    * child of the visual root\n     \\ GRAPH_PRE_ROOT\n      * visual root indented\n\n- If a visual root is child-less we can skip GRAPH_PRE_ROOT state and\n  render the indented commit directly.\n\n      * visual root indented\n    * unrelated commit\n\n- If two or more visual roots are adjacent: by having a lookahead to the\n  next commit that will be rendered, if the next commit is also a visual\n  root and we are on a visual root, meaning two visual root adjacent in\n  the history, the top one can omit the indent, making the one below to\n  indent only once, if there are more adjacent visual commits, the\n  indentation will increase for each adjacent one, cascading.\n\n    * visual root\n      * visual root\n        * visual root\n    * last commit\n\n  Even if the last commit is a root, because there is nothing that will be\n  rendered below we can omit the indentation on purpose.\n\nThe lookahead is not completely reliable, on graphs with filtered parents,\nthe walker when processing the current commit it will simplify its\nparents by removing the ones that won't be shown, (They have the\nTREESAME flag when filtering by path for example), but it doesn't act\nfor the grandparents or the next commit if it is unrelated until we move\nto the next.\n\nFor example given\n\n  A visual root\n  B child\n  C parent of B, visual root FILTERED\n  D last commit\n\nWe would expect\n\n  A\n    B\n  D\n\nWhen processing A, for the walker and the information at the renderer, B\nis still a child of C, as B parent, hasn't been removed yet. This makes\ncascade to not trigger as the lookahead fails to detect if the next\ncommit will be a visual root.\nOnce at B, its parent has been removed and has become a visual root, and\nit just adds its indent to the one left by A. We end up with an extra\nindent:\n\n    A\n      B\n  D\n\nThe output isn't broken as unrelated commits are successfully separated\nby indentation, but an indent level should have been avoided.\n\nCreate a new test file for graph indentations test called\n't4218-log-graph-indentation.sh'.\n\nThe filtered parents edge case is documented as a NEEDSWORK on the\nlookahead function and it has its own 'test_expect_failure' at 't4218'.\n\n[1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n\nMentored-by: Karthik Nayak <karthik.188@gmail.com>\nMentored-by: Chandra Pratap <chandrapratap3519@gmail.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c                          | 262 ++++++++++++++++++++++\n t/meson.build                    |   1 +\n t/t4218-log-graph-indentation.sh | 467 +++++++++++++++++++++++++++++++++++++++\n 3 files changed, 730 insertions(+)\n\ndiff --git a/graph.c b/graph.c\nindex 842282685f..e0d1e2a510 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -60,12 +60,23 @@ struct column {\n \t * index into column_colors.\n \t */\n \tunsigned short color;\n+\t/*\n+\t * Marks if a commit is a non-first parent of a merge. These columns are\n+\t * already visually connected to the merge commit and do not need\n+\t * indentation.\n+\t *\n+\t * The first parent is the one that inherits the column and it can need\n+\t * indentation if turns out to be a visual root and there's still\n+\t * commits to render.\n+\t */\n+\tunsigned is_merge_parent:1;\n };\n \n enum graph_state {\n \tGRAPH_PADDING,\n \tGRAPH_SKIP,\n \tGRAPH_PRE_COMMIT,\n+\tGRAPH_PRE_ROOT,\n \tGRAPH_COMMIT,\n \tGRAPH_POST_MERGE,\n \tGRAPH_COLLAPSING\n@@ -315,6 +326,48 @@ struct git_graph {\n \t * diff_output_prefix_callback().\n \t */\n \tstruct strbuf prefix_buf;\n+\n+\t/*\n+\t * If a commit is a visual root, we need to indent it to prevent\n+\t * unrelated commits from being vertically adjacent to it.\n+\t */\n+\tunsigned is_visual_root:1;\n+\n+\t/*\n+\t * Indentation increases for each visual root adjacent to another visual\n+\t * root, making visual root commits indentation cascade.\n+\t */\n+\tunsigned int visual_root_depth;\n+\n+\t/*\n+\t * When a visual root is adjacent to other visual roots, the first one\n+\t * can avoid indentation and the rest cascades, increasing the indentation\n+\t * for each one.\n+\t */\n+\tunsigned visual_root_cascade:1;\n+\n+\t/*\n+\t * Set when the current commit was already present in graph->columns\n+\t * before being processed.\n+\t */\n+\tunsigned commit_in_columns:1;\n+};\n+\n+struct graph_lookahead_flags {\n+\t/*\n+\t * Set when there will be a commit after the current one that will be\n+\t * rendered.\n+\t */\n+\tunsigned int is_next_visible:1;\n+\t/*\n+\t * Set when the next visible commit is candidate to be a visual root.\n+\t */\n+\tunsigned int is_next_visual_root:1;\n+\t/*\n+\t * Set when the next visible commit will be rendered under the current\n+\t * commit.\n+\t */\n+\tunsigned int next_has_column:1;\n };\n \n static inline int graph_needs_truncation(struct git_graph *graph, int lane)\n@@ -388,6 +441,8 @@ struct git_graph *graph_init(struct rev_info *opt)\n \tgraph->num_columns = 0;\n \tgraph->num_new_columns = 0;\n \tgraph->mapping_size = 0;\n+\tgraph->visual_root_depth = 0;\n+\tgraph->visual_root_cascade = 0;\n \t/*\n \t * Start the column color at the maximum value, since we'll\n \t * always increment it for the first commit we output.\n@@ -561,6 +616,11 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t\t\t\t\t  struct commit *commit,\n \t\t\t\t\t  int idx)\n {\n+\t/*\n+\t * Get the initial merge_layout before it's modified to know if this\n+\t * is a merge.\n+\t */\n+\tint initial_merge_layout = graph->merge_layout;\n \tint i = graph_find_new_column_by_commit(graph, commit);\n \tint mapping_idx;\n \n@@ -572,6 +632,7 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t\ti = graph->num_new_columns++;\n \t\tgraph->new_columns[i].commit = commit;\n \t\tgraph->new_columns[i].color = graph_find_commit_color(graph, commit);\n+\t\tgraph->new_columns[i].is_merge_parent = 0;\n \t}\n \n \tif (graph->num_parents > 1 && idx > -1 && graph->merge_layout == -1) {\n@@ -610,6 +671,12 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t}\n \n \tgraph->mapping[mapping_idx] = i;\n+\n+\t/*\n+\t * Mark non-first parents of a merge.\n+\t */\n+\tif (graph->num_parents > 1 && initial_merge_layout >= 0 && idx > -1)\n+\t\tgraph->new_columns[i].is_merge_parent = 1;\n }\n \n static void graph_update_columns(struct git_graph *graph)\n@@ -701,10 +768,20 @@ static void graph_update_columns(struct git_graph *graph)\n \t\t\tif (graph->num_parents == 0)\n \t\t\t\tgraph->width += 2;\n \t\t} else {\n+\t\t\tint j;\n \t\t\tgraph_insert_into_new_columns(graph, col_commit, -1);\n+\t\t\t/*\n+\t\t\t * This column is not the current commit, but we need to\n+\t\t\t * propagate the flag until the commit is processed.\n+\t\t\t */\n+\t\t\tj = graph_find_new_column_by_commit(graph, col_commit);\n+\t\t\tif (j >= 0 && graph->columns[i].is_merge_parent)\n+\t\t\t\tgraph->new_columns[j].is_merge_parent = 1;\n \t\t}\n \t}\n \n+\tgraph->commit_in_columns = is_commit_in_columns;\n+\n \t/*\n \t * If graph_max_lanes is set, cap the width\n \t */\n@@ -763,9 +840,135 @@ static int graph_needs_pre_commit_line(struct git_graph *graph)\n \t       graph->expansion_row < graph_num_expansion_rows(graph);\n }\n \n+/*\n+ * A commit can be a visual root when:\n+ * - It has no parents.\n+ *\n+ * - It has parents but they are all filtered out and\n+ *   commit->parents arrives NULL.\n+ *\n+ * - It is not a boundary commit. Boundary commits also have no visible\n+ *   parents, but they are not selected as visual roots because they cannot\n+ *.  cause the ambiguity of being vertically adjacent because:\n+ *\n+ *   1. A boundary only appears because an included commit is its child.\n+ *      Children are always above, and the renderer draws an edge down to\n+ *      the boundary from that child. Rather than starting a column like a\n+ *      visual root would do, it inherits its child column.\n+ *\n+ *   2. Included commits cannot appear below a boundary. Boundaries are\n+ *      ancestors of the exclusion point; if an included commit were an\n+ *      ancestor of the boundary it would be excluded and not rendered.\n+ *      Boundaries therefore always sink to the bottom.\n+ */\n+static int graph_is_visual_root_candidate(struct commit *c)\n+{\n+\treturn c->parents == NULL && !(c->object.flags & BOUNDARY);\n+}\n+\n+static int graph_is_visual_root(struct git_graph *graph,\n+\t\t\t\tstruct graph_lookahead_flags *flags)\n+{\n+\t/*\n+\t * This must be only called for the current commit as graph contains\n+\t * the state for the current commit only.\n+\t *\n+\t * To check if a commit is a visual root, call graph_is_visual_root_candidate()\n+\t * but we won't know if it is really a visual root until we get to the\n+\t * next commit state.\n+\t *\n+\t * The current commit is an actual visual root if it is a candidate and\n+\t * the commit is not a non-first parent of a merge.\n+\t *\n+\t *   *\n+\t *   |\\\n+\t *   | *    <- it is a visual root candidate but it shouldn't be indented\n+\t *   *         because it is already connected by an edge.\n+\t *   ^         if commit_in_columns && is_merge_parent means the commit\n+\t *   |         was put by a merge and is connected.\n+\t *   |\n+\t *   `-------- if !is_next_visible means we're on the last commit, avoid\n+\t *             indentation unless the one before is a visual root, then\n+\t *             we need to differentiate from the one above.\n+\t *\n+\t * If next_has_columns means that the next commit has\n+\t * already a column, so it will not be rendered below, the\n+\t * current commit has to act as the last commit and omit\n+\t * indentation.\n+\t */\n+\treturn graph_is_visual_root_candidate(graph->commit) &&\n+\t       !(graph->commit_in_columns &&\n+\t\t graph->columns[graph->commit_index].is_merge_parent) &&\n+\t       flags->is_next_visible &&\n+\t       (!flags->next_has_column || graph->visual_root_depth > 0);\n+}\n+\n+/*\n+ * Iterates the commits queue searching for the next visible commit, once found\n+ * sets visibleness and visual-root flags.\n+ * Knowing if the next commit is also a visual root avoids redundant indentations\n+ *\n+ * NEEDSWORK: The queue is actively being modified by the walker, for each commit\n+ * its parents and itself get simplified and their flags set, but for the next\n+ * unrelated commit or the grandparents they are not simplified yet, which means\n+ * that a commit whose parents are all filtered will not be marked as a visual\n+ * root candidate at the lookahead.\n+ * This causes the lookahead to fail, failing to set the cascade flag to avoid\n+ * redundant indentations.\n+ * See 'test_expect_failure' at t4218-log-graph-indentation.sh.\n+ */\n+static void graph_peek_next_visible(struct git_graph *graph,\n+\t\t\t\t    struct graph_lookahead_flags *flags)\n+{\n+\tstruct commit_list *cl;\n+\n+\tflags->is_next_visible = 0;\n+\tflags->is_next_visual_root = 0;\n+\tflags->next_has_column = 0;\n+\n+\tfor (cl = graph->revs->commits; cl; cl = cl->next) {\n+\t\tif (get_commit_action(graph->revs, cl->item) != commit_show)\n+\t\t\tcontinue;\n+\t\tflags->is_next_visible = 1;\n+\t\tflags->next_has_column = graph_find_new_column_by_commit(graph, cl->item) >= 0;\n+\t\t/*\n+\t\t * We do not need graph->commit_in_columns or is_merge_parent,\n+\t\t * because we only need to know whether the next one might be a\n+\t\t * visual root, affecting the current commit where the cascade\n+\t\t * would have to be set and the first visual root not indented.\n+\t\t *\n+\t\t * It will set next_is_visual_root to true for merge parents that\n+\t\t * graph_is_visual_root() would return false, but if the next is\n+\t\t * a merge parent, the current commit is the child and cannot\n+\t\t * be a visual root and therefore having no effect.\n+\t\t */\n+\t\tif (!graph_is_visual_root_candidate(cl->item))\n+\t\t\treturn;\n+\n+\t\t/*\n+\t\t * The next visible commit is a visual root candidate, but\n+\t\t * only set cascade if it's not the last commit to be rendered.\n+\t\t */\n+\t\tfor (cl = cl->next; cl; cl = cl->next) {\n+\t\t\tif (get_commit_action(graph->revs, cl->item) != commit_show)\n+\t\t\t\tcontinue;\n+\t\t\tflags->is_next_visual_root = 1;\n+\t\t\treturn;\n+\t\t}\n+\t\treturn;\n+\t}\n+}\n+\n+static int graph_needs_pre_root_line(struct git_graph *graph)\n+{\n+\treturn graph->commit_in_columns && graph->is_visual_root &&\n+\t       graph->num_columns > 0 && !graph->visual_root_cascade;\n+}\n+\n void graph_update(struct git_graph *graph, struct commit *commit)\n {\n \tstruct commit_list *parent;\n+\tstruct graph_lookahead_flags flags;\n \n \t/*\n \t * Set the new commit\n@@ -796,6 +999,23 @@ void graph_update(struct git_graph *graph, struct commit *commit)\n \t */\n \tgraph_update_columns(graph);\n \n+\tgraph_peek_next_visible(graph, &flags);\n+\n+\tgraph->is_visual_root = graph_is_visual_root(graph, &flags);\n+\n+\tif (graph->is_visual_root) {\n+\t\t/*\n+\t\t * If next is a visual root we can omit the indent for the first\n+\t\t * visual root and start cascading.\n+\t\t */\n+\t\tif (!graph->visual_root_depth && flags.is_next_visual_root)\n+\t\t\tgraph->visual_root_cascade = 1;\n+\t\tgraph->visual_root_depth++;\n+\t} else {\n+\t\tgraph->visual_root_depth = 0;\n+\t\tgraph->visual_root_cascade = 0;\n+\t}\n+\n \tgraph->expansion_row = 0;\n \n \t/*\n@@ -813,11 +1033,16 @@ void graph_update(struct git_graph *graph, struct commit *commit)\n \t * room for it.  We need to do this only if there is a branch row\n \t * (or more) to the right of this commit.\n \t *\n+\t * If it is a visual root, we need to print an extra row to\n+\t * connect the indentation.\n+\t *\n \t * If there are less than 3 parents, we can immediately print the\n \t * commit line.\n \t */\n \tif (graph->state != GRAPH_PADDING)\n \t\tgraph->state = GRAPH_SKIP;\n+\telse if (graph_needs_pre_root_line(graph))\n+\t\tgraph->state = GRAPH_PRE_ROOT;\n \telse if (graph_needs_pre_commit_line(graph))\n \t\tgraph->state = GRAPH_PRE_COMMIT;\n \telse\n@@ -1065,6 +1290,17 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \n \t\tif (col_commit == graph->commit) {\n \t\t\tseen_this = 1;\n+\t\t\tif (graph->is_visual_root) {\n+\t\t\t\tint depth = graph->visual_root_depth;\n+\t\t\t\t/*\n+\t\t\t\t * Each visual column is 2 characters wide.\n+\t\t\t\t * Omit the indentation for the first visual\n+\t\t\t\t * root in cascade mode.\n+\t\t\t\t */\n+\t\t\t\tint padding = (depth - graph->visual_root_cascade) * 2;\n+\t\t\t\tgraph_line_addchars(line, ' ', padding);\n+\t\t\t\tgraph->width += padding;\n+\t\t\t}\n \t\t\tgraph_output_commit_char(graph, line);\n \n \t\t\tif (graph_needs_truncation(graph, i)) {\n@@ -1436,6 +1672,29 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n \t\tgraph_update_state(graph, GRAPH_PADDING);\n }\n \n+static void graph_output_pre_root_line(struct git_graph *graph, struct graph_line *line)\n+{\n+\t/*\n+\t * This function adds a row before a visual root, to connect the\n+\t * branch to the indented commit. It should only be called on a\n+\t * visual root.\n+\t */\n+\tassert(graph->is_visual_root);\n+\n+\tfor (size_t i = 0; i < graph->num_columns; i++) {\n+\t\tstruct column *col = &graph->columns[i];\n+\t\tif (col->commit == graph->commit) {\n+\t\t\tgraph_line_addch(line, ' ');\n+\t\t\tgraph_line_write_column(line, col, '\\\\');\n+\t\t} else {\n+\t\t\tgraph_line_write_column(line, col, '|');\n+\t\t}\n+\t\tgraph_line_addch(line, ' ');\n+\t}\n+\n+\tgraph_update_state(graph, GRAPH_COMMIT);\n+}\n+\n int graph_next_line(struct git_graph *graph, struct strbuf *sb)\n {\n \tint shown_commit_line = 0;\n@@ -1461,6 +1720,9 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb)\n \tcase GRAPH_PRE_COMMIT:\n \t\tgraph_output_pre_commit_line(graph, &line);\n \t\tbreak;\n+\tcase GRAPH_PRE_ROOT:\n+\t\tgraph_output_pre_root_line(graph, &line);\n+\t\tbreak;\n \tcase GRAPH_COMMIT:\n \t\tgraph_output_commit_line(graph, &line);\n \t\tshown_commit_line = 1;\ndiff --git a/t/meson.build b/t/meson.build\nindex c5832fee05..17037a8465 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -576,6 +576,7 @@ integration_tests = [\n   't4215-log-skewed-merges.sh',\n   't4216-log-bloom.sh',\n   't4217-log-limit.sh',\n+  't4218-log-graph-indentation.sh',\n   't4252-am-options.sh',\n   't4253-am-keep-cr-dos.sh',\n   't4254-am-corrupt.sh',\ndiff --git a/t/t4218-log-graph-indentation.sh b/t/t4218-log-graph-indentation.sh\nnew file mode 100755\nindex 0000000000..ccf15c0a52\n--- /dev/null\n+++ b/t/t4218-log-graph-indentation.sh\n@@ -0,0 +1,467 @@\n+#!/bin/sh\n+\n+test_description='git log --graph visual root indentations'\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-log-graph.sh\n+\n+check_graph_with_description () {\n+\tcat >expect &&\n+\tlib_test_cmp_graph --format=\"%s%ndescription%nsecond-line\" \"$@\"\n+}\n+\n+create_orphan () {\n+\tgit checkout --orphan \"$1\" &&\n+\t{ git rm -rf . || true; }\n+}\n+\n+# disable commit-graph topo order to have the graph to render in different\n+# ways (used in --first-parent tests to have multiple visual roots while a\n+# column is active at the same time).\n+unset_commit_graph() {\n+\tsane_unset GIT_TEST_COMMIT_GRAPH &&\n+\trm -f .git/objects/info/commit-graph &&\n+\trm -rf .git/objects/info/commit-graphs\n+}\n+\n+test_expect_success 'single root commit is not indented' '\n+\tcreate_orphan _1 && test_commit 1_A &&\n+\tlib_test_check_graph _1 <<-\\EOF\n+\t* 1_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indented before unrelated branch' '\n+\tcreate_orphan _2 && test_commit 2_A && test_commit 2_B &&\n+\tcreate_orphan _3 && test_commit 3_A &&\n+\tlib_test_check_graph _2 _3 <<-\\EOF\n+\t  * 3_A\n+\t* 2_B\n+\t* 2_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indentation with --left-right' '\n+\tlib_test_check_graph --left-right _2..._3 <<-\\EOF\n+\t  > 3_A\n+\t< 2_B\n+\t< 2_A\n+\tEOF\n+'\n+\n+# A better case of why indentation is still needed with '--left-right' flag is\n+# that unrelated branches can be on the same side, so it's needed to\n+# differentiate visual roots on the same side.\n+test_expect_success 'visual root indentation with --left-right having unrelated commits on the same side' '\n+\tlib_test_check_graph --left-right _2..._3 _1 <<-\\EOF\n+\t  > 3_A\n+\t< 2_B\n+\t \\\n+\t  < 2_A\n+\t> 1_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indents the description also' '\n+\tcheck_graph_with_description _2 _3 <<-\\EOF\n+\t  * 3_A\n+\t    description\n+\t    second-line\n+\t* 2_B\n+\t| description\n+\t| second-line\n+\t* 2_A\n+\t  description\n+\t  second-line\n+\tEOF\n+'\n+\n+test_expect_success 'indented visual root parent gets connected to its child' '\n+\tcreate_orphan _4 && test_commit 4_A && test_commit 4_B &&\n+\tcreate_orphan _5 && test_commit 5_A && test_commit 5_B &&\n+\tlib_test_check_graph _4 _5<<-\\EOF\n+\t* 5_B\n+\t \\\n+\t  * 5_A\n+\t* 4_B\n+\t* 4_A\n+\tEOF\n+'\n+\n+test_expect_success 'indented visual root parent gets connected to its child with description' '\n+\tcheck_graph_with_description _4 _5 <<-\\EOF\n+\t* 5_B\n+\t| description\n+\t| second-line\n+\t \\\n+\t  * 5_A\n+\t    description\n+\t    second-line\n+\t* 4_B\n+\t| description\n+\t| second-line\n+\t* 4_A\n+\t  description\n+\t  second-line\n+\tEOF\n+'\n+\n+test_expect_success 'visual roots cascade and last root does not' '\n+\tcreate_orphan _7 && test_commit 7_A && test_commit 7_B &&\n+\tcreate_orphan _8 && test_commit 8_A &&\n+\tcreate_orphan _9 && test_commit 9_A &&\n+\tcreate_orphan _10 && test_commit 10_A &&\n+\tlib_test_check_graph _7 _8 _9 _10  <<-\\EOF\n+\t* 10_A\n+\t  * 9_A\n+\t    * 8_A\n+\t* 7_B\n+\t* 7_A\n+\tEOF\n+'\n+\n+test_expect_success 'last root does not cascade' '\n+\tlib_test_check_graph _8 _9 _10 <<-\\EOF\n+\t* 10_A\n+\t  * 9_A\n+\t* 8_A\n+\tEOF\n+'\n+\n+test_expect_success 'merge parents are roots between them but they do not indent' '\n+\tcreate_orphan _11 && test_commit 11_A &&\n+\tcreate_orphan _12 && test_commit 12_A &&\n+\tcreate_orphan _13 && test_commit 13_A &&\n+\tgit checkout _11 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _11 -p _12 -p _13 -m 11_octopus) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _11 <<-\\EOF\n+\t*-.   11_octopus\n+\t|\\ \\\n+\t| | * 13_A\n+\t| * 12_A\n+\t* 11_A\n+\tEOF\n+'\n+\n+# The last parent of a merge can be indented if nothing related to it needs to\n+# be rendered after, if it's another visual root, merge parent must not get\n+# indented but rather activate cascading.\n+test_expect_success 'merge then unrelated visual root and unrelated branch' '\n+\tcreate_orphan _16 && test_commit 16_A && test_commit 16_B &&\n+\tcreate_orphan _17 && test_commit 17_A &&\n+\tcreate_orphan _18 && test_commit 18_A &&\n+\tcreate_orphan _19 && test_commit 19_A &&\n+\tcreate_orphan _20 && test_commit 20_A &&\n+\tgit checkout _18 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _18 -p _19 -p _20 -m 18_octopus) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _18 _17 _16 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t* 18_A\n+\t  * 17_A\n+\t* 16_B\n+\t* 16_A\n+\tEOF\n+'\n+\n+# The last commit root does not get indented, if the next thing after the root\n+# merge parent is the last commit, indent the merge parent.\n+test_expect_success 'merge then unrelated root indents merge parent' '\n+\tlib_test_check_graph _18 _17 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t \\\n+\t  * 18_A\n+\t* 17_A\n+\tEOF\n+'\n+\n+test_expect_success 'merge then unrelated branch indents merge parent' '\n+\tlib_test_check_graph _18 _16 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t \\\n+\t  * 18_A\n+\t* 16_B\n+\t* 16_A\n+\tEOF\n+'\n+\n+test_expect_success 'two-parent merge of orphans' '\n+\tcreate_orphan _21 && test_commit 21_A &&\n+\tcreate_orphan _22 && test_commit 22_A &&\n+\tgit checkout _21 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _21 -p _22 -m 21_merge) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _21 <<-\\EOF\n+\t*   21_merge\n+\t|\\\n+\t| * 22_A\n+\t* 21_A\n+\tEOF\n+'\n+\n+test_expect_success 'commit with filtered parent becomes a visual root' '\n+\tcreate_orphan _23 &&\n+\techo test >other.txt &&\n+\tgit add other.txt &&\n+\tgit commit -m \"23_A\" &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"23_B\" &&\n+\tcreate_orphan _24 &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"24_A\" &&\n+\tlib_test_check_graph _23 _24 -- foo.txt <<-\\EOF\n+\t  * 23_B\n+\t* 24_A\n+\tEOF\n+'\n+\n+# The walker simplifies the commit for the current one and its parents, removing\n+# the filtered parents, but it doesn't go one step ahead, this causes some edge\n+# cases with the lookahead.\n+# Given A (orphan), the walker only processes A, and when we lookahead for B\n+# (child of C) even tho C will be filtered, it hasn't been simplified yet, so we\n+# don't see B as a visual root, therefore cascade indentation isn't applied to A.\n+# (cascade indentation starts the indentation at the second visual root, to avoid\n+# redundant indentation). So A gets an extra indent, and once B is processed,\n+# when rendering it, C has been removed, B is a visual root and as the last commit\n+# isn't considered a visual root as it cannot have unrelated commits below it,\n+# cascading isn't also applied, giving B another indent.\n+#\n+# The final result is an extra indent for A and B:\n+#\n+#\t  A\n+#\t    B\n+#\tD\n+#\n+# This will happen for any case where we find ourselves with the next commit\n+# being a unrelated child of a parent the will be filtered.\n+#\n+# instead of the expected:\n+test_expect_failure 'filtered parent cascading edge case' '\n+\tcreate_orphan _25 &&\n+\tgit rm -rf . &&\n+\techo test >other.txt &&\n+\tgit add other.txt &&\n+\tgit commit -m \"C-filtered\" &&\n+\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"B (child of filtered)\" &&\n+\n+\tcreate_orphan _26 &&\n+\tgit rm -rf . &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"A (visual root)\" &&\n+\n+\tcreate_orphan _27 &&\n+\tgit rm -rf . &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"D (last)\" &&\n+\n+\tlib_test_check_graph _25 _26 _27 -- foo.txt <<-\\EOF\n+\t* A (visual root)\n+\t  * B (child of filtered)\n+\t* D (last)\n+\tEOF\n+'\n+\n+test_expect_failure 'multiple filtered parents in sequence' '\n+\tcreate_orphan _44 &&\n+\tgit rm -rf . &&\n+\techo a >other.txt && git add other.txt && git commit -m \"44_F\" &&\n+\techo b >foo.txt && git add foo.txt && git commit -m \"44_C\" &&\n+\n+\tcreate_orphan _45 &&\n+\tgit rm -rf . &&\n+\techo c >other.txt && git add other.txt && git commit -m \"45_F\" &&\n+\techo d >foo.txt && git add foo.txt && git commit -m \"45_C\" &&\n+\n+\tcreate_orphan _46 &&\n+\tgit rm -rf . &&\n+\techo e >foo.txt && git add foo.txt && git commit -m \"46_A\" &&\n+\n+\tlib_test_check_graph _44 _45 _46 -- foo.txt <<-\\EOF\n+\t* 46_A\n+\t  * 45_C\n+\t* 44_C\n+\tEOF\n+'\n+\n+test_expect_failure 'real orphan root followed by child of filtered parent' '\n+\tcreate_orphan _47 &&\n+\tgit rm -rf . &&\n+\techo a >foo.txt && git add foo.txt && git commit -m \"47_A\" &&\n+\n+\tcreate_orphan _48 &&\n+\tgit rm -rf . &&\n+\techo b >other.txt && git add other.txt && git commit -m \"48_filtered\" &&\n+\techo c >foo.txt && git add foo.txt && git commit -m \"48_B\" &&\n+\n+\tcreate_orphan _49 &&\n+\tgit rm -rf . &&\n+\techo d >foo.txt && git add foo.txt && git commit -m \"49_last\" &&\n+\n+\tlib_test_check_graph _47 _48 _49 -- foo.txt <<-\\EOF\n+\t* 47_A\n+\t  * 48_B\n+\t* 49_last\n+\tEOF\n+'\n+\n+# This tests prove why there is no need to have indentation for boundary\n+# commits.\n+#\n+# Boundary commits rather than starting a column they 'inherit' the one of\n+# its child so there will always be an edge that connects it removing the\n+# ambiguity.\n+test_expect_success 'unrelated boundaries are not ambiguous' '\n+\tcreate_orphan _28 && test_commit 28_A && test_commit 28_B &&\n+\ttest_commit 28_C &&\n+\tcreate_orphan _29 && test_commit 29_A && test_commit 29_B &&\n+\tlib_test_check_graph --boundary 28_A.._28 29_A.._29 <<-\\EOF\n+\t* 29_B\n+\t| * 28_C\n+\t| * 28_B\n+\t| o 28_A\n+\to 29_A\n+\tEOF\n+'\n+\n+# Same structure as t6016\n+test_expect_success 'boundary commits big test' '\n+\t# 3 commits on branch _30\n+\tcreate_orphan _30 &&\n+\ttest_commit 30_A &&\n+\ttest_commit 30_B &&\n+\ttest_commit 30_C &&\n+\n+\t# 2 commits on branch _31, started from 30_A\n+\tgit checkout -b _31 30_A &&\n+\ttest_commit 31_A &&\n+\ttest_commit 31_B &&\n+\n+\t# 2 commits on branch _32, started from 30_B\n+\tgit checkout -b _32 30_B &&\n+\ttest_commit 32_A &&\n+\ttest_commit 32_B &&\n+\n+\t# Octopus merge _31 and _32 into -30\n+\tgit checkout _30 &&\n+\tgit merge _31 _32 -m 30_D &&\n+\tgit tag 30_D &&\n+\ttest_commit 30_E &&\n+\n+\t# More commits on _32, then merge _32 into _30\n+\tgit checkout _32 &&\n+\ttest_commit 32_C &&\n+\ttest_commit 32_D &&\n+\tgit checkout _30 &&\n+\tgit merge -s ours _32 -m 30_F &&\n+\tgit tag 30_F &&\n+\ttest_commit 30_G &&\n+\tlib_test_check_graph --boundary _30 _31 _32 ^32_C <<-\\EOF\n+\t* 30_G\n+\t*   30_F\n+\t|\\\n+\t| * 32_D\n+\t* | 30_E\n+\t| |\n+\t|  \\\n+\t*-. \\   30_D\n+\t|\\ \\ \\\n+\t| * | | 31_B\n+\t| * | | 31_A\n+\t* | | | 30_C\n+\to | | | 30_B\n+\t|/ / /\n+\to / / 30_A\n+\t / /\n+\t| o 32_C\n+\t|/\n+\to 32_B\n+\tEOF\n+'\n+\n+# Filter by --first-parent and then forcing the filtered parents to be shown.\n+test_expect_success '--first-parent flag with the filtered parents' '\n+\t(\n+\t\tunset_commit_graph &&\n+\t\tcreate_orphan _35 && test_commit 35_A && test_commit 35_B &&\n+\t\tcreate_orphan _36 && test_commit 36_A &&\n+\t\tcreate_orphan _37 && test_commit 37_A &&\n+\t\tgit checkout _35 &&\n+\t\tTREE=$(git write-tree) &&\n+\t\tMERGE=$(git commit-tree $TREE -p _35 -p _36 -p _37 -m 35_octopus) &&\n+\t\tgit reset --hard $MERGE &&\n+\t\tlib_test_check_graph --first-parent _35 _36 _37 <<-\\EOF\n+\t\t* 35_octopus\n+\t\t| * 37_A\n+\t\t|   * 36_A\n+\t\t* 35_B\n+\t\t* 35_A\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success '--first-parent with filtered parents but one has a child' '\n+\t(\n+\t\tunset_commit_graph &&\n+\t\tcreate_orphan _38 && test_commit 38_A && test_commit 38_B &&\n+\t\tcreate_orphan _39 && test_commit 39_A &&\n+\t\tcreate_orphan _40 && test_commit 40_A && test_commit 40_B &&\n+\t\tgit checkout _38 &&\n+\t\tTREE=$(git write-tree) &&\n+\t\tMERGE=$(git commit-tree $TREE -p _38 -p _39 -p _40 -m 38_octopus) &&\n+\t\tgit reset --hard $MERGE &&\n+\t\tlib_test_check_graph --first-parent _38 _39 _40 <<-\\EOF\n+\t\t* 38_octopus\n+\t\t| * 40_B\n+\t\t| * 40_A\n+\t\t|   * 39_A\n+\t\t* 38_B\n+\t\t* 38_A\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success '--first-parent with filtered parents but both have childs' '\n+\t(\n+\t\tunset_commit_graph &&\n+\t\tcreate_orphan _41 && test_commit 41_A && test_commit 41_B &&\n+\t\tcreate_orphan _42 && test_commit 42_A && test_commit 42_B &&\n+\t\tcreate_orphan _43 && test_commit 43_A && test_commit 43_B &&\n+\t\tgit checkout _41 &&\n+\t\tTREE=$(git write-tree) &&\n+\t\tMERGE=$(git commit-tree $TREE -p _41 -p _42 -p _43 -m 41_octopus) &&\n+\t\tgit reset --hard $MERGE &&\n+\t\tlib_test_check_graph --first-parent _41 _42 _43 <<-\\EOF\n+\t\t* 41_octopus\n+\t\t| * 43_B\n+\t\t|  \\\n+\t\t|   * 43_A\n+\t\t| * 42_B\n+\t\t| * 42_A\n+\t\t* 41_B\n+\t\t* 41_A\n+\t\tEOF\n+\t)\n+'\n+\n+test_done\n\n-- \n2.54.0\n"},{"id":"545475","messageId":"xmqqo6hdepgy.fsf@gitster.g","threadId":"65419","inReplyTo":"20260613-ps-pre-commit-indent-v5-2-8d308efea63d@gmail.com","subject":"Re: [PATCH v5 2/2] graph: indent visual root in graph","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-14T04:05:33Z","receivedAt":"2026-06-14T04:05:37Z","isPatch":true,"body":"Pablo Sabater <pabloosabaterr@gmail.com> writes:\n\n[jc: Taylor CC'ed for his expertise and opinion on the quoted part\nthat mucks with commit-graph files during the test]\n\n> diff --git a/t/t4218-log-graph-indentation.sh b/t/t4218-log-graph-indentation.sh\n> new file mode 100755\n> index 0000000000..ccf15c0a52\n> --- /dev/null\n> +++ b/t/t4218-log-graph-indentation.sh\n> @@ -0,0 +1,467 @@\n> +#!/bin/sh\n> ...\n> +# disable commit-graph topo order to have the graph to render in different\n> +# ways (used in --first-parent tests to have multiple visual roots while a\n> +# column is active at the same time).\n> +unset_commit_graph() {\n> +\tsane_unset GIT_TEST_COMMIT_GRAPH &&\n> +\trm -f .git/objects/info/commit-graph &&\n> +\trm -rf .git/objects/info/commit-graphs\n> +}\n\nI do not quite understand why having commit-graph makes the test\nresult unpredictable here, but wouldn't we have a more stable way\nto disable use of commit-graph than going into filesystem and muck\nwith the implementation detail like the above?  \n\nThanks.\n\n\n"},{"id":"545477","messageId":"CAN5EUNQ193QyOeTLdu9aXzDeBhFpg38YYBbOLhZLgcg3qfd=uA@mail.gmail.com","threadId":"65419","inReplyTo":"xmqqo6hdepgy.fsf@gitster.g","subject":"Re: [PATCH v5 2/2] graph: indent visual root in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-06-14T05:28:24Z","receivedAt":"2026-06-14T05:28:36Z","isPatch":true,"body":"El dom, 14 jun 2026 a las 6:05, Junio C Hamano (<gitster@pobox.com>) escribió:\n>\n> Pablo Sabater <pabloosabaterr@gmail.com> writes:\n>\n> [jc: Taylor CC'ed for his expertise and opinion on the quoted part\n> that mucks with commit-graph files during the test]\n>\n> > diff --git a/t/t4218-log-graph-indentation.sh b/t/t4218-log-graph-indentation.sh\n> > new file mode 100755\n> > index 0000000000..ccf15c0a52\n> > --- /dev/null\n> > +++ b/t/t4218-log-graph-indentation.sh\n> > @@ -0,0 +1,467 @@\n> > +#!/bin/sh\n> > ...\n> > +# disable commit-graph topo order to have the graph to render in different\n> > +# ways (used in --first-parent tests to have multiple visual roots while a\n> > +# column is active at the same time).\n> > +unset_commit_graph() {\n> > +     sane_unset GIT_TEST_COMMIT_GRAPH &&\n> > +     rm -f .git/objects/info/commit-graph &&\n> > +     rm -rf .git/objects/info/commit-graphs\n> > +}\n>\n> I do not quite understand why having commit-graph makes the test\n> result unpredictable here, but wouldn't we have a more stable way\n> to disable use of commit-graph than going into filesystem and muck\n> with the implementation detail like the above?\n>\n> Thanks.\n\nHi!\n\nIt does not make it unpredictable but it makes it not output what I\nwanted to test, what I wanted to test is having an active column at\nthe same time that visual roots in different cases were being rendered\non another column. However having GIT_TEST_COMMIT_GRAPH in the last\ntext for example changes from:\n\n* 41_octopus\n| * 43_B\n|  \\\n|   * 43_A\n| * 42_B\n| * 42_A\n* 41_B\n* 41_A\n\nto:\n\n* 41_octopus\n* 41_B\n \\\n  * 41_A\n* 43_B\n \\\n  * 43_A\n* 42_B\n* 42_A\n\nWhile the output it's ok and the indentation works the excluded but\nforced to be rendered parents are not being rendered at the same time\nas an active column is.\nOn the unset_commit_graph function I had to remove those files because\neven if it is GIT_TEST_COMMIT_GRAPH unset if there are files from\nprevious tests they still change the output.\nMaybe there is a way to get this more cleanly but from what I tried it\ndidn't work as I wanted, sorry.\n\nThanks,\nPablo.\n"},{"id":"545591","messageId":"xmqqzf0vbyj8.fsf@gitster.g","threadId":"65419","inReplyTo":"CAN5EUNQ193QyOeTLdu9aXzDeBhFpg38YYBbOLhZLgcg3qfd=uA@mail.gmail.com","subject":"Re: [PATCH v5 2/2] graph: indent visual root in graph","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-15T15:42:35Z","receivedAt":"2026-06-15T15:42:38Z","isPatch":true,"body":"Pablo Sabater <pabloosabaterr@gmail.com> writes:\n\n> It does not make it unpredictable but it makes it not output what I\n> wanted to test, what I wanted to test is having an active column at\n> the same time that visual roots in different cases were being rendered\n> on another column.\n\nOh, use of commit-graph changes the traversal order, which would\naffect how the graph is drawn, and there is no way to ensure that we\ntraverse in the same way with or without commit-graph?  That's\ninconvenient.  But even without commit-graph, do we guarantee the\nsame traversal order forever?  I doubt it.  So I suspect that it is\na brittle workaround to disable commit-graph in the longer term.\n\nAs long as the graph engine shows correct graph no matter what order\nthe commits come out of the revision traversal engine, we won't hurt\nend-users, but we need our tests to be reproducible, so that is a\nbit unfortunate.\n\nAnyway, stepping back a bit, \n\n> However having GIT_TEST_COMMIT_GRAPH in the last\n> text for example changes from:\n>\n> * 41_octopus\n> | * 43_B\n> |  \\\n> |   * 43_A\n> | * 42_B\n> | * 42_A\n> * 41_B\n> * 41_A\n\nDoes the \"vertically aligned * on 2nd and later columns do not mean\nany parent-child relationship\" rule no longer apply in this version?\nIOW, does the above graph show that\n\n - 41_A is a parent of 41_B, which is a parent of 41_octopus\n - 42_A is a parent of 42_B, and \n - 43_A is a parent of 43_B but is not related to 42_B\n\n?  Who are the parents of 41_octopus?  It has no relationship with\n42_B and 43_B, and unlike what its name suggests, it has only 41_b\nas its parent (probably with history simplification that makes only\nthese commits shown)?\n\n> to:\n>\n> * 41_octopus\n> * 41_B\n>  \\\n>   * 41_A\n> * 43_B\n>  \\\n>   * 43_A\n> * 42_B\n> * 42_A\n\nAnd this graph shows the same inter-commit relationship.  So both\nare correctly showing what we want to express, but they show the\nsame information differently, making test_cmp unhappy?\n\nThanks.\n"},{"id":"545652","messageId":"CAN5EUNR-o_sLzeWuy7M9UMFHBKxSuytNd=4p2svtFuv40E8vZg@mail.gmail.com","threadId":"65419","inReplyTo":"xmqqzf0vbyj8.fsf@gitster.g","subject":"Re: [PATCH v5 2/2] graph: indent visual root in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-06-16T13:06:43Z","receivedAt":"2026-06-16T13:06:56Z","isPatch":true,"body":"El lun, 15 jun 2026 a las 17:42, Junio C Hamano (<gitster@pobox.com>) escribió:\n>\n> Pablo Sabater <pabloosabaterr@gmail.com> writes:\n>\n> > It does not make it unpredictable but it makes it not output what I\n> > wanted to test, what I wanted to test is having an active column at\n> > the same time that visual roots in different cases were being rendered\n> > on another column.\n>\n> Oh, use of commit-graph changes the traversal order, which would\n> affect how the graph is drawn, and there is no way to ensure that we\n> traverse in the same way with or without commit-graph?  That's\n> inconvenient.  But even without commit-graph, do we guarantee the\n> same traversal order forever?  I doubt it.  So I suspect that it is\n> a brittle workaround to disable commit-graph in the longer term.\n\nHi!\n\nAbout the traversal order, aren't all the graph tests dependent on the\ntraversal order?  If it changed they would all need to be updated\nbecause the tests are hardcoded expects of the graph.\nI guess it might be more brittle than other graph tests specially\nbecause it also depends on removing files, I tried using \"git config\ncore.commitGraph false\" or \"--date-order\" but I still get different\nresults and removing the files fixed it. If someone knows a better way\nof doing it I'm happy to change it.\n\n>\n> As long as the graph engine shows correct graph no matter what order\n> the commits come out of the revision traversal engine, we won't hurt\n> end-users, but we need our tests to be reproducible, so that is a\n> bit unfortunate.\n>\n> Anyway, stepping back a bit,\n>\n> > However having GIT_TEST_COMMIT_GRAPH in the last\n> > text for example changes from:\n> >\n> > * 41_octopus\n> > | * 43_B\n> > |  \\\n> > |   * 43_A\n> > | * 42_B\n> > | * 42_A\n> > * 41_B\n> > * 41_A\n>\n> Does the \"vertically aligned * on 2nd and later columns do not mean\n> any parent-child relationship\" rule no longer apply in this version?\n> IOW, does the above graph show that\n>\n>  - 41_A is a parent of 41_B, which is a parent of 41_octopus\n>  - 42_A is a parent of 42_B, and\n>  - 43_A is a parent of 43_B but is not related to 42_B\n\nYes, this means that all commits vertically adjacent are related,\nthose who are not related and can cause that ambiguity get indented\n(43_A).\n\n>\n> ?  Who are the parents of 41_octopus?  It has no relationship with\n> 42_B and 43_B, and unlike what its name suggests, it has only 41_b\n> as its parent (probably with history simplification that makes only\n> these commits shown)?\n\nOn this test we are using \"--first-parent\" which excludes all the\nparents but the first one, but later we force its excluded parents to\nbe shown.\nWe exclude 42_* and 43_* branches and then force them to appear as\nunrelated branches.\n\n>\n> > to:\n> >\n> > * 41_octopus\n> > * 41_B\n> >  \\\n> >   * 41_A\n> > * 43_B\n> >  \\\n> >   * 43_A\n> > * 42_B\n> > * 42_A\n>\n> And this graph shows the same inter-commit relationship.  So both\n> are correctly showing what we want to express, but they show the\n> same information differently, making test_cmp unhappy?\n\nYes they show the same information.  On the second graph every commit\nis on the first column (or second if they get indented) but on the\nfirst graph we have:\n\n*\n| * <- visual root on second column\n^\n`----- first column remains active\n\nIf you tested v3 with this case you would see that it assumes that\nvisual roots only happen to be rendered on the first column, therefore\nfailing to correctly indent those visual roots on the second column,\nwhich this test proves that they can appear on other columns.\n\nBack to the test:\n\n  * 41_octopus\n  | * 43_B\n  |  \\\n  |   * 43_A\n  | * 42_B\n  | * 42_A\n  * 41_B\n  * 41_A\n\n43_A is rendered on the second column (first column is active by the\n41_* branch) and gets indented to the third one. With commit-graph it\nwould be on the first and get indented to the second, making it the\nsame as more general tests above in \"t4218\", it is an edge case but\nshows that indentation works correctly independently where the visual\nroot is.\n\n>\n> Thanks.\n\nThanks,\n\nPablo\n"},{"id":"545672","messageId":"xmqq8q8e4f3s.fsf@gitster.g","threadId":"65419","inReplyTo":"CAN5EUNR-o_sLzeWuy7M9UMFHBKxSuytNd=4p2svtFuv40E8vZg@mail.gmail.com","subject":"Re: [PATCH v5 2/2] graph: indent visual root in graph","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-16T16:36:23Z","receivedAt":"2026-06-16T16:36:26Z","isPatch":true,"body":"Pablo Sabater <pabloosabaterr@gmail.com> writes:\n\n> Back to the test:\n>\n>   * 41_octopus\n>   | * 43_B\n>   |  \\\n>   |   * 43_A\n>   | * 42_B\n>   | * 42_A\n>   * 41_B\n>   * 41_A\n>\n> 43_A is rendered on the second column (first column is active by the\n> 41_* branch) and gets indented to the third one. With commit-graph it\n> would be on the first and get indented to the second, making it the\n> same as more general tests above in \"t4218\", it is an edge case but\n> shows that indentation works correctly independently where the visual\n> root is.\n\nSounds good.\n"},{"id":"545753","messageId":"CA+J6zkRF8Pm5TGZncO_0=HcVcovJsw2J+3WBfqjCS1CiS1Y_Rg@mail.gmail.com","threadId":"65419","inReplyTo":"20260613-ps-pre-commit-indent-v5-0-8d308efea63d@gmail.com","subject":"Re: [PATCH v5 0/2] graph: indent visual roots in graph","fromName":"Chandra Pratap","fromEmail":"chandrapratap3519@gmail.com","sentAt":"2026-06-17T10:33:16Z","receivedAt":"2026-06-17T10:33:45Z","isPatch":true,"body":"On Sun, 14 Jun 2026 at 00:39, Pablo Sabater <pabloosabaterr@gmail.com> wrote:\n>\n> When rendering a graph, if the history contains multiple \"visual roots\",\n> actual roots or commits that look like roots (i.e. have their parents\n> filtered out) can end up being vertically adjacent to unrelated commits,\n> falsely appearing to be related.\n>\n> A fix for this issue was already attempted [1] a while ago.\n>\n> This series adds indentation to the visual root commits, so they cannot be\n> vertically adjacent anymore making it easier to identify them.\n>\n> before indentation:\n>\n>         * A\n>         * B1\n>         * B2\n>         * C1\n>         * C2\n>\n> after indentation:\n>\n>           * A\n>         * B1\n>          \\\n>           * B2\n>         * C1\n>         * C2\n>\n> Indents the visual root commits that have still commits to show after them, and\n> if they have children it connects them with an edge at a new row.\n>\n> If there are multiple visual roots adjacent in history, the indentation starts\n> with the second one, avoiding redundant indentation of the first one and cascades\n> after the second.\n>\n>         * A\n>           * B\n>             * C\n>         * D1\n>         * D2\n>\n> This series first commit is a cleanup that brings a common function from t4215\n> and t6016 to a graph functions file which they both use, so the new test file\n> for indentation, t4218, can use it as well.\n>\n> The lookahead used to set the cascading and avoid extra indentation is not\n> completely reliable, as the walker goes through the commits it simplifies the\n> history of the current commit and its parents, but it doesn't simplify it\n> for the next unrelated or the grandparents. When the walker simplifies the\n> history, it removes filtered commits from the history and sets its flags.\n> When the next commit is an unrelated commit and its parents will be filtered\n> out, for the lookahead the commit is still a child of, it cannot know that the\n> next commit once simplified (advancing the walker) it will become a visual root.\n> This makes the lookahead fail, failing to set the cascading and starting it\n> with the first visual root, carrying an extra indent for the cascade.\n>\n> given:\n>\n>         * A unrelated (visual root)\n>         * B child of C\n>         * C visual root WILL BE FILTERED OUT\n>         * D unrelated (visual root)\n>\n> the actual output is:\n>\n>           * A\n>             * B\n>         * D\n>\n> A test has been added to t4218 and a NEEDSWORK to the lookahead function to\n> document this edge case but I'm not that familiar with revision.c. Maybe there's\n> a better way to make the lookahead more reliable.\n\nIt's slightly disappointing that we couldn't find a way to fix this\nafter all, but at least the bug is non-breaking and the added\nNEEDSWORK properly documents the issue for someone else\nto tackle in the future.\n\nOther than that, this version looks fine to me.\n\n\n> [1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n>\n> V4 DIFF:\n>\n> - Fixed test to be shown as expected by unsetting COMMIT_GRAPH\n>\n> Signed-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n> ---\n> Pablo Sabater (2):\n>       lib-log-graph: move check_graph function\n>       graph: indent visual root in graph\n>\n>  graph.c                                    | 262 ++++++++++++++++\n>  t/lib-log-graph.sh                         |   5 +\n>  t/meson.build                              |   1 +\n>  t/t4215-log-skewed-merges.sh               |  33 +-\n>  t/t4218-log-graph-indentation.sh           | 467 +++++++++++++++++++++++++++++\n>  t/t6016-rev-list-graph-simplify-history.sh |  25 +-\n>  6 files changed, 759 insertions(+), 34 deletions(-)\n> ---\n> base-commit: 3e65291872de10c3f0bf05ea8c24187e7a71ebf0\n> change-id: 20260612-ps-pre-commit-indent-39ca72816382\n>\n> Best regards,\n> --\n> Pablo Sabater <pabloosabaterr@gmail.com>\n"},{"id":"545798","messageId":"20260617202744.GA3465855@coredump.intra.peff.net","threadId":"65419","inReplyTo":"20260613-ps-pre-commit-indent-v5-2-8d308efea63d@gmail.com","subject":"Re: [PATCH v5 2/2] graph: indent visual root in graph","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-06-17T20:27:44Z","receivedAt":"2026-06-17T20:27:51Z","isPatch":true,"body":"On Sat, Jun 13, 2026 at 09:09:16PM +0200, Pablo Sabater wrote:\n\n> +/*\n> + * Iterates the commits queue searching for the next visible commit, once found\n> + * sets visibleness and visual-root flags.\n> + * Knowing if the next commit is also a visual root avoids redundant indentations\n> + *\n> + * NEEDSWORK: The queue is actively being modified by the walker, for each commit\n> + * its parents and itself get simplified and their flags set, but for the next\n> + * unrelated commit or the grandparents they are not simplified yet, which means\n> + * that a commit whose parents are all filtered will not be marked as a visual\n> + * root candidate at the lookahead.\n> + * This causes the lookahead to fail, failing to set the cascade flag to avoid\n> + * redundant indentations.\n> + * See 'test_expect_failure' at t4218-log-graph-indentation.sh.\n> + */\n> +static void graph_peek_next_visible(struct git_graph *graph,\n> +\t\t\t\t    struct graph_lookahead_flags *flags)\n> +{\n> +\tstruct commit_list *cl;\n> +\n> +\tflags->is_next_visible = 0;\n> +\tflags->is_next_visual_root = 0;\n> +\tflags->next_has_column = 0;\n> +\n> +\tfor (cl = graph->revs->commits; cl; cl = cl->next) {\n> +\t\tif (get_commit_action(graph->revs, cl->item) != commit_show)\n> +\t\t\tcontinue;\n> [...]\n\nI have a feeling this may interact badly with the prio-queue introduced\nby dd4bc01c0a (revision: use priority queue for non-limited streaming\nwalks, 2026-05-27). In that commit, get_revision_1() sucks all of the\ncommits from revs->commits into revs->commit_queue, and then traversal\nputs the parents into that queue, not the commits list.\n\nSo during the traversal, revs->commits does not hold the complete queue\nanymore. I think it does see _some_ commits, since some get placed\ndirectly into revs->commits and then later moved next time\nget_revision() is called. But if we instrument the code like this:\n\ndiff --git a/graph.c b/graph.c\nindex e0d1e2a510..8a5f17a089 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -926,6 +926,10 @@ static void graph_peek_next_visible(struct git_graph *graph,\n \tflags->is_next_visual_root = 0;\n \tflags->next_has_column = 0;\n \n+\twarning(\"peeking at visible commits: %d in list, %d in queue\",\n+\t\tcommit_list_count(graph->revs->commits),\n+\t\t(int)graph->revs->commit_queue.nr);\n+\n \tfor (cl = graph->revs->commits; cl; cl = cl->next) {\n \t\tif (get_commit_action(graph->revs, cl->item) != commit_show)\n \t\t\tcontinue;\n\nand run something like:\n\n  ./git log --graph --oneline -- Makefile\n\nwe can see that we're always considering just one commit, while there\nmay be dozens or hundreds in the queue.\n\nI'm not sure what the solution is. This function wants to peek ahead in\nqueue order, possibly through multiple entries. But a heap-based queue\ninherently only supports peeking at the first entry.\n\nNone of the tests seem to fail, but I'm not sure if that's because I'm\nway off base in my analysis, or there's a gap in the test coverage, or\nif this case is part of the expect_failure ones mentioned in the\ncomment.\n\nI noticed because I have another topic which drops the revs->commits\nlist entirely (and just always uses the queue), which of course doesn't\ncompile when merged with this (I merge with 'jch' for my daily driver,\nwhich now includes this patch).\n\n-Peff\n"},{"id":"545840","messageId":"CAN5EUNSQY2oK7BE4J9Y8APfkP6eJxta050OUu=RoJYhXOjX_OA@mail.gmail.com","threadId":"65419","inReplyTo":"20260617202744.GA3465855@coredump.intra.peff.net","subject":"Re: [PATCH v5 2/2] graph: indent visual root in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-06-18T12:42:16Z","receivedAt":"2026-06-18T12:42:30Z","isPatch":true,"body":"El mié, 17 jun 2026 a las 22:27, Jeff King (<peff@peff.net>) escribió:\n>\n> On Sat, Jun 13, 2026 at 09:09:16PM +0200, Pablo Sabater wrote:\n>\n> > +/*\n> > + * Iterates the commits queue searching for the next visible commit, once found\n> > + * sets visibleness and visual-root flags.\n> > + * Knowing if the next commit is also a visual root avoids redundant indentations\n> > + *\n> > + * NEEDSWORK: The queue is actively being modified by the walker, for each commit\n> > + * its parents and itself get simplified and their flags set, but for the next\n> > + * unrelated commit or the grandparents they are not simplified yet, which means\n> > + * that a commit whose parents are all filtered will not be marked as a visual\n> > + * root candidate at the lookahead.\n> > + * This causes the lookahead to fail, failing to set the cascade flag to avoid\n> > + * redundant indentations.\n> > + * See 'test_expect_failure' at t4218-log-graph-indentation.sh.\n> > + */\n> > +static void graph_peek_next_visible(struct git_graph *graph,\n> > +                                 struct graph_lookahead_flags *flags)\n> > +{\n> > +     struct commit_list *cl;\n> > +\n> > +     flags->is_next_visible = 0;\n> > +     flags->is_next_visual_root = 0;\n> > +     flags->next_has_column = 0;\n> > +\n> > +     for (cl = graph->revs->commits; cl; cl = cl->next) {\n> > +             if (get_commit_action(graph->revs, cl->item) != commit_show)\n> > +                     continue;\n> > [...]\n>\n> I have a feeling this may interact badly with the prio-queue introduced\n> by dd4bc01c0a (revision: use priority queue for non-limited streaming\n> walks, 2026-05-27). In that commit, get_revision_1() sucks all of the\n> commits from revs->commits into revs->commit_queue, and then traversal\n> puts the parents into that queue, not the commits list.\n>\n> So during the traversal, revs->commits does not hold the complete queue\n> anymore. I think it does see _some_ commits, since some get placed\n> directly into revs->commits and then later moved next time\n> get_revision() is called. But if we instrument the code like this:\n>\n> diff --git a/graph.c b/graph.c\n> index e0d1e2a510..8a5f17a089 100644\n> --- a/graph.c\n> +++ b/graph.c\n> @@ -926,6 +926,10 @@ static void graph_peek_next_visible(struct git_graph *graph,\n>         flags->is_next_visual_root = 0;\n>         flags->next_has_column = 0;\n>\n> +       warning(\"peeking at visible commits: %d in list, %d in queue\",\n> +               commit_list_count(graph->revs->commits),\n> +               (int)graph->revs->commit_queue.nr);\n> +\n>         for (cl = graph->revs->commits; cl; cl = cl->next) {\n>                 if (get_commit_action(graph->revs, cl->item) != commit_show)\n>                         continue;\n>\n> and run something like:\n>\n>   ./git log --graph --oneline -- Makefile\n>\n> we can see that we're always considering just one commit, while there\n> may be dozens or hundreds in the queue.\n>\n> I'm not sure what the solution is. This function wants to peek ahead in\n> queue order, possibly through multiple entries. But a heap-based queue\n> inherently only supports peeking at the first entry.\n\nHi Jeff!\n\nYeah, I haven't read dd4bc01c0a yet but from what you say it prob\nwon't work anymore, I didn't know about that series, about the\nlookahead I think it could still work with some tweaks, the important\npart is to set the three lookahead flags.\n\nFrom what I understood, we can only get the direct next commit, but no\nmore reliably ordered.\n\nThe flags should be fine:\n\n- 'is_next_visible' could need to traverse multiple entries, but it\ndoesn't need them to be in order. We just need to know if something\nwill be rendered after.\n- 'next_has_column' only needs the first entry.\n- 'is_next_visual_root' only needs the first entry to know if it could\nbe a visual root, and also if it is not the last one (but we don't\nneed them to be ordered for this last part).\n\nShould I work with 'next' as a base to have dd4bc01c0a? (Sorry I've\njust worked with master).\n\nI'll try to make it work but if not, the lookahead works to avoid\n_redundant_ indentations, but it would still work correctly without\nit.\n\n>\n> None of the tests seem to fail, but I'm not sure if that's because I'm\n> way off base in my analysis, or there's a gap in the test coverage, or\n> if this case is part of the expect_failure ones mentioned in the\n> comment.\n>\n> I noticed because I have another topic which drops the revs->commits\n> list entirely (and just always uses the queue), which of course doesn't\n> compile when merged with this (I merge with 'jch' for my daily driver,\n> which now includes this patch).\n>\n> -Peff\n\nThanks,\nPablo\n"},{"id":"545845","messageId":"xmqq5x3gt1oh.fsf@gitster.g","threadId":"65419","inReplyTo":"CAN5EUNSQY2oK7BE4J9Y8APfkP6eJxta050OUu=RoJYhXOjX_OA@mail.gmail.com","subject":"Re: [PATCH v5 2/2] graph: indent visual root in graph","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-18T13:31:42Z","receivedAt":"2026-06-18T13:31:45Z","isPatch":true,"body":"Pablo Sabater <pabloosabaterr@gmail.com> writes:\n\n> Should I work with 'next' as a base to have dd4bc01c0a? (Sorry I've\n> just worked with master).\n\nAs dd4bc01c (revision: use priority queue for non-limited streaming\nwalks, 2026-05-27) is already in 'master', you should be able to\nwork with 'master' that is no stale than 6e148f82 (Merge branch\n'kk/streaming-walk-pqueue', 2026-06-16).\n"},{"id":"545864","messageId":"20260618160504.GA818042@coredump.intra.peff.net","threadId":"65419","inReplyTo":"CAN5EUNSQY2oK7BE4J9Y8APfkP6eJxta050OUu=RoJYhXOjX_OA@mail.gmail.com","subject":"Re: [PATCH v5 2/2] graph: indent visual root in graph","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-06-18T16:05:04Z","receivedAt":"2026-06-18T16:05:06Z","isPatch":true,"body":"On Thu, Jun 18, 2026 at 02:42:16PM +0200, Pablo Sabater wrote:\n\n> > > +     for (cl = graph->revs->commits; cl; cl = cl->next) {\n> > > +             if (get_commit_action(graph->revs, cl->item) != commit_show)\n> > > +                     continue;\n> [...]\n> > I'm not sure what the solution is. This function wants to peek ahead in\n> > queue order, possibly through multiple entries. But a heap-based queue\n> > inherently only supports peeking at the first entry.\n> \n> Yeah, I haven't read dd4bc01c0a yet but from what you say it prob\n> won't work anymore, I didn't know about that series, about the\n> lookahead I think it could still work with some tweaks, the important\n> part is to set the three lookahead flags.\n\nThanks for looking into it. I meant to also cc the Kristofer, the author\nof dd4bc01c0a, for any thoughts (adding him now).\n\n> From what I understood, we can only get the direct next commit, but no\n> more reliably ordered.\n\nRight. There are other queue implementations that could allow full\nin-order traversal (e.g., a binary tree), but our prio_queue does not. I\nsuspect performance for other cases would suffer if we switched the\nunderlying data structure.\n\n> The flags should be fine:\n> \n> - 'is_next_visible' could need to traverse multiple entries, but it\n> doesn't need them to be in order. We just need to know if something\n> will be rendered after.\n\nYeah, this one seems easy. We are just setting a bit based on whether\nwe'd find any commit to show. So order doesn't matter.\n\n> - 'next_has_column' only needs the first entry.\n\nBut this was the one I was worried about. Walking the linked list in\norder will find us the next commit we're going to show, and the result\nof the flag depends on graph_find_new_column_by_commit(). Is it OK to\nfind _any_ such commit?\n\n(I'm looking at this purely based on reading the existing code, and\nhaven't really thought hard about the problem space).\n\n> - 'is_next_visual_root' only needs the first entry to know if it could\n> be a visual root, and also if it is not the last one (but we don't\n> need them to be ordered for this last part).\n\nThis one just iterates looking for any other commit we'll show after the\nnext one. So finding any two entries would be equivalent to the current\ncode (though we only get to this loop if the first one passes the test\nfor graph_is_visual_root_candidate).\n\nSo if you say order doesn't matter for checking the column and the\nvisual-root-candidate function, I'm happy to believe you. It makes life\nmuch easier. :)\n\n> Should I work with 'next' as a base to have dd4bc01c0a? (Sorry I've\n> just worked with master).\n\nAs Junio noted, that's already in master, so I think you're OK to just\nbase there.\n\nBut for future reference, no, you probably don't want to build off of\n'next'. If your commit has a dependency on another topic it is best to\nbuild directly off of that topic (and note it in the cover letter of the\nseries). That way you do not accidentally depend on other things in\n'next' which might not ever make it to 'master' (and would thus hold\nyour topic hostage).\n\n-Peff\n"},{"id":"545865","messageId":"20260618160743.GA821987@coredump.intra.peff.net","threadId":"65419","inReplyTo":"20260618160504.GA818042@coredump.intra.peff.net","subject":"Re: [PATCH v5 2/2] graph: indent visual root in graph","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-06-18T16:07:43Z","receivedAt":"2026-06-18T16:07:44Z","isPatch":true,"body":"On Thu, Jun 18, 2026 at 12:05:05PM -0400, Jeff King wrote:\n\n> > From what I understood, we can only get the direct next commit, but no\n> > more reliably ordered.\n> \n> Right. There are other queue implementations that could allow full\n> in-order traversal (e.g., a binary tree), but our prio_queue does not. I\n> suspect performance for other cases would suffer if we switched the\n> underlying data structure.\n\nBTW, there's one extra trick here in the iteration: you might see\ncommits in _both_ revs->commits and revs->commit_queue. So you'll have\nto iterate over both of them (and I guess push the loop body into a\nfunction to avoid duplication).\n\nWe may eventually settle on having just one queue sturcture, but I think\ndd4bc01c0a used that to avoid disrupting existing callers.\n\n-Peff\n"},{"id":"545922","messageId":"CAL71e4MAtD4MqE-22UyYaNFVYcFgYmffngihhovEChVfHLmEdA@mail.gmail.com","threadId":"65419","inReplyTo":"20260618160504.GA818042@coredump.intra.peff.net","subject":"Re: [PATCH v5 2/2] graph: indent visual root in graph","fromName":"Kristofer Karlsson","fromEmail":"krka@spotify.com","sentAt":"2026-06-19T07:34:16Z","receivedAt":"2026-06-19T07:34:28Z","isPatch":true,"body":"On Thu, 18 Jun 2026 at 18:05, Jeff King <peff@peff.net> wrote:\n>\n> Thanks for looking into it. I meant to also cc the Kristofer, the author\n> of dd4bc01c0a, for any thoughts (adding him now).\n>\n\nThanks for the CC. I took a look at how this interacts with my\nchange.\n\ndd4bc01c0a doesn't hurt here I think, but future followup changes\nmight. From what I can tell --graph triggers topo_order, so\nthe walk mode is either REV_WALK_TOPO or REV_WALK_LIMITED\nand the prio_queue change only applies to REV_WALK_STREAMING.\n\nThat said, graph_peek_next_visible() reaching directly into\nrevs->commits feels fragile -- especially if we drop revs->commits\nin the future. One option would be to add a thin abstraction in\nrevision.c that dispatches per walk mode, something like:\n\n    int revision_has_more_commits(struct rev_info *revs)\n    {\n        if (revs->topo_walk_info)\n            return revs->topo_walk_info->topo_queue.nr > 0;\n        return revs->commits != NULL;\n    }\n\n    struct commit *revision_peek_next_commit(struct rev_info *revs)\n    {\n        if (revs->topo_walk_info)\n            return prio_queue_peek(&revs->topo_walk_info->topo_queue);\n        if (revs->commits)\n            return revs->commits->item;\n        return NULL;\n    }\n\nThat way graph.c does not need to know which data structure the\nwalker uses, and if the internals change later the API adapts in\none place.\n\nThis would perhaps be an intermediate safety net -- once we have\nfully rolled it out, those functions could be removed again.\n\nAs for the multi-element peek question, I think I would either opt\nfor draining into a buffer if it's really needed, though when looking\nat the code here I think multi-element peeking is not truly needed.\nIt seems like the logic just checks if there is at least another\nelement after the peek, but it does not try to read the actual value,\nso we can just check the queue size instead.\n\nThanks,\nKristofer\n"},{"id":"546016","messageId":"20260620-ps-pre-commit-indent-v6-0-cdc6d8fd5fbc@gmail.com","threadId":"65419","inReplyTo":"20260613-ps-pre-commit-indent-v5-0-8d308efea63d@gmail.com","subject":"[PATCH v6 0/3] graph: indent visual roots in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-06-20T10:11:49Z","receivedAt":"2026-06-20T10:12:00Z","isPatch":true,"body":"When rendering a graph, if the history contains multiple \"visual roots\",\nactual roots or commits that look like roots (i.e. have their parents\nfiltered out) can end up being vertically adjacent to unrelated commits,\nfalsely appearing to be related.\n\nA fix for this issue was already attempted [1] a while ago.\n\nThis series adds indentation to the visual root commits, so they cannot be\nvertically adjacent anymore making it easier to identify them.\n\nbefore indentation:\n\n\t* A\n\t* B1\n\t* B2\n\t* C1\n\t* C2\n\nafter indentation:\n\n\t  * A\n\t* B1\n\t \\\n\t  * B2\n\t* C1\n\t* C2\n\nIndents the visual root commits that have still commits to show after them, and\nif they have children it connects them with an edge at a new row.\n\nIf there are multiple visual roots adjacent in history, the indentation starts\nwith the second one, avoiding redundant indentation of the first one and cascades\nafter the second.\n\n\t* A\n\t  * B\n\t    * C\n\t* D1\n\t* D2\n\nThis series first commit is a cleanup that brings a common function from t4215\nand t6016 to a graph functions file which they both use, so the new test file\nfor indentation, t4218, can use it as well.\n\nThere are two main limitations to predict if the next commit will be a\nvisual root candidate:\n\n1. The peek only gives us the next entry reliably, we cannot see past it\n   reliably in order.\n\n2. Even if we could peek past in order, its parents might not have been\n   simplified yet, so a future commit that will become a visual root is\n   not detected as a visual root in peek-time.\n\nThis causes the cascading to not be set and result in a extra\nindentation. For example:\n\nGiven:\n\n\t* A unrelated (visual root)\n\t* B child of C\n\t* C visual root WILL BE FILTERED OUT\n\t* D unrelated (visual root)\n\nThe actual output is:\n\n\t  * A\n\t    * B\n\t* D\n\nBut we wanted:\n\n\t* A\n\t  * B\n\t* D\n\nA test has been added to t4218 and a NEEDSWORK to the lookahead function\nto document this edge case but I'm not that familiar with revision.c.\nMaybe there's a better way to make the lookahead more reliable.\n\n[1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n\nV5 DIFF:\n\n- Added new commit with lookahead functions to abstract the commit\n  traverse to the graph. Changed the lookahead function from graph to\n  call this new functions.\n- Added new test with two unrelated branches with merges.\n- Fixed test_expect_failure to have the correct expected output.\n- Simplified the NEEDSWORK.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\nPablo Sabater (3):\n      lib-log-graph: move check_graph function\n      revision: add peek functions for lookahead\n      graph: indent visual root in graph\n\n graph.c                                    | 271 +++++++++++++++++\n revision.c                                 |  38 +++\n revision.h                                 |  10 +\n t/lib-log-graph.sh                         |   5 +\n t/meson.build                              |   1 +\n t/t4215-log-skewed-merges.sh               |  33 +-\n t/t4218-log-graph-indentation.sh           | 468 +++++++++++++++++++++++++++++\n t/t6016-rev-list-graph-simplify-history.sh |  25 +-\n 8 files changed, 817 insertions(+), 34 deletions(-)\n---\nbase-commit: 95e20213faefeb95df29277c58ac1980ab68f701\nchange-id: 20260612-ps-pre-commit-indent-39ca72816382\n\nBest regards,\n--  \nPablo Sabater <pabloosabaterr@gmail.com>\n"},{"id":"546017","messageId":"20260620-ps-pre-commit-indent-v6-1-cdc6d8fd5fbc@gmail.com","threadId":"65419","inReplyTo":"20260620-ps-pre-commit-indent-v6-0-cdc6d8fd5fbc@gmail.com","subject":"[PATCH v6 1/3] lib-log-graph: move check_graph function","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-06-20T10:11:50Z","receivedAt":"2026-06-20T10:12:02Z","isPatch":true,"body":"check_graph is a function shared in the test files t4215 and t6016 used\nto format the output graph, but instead of being in a file called by\nboth test, the function code is repeated in each file.\n\nMove check_graph to lib-log-graph.sh file which both tests already\nimport graph functions from, renaming it to lib_test_check_graph.\n\nThis function is needed for the following commit which includes graph\ntests in a new file and requires check_graph.\n\nMentored-by: Karthik Nayak <karthik.188@gmail.com>\nMentored-by: Chandra Pratap <chandrapratap3519@gmail.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n t/lib-log-graph.sh                         |  5 +++++\n t/t4215-log-skewed-merges.sh               | 33 +++++++++++++-----------------\n t/t6016-rev-list-graph-simplify-history.sh | 25 +++++++++-------------\n 3 files changed, 29 insertions(+), 34 deletions(-)\n\ndiff --git a/t/lib-log-graph.sh b/t/lib-log-graph.sh\nindex bf952ef920..1eae8f60c2 100644\n--- a/t/lib-log-graph.sh\n+++ b/t/lib-log-graph.sh\n@@ -26,3 +26,8 @@ lib_test_cmp_colored_graph () {\n \ttest_decode_color <output.colors.raw | sed \"s/ *\\$//\" >output.colors &&\n \ttest_cmp expect.colors output.colors\n }\n+\n+lib_test_check_graph () {\n+\tcat >expect &&\n+\tlib_test_cmp_graph --format=%s \"$@\"\n+}\ndiff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh\nindex 1612f05f1b..eebab71039 100755\n--- a/t/t4215-log-skewed-merges.sh\n+++ b/t/t4215-log-skewed-merges.sh\n@@ -5,11 +5,6 @@ test_description='git log --graph of skewed merges'\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-log-graph.sh\n \n-check_graph () {\n-\tcat >expect &&\n-\tlib_test_cmp_graph --format=%s \"$@\"\n-}\n-\n test_expect_success 'log --graph with merge fusing with its left and right neighbors' '\n \tgit checkout --orphan _p &&\n \ttest_commit A &&\n@@ -21,7 +16,7 @@ test_expect_success 'log --graph with merge fusing with its left and right neigh\n \tgit checkout _p && git merge --no-ff _r -m G &&\n \tgit checkout @^^ && git merge --no-ff _p -m H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   H\n \t|\\\n \t| *   G\n@@ -49,7 +44,7 @@ test_expect_success 'log --graph with left-skewed merge' '\n \tgit checkout 0_p && git merge --no-ff 0_s -m 0_G &&\n \tgit checkout @^ && git merge --no-ff 0_q 0_r 0_t 0_p -m 0_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*-----.   0_H\n \t|\\ \\ \\ \\\n \t| | | | * 0_G\n@@ -83,7 +78,7 @@ test_expect_success 'log --graph with nested left-skewed merge' '\n \tgit checkout 1_p && git merge --no-ff 1_r -m 1_G &&\n \tgit checkout @^^ && git merge --no-ff 1_p -m 1_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   1_H\n \t|\\\n \t| *   1_G\n@@ -115,7 +110,7 @@ test_expect_success 'log --graph with nested left-skewed merge following normal\n \tgit checkout -b 2_s @^^ && git merge --no-ff 2_q -m 2_J &&\n \tgit checkout 2_p && git merge --no-ff 2_s -m 2_K &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   2_K\n \t|\\\n \t| *   2_J\n@@ -151,7 +146,7 @@ test_expect_success 'log --graph with nested right-skewed merge following left-s\n \tgit checkout 3_p && git merge --no-ff 3_r -m 3_H &&\n \tgit checkout @^^ && git merge --no-ff 3_p -m 3_J &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   3_J\n \t|\\\n \t| *   3_H\n@@ -182,7 +177,7 @@ test_expect_success 'log --graph with right-skewed merge following a left-skewed\n \tgit merge --no-ff 4_p -m 4_G &&\n \tgit checkout @^^ && git merge --no-ff 4_s -m 4_H &&\n \n-\tcheck_graph --date-order <<-\\EOF\n+\tlib_test_check_graph --date-order <<-\\EOF\n \t*   4_H\n \t|\\\n \t| *   4_G\n@@ -218,7 +213,7 @@ test_expect_success 'log --graph with octopus merge with column joining its penu\n \tgit checkout 5_r &&\n \tgit merge --no-ff 5_s -m 5_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   5_H\n \t|\\\n \t| *-.   5_G\n@@ -257,7 +252,7 @@ test_expect_success 'log --graph with multiple tips' '\n \tgit checkout 6_1 &&\n \tgit merge --no-ff 6_2 -m 6_I &&\n \n-\tcheck_graph 6_1 6_3 6_5 <<-\\EOF\n+\tlib_test_check_graph 6_1 6_3 6_5 <<-\\EOF\n \t*   6_I\n \t|\\\n \t| | *   6_H\n@@ -334,7 +329,7 @@ test_expect_success 'log --graph with multiple tips' '\n \tgit checkout -b M_7 7_1 &&\n \tgit merge --no-ff 7_2 7_3 -m 7_M4 &&\n \n-\tcheck_graph M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -371,7 +366,7 @@ test_expect_success 'log --graph with multiple tips' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' '\n-\tcheck_graph --graph-lane-limit=2 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=2 M_7 <<-\\EOF\n \t*-.   7_M4\n \t|\\ \\\n \t| | * 7_G\n@@ -388,7 +383,7 @@ test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=1 truncate mid octopus merge' '\n-\tcheck_graph --graph-lane-limit=1 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=1 M_7 <<-\\EOF\n \t*-~  7_M4\n \t|\\~\n \t| ~ 7_G\n@@ -405,7 +400,7 @@ test_expect_success 'log --graph --graph-lane-limit=1 truncate mid octopus merge\n '\n \n test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' '\n-\tcheck_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -441,7 +436,7 @@ test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=6 check if it only shows first of 3 parent merge' '\n-\tcheck_graph --graph-lane-limit=6 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=6 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -478,7 +473,7 @@ test_expect_success 'log --graph --graph-lane-limit=6 check if it only shows fir\n '\n \n test_expect_success 'log --graph --graph-lane-limit=7 check if it shows all 3 parent merge' '\n-\tcheck_graph --graph-lane-limit=7 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=7 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\ndiff --git a/t/t6016-rev-list-graph-simplify-history.sh b/t/t6016-rev-list-graph-simplify-history.sh\nindex 54b0a6f5f8..e0d9c3c1ac 100755\n--- a/t/t6016-rev-list-graph-simplify-history.sh\n+++ b/t/t6016-rev-list-graph-simplify-history.sh\n@@ -13,11 +13,6 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-log-graph.sh\n \n-check_graph () {\n-\tcat >expect &&\n-\tlib_test_cmp_graph --format=%s \"$@\"\n-}\n-\n test_expect_success 'set up rev-list --graph test' '\n \t# 3 commits on branch A\n \ttest_commit A1 foo.txt &&\n@@ -54,7 +49,7 @@ test_expect_success 'set up rev-list --graph test' '\n '\n \n test_expect_success '--graph --all' '\n-\tcheck_graph --all <<-\\EOF\n+\tlib_test_check_graph --all <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -82,7 +77,7 @@ test_expect_success '--graph --all' '\n # that undecorated merges are interesting, even with --simplify-by-decoration\n test_expect_success '--graph --simplify-by-decoration' '\n \tgit tag -d A4 &&\n-\tcheck_graph --all --simplify-by-decoration <<-\\EOF\n+\tlib_test_check_graph --all --simplify-by-decoration <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -114,7 +109,7 @@ test_expect_success 'setup: get rid of decorations on B' '\n \n # Graph with branch B simplified away\n test_expect_success '--graph --simplify-by-decoration prune branch B' '\n-\tcheck_graph --simplify-by-decoration --all <<-\\EOF\n+\tlib_test_check_graph --simplify-by-decoration --all <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -133,7 +128,7 @@ test_expect_success '--graph --simplify-by-decoration prune branch B' '\n '\n \n test_expect_success '--graph --full-history -- bar.txt' '\n-\tcheck_graph --full-history --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --full-history --all -- bar.txt <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -148,7 +143,7 @@ test_expect_success '--graph --full-history -- bar.txt' '\n '\n \n test_expect_success '--graph --full-history --simplify-merges -- bar.txt' '\n-\tcheck_graph --full-history --simplify-merges --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --full-history --simplify-merges --all -- bar.txt <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -161,7 +156,7 @@ test_expect_success '--graph --full-history --simplify-merges -- bar.txt' '\n '\n \n test_expect_success '--graph -- bar.txt' '\n-\tcheck_graph --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --all -- bar.txt <<-\\EOF\n \t* A7\n \t* A5\n \t* A3\n@@ -172,7 +167,7 @@ test_expect_success '--graph -- bar.txt' '\n '\n \n test_expect_success '--graph --sparse -- bar.txt' '\n-\tcheck_graph --sparse --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --sparse --all -- bar.txt <<-\\EOF\n \t* A7\n \t* A6\n \t* A5\n@@ -189,7 +184,7 @@ test_expect_success '--graph --sparse -- bar.txt' '\n '\n \n test_expect_success '--graph ^C4' '\n-\tcheck_graph --all ^C4 <<-\\EOF\n+\tlib_test_check_graph --all ^C4 <<-\\EOF\n \t* A7\n \t* A6\n \t* A5\n@@ -202,7 +197,7 @@ test_expect_success '--graph ^C4' '\n '\n \n test_expect_success '--graph ^C3' '\n-\tcheck_graph --all ^C3 <<-\\EOF\n+\tlib_test_check_graph --all ^C3 <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -220,7 +215,7 @@ test_expect_success '--graph ^C3' '\n # that important, but this test depends on it.  If the ordering ever changes\n # in the code, we'll need to update this test.\n test_expect_success '--graph --boundary ^C3' '\n-\tcheck_graph --boundary --all ^C3 <<-\\EOF\n+\tlib_test_check_graph --boundary --all ^C3 <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n\n-- \n2.54.0\n"},{"id":"546018","messageId":"20260620-ps-pre-commit-indent-v6-2-cdc6d8fd5fbc@gmail.com","threadId":"65419","inReplyTo":"20260620-ps-pre-commit-indent-v6-0-cdc6d8fd5fbc@gmail.com","subject":"[PATCH v6 2/3] revision: add peek functions for lookahead","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-06-20T10:11:51Z","receivedAt":"2026-06-20T10:12:03Z","isPatch":true,"body":"The graph code in a subsequent commit needs to be able to look ahead in\norder to set indentation-related flags.\n\nUsing revs->commits is brittle and the data structure that holds the\npending commits might change in the future.\n\nAdd two functions that abstract this for the graph.\n\nHelped-by: Kristofer Karlsson <stoansen@gmail.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n revision.c | 38 ++++++++++++++++++++++++++++++++++++++\n revision.h | 10 ++++++++++\n 2 files changed, 48 insertions(+)\n\ndiff --git a/revision.c b/revision.c\nindex e91d7e1f11..a472a28853 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -3708,6 +3708,44 @@ static unsigned int count_explore_walked;\n static unsigned int count_indegree_walked;\n static unsigned int count_topo_walked;\n \n+struct commit *revision_peek_next_commit (struct rev_info *revs)\n+{\n+\tstruct topo_walk_info *info = revs->topo_walk_info;\n+\n+\tif (info)\n+\t\treturn prio_queue_peek(&info->topo_queue);\n+\tif (revs->commits)\n+\t\treturn revs->commits->item;\n+\n+\treturn NULL;\n+}\n+\n+int revision_has_commits_after (struct rev_info *revs, int n)\n+{\n+\tstruct topo_walk_info *info = revs->topo_walk_info;\n+\n+\tif (info) {\n+\t\tint visible = 0;\n+\t\tfor (size_t i = 0; i < info->topo_queue.nr && visible < n; i++) {\n+\t\t\tstruct commit *c = info->topo_queue.array[i].data;\n+\t\t\tif (get_commit_action(revs, c) == commit_show)\n+\t\t\t\tvisible++;\n+\t\t}\n+\t\treturn visible > n-1;\n+\t}\n+\tif (revs->commits) {\n+\t\tstruct commit_list *cl;\n+\t\tint visible = 0;\n+\t\tfor (cl = revs->commits; cl && visible < n; cl = cl->next) {\n+\t\t\tif (get_commit_action(revs, cl->item) == commit_show)\n+\t\t\t\tvisible++;\n+\t\t}\n+\t\treturn visible > n-1;\n+\t}\n+\n+\treturn 0;\n+}\n+\n static void trace2_topo_walk_statistics_atexit(void)\n {\n \tstruct json_writer jw = JSON_WRITER_INIT;\ndiff --git a/revision.h b/revision.h\nindex 00c392be37..a10c6b0940 100644\n--- a/revision.h\n+++ b/revision.h\n@@ -572,4 +572,14 @@ int rewrite_parents(struct rev_info *revs,\n  */\n struct commit_list *get_saved_parents(struct rev_info *revs, const struct commit *commit);\n \n+/*\n+ * Peek into revision's next commit without consuming it.\n+ */\n+struct commit *revision_peek_next_commit(struct rev_info *revs);\n+\n+/*\n+ * Check if there are n more commits to be shown yet.\n+ */\n+int revision_has_commits_after(struct rev_info *revs, int n);\n+\n #endif\n\n-- \n2.54.0\n"},{"id":"546019","messageId":"20260620-ps-pre-commit-indent-v6-3-cdc6d8fd5fbc@gmail.com","threadId":"65419","inReplyTo":"20260620-ps-pre-commit-indent-v6-0-cdc6d8fd5fbc@gmail.com","subject":"[PATCH v6 3/3] graph: indent visual root in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-06-20T10:11:52Z","receivedAt":"2026-06-20T10:12:05Z","isPatch":true,"body":"When rendering a graph, if the history contains multiple \"visual roots\",\nactual roots or commits that look like roots (i.e. have their parents\nfiltered out) can end up being vertically adjacent to unrelated commits,\nfalsely appearing to be related.\n\nA fix for this issue was already attempted [1] a while ago.\n\nThis happens because the commits fill the space from left to right and\nwhen a visual root ends, its column becomes free for the following\ncommit even if they are not related. Once this happens the unrelated\ncommit is rendered below the visual root. Because there is no special\ncharacter or way to identify when a visual root is rendered making the\ngraph confusing.\n\nBy indenting the visual roots when there are still commits to show the\nvertical adjacency can be avoided.\n\nAdd is_visual_root flag to git_graph making it visible in all graph states,\ngive graph_update() a new function, graph_is_visual_root() to know if the\ncurrent commit is a visual root and set is_visual_root.\nThe different handled cases are:\n\n- If a visual root has children: similar to GRAPH_PRE_COMMIT state when\n  octopus merges need space, an edge row needs to be printed to connect\n  the child with the indented visual root. A new state GRAPH_PRE_ROOT is\n  needed to connect the child with the visual root:\n\n    * child of the visual root\n     \\ GRAPH_PRE_ROOT\n      * visual root indented\n\n- If a visual root is child-less we can skip GRAPH_PRE_ROOT state and\n  render the indented commit directly.\n\n      * visual root indented\n    * unrelated commit\n\n- If two or more visual roots are adjacent: by having a lookahead to the\n  next commit that will be rendered, if the next commit is also a visual\n  root and we are on a visual root, meaning two visual root adjacent in\n  the history, the top one can omit the indent, making the one below to\n  indent only once, if there are more adjacent visual commits, the\n  indentation will increase for each adjacent one, cascading.\n\n    * visual root\n      * visual root\n        * visual root\n    * last commit\n\n  Even if the last commit is a root, because there is nothing that will be\n  rendered below we can omit the indentation on purpose.\n\nThere are two main limitations to predict if the next commit will be a\nvisual root candidate:\n\n1. The peek only gives us the next entry reliably, we cannot see past it\n   reliably in order.\n\n2. Even if we could peek past in order, its parents might not have been\n   simplified yet, so a future commit that will become a visual root is\n   not detected as a visual root in peek-time.\n\nThis causes the cascading to not be set and result in a extra\nindentation. For example:\n\nGiven:\n\n\t* A unrelated (visual root)\n\t* B child of C\n\t* C visual root WILL BE FILTERED OUT\n\t* D unrelated (visual root)\n\nThe actual output is:\n\n\t  * A\n\t    * B\n\t* D\n\nBut we wanted:\n\n\t* A\n\t  * B\n\t* D\n\nThe output isn't broken as unrelated commits are successfully separated\nby indentation, but an indent level should have been avoided.\n\nCreate a new test file for graph indentations test called\n't4218-log-graph-indentation.sh'.\n\nThe filtered parents edge case is documented as a NEEDSWORK on the\nlookahead function and it has its own 'test_expect_failure' at 't4218'.\n\n[1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n\nMentored-by: Karthik Nayak <karthik.188@gmail.com>\nMentored-by: Chandra Pratap <chandrapratap3519@gmail.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c                          | 271 +++++++++++++++++++++++\n t/meson.build                    |   1 +\n t/t4218-log-graph-indentation.sh | 468 +++++++++++++++++++++++++++++++++++++++\n 3 files changed, 740 insertions(+)\n\ndiff --git a/graph.c b/graph.c\nindex 842282685f..7263aa6283 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -60,12 +60,23 @@ struct column {\n \t * index into column_colors.\n \t */\n \tunsigned short color;\n+\t/*\n+\t * Marks if a commit is a non-first parent of a merge. These columns are\n+\t * already visually connected to the merge commit and do not need\n+\t * indentation.\n+\t *\n+\t * The first parent is the one that inherits the column and it can need\n+\t * indentation if turns out to be a visual root and there's still\n+\t * commits to render.\n+\t */\n+\tunsigned is_merge_parent:1;\n };\n \n enum graph_state {\n \tGRAPH_PADDING,\n \tGRAPH_SKIP,\n \tGRAPH_PRE_COMMIT,\n+\tGRAPH_PRE_ROOT,\n \tGRAPH_COMMIT,\n \tGRAPH_POST_MERGE,\n \tGRAPH_COLLAPSING\n@@ -315,6 +326,48 @@ struct git_graph {\n \t * diff_output_prefix_callback().\n \t */\n \tstruct strbuf prefix_buf;\n+\n+\t/*\n+\t * If a commit is a visual root, we need to indent it to prevent\n+\t * unrelated commits from being vertically adjacent to it.\n+\t */\n+\tunsigned is_visual_root:1;\n+\n+\t/*\n+\t * Indentation increases for each visual root adjacent to another visual\n+\t * root, making visual root commits indentation cascade.\n+\t */\n+\tunsigned int visual_root_depth;\n+\n+\t/*\n+\t * When a visual root is adjacent to other visual roots, the first one\n+\t * can avoid indentation and the rest cascades, increasing the indentation\n+\t * for each one.\n+\t */\n+\tunsigned visual_root_cascade:1;\n+\n+\t/*\n+\t * Set when the current commit was already present in graph->columns\n+\t * before being processed.\n+\t */\n+\tunsigned commit_in_columns:1;\n+};\n+\n+struct graph_lookahead_flags {\n+\t/*\n+\t * Set when there will be a commit after the current one that will be\n+\t * rendered.\n+\t */\n+\tunsigned int is_next_visible:1;\n+\t/*\n+\t * Set when the next visible commit is candidate to be a visual root.\n+\t */\n+\tunsigned int is_next_visual_root:1;\n+\t/*\n+\t * Set when the next visible commit will be rendered under the current\n+\t * commit.\n+\t */\n+\tunsigned int next_has_column:1;\n };\n \n static inline int graph_needs_truncation(struct git_graph *graph, int lane)\n@@ -388,6 +441,8 @@ struct git_graph *graph_init(struct rev_info *opt)\n \tgraph->num_columns = 0;\n \tgraph->num_new_columns = 0;\n \tgraph->mapping_size = 0;\n+\tgraph->visual_root_depth = 0;\n+\tgraph->visual_root_cascade = 0;\n \t/*\n \t * Start the column color at the maximum value, since we'll\n \t * always increment it for the first commit we output.\n@@ -561,6 +616,11 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t\t\t\t\t  struct commit *commit,\n \t\t\t\t\t  int idx)\n {\n+\t/*\n+\t * Get the initial merge_layout before it's modified to know if this\n+\t * is a merge.\n+\t */\n+\tint initial_merge_layout = graph->merge_layout;\n \tint i = graph_find_new_column_by_commit(graph, commit);\n \tint mapping_idx;\n \n@@ -572,6 +632,7 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t\ti = graph->num_new_columns++;\n \t\tgraph->new_columns[i].commit = commit;\n \t\tgraph->new_columns[i].color = graph_find_commit_color(graph, commit);\n+\t\tgraph->new_columns[i].is_merge_parent = 0;\n \t}\n \n \tif (graph->num_parents > 1 && idx > -1 && graph->merge_layout == -1) {\n@@ -610,6 +671,12 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t}\n \n \tgraph->mapping[mapping_idx] = i;\n+\n+\t/*\n+\t * Mark non-first parents of a merge.\n+\t */\n+\tif (graph->num_parents > 1 && initial_merge_layout >= 0 && idx > -1)\n+\t\tgraph->new_columns[i].is_merge_parent = 1;\n }\n \n static void graph_update_columns(struct git_graph *graph)\n@@ -701,10 +768,20 @@ static void graph_update_columns(struct git_graph *graph)\n \t\t\tif (graph->num_parents == 0)\n \t\t\t\tgraph->width += 2;\n \t\t} else {\n+\t\t\tint j;\n \t\t\tgraph_insert_into_new_columns(graph, col_commit, -1);\n+\t\t\t/*\n+\t\t\t * This column is not the current commit, but we need to\n+\t\t\t * propagate the flag until the commit is processed.\n+\t\t\t */\n+\t\t\tj = graph_find_new_column_by_commit(graph, col_commit);\n+\t\t\tif (j >= 0 && graph->columns[i].is_merge_parent)\n+\t\t\t\tgraph->new_columns[j].is_merge_parent = 1;\n \t\t}\n \t}\n \n+\tgraph->commit_in_columns = is_commit_in_columns;\n+\n \t/*\n \t * If graph_max_lanes is set, cap the width\n \t */\n@@ -763,9 +840,144 @@ static int graph_needs_pre_commit_line(struct git_graph *graph)\n \t       graph->expansion_row < graph_num_expansion_rows(graph);\n }\n \n+/*\n+ * A commit can be a visual root when:\n+ * - It has no parents.\n+ *\n+ * - It has parents but they are all filtered out and\n+ *   commit->parents arrives NULL.\n+ *\n+ * - It is not a boundary commit. Boundary commits also have no visible\n+ *   parents, but they are not selected as visual roots because they cannot\n+ *.  cause the ambiguity of being vertically adjacent because:\n+ *\n+ *   1. A boundary only appears because an included commit is its child.\n+ *      Children are always above, and the renderer draws an edge down to\n+ *      the boundary from that child. Rather than starting a column like a\n+ *      visual root would do, it inherits its child column.\n+ *\n+ *   2. Included commits cannot appear below a boundary. Boundaries are\n+ *      ancestors of the exclusion point; if an included commit were an\n+ *      ancestor of the boundary it would be excluded and not rendered.\n+ *      Boundaries therefore always sink to the bottom.\n+ */\n+static int graph_is_visual_root_candidate(struct commit *c)\n+{\n+\treturn c->parents == NULL && !(c->object.flags & BOUNDARY);\n+}\n+\n+static int graph_is_visual_root(struct git_graph *graph,\n+\t\t\t\tstruct graph_lookahead_flags *flags)\n+{\n+\t/*\n+\t * This must be only called for the current commit as graph contains\n+\t * the state for the current commit only.\n+\t *\n+\t * To check if a commit is a visual root, call graph_is_visual_root_candidate()\n+\t * but we won't know if it is really a visual root until we get to the\n+\t * next commit state.\n+\t *\n+\t * The current commit is an actual visual root if it is a candidate and\n+\t * the commit is not a non-first parent of a merge.\n+\t *\n+\t *   *\n+\t *   |\\\n+\t *   | *    <- it is a visual root candidate but it shouldn't be indented\n+\t *   *         because it is already connected by an edge.\n+\t *   ^         if commit_in_columns && is_merge_parent means the commit\n+\t *   |         was put by a merge and is connected.\n+\t *   |\n+\t *   `-------- if !is_next_visible means we're on the last commit, avoid\n+\t *             indentation unless the one before is a visual root, then\n+\t *             we need to differentiate from the one above.\n+\t *\n+\t * If next_has_columns means that the next commit has\n+\t * already a column, so it will not be rendered below, the\n+\t * current commit has to act as the last commit and omit\n+\t * indentation.\n+\t */\n+\treturn graph_is_visual_root_candidate(graph->commit) &&\n+\t       !(graph->commit_in_columns &&\n+\t\t graph->columns[graph->commit_index].is_merge_parent) &&\n+\t       flags->is_next_visible &&\n+\t       (!flags->next_has_column || graph->visual_root_depth > 0);\n+}\n+\n+/*\n+ * Iterates the commits queue searching for the next visible commit, once found\n+ * sets visibleness and visual-root flags.\n+ * Knowing if the next commit is also a visual root avoids redundant indentations\n+ *\n+ * NEEDSWORK: There are two main limitations to predict if the next commit will be\n+ * a visual root candidate:\n+ *\n+ * 1. The peek only gives us the next entry reliably, we cannot see past it\n+ *    reliably in order.\n+ *\n+ * 2. Even if we could peek past in order, its parents might not have been\n+ *    simplified yet, so a future commit that will become a visual root is\n+ *    not detected as a visual root in peek-time.\n+ *\n+ * This results in a redundant indentation.\n+ *\n+ * See 'test_expect_failure' at t4218-log-graph-indentation.sh.\n+ */\n+static void graph_peek_next_visible(struct git_graph *graph,\n+\t\t\t\t    struct graph_lookahead_flags *flags)\n+{\n+\tstruct commit *next;\n+\n+\tflags->is_next_visible = 0;\n+\tflags->is_next_visual_root = 0;\n+\tflags->next_has_column = 0;\n+\n+\tnext = revision_peek_next_commit(graph->revs);\n+\tif (!next)\n+\t\treturn;\n+\n+\tif (get_commit_action(graph->revs, next) != commit_show) {\n+\t\t/*\n+\t\t * next commit won't be shown but there could be a visible\n+\t\t * commit still.\n+\t\t */\n+\t\tif (revision_has_commits_after(graph->revs, 1))\n+\t\t\tflags->is_next_visible = 1;\n+\t\treturn;\n+\t}\n+\n+\tflags->is_next_visible = 1;\n+\tflags->next_has_column = graph_find_new_column_by_commit(graph, next) >= 0;\n+\n+\tif (!graph_is_visual_root_candidate(next))\n+\t\treturn;\n+\t/*\n+\t * Next commit is a visual root candidate but we don't want the last\n+\t * commit to get indented, check if its not the last visible commit\n+\t *\n+\t * We do not need graph->commit_in_columns or is_merge_parent,\n+\t * because we only need to know whether the next one might be a\n+\t * visual root, affecting the current commit where the cascade\n+\t * would have to be set and the first visual root not indented.\n+\t *\n+\t * It will set next_is_visual_root to true for merge parents that\n+\t * graph_is_visual_root() would return false, but if the next is\n+\t * a merge parent, the current commit is the child and cannot\n+\t * be a visual root and therefore having no effect.\n+\t */\n+\tif (revision_has_commits_after(graph->revs, 2))\n+\t\tflags->is_next_visual_root = 1;\n+}\n+\n+static int graph_needs_pre_root_line(struct git_graph *graph)\n+{\n+\treturn graph->commit_in_columns && graph->is_visual_root &&\n+\t       graph->num_columns > 0 && !graph->visual_root_cascade;\n+}\n+\n void graph_update(struct git_graph *graph, struct commit *commit)\n {\n \tstruct commit_list *parent;\n+\tstruct graph_lookahead_flags flags;\n \n \t/*\n \t * Set the new commit\n@@ -796,6 +1008,23 @@ void graph_update(struct git_graph *graph, struct commit *commit)\n \t */\n \tgraph_update_columns(graph);\n \n+\tgraph_peek_next_visible(graph, &flags);\n+\n+\tgraph->is_visual_root = graph_is_visual_root(graph, &flags);\n+\n+\tif (graph->is_visual_root) {\n+\t\t/*\n+\t\t * If next is a visual root we can omit the indent for the first\n+\t\t * visual root and start cascading.\n+\t\t */\n+\t\tif (!graph->visual_root_depth && flags.is_next_visual_root)\n+\t\t\tgraph->visual_root_cascade = 1;\n+\t\tgraph->visual_root_depth++;\n+\t} else {\n+\t\tgraph->visual_root_depth = 0;\n+\t\tgraph->visual_root_cascade = 0;\n+\t}\n+\n \tgraph->expansion_row = 0;\n \n \t/*\n@@ -813,11 +1042,16 @@ void graph_update(struct git_graph *graph, struct commit *commit)\n \t * room for it.  We need to do this only if there is a branch row\n \t * (or more) to the right of this commit.\n \t *\n+\t * If it is a visual root, we need to print an extra row to\n+\t * connect the indentation.\n+\t *\n \t * If there are less than 3 parents, we can immediately print the\n \t * commit line.\n \t */\n \tif (graph->state != GRAPH_PADDING)\n \t\tgraph->state = GRAPH_SKIP;\n+\telse if (graph_needs_pre_root_line(graph))\n+\t\tgraph->state = GRAPH_PRE_ROOT;\n \telse if (graph_needs_pre_commit_line(graph))\n \t\tgraph->state = GRAPH_PRE_COMMIT;\n \telse\n@@ -1065,6 +1299,17 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \n \t\tif (col_commit == graph->commit) {\n \t\t\tseen_this = 1;\n+\t\t\tif (graph->is_visual_root) {\n+\t\t\t\tint depth = graph->visual_root_depth;\n+\t\t\t\t/*\n+\t\t\t\t * Each visual column is 2 characters wide.\n+\t\t\t\t * Omit the indentation for the first visual\n+\t\t\t\t * root in cascade mode.\n+\t\t\t\t */\n+\t\t\t\tint padding = (depth - graph->visual_root_cascade) * 2;\n+\t\t\t\tgraph_line_addchars(line, ' ', padding);\n+\t\t\t\tgraph->width += padding;\n+\t\t\t}\n \t\t\tgraph_output_commit_char(graph, line);\n \n \t\t\tif (graph_needs_truncation(graph, i)) {\n@@ -1436,6 +1681,29 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n \t\tgraph_update_state(graph, GRAPH_PADDING);\n }\n \n+static void graph_output_pre_root_line(struct git_graph *graph, struct graph_line *line)\n+{\n+\t/*\n+\t * This function adds a row before a visual root, to connect the\n+\t * branch to the indented commit. It should only be called on a\n+\t * visual root.\n+\t */\n+\tassert(graph->is_visual_root);\n+\n+\tfor (size_t i = 0; i < graph->num_columns; i++) {\n+\t\tstruct column *col = &graph->columns[i];\n+\t\tif (col->commit == graph->commit) {\n+\t\t\tgraph_line_addch(line, ' ');\n+\t\t\tgraph_line_write_column(line, col, '\\\\');\n+\t\t} else {\n+\t\t\tgraph_line_write_column(line, col, '|');\n+\t\t}\n+\t\tgraph_line_addch(line, ' ');\n+\t}\n+\n+\tgraph_update_state(graph, GRAPH_COMMIT);\n+}\n+\n int graph_next_line(struct git_graph *graph, struct strbuf *sb)\n {\n \tint shown_commit_line = 0;\n@@ -1461,6 +1729,9 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb)\n \tcase GRAPH_PRE_COMMIT:\n \t\tgraph_output_pre_commit_line(graph, &line);\n \t\tbreak;\n+\tcase GRAPH_PRE_ROOT:\n+\t\tgraph_output_pre_root_line(graph, &line);\n+\t\tbreak;\n \tcase GRAPH_COMMIT:\n \t\tgraph_output_commit_line(graph, &line);\n \t\tshown_commit_line = 1;\ndiff --git a/t/meson.build b/t/meson.build\nindex 3219264fe7..6093ff469b 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -576,6 +576,7 @@ integration_tests = [\n   't4215-log-skewed-merges.sh',\n   't4216-log-bloom.sh',\n   't4217-log-limit.sh',\n+  't4218-log-graph-indentation.sh',\n   't4252-am-options.sh',\n   't4253-am-keep-cr-dos.sh',\n   't4254-am-corrupt.sh',\ndiff --git a/t/t4218-log-graph-indentation.sh b/t/t4218-log-graph-indentation.sh\nnew file mode 100755\nindex 0000000000..005ad7c30c\n--- /dev/null\n+++ b/t/t4218-log-graph-indentation.sh\n@@ -0,0 +1,468 @@\n+#!/bin/sh\n+\n+test_description='git log --graph visual root indentations'\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-log-graph.sh\n+\n+check_graph_with_description () {\n+\tcat >expect &&\n+\tlib_test_cmp_graph --format=\"%s%ndescription%nsecond-line\" \"$@\"\n+}\n+\n+create_orphan () {\n+\tgit checkout --orphan \"$1\" &&\n+\t{ git rm -rf . || true; }\n+}\n+\n+# disable commit-graph topo order to have the graph to render in different\n+# ways (used in --first-parent tests to have multiple visual roots while a\n+# column is active at the same time).\n+unset_commit_graph() {\n+\tsane_unset GIT_TEST_COMMIT_GRAPH &&\n+\trm -f .git/objects/info/commit-graph &&\n+\trm -rf .git/objects/info/commit-graphs\n+}\n+\n+test_expect_success 'single root commit is not indented' '\n+\tcreate_orphan _1 && test_commit 1_A &&\n+\tlib_test_check_graph _1 <<-\\EOF\n+\t* 1_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indented before unrelated branch' '\n+\tcreate_orphan _2 && test_commit 2_A && test_commit 2_B &&\n+\tcreate_orphan _3 && test_commit 3_A &&\n+\tlib_test_check_graph _2 _3 <<-\\EOF\n+\t  * 3_A\n+\t* 2_B\n+\t* 2_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indentation with --left-right' '\n+\tlib_test_check_graph --left-right _2..._3 <<-\\EOF\n+\t  > 3_A\n+\t< 2_B\n+\t< 2_A\n+\tEOF\n+'\n+\n+# A better case of why indentation is still needed with '--left-right' flag is\n+# that unrelated branches can be on the same side, so it's needed to\n+# differentiate visual roots on the same side.\n+test_expect_success 'visual root indentation with --left-right having unrelated commits on the same side' '\n+\tlib_test_check_graph --left-right _2..._3 _1 <<-\\EOF\n+\t  > 3_A\n+\t< 2_B\n+\t \\\n+\t  < 2_A\n+\t> 1_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indents the description also' '\n+\tcheck_graph_with_description _2 _3 <<-\\EOF\n+\t  * 3_A\n+\t    description\n+\t    second-line\n+\t* 2_B\n+\t| description\n+\t| second-line\n+\t* 2_A\n+\t  description\n+\t  second-line\n+\tEOF\n+'\n+\n+test_expect_success 'indented visual root parent gets connected to its child' '\n+\tcreate_orphan _4 && test_commit 4_A && test_commit 4_B &&\n+\tcreate_orphan _5 && test_commit 5_A && test_commit 5_B &&\n+\tlib_test_check_graph _4 _5<<-\\EOF\n+\t* 5_B\n+\t \\\n+\t  * 5_A\n+\t* 4_B\n+\t* 4_A\n+\tEOF\n+'\n+\n+test_expect_success 'indented visual root parent gets connected to its child with description' '\n+\tcheck_graph_with_description _4 _5 <<-\\EOF\n+\t* 5_B\n+\t| description\n+\t| second-line\n+\t \\\n+\t  * 5_A\n+\t    description\n+\t    second-line\n+\t* 4_B\n+\t| description\n+\t| second-line\n+\t* 4_A\n+\t  description\n+\t  second-line\n+\tEOF\n+'\n+\n+test_expect_success 'visual roots cascade and last root does not' '\n+\tcreate_orphan _7 && test_commit 7_A && test_commit 7_B &&\n+\tcreate_orphan _8 && test_commit 8_A &&\n+\tcreate_orphan _9 && test_commit 9_A &&\n+\tcreate_orphan _10 && test_commit 10_A &&\n+\tlib_test_check_graph _7 _8 _9 _10  <<-\\EOF\n+\t* 10_A\n+\t  * 9_A\n+\t    * 8_A\n+\t* 7_B\n+\t* 7_A\n+\tEOF\n+'\n+\n+test_expect_success 'last root does not cascade' '\n+\tlib_test_check_graph _8 _9 _10 <<-\\EOF\n+\t* 10_A\n+\t  * 9_A\n+\t* 8_A\n+\tEOF\n+'\n+\n+test_expect_success 'merge parents are roots between them but they do not indent' '\n+\tcreate_orphan _11 && test_commit 11_A &&\n+\tcreate_orphan _12 && test_commit 12_A &&\n+\tcreate_orphan _13 && test_commit 13_A &&\n+\tgit checkout _11 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _11 -p _12 -p _13 -m 11_octopus) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _11 <<-\\EOF\n+\t*-.   11_octopus\n+\t|\\ \\\n+\t| | * 13_A\n+\t| * 12_A\n+\t* 11_A\n+\tEOF\n+'\n+\n+# The last parent of a merge can be indented if nothing related to it needs to\n+# be rendered after, if it's another visual root, merge parent must not get\n+# indented but rather activate cascading.\n+test_expect_success 'merge then unrelated visual root and unrelated branch' '\n+\tcreate_orphan _16 && test_commit 16_A && test_commit 16_B &&\n+\tcreate_orphan _17 && test_commit 17_A &&\n+\tcreate_orphan _18 && test_commit 18_A &&\n+\tcreate_orphan _19 && test_commit 19_A &&\n+\tcreate_orphan _20 && test_commit 20_A &&\n+\tgit checkout _18 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _18 -p _19 -p _20 -m 18_octopus) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _18 _17 _16 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t* 18_A\n+\t  * 17_A\n+\t* 16_B\n+\t* 16_A\n+\tEOF\n+'\n+\n+# The last commit root does not get indented, if the next thing after the root\n+# merge parent is the last commit, indent the merge parent.\n+test_expect_success 'merge then unrelated root indents merge parent' '\n+\tlib_test_check_graph _18 _17 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t \\\n+\t  * 18_A\n+\t* 17_A\n+\tEOF\n+'\n+\n+test_expect_success 'merge then unrelated branch indents merge parent' '\n+\tlib_test_check_graph _18 _16 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t \\\n+\t  * 18_A\n+\t* 16_B\n+\t* 16_A\n+\tEOF\n+'\n+\n+test_expect_success 'two-parent merge of orphans' '\n+\tcreate_orphan _21 && test_commit 21_A &&\n+\tcreate_orphan _22 && test_commit 22_A &&\n+\tgit checkout _21 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _21 -p _22 -m 21_merge) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _21 <<-\\EOF\n+\t*   21_merge\n+\t|\\\n+\t| * 22_A\n+\t* 21_A\n+\tEOF\n+'\n+\n+test_expect_success 'commit with filtered parent becomes a visual root' '\n+\tcreate_orphan _23 &&\n+\techo test >other.txt &&\n+\tgit add other.txt &&\n+\tgit commit -m \"23_A\" &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"23_B\" &&\n+\tcreate_orphan _24 &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"24_A\" &&\n+\tlib_test_check_graph _23 _24 -- foo.txt <<-\\EOF\n+\t  * 23_B\n+\t* 24_A\n+\tEOF\n+'\n+\n+# When a commit's parent will be filtered, the lookahead cannot reliably predict\n+# if the next commit will be shown because the filtering has not happened yet at\n+# peek-time. This makes cascade to not be set causing an extra indentation.\n+#\n+# Expected:\n+#\n+# A\n+#   B\n+# D\n+#\n+# Output:\n+#\n+#   A\n+#     B\n+# D\n+#\n+# This will happen for any case where we find ourselves with the next commit\n+# being a unrelated child of a parent that will be filtered.\n+#\n+# instead of the expected:\n+test_expect_failure 'filtered parent cascading edge case' '\n+\tcreate_orphan _25 &&\n+\techo test >other.txt &&\n+\tgit add other.txt &&\n+\tgit commit -m \"C-filtered\" &&\n+\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"B (child of filtered)\" &&\n+\n+\tcreate_orphan _26 &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"A (visual root)\" &&\n+\n+\tcreate_orphan _27 &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"D (last)\" &&\n+\n+\tlib_test_check_graph _25 _26 _27 -- foo.txt <<-\\EOF\n+\t* B (child of filtered)\n+\t  * A (visual root)\n+\t* D (last)\n+\tEOF\n+'\n+\n+test_expect_failure 'multiple filtered parents in sequence' '\n+\tcreate_orphan _44 &&\n+\techo a >other.txt && git add other.txt && git commit -m \"44_F\" &&\n+\techo b >foo.txt && git add foo.txt && git commit -m \"44_C\" &&\n+\n+\tcreate_orphan _45 &&\n+\techo c >other.txt && git add other.txt && git commit -m \"45_F\" &&\n+\techo d >foo.txt && git add foo.txt && git commit -m \"45_C\" &&\n+\n+\tcreate_orphan _46 &&\n+\techo e >foo.txt && git add foo.txt && git commit -m \"46_A\" &&\n+\n+\tlib_test_check_graph _44 _45 _46 -- foo.txt <<-\\EOF\n+\t* 44_C\n+\t  * 45_C\n+\t* 46_A\n+\tEOF\n+'\n+\n+# This tests prove why there is no need to have indentation for boundary\n+# commits.\n+#\n+# Boundary commits rather than starting a column they 'inherit' the one of\n+# its child so there will always be an edge that connects it removing the\n+# ambiguity.\n+test_expect_success 'unrelated boundaries are not ambiguous' '\n+\tcreate_orphan _28 && test_commit 28_A && test_commit 28_B &&\n+\ttest_commit 28_C &&\n+\tcreate_orphan _29 && test_commit 29_A && test_commit 29_B &&\n+\tlib_test_check_graph --boundary 28_A.._28 29_A.._29 <<-\\EOF\n+\t* 29_B\n+\t| * 28_C\n+\t| * 28_B\n+\t| o 28_A\n+\to 29_A\n+\tEOF\n+'\n+\n+# Same structure as t6016\n+test_expect_success 'boundary commits big test' '\n+\t# 3 commits on branch _30\n+\tcreate_orphan _30 &&\n+\ttest_commit 30_A &&\n+\ttest_commit 30_B &&\n+\ttest_commit 30_C &&\n+\n+\t# 2 commits on branch _31, started from 30_A\n+\tgit checkout -b _31 30_A &&\n+\ttest_commit 31_A &&\n+\ttest_commit 31_B &&\n+\n+\t# 2 commits on branch _32, started from 30_B\n+\tgit checkout -b _32 30_B &&\n+\ttest_commit 32_A &&\n+\ttest_commit 32_B &&\n+\n+\t# Octopus merge _31 and _32 into -30\n+\tgit checkout _30 &&\n+\tgit merge _31 _32 -m 30_D &&\n+\tgit tag 30_D &&\n+\ttest_commit 30_E &&\n+\n+\t# More commits on _32, then merge _32 into _30\n+\tgit checkout _32 &&\n+\ttest_commit 32_C &&\n+\ttest_commit 32_D &&\n+\tgit checkout _30 &&\n+\tgit merge -s ours _32 -m 30_F &&\n+\tgit tag 30_F &&\n+\ttest_commit 30_G &&\n+\tlib_test_check_graph --boundary _30 _31 _32 ^32_C <<-\\EOF\n+\t* 30_G\n+\t*   30_F\n+\t|\\\n+\t| * 32_D\n+\t* | 30_E\n+\t| |\n+\t|  \\\n+\t*-. \\   30_D\n+\t|\\ \\ \\\n+\t| * | | 31_B\n+\t| * | | 31_A\n+\t* | | | 30_C\n+\to | | | 30_B\n+\t|/ / /\n+\to / / 30_A\n+\t / /\n+\t| o 32_C\n+\t|/\n+\to 32_B\n+\tEOF\n+'\n+\n+# Filter by --first-parent and then forcing the filtered parents to be shown.\n+test_expect_success '--first-parent flag with the filtered parents' '\n+\t(\n+\t\tunset_commit_graph &&\n+\t\tcreate_orphan _35 && test_commit 35_A && test_commit 35_B &&\n+\t\tcreate_orphan _36 && test_commit 36_A &&\n+\t\tcreate_orphan _37 && test_commit 37_A &&\n+\t\tgit checkout _35 &&\n+\t\tTREE=$(git write-tree) &&\n+\t\tMERGE=$(git commit-tree $TREE -p _35 -p _36 -p _37 -m 35_octopus) &&\n+\t\tgit reset --hard $MERGE &&\n+\t\tlib_test_check_graph --first-parent _35 _36 _37 <<-\\EOF\n+\t\t* 35_octopus\n+\t\t| * 37_A\n+\t\t|   * 36_A\n+\t\t* 35_B\n+\t\t* 35_A\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success '--first-parent with filtered parents but one has a child' '\n+\t(\n+\t\tunset_commit_graph &&\n+\t\tcreate_orphan _38 && test_commit 38_A && test_commit 38_B &&\n+\t\tcreate_orphan _39 && test_commit 39_A &&\n+\t\tcreate_orphan _40 && test_commit 40_A && test_commit 40_B &&\n+\t\tgit checkout _38 &&\n+\t\tTREE=$(git write-tree) &&\n+\t\tMERGE=$(git commit-tree $TREE -p _38 -p _39 -p _40 -m 38_octopus) &&\n+\t\tgit reset --hard $MERGE &&\n+\t\tlib_test_check_graph --first-parent _38 _39 _40 <<-\\EOF\n+\t\t* 38_octopus\n+\t\t| * 40_B\n+\t\t| * 40_A\n+\t\t|   * 39_A\n+\t\t* 38_B\n+\t\t* 38_A\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success '--first-parent with filtered parents but both have childs' '\n+\t(\n+\t\tunset_commit_graph &&\n+\t\tcreate_orphan _41 && test_commit 41_A && test_commit 41_B &&\n+\t\tcreate_orphan _42 && test_commit 42_A && test_commit 42_B &&\n+\t\tcreate_orphan _43 && test_commit 43_A && test_commit 43_B &&\n+\t\tgit checkout _41 &&\n+\t\tTREE=$(git write-tree) &&\n+\t\tMERGE=$(git commit-tree $TREE -p _41 -p _42 -p _43 -m 41_octopus) &&\n+\t\tgit reset --hard $MERGE &&\n+\t\tlib_test_check_graph --first-parent _41 _42 _43 <<-\\EOF\n+\t\t* 41_octopus\n+\t\t| * 43_B\n+\t\t|  \\\n+\t\t|   * 43_A\n+\t\t| * 42_B\n+\t\t| * 42_A\n+\t\t* 41_B\n+\t\t* 41_A\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'two unrelated merges' '\n+\tcreate_orphan _50 && test_commit 50_A &&\n+\tgit checkout -b _51 &&\n+\ttest_commit 51_A && test_commit 51_B &&\n+\tgit checkout _50 &&\n+\tgit merge --no-ff _51 -m 50_B &&\n+\n+\tcreate_orphan _52 && test_commit 52_A &&\n+\tgit checkout -b _53 &&\n+\ttest_commit 53_A && test_commit 53_B &&\n+\tgit checkout _52 &&\n+\tgit merge --no-ff _53 -m 52_B &&\n+\n+\tlib_test_check_graph _52 _50 <<-\\EOF\n+\t*   52_B\n+\t|\\\n+\t| * 53_B\n+\t| * 53_A\n+\t|/\n+\t \\\n+\t  * 52_A\n+\t*   50_B\n+\t|\\\n+\t| * 51_B\n+\t| * 51_A\n+\t|/\n+\t* 50_A\n+\tEOF\n+'\n+\n+test_done\n\n-- \n2.54.0\n"},{"id":"546020","messageId":"CAN5EUNSj-2hkEBF7N_M6RLsuujDNFNUF3w53zR7SN1_5i2BRyg@mail.gmail.com","threadId":"65419","inReplyTo":"20260620-ps-pre-commit-indent-v6-2-cdc6d8fd5fbc@gmail.com","subject":"Re: [PATCH v6 2/3] revision: add peek functions for lookahead","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-06-20T10:18:28Z","receivedAt":"2026-06-20T10:18:39Z","isPatch":true,"body":"El sáb, 20 jun 2026 a las 12:12, Pablo Sabater\n(<pabloosabaterr@gmail.com>) escribió:\n>\n> The graph code in a subsequent commit needs to be able to look ahead in\n> order to set indentation-related flags.\n>\n> Using revs->commits is brittle and the data structure that holds the\n> pending commits might change in the future.\n>\n> Add two functions that abstract this for the graph.\n>\n> Helped-by: Kristofer Karlsson <stoansen@gmail.com>\n> Signed-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n\nThe email on the trailer is wrong, sorry Kristofer, I'll fix it in the\nnext version.\n\nRegards,\nPablo\n"},{"id":"546032","messageId":"xmqqzf0pfefp.fsf@gitster.g","threadId":"65419","inReplyTo":"20260620-ps-pre-commit-indent-v6-2-cdc6d8fd5fbc@gmail.com","subject":"Re: [PATCH v6 2/3] revision: add peek functions for lookahead","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-20T14:56:42Z","receivedAt":"2026-06-20T14:56:46Z","isPatch":true,"body":"Pablo Sabater <pabloosabaterr@gmail.com> writes:\n\n> The graph code in a subsequent commit needs to be able to look ahead in\n> order to set indentation-related flags.\n>\n> Using revs->commits is brittle and the data structure that holds the\n> pending commits might change in the future.\n>\n> Add two functions that abstract this for the graph.\n>\n> Helped-by: Kristofer Karlsson <stoansen@gmail.com>\n> Signed-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n> ---\n>  revision.c | 38 ++++++++++++++++++++++++++++++++++++++\n>  revision.h | 10 ++++++++++\n>  2 files changed, 48 insertions(+)\n>\n> diff --git a/revision.c b/revision.c\n> index e91d7e1f11..a472a28853 100644\n> --- a/revision.c\n> +++ b/revision.c\n> @@ -3708,6 +3708,44 @@ static unsigned int count_explore_walked;\n>  static unsigned int count_indegree_walked;\n>  static unsigned int count_topo_walked;\n>  \n> +struct commit *revision_peek_next_commit (struct rev_info *revs)\n> +{\n> +\tstruct topo_walk_info *info = revs->topo_walk_info;\n> +\n> +\tif (info)\n> +\t\treturn prio_queue_peek(&info->topo_queue);\n> +\tif (revs->commits)\n> +\t\treturn revs->commits->item;\n> +\n> +\treturn NULL;\n> +}\n\nOK.  \"If we are doing topo_walk, topo_queue is the priority queue to\npeek into, otherwise revs->commits list is being used\" is a bit too\nintimate implementation detail I am not comfortable to depend on,\nbut as long as it is contained inside revision.c it should be OK.\n\nLose the space between the function name and its (parameter list)\nfrom this and the next function.\n\n> +int revision_has_commits_after (struct rev_info *revs, int n)\n> +{\n> +\tstruct topo_walk_info *info = revs->topo_walk_info;\n> +\n> +\tif (info) {\n> +\t\tint visible = 0;\n> +\t\tfor (size_t i = 0; i < info->topo_queue.nr && visible < n; i++) {\n> +\t\t\tstruct commit *c = info->topo_queue.array[i].data;\n> +\t\t\tif (get_commit_action(revs, c) == commit_show)\n> +\t\t\t\tvisible++;\n> +\t\t}\n> +\t\treturn visible > n-1;\n> +\t}\n> +\tif (revs->commits) {\n> +\t\tstruct commit_list *cl;\n> +\t\tint visible = 0;\n> +\t\tfor (cl = revs->commits; cl && visible < n; cl = cl->next) {\n> +\t\t\tif (get_commit_action(revs, cl->item) == commit_show)\n> +\t\t\t\tvisible++;\n> +\t\t}\n> +\t\treturn visible > n-1;\n> +\t}\n> +\n> +\treturn 0;\n> +}\n\nRegarding the use of get_commit_action() here, I wondered if this is\nsafe, because usually get_commit_action() is called only once per\ncommit during history traversal from simplify_commit(), but this\npatch adds calls to it for all of the remaining commits being\nprocessed without consuming them (so get_commit_action() will be\ncalled on these commits again later as the history traversal\nprogresses).\n\nIf get_commit_action() a pure function without any side effects,\nthis may be safe, but line-log has something with side effect in the\nfunction.\n\nI _think_ this is OK, as \"--graph\" sets .rewrite_parents bit (as\nwell as .topo_order bit) on, which makes want_ancestry() to return\ntrue.  Which in turn means even if -L is in effect, we will not call\nline_log_process_ranges_arbitrary_commit() that is the only source\nof side effect in this function.  Somebody needs to sanity check\nthis, but we may want to leave an in-code comment to warn future\ndevelopers not to call get_commit_action() on random commits outside\nof the normal history traversal under what condition (namely, -L\nwithout rewrite_parents).\n\nEven better, perhaps add\n\n\tif (revs->line_level_traverse && !want_ancestry(revs))\n\t\tBUG(\"do not call this\");\n\nat the beginning of revision_has_commits_after() function, and\ndescribe why in the header file comment for this function below?\n\n> diff --git a/revision.h b/revision.h\n> index 00c392be37..a10c6b0940 100644\n> --- a/revision.h\n> +++ b/revision.h\n> @@ -572,4 +572,14 @@ int rewrite_parents(struct rev_info *revs,\n>   */\n>  struct commit_list *get_saved_parents(struct rev_info *revs, const struct commit *commit);\n>  \n> +/*\n> + * Peek into revision's next commit without consuming it.\n> + */\n> +struct commit *revision_peek_next_commit(struct rev_info *revs);\n> +\n> +/*\n> + * Check if there are n more commits to be shown yet.\n> + */\n> +int revision_has_commits_after(struct rev_info *revs, int n);\n> +\n>  #endif\n"},{"id":"546033","messageId":"xmqqtsqxfdl4.fsf@gitster.g","threadId":"65419","inReplyTo":"xmqqzf0pfefp.fsf@gitster.g","subject":"Re: [PATCH v6 2/3] revision: add peek functions for lookahead","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-20T15:15:03Z","receivedAt":"2026-06-20T15:15:07Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> I _think_ this is OK, as \"--graph\" sets .rewrite_parents bit (as\n> well as .topo_order bit) on, which makes want_ancestry() to return\n> true.  Which in turn means even if -L is in effect, we will not call\n> line_log_process_ranges_arbitrary_commit() that is the only source\n> of side effect in this function.  Somebody needs to sanity check\n> this, but we may want to leave an in-code comment to warn future\n> developers not to call get_commit_action() on random commits outside\n> of the normal history traversal under what condition (namely, -L\n> without rewrite_parents).\n>\n> Even better, perhaps add\n>\n> \tif (revs->line_level_traverse && !want_ancestry(revs))\n> \t\tBUG(\"do not call this\");\n>\n> at the beginning of revision_has_commits_after() function, and\n> describe why in the header file comment for this function below?\n\nHaving said all that, in the longer term we might be better off if\nwe fix the line-log code so that get_commit_action() becomes a pure\nfunction again.\n\nIt might be a very simple change to move the \"if we are doing -L and\n!want_ancestry(), call the line_log_process_ranges_arbitrary_commit()\"\nto simplify_commit() before it calls get_commit_action(), but I\nhaven't thought things through.\n\n[jc: Michael Cc'ed as there are a few topics on line-log recently\nfrom him; SZEDER Cc'ed as his 3cb9d2b6 (line-log: more responsive,\nincremental 'git log -L', 2020-05-11) introduced this side effect\nthere.]\n\nIn any case, this is a remote tangent that does not affect how we\nwant to proceed with this series ;-).\n"},{"id":"546049","messageId":"xmqqwlvsek8v.fsf@gitster.g","threadId":"65419","inReplyTo":"20260620-ps-pre-commit-indent-v6-2-cdc6d8fd5fbc@gmail.com","subject":"Re: [PATCH v6 2/3] revision: add peek functions for lookahead","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-21T01:48:48Z","receivedAt":"2026-06-21T01:48:51Z","isPatch":true,"body":"Pablo Sabater <pabloosabaterr@gmail.com> writes:\n\n> +int revision_has_commits_after (struct rev_info *revs, int n)\n> +{\n> +\tstruct topo_walk_info *info = revs->topo_walk_info;\n> +\n> +\tif (info) {\n> +\t\tint visible = 0;\n> +\t\tfor (size_t i = 0; i < info->topo_queue.nr && visible < n; i++) {\n> +\t\t\tstruct commit *c = info->topo_queue.array[i].data;\n> +\t\t\tif (get_commit_action(revs, c) == commit_show)\n> +\t\t\t\tvisible++;\n> +\t\t}\n> +\t\treturn visible > n-1;\n\nThe loop needs to be rethought, perhaps with a better abstraction\nthan \".nr is the number of elements in the queue and we can walk\nthem over as a dense array\", using prio_queue_for_each(), once this\ntopic meets the kk/prio-queue-get-put-fusion topic.\n\nI see Kristofer is already on the CC: line.  Depending on the\ndone-ness of the topic, we may want to include the topic in the\nupdated base for this topic to resolve semantic conflicts early, or\nthe other way around (i.e., let this topic graduate first and then\nrebuild the prio-queue topic on top of the updated 'master').\n\nThanks.\n\n"},{"id":"546065","messageId":"CA+J6zkRbtYu+f52W0+OjgikRGEgcS_nzzeGbdOzUCHZQ3ME-FA@mail.gmail.com","threadId":"65419","inReplyTo":"20260620-ps-pre-commit-indent-v6-2-cdc6d8fd5fbc@gmail.com","subject":"Re: [PATCH v6 2/3] revision: add peek functions for lookahead","fromName":"Chandra Pratap","fromEmail":"chandrapratap3519@gmail.com","sentAt":"2026-06-21T06:42:00Z","receivedAt":"2026-06-21T06:42:28Z","isPatch":true,"body":"> +int revision_has_commits_after (struct rev_info *revs, int n)\n> +{\n> +       struct topo_walk_info *info = revs->topo_walk_info;\n> +\n> +       if (info) {\n> +               int visible = 0;\n> +               for (size_t i = 0; i < info->topo_queue.nr && visible < n; i++) {\n> +                       struct commit *c = info->topo_queue.array[i].data;\n> +                       if (get_commit_action(revs, c) == commit_show)\n> +                               visible++;\n> +               }\n> +               return visible > n-1;\n\nNit: I think 'return visible >= n' will be more readable here. As in,\nmore in-line with this function's description (below).\n\n> +       if (revs->commits) {\n> +               struct commit_list *cl;\n> +               int visible = 0;\n> +               for (cl = revs->commits; cl && visible < n; cl = cl->next) {\n> +                       if (get_commit_action(revs, cl->item) == commit_show)\n> +                               visible++;\n> +               }\n> +               return visible > n-1;\n\nSame here.\n\n> +       }\n> +\n> +       return 0;\n> +}\n> +\n>  static void trace2_topo_walk_statistics_atexit(void)\n>  {\n>         struct json_writer jw = JSON_WRITER_INIT;\n> diff --git a/revision.h b/revision.h\n> index 00c392be37..a10c6b0940 100644\n> --- a/revision.h\n> +++ b/revision.h\n> @@ -572,4 +572,14 @@ int rewrite_parents(struct rev_info *revs,\n>   */\n>  struct commit_list *get_saved_parents(struct rev_info *revs, const struct commit *commit);\n>\n> +/*\n> + * Peek into revision's next commit without consuming it.\n> + */\n> +struct commit *revision_peek_next_commit(struct rev_info *revs);\n> +\n> +/*\n> + * Check if there are n more commits to be shown yet.\n\nShouldn't this be \"n or more\"?\n\n\n> +int revision_has_commits_after(struct rev_info *revs, int n);\n> +\n>  #endif\n>\n> --\n> 2.54.0\n"},{"id":"546083","messageId":"20260621180556.GD2206349@coredump.intra.peff.net","threadId":"65419","inReplyTo":"CAL71e4MAtD4MqE-22UyYaNFVYcFgYmffngihhovEChVfHLmEdA@mail.gmail.com","subject":"Re: [PATCH v5 2/2] graph: indent visual root in graph","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-06-21T18:05:56Z","receivedAt":"2026-06-21T18:05:57Z","isPatch":true,"body":"On Fri, Jun 19, 2026 at 09:34:16AM +0200, Kristofer Karlsson wrote:\n\n> On Thu, 18 Jun 2026 at 18:05, Jeff King <peff@peff.net> wrote:\n> >\n> > Thanks for looking into it. I meant to also cc the Kristofer, the author\n> > of dd4bc01c0a, for any thoughts (adding him now).\n> >\n> \n> Thanks for the CC. I took a look at how this interacts with my\n> change.\n> \n> dd4bc01c0a doesn't hurt here I think, but future followup changes\n> might. From what I can tell --graph triggers topo_order, so\n> the walk mode is either REV_WALK_TOPO or REV_WALK_LIMITED\n> and the prio_queue change only applies to REV_WALK_STREAMING.\n\nI'm not so sure. If I merge 53967f242a (graph: indent visual root in\ngraph, 2026-06-13) into master (so that it has both your commit_queue\nchanges and Pablo's topic), and then apply this:\n\ndiff --git a/graph.c b/graph.c\nindex e0d1e2a510..8a5f17a089 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -926,6 +926,10 @@ static void graph_peek_next_visible(struct git_graph *graph,\n \tflags->is_next_visual_root = 0;\n \tflags->next_has_column = 0;\n \n+\twarning(\"peeking at visible commits: %d in list, %d in queue\",\n+\t\tcommit_list_count(graph->revs->commits),\n+\t\t(int)graph->revs->commit_queue.nr);\n+\n \tfor (cl = graph->revs->commits; cl; cl = cl->next) {\n \t\tif (get_commit_action(graph->revs, cl->item) != commit_show)\n \t\t\tcontinue;\n\nand run:\n\n  ./git log --graph -- Makefile\n\nthen we always see exactly one commit in the list, but an\never-increasing number in the queue (up to ~4000). We do seem to be in\nREV_WALK_TOPO mode, so I think we'd never return the commits via\nget_revision(), but it is weird that we are sticking them in the queue\nat all.\n\nLooks like that happens via rewrite_parents(), which always writes into\ncommit_queue. I guess it doesn't matter because in topo mode we are\nalways pulling off of the topo_walk_info queue anyway? It does make me\nwonder if there is a lurking bug around history simplification and\n--topo-order, though.\n\n> That said, graph_peek_next_visible() reaching directly into\n> revs->commits feels fragile -- especially if we drop revs->commits\n> in the future. One option would be to add a thin abstraction in\n> revision.c that dispatches per walk mode, something like:\n> \n>     int revision_has_more_commits(struct rev_info *revs)\n>     {\n>         if (revs->topo_walk_info)\n>             return revs->topo_walk_info->topo_queue.nr > 0;\n>         return revs->commits != NULL;\n>     }\n> \n>     struct commit *revision_peek_next_commit(struct rev_info *revs)\n>     {\n>         if (revs->topo_walk_info)\n>             return prio_queue_peek(&revs->topo_walk_info->topo_queue);\n>         if (revs->commits)\n>             return revs->commits->item;\n>         return NULL;\n>     }\n> \n> That way graph.c does not need to know which data structure the\n> walker uses, and if the internals change later the API adapts in\n> one place.\n\nYeah, I agree some abstraction would help. I think it would have to be\nfull iteration, though; the graph code wants to know if there is any\ncommit that is actually going to be shown, not just a potential single\nnext one. So we at least need to be able to iterate in arbitrary order.\n\n> As for the multi-element peek question, I think I would either opt\n> for draining into a buffer if it's really needed, though when looking\n> at the code here I think multi-element peeking is not truly needed.\n> It seems like the logic just checks if there is at least another\n> element after the peek, but it does not try to read the actual value,\n> so we can just check the queue size instead.\n\nWe do look at some characteristics of the commit we find by peeking, but\nI'm not sure how much it matters if we get the _next_ commit that will\nbe shown, or if any arbitrary commit is OK.\n\n-Peff\n"},{"id":"546122","messageId":"CAL71e4OQ_kGb+UwHgikHG236-8BVtc7P9OdpV4i4UzYRCoPczw@mail.gmail.com","threadId":"65419","inReplyTo":"20260620-ps-pre-commit-indent-v6-2-cdc6d8fd5fbc@gmail.com","subject":"Re: [PATCH v6 2/3] revision: add peek functions for lookahead","fromName":"Kristofer Karlsson","fromEmail":"krka@spotify.com","sentAt":"2026-06-22T08:28:11Z","receivedAt":"2026-06-22T08:28:22Z","isPatch":true,"body":"> On Sat, 21 Jun 2026, Pablo Sabater <pabloosabaterr@gmail.com> wrote:\n> The graph code in a subsequent commit needs to be able to look ahead in\n> order to set indentation-related flags.\n>\n> Using revs->commits is brittle and the data structure that holds the\n> pending commits might change in the future.\n>\n> Add two functions that abstract this for the graph.\n\nThe abstraction is a step in the right direction, but I think\nthere is a deeper issue with the peek-based approach. I tried\nto understand the problem and ended up with an alternative that\nI think is simpler and also fixes the three test_expect_failure\ncases in t4218.\n\n> +struct commit *revision_peek_next_commit (struct rev_info *revs)\n\n> +int revision_has_commits_after (struct rev_info *revs, int n)\n> +{\n> +               for (size_t i = 0; i < info->topo_queue.nr && visible < n; i++) {\n> +                       struct commit *c = info->topo_queue.array[i].data;\n> +                       if (get_commit_action(revs, c) == commit_show)\n> +                               visible++;\n\nScanning the pending queue does not work, because it may not contain\nall relevant entries yet. Processing the first entry in the queue may\naffect the second entry.\n\nThere is also a second problem: commits in the queue have not\nbeen through simplify_commit() yet, so their parent lists are\nstill the raw ones. graph_is_visual_root_candidate() checks\n\"parents == NULL\", but with a pathspec filter a commit's\nTREESAME parent might get removed by simplification, turning\nthe commit into a visual root. Peeking at the raw queue misses\nthis, which is the cause of the t4218 test_expect_failure cases.\n\nThe solution is to skip peeking entirely and instead call\nget_revision_internal() to populate a small lookahead buffer -\nit only needs two slots.\n\n    struct git_graph {\n        // ...\n        struct commit *lookahead[2];\n        int lookahead_nr;\n    }\n\n    while (revs->graph->lookahead_nr < 2) {\n        struct commit *next = get_revision_internal(revs);\n        if (!next)\n            break;\n        graph_push_lookahead(revs->graph, next);\n    }\n\nAfter prototyping this locally, the three test_expect_failure\ncases in t4218 went away (though I had to do some minor tweaks\nto ensure it become fully deterministic by ticking the commit\ntimestamps.\n\nOne subtlety worth mentioning: get_revision_internal() sets\nSHOWN on commits, so lookahead commits are marked SHOWN before\ngraph_update() processes them. This makes graph_is_interesting()\nthink they are already displayed. The fix is a small check in\ngraph_is_interesting() that recognizes commits in the lookahead\nbuffer as interesting regardless of their SHOWN flag.\n\n    for (i = 0; i < graph->lookahead_nr; i++)\n        if (graph->lookahead[i] == commit)\n            return 1;\n    // other checks after this ...\n\nThis approach ultimately removes the need for\nrevision_peek_next_commit() and revision_has_commits_after()\nentirely - the graph code no longer needs to peek\nat rev_info internals.\n\nKristofer\n"},{"id":"546135","messageId":"CAL71e4NCV5uPJ-LsEQmy2R3gAjF8C60E=YL24tTABaxs+QBSXA@mail.gmail.com","threadId":"65419","inReplyTo":"20260621180556.GD2206349@coredump.intra.peff.net","subject":"Re: [PATCH v5 2/2] graph: indent visual root in graph","fromName":"Kristofer Karlsson","fromEmail":"krka@spotify.com","sentAt":"2026-06-22T08:29:33Z","receivedAt":"2026-06-22T08:29:44Z","isPatch":true,"body":"On Sun, 21 Jun 2026 at 20:05, Jeff King <peff@peff.net> wrote:\n> Looks like that happens via rewrite_parents(), which always writes into\n> commit_queue. I guess it doesn't matter because in topo mode we are\n> always pulling off of the topo_walk_info queue anyway? It does make me\n> wonder if there is a lurking bug around history simplification and\n> --topo-order, though.\n\nThanks for the analysis. You are right that rewrite_one() leaks\nparents into commit_queue that are never consumed in topo mode.\nI have not explored the graph code very much, so I cannot say\nhow this affects the lookahead.\n\nI am thinking that revs->commits is somewhat multi-purpose\n-- it serves as initial tips, work queue, topo sort buffer,\nboundary staging, and reverse output buffer depending on mode\nand phase. Now that we have two representations (commits and\ncommit_queue) it is both multi-purpose and unclear which one to\nuse. That is not a great situation.\n\nI originally just set out to optimize the prio queue usage\nand speed up expensive walks, but I think I also need to be a good\ncitizen and help clean up some of the mess that comes with having two\nseparate containers (I am not sure exactly how yet - maybe even\nadding _more containers but with more semantically clear purpose).\n\n> > As for the multi-element peek question, I think I would either opt\n> > for draining into a buffer if it's really needed, though when looking\n> > at the code here I think multi-element peeking is not truly needed.\n> > It seems like the logic just checks if there is at least another\n> > element after the peek, but it does not try to read the actual value,\n> > so we can just check the queue size instead.\n>\n> We do look at some characteristics of the commit we find by peeking, but\n> I'm not sure how much it matters if we get the _next_ commit that will\n> be shown, or if any arbitrary commit is OK.\n\nI am not sure if arbitrary order is valid - I think simply having an\nintermediate buffer where the filtering has been applied would be sufficient.\nI think the peeking approach is wrong - peeking into the second element\ndoesn't work since after processing the first element the second element\ncould have changed. I prototyped something locally that uses\na lookahead buffer instead and it seems to work and then we don't need\nto manually filter on get_commit_action - get_revision_internal will\napply the right filtering. Will reply with that finding in the right place\nthough.\n\nThanks,\nKristofer\n"},{"id":"546720","messageId":"xmqqechpt3i9.fsf@gitster.g","threadId":"65419","inReplyTo":"CAL71e4OQ_kGb+UwHgikHG236-8BVtc7P9OdpV4i4UzYRCoPczw@mail.gmail.com","subject":"Re: [PATCH v6 2/3] revision: add peek functions for lookahead","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-29T21:56:14Z","receivedAt":"2026-06-29T21:56:16Z","isPatch":true,"body":"Kristofer Karlsson <krka@spotify.com> writes:\n\n> The solution is to skip peeking entirely and instead call\n> get_revision_internal() to populate a small lookahead buffer -\n> it only needs two slots.\n>\n>     struct git_graph {\n>         // ...\n>         struct commit *lookahead[2];\n>         int lookahead_nr;\n>     }\n>\n>     while (revs->graph->lookahead_nr < 2) {\n>         struct commit *next = get_revision_internal(revs);\n>         if (!next)\n>             break;\n>         graph_push_lookahead(revs->graph, next);\n>     }\n>\n> After prototyping this locally, the three test_expect_failure\n> cases in t4218 went away (though I had to do some minor tweaks\n> to ensure it become fully deterministic by ticking the commit\n> timestamps.\n>\n> One subtlety worth mentioning: get_revision_internal() sets\n> SHOWN on commits, so lookahead commits are marked SHOWN before\n> graph_update() processes them. This makes graph_is_interesting()\n> think they are already displayed. The fix is a small check in\n> graph_is_interesting() that recognizes commits in the lookahead\n> buffer as interesting regardless of their SHOWN flag.\n>\n>     for (i = 0; i < graph->lookahead_nr; i++)\n>         if (graph->lookahead[i] == commit)\n>             return 1;\n>     // other checks after this ...\n>\n> This approach ultimately removes the need for\n> revision_peek_next_commit() and revision_has_commits_after()\n> entirely - the graph code no longer needs to peek\n> at rev_info internals.\n\nSorry I lost track, but I think the message I am responding to is\none of the latest messages in the thread.  Whose court is the\nball in right now?\n\nThanks.\n\n"},{"id":"546722","messageId":"CAN5EUNTQV68_eofa7BGb0BukMe=U2d4-FEVmJwW4dObQ2r6LuA@mail.gmail.com","threadId":"65419","inReplyTo":"xmqqechpt3i9.fsf@gitster.g","subject":"Re: [PATCH v6 2/3] revision: add peek functions for lookahead","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-06-29T23:29:14Z","receivedAt":"2026-06-29T23:29:26Z","isPatch":true,"body":"El lun, 29 jun 2026 a las 23:56, Junio C Hamano (<gitster@pobox.com>) escribió:\n>\n> Kristofer Karlsson <krka@spotify.com> writes:\n>\n> > The solution is to skip peeking entirely and instead call\n> > get_revision_internal() to populate a small lookahead buffer -\n> > it only needs two slots.\n> >\n> >     struct git_graph {\n> >         // ...\n> >         struct commit *lookahead[2];\n> >         int lookahead_nr;\n> >     }\n> >\n> >     while (revs->graph->lookahead_nr < 2) {\n> >         struct commit *next = get_revision_internal(revs);\n> >         if (!next)\n> >             break;\n> >         graph_push_lookahead(revs->graph, next);\n> >     }\n> >\n> > After prototyping this locally, the three test_expect_failure\n> > cases in t4218 went away (though I had to do some minor tweaks\n> > to ensure it become fully deterministic by ticking the commit\n> > timestamps.\n> >\n> > One subtlety worth mentioning: get_revision_internal() sets\n> > SHOWN on commits, so lookahead commits are marked SHOWN before\n> > graph_update() processes them. This makes graph_is_interesting()\n> > think they are already displayed. The fix is a small check in\n> > graph_is_interesting() that recognizes commits in the lookahead\n> > buffer as interesting regardless of their SHOWN flag.\n> >\n> >     for (i = 0; i < graph->lookahead_nr; i++)\n> >         if (graph->lookahead[i] == commit)\n> >             return 1;\n> >     // other checks after this ...\n> >\n> > This approach ultimately removes the need for\n> > revision_peek_next_commit() and revision_has_commits_after()\n> > entirely - the graph code no longer needs to peek\n> > at rev_info internals.\n>\n> Sorry I lost track, but I think the message I am responding to is\n> one of the latest messages in the thread.  Whose court is the\n> ball in right now?\n\nHi!\n\nIt's still mine :).\nSorry I haven't worked on this patch these last days, I tested this\nmorning what Kristofer told me to do, but I didn't finish it. I think\nI'll have it by tomorrow.\n\nSo, the next step is a reroll from me.\n\n>\n> Thanks.\n>\n\nThanks for the patience,\nPablo.\n"},{"id":"547128","messageId":"20260704-ps-pre-commit-indent-v7-0-a94706cc8376@gmail.com","threadId":"65419","inReplyTo":"20260620-ps-pre-commit-indent-v6-0-cdc6d8fd5fbc@gmail.com","subject":"[PATCH v7 0/3] graph: indent visual roots in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-04T08:52:32Z","receivedAt":"2026-07-04T08:53:26Z","isPatch":true,"body":"When rendering a graph, if the history contains multiple \"visual roots\",\nactual roots or commits that look like roots (i.e. have their parents\nfiltered out) can end up being vertically adjacent to unrelated commits,\nfalsely appearing to be related.\n\nA fix for this issue was already attempted [1] a while ago.\n\nThis series adds indentation to the visual root commits, so they cannot be\nvertically adjacent anymore making it easier to identify them.\n\nBefore indentation:\n\n\t* A\n\t* B1\n\t* B2\n\t* C1\n\t* C2\n\nAfter indentation:\n\n\t  * A\n\t* B1\n\t \\\n\t  * B2\n\t* C1\n\t* C2\n\nIndents the visual root commits that have still commits to show after\nthem, and if they have children it connects them with an edge at a new\nrow.\n\nIf there are multiple visual roots adjacent in history, the indentation\nstarts with the second one, avoiding redundant indentation of the first\none and cascades after the second.\n\n\t* A\n\t  * B\n\t    * C\n\t* D1\n\t* D2\n\nThis series first commit is a cleanup that brings a common function\nfrom t4215 and t6016 to a graph functions file which they both use, so\nthe new test file for indentation, t4218, can use it as well.\n\n[1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n\nV6 DIFF:\n\n- Replaced the queue peeking with a 2-entry lookahead buffer populated by\n  get_revision_internal() (second commit and graph_peek_next_visible()).\n\n- Changed assert() with BUG() at graph_output_pre_root_line().\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\nPablo Sabater (3):\n      lib-log-graph: move check_graph function\n      graph: add a 2 commit buffer for lookahead\n      graph: indent visual root in graph\n\n graph.c                                    | 282 ++++++++++++++++++\n graph.h                                    |  17 ++\n revision.c                                 |  17 +-\n t/lib-log-graph.sh                         |   5 +\n t/meson.build                              |   1 +\n t/t4215-log-skewed-merges.sh               |  33 +--\n t/t4218-log-graph-indentation.sh           | 453 +++++++++++++++++++++++++++++\n t/t6016-rev-list-graph-simplify-history.sh |  25 +-\n 8 files changed, 798 insertions(+), 35 deletions(-)\n---\nbase-commit: e9019fcafe0040228b8631c30f97ae1adb61bcdc\nchange-id: 20260612-ps-pre-commit-indent-39ca72816382\n\nBest regards,\n--  \nPablo Sabater <pabloosabaterr@gmail.com>\n"},{"id":"547129","messageId":"20260704-ps-pre-commit-indent-v7-1-a94706cc8376@gmail.com","threadId":"65419","inReplyTo":"20260704-ps-pre-commit-indent-v7-0-a94706cc8376@gmail.com","subject":"[PATCH v7 1/3] lib-log-graph: move check_graph function","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-04T08:52:33Z","receivedAt":"2026-07-04T08:53:40Z","isPatch":true,"body":"check_graph is a function shared in the test files t4215 and t6016 used\nto format the output graph, but instead of being in a file called by\nboth test, the function code is repeated in each file.\n\nMove check_graph to lib-log-graph.sh file which both tests already\nimport graph functions from, renaming it to lib_test_check_graph.\n\nThis function is needed for the following commit which includes graph\ntests in a new file and requires check_graph.\n\nMentored-by: Karthik Nayak <karthik.188@gmail.com>\nMentored-by: Chandra Pratap <chandrapratap3519@gmail.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n t/lib-log-graph.sh                         |  5 +++++\n t/t4215-log-skewed-merges.sh               | 33 +++++++++++++-----------------\n t/t6016-rev-list-graph-simplify-history.sh | 25 +++++++++-------------\n 3 files changed, 29 insertions(+), 34 deletions(-)\n\ndiff --git a/t/lib-log-graph.sh b/t/lib-log-graph.sh\nindex bf952ef920..1eae8f60c2 100644\n--- a/t/lib-log-graph.sh\n+++ b/t/lib-log-graph.sh\n@@ -26,3 +26,8 @@ lib_test_cmp_colored_graph () {\n \ttest_decode_color <output.colors.raw | sed \"s/ *\\$//\" >output.colors &&\n \ttest_cmp expect.colors output.colors\n }\n+\n+lib_test_check_graph () {\n+\tcat >expect &&\n+\tlib_test_cmp_graph --format=%s \"$@\"\n+}\ndiff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh\nindex 1612f05f1b..eebab71039 100755\n--- a/t/t4215-log-skewed-merges.sh\n+++ b/t/t4215-log-skewed-merges.sh\n@@ -5,11 +5,6 @@ test_description='git log --graph of skewed merges'\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-log-graph.sh\n \n-check_graph () {\n-\tcat >expect &&\n-\tlib_test_cmp_graph --format=%s \"$@\"\n-}\n-\n test_expect_success 'log --graph with merge fusing with its left and right neighbors' '\n \tgit checkout --orphan _p &&\n \ttest_commit A &&\n@@ -21,7 +16,7 @@ test_expect_success 'log --graph with merge fusing with its left and right neigh\n \tgit checkout _p && git merge --no-ff _r -m G &&\n \tgit checkout @^^ && git merge --no-ff _p -m H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   H\n \t|\\\n \t| *   G\n@@ -49,7 +44,7 @@ test_expect_success 'log --graph with left-skewed merge' '\n \tgit checkout 0_p && git merge --no-ff 0_s -m 0_G &&\n \tgit checkout @^ && git merge --no-ff 0_q 0_r 0_t 0_p -m 0_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*-----.   0_H\n \t|\\ \\ \\ \\\n \t| | | | * 0_G\n@@ -83,7 +78,7 @@ test_expect_success 'log --graph with nested left-skewed merge' '\n \tgit checkout 1_p && git merge --no-ff 1_r -m 1_G &&\n \tgit checkout @^^ && git merge --no-ff 1_p -m 1_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   1_H\n \t|\\\n \t| *   1_G\n@@ -115,7 +110,7 @@ test_expect_success 'log --graph with nested left-skewed merge following normal\n \tgit checkout -b 2_s @^^ && git merge --no-ff 2_q -m 2_J &&\n \tgit checkout 2_p && git merge --no-ff 2_s -m 2_K &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   2_K\n \t|\\\n \t| *   2_J\n@@ -151,7 +146,7 @@ test_expect_success 'log --graph with nested right-skewed merge following left-s\n \tgit checkout 3_p && git merge --no-ff 3_r -m 3_H &&\n \tgit checkout @^^ && git merge --no-ff 3_p -m 3_J &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   3_J\n \t|\\\n \t| *   3_H\n@@ -182,7 +177,7 @@ test_expect_success 'log --graph with right-skewed merge following a left-skewed\n \tgit merge --no-ff 4_p -m 4_G &&\n \tgit checkout @^^ && git merge --no-ff 4_s -m 4_H &&\n \n-\tcheck_graph --date-order <<-\\EOF\n+\tlib_test_check_graph --date-order <<-\\EOF\n \t*   4_H\n \t|\\\n \t| *   4_G\n@@ -218,7 +213,7 @@ test_expect_success 'log --graph with octopus merge with column joining its penu\n \tgit checkout 5_r &&\n \tgit merge --no-ff 5_s -m 5_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   5_H\n \t|\\\n \t| *-.   5_G\n@@ -257,7 +252,7 @@ test_expect_success 'log --graph with multiple tips' '\n \tgit checkout 6_1 &&\n \tgit merge --no-ff 6_2 -m 6_I &&\n \n-\tcheck_graph 6_1 6_3 6_5 <<-\\EOF\n+\tlib_test_check_graph 6_1 6_3 6_5 <<-\\EOF\n \t*   6_I\n \t|\\\n \t| | *   6_H\n@@ -334,7 +329,7 @@ test_expect_success 'log --graph with multiple tips' '\n \tgit checkout -b M_7 7_1 &&\n \tgit merge --no-ff 7_2 7_3 -m 7_M4 &&\n \n-\tcheck_graph M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -371,7 +366,7 @@ test_expect_success 'log --graph with multiple tips' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' '\n-\tcheck_graph --graph-lane-limit=2 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=2 M_7 <<-\\EOF\n \t*-.   7_M4\n \t|\\ \\\n \t| | * 7_G\n@@ -388,7 +383,7 @@ test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=1 truncate mid octopus merge' '\n-\tcheck_graph --graph-lane-limit=1 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=1 M_7 <<-\\EOF\n \t*-~  7_M4\n \t|\\~\n \t| ~ 7_G\n@@ -405,7 +400,7 @@ test_expect_success 'log --graph --graph-lane-limit=1 truncate mid octopus merge\n '\n \n test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' '\n-\tcheck_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -441,7 +436,7 @@ test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=6 check if it only shows first of 3 parent merge' '\n-\tcheck_graph --graph-lane-limit=6 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=6 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -478,7 +473,7 @@ test_expect_success 'log --graph --graph-lane-limit=6 check if it only shows fir\n '\n \n test_expect_success 'log --graph --graph-lane-limit=7 check if it shows all 3 parent merge' '\n-\tcheck_graph --graph-lane-limit=7 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=7 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\ndiff --git a/t/t6016-rev-list-graph-simplify-history.sh b/t/t6016-rev-list-graph-simplify-history.sh\nindex 54b0a6f5f8..e0d9c3c1ac 100755\n--- a/t/t6016-rev-list-graph-simplify-history.sh\n+++ b/t/t6016-rev-list-graph-simplify-history.sh\n@@ -13,11 +13,6 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-log-graph.sh\n \n-check_graph () {\n-\tcat >expect &&\n-\tlib_test_cmp_graph --format=%s \"$@\"\n-}\n-\n test_expect_success 'set up rev-list --graph test' '\n \t# 3 commits on branch A\n \ttest_commit A1 foo.txt &&\n@@ -54,7 +49,7 @@ test_expect_success 'set up rev-list --graph test' '\n '\n \n test_expect_success '--graph --all' '\n-\tcheck_graph --all <<-\\EOF\n+\tlib_test_check_graph --all <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -82,7 +77,7 @@ test_expect_success '--graph --all' '\n # that undecorated merges are interesting, even with --simplify-by-decoration\n test_expect_success '--graph --simplify-by-decoration' '\n \tgit tag -d A4 &&\n-\tcheck_graph --all --simplify-by-decoration <<-\\EOF\n+\tlib_test_check_graph --all --simplify-by-decoration <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -114,7 +109,7 @@ test_expect_success 'setup: get rid of decorations on B' '\n \n # Graph with branch B simplified away\n test_expect_success '--graph --simplify-by-decoration prune branch B' '\n-\tcheck_graph --simplify-by-decoration --all <<-\\EOF\n+\tlib_test_check_graph --simplify-by-decoration --all <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -133,7 +128,7 @@ test_expect_success '--graph --simplify-by-decoration prune branch B' '\n '\n \n test_expect_success '--graph --full-history -- bar.txt' '\n-\tcheck_graph --full-history --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --full-history --all -- bar.txt <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -148,7 +143,7 @@ test_expect_success '--graph --full-history -- bar.txt' '\n '\n \n test_expect_success '--graph --full-history --simplify-merges -- bar.txt' '\n-\tcheck_graph --full-history --simplify-merges --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --full-history --simplify-merges --all -- bar.txt <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -161,7 +156,7 @@ test_expect_success '--graph --full-history --simplify-merges -- bar.txt' '\n '\n \n test_expect_success '--graph -- bar.txt' '\n-\tcheck_graph --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --all -- bar.txt <<-\\EOF\n \t* A7\n \t* A5\n \t* A3\n@@ -172,7 +167,7 @@ test_expect_success '--graph -- bar.txt' '\n '\n \n test_expect_success '--graph --sparse -- bar.txt' '\n-\tcheck_graph --sparse --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --sparse --all -- bar.txt <<-\\EOF\n \t* A7\n \t* A6\n \t* A5\n@@ -189,7 +184,7 @@ test_expect_success '--graph --sparse -- bar.txt' '\n '\n \n test_expect_success '--graph ^C4' '\n-\tcheck_graph --all ^C4 <<-\\EOF\n+\tlib_test_check_graph --all ^C4 <<-\\EOF\n \t* A7\n \t* A6\n \t* A5\n@@ -202,7 +197,7 @@ test_expect_success '--graph ^C4' '\n '\n \n test_expect_success '--graph ^C3' '\n-\tcheck_graph --all ^C3 <<-\\EOF\n+\tlib_test_check_graph --all ^C3 <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -220,7 +215,7 @@ test_expect_success '--graph ^C3' '\n # that important, but this test depends on it.  If the ordering ever changes\n # in the code, we'll need to update this test.\n test_expect_success '--graph --boundary ^C3' '\n-\tcheck_graph --boundary --all ^C3 <<-\\EOF\n+\tlib_test_check_graph --boundary --all ^C3 <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n\n-- \n2.54.0\n"},{"id":"547130","messageId":"20260704-ps-pre-commit-indent-v7-2-a94706cc8376@gmail.com","threadId":"65419","inReplyTo":"20260704-ps-pre-commit-indent-v7-0-a94706cc8376@gmail.com","subject":"[PATCH v7 2/3] graph: add a 2 commit buffer for lookahead","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-04T08:52:34Z","receivedAt":"2026-07-04T08:54:01Z","isPatch":true,"body":"In a subsequent commit the graph renderer needs to know if the next\ncommit is a visual root or if it is the last commit to be shown. This\nrequires peeking 2 commits ahead.\n\nCommits are pre-fetched at get_revision_internal() where they are also\nmarked as SHOWN.\n\nUpdate graph_is_interesting() so it considers commits inside the\nlookahead as interesting as well.\n\nHelped-by: Kristofer Karlsson <krka@spotify.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c    | 47 +++++++++++++++++++++++++++++++++++++++++++++++\n graph.h    | 17 +++++++++++++++++\n revision.c | 17 ++++++++++++++++-\n 3 files changed, 80 insertions(+), 1 deletion(-)\n\ndiff --git a/graph.c b/graph.c\nindex 842282685f..300ae67669 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -315,6 +315,14 @@ struct git_graph {\n \t * diff_output_prefix_callback().\n \t */\n \tstruct strbuf prefix_buf;\n+\n+\t/*\n+\t * Lookahead buffer: up to 2 pre-fetched commits that will be shown.\n+\t * Populated by get_revision() so graph_peek_next_visible() can use\n+\t * actual walk results instead of peeking at rev_info internals.\n+\t */\n+\tstruct commit *lookahead[2];\n+\tint lookahead_nr;\n };\n \n static inline int graph_needs_truncation(struct git_graph *graph, int lane)\n@@ -388,6 +396,9 @@ struct git_graph *graph_init(struct rev_info *opt)\n \tgraph->num_columns = 0;\n \tgraph->num_new_columns = 0;\n \tgraph->mapping_size = 0;\n+\tgraph->lookahead[0] = NULL;\n+\tgraph->lookahead[1] = NULL;\n+\tgraph->lookahead_nr = 0;\n \t/*\n \t * Start the column color at the maximum value, since we'll\n \t * always increment it for the first commit we output.\n@@ -456,6 +467,15 @@ static void graph_ensure_capacity(struct git_graph *graph, int num_columns)\n  */\n static int graph_is_interesting(struct git_graph *graph, struct commit *commit)\n {\n+\t/*\n+\t * Commits in the lookahead buffer have been pre-fetched by\n+\t * get_revision() and will be shown in the future. They already\n+\t * have the SHOWN flag set by get_revision_internal(), but the\n+\t * graph still needs to treat them as interesting parents.\n+\t */\n+\tfor (int i = 0; i < graph->lookahead_nr; i++)\n+\t\tif (graph->lookahead[i] == commit)\n+\t\t\treturn 1;\n \t/*\n \t * If revs->boundary is set, commits whose children have\n \t * been shown are always interesting, even if they have the\n@@ -763,6 +783,33 @@ static int graph_needs_pre_commit_line(struct git_graph *graph)\n \t       graph->expansion_row < graph_num_expansion_rows(graph);\n }\n \n+struct commit *graph_pop_lookahead(struct git_graph *graph)\n+{\n+\tstruct commit *c;\n+\n+\tif (!graph->lookahead_nr)\n+\t\treturn NULL;\n+\n+\tc = graph->lookahead[0];\n+\tgraph->lookahead[0] = graph->lookahead[1];\n+\tgraph->lookahead[1] = NULL;\n+\tgraph->lookahead_nr--;\n+\treturn c;\n+}\n+\n+int graph_get_lookahead_room(struct git_graph *graph)\n+{\n+\treturn 2 - graph->lookahead_nr;\n+}\n+\n+void graph_push_lookahead(struct git_graph *graph, struct commit *c)\n+{\n+\tif (!graph_get_lookahead_room(graph))\n+\t\tBUG(\"pushing into lookahead buffer when it is already full\");\n+\n+\tgraph->lookahead[graph->lookahead_nr++] = c;\n+}\n+\n void graph_update(struct git_graph *graph, struct commit *commit)\n {\n \tstruct commit_list *parent;\ndiff --git a/graph.h b/graph.h\nindex 3fd1dcb2e9..281603b020 100644\n--- a/graph.h\n+++ b/graph.h\n@@ -262,4 +262,21 @@ void graph_show_commit_msg(struct git_graph *graph,\n \t\t\t   FILE *file,\n \t\t\t   struct strbuf const *sb);\n \n+/*\n+ * Pop the first commit from the graph's lookahead buffer.\n+ * Returns NULL if the buffer is empty.\n+ */\n+struct commit *graph_pop_lookahead(struct git_graph *graph);\n+\n+/*\n+ * Returns how many more commits can be added to the lookahead buffer.\n+ */\n+int graph_get_lookahead_room(struct git_graph *graph);\n+\n+/*\n+ * Push a commit into the lookahead buffer. Must only be called when\n+ * graph_lookahead_room() returns > 0.\n+ */\n+void graph_push_lookahead(struct git_graph *graph, struct commit *c);\n+\n #endif /* GRAPH_H */\ndiff --git a/revision.c b/revision.c\nindex e91d7e1f11..58351aeeff 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -4699,12 +4699,27 @@ struct commit *get_revision(struct rev_info *revs)\n \t\t\t\tfor (p = c->parents; p; p = p->next)\n \t\t\t\t\tp->item->object.flags |= CHILD_SHOWN;\n \t\t}\n+\t} else if (revs->graph) {\n+\t\tc = graph_pop_lookahead(revs->graph);\n+\t\tif (!c)\n+\t\t\tc = get_revision_internal(revs);\n+\n \t} else {\n \t\tc = get_revision_internal(revs);\n \t}\n \n-\tif (c && revs->graph)\n+\tif (c && revs->graph) {\n+\t\tif (!revs->max_count_stage && !revs->reverse_output_stage) {\n+\t\t\twhile (graph_get_lookahead_room(revs->graph)) {\n+\t\t\t\tstruct commit *next = get_revision_internal(revs);\n+\t\t\t\tif (!next)\n+\t\t\t\t\tbreak;\n+\t\t\t\tgraph_push_lookahead(revs->graph, next);\n+\t\t\t}\n+\t\t}\n \t\tgraph_update(revs->graph, c);\n+\t}\n+\n \tif (!c) {\n \t\tfree_saved_parents(revs);\n \t\tcommit_list_free(revs->previous_parents);\n\n-- \n2.54.0\n"},{"id":"547131","messageId":"20260704-ps-pre-commit-indent-v7-3-a94706cc8376@gmail.com","threadId":"65419","inReplyTo":"20260704-ps-pre-commit-indent-v7-0-a94706cc8376@gmail.com","subject":"[PATCH v7 3/3] graph: indent visual root in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-04T08:52:35Z","receivedAt":"2026-07-04T08:54:04Z","isPatch":true,"body":"When rendering a graph, if the history contains multiple \"visual roots\",\nactual roots or commits that look like roots (i.e. have their parents\nfiltered out) can end up being vertically adjacent to unrelated commits,\nfalsely appearing to be related.\n\nA fix for this issue was already attempted [1] a while ago.\n\nThis happens because the commits fill the space from left to right and\nwhen a visual root ends, its column becomes free for the following\ncommit even if they are not related. Once this happens the unrelated\ncommit is rendered below the visual root. Because there is no special\ncharacter or way to identify when a visual root is rendered making the\ngraph confusing.\n\nBy indenting the visual roots when there are still commits to show the\nvertical adjacency can be avoided.\n\nAdd is_visual_root flag to git_graph making it visible in all graph states,\ngive graph_update() a new function, graph_is_visual_root() to know if the\ncurrent commit is a visual root and set is_visual_root.\nThe different handled cases are:\n\n- If a visual root has children: similar to GRAPH_PRE_COMMIT state when\n  octopus merges need space, an edge row needs to be printed to connect\n  the child with the indented visual root. A new state GRAPH_PRE_ROOT is\n  needed to connect the child with the visual root:\n\n    * child of the visual root\n     \\ GRAPH_PRE_ROOT\n      * visual root indented\n\n- If a visual root is child-less we can skip GRAPH_PRE_ROOT state and\n  render the indented commit directly.\n\n      * visual root indented\n    * unrelated commit\n\n- If two or more visual roots are adjacent: by having a lookahead to the\n  next commit that will be rendered, if the next commit is also a visual\n  root and we are on a visual root, meaning two visual root adjacent in\n  the history, the top one can omit the indent, making the one below to\n  indent only once, if there are more adjacent visual commits, the\n  indentation will increase for each adjacent one, cascading.\n\n    * visual root\n      * visual root\n        * visual root\n    * last commit\n\n  Even if the last commit is a root, because there is nothing that will be\n  rendered below we can omit the indentation on purpose.\n\n[1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n\nHelp-by: Kristofer Karlsson <krka@spotify.com>\nMentored-by: Karthik Nayak <karthik.188@gmail.com>\nMentored-by: Chandra Pratap <chandrapratap3519@gmail.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c                          | 235 ++++++++++++++++++++\n t/meson.build                    |   1 +\n t/t4218-log-graph-indentation.sh | 453 +++++++++++++++++++++++++++++++++++++++\n 3 files changed, 689 insertions(+)\n\ndiff --git a/graph.c b/graph.c\nindex 300ae67669..75162cea23 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -60,12 +60,23 @@ struct column {\n \t * index into column_colors.\n \t */\n \tunsigned short color;\n+\t/*\n+\t * Marks if a commit is a non-first parent of a merge. These columns are\n+\t * already visually connected to the merge commit and do not need\n+\t * indentation.\n+\t *\n+\t * The first parent is the one that inherits the column and it can need\n+\t * indentation if turns out to be a visual root and there's still\n+\t * commits to render.\n+\t */\n+\tunsigned is_merge_parent:1;\n };\n \n enum graph_state {\n \tGRAPH_PADDING,\n \tGRAPH_SKIP,\n \tGRAPH_PRE_COMMIT,\n+\tGRAPH_PRE_ROOT,\n \tGRAPH_COMMIT,\n \tGRAPH_POST_MERGE,\n \tGRAPH_COLLAPSING\n@@ -323,6 +334,51 @@ struct git_graph {\n \t */\n \tstruct commit *lookahead[2];\n \tint lookahead_nr;\n+\n+\t/*\n+\t * If a commit is a visual root, we need to indent it to prevent\n+\t * unrelated commits from being vertically adjacent to it.\n+\t */\n+\tunsigned is_visual_root:1;\n+\n+\t/*\n+\t * Indentation increases for each visual root adjacent to another visual\n+\t * root, making visual root commits indentation cascade.\n+\t */\n+\tunsigned int visual_root_depth;\n+\n+\t/*\n+\t * When a visual root is adjacent to other visual roots, the first one\n+\t * can avoid indentation and the rest cascades, increasing the indentation\n+\t * for each one.\n+\t */\n+\tunsigned visual_root_cascade:1;\n+\n+\t/*\n+\t * Set when the current commit was already present in graph->columns\n+\t * before being processed.\n+\t */\n+\tunsigned commit_in_columns:1;\n+};\n+\n+struct graph_lookahead_flags {\n+\n+\t/*\n+\t * Set when there will be a commit after the current one that will be\n+\t * rendered.\n+\t */\n+\tunsigned int is_next_visible:1;\n+\n+\t/*\n+\t * Set when the next visible commit is candidate to be a visual root.\n+\t */\n+\tunsigned int is_next_visual_root:1;\n+\n+\t/*\n+\t * Set when the next visible commit will be rendered under the current\n+\t * commit.\n+\t */\n+\tunsigned int next_has_column:1;\n };\n \n static inline int graph_needs_truncation(struct git_graph *graph, int lane)\n@@ -399,6 +455,8 @@ struct git_graph *graph_init(struct rev_info *opt)\n \tgraph->lookahead[0] = NULL;\n \tgraph->lookahead[1] = NULL;\n \tgraph->lookahead_nr = 0;\n+\tgraph->visual_root_depth = 0;\n+\tgraph->visual_root_cascade = 0;\n \t/*\n \t * Start the column color at the maximum value, since we'll\n \t * always increment it for the first commit we output.\n@@ -581,6 +639,11 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t\t\t\t\t  struct commit *commit,\n \t\t\t\t\t  int idx)\n {\n+\t/*\n+\t * Get the initial merge_layout before it's modified to know if this\n+\t * is a merge.\n+\t */\n+\tint initial_merge_layout = graph->merge_layout;\n \tint i = graph_find_new_column_by_commit(graph, commit);\n \tint mapping_idx;\n \n@@ -592,6 +655,7 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t\ti = graph->num_new_columns++;\n \t\tgraph->new_columns[i].commit = commit;\n \t\tgraph->new_columns[i].color = graph_find_commit_color(graph, commit);\n+\t\tgraph->new_columns[i].is_merge_parent = 0;\n \t}\n \n \tif (graph->num_parents > 1 && idx > -1 && graph->merge_layout == -1) {\n@@ -630,6 +694,12 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t}\n \n \tgraph->mapping[mapping_idx] = i;\n+\n+\t/*\n+\t * Mark non-first parents of a merge.\n+\t */\n+\tif (graph->num_parents > 1 && initial_merge_layout >= 0 && idx > -1)\n+\t\tgraph->new_columns[i].is_merge_parent = 1;\n }\n \n static void graph_update_columns(struct git_graph *graph)\n@@ -721,10 +791,20 @@ static void graph_update_columns(struct git_graph *graph)\n \t\t\tif (graph->num_parents == 0)\n \t\t\t\tgraph->width += 2;\n \t\t} else {\n+\t\t\tint j;\n \t\t\tgraph_insert_into_new_columns(graph, col_commit, -1);\n+\t\t\t/*\n+\t\t\t * This column is not the current commit, but we need to\n+\t\t\t * propagate the flag until the commit is processed.\n+\t\t\t */\n+\t\t\tj = graph_find_new_column_by_commit(graph, col_commit);\n+\t\t\tif (j >= 0 && graph->columns[i].is_merge_parent)\n+\t\t\t\tgraph->new_columns[j].is_merge_parent = 1;\n \t\t}\n \t}\n \n+\tgraph->commit_in_columns = is_commit_in_columns;\n+\n \t/*\n \t * If graph_max_lanes is set, cap the width\n \t */\n@@ -810,9 +890,104 @@ void graph_push_lookahead(struct git_graph *graph, struct commit *c)\n \tgraph->lookahead[graph->lookahead_nr++] = c;\n }\n \n+/*\n+ * A commit can be a visual root when:\n+ *\n+ * - It has no parents.\n+ *\n+ * - It has parents but they are all filtered out and\n+ *   commit->parents arrives NULL.\n+ *\n+ * - It is not a boundary commit. Boundary commits also have no visible\n+ *   parents, but they are not selected as visual roots because they cannot\n+ *   cause the ambiguity of being vertically adjacent because:\n+ *\n+ *   1. A boundary only appears because an included commit is its child.\n+ *      Children are always above, and the renderer draws an edge down to\n+ *      the boundary from that child. Rather than starting a column like a\n+ *      visual root would do, it inherits its child column.\n+ *\n+ *   2. Included commits cannot appear below a boundary. Boundaries are\n+ *      ancestors of the exclusion point; if an included commit were an\n+ *      ancestor of the boundary it would be excluded and not rendered.\n+ *      Boundaries therefore always sink to the bottom.\n+ */\n+static int graph_is_visual_root_candidate(struct commit *c)\n+{\n+\treturn c->parents == NULL && !(c->object.flags & BOUNDARY);\n+}\n+\n+static int graph_is_visual_root(struct git_graph *graph,\n+\t\t\t\tstruct graph_lookahead_flags *flags)\n+{\n+\t/*\n+\t * This must be only called for the current commit as graph contains\n+\t * the state for the current commit only.\n+\t *\n+\t * To check if a commit is a visual root, call graph_is_visual_root_candidate()\n+\t * but we won't know if it is really a visual root until we get to the\n+\t * next commit state.\n+\t *\n+\t * The current commit is an actual visual root if it is a candidate and\n+\t * the commit is not a non-first parent of a merge.\n+\t *\n+\t *   *\n+\t *   |\\\n+\t *   | *    <- it is a visual root candidate but it shouldn't be indented\n+\t *   *         because it is already connected by an edge.\n+\t *   ^         if commit_in_columns && is_merge_parent means the commit\n+\t *   |         was put by a merge and is connected.\n+\t *   |\n+\t *   `-------- if !is_next_visible means we're on the last commit, avoid\n+\t *             indentation unless the one before is a visual root, then\n+\t *             we need to differentiate from the one above.\n+\t *\n+\t * If next_has_columns means that the next commit has\n+\t * already a column, so it will not be rendered below, the\n+\t * current commit has to act as the last commit and omit\n+\t * indentation.\n+\t */\n+\treturn graph_is_visual_root_candidate(graph->commit) &&\n+\t       !(graph->commit_in_columns &&\n+\t\t graph->columns[graph->commit_index].is_merge_parent) &&\n+\t       flags->is_next_visible &&\n+\t       (!flags->next_has_column || graph->visual_root_depth > 0);\n+}\n+\n+/*\n+ * Peeks the next commits via the lookahead buffer and sets the lookahead flags.\n+ */\n+static void graph_peek_next_visible(struct git_graph *graph,\n+\t\t\t\t    struct graph_lookahead_flags *flags)\n+{\n+\tflags->is_next_visible = 0;\n+\tflags->is_next_visual_root = 0;\n+\tflags->next_has_column = 0;\n+\n+\tif (!graph->lookahead_nr)\n+\t\treturn;\n+\n+\tflags->is_next_visible = 1;\n+\tflags->next_has_column =\n+\t\tgraph_find_new_column_by_commit(graph, graph->lookahead[0]) >= 0;\n+\n+\tif (!graph_is_visual_root_candidate(graph->lookahead[0]))\n+\t\treturn;\n+\n+\tif (graph->lookahead_nr >= 2)\n+\t\tflags->is_next_visual_root = 1;\n+}\n+\n+static int graph_needs_pre_root_line(struct git_graph *graph)\n+{\n+\treturn graph->commit_in_columns && graph->is_visual_root &&\n+\t       graph->num_columns > 0 && !graph->visual_root_cascade;\n+}\n+\n void graph_update(struct git_graph *graph, struct commit *commit)\n {\n \tstruct commit_list *parent;\n+\tstruct graph_lookahead_flags flags;\n \n \t/*\n \t * Set the new commit\n@@ -843,6 +1018,23 @@ void graph_update(struct git_graph *graph, struct commit *commit)\n \t */\n \tgraph_update_columns(graph);\n \n+\tgraph_peek_next_visible(graph, &flags);\n+\n+\tgraph->is_visual_root = graph_is_visual_root(graph, &flags);\n+\n+\tif (graph->is_visual_root) {\n+\t\t/*\n+\t\t * If next is a visual root we can omit the indent for the first\n+\t\t * visual root and start cascading.\n+\t\t */\n+\t\tif (!graph->visual_root_depth && flags.is_next_visual_root)\n+\t\t\tgraph->visual_root_cascade = 1;\n+\t\tgraph->visual_root_depth++;\n+\t} else {\n+\t\tgraph->visual_root_depth = 0;\n+\t\tgraph->visual_root_cascade = 0;\n+\t}\n+\n \tgraph->expansion_row = 0;\n \n \t/*\n@@ -860,11 +1052,16 @@ void graph_update(struct git_graph *graph, struct commit *commit)\n \t * room for it.  We need to do this only if there is a branch row\n \t * (or more) to the right of this commit.\n \t *\n+\t * If it is a visual root, we need to print an extra row to\n+\t * connect the indentation.\n+\t *\n \t * If there are less than 3 parents, we can immediately print the\n \t * commit line.\n \t */\n \tif (graph->state != GRAPH_PADDING)\n \t\tgraph->state = GRAPH_SKIP;\n+\telse if (graph_needs_pre_root_line(graph))\n+\t\tgraph->state = GRAPH_PRE_ROOT;\n \telse if (graph_needs_pre_commit_line(graph))\n \t\tgraph->state = GRAPH_PRE_COMMIT;\n \telse\n@@ -1112,6 +1309,17 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \n \t\tif (col_commit == graph->commit) {\n \t\t\tseen_this = 1;\n+\t\t\tif (graph->is_visual_root) {\n+\t\t\t\tint depth = graph->visual_root_depth;\n+\t\t\t\t/*\n+\t\t\t\t * Each visual column is 2 characters wide.\n+\t\t\t\t * Omit the indentation for the first visual\n+\t\t\t\t * root in cascade mode.\n+\t\t\t\t */\n+\t\t\t\tint padding = (depth - graph->visual_root_cascade) * 2;\n+\t\t\t\tgraph_line_addchars(line, ' ', padding);\n+\t\t\t\tgraph->width += padding;\n+\t\t\t}\n \t\t\tgraph_output_commit_char(graph, line);\n \n \t\t\tif (graph_needs_truncation(graph, i)) {\n@@ -1483,6 +1691,30 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n \t\tgraph_update_state(graph, GRAPH_PADDING);\n }\n \n+static void graph_output_pre_root_line(struct git_graph *graph, struct graph_line *line)\n+{\n+\t/*\n+\t * This function adds a row before a visual root, to connect the\n+\t * branch to the indented commit. It must only be called on a\n+\t * visual root.\n+\t */\n+\tif (!graph->is_visual_root)\n+\t\tBUG(\"commit must be a visual root to call pre_root_line\");\n+\n+\tfor (size_t i = 0; i < graph->num_columns; i++) {\n+\t\tstruct column *col = &graph->columns[i];\n+\t\tif (col->commit == graph->commit) {\n+\t\t\tgraph_line_addch(line, ' ');\n+\t\t\tgraph_line_write_column(line, col, '\\\\');\n+\t\t} else {\n+\t\t\tgraph_line_write_column(line, col, '|');\n+\t\t}\n+\t\tgraph_line_addch(line, ' ');\n+\t}\n+\n+\tgraph_update_state(graph, GRAPH_COMMIT);\n+}\n+\n int graph_next_line(struct git_graph *graph, struct strbuf *sb)\n {\n \tint shown_commit_line = 0;\n@@ -1508,6 +1740,9 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb)\n \tcase GRAPH_PRE_COMMIT:\n \t\tgraph_output_pre_commit_line(graph, &line);\n \t\tbreak;\n+\tcase GRAPH_PRE_ROOT:\n+\t\tgraph_output_pre_root_line(graph, &line);\n+\t\tbreak;\n \tcase GRAPH_COMMIT:\n \t\tgraph_output_commit_line(graph, &line);\n \t\tshown_commit_line = 1;\ndiff --git a/t/meson.build b/t/meson.build\nindex 3219264fe7..6093ff469b 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -576,6 +576,7 @@ integration_tests = [\n   't4215-log-skewed-merges.sh',\n   't4216-log-bloom.sh',\n   't4217-log-limit.sh',\n+  't4218-log-graph-indentation.sh',\n   't4252-am-options.sh',\n   't4253-am-keep-cr-dos.sh',\n   't4254-am-corrupt.sh',\ndiff --git a/t/t4218-log-graph-indentation.sh b/t/t4218-log-graph-indentation.sh\nnew file mode 100755\nindex 0000000000..c70dab384f\n--- /dev/null\n+++ b/t/t4218-log-graph-indentation.sh\n@@ -0,0 +1,453 @@\n+#!/bin/sh\n+\n+test_description='git log --graph visual root indentations'\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-log-graph.sh\n+\n+check_graph_with_description () {\n+\tcat >expect &&\n+\tlib_test_cmp_graph --format=\"%s%ndescription%nsecond-line\" \"$@\"\n+}\n+\n+create_orphan () {\n+\tgit checkout --orphan \"$1\" &&\n+\t{ git rm -rf . || true; }\n+}\n+\n+# disable commit-graph topo order to have the graph to render in different\n+# ways (used in --first-parent tests to have multiple visual roots while a\n+# column is active at the same time).\n+unset_commit_graph() {\n+\tsane_unset GIT_TEST_COMMIT_GRAPH &&\n+\trm -f .git/objects/info/commit-graph &&\n+\trm -rf .git/objects/info/commit-graphs\n+}\n+\n+test_expect_success 'single root commit is not indented' '\n+\tcreate_orphan _1 && test_commit 1_A &&\n+\tlib_test_check_graph _1 <<-\\EOF\n+\t* 1_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indented before unrelated branch' '\n+\tcreate_orphan _2 && test_commit 2_A && test_commit 2_B &&\n+\tcreate_orphan _3 && test_commit 3_A &&\n+\tlib_test_check_graph _2 _3 <<-\\EOF\n+\t  * 3_A\n+\t* 2_B\n+\t* 2_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indentation with --left-right' '\n+\tlib_test_check_graph --left-right _2..._3 <<-\\EOF\n+\t  > 3_A\n+\t< 2_B\n+\t< 2_A\n+\tEOF\n+'\n+\n+# A better case of why indentation is still needed with '--left-right' flag is\n+# that unrelated branches can be on the same side, so it's needed to\n+# differentiate visual roots on the same side.\n+test_expect_success 'visual root indentation with --left-right having unrelated commits on the same side' '\n+\tlib_test_check_graph --left-right _2..._3 _1 <<-\\EOF\n+\t  > 3_A\n+\t< 2_B\n+\t \\\n+\t  < 2_A\n+\t> 1_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indents the description also' '\n+\tcheck_graph_with_description _2 _3 <<-\\EOF\n+\t  * 3_A\n+\t    description\n+\t    second-line\n+\t* 2_B\n+\t| description\n+\t| second-line\n+\t* 2_A\n+\t  description\n+\t  second-line\n+\tEOF\n+'\n+\n+test_expect_success 'indented visual root parent gets connected to its child' '\n+\tcreate_orphan _4 && test_commit 4_A && test_commit 4_B &&\n+\tcreate_orphan _5 && test_commit 5_A && test_commit 5_B &&\n+\tlib_test_check_graph _4 _5<<-\\EOF\n+\t* 5_B\n+\t \\\n+\t  * 5_A\n+\t* 4_B\n+\t* 4_A\n+\tEOF\n+'\n+\n+test_expect_success 'indented visual root parent gets connected to its child with description' '\n+\tcheck_graph_with_description _4 _5 <<-\\EOF\n+\t* 5_B\n+\t| description\n+\t| second-line\n+\t \\\n+\t  * 5_A\n+\t    description\n+\t    second-line\n+\t* 4_B\n+\t| description\n+\t| second-line\n+\t* 4_A\n+\t  description\n+\t  second-line\n+\tEOF\n+'\n+\n+test_expect_success 'visual roots cascade and last root does not' '\n+\tcreate_orphan _7 && test_commit 7_A && test_commit 7_B &&\n+\tcreate_orphan _8 && test_commit 8_A &&\n+\tcreate_orphan _9 && test_commit 9_A &&\n+\tcreate_orphan _10 && test_commit 10_A &&\n+\tlib_test_check_graph _7 _8 _9 _10  <<-\\EOF\n+\t* 10_A\n+\t  * 9_A\n+\t    * 8_A\n+\t* 7_B\n+\t* 7_A\n+\tEOF\n+'\n+\n+test_expect_success 'last root does not cascade' '\n+\tlib_test_check_graph _8 _9 _10 <<-\\EOF\n+\t* 10_A\n+\t  * 9_A\n+\t* 8_A\n+\tEOF\n+'\n+\n+test_expect_success 'merge parents are roots between them but they do not indent' '\n+\tcreate_orphan _11 && test_commit 11_A &&\n+\tcreate_orphan _12 && test_commit 12_A &&\n+\tcreate_orphan _13 && test_commit 13_A &&\n+\tgit checkout _11 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _11 -p _12 -p _13 -m 11_octopus) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _11 <<-\\EOF\n+\t*-.   11_octopus\n+\t|\\ \\\n+\t| | * 13_A\n+\t| * 12_A\n+\t* 11_A\n+\tEOF\n+'\n+\n+# The last parent of a merge can be indented if nothing related to it needs to\n+# be rendered after, if it's another visual root, merge parent must not get\n+# indented but rather activate cascading.\n+test_expect_success 'merge then unrelated visual root and unrelated branch' '\n+\tcreate_orphan _16 && test_commit 16_A && test_commit 16_B &&\n+\tcreate_orphan _17 && test_commit 17_A &&\n+\tcreate_orphan _18 && test_commit 18_A &&\n+\tcreate_orphan _19 && test_commit 19_A &&\n+\tcreate_orphan _20 && test_commit 20_A &&\n+\tgit checkout _18 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _18 -p _19 -p _20 -m 18_octopus) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _18 _17 _16 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t* 18_A\n+\t  * 17_A\n+\t* 16_B\n+\t* 16_A\n+\tEOF\n+'\n+\n+# The last commit root does not get indented, if the next thing after the root\n+# merge parent is the last commit, indent the merge parent.\n+test_expect_success 'merge then unrelated root indents merge parent' '\n+\tlib_test_check_graph _18 _17 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t \\\n+\t  * 18_A\n+\t* 17_A\n+\tEOF\n+'\n+\n+test_expect_success 'merge then unrelated branch indents merge parent' '\n+\tlib_test_check_graph _18 _16 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t \\\n+\t  * 18_A\n+\t* 16_B\n+\t* 16_A\n+\tEOF\n+'\n+\n+test_expect_success 'two-parent merge of orphans' '\n+\tcreate_orphan _21 && test_commit 21_A &&\n+\tcreate_orphan _22 && test_commit 22_A &&\n+\tgit checkout _21 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _21 -p _22 -m 21_merge) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _21 <<-\\EOF\n+\t*   21_merge\n+\t|\\\n+\t| * 22_A\n+\t* 21_A\n+\tEOF\n+'\n+\n+test_expect_success 'commit with filtered parent becomes a visual root' '\n+\tcreate_orphan _23 &&\n+\techo test >other.txt &&\n+\tgit add other.txt &&\n+\tgit commit -m \"23_A\" &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"23_B\" &&\n+\tcreate_orphan _24 &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"24_A\" &&\n+\tlib_test_check_graph _23 _24 -- foo.txt <<-\\EOF\n+\t  * 23_B\n+\t* 24_A\n+\tEOF\n+'\n+\n+test_expect_success 'filtered parent cascading edge case' '\n+\tcreate_orphan _27 &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\ttest_tick &&\n+\tgit commit -m \"D (last)\" &&\n+\n+\tcreate_orphan _25 &&\n+\techo test >other.txt &&\n+\tgit add other.txt &&\n+\ttest_tick &&\n+\tgit commit -m \"C-filtered\" &&\n+\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\ttest_tick &&\n+\tgit commit -m \"B (child of filtered)\" &&\n+\n+\tcreate_orphan _26 &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\ttest_tick &&\n+\tgit commit -m \"A (visual root)\" &&\n+\n+\n+\tlib_test_check_graph _25 _26 _27 -- foo.txt <<-\\EOF\n+\t* A (visual root)\n+\t  * B (child of filtered)\n+\t* D (last)\n+\tEOF\n+'\n+\n+test_expect_success 'multiple filtered parents in sequence' '\n+\tcreate_orphan _44 &&\n+\techo a >other.txt && git add other.txt && git commit -m \"44_F\" &&\n+\techo b >foo.txt && git add foo.txt && git commit -m \"44_C\" &&\n+\n+\tcreate_orphan _45 &&\n+\techo c >other.txt && git add other.txt && git commit -m \"45_F\" &&\n+\techo d >foo.txt && git add foo.txt && git commit -m \"45_C\" &&\n+\n+\tcreate_orphan _46 &&\n+\techo e >foo.txt && git add foo.txt && git commit -m \"46_A\" &&\n+\n+\tlib_test_check_graph _44 _45 _46 -- foo.txt <<-\\EOF\n+\t* 44_C\n+\t  * 45_C\n+\t* 46_A\n+\tEOF\n+'\n+\n+# This tests prove why there is no need to have indentation for boundary\n+# commits.\n+#\n+# Boundary commits rather than starting a column they 'inherit' the one of\n+# its child so there will always be an edge that connects it removing the\n+# ambiguity.\n+test_expect_success 'unrelated boundaries are not ambiguous' '\n+\tcreate_orphan _28 && test_commit 28_A && test_commit 28_B &&\n+\ttest_commit 28_C &&\n+\tcreate_orphan _29 && test_commit 29_A && test_commit 29_B &&\n+\tlib_test_check_graph --boundary 28_A.._28 29_A.._29 <<-\\EOF\n+\t* 29_B\n+\t| * 28_C\n+\t| * 28_B\n+\t| o 28_A\n+\to 29_A\n+\tEOF\n+'\n+\n+# Same structure as t6016\n+test_expect_success 'boundary commits big test' '\n+\t# 3 commits on branch _30\n+\tcreate_orphan _30 &&\n+\ttest_commit 30_A &&\n+\ttest_commit 30_B &&\n+\ttest_commit 30_C &&\n+\n+\t# 2 commits on branch _31, started from 30_A\n+\tgit checkout -b _31 30_A &&\n+\ttest_commit 31_A &&\n+\ttest_commit 31_B &&\n+\n+\t# 2 commits on branch _32, started from 30_B\n+\tgit checkout -b _32 30_B &&\n+\ttest_commit 32_A &&\n+\ttest_commit 32_B &&\n+\n+\t# Octopus merge _31 and _32 into -30\n+\tgit checkout _30 &&\n+\tgit merge _31 _32 -m 30_D &&\n+\tgit tag 30_D &&\n+\ttest_commit 30_E &&\n+\n+\t# More commits on _32, then merge _32 into _30\n+\tgit checkout _32 &&\n+\ttest_commit 32_C &&\n+\ttest_commit 32_D &&\n+\tgit checkout _30 &&\n+\tgit merge -s ours _32 -m 30_F &&\n+\tgit tag 30_F &&\n+\ttest_commit 30_G &&\n+\tlib_test_check_graph --boundary _30 _31 _32 ^32_C <<-\\EOF\n+\t* 30_G\n+\t*   30_F\n+\t|\\\n+\t| * 32_D\n+\t* | 30_E\n+\t| |\n+\t|  \\\n+\t*-. \\   30_D\n+\t|\\ \\ \\\n+\t| * | | 31_B\n+\t| * | | 31_A\n+\t* | | | 30_C\n+\to | | | 30_B\n+\t|/ / /\n+\to / / 30_A\n+\t / /\n+\t| o 32_C\n+\t|/\n+\to 32_B\n+\tEOF\n+'\n+\n+# Filter by --first-parent and then forcing the filtered parents to be shown.\n+test_expect_success '--first-parent flag with the filtered parents' '\n+\t(\n+\t\tunset_commit_graph &&\n+\t\tcreate_orphan _35 && test_commit 35_A && test_commit 35_B &&\n+\t\tcreate_orphan _36 && test_commit 36_A &&\n+\t\tcreate_orphan _37 && test_commit 37_A &&\n+\t\tgit checkout _35 &&\n+\t\tTREE=$(git write-tree) &&\n+\t\tMERGE=$(git commit-tree $TREE -p _35 -p _36 -p _37 -m 35_octopus) &&\n+\t\tgit reset --hard $MERGE &&\n+\t\tlib_test_check_graph --first-parent _35 _36 _37 <<-\\EOF\n+\t\t* 35_octopus\n+\t\t| * 37_A\n+\t\t|   * 36_A\n+\t\t* 35_B\n+\t\t* 35_A\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success '--first-parent with filtered parents but one has a child' '\n+\t(\n+\t\tunset_commit_graph &&\n+\t\tcreate_orphan _38 && test_commit 38_A && test_commit 38_B &&\n+\t\tcreate_orphan _39 && test_commit 39_A &&\n+\t\tcreate_orphan _40 && test_commit 40_A && test_commit 40_B &&\n+\t\tgit checkout _38 &&\n+\t\tTREE=$(git write-tree) &&\n+\t\tMERGE=$(git commit-tree $TREE -p _38 -p _39 -p _40 -m 38_octopus) &&\n+\t\tgit reset --hard $MERGE &&\n+\t\tlib_test_check_graph --first-parent _38 _39 _40 <<-\\EOF\n+\t\t* 38_octopus\n+\t\t| * 40_B\n+\t\t| * 40_A\n+\t\t|   * 39_A\n+\t\t* 38_B\n+\t\t* 38_A\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success '--first-parent with filtered parents but both have childs' '\n+\t(\n+\t\tunset_commit_graph &&\n+\t\tcreate_orphan _41 && test_commit 41_A && test_commit 41_B &&\n+\t\tcreate_orphan _42 && test_commit 42_A && test_commit 42_B &&\n+\t\tcreate_orphan _43 && test_commit 43_A && test_commit 43_B &&\n+\t\tgit checkout _41 &&\n+\t\tTREE=$(git write-tree) &&\n+\t\tMERGE=$(git commit-tree $TREE -p _41 -p _42 -p _43 -m 41_octopus) &&\n+\t\tgit reset --hard $MERGE &&\n+\t\tlib_test_check_graph --first-parent _41 _42 _43 <<-\\EOF\n+\t\t* 41_octopus\n+\t\t| * 43_B\n+\t\t|  \\\n+\t\t|   * 43_A\n+\t\t| * 42_B\n+\t\t| * 42_A\n+\t\t* 41_B\n+\t\t* 41_A\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'two unrelated merges' '\n+\tcreate_orphan _50 && test_commit 50_A &&\n+\tgit checkout -b _51 &&\n+\ttest_commit 51_A && test_commit 51_B &&\n+\tgit checkout _50 &&\n+\tgit merge --no-ff _51 -m 50_B &&\n+\n+\tcreate_orphan _52 && test_commit 52_A &&\n+\tgit checkout -b _53 &&\n+\ttest_commit 53_A && test_commit 53_B &&\n+\tgit checkout _52 &&\n+\tgit merge --no-ff _53 -m 52_B &&\n+\n+\tlib_test_check_graph _52 _50 <<-\\EOF\n+\t*   52_B\n+\t|\\\n+\t| * 53_B\n+\t| * 53_A\n+\t|/\n+\t \\\n+\t  * 52_A\n+\t*   50_B\n+\t|\\\n+\t| * 51_B\n+\t| * 51_A\n+\t|/\n+\t* 50_A\n+\tEOF\n+'\n+\n+test_done\n\n-- \n2.54.0\n"},{"id":"547208","messageId":"CA+J6zkQFsTA3QfU5VVjQ=KhJCg_pCrTgW9zinAUC4D9YwsyOkQ@mail.gmail.com","threadId":"65419","inReplyTo":"20260704-ps-pre-commit-indent-v7-2-a94706cc8376@gmail.com","subject":"Re: [PATCH v7 2/3] graph: add a 2 commit buffer for lookahead","fromName":"Chandra Pratap","fromEmail":"chandrapratap3519@gmail.com","sentAt":"2026-07-06T09:49:31Z","receivedAt":"2026-07-06T09:49:59Z","isPatch":true,"body":"On Sat, 4 Jul 2026 at 14:24, Pablo Sabater <pabloosabaterr@gmail.com> wrote:\n>\n> In a subsequent commit the graph renderer needs to know if the next\n> commit is a visual root or if it is the last commit to be shown. This\n> requires peeking 2 commits ahead.\n>\n> Commits are pre-fetched at get_revision_internal() where they are also\n> marked as SHOWN.\n>\n> Update graph_is_interesting() so it considers commits inside the\n> lookahead as interesting as well.\n\nNit: lookahead -> lookahead buffer.\n\n> Helped-by: Kristofer Karlsson <krka@spotify.com>\n> Signed-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n> ---\n>  graph.c    | 47 +++++++++++++++++++++++++++++++++++++++++++++++\n>  graph.h    | 17 +++++++++++++++++\n>  revision.c | 17 ++++++++++++++++-\n>  3 files changed, 80 insertions(+), 1 deletion(-)\n>\n> diff --git a/graph.c b/graph.c\n> index 842282685f..300ae67669 100644\n> --- a/graph.c\n> +++ b/graph.c\n> @@ -315,6 +315,14 @@ struct git_graph {\n>          * diff_output_prefix_callback().\n>          */\n>         struct strbuf prefix_buf;\n> +\n> +       /*\n> +        * Lookahead buffer: up to 2 pre-fetched commits that will be shown.\n> +        * Populated by get_revision() so graph_peek_next_visible() can use\n> +        * actual walk results instead of peeking at rev_info internals.\n> +        */\n> +       struct commit *lookahead[2];\n> +       int lookahead_nr;\n>  };\n>\n>  static inline int graph_needs_truncation(struct git_graph *graph, int lane)\n> @@ -388,6 +396,9 @@ struct git_graph *graph_init(struct rev_info *opt)\n>         graph->num_columns = 0;\n>         graph->num_new_columns = 0;\n>         graph->mapping_size = 0;\n> +       graph->lookahead[0] = NULL;\n> +       graph->lookahead[1] = NULL;\n\nStyle: Manually NULLing out each entry doesn't look quite right to me.\nMaybe do something like this instead?\n\nmemset(graph->lookahead, 0, sizeof(graph->lookahead));\n\nAlthough for an array of only two elements, manually NULLing is still quite\nreadable and avoids the minor function-call overhead of memset().\n\nFeel free to ignore this if you want.\n\n> +       graph->lookahead_nr = 0;\n>         /*\n>          * Start the column color at the maximum value, since we'll\n>          * always increment it for the first commit we output.\n> @@ -456,6 +467,15 @@ static void graph_ensure_capacity(struct git_graph *graph, int num_columns)\n>   */\n>  static int graph_is_interesting(struct git_graph *graph, struct commit *commit)\n>  {\n> +       /*\n> +        * Commits in the lookahead buffer have been pre-fetched by\n> +        * get_revision() and will be shown in the future. They already\n> +        * have the SHOWN flag set by get_revision_internal(), but the\n> +        * graph still needs to treat them as interesting parents.\n> +        */\n> +       for (int i = 0; i < graph->lookahead_nr; i++)\n> +               if (graph->lookahead[i] == commit)\n> +                       return 1;\n>         /*\n>          * If revs->boundary is set, commits whose children have\n>          * been shown are always interesting, even if they have the\n> @@ -763,6 +783,33 @@ static int graph_needs_pre_commit_line(struct git_graph *graph)\n>                graph->expansion_row < graph_num_expansion_rows(graph);\n>  }\n>\n> +struct commit *graph_pop_lookahead(struct git_graph *graph)\n> +{\n> +       struct commit *c;\n> +\n> +       if (!graph->lookahead_nr)\n> +               return NULL;\n> +\n> +       c = graph->lookahead[0];\n> +       graph->lookahead[0] = graph->lookahead[1];\n> +       graph->lookahead[1] = NULL;\n\nDo we need to NULL out the retrieved buffer entries? If so, it is\nworthwhile asserting that the entire buffer is NULLed out in the\n!graph->lookahead_nr check above.\n\n> +       graph->lookahead_nr--;\n> +       return c;\n> +}\n\nNot the best engineering practice, but I guess it is fine to constrain\nthe logic to _only_ a 2-entry buffer since that's what we'll always\ndeal with anyway.\n\n> +\n> +int graph_get_lookahead_room(struct git_graph *graph)\n> +{\n> +       return 2 - graph->lookahead_nr;\n\nWe should use ARRAY_SIZE(graph->lookahead) instead of hardcoding\nthe value 2.\n[snip]\n"},{"id":"547231","messageId":"CAL71e4O1tLE_VSDeeZQ_p=8kAXvk9JQ9EqdPaYMZnNs+Xj+RYA@mail.gmail.com","threadId":"65419","inReplyTo":"CA+J6zkQFsTA3QfU5VVjQ=KhJCg_pCrTgW9zinAUC4D9YwsyOkQ@mail.gmail.com","subject":"Re: [PATCH v7 2/3] graph: add a 2 commit buffer for lookahead","fromName":"Kristofer Karlsson","fromEmail":"krka@spotify.com","sentAt":"2026-07-06T13:44:55Z","receivedAt":"2026-07-06T13:45:07Z","isPatch":true,"body":"The hardcoded size-2 lookahead buffer was my suggestion,\nso I am responding inline with my thoughts although Pablo is\nthe right person for making further changes (if any).\n\nOn Mon, 6 Jul 2026, Chandra Pratap <chandrapratap3519@gmail.com> wrote:\n> Do we need to NULL out the retrieved buffer entries? If so, it is\n> worthwhile asserting that the entire buffer is NULLed out in the\n> !graph->lookahead_nr check above.\n\nYou're right, it's not technically needed, and there are many places\nin the repo where stale data remains in buffers, and it would be possible\nto do that here too. I don't think it matters much in practice though,\nand NULLing them out would perhaps prevent some accidental reuse on bugs\n(NULL would crash instead).\n\nAs for asserting: rather than checking that empty slots are NULL\n(which just verifies our own cleanup), it might be more useful to\nassert that a slot is non-NULL when lookahead_nr says it should be\npopulated, i.e. assert on read rather than on empty. But even that\nmay be overkill for a 2-element internal buffer.\n\n> Not the best engineering practice, but I guess it is fine to constrain\n> the logic to _only_ a 2-entry buffer since that's what we'll always\n> deal with anyway.\n\nI did consider making it a proper ring buffer, but it felt like\noverkill (and I could not find any other existing ring buffer to\npiggy-back on in the repo), and the lookahead depth is\nstructurally tied to the algorithm - we only ever need two more\nelements.\n\nIt also helps that this is entirely internal to graph.c. If the\nbuffer were part of a broader API, a less hardcoded approach\nwould be more appropriate indeed.\n\n> We should use ARRAY_SIZE(graph->lookahead) instead of hardcoding\n> the value 2.\n\nAgreed, that is a nice improvement. What do you think Pablo?\n\nThanks,\nKristofer\n"},{"id":"547249","messageId":"CA+J6zkSrcJVcKmm0duTQwWcLxrsZ6eZkVgL=hQUQHegKGsWsxg@mail.gmail.com","threadId":"65419","inReplyTo":"CAL71e4O1tLE_VSDeeZQ_p=8kAXvk9JQ9EqdPaYMZnNs+Xj+RYA@mail.gmail.com","subject":"Re: [PATCH v7 2/3] graph: add a 2 commit buffer for lookahead","fromName":"Chandra Pratap","fromEmail":"chandrapratap3519@gmail.com","sentAt":"2026-07-06T15:33:12Z","receivedAt":"2026-07-06T15:33:42Z","isPatch":true,"body":"On Mon, 6 Jul 2026 at 19:15, Kristofer Karlsson <krka@spotify.com> wrote:\n>\n> The hardcoded size-2 lookahead buffer was my suggestion,\n> so I am responding inline with my thoughts although Pablo is\n> the right person for making further changes (if any).\n>\n> On Mon, 6 Jul 2026, Chandra Pratap <chandrapratap3519@gmail.com> wrote:\n> > Do we need to NULL out the retrieved buffer entries? If so, it is\n> > worthwhile asserting that the entire buffer is NULLed out in the\n> > !graph->lookahead_nr check above.\n>\n> You're right, it's not technically needed, and there are many places\n> in the repo where stale data remains in buffers, and it would be possible\n> to do that here too. I don't think it matters much in practice though,\n> and NULLing them out would perhaps prevent some accidental reuse on bugs\n> (NULL would crash instead).\n>\n> As for asserting: rather than checking that empty slots are NULL\n> (which just verifies our own cleanup), it might be more useful to\n> assert that a slot is non-NULL when lookahead_nr says it should be\n> populated, i.e. assert on read rather than on empty. But even that\n> may be overkill for a 2-element internal buffer.\n\nTrue. But since we're already going through the pains of initializing the\nbuffer and NULLing it upon a pop, I'd much rather go the extra length\nand verify what we're trying to do, shouldn't be that complicated anyway.\n\nWhether that means checking for NULL here, on a push, or on a read\nis something I don't feel strongly about, either is fine with me.\n\n> > Not the best engineering practice, but I guess it is fine to constrain\n> > the logic to _only_ a 2-entry buffer since that's what we'll always\n> > deal with anyway.\n>\n> I did consider making it a proper ring buffer, but it felt like\n> overkill (and I could not find any other existing ring buffer to\n> piggy-back on in the repo), and the lookahead depth is\n> structurally tied to the algorithm - we only ever need two more\n> elements.\n>\n> It also helps that this is entirely internal to graph.c. If the\n> buffer were part of a broader API, a less hardcoded approach\n> would be more appropriate indeed.\n\nAgreed.\n\n> > We should use ARRAY_SIZE(graph->lookahead) instead of hardcoding\n> > the value 2.\n>\n> Agreed, that is a nice improvement. What do you think Pablo?\n>\n> Thanks,\n> Kristofer\n"},{"id":"547297","messageId":"CAN5EUNQoLtJ9cGwe8RNJTTdngM=qoak2=5F+yc7TH94TmQn7uw@mail.gmail.com","threadId":"65419","inReplyTo":"CA+J6zkSrcJVcKmm0duTQwWcLxrsZ6eZkVgL=hQUQHegKGsWsxg@mail.gmail.com","subject":"Re: [PATCH v7 2/3] graph: add a 2 commit buffer for lookahead","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-07T06:31:35Z","receivedAt":"2026-07-07T06:31:46Z","isPatch":true,"body":"El lun, 6 jul 2026 a las 17:33, Chandra Pratap\n(<chandrapratap3519@gmail.com>) escribió:\n>\n> On Mon, 6 Jul 2026 at 19:15, Kristofer Karlsson <krka@spotify.com> wrote:\n> >\n> > The hardcoded size-2 lookahead buffer was my suggestion,\n> > so I am responding inline with my thoughts although Pablo is\n> > the right person for making further changes (if any).\n> >\n> > On Mon, 6 Jul 2026, Chandra Pratap <chandrapratap3519@gmail.com> wrote:\n> > > Do we need to NULL out the retrieved buffer entries? If so, it is\n> > > worthwhile asserting that the entire buffer is NULLed out in the\n> > > !graph->lookahead_nr check above.\n> >\n> > You're right, it's not technically needed, and there are many places\n> > in the repo where stale data remains in buffers, and it would be possible\n> > to do that here too. I don't think it matters much in practice though,\n> > and NULLing them out would perhaps prevent some accidental reuse on bugs\n> > (NULL would crash instead).\n\nIt is not really needed to NULL because every time we access it (pop\nor the graph_is_interesting()) we are limited by graph->lookahead_nr,\nhowever I thought that it is better to have it NULL.\n\nImagine that somehow the lookahead_nr is 1 when it should be 0, having\nNULL would segfault or if it doesn't at least we are sure that\ngraph_is_interesting() won't re-process as interesting a commit left\nas stale on the buffer. Anyway, this is just speculation. I think it's\nbetter to leave it like this.\n\n> >\n> > As for asserting: rather than checking that empty slots are NULL\n> > (which just verifies our own cleanup), it might be more useful to\n> > assert that a slot is non-NULL when lookahead_nr says it should be\n> > populated, i.e. assert on read rather than on empty. But even that\n> > may be overkill for a 2-element internal buffer.\n>\n> True. But since we're already going through the pains of initializing the\n> buffer and NULLing it upon a pop, I'd much rather go the extra length\n> and verify what we're trying to do, shouldn't be that complicated anyway.\n>\n> Whether that means checking for NULL here, on a push, or on a read\n> is something I don't feel strongly about, either is fine with me.\n\nAbout asserting, I think that the best is, because we are popping, to\ncheck the first element only just in case we are in the imaginary\nscenario that lookahead_nr is lying, but because we pop, we don't\nreally care about what's on the second entry.\n\n>\n> > > Not the best engineering practice, but I guess it is fine to constrain\n> > > the logic to _only_ a 2-entry buffer since that's what we'll always\n> > > deal with anyway.\n> >\n> > I did consider making it a proper ring buffer, but it felt like\n> > overkill (and I could not find any other existing ring buffer to\n> > piggy-back on in the repo), and the lookahead depth is\n> > structurally tied to the algorithm - we only ever need two more\n> > elements.\n> >\n> > It also helps that this is entirely internal to graph.c. If the\n> > buffer were part of a broader API, a less hardcoded approach\n> > would be more appropriate indeed.\n>\n> Agreed.\n>\n> > > We should use ARRAY_SIZE(graph->lookahead) instead of hardcoding\n> > > the value 2.\n> >\n> > Agreed, that is a nice improvement. What do you think Pablo?\n\nYes, I'll do that on reroll.\n\n> >\n> > Thanks,\n> > Kristofer\n\nNot related with this feedback but worth saying:\n\nre-reading what's done on revision.c there is this if line:\n> if (!revs->max_count_stage && !revs->reverse_output_stage)\n\nGraph is not compatible with --reverse, so the right-side will always be true.\nAbout --max-count, I made a few tests and the lookahead behaves the\nsame regardless of the number of commits to be shown (even if capped).\n\nSo this whole if block can be dropped and we can try to populate the\nlookahead buffer always.\n\nThanks both for the feedback and review,\nPablo.\n"},{"id":"547378","messageId":"CAN5EUNREij1M46qpiERuD3knCGbQeVLOL=sV_OPXg26NxFcrxA@mail.gmail.com","threadId":"65419","inReplyTo":"CAN5EUNQoLtJ9cGwe8RNJTTdngM=qoak2=5F+yc7TH94TmQn7uw@mail.gmail.com","subject":"Re: [PATCH v7 2/3] graph: add a 2 commit buffer for lookahead","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-07T18:12:19Z","receivedAt":"2026-07-07T18:12:31Z","isPatch":true,"body":"El mar, 7 jul 2026 a las 8:31, Pablo Sabater\n(<pabloosabaterr@gmail.com>) escribió:\n>\n> El lun, 6 jul 2026 a las 17:33, Chandra Pratap\n> (<chandrapratap3519@gmail.com>) escribió:\n> >\n> > On Mon, 6 Jul 2026 at 19:15, Kristofer Karlsson <krka@spotify.com> wrote:\n> > >\n> > > The hardcoded size-2 lookahead buffer was my suggestion,\n> > > so I am responding inline with my thoughts although Pablo is\n> > > the right person for making further changes (if any).\n> > >\n> > > On Mon, 6 Jul 2026, Chandra Pratap <chandrapratap3519@gmail.com> wrote:\n> > > > Do we need to NULL out the retrieved buffer entries? If so, it is\n> > > > worthwhile asserting that the entire buffer is NULLed out in the\n> > > > !graph->lookahead_nr check above.\n> > >\n> > > You're right, it's not technically needed, and there are many places\n> > > in the repo where stale data remains in buffers, and it would be possible\n> > > to do that here too. I don't think it matters much in practice though,\n> > > and NULLing them out would perhaps prevent some accidental reuse on bugs\n> > > (NULL would crash instead).\n>\n> It is not really needed to NULL because every time we access it (pop\n> or the graph_is_interesting()) we are limited by graph->lookahead_nr,\n> however I thought that it is better to have it NULL.\n>\n> Imagine that somehow the lookahead_nr is 1 when it should be 0, having\n> NULL would segfault or if it doesn't at least we are sure that\n> graph_is_interesting() won't re-process as interesting a commit left\n> as stale on the buffer. Anyway, this is just speculation. I think it's\n> better to leave it like this.\n>\n> > >\n> > > As for asserting: rather than checking that empty slots are NULL\n> > > (which just verifies our own cleanup), it might be more useful to\n> > > assert that a slot is non-NULL when lookahead_nr says it should be\n> > > populated, i.e. assert on read rather than on empty. But even that\n> > > may be overkill for a 2-element internal buffer.\n> >\n> > True. But since we're already going through the pains of initializing the\n> > buffer and NULLing it upon a pop, I'd much rather go the extra length\n> > and verify what we're trying to do, shouldn't be that complicated anyway.\n> >\n> > Whether that means checking for NULL here, on a push, or on a read\n> > is something I don't feel strongly about, either is fine with me.\n>\n> About asserting, I think that the best is, because we are popping, to\n> check the first element only just in case we are in the imaginary\n> scenario that lookahead_nr is lying, but because we pop, we don't\n> really care about what's on the second entry.\n>\n> >\n> > > > Not the best engineering practice, but I guess it is fine to constrain\n> > > > the logic to _only_ a 2-entry buffer since that's what we'll always\n> > > > deal with anyway.\n> > >\n> > > I did consider making it a proper ring buffer, but it felt like\n> > > overkill (and I could not find any other existing ring buffer to\n> > > piggy-back on in the repo), and the lookahead depth is\n> > > structurally tied to the algorithm - we only ever need two more\n> > > elements.\n> > >\n> > > It also helps that this is entirely internal to graph.c. If the\n> > > buffer were part of a broader API, a less hardcoded approach\n> > > would be more appropriate indeed.\n> >\n> > Agreed.\n> >\n> > > > We should use ARRAY_SIZE(graph->lookahead) instead of hardcoding\n> > > > the value 2.\n> > >\n> > > Agreed, that is a nice improvement. What do you think Pablo?\n>\n> Yes, I'll do that on reroll.\n>\n> > >\n> > > Thanks,\n> > > Kristofer\n>\n> Not related with this feedback but worth saying:\n>\n> re-reading what's done on revision.c there is this if line:\n> > if (!revs->max_count_stage && !revs->reverse_output_stage)\n>\n> Graph is not compatible with --reverse, so the right-side will always be true.\n> About --max-count, I made a few tests and the lookahead behaves the\n> same regardless of the number of commits to be shown (even if capped).\n\nNow that I saw the GitHub CI tests, at t4202 there is a graph option\n\"--max-count-oldest\" that makes the !revs->max_count_stage check\nnecessary.\nAfter that everything seems to work, if anything I'll explain it on\nthe cover letter soonly.\n\n>\n> So this whole if block can be dropped and we can try to populate the\n> lookahead buffer always.\n>\n> Thanks both for the feedback and review,\n> Pablo.\n\nRegards,\nPablo\n"},{"id":"547712","messageId":"20260710-ps-pre-commit-indent-v8-0-d3b636463bf4@gmail.com","threadId":"65419","inReplyTo":"20260704-ps-pre-commit-indent-v7-0-a94706cc8376@gmail.com","subject":"[PATCH v8 0/4] graph: indent visual roots in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-10T10:37:03Z","receivedAt":"2026-07-10T10:37:39Z","isPatch":true,"body":"When rendering a graph, if the history contains multiple \"visual roots\",\nactual roots or commits that look like roots (i.e. have their parents\nfiltered out) can end up being vertically adjacent to unrelated commits,\nfalsely appearing to be related.\n\nA fix for this issue was already attempted [1] a while ago.\n\nThis series adds indentation to the visual root commits, so they cannot be\nvertically adjacent anymore making it easier to identify them.\n\nBefore indentation:\n\n\t* A\n\t* B1\n\t* B2\n\t* C1\n\t* C2\n\nAfter indentation:\n\n\t  * A\n\t* B1\n\t \\\n\t  * B2\n\t* C1\n\t* C2\n\nIndents the visual root commits that have still commits to show after\nthem, and if they have children it connects them with an edge at a new\nrow.\n\nIf there are multiple visual roots adjacent in history, the indentation\nstarts with the second one, avoiding redundant indentation of the first\none and cascades after the second.\n\n\t* A\n\t  * B\n\t    * C\n\t* D1\n\t* D2\n\nThis series first commit is a cleanup that brings a common function\nfrom t4215 and t6016 to a graph functions file which they both use, so\nthe new test file for indentation, t4218, can use it as well.\n\nGitHub CI: https://github.com/pabloosabaterr/git/actions/runs/29082267633\n\n[1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n\nV7 DIFF:\n\n- New commit (2nd) \"revision: add next_commit_to_show()\" I wanted to\n  drop (!revs->max_count_stage && !revs->reverse_output_stage) because\n  --reverse is not compatible with --graph and I wanted the indentation\n  to work with --max-count-oldest. Dropping it broke tests at t4202\n  because the lookahead buffer was being populated from a different\n  source.\n  This new commit adds a helper to get the commits for the lookahead\n  from the same source.\n- Typos and style.\n- Added --max-count and --max-count-oldest tests.\n- graph_get_lookahead_room() now uses ARRAY_SIZE() instead of the\n  hardcoded size 2.\n- Added an assert on graph_pop_lookahead()\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\nPablo Sabater (4):\n      lib-log-graph: move check_graph function\n      revision: add next_commit_to_show()\n      graph: add a 2 commit buffer for lookahead\n      graph: indent visual root in graph\n\n graph.c                                    | 286 +++++++++++++++++\n graph.h                                    |  17 ++\n revision.c                                 |  48 ++-\n t/lib-log-graph.sh                         |   5 +\n t/meson.build                              |   1 +\n t/t4215-log-skewed-merges.sh               |  33 +-\n t/t4218-log-graph-indentation.sh           | 473 +++++++++++++++++++++++++++++\n t/t6016-rev-list-graph-simplify-history.sh |  25 +-\n 8 files changed, 843 insertions(+), 45 deletions(-)\n\nbase-commit: f85a7e662054a7b0d9070e432508831afa214b47\n"},{"id":"547713","messageId":"20260710-ps-pre-commit-indent-v8-1-d3b636463bf4@gmail.com","threadId":"65419","inReplyTo":"20260710-ps-pre-commit-indent-v8-0-d3b636463bf4@gmail.com","subject":"[PATCH v8 1/4] lib-log-graph: move check_graph function","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-10T10:37:04Z","receivedAt":"2026-07-10T10:37:44Z","isPatch":true,"body":"check_graph is a function shared in the test files t4215 and t6016 used\nto format the output graph, but instead of being in a file called by\nboth test, the function code is repeated in each file.\n\nMove check_graph to lib-log-graph.sh file which both tests already\nimport graph functions from, renaming it to lib_test_check_graph.\n\nThis function is needed for the following commit which includes graph\ntests in a new file and requires check_graph.\n\nMentored-by: Karthik Nayak <karthik.188@gmail.com>\nMentored-by: Chandra Pratap <chandrapratap3519@gmail.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n t/lib-log-graph.sh                         |  5 +++++\n t/t4215-log-skewed-merges.sh               | 33 +++++++++++++-----------------\n t/t6016-rev-list-graph-simplify-history.sh | 25 +++++++++-------------\n 3 files changed, 29 insertions(+), 34 deletions(-)\n\ndiff --git a/t/lib-log-graph.sh b/t/lib-log-graph.sh\nindex bf952ef920..1eae8f60c2 100644\n--- a/t/lib-log-graph.sh\n+++ b/t/lib-log-graph.sh\n@@ -26,3 +26,8 @@ lib_test_cmp_colored_graph () {\n \ttest_decode_color <output.colors.raw | sed \"s/ *\\$//\" >output.colors &&\n \ttest_cmp expect.colors output.colors\n }\n+\n+lib_test_check_graph () {\n+\tcat >expect &&\n+\tlib_test_cmp_graph --format=%s \"$@\"\n+}\ndiff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh\nindex 1612f05f1b..eebab71039 100755\n--- a/t/t4215-log-skewed-merges.sh\n+++ b/t/t4215-log-skewed-merges.sh\n@@ -5,11 +5,6 @@ test_description='git log --graph of skewed merges'\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-log-graph.sh\n \n-check_graph () {\n-\tcat >expect &&\n-\tlib_test_cmp_graph --format=%s \"$@\"\n-}\n-\n test_expect_success 'log --graph with merge fusing with its left and right neighbors' '\n \tgit checkout --orphan _p &&\n \ttest_commit A &&\n@@ -21,7 +16,7 @@ test_expect_success 'log --graph with merge fusing with its left and right neigh\n \tgit checkout _p && git merge --no-ff _r -m G &&\n \tgit checkout @^^ && git merge --no-ff _p -m H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   H\n \t|\\\n \t| *   G\n@@ -49,7 +44,7 @@ test_expect_success 'log --graph with left-skewed merge' '\n \tgit checkout 0_p && git merge --no-ff 0_s -m 0_G &&\n \tgit checkout @^ && git merge --no-ff 0_q 0_r 0_t 0_p -m 0_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*-----.   0_H\n \t|\\ \\ \\ \\\n \t| | | | * 0_G\n@@ -83,7 +78,7 @@ test_expect_success 'log --graph with nested left-skewed merge' '\n \tgit checkout 1_p && git merge --no-ff 1_r -m 1_G &&\n \tgit checkout @^^ && git merge --no-ff 1_p -m 1_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   1_H\n \t|\\\n \t| *   1_G\n@@ -115,7 +110,7 @@ test_expect_success 'log --graph with nested left-skewed merge following normal\n \tgit checkout -b 2_s @^^ && git merge --no-ff 2_q -m 2_J &&\n \tgit checkout 2_p && git merge --no-ff 2_s -m 2_K &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   2_K\n \t|\\\n \t| *   2_J\n@@ -151,7 +146,7 @@ test_expect_success 'log --graph with nested right-skewed merge following left-s\n \tgit checkout 3_p && git merge --no-ff 3_r -m 3_H &&\n \tgit checkout @^^ && git merge --no-ff 3_p -m 3_J &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   3_J\n \t|\\\n \t| *   3_H\n@@ -182,7 +177,7 @@ test_expect_success 'log --graph with right-skewed merge following a left-skewed\n \tgit merge --no-ff 4_p -m 4_G &&\n \tgit checkout @^^ && git merge --no-ff 4_s -m 4_H &&\n \n-\tcheck_graph --date-order <<-\\EOF\n+\tlib_test_check_graph --date-order <<-\\EOF\n \t*   4_H\n \t|\\\n \t| *   4_G\n@@ -218,7 +213,7 @@ test_expect_success 'log --graph with octopus merge with column joining its penu\n \tgit checkout 5_r &&\n \tgit merge --no-ff 5_s -m 5_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   5_H\n \t|\\\n \t| *-.   5_G\n@@ -257,7 +252,7 @@ test_expect_success 'log --graph with multiple tips' '\n \tgit checkout 6_1 &&\n \tgit merge --no-ff 6_2 -m 6_I &&\n \n-\tcheck_graph 6_1 6_3 6_5 <<-\\EOF\n+\tlib_test_check_graph 6_1 6_3 6_5 <<-\\EOF\n \t*   6_I\n \t|\\\n \t| | *   6_H\n@@ -334,7 +329,7 @@ test_expect_success 'log --graph with multiple tips' '\n \tgit checkout -b M_7 7_1 &&\n \tgit merge --no-ff 7_2 7_3 -m 7_M4 &&\n \n-\tcheck_graph M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -371,7 +366,7 @@ test_expect_success 'log --graph with multiple tips' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' '\n-\tcheck_graph --graph-lane-limit=2 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=2 M_7 <<-\\EOF\n \t*-.   7_M4\n \t|\\ \\\n \t| | * 7_G\n@@ -388,7 +383,7 @@ test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=1 truncate mid octopus merge' '\n-\tcheck_graph --graph-lane-limit=1 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=1 M_7 <<-\\EOF\n \t*-~  7_M4\n \t|\\~\n \t| ~ 7_G\n@@ -405,7 +400,7 @@ test_expect_success 'log --graph --graph-lane-limit=1 truncate mid octopus merge\n '\n \n test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' '\n-\tcheck_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -441,7 +436,7 @@ test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=6 check if it only shows first of 3 parent merge' '\n-\tcheck_graph --graph-lane-limit=6 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=6 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -478,7 +473,7 @@ test_expect_success 'log --graph --graph-lane-limit=6 check if it only shows fir\n '\n \n test_expect_success 'log --graph --graph-lane-limit=7 check if it shows all 3 parent merge' '\n-\tcheck_graph --graph-lane-limit=7 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=7 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\ndiff --git a/t/t6016-rev-list-graph-simplify-history.sh b/t/t6016-rev-list-graph-simplify-history.sh\nindex 54b0a6f5f8..e0d9c3c1ac 100755\n--- a/t/t6016-rev-list-graph-simplify-history.sh\n+++ b/t/t6016-rev-list-graph-simplify-history.sh\n@@ -13,11 +13,6 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-log-graph.sh\n \n-check_graph () {\n-\tcat >expect &&\n-\tlib_test_cmp_graph --format=%s \"$@\"\n-}\n-\n test_expect_success 'set up rev-list --graph test' '\n \t# 3 commits on branch A\n \ttest_commit A1 foo.txt &&\n@@ -54,7 +49,7 @@ test_expect_success 'set up rev-list --graph test' '\n '\n \n test_expect_success '--graph --all' '\n-\tcheck_graph --all <<-\\EOF\n+\tlib_test_check_graph --all <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -82,7 +77,7 @@ test_expect_success '--graph --all' '\n # that undecorated merges are interesting, even with --simplify-by-decoration\n test_expect_success '--graph --simplify-by-decoration' '\n \tgit tag -d A4 &&\n-\tcheck_graph --all --simplify-by-decoration <<-\\EOF\n+\tlib_test_check_graph --all --simplify-by-decoration <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -114,7 +109,7 @@ test_expect_success 'setup: get rid of decorations on B' '\n \n # Graph with branch B simplified away\n test_expect_success '--graph --simplify-by-decoration prune branch B' '\n-\tcheck_graph --simplify-by-decoration --all <<-\\EOF\n+\tlib_test_check_graph --simplify-by-decoration --all <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -133,7 +128,7 @@ test_expect_success '--graph --simplify-by-decoration prune branch B' '\n '\n \n test_expect_success '--graph --full-history -- bar.txt' '\n-\tcheck_graph --full-history --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --full-history --all -- bar.txt <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -148,7 +143,7 @@ test_expect_success '--graph --full-history -- bar.txt' '\n '\n \n test_expect_success '--graph --full-history --simplify-merges -- bar.txt' '\n-\tcheck_graph --full-history --simplify-merges --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --full-history --simplify-merges --all -- bar.txt <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -161,7 +156,7 @@ test_expect_success '--graph --full-history --simplify-merges -- bar.txt' '\n '\n \n test_expect_success '--graph -- bar.txt' '\n-\tcheck_graph --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --all -- bar.txt <<-\\EOF\n \t* A7\n \t* A5\n \t* A3\n@@ -172,7 +167,7 @@ test_expect_success '--graph -- bar.txt' '\n '\n \n test_expect_success '--graph --sparse -- bar.txt' '\n-\tcheck_graph --sparse --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --sparse --all -- bar.txt <<-\\EOF\n \t* A7\n \t* A6\n \t* A5\n@@ -189,7 +184,7 @@ test_expect_success '--graph --sparse -- bar.txt' '\n '\n \n test_expect_success '--graph ^C4' '\n-\tcheck_graph --all ^C4 <<-\\EOF\n+\tlib_test_check_graph --all ^C4 <<-\\EOF\n \t* A7\n \t* A6\n \t* A5\n@@ -202,7 +197,7 @@ test_expect_success '--graph ^C4' '\n '\n \n test_expect_success '--graph ^C3' '\n-\tcheck_graph --all ^C3 <<-\\EOF\n+\tlib_test_check_graph --all ^C3 <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -220,7 +215,7 @@ test_expect_success '--graph ^C3' '\n # that important, but this test depends on it.  If the ordering ever changes\n # in the code, we'll need to update this test.\n test_expect_success '--graph --boundary ^C3' '\n-\tcheck_graph --boundary --all ^C3 <<-\\EOF\n+\tlib_test_check_graph --boundary --all ^C3 <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n\n-- \n2.54.0\n"},{"id":"547714","messageId":"20260710-ps-pre-commit-indent-v8-2-d3b636463bf4@gmail.com","threadId":"65419","inReplyTo":"20260710-ps-pre-commit-indent-v8-0-d3b636463bf4@gmail.com","subject":"[PATCH v8 2/4] revision: add next_commit_to_show()","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-10T10:37:05Z","receivedAt":"2026-07-10T10:37:46Z","isPatch":true,"body":"get_revision() gets its commits from two sources depending on the mode:\n\n1. Normally it gets the commits from get_revision_internal().\n\n2. --max-count-oldest which was introduced at bb4ce23284 (revision.c:\n   implement --max-count-oldest, 2026-05-19) gets the commits by popping\n   from a saved list at revs->commits marking SHOWN and CHILD_SHOWN on\n   each popped commit.\n\nExtract the choice logic into a helper, next_commit_to_show(), which\nreturns the next commit regardless of the source it comes from.\n\nThis has no change in behavior. The helper is needed in a subsequent\ncommit that pre-fetches two commits into a buffer for lookahead purposes\nand needs to pre-fetch from the same source.\n\nThe --reverse branch keeps its own pop loop. Using the helper for\n--reverse would additionally set SHOWN and CHILD_SHOWN which is not\ndesired and a behavior change.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n revision.c | 36 ++++++++++++++++++++++++------------\n 1 file changed, 24 insertions(+), 12 deletions(-)\n\ndiff --git a/revision.c b/revision.c\nindex 0c95edef59..288935943f 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -4658,12 +4658,34 @@ static void retrieve_oldest_commits(struct rev_info *revs,\n \t\tcommit_list_insert(c, queue);\n }\n \n+/*\n+ * Returns the next commit that will be shown, regardless of whether it comes\n+ * directly from the revision walk or from the list saved by the staged output\n+ * of --max-count-oldest.\n+ */\n+static struct commit *next_commit_to_show(struct rev_info *revs)\n+{\n+\tstruct commit *c;\n+\tstruct commit_list *p;\n+\n+\tif (!revs->max_count_stage)\n+\t\treturn get_revision_internal(revs);\n+\n+\tc = pop_commit(&revs->commits);\n+\tif (c) {\n+\t\tc->object.flags |= SHOWN;\n+\t\tif (!(c->object.flags & BOUNDARY))\n+\t\t\tfor (p = c->parents; p; p = p->next)\n+\t\t\t\tp->item->object.flags |= CHILD_SHOWN;\n+\t}\n+\treturn c;\n+}\n+\n struct commit *get_revision(struct rev_info *revs)\n {\n \tstruct commit *c;\n \tstruct commit_list *reversed;\n \tstruct commit_list *queue = NULL;\n-\tstruct commit_list *p;\n \n \tif (revs->max_count_type == 1 && !revs->max_count_stage) {\n \t\tretrieve_oldest_commits(revs, &queue);\n@@ -4693,17 +4715,7 @@ struct commit *get_revision(struct rev_info *revs)\n \t\treturn c;\n \t}\n \n-\tif (revs->max_count_stage) {\n-\t\tc = pop_commit(&revs->commits);\n-\t\tif (c) {\n-\t\t\tc->object.flags |= SHOWN;\n-\t\t\tif (!(c->object.flags & BOUNDARY))\n-\t\t\t\tfor (p = c->parents; p; p = p->next)\n-\t\t\t\t\tp->item->object.flags |= CHILD_SHOWN;\n-\t\t}\n-\t} else {\n-\t\tc = get_revision_internal(revs);\n-\t}\n+\tc = next_commit_to_show(revs);\n \n \tif (c && revs->graph)\n \t\tgraph_update(revs->graph, c);\n\n-- \n2.54.0\n"},{"id":"547715","messageId":"20260710-ps-pre-commit-indent-v8-3-d3b636463bf4@gmail.com","threadId":"65419","inReplyTo":"20260710-ps-pre-commit-indent-v8-0-d3b636463bf4@gmail.com","subject":"[PATCH v8 3/4] graph: add a 2 commit buffer for lookahead","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-10T10:37:06Z","receivedAt":"2026-07-10T10:37:48Z","isPatch":true,"body":"In a subsequent commit the graph renderer needs to know if the next\ncommit is a visual root or if it is the last commit to be shown. This\nrequires peeking 2 commits ahead.\n\nCommits are pre-fetched in get_revision() through next_commit_to_show()\nwhere they are also marked as SHOWN, regardless the source they come\nfrom.\n\nUpdate graph_is_interesting() so it considers commits inside the\nlookahead buffer as interesting as well.\n\nHelped-by: Kristofer Karlsson <krka@spotify.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c    | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++\n graph.h    | 17 +++++++++++++++++\n revision.c | 18 ++++++++++++++++--\n 3 files changed, 84 insertions(+), 2 deletions(-)\n\ndiff --git a/graph.c b/graph.c\nindex 842282685f..89ebcf7540 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -315,6 +315,14 @@ struct git_graph {\n \t * diff_output_prefix_callback().\n \t */\n \tstruct strbuf prefix_buf;\n+\n+\t/*\n+\t * Lookahead buffer: up to 2 pre-fetched commits that will be shown.\n+\t * Populated by get_revision() so graph_peek_next_visible() can use\n+\t * actual walk results instead of peeking at rev_info internals.\n+\t */\n+\tstruct commit *lookahead[2];\n+\tint lookahead_nr;\n };\n \n static inline int graph_needs_truncation(struct git_graph *graph, int lane)\n@@ -388,6 +396,9 @@ struct git_graph *graph_init(struct rev_info *opt)\n \tgraph->num_columns = 0;\n \tgraph->num_new_columns = 0;\n \tgraph->mapping_size = 0;\n+\tgraph->lookahead[0] = NULL;\n+\tgraph->lookahead[1] = NULL;\n+\tgraph->lookahead_nr = 0;\n \t/*\n \t * Start the column color at the maximum value, since we'll\n \t * always increment it for the first commit we output.\n@@ -456,6 +467,15 @@ static void graph_ensure_capacity(struct git_graph *graph, int num_columns)\n  */\n static int graph_is_interesting(struct git_graph *graph, struct commit *commit)\n {\n+\t/*\n+\t * Commits in the lookahead buffer have been pre-fetched by\n+\t * get_revision() and will be shown in the future. They already have\n+\t * the SHOWN flag set when they were pre-fetched but the graph still\n+\t * needs to treat them as interesting parents.\n+\t */\n+\tfor (int i = 0; i < graph->lookahead_nr; i++)\n+\t\tif (graph->lookahead[i] == commit)\n+\t\t\treturn 1;\n \t/*\n \t * If revs->boundary is set, commits whose children have\n \t * been shown are always interesting, even if they have the\n@@ -763,6 +783,37 @@ static int graph_needs_pre_commit_line(struct git_graph *graph)\n \t       graph->expansion_row < graph_num_expansion_rows(graph);\n }\n \n+struct commit *graph_pop_lookahead(struct git_graph *graph)\n+{\n+\tstruct commit *c;\n+\n+\tif (!graph->lookahead_nr)\n+\t\treturn NULL;\n+\n+\tc = graph->lookahead[0];\n+\tif (!c)\n+\t\tBUG(\"lookahead buffer has %d entries but the first one is NULL\",\n+\t\t    graph->lookahead_nr);\n+\n+\tgraph->lookahead[0] = graph->lookahead[1];\n+\tgraph->lookahead[1] = NULL;\n+\tgraph->lookahead_nr--;\n+\treturn c;\n+}\n+\n+int graph_get_lookahead_room(struct git_graph *graph)\n+{\n+\treturn (int)ARRAY_SIZE(graph->lookahead) - graph->lookahead_nr;\n+}\n+\n+void graph_push_lookahead(struct git_graph *graph, struct commit *c)\n+{\n+\tif (!graph_get_lookahead_room(graph))\n+\t\tBUG(\"pushing into lookahead buffer when it is already full\");\n+\n+\tgraph->lookahead[graph->lookahead_nr++] = c;\n+}\n+\n void graph_update(struct git_graph *graph, struct commit *commit)\n {\n \tstruct commit_list *parent;\ndiff --git a/graph.h b/graph.h\nindex 3fd1dcb2e9..1193711fb8 100644\n--- a/graph.h\n+++ b/graph.h\n@@ -262,4 +262,21 @@ void graph_show_commit_msg(struct git_graph *graph,\n \t\t\t   FILE *file,\n \t\t\t   struct strbuf const *sb);\n \n+/*\n+ * Pop the first commit from the graph's lookahead buffer.\n+ * Returns NULL if the buffer is empty.\n+ */\n+struct commit *graph_pop_lookahead(struct git_graph *graph);\n+\n+/*\n+ * Returns how many more commits can be added to the lookahead buffer.\n+ */\n+int graph_get_lookahead_room(struct git_graph *graph);\n+\n+/*\n+ * Push a commit into the lookahead buffer. Must only be called when\n+ * graph_get_lookahead_room() returns > 0.\n+ */\n+void graph_push_lookahead(struct git_graph *graph, struct commit *c);\n+\n #endif /* GRAPH_H */\ndiff --git a/revision.c b/revision.c\nindex 288935943f..258c3cf782 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -4715,10 +4715,24 @@ struct commit *get_revision(struct rev_info *revs)\n \t\treturn c;\n \t}\n \n-\tc = next_commit_to_show(revs);\n+\tif (revs->graph) {\n+\t\tc = graph_pop_lookahead(revs->graph);\n+\t\tif (!c)\n+\t\t\tc = next_commit_to_show(revs);\n+\t} else {\n+\t\tc = next_commit_to_show(revs);\n+\t}\n \n-\tif (c && revs->graph)\n+\tif (c && revs->graph) {\n+\t\twhile (graph_get_lookahead_room(revs->graph)) {\n+\t\t\tstruct commit *next = next_commit_to_show(revs);\n+\t\t\tif (!next)\n+\t\t\t\tbreak;\n+\t\t\tgraph_push_lookahead(revs->graph, next);\n+\t\t}\n \t\tgraph_update(revs->graph, c);\n+\t}\n+\n \tif (!c) {\n \t\tfree_saved_parents(revs);\n \t\tcommit_list_free(revs->previous_parents);\n\n-- \n2.54.0\n"},{"id":"547716","messageId":"20260710-ps-pre-commit-indent-v8-4-d3b636463bf4@gmail.com","threadId":"65419","inReplyTo":"20260710-ps-pre-commit-indent-v8-0-d3b636463bf4@gmail.com","subject":"[PATCH v8 4/4] graph: indent visual root in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-10T10:37:07Z","receivedAt":"2026-07-10T10:37:49Z","isPatch":true,"body":"When rendering a graph, if the history contains multiple \"visual roots\",\nactual roots or commits that look like roots (i.e. have their parents\nfiltered out) can end up being vertically adjacent to unrelated commits,\nfalsely appearing to be related.\n\nA fix for this issue was already attempted [1] a while ago.\n\nThis happens because the commits fill the space from left to right and\nwhen a visual root ends, its column becomes free for the following\ncommit even if they are not related. Once this happens the unrelated\ncommit is rendered below the visual root. Because there is no special\ncharacter or way to identify when a visual root is rendered making the\ngraph confusing.\n\nBy indenting the visual roots when there are still commits to show the\nvertical adjacency can be avoided.\n\nAdd is_visual_root flag to git_graph making it visible in all graph states,\ngive graph_update() a new function, graph_is_visual_root() to know if the\ncurrent commit is a visual root and set is_visual_root.\nThe different handled cases are:\n\n- If a visual root has children: similar to GRAPH_PRE_COMMIT state when\n  octopus merges need space, an edge row needs to be printed to connect\n  the child with the indented visual root. A new state GRAPH_PRE_ROOT is\n  needed to connect the child with the visual root:\n\n    * child of the visual root\n     \\ GRAPH_PRE_ROOT\n      * visual root indented\n\n- If a visual root is child-less we can skip GRAPH_PRE_ROOT state and\n  render the indented commit directly.\n\n      * visual root indented\n    * unrelated commit\n\n- If two or more visual roots are adjacent: by having a lookahead to the\n  next commit that will be rendered, if the next commit is also a visual\n  root and we are on a visual root, meaning two visual root adjacent in\n  the history, the top one can omit the indent, making the one below to\n  indent only once, if there are more adjacent visual commits, the\n  indentation will increase for each adjacent one, cascading.\n\n    * visual root\n      * visual root\n        * visual root\n    * last commit\n\n  Even if the last commit is a root, because there is nothing that will be\n  rendered below we can omit the indentation on purpose.\n\n[1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n\nHelped-by: Kristofer Karlsson <krka@spotify.com>\nMentored-by: Karthik Nayak <karthik.188@gmail.com>\nMentored-by: Chandra Pratap <chandrapratap3519@gmail.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c                          | 235 +++++++++++++++++++\n t/meson.build                    |   1 +\n t/t4218-log-graph-indentation.sh | 473 +++++++++++++++++++++++++++++++++++++++\n 3 files changed, 709 insertions(+)\n\ndiff --git a/graph.c b/graph.c\nindex 89ebcf7540..68ddde24b6 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -60,12 +60,23 @@ struct column {\n \t * index into column_colors.\n \t */\n \tunsigned short color;\n+\t/*\n+\t * Marks if a commit is a non-first parent of a merge. These columns are\n+\t * already visually connected to the merge commit and do not need\n+\t * indentation.\n+\t *\n+\t * The first parent is the one that inherits the column and it can need\n+\t * indentation if turns out to be a visual root and there's still\n+\t * commits to render.\n+\t */\n+\tunsigned int is_merge_parent:1;\n };\n \n enum graph_state {\n \tGRAPH_PADDING,\n \tGRAPH_SKIP,\n \tGRAPH_PRE_COMMIT,\n+\tGRAPH_PRE_ROOT,\n \tGRAPH_COMMIT,\n \tGRAPH_POST_MERGE,\n \tGRAPH_COLLAPSING\n@@ -323,6 +334,51 @@ struct git_graph {\n \t */\n \tstruct commit *lookahead[2];\n \tint lookahead_nr;\n+\n+\t/*\n+\t * If a commit is a visual root, we need to indent it to prevent\n+\t * unrelated commits from being vertically adjacent to it.\n+\t */\n+\tunsigned int is_visual_root:1;\n+\n+\t/*\n+\t * Indentation increases for each visual root adjacent to another visual\n+\t * root, making visual root commits indentation cascade.\n+\t */\n+\tunsigned int visual_root_depth;\n+\n+\t/*\n+\t * When a visual root is adjacent to other visual roots, the first one\n+\t * can avoid indentation and the rest cascades, increasing the indentation\n+\t * for each one.\n+\t */\n+\tunsigned int visual_root_cascade:1;\n+\n+\t/*\n+\t * Set when the current commit was already present in graph->columns\n+\t * before being processed.\n+\t */\n+\tunsigned int commit_in_columns:1;\n+};\n+\n+struct graph_lookahead_flags {\n+\n+\t/*\n+\t * Set when there will be a commit after the current one that will be\n+\t * rendered.\n+\t */\n+\tunsigned int is_next_visible:1;\n+\n+\t/*\n+\t * Set when the next visible commit is candidate to be a visual root.\n+\t */\n+\tunsigned int is_next_visual_root:1;\n+\n+\t/*\n+\t * Set when the next visible commit will be rendered under the current\n+\t * commit.\n+\t */\n+\tunsigned int next_has_column:1;\n };\n \n static inline int graph_needs_truncation(struct git_graph *graph, int lane)\n@@ -399,6 +455,8 @@ struct git_graph *graph_init(struct rev_info *opt)\n \tgraph->lookahead[0] = NULL;\n \tgraph->lookahead[1] = NULL;\n \tgraph->lookahead_nr = 0;\n+\tgraph->visual_root_depth = 0;\n+\tgraph->visual_root_cascade = 0;\n \t/*\n \t * Start the column color at the maximum value, since we'll\n \t * always increment it for the first commit we output.\n@@ -581,6 +639,11 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t\t\t\t\t  struct commit *commit,\n \t\t\t\t\t  int idx)\n {\n+\t/*\n+\t * Get the initial merge_layout before it's modified to know if this\n+\t * is a merge.\n+\t */\n+\tint initial_merge_layout = graph->merge_layout;\n \tint i = graph_find_new_column_by_commit(graph, commit);\n \tint mapping_idx;\n \n@@ -592,6 +655,7 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t\ti = graph->num_new_columns++;\n \t\tgraph->new_columns[i].commit = commit;\n \t\tgraph->new_columns[i].color = graph_find_commit_color(graph, commit);\n+\t\tgraph->new_columns[i].is_merge_parent = 0;\n \t}\n \n \tif (graph->num_parents > 1 && idx > -1 && graph->merge_layout == -1) {\n@@ -630,6 +694,12 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t}\n \n \tgraph->mapping[mapping_idx] = i;\n+\n+\t/*\n+\t * Mark non-first parents of a merge.\n+\t */\n+\tif (graph->num_parents > 1 && initial_merge_layout >= 0 && idx > -1)\n+\t\tgraph->new_columns[i].is_merge_parent = 1;\n }\n \n static void graph_update_columns(struct git_graph *graph)\n@@ -721,10 +791,20 @@ static void graph_update_columns(struct git_graph *graph)\n \t\t\tif (graph->num_parents == 0)\n \t\t\t\tgraph->width += 2;\n \t\t} else {\n+\t\t\tint j;\n \t\t\tgraph_insert_into_new_columns(graph, col_commit, -1);\n+\t\t\t/*\n+\t\t\t * This column is not the current commit, but we need to\n+\t\t\t * propagate the flag until the commit is processed.\n+\t\t\t */\n+\t\t\tj = graph_find_new_column_by_commit(graph, col_commit);\n+\t\t\tif (j >= 0 && graph->columns[i].is_merge_parent)\n+\t\t\t\tgraph->new_columns[j].is_merge_parent = 1;\n \t\t}\n \t}\n \n+\tgraph->commit_in_columns = is_commit_in_columns;\n+\n \t/*\n \t * If graph_max_lanes is set, cap the width\n \t */\n@@ -814,9 +894,104 @@ void graph_push_lookahead(struct git_graph *graph, struct commit *c)\n \tgraph->lookahead[graph->lookahead_nr++] = c;\n }\n \n+/*\n+ * A commit can be a visual root when:\n+ *\n+ * - It has no parents.\n+ *\n+ * - It has parents but they are all filtered out and\n+ *   commit->parents arrives NULL.\n+ *\n+ * - It is not a boundary commit. Boundary commits also have no visible\n+ *   parents, but they are not selected as visual roots because they cannot\n+ *   cause the ambiguity of being vertically adjacent because:\n+ *\n+ *   1. A boundary only appears because an included commit is its child.\n+ *      Children are always above, and the renderer draws an edge down to\n+ *      the boundary from that child. Rather than starting a column like a\n+ *      visual root would do, it inherits its child column.\n+ *\n+ *   2. Included commits cannot appear below a boundary. Boundaries are\n+ *      ancestors of the exclusion point; if an included commit were an\n+ *      ancestor of the boundary it would be excluded and not rendered.\n+ *      Boundaries therefore always sink to the bottom.\n+ */\n+static int graph_is_visual_root_candidate(struct commit *c)\n+{\n+\treturn c->parents == NULL && !(c->object.flags & BOUNDARY);\n+}\n+\n+static int graph_is_visual_root(struct git_graph *graph,\n+\t\t\t\tstruct graph_lookahead_flags *flags)\n+{\n+\t/*\n+\t * This must be only called for the current commit as graph contains\n+\t * the state for the current commit only.\n+\t *\n+\t * To check if a commit is a visual root, call graph_is_visual_root_candidate()\n+\t * but we won't know if it is really a visual root until we get to the\n+\t * next commit state.\n+\t *\n+\t * The current commit is an actual visual root if it is a candidate and\n+\t * the commit is not a non-first parent of a merge.\n+\t *\n+\t *   *\n+\t *   |\\\n+\t *   | *    <- it is a visual root candidate but it shouldn't be indented\n+\t *   *         because it is already connected by an edge.\n+\t *   ^         if commit_in_columns && is_merge_parent means the commit\n+\t *   |         was put by a merge and is connected.\n+\t *   |\n+\t *   `-------- if !is_next_visible means we're on the last commit, avoid\n+\t *             indentation unless the one before is a visual root, then\n+\t *             we need to differentiate from the one above.\n+\t *\n+\t * If next_has_columns means that the next commit has\n+\t * already a column, so it will not be rendered below, the\n+\t * current commit has to act as the last commit and omit\n+\t * indentation.\n+\t */\n+\treturn graph_is_visual_root_candidate(graph->commit) &&\n+\t       !(graph->commit_in_columns &&\n+\t\t graph->columns[graph->commit_index].is_merge_parent) &&\n+\t       flags->is_next_visible &&\n+\t       (!flags->next_has_column || graph->visual_root_depth > 0);\n+}\n+\n+/*\n+ * Peeks the next commits via the lookahead buffer and sets the lookahead flags.\n+ */\n+static void graph_peek_next_visible(struct git_graph *graph,\n+\t\t\t\t    struct graph_lookahead_flags *flags)\n+{\n+\tflags->is_next_visible = 0;\n+\tflags->is_next_visual_root = 0;\n+\tflags->next_has_column = 0;\n+\n+\tif (!graph->lookahead_nr)\n+\t\treturn;\n+\n+\tflags->is_next_visible = 1;\n+\tflags->next_has_column =\n+\t\tgraph_find_new_column_by_commit(graph, graph->lookahead[0]) >= 0;\n+\n+\tif (!graph_is_visual_root_candidate(graph->lookahead[0]))\n+\t\treturn;\n+\n+\tif (graph->lookahead_nr >= 2)\n+\t\tflags->is_next_visual_root = 1;\n+}\n+\n+static int graph_needs_pre_root_line(struct git_graph *graph)\n+{\n+\treturn graph->commit_in_columns && graph->is_visual_root &&\n+\t       graph->num_columns > 0 && !graph->visual_root_cascade;\n+}\n+\n void graph_update(struct git_graph *graph, struct commit *commit)\n {\n \tstruct commit_list *parent;\n+\tstruct graph_lookahead_flags flags;\n \n \t/*\n \t * Set the new commit\n@@ -847,6 +1022,23 @@ void graph_update(struct git_graph *graph, struct commit *commit)\n \t */\n \tgraph_update_columns(graph);\n \n+\tgraph_peek_next_visible(graph, &flags);\n+\n+\tgraph->is_visual_root = graph_is_visual_root(graph, &flags);\n+\n+\tif (graph->is_visual_root) {\n+\t\t/*\n+\t\t * If next is a visual root we can omit the indent for the first\n+\t\t * visual root and start cascading.\n+\t\t */\n+\t\tif (!graph->visual_root_depth && flags.is_next_visual_root)\n+\t\t\tgraph->visual_root_cascade = 1;\n+\t\tgraph->visual_root_depth++;\n+\t} else {\n+\t\tgraph->visual_root_depth = 0;\n+\t\tgraph->visual_root_cascade = 0;\n+\t}\n+\n \tgraph->expansion_row = 0;\n \n \t/*\n@@ -864,11 +1056,16 @@ void graph_update(struct git_graph *graph, struct commit *commit)\n \t * room for it.  We need to do this only if there is a branch row\n \t * (or more) to the right of this commit.\n \t *\n+\t * If it is a visual root, we need to print an extra row to\n+\t * connect the indentation.\n+\t *\n \t * If there are less than 3 parents, we can immediately print the\n \t * commit line.\n \t */\n \tif (graph->state != GRAPH_PADDING)\n \t\tgraph->state = GRAPH_SKIP;\n+\telse if (graph_needs_pre_root_line(graph))\n+\t\tgraph->state = GRAPH_PRE_ROOT;\n \telse if (graph_needs_pre_commit_line(graph))\n \t\tgraph->state = GRAPH_PRE_COMMIT;\n \telse\n@@ -1116,6 +1313,17 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \n \t\tif (col_commit == graph->commit) {\n \t\t\tseen_this = 1;\n+\t\t\tif (graph->is_visual_root) {\n+\t\t\t\tint depth = graph->visual_root_depth;\n+\t\t\t\t/*\n+\t\t\t\t * Each visual column is 2 characters wide.\n+\t\t\t\t * Omit the indentation for the first visual\n+\t\t\t\t * root in cascade mode.\n+\t\t\t\t */\n+\t\t\t\tint padding = (depth - graph->visual_root_cascade) * 2;\n+\t\t\t\tgraph_line_addchars(line, ' ', padding);\n+\t\t\t\tgraph->width += padding;\n+\t\t\t}\n \t\t\tgraph_output_commit_char(graph, line);\n \n \t\t\tif (graph_needs_truncation(graph, i)) {\n@@ -1487,6 +1695,30 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n \t\tgraph_update_state(graph, GRAPH_PADDING);\n }\n \n+static void graph_output_pre_root_line(struct git_graph *graph, struct graph_line *line)\n+{\n+\t/*\n+\t * This function adds a row before a visual root, to connect the\n+\t * branch to the indented commit. It must only be called on a\n+\t * visual root.\n+\t */\n+\tif (!graph->is_visual_root)\n+\t\tBUG(\"commit must be a visual root to call pre_root_line\");\n+\n+\tfor (int i = 0; i < graph->num_columns; i++) {\n+\t\tstruct column *col = &graph->columns[i];\n+\t\tif (col->commit == graph->commit) {\n+\t\t\tgraph_line_addch(line, ' ');\n+\t\t\tgraph_line_write_column(line, col, '\\\\');\n+\t\t} else {\n+\t\t\tgraph_line_write_column(line, col, '|');\n+\t\t}\n+\t\tgraph_line_addch(line, ' ');\n+\t}\n+\n+\tgraph_update_state(graph, GRAPH_COMMIT);\n+}\n+\n int graph_next_line(struct git_graph *graph, struct strbuf *sb)\n {\n \tint shown_commit_line = 0;\n@@ -1512,6 +1744,9 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb)\n \tcase GRAPH_PRE_COMMIT:\n \t\tgraph_output_pre_commit_line(graph, &line);\n \t\tbreak;\n+\tcase GRAPH_PRE_ROOT:\n+\t\tgraph_output_pre_root_line(graph, &line);\n+\t\tbreak;\n \tcase GRAPH_COMMIT:\n \t\tgraph_output_commit_line(graph, &line);\n \t\tshown_commit_line = 1;\ndiff --git a/t/meson.build b/t/meson.build\nindex 7c3c070426..cce5ba71f9 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -577,6 +577,7 @@ integration_tests = [\n   't4215-log-skewed-merges.sh',\n   't4216-log-bloom.sh',\n   't4217-log-limit.sh',\n+  't4218-log-graph-indentation.sh',\n   't4219-log-follow-merge.sh',\n   't4252-am-options.sh',\n   't4253-am-keep-cr-dos.sh',\ndiff --git a/t/t4218-log-graph-indentation.sh b/t/t4218-log-graph-indentation.sh\nnew file mode 100755\nindex 0000000000..a4768cd3f6\n--- /dev/null\n+++ b/t/t4218-log-graph-indentation.sh\n@@ -0,0 +1,473 @@\n+#!/bin/sh\n+\n+test_description='git log --graph visual root indentations'\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-log-graph.sh\n+\n+check_graph_with_description () {\n+\tcat >expect &&\n+\tlib_test_cmp_graph --format=\"%s%ndescription%nsecond-line\" \"$@\"\n+}\n+\n+create_orphan () {\n+\tgit checkout --orphan \"$1\" &&\n+\ttest_might_fail git rm -rf .\n+}\n+\n+# disable commit-graph topo order to have the graph to render in different\n+# ways (used in --first-parent tests to have multiple visual roots while a\n+# column is active at the same time).\n+unset_commit_graph () {\n+\tsane_unset GIT_TEST_COMMIT_GRAPH &&\n+\trm -f .git/objects/info/commit-graph &&\n+\trm -rf .git/objects/info/commit-graphs\n+}\n+\n+test_expect_success 'single root commit is not indented' '\n+\tcreate_orphan _1 && test_commit 1_A &&\n+\tlib_test_check_graph _1 <<-\\EOF\n+\t* 1_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indented before unrelated branch' '\n+\tcreate_orphan _2 && test_commit 2_A && test_commit 2_B &&\n+\tcreate_orphan _3 && test_commit 3_A &&\n+\tlib_test_check_graph _2 _3 <<-\\EOF\n+\t  * 3_A\n+\t* 2_B\n+\t* 2_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indentation with --left-right' '\n+\tlib_test_check_graph --left-right _2..._3 <<-\\EOF\n+\t  > 3_A\n+\t< 2_B\n+\t< 2_A\n+\tEOF\n+'\n+\n+# A better case of why indentation is still needed with '--left-right' flag is\n+# that unrelated branches can be on the same side, so it's needed to\n+# differentiate visual roots on the same side.\n+test_expect_success 'visual root indentation with --left-right having unrelated commits on the same side' '\n+\tlib_test_check_graph --left-right _2..._3 _1 <<-\\EOF\n+\t  > 3_A\n+\t< 2_B\n+\t \\\n+\t  < 2_A\n+\t> 1_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indents the description also' '\n+\tcheck_graph_with_description _2 _3 <<-\\EOF\n+\t  * 3_A\n+\t    description\n+\t    second-line\n+\t* 2_B\n+\t| description\n+\t| second-line\n+\t* 2_A\n+\t  description\n+\t  second-line\n+\tEOF\n+'\n+\n+test_expect_success 'indented visual root parent gets connected to its child' '\n+\tcreate_orphan _4 && test_commit 4_A && test_commit 4_B &&\n+\tcreate_orphan _5 && test_commit 5_A && test_commit 5_B &&\n+\tlib_test_check_graph _4 _5 <<-\\EOF\n+\t* 5_B\n+\t \\\n+\t  * 5_A\n+\t* 4_B\n+\t* 4_A\n+\tEOF\n+'\n+\n+test_expect_success 'indented visual root parent gets connected to its child with description' '\n+\tcheck_graph_with_description _4 _5 <<-\\EOF\n+\t* 5_B\n+\t| description\n+\t| second-line\n+\t \\\n+\t  * 5_A\n+\t    description\n+\t    second-line\n+\t* 4_B\n+\t| description\n+\t| second-line\n+\t* 4_A\n+\t  description\n+\t  second-line\n+\tEOF\n+'\n+\n+test_expect_success 'visual roots cascade and last root does not' '\n+\tcreate_orphan _7 && test_commit 7_A && test_commit 7_B &&\n+\tcreate_orphan _8 && test_commit 8_A &&\n+\tcreate_orphan _9 && test_commit 9_A &&\n+\tcreate_orphan _10 && test_commit 10_A &&\n+\tlib_test_check_graph _7 _8 _9 _10 <<-\\EOF\n+\t* 10_A\n+\t  * 9_A\n+\t    * 8_A\n+\t* 7_B\n+\t* 7_A\n+\tEOF\n+'\n+\n+test_expect_success 'last root does not cascade' '\n+\tlib_test_check_graph _8 _9 _10 <<-\\EOF\n+\t* 10_A\n+\t  * 9_A\n+\t* 8_A\n+\tEOF\n+'\n+\n+test_expect_success 'merge parents are roots between them but they do not indent' '\n+\tcreate_orphan _11 && test_commit 11_A &&\n+\tcreate_orphan _12 && test_commit 12_A &&\n+\tcreate_orphan _13 && test_commit 13_A &&\n+\tgit checkout _11 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _11 -p _12 -p _13 -m 11_octopus) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _11 <<-\\EOF\n+\t*-.   11_octopus\n+\t|\\ \\\n+\t| | * 13_A\n+\t| * 12_A\n+\t* 11_A\n+\tEOF\n+'\n+\n+# The last parent of a merge can be indented if nothing related to it needs to\n+# be rendered after, if it's another visual root, merge parent must not get\n+# indented but rather activate cascading.\n+test_expect_success 'merge then unrelated visual root and unrelated branch' '\n+\tcreate_orphan _16 && test_commit 16_A && test_commit 16_B &&\n+\tcreate_orphan _17 && test_commit 17_A &&\n+\tcreate_orphan _18 && test_commit 18_A &&\n+\tcreate_orphan _19 && test_commit 19_A &&\n+\tcreate_orphan _20 && test_commit 20_A &&\n+\tgit checkout _18 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _18 -p _19 -p _20 -m 18_octopus) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _18 _17 _16 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t* 18_A\n+\t  * 17_A\n+\t* 16_B\n+\t* 16_A\n+\tEOF\n+'\n+\n+# The last commit root does not get indented, if the next thing after the root\n+# merge parent is the last commit, indent the merge parent.\n+test_expect_success 'merge then unrelated root indents merge parent' '\n+\tlib_test_check_graph _18 _17 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t \\\n+\t  * 18_A\n+\t* 17_A\n+\tEOF\n+'\n+\n+test_expect_success 'merge then unrelated branch indents merge parent' '\n+\tlib_test_check_graph _18 _16 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t \\\n+\t  * 18_A\n+\t* 16_B\n+\t* 16_A\n+\tEOF\n+'\n+\n+test_expect_success 'two-parent merge of orphans' '\n+\tcreate_orphan _21 && test_commit 21_A &&\n+\tcreate_orphan _22 && test_commit 22_A &&\n+\tgit checkout _21 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _21 -p _22 -m 21_merge) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _21 <<-\\EOF\n+\t*   21_merge\n+\t|\\\n+\t| * 22_A\n+\t* 21_A\n+\tEOF\n+'\n+\n+test_expect_success 'commit with filtered parent becomes a visual root' '\n+\tcreate_orphan _23 &&\n+\techo test >other.txt &&\n+\tgit add other.txt &&\n+\tgit commit -m \"23_A\" &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"23_B\" &&\n+\tcreate_orphan _24 &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"24_A\" &&\n+\tlib_test_check_graph _23 _24 -- foo.txt <<-\\EOF\n+\t  * 23_B\n+\t* 24_A\n+\tEOF\n+'\n+\n+test_expect_success 'filtered parent cascading edge case' '\n+\tcreate_orphan _27 &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\ttest_tick &&\n+\tgit commit -m \"D (last)\" &&\n+\n+\tcreate_orphan _25 &&\n+\techo test >other.txt &&\n+\tgit add other.txt &&\n+\ttest_tick &&\n+\tgit commit -m \"C-filtered\" &&\n+\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\ttest_tick &&\n+\tgit commit -m \"B (child of filtered)\" &&\n+\n+\tcreate_orphan _26 &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\ttest_tick &&\n+\tgit commit -m \"A (visual root)\" &&\n+\n+\tlib_test_check_graph _25 _26 _27 -- foo.txt <<-\\EOF\n+\t* A (visual root)\n+\t  * B (child of filtered)\n+\t* D (last)\n+\tEOF\n+'\n+\n+test_expect_success 'multiple filtered parents in sequence' '\n+\tcreate_orphan _44 &&\n+\techo a >other.txt && git add other.txt && git commit -m \"44_F\" &&\n+\techo b >foo.txt && git add foo.txt && git commit -m \"44_C\" &&\n+\n+\tcreate_orphan _45 &&\n+\techo c >other.txt && git add other.txt && git commit -m \"45_F\" &&\n+\techo d >foo.txt && git add foo.txt && git commit -m \"45_C\" &&\n+\n+\tcreate_orphan _46 &&\n+\techo e >foo.txt && git add foo.txt && git commit -m \"46_A\" &&\n+\n+\tlib_test_check_graph _44 _45 _46 -- foo.txt <<-\\EOF\n+\t* 44_C\n+\t  * 45_C\n+\t* 46_A\n+\tEOF\n+'\n+\n+# These tests prove why there is no need to have indentation for boundary\n+# commits.\n+#\n+# Boundary commits rather than starting a column they 'inherit' the one of\n+# its child so there will always be an edge that connects it removing the\n+# ambiguity.\n+test_expect_success 'unrelated boundaries are not ambiguous' '\n+\tcreate_orphan _28 && test_commit 28_A && test_commit 28_B &&\n+\ttest_commit 28_C &&\n+\tcreate_orphan _29 && test_commit 29_A && test_commit 29_B &&\n+\tlib_test_check_graph --boundary 28_A.._28 29_A.._29 <<-\\EOF\n+\t* 29_B\n+\t| * 28_C\n+\t| * 28_B\n+\t| o 28_A\n+\to 29_A\n+\tEOF\n+'\n+\n+# Same structure as t6016\n+test_expect_success 'boundary commits big test' '\n+\t# 3 commits on branch _30\n+\tcreate_orphan _30 &&\n+\ttest_commit 30_A &&\n+\ttest_commit 30_B &&\n+\ttest_commit 30_C &&\n+\n+\t# 2 commits on branch _31, started from 30_A\n+\tgit checkout -b _31 30_A &&\n+\ttest_commit 31_A &&\n+\ttest_commit 31_B &&\n+\n+\t# 2 commits on branch _32, started from 30_B\n+\tgit checkout -b _32 30_B &&\n+\ttest_commit 32_A &&\n+\ttest_commit 32_B &&\n+\n+\t# Octopus merge _31 and _32 into -30\n+\tgit checkout _30 &&\n+\tgit merge _31 _32 -m 30_D &&\n+\tgit tag 30_D &&\n+\ttest_commit 30_E &&\n+\n+\t# More commits on _32, then merge _32 into _30\n+\tgit checkout _32 &&\n+\ttest_commit 32_C &&\n+\ttest_commit 32_D &&\n+\tgit checkout _30 &&\n+\tgit merge -s ours _32 -m 30_F &&\n+\tgit tag 30_F &&\n+\ttest_commit 30_G &&\n+\tlib_test_check_graph --boundary _30 _31 _32 ^32_C <<-\\EOF\n+\t* 30_G\n+\t*   30_F\n+\t|\\\n+\t| * 32_D\n+\t* | 30_E\n+\t| |\n+\t|  \\\n+\t*-. \\   30_D\n+\t|\\ \\ \\\n+\t| * | | 31_B\n+\t| * | | 31_A\n+\t* | | | 30_C\n+\to | | | 30_B\n+\t|/ / /\n+\to / / 30_A\n+\t / /\n+\t| o 32_C\n+\t|/\n+\to 32_B\n+\tEOF\n+'\n+\n+# Filter by --first-parent and then forcing the filtered parents to be shown.\n+test_expect_success '--first-parent flag with the filtered parents' '\n+\t(\n+\t\tunset_commit_graph &&\n+\t\tcreate_orphan _35 && test_commit 35_A && test_commit 35_B &&\n+\t\tcreate_orphan _36 && test_commit 36_A &&\n+\t\tcreate_orphan _37 && test_commit 37_A &&\n+\t\tgit checkout _35 &&\n+\t\tTREE=$(git write-tree) &&\n+\t\tMERGE=$(git commit-tree $TREE -p _35 -p _36 -p _37 -m 35_octopus) &&\n+\t\tgit reset --hard $MERGE &&\n+\t\tlib_test_check_graph --first-parent _35 _36 _37 <<-\\EOF\n+\t\t* 35_octopus\n+\t\t| * 37_A\n+\t\t|   * 36_A\n+\t\t* 35_B\n+\t\t* 35_A\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success '--first-parent with filtered parents but one has a child' '\n+\t(\n+\t\tunset_commit_graph &&\n+\t\tcreate_orphan _38 && test_commit 38_A && test_commit 38_B &&\n+\t\tcreate_orphan _39 && test_commit 39_A &&\n+\t\tcreate_orphan _40 && test_commit 40_A && test_commit 40_B &&\n+\t\tgit checkout _38 &&\n+\t\tTREE=$(git write-tree) &&\n+\t\tMERGE=$(git commit-tree $TREE -p _38 -p _39 -p _40 -m 38_octopus) &&\n+\t\tgit reset --hard $MERGE &&\n+\t\tlib_test_check_graph --first-parent _38 _39 _40 <<-\\EOF\n+\t\t* 38_octopus\n+\t\t| * 40_B\n+\t\t| * 40_A\n+\t\t|   * 39_A\n+\t\t* 38_B\n+\t\t* 38_A\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success '--first-parent with filtered parents but both have children' '\n+\t(\n+\t\tunset_commit_graph &&\n+\t\tcreate_orphan _41 && test_commit 41_A && test_commit 41_B &&\n+\t\tcreate_orphan _42 && test_commit 42_A && test_commit 42_B &&\n+\t\tcreate_orphan _43 && test_commit 43_A && test_commit 43_B &&\n+\t\tgit checkout _41 &&\n+\t\tTREE=$(git write-tree) &&\n+\t\tMERGE=$(git commit-tree $TREE -p _41 -p _42 -p _43 -m 41_octopus) &&\n+\t\tgit reset --hard $MERGE &&\n+\t\tlib_test_check_graph --first-parent _41 _42 _43 <<-\\EOF\n+\t\t* 41_octopus\n+\t\t| * 43_B\n+\t\t|  \\\n+\t\t|   * 43_A\n+\t\t| * 42_B\n+\t\t| * 42_A\n+\t\t* 41_B\n+\t\t* 41_A\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'two unrelated merges' '\n+\tcreate_orphan _50 && test_commit 50_A &&\n+\tgit checkout -b _51 &&\n+\ttest_commit 51_A && test_commit 51_B &&\n+\tgit checkout _50 &&\n+\tgit merge --no-ff _51 -m 50_B &&\n+\n+\tcreate_orphan _52 && test_commit 52_A &&\n+\tgit checkout -b _53 &&\n+\ttest_commit 53_A && test_commit 53_B &&\n+\tgit checkout _52 &&\n+\tgit merge --no-ff _53 -m 52_B &&\n+\n+\tlib_test_check_graph _52 _50 <<-\\EOF\n+\t*   52_B\n+\t|\\\n+\t| * 53_B\n+\t| * 53_A\n+\t|/\n+\t \\\n+\t  * 52_A\n+\t*   50_B\n+\t|\\\n+\t| * 51_B\n+\t| * 51_A\n+\t|/\n+\t* 50_A\n+\tEOF\n+'\n+\n+test_expect_success '--max-count treats the last visible commit as the last commit' '\n+\tlib_test_check_graph --max-count=2 _8 _9 _10 <<-\\EOF\n+\t  * 10_A\n+\t* 9_A\n+\tEOF\n+'\n+\n+test_expect_success '--max-count=1 shows a single root without indentation' '\n+\tlib_test_check_graph --max-count=1 _8 _9 _10 <<-\\EOF\n+\t* 10_A\n+\tEOF\n+'\n+\n+test_expect_success '--max-count-oldest indents visual roots' '\n+\tlib_test_check_graph --max-count-oldest=3 _8 _9 _10 <<-\\EOF\n+\t* 10_A\n+\t  * 9_A\n+\t* 8_A\n+\tEOF\n+'\n+\n+test_done\n\n-- \n2.54.0\n"},{"id":"547789","messageId":"alEroo_DhFaWm3DH@exploit","threadId":"65419","inReplyTo":"20260710-ps-pre-commit-indent-v8-4-d3b636463bf4@gmail.com","subject":"Re: [PATCH v8 4/4] graph: indent visual root in graph","fromName":"Mirko Faina","fromEmail":"mroik@delayed.space","sentAt":"2026-07-10T18:07:07Z","receivedAt":"2026-07-10T18:14:57Z","isPatch":true,"body":"On Fri, Jul 10, 2026 at 12:37:07PM +0200, Pablo Sabater wrote:\n> When rendering a graph, if the history contains multiple \"visual roots\",\n> actual roots or commits that look like roots (i.e. have their parents\n> filtered out) can end up being vertically adjacent to unrelated commits,\n> falsely appearing to be related.\n> \n> A fix for this issue was already attempted [1] a while ago.\n> \n> This happens because the commits fill the space from left to right and\n> when a visual root ends, its column becomes free for the following\n> commit even if they are not related. Once this happens the unrelated\n> commit is rendered below the visual root. Because there is no special\n> character or way to identify when a visual root is rendered making the\n> graph confusing.\n> \n> By indenting the visual roots when there are still commits to show the\n> vertical adjacency can be avoided.\n> \n> Add is_visual_root flag to git_graph making it visible in all graph states,\n> give graph_update() a new function, graph_is_visual_root() to know if the\n> current commit is a visual root and set is_visual_root.\n> The different handled cases are:\n> \n> - If a visual root has children: similar to GRAPH_PRE_COMMIT state when\n>   octopus merges need space, an edge row needs to be printed to connect\n>   the child with the indented visual root. A new state GRAPH_PRE_ROOT is\n>   needed to connect the child with the visual root:\n> \n>     * child of the visual root\n>      \\ GRAPH_PRE_ROOT\n>       * visual root indented\n> \n> - If a visual root is child-less we can skip GRAPH_PRE_ROOT state and\n>   render the indented commit directly.\n> \n>       * visual root indented\n>     * unrelated commit\n> \n> - If two or more visual roots are adjacent: by having a lookahead to the\n>   next commit that will be rendered, if the next commit is also a visual\n>   root and we are on a visual root, meaning two visual root adjacent in\n>   the history, the top one can omit the indent, making the one below to\n>   indent only once, if there are more adjacent visual commits, the\n>   indentation will increase for each adjacent one, cascading.\n> \n>     * visual root\n>       * visual root\n>         * visual root\n>     * last commit\n> \n>   Even if the last commit is a root, because there is nothing that will be\n>   rendered below we can omit the indentation on purpose.\n> \n> [1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n> \n> Helped-by: Kristofer Karlsson <krka@spotify.com>\n> Mentored-by: Karthik Nayak <karthik.188@gmail.com>\n> Mentored-by: Chandra Pratap <chandrapratap3519@gmail.com>\n> Signed-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n> ---\n>  graph.c                          | 235 +++++++++++++++++++\n>  t/meson.build                    |   1 +\n>  t/t4218-log-graph-indentation.sh | 473 +++++++++++++++++++++++++++++++++++++++\n>  3 files changed, 709 insertions(+)\n\nThis doesn't seem to work for every visual root e.g.\n\n    git log --graph --oneline --author=\"Mirko Faina\"\n\nThe visual roots are not indented.\n\n> +/*\n> + * A commit can be a visual root when:\n> + *\n> + * - It has no parents.\n> + *\n> + * - It has parents but they are all filtered out and\n> + *   commit->parents arrives NULL.\n> + *\n> + * - It is not a boundary commit. Boundary commits also have no visible\n> + *   parents, but they are not selected as visual roots because they cannot\n> + *   cause the ambiguity of being vertically adjacent because:\n> + *\n> + *   1. A boundary only appears because an included commit is its child.\n> + *      Children are always above, and the renderer draws an edge down to\n> + *      the boundary from that child. Rather than starting a column like a\n> + *      visual root would do, it inherits its child column.\n> + *\n> + *   2. Included commits cannot appear below a boundary. Boundaries are\n> + *      ancestors of the exclusion point; if an included commit were an\n> + *      ancestor of the boundary it would be excluded and not rendered.\n> + *      Boundaries therefore always sink to the bottom.\n> + */\n> +static int graph_is_visual_root_candidate(struct commit *c)\n> +{\n> +\treturn c->parents == NULL && !(c->object.flags & BOUNDARY);\n> +}\n\nI suspect this behaviour is due to these assumptions being too strict.\n\nWhen we use the --author option the parents are not filtered out, so it\ndoesn't return NULL desipte being a visual root. We realize it is a\nvisual root only on the next commit, but once we are on the next commit\nwe can't indent as we have already printed this commit.\n\nWe realize only on the next commit after hitting simplify_commit(), it\ncalls get_commit_action() and checks if should keep the commit based on\nthe regex we provided. If the regex is not matched the commit is just\nignored (we do not filter parents based on regex when we expand a topo\nwalk).\n\nAt least that's what I gather, if anyone can confirm this...\n"},{"id":"547797","messageId":"CAN5EUNQNFsUZD=7yLMo0q4hgNEdWxX+fifG+zxJeL+-eRKSDuw@mail.gmail.com","threadId":"65419","inReplyTo":"alEroo_DhFaWm3DH@exploit","subject":"Re: [PATCH v8 4/4] graph: indent visual root in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-10T20:29:44Z","receivedAt":"2026-07-10T20:29:56Z","isPatch":true,"body":"El vie, 10 jul 2026 a las 20:07, Mirko Faina (<mroik@delayed.space>) escribió:\n>\n> On Fri, Jul 10, 2026 at 12:37:07PM +0200, Pablo Sabater wrote:\n> > When rendering a graph, if the history contains multiple \"visual roots\",\n> > actual roots or commits that look like roots (i.e. have their parents\n> > filtered out) can end up being vertically adjacent to unrelated commits,\n> > falsely appearing to be related.\n> >\n> > A fix for this issue was already attempted [1] a while ago.\n> >\n> > This happens because the commits fill the space from left to right and\n> > when a visual root ends, its column becomes free for the following\n> > commit even if they are not related. Once this happens the unrelated\n> > commit is rendered below the visual root. Because there is no special\n> > character or way to identify when a visual root is rendered making the\n> > graph confusing.\n> >\n> > By indenting the visual roots when there are still commits to show the\n> > vertical adjacency can be avoided.\n> >\n> > Add is_visual_root flag to git_graph making it visible in all graph states,\n> > give graph_update() a new function, graph_is_visual_root() to know if the\n> > current commit is a visual root and set is_visual_root.\n> > The different handled cases are:\n> >\n> > - If a visual root has children: similar to GRAPH_PRE_COMMIT state when\n> >   octopus merges need space, an edge row needs to be printed to connect\n> >   the child with the indented visual root. A new state GRAPH_PRE_ROOT is\n> >   needed to connect the child with the visual root:\n> >\n> >     * child of the visual root\n> >      \\ GRAPH_PRE_ROOT\n> >       * visual root indented\n> >\n> > - If a visual root is child-less we can skip GRAPH_PRE_ROOT state and\n> >   render the indented commit directly.\n> >\n> >       * visual root indented\n> >     * unrelated commit\n> >\n> > - If two or more visual roots are adjacent: by having a lookahead to the\n> >   next commit that will be rendered, if the next commit is also a visual\n> >   root and we are on a visual root, meaning two visual root adjacent in\n> >   the history, the top one can omit the indent, making the one below to\n> >   indent only once, if there are more adjacent visual commits, the\n> >   indentation will increase for each adjacent one, cascading.\n> >\n> >     * visual root\n> >       * visual root\n> >         * visual root\n> >     * last commit\n> >\n> >   Even if the last commit is a root, because there is nothing that will be\n> >   rendered below we can omit the indentation on purpose.\n> >\n> > [1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n> >\n> > Helped-by: Kristofer Karlsson <krka@spotify.com>\n> > Mentored-by: Karthik Nayak <karthik.188@gmail.com>\n> > Mentored-by: Chandra Pratap <chandrapratap3519@gmail.com>\n> > Signed-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n> > ---\n> >  graph.c                          | 235 +++++++++++++++++++\n> >  t/meson.build                    |   1 +\n> >  t/t4218-log-graph-indentation.sh | 473 +++++++++++++++++++++++++++++++++++++++\n> >  3 files changed, 709 insertions(+)\n>\n> This doesn't seem to work for every visual root e.g.\n>\n>     git log --graph --oneline --author=\"Mirko Faina\"\n>\n> The visual roots are not indented.\n>\n> > +/*\n> > + * A commit can be a visual root when:\n> > + *\n> > + * - It has no parents.\n> > + *\n> > + * - It has parents but they are all filtered out and\n> > + *   commit->parents arrives NULL.\n> > + *\n> > + * - It is not a boundary commit. Boundary commits also have no visible\n> > + *   parents, but they are not selected as visual roots because they cannot\n> > + *   cause the ambiguity of being vertically adjacent because:\n> > + *\n> > + *   1. A boundary only appears because an included commit is its child.\n> > + *      Children are always above, and the renderer draws an edge down to\n> > + *      the boundary from that child. Rather than starting a column like a\n> > + *      visual root would do, it inherits its child column.\n> > + *\n> > + *   2. Included commits cannot appear below a boundary. Boundaries are\n> > + *      ancestors of the exclusion point; if an included commit were an\n> > + *      ancestor of the boundary it would be excluded and not rendered.\n> > + *      Boundaries therefore always sink to the bottom.\n> > + */\n> > +static int graph_is_visual_root_candidate(struct commit *c)\n> > +{\n> > +     return c->parents == NULL && !(c->object.flags & BOUNDARY);\n> > +}\n>\n> I suspect this behaviour is due to these assumptions being too strict.\n>\n> When we use the --author option the parents are not filtered out, so it\n> doesn't return NULL desipte being a visual root. We realize it is a\n> visual root only on the next commit, but once we are on the next commit\n> we can't indent as we have already printed this commit.\n>\n> We realize only on the next commit after hitting simplify_commit(), it\n> calls get_commit_action() and checks if should keep the commit based on\n> the regex we provided. If the regex is not matched the commit is just\n> ignored (we do not filter parents based on regex when we expand a topo\n> walk).\n>\n> At least that's what I gather, if anyone can confirm this...\n\nHi!\n\nYes, I just tried with the same:\n  git log --graph --oneline --author=\"Mirko Faina\"\n\nAnd no indentation sadly, if when we use --author the parents are not\nexcluded then the c->parents is not enough.\nI think that it should be fine if we iterate each parent and call\ngraph_is_interesting() that also calls get_commit_action() as a\nfallback.\nSomething like:\n\ngraph_is_visual_root_candidate():\n\n/* We keep ignoring boundary commits */\nif (c->object.flags & BOUNDARY)\n        return 0;\n/* Check the parents if they are not excluded because of options like\n--author */\nfor (p = c->parents; p; p = p->next)\n        if(graph_is_interesting(graph, p->item))\n                return 0;\n\nreturn 1;\n\nI haven't tried yet though, but if --author has this problem, probably\nother options like --grep would likely fail too because of the same\nreason.\n\nThanks for the feedback,\nPablo\n"},{"id":"547831","messageId":"20260711-ps-pre-commit-indent-v9-0-eab6676e82f7@gmail.com","threadId":"65419","inReplyTo":"20260710-ps-pre-commit-indent-v8-0-d3b636463bf4@gmail.com","subject":"[PATCH v9 0/4] graph: indent visual roots in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-11T13:37:49Z","receivedAt":"2026-07-11T13:38:01Z","isPatch":true,"body":"When rendering a graph, if the history contains multiple \"visual roots\",\nactual roots or commits that look like roots (i.e. have their parents\nfiltered out) can end up being vertically adjacent to unrelated commits,\nfalsely appearing to be related.\n\nA fix for this issue was already attempted [1] a while ago.\n\nThis series adds indentation to the visual root commits, so they cannot be\nvertically adjacent anymore making it easier to identify them.\n\nBefore indentation:\n\n\t* A\n\t* B1\n\t* B2\n\t* C1\n\t* C2\n\nAfter indentation:\n\n\t  * A\n\t* B1\n\t \\\n\t  * B2\n\t* C1\n\t* C2\n\nIndents the visual root commits that have still commits to show after\nthem, and if they have children it connects them with an edge at a new\nrow.\n\nIf there are multiple visual roots adjacent in history, the indentation\nstarts with the second one, avoiding redundant indentation of the first\none and cascades after the second.\n\n\t* A\n\t  * B\n\t    * C\n\t* D1\n\t* D2\n\nThis series first commit is a cleanup that brings a common function\nfrom t4215 and t6016 to a graph functions file which they both use, so\nthe new test file for indentation, t4218, can use it as well.\n\nGitHub CI: https://github.com/pabloosabaterr/git/actions/runs/29154333559\n\n[1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n\nV8 DIFF:\n\n- Checking if the parents of a commit are NULL is not enough to know if\n  the commit is a visual root due to options that filter the commit\n  parents but they do not remove them (--author, --grep, etc).\n  At graph_is_visual_root_candidate(), iterate the parents and call\n  graph_is_interesting() for each of them to know whether they will be\n  shown or not.\n- Add a --author and a --grep test.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n\n---\nPablo Sabater (4):\n      lib-log-graph: move check_graph function\n      revision: add next_commit_to_show()\n      graph: add a 2 commit buffer for lookahead\n      graph: indent visual root in graph\n\n graph.c                                    | 295 +++++++++++++++++\n graph.h                                    |  17 +\n revision.c                                 |  48 ++-\n t/lib-log-graph.sh                         |   5 +\n t/meson.build                              |   1 +\n t/t4215-log-skewed-merges.sh               |  33 +-\n t/t4218-log-graph-indentation.sh           | 514 +++++++++++++++++++++++++++++\n t/t6016-rev-list-graph-simplify-history.sh |  25 +-\n 8 files changed, 893 insertions(+), 45 deletions(-)\n\nRange-diff versus v8:\n\n1:  ce4f6419c2 = 1:  22ab444372 lib-log-graph: move check_graph function\n2:  8c7326745e = 2:  ebb88c8b29 revision: add next_commit_to_show()\n3:  f2e895c72b = 3:  0705ee321e graph: add a 2 commit buffer for lookahead\n4:  90d5d22344 ! 4:  fa2e60fb3f graph: indent visual root in graph\n    @@ graph.c: void graph_push_lookahead(struct git_graph *graph, struct commit *c)\n     + * - It has parents but they are all filtered out and\n     + *   commit->parents arrives NULL.\n     + *\n    ++ * - Its parents are uninteresting.\n    ++ *\n     + * - It is not a boundary commit. Boundary commits also have no visible\n     + *   parents, but they are not selected as visual roots because they cannot\n     + *   cause the ambiguity of being vertically adjacent because:\n    @@ graph.c: void graph_push_lookahead(struct git_graph *graph, struct commit *c)\n     + *      ancestor of the boundary it would be excluded and not rendered.\n     + *      Boundaries therefore always sink to the bottom.\n     + */\n    -+static int graph_is_visual_root_candidate(struct commit *c)\n    ++static int graph_is_visual_root_candidate(struct commit *c, struct git_graph *graph)\n     +{\n    -+\treturn c->parents == NULL && !(c->object.flags & BOUNDARY);\n    ++\tstruct commit_list *p;\n    ++\n    ++\tif (c->object.flags & BOUNDARY)\n    ++\t\treturn 0;\n    ++\tfor (p = c->parents; p; p = p->next)\n    ++\t\tif (graph_is_interesting(graph, p->item))\n    ++\t\t\treturn 0;\n    ++\treturn 1;\n     +}\n     +\n     +static int graph_is_visual_root(struct git_graph *graph,\n    @@ graph.c: void graph_push_lookahead(struct git_graph *graph, struct commit *c)\n     +\t * current commit has to act as the last commit and omit\n     +\t * indentation.\n     +\t */\n    -+\treturn graph_is_visual_root_candidate(graph->commit) &&\n    ++\treturn graph_is_visual_root_candidate(graph->commit, graph) &&\n     +\t       !(graph->commit_in_columns &&\n     +\t\t graph->columns[graph->commit_index].is_merge_parent) &&\n     +\t       flags->is_next_visible &&\n    @@ graph.c: void graph_push_lookahead(struct git_graph *graph, struct commit *c)\n     +\tflags->next_has_column =\n     +\t\tgraph_find_new_column_by_commit(graph, graph->lookahead[0]) >= 0;\n     +\n    -+\tif (!graph_is_visual_root_candidate(graph->lookahead[0]))\n    ++\tif (!graph_is_visual_root_candidate(graph->lookahead[0], graph))\n     +\t\treturn;\n     +\n     +\tif (graph->lookahead_nr >= 2)\n    @@ t/t4218-log-graph-indentation.sh (new)\n     +\tEOF\n     +'\n     +\n    ++# when the graph commits are filtered with regex options like --author, the\n    ++# commit parents do not come NULL so it is needed to check if the parents are\n    ++# interesting.\n    ++test_expect_success '--author skipped parent makes a visual root' '\n    ++\tcreate_orphan _55 &&\n    ++\ttest_tick &&\n    ++\tgit commit --allow-empty -m 55_A &&\n    ++\tcreate_orphan _54 &&\n    ++\ttest_tick &&\n    ++\tgit commit --allow-empty --author=\"Other <other@example.com>\" -m 54_A &&\n    ++\ttest_tick &&\n    ++\tgit commit --allow-empty -m 54_B &&\n    ++\ttest_tick &&\n    ++\tgit commit --allow-empty -m 54_C &&\n    ++\tlib_test_check_graph --author=\"A U Thor\" _54 _55 <<-\\EOF\n    ++\t* 54_C\n    ++\t \\\n    ++\t  * 54_B\n    ++\t* 55_A\n    ++\tEOF\n    ++'\n    ++\n    ++test_expect_success '--grep skipped parent makes a visual root' '\n    ++\tcreate_orphan _57 &&\n    ++\ttest_tick &&\n    ++\tgit commit --allow-empty -m 57_keep_A &&\n    ++\tcreate_orphan _56 &&\n    ++\ttest_tick &&\n    ++\tgit commit --allow-empty -m 56_skip &&\n    ++\ttest_tick &&\n    ++\tgit commit --allow-empty -m 56_keep_A &&\n    ++\ttest_tick &&\n    ++\tgit commit --allow-empty -m 56_keep_B &&\n    ++\tlib_test_check_graph --grep=keep _56 _57 <<-\\EOF\n    ++\t* 56_keep_B\n    ++\t \\\n    ++\t  * 56_keep_A\n    ++\t* 57_keep_A\n    ++\tEOF\n    ++'\n    ++\n     +test_done\n\n---\nbase-commit: f85a7e662054a7b0d9070e432508831afa214b47\n"},{"id":"547832","messageId":"20260711-ps-pre-commit-indent-v9-1-eab6676e82f7@gmail.com","threadId":"65419","inReplyTo":"20260711-ps-pre-commit-indent-v9-0-eab6676e82f7@gmail.com","subject":"[PATCH v9 1/4] lib-log-graph: move check_graph function","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-11T13:37:50Z","receivedAt":"2026-07-11T13:38:03Z","isPatch":true,"body":"check_graph is a function shared in the test files t4215 and t6016 used\nto format the output graph, but instead of being in a file called by\nboth test, the function code is repeated in each file.\n\nMove check_graph to lib-log-graph.sh file which both tests already\nimport graph functions from, renaming it to lib_test_check_graph.\n\nThis function is needed for the following commit which includes graph\ntests in a new file and requires check_graph.\n\nMentored-by: Karthik Nayak <karthik.188@gmail.com>\nMentored-by: Chandra Pratap <chandrapratap3519@gmail.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n t/lib-log-graph.sh                         |  5 +++++\n t/t4215-log-skewed-merges.sh               | 33 +++++++++++++-----------------\n t/t6016-rev-list-graph-simplify-history.sh | 25 +++++++++-------------\n 3 files changed, 29 insertions(+), 34 deletions(-)\n\ndiff --git a/t/lib-log-graph.sh b/t/lib-log-graph.sh\nindex bf952ef920..1eae8f60c2 100644\n--- a/t/lib-log-graph.sh\n+++ b/t/lib-log-graph.sh\n@@ -26,3 +26,8 @@ lib_test_cmp_colored_graph () {\n \ttest_decode_color <output.colors.raw | sed \"s/ *\\$//\" >output.colors &&\n \ttest_cmp expect.colors output.colors\n }\n+\n+lib_test_check_graph () {\n+\tcat >expect &&\n+\tlib_test_cmp_graph --format=%s \"$@\"\n+}\ndiff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh\nindex 1612f05f1b..eebab71039 100755\n--- a/t/t4215-log-skewed-merges.sh\n+++ b/t/t4215-log-skewed-merges.sh\n@@ -5,11 +5,6 @@ test_description='git log --graph of skewed merges'\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-log-graph.sh\n \n-check_graph () {\n-\tcat >expect &&\n-\tlib_test_cmp_graph --format=%s \"$@\"\n-}\n-\n test_expect_success 'log --graph with merge fusing with its left and right neighbors' '\n \tgit checkout --orphan _p &&\n \ttest_commit A &&\n@@ -21,7 +16,7 @@ test_expect_success 'log --graph with merge fusing with its left and right neigh\n \tgit checkout _p && git merge --no-ff _r -m G &&\n \tgit checkout @^^ && git merge --no-ff _p -m H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   H\n \t|\\\n \t| *   G\n@@ -49,7 +44,7 @@ test_expect_success 'log --graph with left-skewed merge' '\n \tgit checkout 0_p && git merge --no-ff 0_s -m 0_G &&\n \tgit checkout @^ && git merge --no-ff 0_q 0_r 0_t 0_p -m 0_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*-----.   0_H\n \t|\\ \\ \\ \\\n \t| | | | * 0_G\n@@ -83,7 +78,7 @@ test_expect_success 'log --graph with nested left-skewed merge' '\n \tgit checkout 1_p && git merge --no-ff 1_r -m 1_G &&\n \tgit checkout @^^ && git merge --no-ff 1_p -m 1_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   1_H\n \t|\\\n \t| *   1_G\n@@ -115,7 +110,7 @@ test_expect_success 'log --graph with nested left-skewed merge following normal\n \tgit checkout -b 2_s @^^ && git merge --no-ff 2_q -m 2_J &&\n \tgit checkout 2_p && git merge --no-ff 2_s -m 2_K &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   2_K\n \t|\\\n \t| *   2_J\n@@ -151,7 +146,7 @@ test_expect_success 'log --graph with nested right-skewed merge following left-s\n \tgit checkout 3_p && git merge --no-ff 3_r -m 3_H &&\n \tgit checkout @^^ && git merge --no-ff 3_p -m 3_J &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   3_J\n \t|\\\n \t| *   3_H\n@@ -182,7 +177,7 @@ test_expect_success 'log --graph with right-skewed merge following a left-skewed\n \tgit merge --no-ff 4_p -m 4_G &&\n \tgit checkout @^^ && git merge --no-ff 4_s -m 4_H &&\n \n-\tcheck_graph --date-order <<-\\EOF\n+\tlib_test_check_graph --date-order <<-\\EOF\n \t*   4_H\n \t|\\\n \t| *   4_G\n@@ -218,7 +213,7 @@ test_expect_success 'log --graph with octopus merge with column joining its penu\n \tgit checkout 5_r &&\n \tgit merge --no-ff 5_s -m 5_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   5_H\n \t|\\\n \t| *-.   5_G\n@@ -257,7 +252,7 @@ test_expect_success 'log --graph with multiple tips' '\n \tgit checkout 6_1 &&\n \tgit merge --no-ff 6_2 -m 6_I &&\n \n-\tcheck_graph 6_1 6_3 6_5 <<-\\EOF\n+\tlib_test_check_graph 6_1 6_3 6_5 <<-\\EOF\n \t*   6_I\n \t|\\\n \t| | *   6_H\n@@ -334,7 +329,7 @@ test_expect_success 'log --graph with multiple tips' '\n \tgit checkout -b M_7 7_1 &&\n \tgit merge --no-ff 7_2 7_3 -m 7_M4 &&\n \n-\tcheck_graph M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -371,7 +366,7 @@ test_expect_success 'log --graph with multiple tips' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' '\n-\tcheck_graph --graph-lane-limit=2 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=2 M_7 <<-\\EOF\n \t*-.   7_M4\n \t|\\ \\\n \t| | * 7_G\n@@ -388,7 +383,7 @@ test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=1 truncate mid octopus merge' '\n-\tcheck_graph --graph-lane-limit=1 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=1 M_7 <<-\\EOF\n \t*-~  7_M4\n \t|\\~\n \t| ~ 7_G\n@@ -405,7 +400,7 @@ test_expect_success 'log --graph --graph-lane-limit=1 truncate mid octopus merge\n '\n \n test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' '\n-\tcheck_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -441,7 +436,7 @@ test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=6 check if it only shows first of 3 parent merge' '\n-\tcheck_graph --graph-lane-limit=6 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=6 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -478,7 +473,7 @@ test_expect_success 'log --graph --graph-lane-limit=6 check if it only shows fir\n '\n \n test_expect_success 'log --graph --graph-lane-limit=7 check if it shows all 3 parent merge' '\n-\tcheck_graph --graph-lane-limit=7 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=7 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\ndiff --git a/t/t6016-rev-list-graph-simplify-history.sh b/t/t6016-rev-list-graph-simplify-history.sh\nindex 54b0a6f5f8..e0d9c3c1ac 100755\n--- a/t/t6016-rev-list-graph-simplify-history.sh\n+++ b/t/t6016-rev-list-graph-simplify-history.sh\n@@ -13,11 +13,6 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-log-graph.sh\n \n-check_graph () {\n-\tcat >expect &&\n-\tlib_test_cmp_graph --format=%s \"$@\"\n-}\n-\n test_expect_success 'set up rev-list --graph test' '\n \t# 3 commits on branch A\n \ttest_commit A1 foo.txt &&\n@@ -54,7 +49,7 @@ test_expect_success 'set up rev-list --graph test' '\n '\n \n test_expect_success '--graph --all' '\n-\tcheck_graph --all <<-\\EOF\n+\tlib_test_check_graph --all <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -82,7 +77,7 @@ test_expect_success '--graph --all' '\n # that undecorated merges are interesting, even with --simplify-by-decoration\n test_expect_success '--graph --simplify-by-decoration' '\n \tgit tag -d A4 &&\n-\tcheck_graph --all --simplify-by-decoration <<-\\EOF\n+\tlib_test_check_graph --all --simplify-by-decoration <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -114,7 +109,7 @@ test_expect_success 'setup: get rid of decorations on B' '\n \n # Graph with branch B simplified away\n test_expect_success '--graph --simplify-by-decoration prune branch B' '\n-\tcheck_graph --simplify-by-decoration --all <<-\\EOF\n+\tlib_test_check_graph --simplify-by-decoration --all <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -133,7 +128,7 @@ test_expect_success '--graph --simplify-by-decoration prune branch B' '\n '\n \n test_expect_success '--graph --full-history -- bar.txt' '\n-\tcheck_graph --full-history --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --full-history --all -- bar.txt <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -148,7 +143,7 @@ test_expect_success '--graph --full-history -- bar.txt' '\n '\n \n test_expect_success '--graph --full-history --simplify-merges -- bar.txt' '\n-\tcheck_graph --full-history --simplify-merges --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --full-history --simplify-merges --all -- bar.txt <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -161,7 +156,7 @@ test_expect_success '--graph --full-history --simplify-merges -- bar.txt' '\n '\n \n test_expect_success '--graph -- bar.txt' '\n-\tcheck_graph --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --all -- bar.txt <<-\\EOF\n \t* A7\n \t* A5\n \t* A3\n@@ -172,7 +167,7 @@ test_expect_success '--graph -- bar.txt' '\n '\n \n test_expect_success '--graph --sparse -- bar.txt' '\n-\tcheck_graph --sparse --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --sparse --all -- bar.txt <<-\\EOF\n \t* A7\n \t* A6\n \t* A5\n@@ -189,7 +184,7 @@ test_expect_success '--graph --sparse -- bar.txt' '\n '\n \n test_expect_success '--graph ^C4' '\n-\tcheck_graph --all ^C4 <<-\\EOF\n+\tlib_test_check_graph --all ^C4 <<-\\EOF\n \t* A7\n \t* A6\n \t* A5\n@@ -202,7 +197,7 @@ test_expect_success '--graph ^C4' '\n '\n \n test_expect_success '--graph ^C3' '\n-\tcheck_graph --all ^C3 <<-\\EOF\n+\tlib_test_check_graph --all ^C3 <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -220,7 +215,7 @@ test_expect_success '--graph ^C3' '\n # that important, but this test depends on it.  If the ordering ever changes\n # in the code, we'll need to update this test.\n test_expect_success '--graph --boundary ^C3' '\n-\tcheck_graph --boundary --all ^C3 <<-\\EOF\n+\tlib_test_check_graph --boundary --all ^C3 <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n\n-- \n2.54.0\n"},{"id":"547833","messageId":"20260711-ps-pre-commit-indent-v9-2-eab6676e82f7@gmail.com","threadId":"65419","inReplyTo":"20260711-ps-pre-commit-indent-v9-0-eab6676e82f7@gmail.com","subject":"[PATCH v9 2/4] revision: add next_commit_to_show()","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-11T13:37:51Z","receivedAt":"2026-07-11T13:38:04Z","isPatch":true,"body":"get_revision() gets its commits from two sources depending on the mode:\n\n1. Normally it gets the commits from get_revision_internal().\n\n2. --max-count-oldest which was introduced at bb4ce23284 (revision.c:\n   implement --max-count-oldest, 2026-05-19) gets the commits by popping\n   from a saved list at revs->commits marking SHOWN and CHILD_SHOWN on\n   each popped commit.\n\nExtract the choice logic into a helper, next_commit_to_show(), which\nreturns the next commit regardless of the source it comes from.\n\nThis has no change in behavior. The helper is needed in a subsequent\ncommit that pre-fetches two commits into a buffer for lookahead purposes\nand needs to pre-fetch from the same source.\n\nThe --reverse branch keeps its own pop loop. Using the helper for\n--reverse would additionally set SHOWN and CHILD_SHOWN which is not\ndesired and a behavior change.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n revision.c | 36 ++++++++++++++++++++++++------------\n 1 file changed, 24 insertions(+), 12 deletions(-)\n\ndiff --git a/revision.c b/revision.c\nindex 0c95edef59..288935943f 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -4658,12 +4658,34 @@ static void retrieve_oldest_commits(struct rev_info *revs,\n \t\tcommit_list_insert(c, queue);\n }\n \n+/*\n+ * Returns the next commit that will be shown, regardless of whether it comes\n+ * directly from the revision walk or from the list saved by the staged output\n+ * of --max-count-oldest.\n+ */\n+static struct commit *next_commit_to_show(struct rev_info *revs)\n+{\n+\tstruct commit *c;\n+\tstruct commit_list *p;\n+\n+\tif (!revs->max_count_stage)\n+\t\treturn get_revision_internal(revs);\n+\n+\tc = pop_commit(&revs->commits);\n+\tif (c) {\n+\t\tc->object.flags |= SHOWN;\n+\t\tif (!(c->object.flags & BOUNDARY))\n+\t\t\tfor (p = c->parents; p; p = p->next)\n+\t\t\t\tp->item->object.flags |= CHILD_SHOWN;\n+\t}\n+\treturn c;\n+}\n+\n struct commit *get_revision(struct rev_info *revs)\n {\n \tstruct commit *c;\n \tstruct commit_list *reversed;\n \tstruct commit_list *queue = NULL;\n-\tstruct commit_list *p;\n \n \tif (revs->max_count_type == 1 && !revs->max_count_stage) {\n \t\tretrieve_oldest_commits(revs, &queue);\n@@ -4693,17 +4715,7 @@ struct commit *get_revision(struct rev_info *revs)\n \t\treturn c;\n \t}\n \n-\tif (revs->max_count_stage) {\n-\t\tc = pop_commit(&revs->commits);\n-\t\tif (c) {\n-\t\t\tc->object.flags |= SHOWN;\n-\t\t\tif (!(c->object.flags & BOUNDARY))\n-\t\t\t\tfor (p = c->parents; p; p = p->next)\n-\t\t\t\t\tp->item->object.flags |= CHILD_SHOWN;\n-\t\t}\n-\t} else {\n-\t\tc = get_revision_internal(revs);\n-\t}\n+\tc = next_commit_to_show(revs);\n \n \tif (c && revs->graph)\n \t\tgraph_update(revs->graph, c);\n\n-- \n2.54.0\n"},{"id":"547834","messageId":"20260711-ps-pre-commit-indent-v9-3-eab6676e82f7@gmail.com","threadId":"65419","inReplyTo":"20260711-ps-pre-commit-indent-v9-0-eab6676e82f7@gmail.com","subject":"[PATCH v9 3/4] graph: add a 2 commit buffer for lookahead","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-11T13:37:52Z","receivedAt":"2026-07-11T13:38:06Z","isPatch":true,"body":"In a subsequent commit the graph renderer needs to know if the next\ncommit is a visual root or if it is the last commit to be shown. This\nrequires peeking 2 commits ahead.\n\nCommits are pre-fetched in get_revision() through next_commit_to_show()\nwhere they are also marked as SHOWN, regardless the source they come\nfrom.\n\nUpdate graph_is_interesting() so it considers commits inside the\nlookahead buffer as interesting as well.\n\nHelped-by: Kristofer Karlsson <krka@spotify.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c    | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++\n graph.h    | 17 +++++++++++++++++\n revision.c | 18 ++++++++++++++++--\n 3 files changed, 84 insertions(+), 2 deletions(-)\n\ndiff --git a/graph.c b/graph.c\nindex 842282685f..89ebcf7540 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -315,6 +315,14 @@ struct git_graph {\n \t * diff_output_prefix_callback().\n \t */\n \tstruct strbuf prefix_buf;\n+\n+\t/*\n+\t * Lookahead buffer: up to 2 pre-fetched commits that will be shown.\n+\t * Populated by get_revision() so graph_peek_next_visible() can use\n+\t * actual walk results instead of peeking at rev_info internals.\n+\t */\n+\tstruct commit *lookahead[2];\n+\tint lookahead_nr;\n };\n \n static inline int graph_needs_truncation(struct git_graph *graph, int lane)\n@@ -388,6 +396,9 @@ struct git_graph *graph_init(struct rev_info *opt)\n \tgraph->num_columns = 0;\n \tgraph->num_new_columns = 0;\n \tgraph->mapping_size = 0;\n+\tgraph->lookahead[0] = NULL;\n+\tgraph->lookahead[1] = NULL;\n+\tgraph->lookahead_nr = 0;\n \t/*\n \t * Start the column color at the maximum value, since we'll\n \t * always increment it for the first commit we output.\n@@ -456,6 +467,15 @@ static void graph_ensure_capacity(struct git_graph *graph, int num_columns)\n  */\n static int graph_is_interesting(struct git_graph *graph, struct commit *commit)\n {\n+\t/*\n+\t * Commits in the lookahead buffer have been pre-fetched by\n+\t * get_revision() and will be shown in the future. They already have\n+\t * the SHOWN flag set when they were pre-fetched but the graph still\n+\t * needs to treat them as interesting parents.\n+\t */\n+\tfor (int i = 0; i < graph->lookahead_nr; i++)\n+\t\tif (graph->lookahead[i] == commit)\n+\t\t\treturn 1;\n \t/*\n \t * If revs->boundary is set, commits whose children have\n \t * been shown are always interesting, even if they have the\n@@ -763,6 +783,37 @@ static int graph_needs_pre_commit_line(struct git_graph *graph)\n \t       graph->expansion_row < graph_num_expansion_rows(graph);\n }\n \n+struct commit *graph_pop_lookahead(struct git_graph *graph)\n+{\n+\tstruct commit *c;\n+\n+\tif (!graph->lookahead_nr)\n+\t\treturn NULL;\n+\n+\tc = graph->lookahead[0];\n+\tif (!c)\n+\t\tBUG(\"lookahead buffer has %d entries but the first one is NULL\",\n+\t\t    graph->lookahead_nr);\n+\n+\tgraph->lookahead[0] = graph->lookahead[1];\n+\tgraph->lookahead[1] = NULL;\n+\tgraph->lookahead_nr--;\n+\treturn c;\n+}\n+\n+int graph_get_lookahead_room(struct git_graph *graph)\n+{\n+\treturn (int)ARRAY_SIZE(graph->lookahead) - graph->lookahead_nr;\n+}\n+\n+void graph_push_lookahead(struct git_graph *graph, struct commit *c)\n+{\n+\tif (!graph_get_lookahead_room(graph))\n+\t\tBUG(\"pushing into lookahead buffer when it is already full\");\n+\n+\tgraph->lookahead[graph->lookahead_nr++] = c;\n+}\n+\n void graph_update(struct git_graph *graph, struct commit *commit)\n {\n \tstruct commit_list *parent;\ndiff --git a/graph.h b/graph.h\nindex 3fd1dcb2e9..1193711fb8 100644\n--- a/graph.h\n+++ b/graph.h\n@@ -262,4 +262,21 @@ void graph_show_commit_msg(struct git_graph *graph,\n \t\t\t   FILE *file,\n \t\t\t   struct strbuf const *sb);\n \n+/*\n+ * Pop the first commit from the graph's lookahead buffer.\n+ * Returns NULL if the buffer is empty.\n+ */\n+struct commit *graph_pop_lookahead(struct git_graph *graph);\n+\n+/*\n+ * Returns how many more commits can be added to the lookahead buffer.\n+ */\n+int graph_get_lookahead_room(struct git_graph *graph);\n+\n+/*\n+ * Push a commit into the lookahead buffer. Must only be called when\n+ * graph_get_lookahead_room() returns > 0.\n+ */\n+void graph_push_lookahead(struct git_graph *graph, struct commit *c);\n+\n #endif /* GRAPH_H */\ndiff --git a/revision.c b/revision.c\nindex 288935943f..258c3cf782 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -4715,10 +4715,24 @@ struct commit *get_revision(struct rev_info *revs)\n \t\treturn c;\n \t}\n \n-\tc = next_commit_to_show(revs);\n+\tif (revs->graph) {\n+\t\tc = graph_pop_lookahead(revs->graph);\n+\t\tif (!c)\n+\t\t\tc = next_commit_to_show(revs);\n+\t} else {\n+\t\tc = next_commit_to_show(revs);\n+\t}\n \n-\tif (c && revs->graph)\n+\tif (c && revs->graph) {\n+\t\twhile (graph_get_lookahead_room(revs->graph)) {\n+\t\t\tstruct commit *next = next_commit_to_show(revs);\n+\t\t\tif (!next)\n+\t\t\t\tbreak;\n+\t\t\tgraph_push_lookahead(revs->graph, next);\n+\t\t}\n \t\tgraph_update(revs->graph, c);\n+\t}\n+\n \tif (!c) {\n \t\tfree_saved_parents(revs);\n \t\tcommit_list_free(revs->previous_parents);\n\n-- \n2.54.0\n"},{"id":"547835","messageId":"20260711-ps-pre-commit-indent-v9-4-eab6676e82f7@gmail.com","threadId":"65419","inReplyTo":"20260711-ps-pre-commit-indent-v9-0-eab6676e82f7@gmail.com","subject":"[PATCH v9 4/4] graph: indent visual root in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-11T13:37:53Z","receivedAt":"2026-07-11T13:38:08Z","isPatch":true,"body":"When rendering a graph, if the history contains multiple \"visual roots\",\nactual roots or commits that look like roots (i.e. have their parents\nfiltered out) can end up being vertically adjacent to unrelated commits,\nfalsely appearing to be related.\n\nA fix for this issue was already attempted [1] a while ago.\n\nThis happens because the commits fill the space from left to right and\nwhen a visual root ends, its column becomes free for the following\ncommit even if they are not related. Once this happens the unrelated\ncommit is rendered below the visual root. Because there is no special\ncharacter or way to identify when a visual root is rendered making the\ngraph confusing.\n\nBy indenting the visual roots when there are still commits to show the\nvertical adjacency can be avoided.\n\nAdd is_visual_root flag to git_graph making it visible in all graph states,\ngive graph_update() a new function, graph_is_visual_root() to know if the\ncurrent commit is a visual root and set is_visual_root.\nThe different handled cases are:\n\n- If a visual root has children: similar to GRAPH_PRE_COMMIT state when\n  octopus merges need space, an edge row needs to be printed to connect\n  the child with the indented visual root. A new state GRAPH_PRE_ROOT is\n  needed to connect the child with the visual root:\n\n    * child of the visual root\n     \\ GRAPH_PRE_ROOT\n      * visual root indented\n\n- If a visual root is child-less we can skip GRAPH_PRE_ROOT state and\n  render the indented commit directly.\n\n      * visual root indented\n    * unrelated commit\n\n- If two or more visual roots are adjacent: by having a lookahead to the\n  next commit that will be rendered, if the next commit is also a visual\n  root and we are on a visual root, meaning two visual root adjacent in\n  the history, the top one can omit the indent, making the one below to\n  indent only once, if there are more adjacent visual commits, the\n  indentation will increase for each adjacent one, cascading.\n\n    * visual root\n      * visual root\n        * visual root\n    * last commit\n\n  Even if the last commit is a root, because there is nothing that will be\n  rendered below we can omit the indentation on purpose.\n\n[1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n\nHelped-by: Kristofer Karlsson <krka@spotify.com>\nMentored-by: Karthik Nayak <karthik.188@gmail.com>\nMentored-by: Chandra Pratap <chandrapratap3519@gmail.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c                          | 244 +++++++++++++++++++\n t/meson.build                    |   1 +\n t/t4218-log-graph-indentation.sh | 514 +++++++++++++++++++++++++++++++++++++++\n 3 files changed, 759 insertions(+)\n\ndiff --git a/graph.c b/graph.c\nindex 89ebcf7540..087094189f 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -60,12 +60,23 @@ struct column {\n \t * index into column_colors.\n \t */\n \tunsigned short color;\n+\t/*\n+\t * Marks if a commit is a non-first parent of a merge. These columns are\n+\t * already visually connected to the merge commit and do not need\n+\t * indentation.\n+\t *\n+\t * The first parent is the one that inherits the column and it can need\n+\t * indentation if turns out to be a visual root and there's still\n+\t * commits to render.\n+\t */\n+\tunsigned int is_merge_parent:1;\n };\n \n enum graph_state {\n \tGRAPH_PADDING,\n \tGRAPH_SKIP,\n \tGRAPH_PRE_COMMIT,\n+\tGRAPH_PRE_ROOT,\n \tGRAPH_COMMIT,\n \tGRAPH_POST_MERGE,\n \tGRAPH_COLLAPSING\n@@ -323,6 +334,51 @@ struct git_graph {\n \t */\n \tstruct commit *lookahead[2];\n \tint lookahead_nr;\n+\n+\t/*\n+\t * If a commit is a visual root, we need to indent it to prevent\n+\t * unrelated commits from being vertically adjacent to it.\n+\t */\n+\tunsigned int is_visual_root:1;\n+\n+\t/*\n+\t * Indentation increases for each visual root adjacent to another visual\n+\t * root, making visual root commits indentation cascade.\n+\t */\n+\tunsigned int visual_root_depth;\n+\n+\t/*\n+\t * When a visual root is adjacent to other visual roots, the first one\n+\t * can avoid indentation and the rest cascades, increasing the indentation\n+\t * for each one.\n+\t */\n+\tunsigned int visual_root_cascade:1;\n+\n+\t/*\n+\t * Set when the current commit was already present in graph->columns\n+\t * before being processed.\n+\t */\n+\tunsigned int commit_in_columns:1;\n+};\n+\n+struct graph_lookahead_flags {\n+\n+\t/*\n+\t * Set when there will be a commit after the current one that will be\n+\t * rendered.\n+\t */\n+\tunsigned int is_next_visible:1;\n+\n+\t/*\n+\t * Set when the next visible commit is candidate to be a visual root.\n+\t */\n+\tunsigned int is_next_visual_root:1;\n+\n+\t/*\n+\t * Set when the next visible commit will be rendered under the current\n+\t * commit.\n+\t */\n+\tunsigned int next_has_column:1;\n };\n \n static inline int graph_needs_truncation(struct git_graph *graph, int lane)\n@@ -399,6 +455,8 @@ struct git_graph *graph_init(struct rev_info *opt)\n \tgraph->lookahead[0] = NULL;\n \tgraph->lookahead[1] = NULL;\n \tgraph->lookahead_nr = 0;\n+\tgraph->visual_root_depth = 0;\n+\tgraph->visual_root_cascade = 0;\n \t/*\n \t * Start the column color at the maximum value, since we'll\n \t * always increment it for the first commit we output.\n@@ -581,6 +639,11 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t\t\t\t\t  struct commit *commit,\n \t\t\t\t\t  int idx)\n {\n+\t/*\n+\t * Get the initial merge_layout before it's modified to know if this\n+\t * is a merge.\n+\t */\n+\tint initial_merge_layout = graph->merge_layout;\n \tint i = graph_find_new_column_by_commit(graph, commit);\n \tint mapping_idx;\n \n@@ -592,6 +655,7 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t\ti = graph->num_new_columns++;\n \t\tgraph->new_columns[i].commit = commit;\n \t\tgraph->new_columns[i].color = graph_find_commit_color(graph, commit);\n+\t\tgraph->new_columns[i].is_merge_parent = 0;\n \t}\n \n \tif (graph->num_parents > 1 && idx > -1 && graph->merge_layout == -1) {\n@@ -630,6 +694,12 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t}\n \n \tgraph->mapping[mapping_idx] = i;\n+\n+\t/*\n+\t * Mark non-first parents of a merge.\n+\t */\n+\tif (graph->num_parents > 1 && initial_merge_layout >= 0 && idx > -1)\n+\t\tgraph->new_columns[i].is_merge_parent = 1;\n }\n \n static void graph_update_columns(struct git_graph *graph)\n@@ -721,10 +791,20 @@ static void graph_update_columns(struct git_graph *graph)\n \t\t\tif (graph->num_parents == 0)\n \t\t\t\tgraph->width += 2;\n \t\t} else {\n+\t\t\tint j;\n \t\t\tgraph_insert_into_new_columns(graph, col_commit, -1);\n+\t\t\t/*\n+\t\t\t * This column is not the current commit, but we need to\n+\t\t\t * propagate the flag until the commit is processed.\n+\t\t\t */\n+\t\t\tj = graph_find_new_column_by_commit(graph, col_commit);\n+\t\t\tif (j >= 0 && graph->columns[i].is_merge_parent)\n+\t\t\t\tgraph->new_columns[j].is_merge_parent = 1;\n \t\t}\n \t}\n \n+\tgraph->commit_in_columns = is_commit_in_columns;\n+\n \t/*\n \t * If graph_max_lanes is set, cap the width\n \t */\n@@ -814,9 +894,113 @@ void graph_push_lookahead(struct git_graph *graph, struct commit *c)\n \tgraph->lookahead[graph->lookahead_nr++] = c;\n }\n \n+/*\n+ * A commit can be a visual root when:\n+ *\n+ * - It has no parents.\n+ *\n+ * - It has parents but they are all filtered out and\n+ *   commit->parents arrives NULL.\n+ *\n+ * - Its parents are uninteresting.\n+ *\n+ * - It is not a boundary commit. Boundary commits also have no visible\n+ *   parents, but they are not selected as visual roots because they cannot\n+ *   cause the ambiguity of being vertically adjacent because:\n+ *\n+ *   1. A boundary only appears because an included commit is its child.\n+ *      Children are always above, and the renderer draws an edge down to\n+ *      the boundary from that child. Rather than starting a column like a\n+ *      visual root would do, it inherits its child column.\n+ *\n+ *   2. Included commits cannot appear below a boundary. Boundaries are\n+ *      ancestors of the exclusion point; if an included commit were an\n+ *      ancestor of the boundary it would be excluded and not rendered.\n+ *      Boundaries therefore always sink to the bottom.\n+ */\n+static int graph_is_visual_root_candidate(struct commit *c, struct git_graph *graph)\n+{\n+\tstruct commit_list *p;\n+\n+\tif (c->object.flags & BOUNDARY)\n+\t\treturn 0;\n+\tfor (p = c->parents; p; p = p->next)\n+\t\tif (graph_is_interesting(graph, p->item))\n+\t\t\treturn 0;\n+\treturn 1;\n+}\n+\n+static int graph_is_visual_root(struct git_graph *graph,\n+\t\t\t\tstruct graph_lookahead_flags *flags)\n+{\n+\t/*\n+\t * This must be only called for the current commit as graph contains\n+\t * the state for the current commit only.\n+\t *\n+\t * To check if a commit is a visual root, call graph_is_visual_root_candidate()\n+\t * but we won't know if it is really a visual root until we get to the\n+\t * next commit state.\n+\t *\n+\t * The current commit is an actual visual root if it is a candidate and\n+\t * the commit is not a non-first parent of a merge.\n+\t *\n+\t *   *\n+\t *   |\\\n+\t *   | *    <- it is a visual root candidate but it shouldn't be indented\n+\t *   *         because it is already connected by an edge.\n+\t *   ^         if commit_in_columns && is_merge_parent means the commit\n+\t *   |         was put by a merge and is connected.\n+\t *   |\n+\t *   `-------- if !is_next_visible means we're on the last commit, avoid\n+\t *             indentation unless the one before is a visual root, then\n+\t *             we need to differentiate from the one above.\n+\t *\n+\t * If next_has_columns means that the next commit has\n+\t * already a column, so it will not be rendered below, the\n+\t * current commit has to act as the last commit and omit\n+\t * indentation.\n+\t */\n+\treturn graph_is_visual_root_candidate(graph->commit, graph) &&\n+\t       !(graph->commit_in_columns &&\n+\t\t graph->columns[graph->commit_index].is_merge_parent) &&\n+\t       flags->is_next_visible &&\n+\t       (!flags->next_has_column || graph->visual_root_depth > 0);\n+}\n+\n+/*\n+ * Peeks the next commits via the lookahead buffer and sets the lookahead flags.\n+ */\n+static void graph_peek_next_visible(struct git_graph *graph,\n+\t\t\t\t    struct graph_lookahead_flags *flags)\n+{\n+\tflags->is_next_visible = 0;\n+\tflags->is_next_visual_root = 0;\n+\tflags->next_has_column = 0;\n+\n+\tif (!graph->lookahead_nr)\n+\t\treturn;\n+\n+\tflags->is_next_visible = 1;\n+\tflags->next_has_column =\n+\t\tgraph_find_new_column_by_commit(graph, graph->lookahead[0]) >= 0;\n+\n+\tif (!graph_is_visual_root_candidate(graph->lookahead[0], graph))\n+\t\treturn;\n+\n+\tif (graph->lookahead_nr >= 2)\n+\t\tflags->is_next_visual_root = 1;\n+}\n+\n+static int graph_needs_pre_root_line(struct git_graph *graph)\n+{\n+\treturn graph->commit_in_columns && graph->is_visual_root &&\n+\t       graph->num_columns > 0 && !graph->visual_root_cascade;\n+}\n+\n void graph_update(struct git_graph *graph, struct commit *commit)\n {\n \tstruct commit_list *parent;\n+\tstruct graph_lookahead_flags flags;\n \n \t/*\n \t * Set the new commit\n@@ -847,6 +1031,23 @@ void graph_update(struct git_graph *graph, struct commit *commit)\n \t */\n \tgraph_update_columns(graph);\n \n+\tgraph_peek_next_visible(graph, &flags);\n+\n+\tgraph->is_visual_root = graph_is_visual_root(graph, &flags);\n+\n+\tif (graph->is_visual_root) {\n+\t\t/*\n+\t\t * If next is a visual root we can omit the indent for the first\n+\t\t * visual root and start cascading.\n+\t\t */\n+\t\tif (!graph->visual_root_depth && flags.is_next_visual_root)\n+\t\t\tgraph->visual_root_cascade = 1;\n+\t\tgraph->visual_root_depth++;\n+\t} else {\n+\t\tgraph->visual_root_depth = 0;\n+\t\tgraph->visual_root_cascade = 0;\n+\t}\n+\n \tgraph->expansion_row = 0;\n \n \t/*\n@@ -864,11 +1065,16 @@ void graph_update(struct git_graph *graph, struct commit *commit)\n \t * room for it.  We need to do this only if there is a branch row\n \t * (or more) to the right of this commit.\n \t *\n+\t * If it is a visual root, we need to print an extra row to\n+\t * connect the indentation.\n+\t *\n \t * If there are less than 3 parents, we can immediately print the\n \t * commit line.\n \t */\n \tif (graph->state != GRAPH_PADDING)\n \t\tgraph->state = GRAPH_SKIP;\n+\telse if (graph_needs_pre_root_line(graph))\n+\t\tgraph->state = GRAPH_PRE_ROOT;\n \telse if (graph_needs_pre_commit_line(graph))\n \t\tgraph->state = GRAPH_PRE_COMMIT;\n \telse\n@@ -1116,6 +1322,17 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \n \t\tif (col_commit == graph->commit) {\n \t\t\tseen_this = 1;\n+\t\t\tif (graph->is_visual_root) {\n+\t\t\t\tint depth = graph->visual_root_depth;\n+\t\t\t\t/*\n+\t\t\t\t * Each visual column is 2 characters wide.\n+\t\t\t\t * Omit the indentation for the first visual\n+\t\t\t\t * root in cascade mode.\n+\t\t\t\t */\n+\t\t\t\tint padding = (depth - graph->visual_root_cascade) * 2;\n+\t\t\t\tgraph_line_addchars(line, ' ', padding);\n+\t\t\t\tgraph->width += padding;\n+\t\t\t}\n \t\t\tgraph_output_commit_char(graph, line);\n \n \t\t\tif (graph_needs_truncation(graph, i)) {\n@@ -1487,6 +1704,30 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n \t\tgraph_update_state(graph, GRAPH_PADDING);\n }\n \n+static void graph_output_pre_root_line(struct git_graph *graph, struct graph_line *line)\n+{\n+\t/*\n+\t * This function adds a row before a visual root, to connect the\n+\t * branch to the indented commit. It must only be called on a\n+\t * visual root.\n+\t */\n+\tif (!graph->is_visual_root)\n+\t\tBUG(\"commit must be a visual root to call pre_root_line\");\n+\n+\tfor (int i = 0; i < graph->num_columns; i++) {\n+\t\tstruct column *col = &graph->columns[i];\n+\t\tif (col->commit == graph->commit) {\n+\t\t\tgraph_line_addch(line, ' ');\n+\t\t\tgraph_line_write_column(line, col, '\\\\');\n+\t\t} else {\n+\t\t\tgraph_line_write_column(line, col, '|');\n+\t\t}\n+\t\tgraph_line_addch(line, ' ');\n+\t}\n+\n+\tgraph_update_state(graph, GRAPH_COMMIT);\n+}\n+\n int graph_next_line(struct git_graph *graph, struct strbuf *sb)\n {\n \tint shown_commit_line = 0;\n@@ -1512,6 +1753,9 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb)\n \tcase GRAPH_PRE_COMMIT:\n \t\tgraph_output_pre_commit_line(graph, &line);\n \t\tbreak;\n+\tcase GRAPH_PRE_ROOT:\n+\t\tgraph_output_pre_root_line(graph, &line);\n+\t\tbreak;\n \tcase GRAPH_COMMIT:\n \t\tgraph_output_commit_line(graph, &line);\n \t\tshown_commit_line = 1;\ndiff --git a/t/meson.build b/t/meson.build\nindex 7c3c070426..cce5ba71f9 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -577,6 +577,7 @@ integration_tests = [\n   't4215-log-skewed-merges.sh',\n   't4216-log-bloom.sh',\n   't4217-log-limit.sh',\n+  't4218-log-graph-indentation.sh',\n   't4219-log-follow-merge.sh',\n   't4252-am-options.sh',\n   't4253-am-keep-cr-dos.sh',\ndiff --git a/t/t4218-log-graph-indentation.sh b/t/t4218-log-graph-indentation.sh\nnew file mode 100755\nindex 0000000000..60c7d84af7\n--- /dev/null\n+++ b/t/t4218-log-graph-indentation.sh\n@@ -0,0 +1,514 @@\n+#!/bin/sh\n+\n+test_description='git log --graph visual root indentations'\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-log-graph.sh\n+\n+check_graph_with_description () {\n+\tcat >expect &&\n+\tlib_test_cmp_graph --format=\"%s%ndescription%nsecond-line\" \"$@\"\n+}\n+\n+create_orphan () {\n+\tgit checkout --orphan \"$1\" &&\n+\ttest_might_fail git rm -rf .\n+}\n+\n+# disable commit-graph topo order to have the graph to render in different\n+# ways (used in --first-parent tests to have multiple visual roots while a\n+# column is active at the same time).\n+unset_commit_graph () {\n+\tsane_unset GIT_TEST_COMMIT_GRAPH &&\n+\trm -f .git/objects/info/commit-graph &&\n+\trm -rf .git/objects/info/commit-graphs\n+}\n+\n+test_expect_success 'single root commit is not indented' '\n+\tcreate_orphan _1 && test_commit 1_A &&\n+\tlib_test_check_graph _1 <<-\\EOF\n+\t* 1_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indented before unrelated branch' '\n+\tcreate_orphan _2 && test_commit 2_A && test_commit 2_B &&\n+\tcreate_orphan _3 && test_commit 3_A &&\n+\tlib_test_check_graph _2 _3 <<-\\EOF\n+\t  * 3_A\n+\t* 2_B\n+\t* 2_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indentation with --left-right' '\n+\tlib_test_check_graph --left-right _2..._3 <<-\\EOF\n+\t  > 3_A\n+\t< 2_B\n+\t< 2_A\n+\tEOF\n+'\n+\n+# A better case of why indentation is still needed with '--left-right' flag is\n+# that unrelated branches can be on the same side, so it's needed to\n+# differentiate visual roots on the same side.\n+test_expect_success 'visual root indentation with --left-right having unrelated commits on the same side' '\n+\tlib_test_check_graph --left-right _2..._3 _1 <<-\\EOF\n+\t  > 3_A\n+\t< 2_B\n+\t \\\n+\t  < 2_A\n+\t> 1_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indents the description also' '\n+\tcheck_graph_with_description _2 _3 <<-\\EOF\n+\t  * 3_A\n+\t    description\n+\t    second-line\n+\t* 2_B\n+\t| description\n+\t| second-line\n+\t* 2_A\n+\t  description\n+\t  second-line\n+\tEOF\n+'\n+\n+test_expect_success 'indented visual root parent gets connected to its child' '\n+\tcreate_orphan _4 && test_commit 4_A && test_commit 4_B &&\n+\tcreate_orphan _5 && test_commit 5_A && test_commit 5_B &&\n+\tlib_test_check_graph _4 _5 <<-\\EOF\n+\t* 5_B\n+\t \\\n+\t  * 5_A\n+\t* 4_B\n+\t* 4_A\n+\tEOF\n+'\n+\n+test_expect_success 'indented visual root parent gets connected to its child with description' '\n+\tcheck_graph_with_description _4 _5 <<-\\EOF\n+\t* 5_B\n+\t| description\n+\t| second-line\n+\t \\\n+\t  * 5_A\n+\t    description\n+\t    second-line\n+\t* 4_B\n+\t| description\n+\t| second-line\n+\t* 4_A\n+\t  description\n+\t  second-line\n+\tEOF\n+'\n+\n+test_expect_success 'visual roots cascade and last root does not' '\n+\tcreate_orphan _7 && test_commit 7_A && test_commit 7_B &&\n+\tcreate_orphan _8 && test_commit 8_A &&\n+\tcreate_orphan _9 && test_commit 9_A &&\n+\tcreate_orphan _10 && test_commit 10_A &&\n+\tlib_test_check_graph _7 _8 _9 _10 <<-\\EOF\n+\t* 10_A\n+\t  * 9_A\n+\t    * 8_A\n+\t* 7_B\n+\t* 7_A\n+\tEOF\n+'\n+\n+test_expect_success 'last root does not cascade' '\n+\tlib_test_check_graph _8 _9 _10 <<-\\EOF\n+\t* 10_A\n+\t  * 9_A\n+\t* 8_A\n+\tEOF\n+'\n+\n+test_expect_success 'merge parents are roots between them but they do not indent' '\n+\tcreate_orphan _11 && test_commit 11_A &&\n+\tcreate_orphan _12 && test_commit 12_A &&\n+\tcreate_orphan _13 && test_commit 13_A &&\n+\tgit checkout _11 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _11 -p _12 -p _13 -m 11_octopus) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _11 <<-\\EOF\n+\t*-.   11_octopus\n+\t|\\ \\\n+\t| | * 13_A\n+\t| * 12_A\n+\t* 11_A\n+\tEOF\n+'\n+\n+# The last parent of a merge can be indented if nothing related to it needs to\n+# be rendered after, if it's another visual root, merge parent must not get\n+# indented but rather activate cascading.\n+test_expect_success 'merge then unrelated visual root and unrelated branch' '\n+\tcreate_orphan _16 && test_commit 16_A && test_commit 16_B &&\n+\tcreate_orphan _17 && test_commit 17_A &&\n+\tcreate_orphan _18 && test_commit 18_A &&\n+\tcreate_orphan _19 && test_commit 19_A &&\n+\tcreate_orphan _20 && test_commit 20_A &&\n+\tgit checkout _18 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _18 -p _19 -p _20 -m 18_octopus) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _18 _17 _16 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t* 18_A\n+\t  * 17_A\n+\t* 16_B\n+\t* 16_A\n+\tEOF\n+'\n+\n+# The last commit root does not get indented, if the next thing after the root\n+# merge parent is the last commit, indent the merge parent.\n+test_expect_success 'merge then unrelated root indents merge parent' '\n+\tlib_test_check_graph _18 _17 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t \\\n+\t  * 18_A\n+\t* 17_A\n+\tEOF\n+'\n+\n+test_expect_success 'merge then unrelated branch indents merge parent' '\n+\tlib_test_check_graph _18 _16 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t \\\n+\t  * 18_A\n+\t* 16_B\n+\t* 16_A\n+\tEOF\n+'\n+\n+test_expect_success 'two-parent merge of orphans' '\n+\tcreate_orphan _21 && test_commit 21_A &&\n+\tcreate_orphan _22 && test_commit 22_A &&\n+\tgit checkout _21 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _21 -p _22 -m 21_merge) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _21 <<-\\EOF\n+\t*   21_merge\n+\t|\\\n+\t| * 22_A\n+\t* 21_A\n+\tEOF\n+'\n+\n+test_expect_success 'commit with filtered parent becomes a visual root' '\n+\tcreate_orphan _23 &&\n+\techo test >other.txt &&\n+\tgit add other.txt &&\n+\tgit commit -m \"23_A\" &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"23_B\" &&\n+\tcreate_orphan _24 &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"24_A\" &&\n+\tlib_test_check_graph _23 _24 -- foo.txt <<-\\EOF\n+\t  * 23_B\n+\t* 24_A\n+\tEOF\n+'\n+\n+test_expect_success 'filtered parent cascading edge case' '\n+\tcreate_orphan _27 &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\ttest_tick &&\n+\tgit commit -m \"D (last)\" &&\n+\n+\tcreate_orphan _25 &&\n+\techo test >other.txt &&\n+\tgit add other.txt &&\n+\ttest_tick &&\n+\tgit commit -m \"C-filtered\" &&\n+\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\ttest_tick &&\n+\tgit commit -m \"B (child of filtered)\" &&\n+\n+\tcreate_orphan _26 &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\ttest_tick &&\n+\tgit commit -m \"A (visual root)\" &&\n+\n+\tlib_test_check_graph _25 _26 _27 -- foo.txt <<-\\EOF\n+\t* A (visual root)\n+\t  * B (child of filtered)\n+\t* D (last)\n+\tEOF\n+'\n+\n+test_expect_success 'multiple filtered parents in sequence' '\n+\tcreate_orphan _44 &&\n+\techo a >other.txt && git add other.txt && git commit -m \"44_F\" &&\n+\techo b >foo.txt && git add foo.txt && git commit -m \"44_C\" &&\n+\n+\tcreate_orphan _45 &&\n+\techo c >other.txt && git add other.txt && git commit -m \"45_F\" &&\n+\techo d >foo.txt && git add foo.txt && git commit -m \"45_C\" &&\n+\n+\tcreate_orphan _46 &&\n+\techo e >foo.txt && git add foo.txt && git commit -m \"46_A\" &&\n+\n+\tlib_test_check_graph _44 _45 _46 -- foo.txt <<-\\EOF\n+\t* 44_C\n+\t  * 45_C\n+\t* 46_A\n+\tEOF\n+'\n+\n+# These tests prove why there is no need to have indentation for boundary\n+# commits.\n+#\n+# Boundary commits rather than starting a column they 'inherit' the one of\n+# its child so there will always be an edge that connects it removing the\n+# ambiguity.\n+test_expect_success 'unrelated boundaries are not ambiguous' '\n+\tcreate_orphan _28 && test_commit 28_A && test_commit 28_B &&\n+\ttest_commit 28_C &&\n+\tcreate_orphan _29 && test_commit 29_A && test_commit 29_B &&\n+\tlib_test_check_graph --boundary 28_A.._28 29_A.._29 <<-\\EOF\n+\t* 29_B\n+\t| * 28_C\n+\t| * 28_B\n+\t| o 28_A\n+\to 29_A\n+\tEOF\n+'\n+\n+# Same structure as t6016\n+test_expect_success 'boundary commits big test' '\n+\t# 3 commits on branch _30\n+\tcreate_orphan _30 &&\n+\ttest_commit 30_A &&\n+\ttest_commit 30_B &&\n+\ttest_commit 30_C &&\n+\n+\t# 2 commits on branch _31, started from 30_A\n+\tgit checkout -b _31 30_A &&\n+\ttest_commit 31_A &&\n+\ttest_commit 31_B &&\n+\n+\t# 2 commits on branch _32, started from 30_B\n+\tgit checkout -b _32 30_B &&\n+\ttest_commit 32_A &&\n+\ttest_commit 32_B &&\n+\n+\t# Octopus merge _31 and _32 into -30\n+\tgit checkout _30 &&\n+\tgit merge _31 _32 -m 30_D &&\n+\tgit tag 30_D &&\n+\ttest_commit 30_E &&\n+\n+\t# More commits on _32, then merge _32 into _30\n+\tgit checkout _32 &&\n+\ttest_commit 32_C &&\n+\ttest_commit 32_D &&\n+\tgit checkout _30 &&\n+\tgit merge -s ours _32 -m 30_F &&\n+\tgit tag 30_F &&\n+\ttest_commit 30_G &&\n+\tlib_test_check_graph --boundary _30 _31 _32 ^32_C <<-\\EOF\n+\t* 30_G\n+\t*   30_F\n+\t|\\\n+\t| * 32_D\n+\t* | 30_E\n+\t| |\n+\t|  \\\n+\t*-. \\   30_D\n+\t|\\ \\ \\\n+\t| * | | 31_B\n+\t| * | | 31_A\n+\t* | | | 30_C\n+\to | | | 30_B\n+\t|/ / /\n+\to / / 30_A\n+\t / /\n+\t| o 32_C\n+\t|/\n+\to 32_B\n+\tEOF\n+'\n+\n+# Filter by --first-parent and then forcing the filtered parents to be shown.\n+test_expect_success '--first-parent flag with the filtered parents' '\n+\t(\n+\t\tunset_commit_graph &&\n+\t\tcreate_orphan _35 && test_commit 35_A && test_commit 35_B &&\n+\t\tcreate_orphan _36 && test_commit 36_A &&\n+\t\tcreate_orphan _37 && test_commit 37_A &&\n+\t\tgit checkout _35 &&\n+\t\tTREE=$(git write-tree) &&\n+\t\tMERGE=$(git commit-tree $TREE -p _35 -p _36 -p _37 -m 35_octopus) &&\n+\t\tgit reset --hard $MERGE &&\n+\t\tlib_test_check_graph --first-parent _35 _36 _37 <<-\\EOF\n+\t\t* 35_octopus\n+\t\t| * 37_A\n+\t\t|   * 36_A\n+\t\t* 35_B\n+\t\t* 35_A\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success '--first-parent with filtered parents but one has a child' '\n+\t(\n+\t\tunset_commit_graph &&\n+\t\tcreate_orphan _38 && test_commit 38_A && test_commit 38_B &&\n+\t\tcreate_orphan _39 && test_commit 39_A &&\n+\t\tcreate_orphan _40 && test_commit 40_A && test_commit 40_B &&\n+\t\tgit checkout _38 &&\n+\t\tTREE=$(git write-tree) &&\n+\t\tMERGE=$(git commit-tree $TREE -p _38 -p _39 -p _40 -m 38_octopus) &&\n+\t\tgit reset --hard $MERGE &&\n+\t\tlib_test_check_graph --first-parent _38 _39 _40 <<-\\EOF\n+\t\t* 38_octopus\n+\t\t| * 40_B\n+\t\t| * 40_A\n+\t\t|   * 39_A\n+\t\t* 38_B\n+\t\t* 38_A\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success '--first-parent with filtered parents but both have children' '\n+\t(\n+\t\tunset_commit_graph &&\n+\t\tcreate_orphan _41 && test_commit 41_A && test_commit 41_B &&\n+\t\tcreate_orphan _42 && test_commit 42_A && test_commit 42_B &&\n+\t\tcreate_orphan _43 && test_commit 43_A && test_commit 43_B &&\n+\t\tgit checkout _41 &&\n+\t\tTREE=$(git write-tree) &&\n+\t\tMERGE=$(git commit-tree $TREE -p _41 -p _42 -p _43 -m 41_octopus) &&\n+\t\tgit reset --hard $MERGE &&\n+\t\tlib_test_check_graph --first-parent _41 _42 _43 <<-\\EOF\n+\t\t* 41_octopus\n+\t\t| * 43_B\n+\t\t|  \\\n+\t\t|   * 43_A\n+\t\t| * 42_B\n+\t\t| * 42_A\n+\t\t* 41_B\n+\t\t* 41_A\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'two unrelated merges' '\n+\tcreate_orphan _50 && test_commit 50_A &&\n+\tgit checkout -b _51 &&\n+\ttest_commit 51_A && test_commit 51_B &&\n+\tgit checkout _50 &&\n+\tgit merge --no-ff _51 -m 50_B &&\n+\n+\tcreate_orphan _52 && test_commit 52_A &&\n+\tgit checkout -b _53 &&\n+\ttest_commit 53_A && test_commit 53_B &&\n+\tgit checkout _52 &&\n+\tgit merge --no-ff _53 -m 52_B &&\n+\n+\tlib_test_check_graph _52 _50 <<-\\EOF\n+\t*   52_B\n+\t|\\\n+\t| * 53_B\n+\t| * 53_A\n+\t|/\n+\t \\\n+\t  * 52_A\n+\t*   50_B\n+\t|\\\n+\t| * 51_B\n+\t| * 51_A\n+\t|/\n+\t* 50_A\n+\tEOF\n+'\n+\n+test_expect_success '--max-count treats the last visible commit as the last commit' '\n+\tlib_test_check_graph --max-count=2 _8 _9 _10 <<-\\EOF\n+\t  * 10_A\n+\t* 9_A\n+\tEOF\n+'\n+\n+test_expect_success '--max-count=1 shows a single root without indentation' '\n+\tlib_test_check_graph --max-count=1 _8 _9 _10 <<-\\EOF\n+\t* 10_A\n+\tEOF\n+'\n+\n+test_expect_success '--max-count-oldest indents visual roots' '\n+\tlib_test_check_graph --max-count-oldest=3 _8 _9 _10 <<-\\EOF\n+\t* 10_A\n+\t  * 9_A\n+\t* 8_A\n+\tEOF\n+'\n+\n+# when the graph commits are filtered with regex options like --author, the\n+# commit parents do not come NULL so it is needed to check if the parents are\n+# interesting.\n+test_expect_success '--author skipped parent makes a visual root' '\n+\tcreate_orphan _55 &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 55_A &&\n+\tcreate_orphan _54 &&\n+\ttest_tick &&\n+\tgit commit --allow-empty --author=\"Other <other@example.com>\" -m 54_A &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 54_B &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 54_C &&\n+\tlib_test_check_graph --author=\"A U Thor\" _54 _55 <<-\\EOF\n+\t* 54_C\n+\t \\\n+\t  * 54_B\n+\t* 55_A\n+\tEOF\n+'\n+\n+test_expect_success '--grep skipped parent makes a visual root' '\n+\tcreate_orphan _57 &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 57_keep_A &&\n+\tcreate_orphan _56 &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 56_skip &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 56_keep_A &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 56_keep_B &&\n+\tlib_test_check_graph --grep=keep _56 _57 <<-\\EOF\n+\t* 56_keep_B\n+\t \\\n+\t  * 56_keep_A\n+\t* 57_keep_A\n+\tEOF\n+'\n+\n+test_done\n\n-- \n2.54.0\n"},{"id":"547837","messageId":"alJOgYmAfGg37hsB@exploit","threadId":"65419","inReplyTo":"20260711-ps-pre-commit-indent-v9-0-eab6676e82f7@gmail.com","subject":"Re: [PATCH v9 0/4] graph: indent visual roots in graph","fromName":"Mirko Faina","fromEmail":"mroik@delayed.space","sentAt":"2026-07-11T14:15:32Z","receivedAt":"2026-07-11T14:15:36Z","isPatch":true,"body":"On Sat, Jul 11, 2026 at 03:37:49PM +0200, Pablo Sabater wrote:\n> When rendering a graph, if the history contains multiple \"visual roots\",\n> actual roots or commits that look like roots (i.e. have their parents\n> filtered out) can end up being vertically adjacent to unrelated commits,\n> falsely appearing to be related.\n> \n> A fix for this issue was already attempted [1] a while ago.\n> \n> This series adds indentation to the visual root commits, so they cannot be\n> vertically adjacent anymore making it easier to identify them.\n> \n> Before indentation:\n> \n> \t* A\n> \t* B1\n> \t* B2\n> \t* C1\n> \t* C2\n> \n> After indentation:\n> \n> \t  * A\n> \t* B1\n> \t \\\n> \t  * B2\n> \t* C1\n> \t* C2\n> \n> Indents the visual root commits that have still commits to show after\n> them, and if they have children it connects them with an edge at a new\n> row.\n> \n> If there are multiple visual roots adjacent in history, the indentation\n> starts with the second one, avoiding redundant indentation of the first\n> one and cascades after the second.\n> \n> \t* A\n> \t  * B\n> \t    * C\n> \t* D1\n> \t* D2\n> \n> This series first commit is a cleanup that brings a common function\n> from t4215 and t6016 to a graph functions file which they both use, so\n> the new test file for indentation, t4218, can use it as well.\n> \n> GitHub CI: https://github.com/pabloosabaterr/git/actions/runs/29154333559\n> \n> [1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n> \n> V8 DIFF:\n> \n> - Checking if the parents of a commit are NULL is not enough to know if\n>   the commit is a visual root due to options that filter the commit\n>   parents but they do not remove them (--author, --grep, etc).\n>   At graph_is_visual_root_candidate(), iterate the parents and call\n>   graph_is_interesting() for each of them to know whether they will be\n>   shown or not.\n> - Add a --author and a --grep test.\n> \n> Signed-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n> \n> ---\n> Pablo Sabater (4):\n>       lib-log-graph: move check_graph function\n>       revision: add next_commit_to_show()\n>       graph: add a 2 commit buffer for lookahead\n>       graph: indent visual root in graph\n> \n>  graph.c                                    | 295 +++++++++++++++++\n>  graph.h                                    |  17 +\n>  revision.c                                 |  48 ++-\n>  t/lib-log-graph.sh                         |   5 +\n>  t/meson.build                              |   1 +\n>  t/t4215-log-skewed-merges.sh               |  33 +-\n>  t/t4218-log-graph-indentation.sh           | 514 +++++++++++++++++++++++++++++\n>  t/t6016-rev-list-graph-simplify-history.sh |  25 +-\n>  8 files changed, 893 insertions(+), 45 deletions(-)\n\nSorry, I know I'm a bit late to the discussion regarding the design,\nbut, could we maybe have two different code paths for printing graphs?\nHaving the old one as a default and this new one only when we're using\n--oneline (well, --format=reference would benefit too)? As it is now if\nI have multiple one-patch series in sequence the entries are\nunnecessarily indented.\n\nThanks\n"},{"id":"547839","messageId":"DJVUU76PUXR4.2BYRTA8SEEBVC@gmail.com","threadId":"65419","inReplyTo":"alJOgYmAfGg37hsB@exploit","subject":"Re: [PATCH v9 0/4] graph: indent visual roots in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-11T15:41:58Z","receivedAt":"2026-07-11T15:42:01Z","isPatch":true,"body":"On Sat Jul 11, 2026 at 4:15 PM CEST, Mirko Faina wrote:\n> On Sat, Jul 11, 2026 at 03:37:49PM +0200, Pablo Sabater wrote:\n>> When rendering a graph, if the history contains multiple \"visual roots\",\n>> actual roots or commits that look like roots (i.e. have their parents\n>> filtered out) can end up being vertically adjacent to unrelated commits,\n>> falsely appearing to be related.\n>>\n>> A fix for this issue was already attempted [1] a while ago.\n>>\n>> This series adds indentation to the visual root commits, so they cannot be\n>> vertically adjacent anymore making it easier to identify them.\n\n [snip]\n\n>\n> Sorry, I know I'm a bit late to the discussion regarding the design,\n> but, could we maybe have two different code paths for printing graphs?\n> Having the old one as a default and this new one only when we're using\n> --oneline (well, --format=reference would benefit too)? As it is now if\n> I have multiple one-patch series in sequence the entries are\n> unnecessarily indented.\n>\n> Thanks\n\nNo worries :)\n\nWell, I thought that it could become annoying given the scenario of\nhaving too many visual roots one after the other. But I didn't have a\nclear way of having that scenario without forcing it.\n\nI think that this solves an ambiguity so it should be the default option\nand someone who doesn't want the indentation has to explicitly unset it\nmaybe with something like '--no-graph-indent'.\n\nApart from having an option to disable indentation.\n\nWe could have the cascading to have a limit or make it zig-zag:\n\ninstead of:\n\nA\n  B\n    C\n      D\n\nWe could do:\n\nA\n  B\nC\n  D\n\nThis would have its own edge cases like:\n\nA\n  B\nC <- if we zig-zag here C and D become ambiguous, currently we are\nD    indenting only the last commits (visual roots) here we would have\nD    to chose between continuing cascading or indenting the first of D.\n\nI'm not so sure if I like the zig-zag solution because we need to think again\nif it causes an ambiguity, but I wanted to mention it.\n\nI think we need some more opinions about the design.\n\nThanks,\nPablo\n"},{"id":"547844","messageId":"alJpjTXfZmYQccwk@exploit","threadId":"65419","inReplyTo":"DJVUU76PUXR4.2BYRTA8SEEBVC@gmail.com","subject":"Re: [PATCH v9 0/4] graph: indent visual roots in graph","fromName":"Mirko Faina","fromEmail":"mroik@delayed.space","sentAt":"2026-07-11T16:25:05Z","receivedAt":"2026-07-11T16:25:09Z","isPatch":true,"body":"On Sat, Jul 11, 2026 at 05:41:58PM +0200, Pablo Sabater wrote:\n> I think that this solves an ambiguity so it should be the default option\n> and someone who doesn't want the indentation has to explicitly unset it\n> maybe with something like '--no-graph-indent'.\n\nThe reason I prefer the current way of printing as the default is\nbecause the ambiguity arises only when each commit occupies exactly one\nline. In any other case we can clearly see the edges connecting the\nvertices. I'd rather have --oneline imply what would be --graph-indent\ninstead of having to pass --no-graph-indent on any other format\ndifferent from --oneline or --format=reference.\n\n> Apart from having an option to disable indentation.\n> \n> We could have the cascading to have a limit or make it zig-zag:\n> \n> instead of:\n> \n> A\n>   B\n>     C\n>       D\n> \n> We could do:\n> \n> A\n>   B\n> C\n>   D\n> \n> This would have its own edge cases like:\n> \n> A\n>   B\n> C <- if we zig-zag here C and D become ambiguous, currently we are\n> D    indenting only the last commits (visual roots) here we would have\n> D    to chose between continuing cascading or indenting the first of D.\n> \n> I'm not so sure if I like the zig-zag solution because we need to think again\n> if it causes an ambiguity, but I wanted to mention it.\n> \n> I think we need some more opinions about the design.\n\nI don't dislike the the current solution but I can see it degenerating\nif someone contributes a lot of one-patch series.\n\nMaybe you could indent commits that are both head and tail up to two\nlevels and then on the third go back to the beginning of the line. That\nway you kind of have a zig-zag but without ambiguity. You'd only have to\nadd a counter to keep track of the level of indentation.\n"},{"id":"547876","messageId":"CA+J6zkQcHu-LVKE-1ypfT=59gEzo4qBzi-pmhSJNC_udCDCJZg@mail.gmail.com","threadId":"65419","inReplyTo":"alJpjTXfZmYQccwk@exploit","subject":"Re: [PATCH v9 0/4] graph: indent visual roots in graph","fromName":"Chandra Pratap","fromEmail":"chandrapratap3519@gmail.com","sentAt":"2026-07-12T05:56:27Z","receivedAt":"2026-07-12T05:56:56Z","isPatch":true,"body":"On Sat, 11 Jul 2026 at 21:55, Mirko Faina <mroik@delayed.space> wrote:\n>\n> On Sat, Jul 11, 2026 at 05:41:58PM +0200, Pablo Sabater wrote:\n> > I think that this solves an ambiguity so it should be the default option\n> > and someone who doesn't want the indentation has to explicitly unset it\n> > maybe with something like '--no-graph-indent'.\n>\n> The reason I prefer the current way of printing as the default is\n> because the ambiguity arises only when each commit occupies exactly one\n> line. In any other case we can clearly see the edges connecting the\n> vertices. I'd rather have --oneline imply what would be --graph-indent\n> instead of having to pass --no-graph-indent on any other format\n> different from --oneline or --format=reference.\n\nTying graph-drawing logic to specific formatting flags could introduce\ninconsistencies. For example, if a user relies on a custom format like\n--format=\"%h %s\", the output is functionally single-line and suffers\nfrom the exact same ambiguity, but it would miss the fix.\n\nEven in multi-line formats, relying on the absence of a '|' character to spot\nunrelated commits requires active effort. Indentation provides an immediate\nvisual cue that breaks the vertical lineage, which is helpful regardless of the\ncommit message length.\n\nI agree with Pablo: for users who strictly want the old behavior, an opt-out\nflag keeps the graph logic decoupled from the formatting logic.\n\n> > Apart from having an option to disable indentation.\n> >\n> > We could have the cascading to have a limit or make it zig-zag:\n> >\n> > instead of:\n> >\n> > A\n> >   B\n> >     C\n> >       D\n> >\n> > We could do:\n> >\n> > A\n> >   B\n> > C\n> >   D\n> >\n> > This would have its own edge cases like:\n> >\n> > A\n> >   B\n> > C <- if we zig-zag here C and D become ambiguous, currently we are\n> > D    indenting only the last commits (visual roots) here we would have\n> > D    to chose between continuing cascading or indenting the first of D.\n> >\n> > I'm not so sure if I like the zig-zag solution because we need to think again\n> > if it causes an ambiguity, but I wanted to mention it.\n> >\n> > I think we need some more opinions about the design.\n>\n> I don't dislike the the current solution but I can see it degenerating\n> if someone contributes a lot of one-patch series.\n>\n> Maybe you could indent commits that are both head and tail up to two\n> levels and then on the third go back to the beginning of the line. That\n> way you kind of have a zig-zag but without ambiguity. You'd only have to\n> add a counter to keep track of the level of indentation.\n\nNot sure about this. A zig-zag pattern visually mimics branching and\nmerging, which makes unrelated commits look like a complex merge topology.\n\nI also have a feeling that this will end up recreating the exact ambiguity this\npatch series is trying to fix.\n"},{"id":"547896","messageId":"alOOXKGIB8BqACxR@exploit","threadId":"65419","inReplyTo":"CA+J6zkQcHu-LVKE-1ypfT=59gEzo4qBzi-pmhSJNC_udCDCJZg@mail.gmail.com","subject":"Re: [PATCH v9 0/4] graph: indent visual roots in graph","fromName":"Mirko Faina","fromEmail":"mroik@delayed.space","sentAt":"2026-07-12T13:10:49Z","receivedAt":"2026-07-12T13:10:59Z","isPatch":true,"body":"On Sun, Jul 12, 2026 at 11:26:27AM +0530, Chandra Pratap wrote:\n> Tying graph-drawing logic to specific formatting flags could introduce\n> inconsistencies. For example, if a user relies on a custom format like\n> --format=\"%h %s\", the output is functionally single-line and suffers\n> from the exact same ambiguity, but it would miss the fix.\n> \n> Even in multi-line formats, relying on the absence of a '|' character to spot\n> unrelated commits requires active effort. Indentation provides an immediate\n> visual cue that breaks the vertical lineage, which is helpful regardless of the\n> commit message length.\n> \n> I agree with Pablo: for users who strictly want the old behavior, an opt-out\n> flag keeps the graph logic decoupled from the formatting logic.\n\nIn that case, together with --[no]-graph-indent, a configuration\nvariable like \"graph.indent\" could be introduced to reduce the usage of\n--[no]-graph-indent for those that would like to retain the old\nbehaviour for most formats.\n\n> > > Apart from having an option to disable indentation.\n> > >\n> > > We could have the cascading to have a limit or make it zig-zag:\n> > >\n> > > instead of:\n> > >\n> > > A\n> > >   B\n> > >     C\n> > >       D\n> > >\n> > > We could do:\n> > >\n> > > A\n> > >   B\n> > > C\n> > >   D\n> > >\n> > > This would have its own edge cases like:\n> > >\n> > > A\n> > >   B\n> > > C <- if we zig-zag here C and D become ambiguous, currently we are\n> > > D    indenting only the last commits (visual roots) here we would have\n> > > D    to chose between continuing cascading or indenting the first of D.\n> > >\n> > > I'm not so sure if I like the zig-zag solution because we need to think again\n> > > if it causes an ambiguity, but I wanted to mention it.\n> > >\n> > > I think we need some more opinions about the design.\n> >\n> > I don't dislike the the current solution but I can see it degenerating\n> > if someone contributes a lot of one-patch series.\n> >\n> > Maybe you could indent commits that are both head and tail up to two\n> > levels and then on the third go back to the beginning of the line. That\n> > way you kind of have a zig-zag but without ambiguity. You'd only have to\n> > add a counter to keep track of the level of indentation.\n> \n> Not sure about this. A zig-zag pattern visually mimics branching and\n> merging, which makes unrelated commits look like a complex merge topology.\n> \n> I also have a feeling that this will end up recreating the exact ambiguity this\n> patch series is trying to fix.\n\nWhile a zig-zag pattern might be ambiguous, what I proposed is a little\ndifferent.\n\nWhat I proposed is effectively a wrapping for anything that goes beyond\ntwo levels of indentation. I don't think it would look anything like a\nfork/merge pattern.\n\n* A\n  * B\n    * C\n* D\n  * E\n    * F\n\nThe difference between two indentation levels and no indentation is very\nnoticeble, I don't think anyone confused this. This would fix the\nstaircase pattern on adjacent one-patch series.\n"},{"id":"547897","messageId":"alOSir26zBU5QO0k@exploit","threadId":"65419","inReplyTo":"alOOXKGIB8BqACxR@exploit","subject":"Re: [PATCH v9 0/4] graph: indent visual roots in graph","fromName":"Mirko Faina","fromEmail":"mroik@delayed.space","sentAt":"2026-07-12T13:12:59Z","receivedAt":"2026-07-12T13:13:03Z","isPatch":true,"body":"On Sun, Jul 12, 2026 at 03:10:49PM +0200, Mirko Faina wrote:\n> The difference between two indentation levels and no indentation is very\n> noticeble, I don't think anyone confused this. This would fix the\n> staircase pattern on adjacent one-patch series.\n\nSorry, meant to write, \"I don't think anyone would be confused by this\".\n"},{"id":"547912","messageId":"DJWR4GEV14P4.3G9N0ZL1R8VDL@gmail.com","threadId":"65419","inReplyTo":"alOOXKGIB8BqACxR@exploit","subject":"Re: [PATCH v9 0/4] graph: indent visual roots in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-12T16:59:58Z","receivedAt":"2026-07-12T17:00:01Z","isPatch":true,"body":"On Sun Jul 12, 2026 at 3:10 PM CEST, Mirko Faina wrote:\n> On Sun, Jul 12, 2026 at 11:26:27AM +0530, Chandra Pratap wrote:\n>> Tying graph-drawing logic to specific formatting flags could introduce\n>> inconsistencies. For example, if a user relies on a custom format like\n>> --format=\"%h %s\", the output is functionally single-line and suffers\n>> from the exact same ambiguity, but it would miss the fix.\n>>\n>> Even in multi-line formats, relying on the absence of a '|' character to spot\n>> unrelated commits requires active effort. Indentation provides an immediate\n>> visual cue that breaks the vertical lineage, which is helpful regardless of the\n>> commit message length.\n>>\n>> I agree with Pablo: for users who strictly want the old behavior, an opt-out\n>> flag keeps the graph logic decoupled from the formatting logic.\n>\n> In that case, together with --[no]-graph-indent, a configuration\n> variable like \"graph.indent\" could be introduced to reduce the usage of\n> --[no]-graph-indent for those that would like to retain the old\n> behaviour for most formats.\n>\n>> > > Apart from having an option to disable indentation.\n>> > >\n>> > > We could have the cascading to have a limit or make it zig-zag:\n>> > >\n>> > > instead of:\n>> > >\n>> > > A\n>> > >   B\n>> > >     C\n>> > >       D\n>> > >\n>> > > We could do:\n>> > >\n>> > > A\n>> > >   B\n>> > > C\n>> > >   D\n>> > >\n>> > > This would have its own edge cases like:\n>> > >\n>> > > A\n>> > >   B\n>> > > C <- if we zig-zag here C and D become ambiguous, currently we are\n>> > > D    indenting only the last commits (visual roots) here we would have\n>> > > D    to chose between continuing cascading or indenting the first of D.\n>> > >\n>> > > I'm not so sure if I like the zig-zag solution because we need to think again\n>> > > if it causes an ambiguity, but I wanted to mention it.\n>> > >\n>> > > I think we need some more opinions about the design.\n>> >\n>> > I don't dislike the the current solution but I can see it degenerating\n>> > if someone contributes a lot of one-patch series.\n>> >\n>> > Maybe you could indent commits that are both head and tail up to two\n>> > levels and then on the third go back to the beginning of the line. That\n>> > way you kind of have a zig-zag but without ambiguity. You'd only have to\n>> > add a counter to keep track of the level of indentation.\n>>\n>> Not sure about this. A zig-zag pattern visually mimics branching and\n>> merging, which makes unrelated commits look like a complex merge topology.\n>>\n>> I also have a feeling that this will end up recreating the exact ambiguity this\n>> patch series is trying to fix.\n>\n> While a zig-zag pattern might be ambiguous, what I proposed is a little\n> different.\n>\n> What I proposed is effectively a wrapping for anything that goes beyond\n> two levels of indentation. I don't think it would look anything like a\n> fork/merge pattern.\n>\n> * A\n>   * B\n>     * C\n> * D\n>   * E\n>     * F\n>\n> The difference between two indentation levels and no indentation is very\n> noticeble, I don't think anyone confused this. This would fix the\n> staircase pattern on adjacent one-patch series.\n\nI agree that having an infinite stair is not a good solution. the 3\ncolumn wrap looks reasonable.\n\nI see two cases with this wrap:\n\n1. No conflict case:\n\n  A\n    B\n      C\n  D\n    E\n      F\n\nNo ambiguity, this would be the ideal case.\n\n2. Ambiguity:\n\nIf it happens that the visual number on visual roots meet the condition\n(number_of_visual_roots % 3 == 0) and the next commit is NOT a visual\nroot this would happen:\n\n  A\n    B\n      C\n  D\n  E\n  E\n\nWhich would be ambiguous. The solution is to check with the lookahead\nbuffer that we have since patch 3 if the next is a visual root, if it's\nnot we indent D anyway:\n\n  A\n    B\n      C\n    D\n  E\n  E\n\nWhich I find the pyramid effect uncomfortable.\nWhat about capping at 4 columns?\n\n1.\n\n  A\n    B\n      C\n        D\n  E\n    F\n      G\n        H\n\n2.\n\n  A\n    B\n      C\n        D\n    E\n  F\n  F\n\nI prefer the 4 column wrap because it looks more abrupt and IMO shows\nbetter that the commits are unrelated.\n\nWhat do you think?\n\nAlso, about the no-opt option \"--no-graph-indent\" is still wanted\nregardless of the final design that we choose?\n\nThanks,\nPablo\n\n"},{"id":"547917","messageId":"alPqaebsr2OPQk5H@exploit","threadId":"65419","inReplyTo":"DJWR4GEV14P4.3G9N0ZL1R8VDL@gmail.com","subject":"Re: [PATCH v9 0/4] graph: indent visual roots in graph","fromName":"Mirko Faina","fromEmail":"mroik@delayed.space","sentAt":"2026-07-12T19:33:11Z","receivedAt":"2026-07-12T19:33:16Z","isPatch":true,"body":"On Sun, Jul 12, 2026 at 06:59:58PM +0200, Pablo Sabater wrote:\n> 2. Ambiguity:\n> \n> If it happens that the visual number on visual roots meet the condition\n> (number_of_visual_roots % 3 == 0) and the next commit is NOT a visual\n> root this would happen:\n> \n>   A\n>     B\n>       C\n>   D\n>   E\n>   E\n> \n> Which would be ambiguous. The solution is to check with the lookahead\n> buffer that we have since patch 3 if the next is a visual root, if it's\n> not we indent D anyway:\n> \n>   A\n>     B\n>       C\n>     D\n>   E\n>   E\n> \n> Which I find the pyramid effect uncomfortable.\n> What about capping at 4 columns?\n> \n> 1.\n> \n>   A\n>     B\n>       C\n>         D\n>   E\n>     F\n>       G\n>         H\n> \n> 2.\n> \n>   A\n>     B\n>       C\n>         D\n>     E\n>   F\n>   F\n> \n> I prefer the 4 column wrap because it looks more abrupt and IMO shows\n> better that the commits are unrelated.\n> \n> What do you think?\n\nI agree, warpping beyond three levels instead of two would resolve this\nambiguity.\n\n> Also, about the no-opt option \"--no-graph-indent\" is still wanted\n> regardless of the final design that we choose?\n\nYes, I personally wouldn't want indentation on non-oneline formats.\n\nThank you.\n"},{"id":"547961","messageId":"CA+J6zkT+Do2P2O2piaMsprhOMx7rBvm26h4i_3NKGG-5g8O=1g@mail.gmail.com","threadId":"65419","inReplyTo":"DJWR4GEV14P4.3G9N0ZL1R8VDL@gmail.com","subject":"Re: [PATCH v9 0/4] graph: indent visual roots in graph","fromName":"Chandra Pratap","fromEmail":"chandrapratap3519@gmail.com","sentAt":"2026-07-13T07:41:05Z","receivedAt":"2026-07-13T07:41:36Z","isPatch":true,"body":"[snip]\n> I agree that having an infinite stair is not a good solution. the 3\n> column wrap looks reasonable.\n>\n> I see two cases with this wrap:\n>\n> 1. No conflict case:\n>\n>   A\n>     B\n>       C\n>   D\n>     E\n>       F\n>\n> No ambiguity, this would be the ideal case.\n>\n> 2. Ambiguity:\n>\n> If it happens that the visual number on visual roots meet the condition\n> (number_of_visual_roots % 3 == 0) and the next commit is NOT a visual\n> root this would happen:\n>\n>   A\n>     B\n>       C\n>   D\n>   E\n>   E\n>\n> Which would be ambiguous. The solution is to check with the lookahead\n> buffer that we have since patch 3 if the next is a visual root, if it's\n> not we indent D anyway:\n>\n>   A\n>     B\n>       C\n>     D\n>   E\n>   E\n>\n> Which I find the pyramid effect uncomfortable.\n> What about capping at 4 columns?\n>\n> 1.\n>\n>   A\n>     B\n>       C\n>         D\n>   E\n>     F\n>       G\n>         H\n>\n> 2.\n>\n>   A\n>     B\n>       C\n>         D\n>     E\n>   F\n>   F\n>\n> I prefer the 4 column wrap because it looks more abrupt and IMO shows\n> better that the commits are unrelated.\n>\n> What do you think?\n\nI agree with Mirko, the 4-column wrap looks like a reasonable compromise.\n\n> Also, about the no-opt option \"--no-graph-indent\" is still wanted\n> regardless of the final design that we choose?\n\nI feel indifferent about this personally, but there are clearly people who have\na use-case for such a flag.\n\nLet us add an explicit opt-out flag: --no-graph-indent alongside a configuration\nvariable: graph.indent, log.graphIndent, or something similar.\n\nA heads up: I think it would best to add these changes as two new commits to\nthe series.\n\nThanks,\nChandra.\n"},{"id":"547965","messageId":"20260713-ps-pre-commit-indent-v10-0-82ddab26bc96@gmail.com","threadId":"65419","inReplyTo":"20260711-ps-pre-commit-indent-v9-0-eab6676e82f7@gmail.com","subject":"[PATCH v10 0/7] graph: indent visual roots in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-13T10:44:35Z","receivedAt":"2026-07-13T10:44:57Z","isPatch":true,"body":"When rendering a graph, if the history contains multiple \"visual roots\",\nactual roots or commits that look like roots (i.e. have their parents\nfiltered out) can end up being vertically adjacent to unrelated commits,\nfalsely appearing to be related.\n\nA fix for this issue was already attempted [1] a while ago.\n\nThis series adds indentation to the visual root commits, so they cannot be\nvertically adjacent anymore making it easier to identify them.\n\nBefore indentation:\n\n\t* A\n\t* B1\n\t* B2\n\t* C1\n\t* C2\n\nAfter indentation:\n\n\t  * A\n\t* B1\n\t \\\n\t  * B2\n\t* C1\n\t* C2\n\nIndents the visual root commits that have still commits to show after\nthem, and if they have children it connects them with an edge at a new\nrow.\n\nIf there are multiple visual roots adjacent in history, the indentation\nstarts with the second one, avoiding redundant indentation of the first\none and cascades after the second.\n\n\t* A\n\t  * B\n\t    * C\n\t      * D\n\t* E\n\t  * F\n\t    * G\n\t      * H\n\t  * I\n\t* J1\n\t* J2\n\nThe indentation wraps after cascading columns and when wrapping back to\nthe initial column if the next commit is a non-visual-root commit, force\nthe indentation one extra level.\n\nSeries explanation:\n\n- Cleanup to bring a common function from t4215 and t6016 that will be\n  used in t4218.\n\n- Logic extraction of the chose of from where the commit source comes\n  from.\n\n- Add a buffer for lookahead purposes.\n\n- Principal commit. Implement the logic to get the visual roots\n  indented.\n\n- Make visual root cascading wrap after 4 columns\n\n- Add --[no-]graph-indent and log.graphIndent options.\n\nGitHub CI: https://github.com/pabloosabaterr/git/actions/runs/29241054418\n\n[1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n\nV9 DIFF:\n\n- visual roots cascading now wrap after 4 columns. This was introduced\n  into a new commit to make reviewing easier because the Main one is\n  already big and has gone through multiple rounds already.\n\n- Made a new graph_read_config() function where the calls to\n  repo_config_get_*() live to leave graph_init() simpler.\n\n- Added --[no-]graph-indent and log.graphIndent options so a user can\n  set his preferences about the graph indentation.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n\n---\nPablo Sabater (7):\n      lib-log-graph: move check_graph function\n      revision: add next_commit_to_show()\n      graph: add a 2 commit buffer for lookahead\n      graph: indent visual root in graph\n      graph: wrap cascading commits after 4 columns\n      graph: move config reading into graph_read_config()\n      graph: add --[no-]graph-indent and log.graphIndent\n\n Documentation/config/log.adoc              |   4 +\n Documentation/rev-list-options.adoc        |   8 +\n graph.c                                    | 332 +++++++++++++++-\n graph.h                                    |  17 +\n revision.c                                 |  57 ++-\n revision.h                                 |   2 +\n t/lib-log-graph.sh                         |   5 +\n t/meson.build                              |   1 +\n t/t4215-log-skewed-merges.sh               |  33 +-\n t/t4218-log-graph-indentation.sh           | 595 +++++++++++++++++++++++++++++\n t/t6016-rev-list-graph-simplify-history.sh |  25 +-\n 11 files changed, 1031 insertions(+), 48 deletions(-)\n\nRange-diff versus v9:\n\n1:  22ab444372 = 1:  9541b410b7 lib-log-graph: move check_graph function\n2:  ebb88c8b29 = 2:  4f8fb2cc1d revision: add next_commit_to_show()\n3:  0705ee321e = 3:  b50574bbe1 graph: add a 2 commit buffer for lookahead\n4:  fa2e60fb3f = 4:  fc3a8253fd graph: indent visual root in graph\n-:  ---------- > 5:  204aae5061 graph: wrap cascading commits after 4 columns\n-:  ---------- > 6:  1b42ed86a1 graph: move config reading into graph_read_config()\n-:  ---------- > 7:  737331b68d graph: add --[no-]graph-indent and log.graphIndent\n\n---\nbase-commit: f60db8d575adb79761d363e026fb49bddf330c73\n"},{"id":"547966","messageId":"20260713-ps-pre-commit-indent-v10-1-82ddab26bc96@gmail.com","threadId":"65419","inReplyTo":"20260713-ps-pre-commit-indent-v10-0-82ddab26bc96@gmail.com","subject":"[PATCH v10 1/7] lib-log-graph: move check_graph function","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-13T10:44:36Z","receivedAt":"2026-07-13T10:44:59Z","isPatch":true,"body":"check_graph is a function shared in the test files t4215 and t6016 used\nto format the output graph, but instead of being in a file called by\nboth test, the function code is repeated in each file.\n\nMove check_graph to lib-log-graph.sh file which both tests already\nimport graph functions from, renaming it to lib_test_check_graph.\n\nThis function is needed for the following commit which includes graph\ntests in a new file and requires check_graph.\n\nMentored-by: Karthik Nayak <karthik.188@gmail.com>\nMentored-by: Chandra Pratap <chandrapratap3519@gmail.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n t/lib-log-graph.sh                         |  5 +++++\n t/t4215-log-skewed-merges.sh               | 33 +++++++++++++-----------------\n t/t6016-rev-list-graph-simplify-history.sh | 25 +++++++++-------------\n 3 files changed, 29 insertions(+), 34 deletions(-)\n\ndiff --git a/t/lib-log-graph.sh b/t/lib-log-graph.sh\nindex bf952ef920..1eae8f60c2 100644\n--- a/t/lib-log-graph.sh\n+++ b/t/lib-log-graph.sh\n@@ -26,3 +26,8 @@ lib_test_cmp_colored_graph () {\n \ttest_decode_color <output.colors.raw | sed \"s/ *\\$//\" >output.colors &&\n \ttest_cmp expect.colors output.colors\n }\n+\n+lib_test_check_graph () {\n+\tcat >expect &&\n+\tlib_test_cmp_graph --format=%s \"$@\"\n+}\ndiff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh\nindex 1612f05f1b..eebab71039 100755\n--- a/t/t4215-log-skewed-merges.sh\n+++ b/t/t4215-log-skewed-merges.sh\n@@ -5,11 +5,6 @@ test_description='git log --graph of skewed merges'\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-log-graph.sh\n \n-check_graph () {\n-\tcat >expect &&\n-\tlib_test_cmp_graph --format=%s \"$@\"\n-}\n-\n test_expect_success 'log --graph with merge fusing with its left and right neighbors' '\n \tgit checkout --orphan _p &&\n \ttest_commit A &&\n@@ -21,7 +16,7 @@ test_expect_success 'log --graph with merge fusing with its left and right neigh\n \tgit checkout _p && git merge --no-ff _r -m G &&\n \tgit checkout @^^ && git merge --no-ff _p -m H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   H\n \t|\\\n \t| *   G\n@@ -49,7 +44,7 @@ test_expect_success 'log --graph with left-skewed merge' '\n \tgit checkout 0_p && git merge --no-ff 0_s -m 0_G &&\n \tgit checkout @^ && git merge --no-ff 0_q 0_r 0_t 0_p -m 0_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*-----.   0_H\n \t|\\ \\ \\ \\\n \t| | | | * 0_G\n@@ -83,7 +78,7 @@ test_expect_success 'log --graph with nested left-skewed merge' '\n \tgit checkout 1_p && git merge --no-ff 1_r -m 1_G &&\n \tgit checkout @^^ && git merge --no-ff 1_p -m 1_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   1_H\n \t|\\\n \t| *   1_G\n@@ -115,7 +110,7 @@ test_expect_success 'log --graph with nested left-skewed merge following normal\n \tgit checkout -b 2_s @^^ && git merge --no-ff 2_q -m 2_J &&\n \tgit checkout 2_p && git merge --no-ff 2_s -m 2_K &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   2_K\n \t|\\\n \t| *   2_J\n@@ -151,7 +146,7 @@ test_expect_success 'log --graph with nested right-skewed merge following left-s\n \tgit checkout 3_p && git merge --no-ff 3_r -m 3_H &&\n \tgit checkout @^^ && git merge --no-ff 3_p -m 3_J &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   3_J\n \t|\\\n \t| *   3_H\n@@ -182,7 +177,7 @@ test_expect_success 'log --graph with right-skewed merge following a left-skewed\n \tgit merge --no-ff 4_p -m 4_G &&\n \tgit checkout @^^ && git merge --no-ff 4_s -m 4_H &&\n \n-\tcheck_graph --date-order <<-\\EOF\n+\tlib_test_check_graph --date-order <<-\\EOF\n \t*   4_H\n \t|\\\n \t| *   4_G\n@@ -218,7 +213,7 @@ test_expect_success 'log --graph with octopus merge with column joining its penu\n \tgit checkout 5_r &&\n \tgit merge --no-ff 5_s -m 5_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   5_H\n \t|\\\n \t| *-.   5_G\n@@ -257,7 +252,7 @@ test_expect_success 'log --graph with multiple tips' '\n \tgit checkout 6_1 &&\n \tgit merge --no-ff 6_2 -m 6_I &&\n \n-\tcheck_graph 6_1 6_3 6_5 <<-\\EOF\n+\tlib_test_check_graph 6_1 6_3 6_5 <<-\\EOF\n \t*   6_I\n \t|\\\n \t| | *   6_H\n@@ -334,7 +329,7 @@ test_expect_success 'log --graph with multiple tips' '\n \tgit checkout -b M_7 7_1 &&\n \tgit merge --no-ff 7_2 7_3 -m 7_M4 &&\n \n-\tcheck_graph M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -371,7 +366,7 @@ test_expect_success 'log --graph with multiple tips' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' '\n-\tcheck_graph --graph-lane-limit=2 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=2 M_7 <<-\\EOF\n \t*-.   7_M4\n \t|\\ \\\n \t| | * 7_G\n@@ -388,7 +383,7 @@ test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=1 truncate mid octopus merge' '\n-\tcheck_graph --graph-lane-limit=1 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=1 M_7 <<-\\EOF\n \t*-~  7_M4\n \t|\\~\n \t| ~ 7_G\n@@ -405,7 +400,7 @@ test_expect_success 'log --graph --graph-lane-limit=1 truncate mid octopus merge\n '\n \n test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' '\n-\tcheck_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -441,7 +436,7 @@ test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=6 check if it only shows first of 3 parent merge' '\n-\tcheck_graph --graph-lane-limit=6 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=6 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -478,7 +473,7 @@ test_expect_success 'log --graph --graph-lane-limit=6 check if it only shows fir\n '\n \n test_expect_success 'log --graph --graph-lane-limit=7 check if it shows all 3 parent merge' '\n-\tcheck_graph --graph-lane-limit=7 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=7 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\ndiff --git a/t/t6016-rev-list-graph-simplify-history.sh b/t/t6016-rev-list-graph-simplify-history.sh\nindex 54b0a6f5f8..e0d9c3c1ac 100755\n--- a/t/t6016-rev-list-graph-simplify-history.sh\n+++ b/t/t6016-rev-list-graph-simplify-history.sh\n@@ -13,11 +13,6 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-log-graph.sh\n \n-check_graph () {\n-\tcat >expect &&\n-\tlib_test_cmp_graph --format=%s \"$@\"\n-}\n-\n test_expect_success 'set up rev-list --graph test' '\n \t# 3 commits on branch A\n \ttest_commit A1 foo.txt &&\n@@ -54,7 +49,7 @@ test_expect_success 'set up rev-list --graph test' '\n '\n \n test_expect_success '--graph --all' '\n-\tcheck_graph --all <<-\\EOF\n+\tlib_test_check_graph --all <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -82,7 +77,7 @@ test_expect_success '--graph --all' '\n # that undecorated merges are interesting, even with --simplify-by-decoration\n test_expect_success '--graph --simplify-by-decoration' '\n \tgit tag -d A4 &&\n-\tcheck_graph --all --simplify-by-decoration <<-\\EOF\n+\tlib_test_check_graph --all --simplify-by-decoration <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -114,7 +109,7 @@ test_expect_success 'setup: get rid of decorations on B' '\n \n # Graph with branch B simplified away\n test_expect_success '--graph --simplify-by-decoration prune branch B' '\n-\tcheck_graph --simplify-by-decoration --all <<-\\EOF\n+\tlib_test_check_graph --simplify-by-decoration --all <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -133,7 +128,7 @@ test_expect_success '--graph --simplify-by-decoration prune branch B' '\n '\n \n test_expect_success '--graph --full-history -- bar.txt' '\n-\tcheck_graph --full-history --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --full-history --all -- bar.txt <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -148,7 +143,7 @@ test_expect_success '--graph --full-history -- bar.txt' '\n '\n \n test_expect_success '--graph --full-history --simplify-merges -- bar.txt' '\n-\tcheck_graph --full-history --simplify-merges --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --full-history --simplify-merges --all -- bar.txt <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -161,7 +156,7 @@ test_expect_success '--graph --full-history --simplify-merges -- bar.txt' '\n '\n \n test_expect_success '--graph -- bar.txt' '\n-\tcheck_graph --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --all -- bar.txt <<-\\EOF\n \t* A7\n \t* A5\n \t* A3\n@@ -172,7 +167,7 @@ test_expect_success '--graph -- bar.txt' '\n '\n \n test_expect_success '--graph --sparse -- bar.txt' '\n-\tcheck_graph --sparse --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --sparse --all -- bar.txt <<-\\EOF\n \t* A7\n \t* A6\n \t* A5\n@@ -189,7 +184,7 @@ test_expect_success '--graph --sparse -- bar.txt' '\n '\n \n test_expect_success '--graph ^C4' '\n-\tcheck_graph --all ^C4 <<-\\EOF\n+\tlib_test_check_graph --all ^C4 <<-\\EOF\n \t* A7\n \t* A6\n \t* A5\n@@ -202,7 +197,7 @@ test_expect_success '--graph ^C4' '\n '\n \n test_expect_success '--graph ^C3' '\n-\tcheck_graph --all ^C3 <<-\\EOF\n+\tlib_test_check_graph --all ^C3 <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -220,7 +215,7 @@ test_expect_success '--graph ^C3' '\n # that important, but this test depends on it.  If the ordering ever changes\n # in the code, we'll need to update this test.\n test_expect_success '--graph --boundary ^C3' '\n-\tcheck_graph --boundary --all ^C3 <<-\\EOF\n+\tlib_test_check_graph --boundary --all ^C3 <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n\n-- \n2.54.0\n"},{"id":"547967","messageId":"20260713-ps-pre-commit-indent-v10-2-82ddab26bc96@gmail.com","threadId":"65419","inReplyTo":"20260713-ps-pre-commit-indent-v10-0-82ddab26bc96@gmail.com","subject":"[PATCH v10 2/7] revision: add next_commit_to_show()","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-13T10:44:37Z","receivedAt":"2026-07-13T10:45:00Z","isPatch":true,"body":"get_revision() gets its commits from two sources depending on the mode:\n\n1. Normally it gets the commits from get_revision_internal().\n\n2. --max-count-oldest which was introduced at bb4ce23284 (revision.c:\n   implement --max-count-oldest, 2026-05-19) gets the commits by popping\n   from a saved list at revs->commits marking SHOWN and CHILD_SHOWN on\n   each popped commit.\n\nExtract the choice logic into a helper, next_commit_to_show(), which\nreturns the next commit regardless of the source it comes from.\n\nThis has no change in behavior. The helper is needed in a subsequent\ncommit that pre-fetches two commits into a buffer for lookahead purposes\nand needs to pre-fetch from the same source.\n\nThe --reverse branch keeps its own pop loop. Using the helper for\n--reverse would additionally set SHOWN and CHILD_SHOWN which is not\ndesired and a behavior change.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n revision.c | 36 ++++++++++++++++++++++++------------\n 1 file changed, 24 insertions(+), 12 deletions(-)\n\ndiff --git a/revision.c b/revision.c\nindex 0c95edef59..288935943f 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -4658,12 +4658,34 @@ static void retrieve_oldest_commits(struct rev_info *revs,\n \t\tcommit_list_insert(c, queue);\n }\n \n+/*\n+ * Returns the next commit that will be shown, regardless of whether it comes\n+ * directly from the revision walk or from the list saved by the staged output\n+ * of --max-count-oldest.\n+ */\n+static struct commit *next_commit_to_show(struct rev_info *revs)\n+{\n+\tstruct commit *c;\n+\tstruct commit_list *p;\n+\n+\tif (!revs->max_count_stage)\n+\t\treturn get_revision_internal(revs);\n+\n+\tc = pop_commit(&revs->commits);\n+\tif (c) {\n+\t\tc->object.flags |= SHOWN;\n+\t\tif (!(c->object.flags & BOUNDARY))\n+\t\t\tfor (p = c->parents; p; p = p->next)\n+\t\t\t\tp->item->object.flags |= CHILD_SHOWN;\n+\t}\n+\treturn c;\n+}\n+\n struct commit *get_revision(struct rev_info *revs)\n {\n \tstruct commit *c;\n \tstruct commit_list *reversed;\n \tstruct commit_list *queue = NULL;\n-\tstruct commit_list *p;\n \n \tif (revs->max_count_type == 1 && !revs->max_count_stage) {\n \t\tretrieve_oldest_commits(revs, &queue);\n@@ -4693,17 +4715,7 @@ struct commit *get_revision(struct rev_info *revs)\n \t\treturn c;\n \t}\n \n-\tif (revs->max_count_stage) {\n-\t\tc = pop_commit(&revs->commits);\n-\t\tif (c) {\n-\t\t\tc->object.flags |= SHOWN;\n-\t\t\tif (!(c->object.flags & BOUNDARY))\n-\t\t\t\tfor (p = c->parents; p; p = p->next)\n-\t\t\t\t\tp->item->object.flags |= CHILD_SHOWN;\n-\t\t}\n-\t} else {\n-\t\tc = get_revision_internal(revs);\n-\t}\n+\tc = next_commit_to_show(revs);\n \n \tif (c && revs->graph)\n \t\tgraph_update(revs->graph, c);\n\n-- \n2.54.0\n"},{"id":"547968","messageId":"20260713-ps-pre-commit-indent-v10-3-82ddab26bc96@gmail.com","threadId":"65419","inReplyTo":"20260713-ps-pre-commit-indent-v10-0-82ddab26bc96@gmail.com","subject":"[PATCH v10 3/7] graph: add a 2 commit buffer for lookahead","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-13T10:44:38Z","receivedAt":"2026-07-13T10:45:01Z","isPatch":true,"body":"In a subsequent commit the graph renderer needs to know if the next\ncommit is a visual root or if it is the last commit to be shown. This\nrequires peeking 2 commits ahead.\n\nCommits are pre-fetched in get_revision() through next_commit_to_show()\nwhere they are also marked as SHOWN, regardless the source they come\nfrom.\n\nUpdate graph_is_interesting() so it considers commits inside the\nlookahead buffer as interesting as well.\n\nHelped-by: Kristofer Karlsson <krka@spotify.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c    | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++\n graph.h    | 17 +++++++++++++++++\n revision.c | 18 ++++++++++++++++--\n 3 files changed, 84 insertions(+), 2 deletions(-)\n\ndiff --git a/graph.c b/graph.c\nindex 842282685f..89ebcf7540 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -315,6 +315,14 @@ struct git_graph {\n \t * diff_output_prefix_callback().\n \t */\n \tstruct strbuf prefix_buf;\n+\n+\t/*\n+\t * Lookahead buffer: up to 2 pre-fetched commits that will be shown.\n+\t * Populated by get_revision() so graph_peek_next_visible() can use\n+\t * actual walk results instead of peeking at rev_info internals.\n+\t */\n+\tstruct commit *lookahead[2];\n+\tint lookahead_nr;\n };\n \n static inline int graph_needs_truncation(struct git_graph *graph, int lane)\n@@ -388,6 +396,9 @@ struct git_graph *graph_init(struct rev_info *opt)\n \tgraph->num_columns = 0;\n \tgraph->num_new_columns = 0;\n \tgraph->mapping_size = 0;\n+\tgraph->lookahead[0] = NULL;\n+\tgraph->lookahead[1] = NULL;\n+\tgraph->lookahead_nr = 0;\n \t/*\n \t * Start the column color at the maximum value, since we'll\n \t * always increment it for the first commit we output.\n@@ -456,6 +467,15 @@ static void graph_ensure_capacity(struct git_graph *graph, int num_columns)\n  */\n static int graph_is_interesting(struct git_graph *graph, struct commit *commit)\n {\n+\t/*\n+\t * Commits in the lookahead buffer have been pre-fetched by\n+\t * get_revision() and will be shown in the future. They already have\n+\t * the SHOWN flag set when they were pre-fetched but the graph still\n+\t * needs to treat them as interesting parents.\n+\t */\n+\tfor (int i = 0; i < graph->lookahead_nr; i++)\n+\t\tif (graph->lookahead[i] == commit)\n+\t\t\treturn 1;\n \t/*\n \t * If revs->boundary is set, commits whose children have\n \t * been shown are always interesting, even if they have the\n@@ -763,6 +783,37 @@ static int graph_needs_pre_commit_line(struct git_graph *graph)\n \t       graph->expansion_row < graph_num_expansion_rows(graph);\n }\n \n+struct commit *graph_pop_lookahead(struct git_graph *graph)\n+{\n+\tstruct commit *c;\n+\n+\tif (!graph->lookahead_nr)\n+\t\treturn NULL;\n+\n+\tc = graph->lookahead[0];\n+\tif (!c)\n+\t\tBUG(\"lookahead buffer has %d entries but the first one is NULL\",\n+\t\t    graph->lookahead_nr);\n+\n+\tgraph->lookahead[0] = graph->lookahead[1];\n+\tgraph->lookahead[1] = NULL;\n+\tgraph->lookahead_nr--;\n+\treturn c;\n+}\n+\n+int graph_get_lookahead_room(struct git_graph *graph)\n+{\n+\treturn (int)ARRAY_SIZE(graph->lookahead) - graph->lookahead_nr;\n+}\n+\n+void graph_push_lookahead(struct git_graph *graph, struct commit *c)\n+{\n+\tif (!graph_get_lookahead_room(graph))\n+\t\tBUG(\"pushing into lookahead buffer when it is already full\");\n+\n+\tgraph->lookahead[graph->lookahead_nr++] = c;\n+}\n+\n void graph_update(struct git_graph *graph, struct commit *commit)\n {\n \tstruct commit_list *parent;\ndiff --git a/graph.h b/graph.h\nindex 3fd1dcb2e9..1193711fb8 100644\n--- a/graph.h\n+++ b/graph.h\n@@ -262,4 +262,21 @@ void graph_show_commit_msg(struct git_graph *graph,\n \t\t\t   FILE *file,\n \t\t\t   struct strbuf const *sb);\n \n+/*\n+ * Pop the first commit from the graph's lookahead buffer.\n+ * Returns NULL if the buffer is empty.\n+ */\n+struct commit *graph_pop_lookahead(struct git_graph *graph);\n+\n+/*\n+ * Returns how many more commits can be added to the lookahead buffer.\n+ */\n+int graph_get_lookahead_room(struct git_graph *graph);\n+\n+/*\n+ * Push a commit into the lookahead buffer. Must only be called when\n+ * graph_get_lookahead_room() returns > 0.\n+ */\n+void graph_push_lookahead(struct git_graph *graph, struct commit *c);\n+\n #endif /* GRAPH_H */\ndiff --git a/revision.c b/revision.c\nindex 288935943f..258c3cf782 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -4715,10 +4715,24 @@ struct commit *get_revision(struct rev_info *revs)\n \t\treturn c;\n \t}\n \n-\tc = next_commit_to_show(revs);\n+\tif (revs->graph) {\n+\t\tc = graph_pop_lookahead(revs->graph);\n+\t\tif (!c)\n+\t\t\tc = next_commit_to_show(revs);\n+\t} else {\n+\t\tc = next_commit_to_show(revs);\n+\t}\n \n-\tif (c && revs->graph)\n+\tif (c && revs->graph) {\n+\t\twhile (graph_get_lookahead_room(revs->graph)) {\n+\t\t\tstruct commit *next = next_commit_to_show(revs);\n+\t\t\tif (!next)\n+\t\t\t\tbreak;\n+\t\t\tgraph_push_lookahead(revs->graph, next);\n+\t\t}\n \t\tgraph_update(revs->graph, c);\n+\t}\n+\n \tif (!c) {\n \t\tfree_saved_parents(revs);\n \t\tcommit_list_free(revs->previous_parents);\n\n-- \n2.54.0\n"},{"id":"547969","messageId":"20260713-ps-pre-commit-indent-v10-4-82ddab26bc96@gmail.com","threadId":"65419","inReplyTo":"20260713-ps-pre-commit-indent-v10-0-82ddab26bc96@gmail.com","subject":"[PATCH v10 4/7] graph: indent visual root in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-13T10:44:39Z","receivedAt":"2026-07-13T10:45:02Z","isPatch":true,"body":"When rendering a graph, if the history contains multiple \"visual roots\",\nactual roots or commits that look like roots (i.e. have their parents\nfiltered out) can end up being vertically adjacent to unrelated commits,\nfalsely appearing to be related.\n\nA fix for this issue was already attempted [1] a while ago.\n\nThis happens because the commits fill the space from left to right and\nwhen a visual root ends, its column becomes free for the following\ncommit even if they are not related. Once this happens the unrelated\ncommit is rendered below the visual root. Because there is no special\ncharacter or way to identify when a visual root is rendered making the\ngraph confusing.\n\nBy indenting the visual roots when there are still commits to show the\nvertical adjacency can be avoided.\n\nAdd is_visual_root flag to git_graph making it visible in all graph states,\ngive graph_update() a new function, graph_is_visual_root() to know if the\ncurrent commit is a visual root and set is_visual_root.\nThe different handled cases are:\n\n- If a visual root has children: similar to GRAPH_PRE_COMMIT state when\n  octopus merges need space, an edge row needs to be printed to connect\n  the child with the indented visual root. A new state GRAPH_PRE_ROOT is\n  needed to connect the child with the visual root:\n\n    * child of the visual root\n     \\ GRAPH_PRE_ROOT\n      * visual root indented\n\n- If a visual root is child-less we can skip GRAPH_PRE_ROOT state and\n  render the indented commit directly.\n\n      * visual root indented\n    * unrelated commit\n\n- If two or more visual roots are adjacent: by having a lookahead to the\n  next commit that will be rendered, if the next commit is also a visual\n  root and we are on a visual root, meaning two visual root adjacent in\n  the history, the top one can omit the indent, making the one below to\n  indent only once, if there are more adjacent visual commits, the\n  indentation will increase for each adjacent one, cascading.\n\n    * visual root\n      * visual root\n        * visual root\n    * last commit\n\n  Even if the last commit is a root, because there is nothing that will be\n  rendered below we can omit the indentation on purpose.\n\n[1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n\nHelped-by: Kristofer Karlsson <krka@spotify.com>\nMentored-by: Karthik Nayak <karthik.188@gmail.com>\nMentored-by: Chandra Pratap <chandrapratap3519@gmail.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c                          | 244 +++++++++++++++++++\n t/meson.build                    |   1 +\n t/t4218-log-graph-indentation.sh | 514 +++++++++++++++++++++++++++++++++++++++\n 3 files changed, 759 insertions(+)\n\ndiff --git a/graph.c b/graph.c\nindex 89ebcf7540..087094189f 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -60,12 +60,23 @@ struct column {\n \t * index into column_colors.\n \t */\n \tunsigned short color;\n+\t/*\n+\t * Marks if a commit is a non-first parent of a merge. These columns are\n+\t * already visually connected to the merge commit and do not need\n+\t * indentation.\n+\t *\n+\t * The first parent is the one that inherits the column and it can need\n+\t * indentation if turns out to be a visual root and there's still\n+\t * commits to render.\n+\t */\n+\tunsigned int is_merge_parent:1;\n };\n \n enum graph_state {\n \tGRAPH_PADDING,\n \tGRAPH_SKIP,\n \tGRAPH_PRE_COMMIT,\n+\tGRAPH_PRE_ROOT,\n \tGRAPH_COMMIT,\n \tGRAPH_POST_MERGE,\n \tGRAPH_COLLAPSING\n@@ -323,6 +334,51 @@ struct git_graph {\n \t */\n \tstruct commit *lookahead[2];\n \tint lookahead_nr;\n+\n+\t/*\n+\t * If a commit is a visual root, we need to indent it to prevent\n+\t * unrelated commits from being vertically adjacent to it.\n+\t */\n+\tunsigned int is_visual_root:1;\n+\n+\t/*\n+\t * Indentation increases for each visual root adjacent to another visual\n+\t * root, making visual root commits indentation cascade.\n+\t */\n+\tunsigned int visual_root_depth;\n+\n+\t/*\n+\t * When a visual root is adjacent to other visual roots, the first one\n+\t * can avoid indentation and the rest cascades, increasing the indentation\n+\t * for each one.\n+\t */\n+\tunsigned int visual_root_cascade:1;\n+\n+\t/*\n+\t * Set when the current commit was already present in graph->columns\n+\t * before being processed.\n+\t */\n+\tunsigned int commit_in_columns:1;\n+};\n+\n+struct graph_lookahead_flags {\n+\n+\t/*\n+\t * Set when there will be a commit after the current one that will be\n+\t * rendered.\n+\t */\n+\tunsigned int is_next_visible:1;\n+\n+\t/*\n+\t * Set when the next visible commit is candidate to be a visual root.\n+\t */\n+\tunsigned int is_next_visual_root:1;\n+\n+\t/*\n+\t * Set when the next visible commit will be rendered under the current\n+\t * commit.\n+\t */\n+\tunsigned int next_has_column:1;\n };\n \n static inline int graph_needs_truncation(struct git_graph *graph, int lane)\n@@ -399,6 +455,8 @@ struct git_graph *graph_init(struct rev_info *opt)\n \tgraph->lookahead[0] = NULL;\n \tgraph->lookahead[1] = NULL;\n \tgraph->lookahead_nr = 0;\n+\tgraph->visual_root_depth = 0;\n+\tgraph->visual_root_cascade = 0;\n \t/*\n \t * Start the column color at the maximum value, since we'll\n \t * always increment it for the first commit we output.\n@@ -581,6 +639,11 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t\t\t\t\t  struct commit *commit,\n \t\t\t\t\t  int idx)\n {\n+\t/*\n+\t * Get the initial merge_layout before it's modified to know if this\n+\t * is a merge.\n+\t */\n+\tint initial_merge_layout = graph->merge_layout;\n \tint i = graph_find_new_column_by_commit(graph, commit);\n \tint mapping_idx;\n \n@@ -592,6 +655,7 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t\ti = graph->num_new_columns++;\n \t\tgraph->new_columns[i].commit = commit;\n \t\tgraph->new_columns[i].color = graph_find_commit_color(graph, commit);\n+\t\tgraph->new_columns[i].is_merge_parent = 0;\n \t}\n \n \tif (graph->num_parents > 1 && idx > -1 && graph->merge_layout == -1) {\n@@ -630,6 +694,12 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t}\n \n \tgraph->mapping[mapping_idx] = i;\n+\n+\t/*\n+\t * Mark non-first parents of a merge.\n+\t */\n+\tif (graph->num_parents > 1 && initial_merge_layout >= 0 && idx > -1)\n+\t\tgraph->new_columns[i].is_merge_parent = 1;\n }\n \n static void graph_update_columns(struct git_graph *graph)\n@@ -721,10 +791,20 @@ static void graph_update_columns(struct git_graph *graph)\n \t\t\tif (graph->num_parents == 0)\n \t\t\t\tgraph->width += 2;\n \t\t} else {\n+\t\t\tint j;\n \t\t\tgraph_insert_into_new_columns(graph, col_commit, -1);\n+\t\t\t/*\n+\t\t\t * This column is not the current commit, but we need to\n+\t\t\t * propagate the flag until the commit is processed.\n+\t\t\t */\n+\t\t\tj = graph_find_new_column_by_commit(graph, col_commit);\n+\t\t\tif (j >= 0 && graph->columns[i].is_merge_parent)\n+\t\t\t\tgraph->new_columns[j].is_merge_parent = 1;\n \t\t}\n \t}\n \n+\tgraph->commit_in_columns = is_commit_in_columns;\n+\n \t/*\n \t * If graph_max_lanes is set, cap the width\n \t */\n@@ -814,9 +894,113 @@ void graph_push_lookahead(struct git_graph *graph, struct commit *c)\n \tgraph->lookahead[graph->lookahead_nr++] = c;\n }\n \n+/*\n+ * A commit can be a visual root when:\n+ *\n+ * - It has no parents.\n+ *\n+ * - It has parents but they are all filtered out and\n+ *   commit->parents arrives NULL.\n+ *\n+ * - Its parents are uninteresting.\n+ *\n+ * - It is not a boundary commit. Boundary commits also have no visible\n+ *   parents, but they are not selected as visual roots because they cannot\n+ *   cause the ambiguity of being vertically adjacent because:\n+ *\n+ *   1. A boundary only appears because an included commit is its child.\n+ *      Children are always above, and the renderer draws an edge down to\n+ *      the boundary from that child. Rather than starting a column like a\n+ *      visual root would do, it inherits its child column.\n+ *\n+ *   2. Included commits cannot appear below a boundary. Boundaries are\n+ *      ancestors of the exclusion point; if an included commit were an\n+ *      ancestor of the boundary it would be excluded and not rendered.\n+ *      Boundaries therefore always sink to the bottom.\n+ */\n+static int graph_is_visual_root_candidate(struct commit *c, struct git_graph *graph)\n+{\n+\tstruct commit_list *p;\n+\n+\tif (c->object.flags & BOUNDARY)\n+\t\treturn 0;\n+\tfor (p = c->parents; p; p = p->next)\n+\t\tif (graph_is_interesting(graph, p->item))\n+\t\t\treturn 0;\n+\treturn 1;\n+}\n+\n+static int graph_is_visual_root(struct git_graph *graph,\n+\t\t\t\tstruct graph_lookahead_flags *flags)\n+{\n+\t/*\n+\t * This must be only called for the current commit as graph contains\n+\t * the state for the current commit only.\n+\t *\n+\t * To check if a commit is a visual root, call graph_is_visual_root_candidate()\n+\t * but we won't know if it is really a visual root until we get to the\n+\t * next commit state.\n+\t *\n+\t * The current commit is an actual visual root if it is a candidate and\n+\t * the commit is not a non-first parent of a merge.\n+\t *\n+\t *   *\n+\t *   |\\\n+\t *   | *    <- it is a visual root candidate but it shouldn't be indented\n+\t *   *         because it is already connected by an edge.\n+\t *   ^         if commit_in_columns && is_merge_parent means the commit\n+\t *   |         was put by a merge and is connected.\n+\t *   |\n+\t *   `-------- if !is_next_visible means we're on the last commit, avoid\n+\t *             indentation unless the one before is a visual root, then\n+\t *             we need to differentiate from the one above.\n+\t *\n+\t * If next_has_columns means that the next commit has\n+\t * already a column, so it will not be rendered below, the\n+\t * current commit has to act as the last commit and omit\n+\t * indentation.\n+\t */\n+\treturn graph_is_visual_root_candidate(graph->commit, graph) &&\n+\t       !(graph->commit_in_columns &&\n+\t\t graph->columns[graph->commit_index].is_merge_parent) &&\n+\t       flags->is_next_visible &&\n+\t       (!flags->next_has_column || graph->visual_root_depth > 0);\n+}\n+\n+/*\n+ * Peeks the next commits via the lookahead buffer and sets the lookahead flags.\n+ */\n+static void graph_peek_next_visible(struct git_graph *graph,\n+\t\t\t\t    struct graph_lookahead_flags *flags)\n+{\n+\tflags->is_next_visible = 0;\n+\tflags->is_next_visual_root = 0;\n+\tflags->next_has_column = 0;\n+\n+\tif (!graph->lookahead_nr)\n+\t\treturn;\n+\n+\tflags->is_next_visible = 1;\n+\tflags->next_has_column =\n+\t\tgraph_find_new_column_by_commit(graph, graph->lookahead[0]) >= 0;\n+\n+\tif (!graph_is_visual_root_candidate(graph->lookahead[0], graph))\n+\t\treturn;\n+\n+\tif (graph->lookahead_nr >= 2)\n+\t\tflags->is_next_visual_root = 1;\n+}\n+\n+static int graph_needs_pre_root_line(struct git_graph *graph)\n+{\n+\treturn graph->commit_in_columns && graph->is_visual_root &&\n+\t       graph->num_columns > 0 && !graph->visual_root_cascade;\n+}\n+\n void graph_update(struct git_graph *graph, struct commit *commit)\n {\n \tstruct commit_list *parent;\n+\tstruct graph_lookahead_flags flags;\n \n \t/*\n \t * Set the new commit\n@@ -847,6 +1031,23 @@ void graph_update(struct git_graph *graph, struct commit *commit)\n \t */\n \tgraph_update_columns(graph);\n \n+\tgraph_peek_next_visible(graph, &flags);\n+\n+\tgraph->is_visual_root = graph_is_visual_root(graph, &flags);\n+\n+\tif (graph->is_visual_root) {\n+\t\t/*\n+\t\t * If next is a visual root we can omit the indent for the first\n+\t\t * visual root and start cascading.\n+\t\t */\n+\t\tif (!graph->visual_root_depth && flags.is_next_visual_root)\n+\t\t\tgraph->visual_root_cascade = 1;\n+\t\tgraph->visual_root_depth++;\n+\t} else {\n+\t\tgraph->visual_root_depth = 0;\n+\t\tgraph->visual_root_cascade = 0;\n+\t}\n+\n \tgraph->expansion_row = 0;\n \n \t/*\n@@ -864,11 +1065,16 @@ void graph_update(struct git_graph *graph, struct commit *commit)\n \t * room for it.  We need to do this only if there is a branch row\n \t * (or more) to the right of this commit.\n \t *\n+\t * If it is a visual root, we need to print an extra row to\n+\t * connect the indentation.\n+\t *\n \t * If there are less than 3 parents, we can immediately print the\n \t * commit line.\n \t */\n \tif (graph->state != GRAPH_PADDING)\n \t\tgraph->state = GRAPH_SKIP;\n+\telse if (graph_needs_pre_root_line(graph))\n+\t\tgraph->state = GRAPH_PRE_ROOT;\n \telse if (graph_needs_pre_commit_line(graph))\n \t\tgraph->state = GRAPH_PRE_COMMIT;\n \telse\n@@ -1116,6 +1322,17 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \n \t\tif (col_commit == graph->commit) {\n \t\t\tseen_this = 1;\n+\t\t\tif (graph->is_visual_root) {\n+\t\t\t\tint depth = graph->visual_root_depth;\n+\t\t\t\t/*\n+\t\t\t\t * Each visual column is 2 characters wide.\n+\t\t\t\t * Omit the indentation for the first visual\n+\t\t\t\t * root in cascade mode.\n+\t\t\t\t */\n+\t\t\t\tint padding = (depth - graph->visual_root_cascade) * 2;\n+\t\t\t\tgraph_line_addchars(line, ' ', padding);\n+\t\t\t\tgraph->width += padding;\n+\t\t\t}\n \t\t\tgraph_output_commit_char(graph, line);\n \n \t\t\tif (graph_needs_truncation(graph, i)) {\n@@ -1487,6 +1704,30 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n \t\tgraph_update_state(graph, GRAPH_PADDING);\n }\n \n+static void graph_output_pre_root_line(struct git_graph *graph, struct graph_line *line)\n+{\n+\t/*\n+\t * This function adds a row before a visual root, to connect the\n+\t * branch to the indented commit. It must only be called on a\n+\t * visual root.\n+\t */\n+\tif (!graph->is_visual_root)\n+\t\tBUG(\"commit must be a visual root to call pre_root_line\");\n+\n+\tfor (int i = 0; i < graph->num_columns; i++) {\n+\t\tstruct column *col = &graph->columns[i];\n+\t\tif (col->commit == graph->commit) {\n+\t\t\tgraph_line_addch(line, ' ');\n+\t\t\tgraph_line_write_column(line, col, '\\\\');\n+\t\t} else {\n+\t\t\tgraph_line_write_column(line, col, '|');\n+\t\t}\n+\t\tgraph_line_addch(line, ' ');\n+\t}\n+\n+\tgraph_update_state(graph, GRAPH_COMMIT);\n+}\n+\n int graph_next_line(struct git_graph *graph, struct strbuf *sb)\n {\n \tint shown_commit_line = 0;\n@@ -1512,6 +1753,9 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb)\n \tcase GRAPH_PRE_COMMIT:\n \t\tgraph_output_pre_commit_line(graph, &line);\n \t\tbreak;\n+\tcase GRAPH_PRE_ROOT:\n+\t\tgraph_output_pre_root_line(graph, &line);\n+\t\tbreak;\n \tcase GRAPH_COMMIT:\n \t\tgraph_output_commit_line(graph, &line);\n \t\tshown_commit_line = 1;\ndiff --git a/t/meson.build b/t/meson.build\nindex 7c3c070426..cce5ba71f9 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -577,6 +577,7 @@ integration_tests = [\n   't4215-log-skewed-merges.sh',\n   't4216-log-bloom.sh',\n   't4217-log-limit.sh',\n+  't4218-log-graph-indentation.sh',\n   't4219-log-follow-merge.sh',\n   't4252-am-options.sh',\n   't4253-am-keep-cr-dos.sh',\ndiff --git a/t/t4218-log-graph-indentation.sh b/t/t4218-log-graph-indentation.sh\nnew file mode 100755\nindex 0000000000..60c7d84af7\n--- /dev/null\n+++ b/t/t4218-log-graph-indentation.sh\n@@ -0,0 +1,514 @@\n+#!/bin/sh\n+\n+test_description='git log --graph visual root indentations'\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-log-graph.sh\n+\n+check_graph_with_description () {\n+\tcat >expect &&\n+\tlib_test_cmp_graph --format=\"%s%ndescription%nsecond-line\" \"$@\"\n+}\n+\n+create_orphan () {\n+\tgit checkout --orphan \"$1\" &&\n+\ttest_might_fail git rm -rf .\n+}\n+\n+# disable commit-graph topo order to have the graph to render in different\n+# ways (used in --first-parent tests to have multiple visual roots while a\n+# column is active at the same time).\n+unset_commit_graph () {\n+\tsane_unset GIT_TEST_COMMIT_GRAPH &&\n+\trm -f .git/objects/info/commit-graph &&\n+\trm -rf .git/objects/info/commit-graphs\n+}\n+\n+test_expect_success 'single root commit is not indented' '\n+\tcreate_orphan _1 && test_commit 1_A &&\n+\tlib_test_check_graph _1 <<-\\EOF\n+\t* 1_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indented before unrelated branch' '\n+\tcreate_orphan _2 && test_commit 2_A && test_commit 2_B &&\n+\tcreate_orphan _3 && test_commit 3_A &&\n+\tlib_test_check_graph _2 _3 <<-\\EOF\n+\t  * 3_A\n+\t* 2_B\n+\t* 2_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indentation with --left-right' '\n+\tlib_test_check_graph --left-right _2..._3 <<-\\EOF\n+\t  > 3_A\n+\t< 2_B\n+\t< 2_A\n+\tEOF\n+'\n+\n+# A better case of why indentation is still needed with '--left-right' flag is\n+# that unrelated branches can be on the same side, so it's needed to\n+# differentiate visual roots on the same side.\n+test_expect_success 'visual root indentation with --left-right having unrelated commits on the same side' '\n+\tlib_test_check_graph --left-right _2..._3 _1 <<-\\EOF\n+\t  > 3_A\n+\t< 2_B\n+\t \\\n+\t  < 2_A\n+\t> 1_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indents the description also' '\n+\tcheck_graph_with_description _2 _3 <<-\\EOF\n+\t  * 3_A\n+\t    description\n+\t    second-line\n+\t* 2_B\n+\t| description\n+\t| second-line\n+\t* 2_A\n+\t  description\n+\t  second-line\n+\tEOF\n+'\n+\n+test_expect_success 'indented visual root parent gets connected to its child' '\n+\tcreate_orphan _4 && test_commit 4_A && test_commit 4_B &&\n+\tcreate_orphan _5 && test_commit 5_A && test_commit 5_B &&\n+\tlib_test_check_graph _4 _5 <<-\\EOF\n+\t* 5_B\n+\t \\\n+\t  * 5_A\n+\t* 4_B\n+\t* 4_A\n+\tEOF\n+'\n+\n+test_expect_success 'indented visual root parent gets connected to its child with description' '\n+\tcheck_graph_with_description _4 _5 <<-\\EOF\n+\t* 5_B\n+\t| description\n+\t| second-line\n+\t \\\n+\t  * 5_A\n+\t    description\n+\t    second-line\n+\t* 4_B\n+\t| description\n+\t| second-line\n+\t* 4_A\n+\t  description\n+\t  second-line\n+\tEOF\n+'\n+\n+test_expect_success 'visual roots cascade and last root does not' '\n+\tcreate_orphan _7 && test_commit 7_A && test_commit 7_B &&\n+\tcreate_orphan _8 && test_commit 8_A &&\n+\tcreate_orphan _9 && test_commit 9_A &&\n+\tcreate_orphan _10 && test_commit 10_A &&\n+\tlib_test_check_graph _7 _8 _9 _10 <<-\\EOF\n+\t* 10_A\n+\t  * 9_A\n+\t    * 8_A\n+\t* 7_B\n+\t* 7_A\n+\tEOF\n+'\n+\n+test_expect_success 'last root does not cascade' '\n+\tlib_test_check_graph _8 _9 _10 <<-\\EOF\n+\t* 10_A\n+\t  * 9_A\n+\t* 8_A\n+\tEOF\n+'\n+\n+test_expect_success 'merge parents are roots between them but they do not indent' '\n+\tcreate_orphan _11 && test_commit 11_A &&\n+\tcreate_orphan _12 && test_commit 12_A &&\n+\tcreate_orphan _13 && test_commit 13_A &&\n+\tgit checkout _11 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _11 -p _12 -p _13 -m 11_octopus) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _11 <<-\\EOF\n+\t*-.   11_octopus\n+\t|\\ \\\n+\t| | * 13_A\n+\t| * 12_A\n+\t* 11_A\n+\tEOF\n+'\n+\n+# The last parent of a merge can be indented if nothing related to it needs to\n+# be rendered after, if it's another visual root, merge parent must not get\n+# indented but rather activate cascading.\n+test_expect_success 'merge then unrelated visual root and unrelated branch' '\n+\tcreate_orphan _16 && test_commit 16_A && test_commit 16_B &&\n+\tcreate_orphan _17 && test_commit 17_A &&\n+\tcreate_orphan _18 && test_commit 18_A &&\n+\tcreate_orphan _19 && test_commit 19_A &&\n+\tcreate_orphan _20 && test_commit 20_A &&\n+\tgit checkout _18 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _18 -p _19 -p _20 -m 18_octopus) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _18 _17 _16 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t* 18_A\n+\t  * 17_A\n+\t* 16_B\n+\t* 16_A\n+\tEOF\n+'\n+\n+# The last commit root does not get indented, if the next thing after the root\n+# merge parent is the last commit, indent the merge parent.\n+test_expect_success 'merge then unrelated root indents merge parent' '\n+\tlib_test_check_graph _18 _17 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t \\\n+\t  * 18_A\n+\t* 17_A\n+\tEOF\n+'\n+\n+test_expect_success 'merge then unrelated branch indents merge parent' '\n+\tlib_test_check_graph _18 _16 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t \\\n+\t  * 18_A\n+\t* 16_B\n+\t* 16_A\n+\tEOF\n+'\n+\n+test_expect_success 'two-parent merge of orphans' '\n+\tcreate_orphan _21 && test_commit 21_A &&\n+\tcreate_orphan _22 && test_commit 22_A &&\n+\tgit checkout _21 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _21 -p _22 -m 21_merge) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _21 <<-\\EOF\n+\t*   21_merge\n+\t|\\\n+\t| * 22_A\n+\t* 21_A\n+\tEOF\n+'\n+\n+test_expect_success 'commit with filtered parent becomes a visual root' '\n+\tcreate_orphan _23 &&\n+\techo test >other.txt &&\n+\tgit add other.txt &&\n+\tgit commit -m \"23_A\" &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"23_B\" &&\n+\tcreate_orphan _24 &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"24_A\" &&\n+\tlib_test_check_graph _23 _24 -- foo.txt <<-\\EOF\n+\t  * 23_B\n+\t* 24_A\n+\tEOF\n+'\n+\n+test_expect_success 'filtered parent cascading edge case' '\n+\tcreate_orphan _27 &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\ttest_tick &&\n+\tgit commit -m \"D (last)\" &&\n+\n+\tcreate_orphan _25 &&\n+\techo test >other.txt &&\n+\tgit add other.txt &&\n+\ttest_tick &&\n+\tgit commit -m \"C-filtered\" &&\n+\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\ttest_tick &&\n+\tgit commit -m \"B (child of filtered)\" &&\n+\n+\tcreate_orphan _26 &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\ttest_tick &&\n+\tgit commit -m \"A (visual root)\" &&\n+\n+\tlib_test_check_graph _25 _26 _27 -- foo.txt <<-\\EOF\n+\t* A (visual root)\n+\t  * B (child of filtered)\n+\t* D (last)\n+\tEOF\n+'\n+\n+test_expect_success 'multiple filtered parents in sequence' '\n+\tcreate_orphan _44 &&\n+\techo a >other.txt && git add other.txt && git commit -m \"44_F\" &&\n+\techo b >foo.txt && git add foo.txt && git commit -m \"44_C\" &&\n+\n+\tcreate_orphan _45 &&\n+\techo c >other.txt && git add other.txt && git commit -m \"45_F\" &&\n+\techo d >foo.txt && git add foo.txt && git commit -m \"45_C\" &&\n+\n+\tcreate_orphan _46 &&\n+\techo e >foo.txt && git add foo.txt && git commit -m \"46_A\" &&\n+\n+\tlib_test_check_graph _44 _45 _46 -- foo.txt <<-\\EOF\n+\t* 44_C\n+\t  * 45_C\n+\t* 46_A\n+\tEOF\n+'\n+\n+# These tests prove why there is no need to have indentation for boundary\n+# commits.\n+#\n+# Boundary commits rather than starting a column they 'inherit' the one of\n+# its child so there will always be an edge that connects it removing the\n+# ambiguity.\n+test_expect_success 'unrelated boundaries are not ambiguous' '\n+\tcreate_orphan _28 && test_commit 28_A && test_commit 28_B &&\n+\ttest_commit 28_C &&\n+\tcreate_orphan _29 && test_commit 29_A && test_commit 29_B &&\n+\tlib_test_check_graph --boundary 28_A.._28 29_A.._29 <<-\\EOF\n+\t* 29_B\n+\t| * 28_C\n+\t| * 28_B\n+\t| o 28_A\n+\to 29_A\n+\tEOF\n+'\n+\n+# Same structure as t6016\n+test_expect_success 'boundary commits big test' '\n+\t# 3 commits on branch _30\n+\tcreate_orphan _30 &&\n+\ttest_commit 30_A &&\n+\ttest_commit 30_B &&\n+\ttest_commit 30_C &&\n+\n+\t# 2 commits on branch _31, started from 30_A\n+\tgit checkout -b _31 30_A &&\n+\ttest_commit 31_A &&\n+\ttest_commit 31_B &&\n+\n+\t# 2 commits on branch _32, started from 30_B\n+\tgit checkout -b _32 30_B &&\n+\ttest_commit 32_A &&\n+\ttest_commit 32_B &&\n+\n+\t# Octopus merge _31 and _32 into -30\n+\tgit checkout _30 &&\n+\tgit merge _31 _32 -m 30_D &&\n+\tgit tag 30_D &&\n+\ttest_commit 30_E &&\n+\n+\t# More commits on _32, then merge _32 into _30\n+\tgit checkout _32 &&\n+\ttest_commit 32_C &&\n+\ttest_commit 32_D &&\n+\tgit checkout _30 &&\n+\tgit merge -s ours _32 -m 30_F &&\n+\tgit tag 30_F &&\n+\ttest_commit 30_G &&\n+\tlib_test_check_graph --boundary _30 _31 _32 ^32_C <<-\\EOF\n+\t* 30_G\n+\t*   30_F\n+\t|\\\n+\t| * 32_D\n+\t* | 30_E\n+\t| |\n+\t|  \\\n+\t*-. \\   30_D\n+\t|\\ \\ \\\n+\t| * | | 31_B\n+\t| * | | 31_A\n+\t* | | | 30_C\n+\to | | | 30_B\n+\t|/ / /\n+\to / / 30_A\n+\t / /\n+\t| o 32_C\n+\t|/\n+\to 32_B\n+\tEOF\n+'\n+\n+# Filter by --first-parent and then forcing the filtered parents to be shown.\n+test_expect_success '--first-parent flag with the filtered parents' '\n+\t(\n+\t\tunset_commit_graph &&\n+\t\tcreate_orphan _35 && test_commit 35_A && test_commit 35_B &&\n+\t\tcreate_orphan _36 && test_commit 36_A &&\n+\t\tcreate_orphan _37 && test_commit 37_A &&\n+\t\tgit checkout _35 &&\n+\t\tTREE=$(git write-tree) &&\n+\t\tMERGE=$(git commit-tree $TREE -p _35 -p _36 -p _37 -m 35_octopus) &&\n+\t\tgit reset --hard $MERGE &&\n+\t\tlib_test_check_graph --first-parent _35 _36 _37 <<-\\EOF\n+\t\t* 35_octopus\n+\t\t| * 37_A\n+\t\t|   * 36_A\n+\t\t* 35_B\n+\t\t* 35_A\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success '--first-parent with filtered parents but one has a child' '\n+\t(\n+\t\tunset_commit_graph &&\n+\t\tcreate_orphan _38 && test_commit 38_A && test_commit 38_B &&\n+\t\tcreate_orphan _39 && test_commit 39_A &&\n+\t\tcreate_orphan _40 && test_commit 40_A && test_commit 40_B &&\n+\t\tgit checkout _38 &&\n+\t\tTREE=$(git write-tree) &&\n+\t\tMERGE=$(git commit-tree $TREE -p _38 -p _39 -p _40 -m 38_octopus) &&\n+\t\tgit reset --hard $MERGE &&\n+\t\tlib_test_check_graph --first-parent _38 _39 _40 <<-\\EOF\n+\t\t* 38_octopus\n+\t\t| * 40_B\n+\t\t| * 40_A\n+\t\t|   * 39_A\n+\t\t* 38_B\n+\t\t* 38_A\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success '--first-parent with filtered parents but both have children' '\n+\t(\n+\t\tunset_commit_graph &&\n+\t\tcreate_orphan _41 && test_commit 41_A && test_commit 41_B &&\n+\t\tcreate_orphan _42 && test_commit 42_A && test_commit 42_B &&\n+\t\tcreate_orphan _43 && test_commit 43_A && test_commit 43_B &&\n+\t\tgit checkout _41 &&\n+\t\tTREE=$(git write-tree) &&\n+\t\tMERGE=$(git commit-tree $TREE -p _41 -p _42 -p _43 -m 41_octopus) &&\n+\t\tgit reset --hard $MERGE &&\n+\t\tlib_test_check_graph --first-parent _41 _42 _43 <<-\\EOF\n+\t\t* 41_octopus\n+\t\t| * 43_B\n+\t\t|  \\\n+\t\t|   * 43_A\n+\t\t| * 42_B\n+\t\t| * 42_A\n+\t\t* 41_B\n+\t\t* 41_A\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'two unrelated merges' '\n+\tcreate_orphan _50 && test_commit 50_A &&\n+\tgit checkout -b _51 &&\n+\ttest_commit 51_A && test_commit 51_B &&\n+\tgit checkout _50 &&\n+\tgit merge --no-ff _51 -m 50_B &&\n+\n+\tcreate_orphan _52 && test_commit 52_A &&\n+\tgit checkout -b _53 &&\n+\ttest_commit 53_A && test_commit 53_B &&\n+\tgit checkout _52 &&\n+\tgit merge --no-ff _53 -m 52_B &&\n+\n+\tlib_test_check_graph _52 _50 <<-\\EOF\n+\t*   52_B\n+\t|\\\n+\t| * 53_B\n+\t| * 53_A\n+\t|/\n+\t \\\n+\t  * 52_A\n+\t*   50_B\n+\t|\\\n+\t| * 51_B\n+\t| * 51_A\n+\t|/\n+\t* 50_A\n+\tEOF\n+'\n+\n+test_expect_success '--max-count treats the last visible commit as the last commit' '\n+\tlib_test_check_graph --max-count=2 _8 _9 _10 <<-\\EOF\n+\t  * 10_A\n+\t* 9_A\n+\tEOF\n+'\n+\n+test_expect_success '--max-count=1 shows a single root without indentation' '\n+\tlib_test_check_graph --max-count=1 _8 _9 _10 <<-\\EOF\n+\t* 10_A\n+\tEOF\n+'\n+\n+test_expect_success '--max-count-oldest indents visual roots' '\n+\tlib_test_check_graph --max-count-oldest=3 _8 _9 _10 <<-\\EOF\n+\t* 10_A\n+\t  * 9_A\n+\t* 8_A\n+\tEOF\n+'\n+\n+# when the graph commits are filtered with regex options like --author, the\n+# commit parents do not come NULL so it is needed to check if the parents are\n+# interesting.\n+test_expect_success '--author skipped parent makes a visual root' '\n+\tcreate_orphan _55 &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 55_A &&\n+\tcreate_orphan _54 &&\n+\ttest_tick &&\n+\tgit commit --allow-empty --author=\"Other <other@example.com>\" -m 54_A &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 54_B &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 54_C &&\n+\tlib_test_check_graph --author=\"A U Thor\" _54 _55 <<-\\EOF\n+\t* 54_C\n+\t \\\n+\t  * 54_B\n+\t* 55_A\n+\tEOF\n+'\n+\n+test_expect_success '--grep skipped parent makes a visual root' '\n+\tcreate_orphan _57 &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 57_keep_A &&\n+\tcreate_orphan _56 &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 56_skip &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 56_keep_A &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 56_keep_B &&\n+\tlib_test_check_graph --grep=keep _56 _57 <<-\\EOF\n+\t* 56_keep_B\n+\t \\\n+\t  * 56_keep_A\n+\t* 57_keep_A\n+\tEOF\n+'\n+\n+test_done\n\n-- \n2.54.0\n"},{"id":"547970","messageId":"20260713-ps-pre-commit-indent-v10-5-82ddab26bc96@gmail.com","threadId":"65419","inReplyTo":"20260713-ps-pre-commit-indent-v10-0-82ddab26bc96@gmail.com","subject":"[PATCH v10 5/7] graph: wrap cascading commits after 4 columns","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-13T10:44:40Z","receivedAt":"2026-07-13T10:45:04Z","isPatch":true,"body":"Currently the visual root commits in a graph cascade indefinitely until\na commit which is not a visual root or the last commit appears.\nOn filters like --author where one author might contribute mostly on\nsingle patches this can become a visual issue.\n\nMake the cascading wrap after 4 columns.\n\nThere are two possible cases of the wrap:\n\n1. No ambiguity:\n\n* A\n  * B\n    * C\n      * D\n* E\n  * F\n\n2. Ambiguous conflict:\n\nIf F happens to not be a visual root and E gets wrapped back to the\ninitial column then E and F would be vertically adjacent. The solution\nis to forcefully indent E one level:\n\n* A\n  * B\n    * C\n      * D\n  * E\n* F\n* F\n\nThe magic number 4 comes as the minimum number of columns to wrap where\nthe output shows clearly the commits are unrelated and doesn't cause too\nmuch \"pyramid\" effects\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c                          | 22 +++++++++++++++++++++-\n t/t4218-log-graph-indentation.sh | 29 +++++++++++++++++++++++++++++\n 2 files changed, 50 insertions(+), 1 deletion(-)\n\ndiff --git a/graph.c b/graph.c\nindex 087094189f..e3e206170c 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -1042,6 +1042,23 @@ void graph_update(struct git_graph *graph, struct commit *commit)\n \t\t */\n \t\tif (!graph->visual_root_depth && flags.is_next_visual_root)\n \t\t\tgraph->visual_root_cascade = 1;\n+\n+\t\t/*\n+\t\t * We wrap the cascading at a max of four columns at most, after\n+\t\t * that we wrap it back to the initial column.\n+\t\t *\n+\t\t * This could cause ambiguity in case of the next commit not\n+\t\t * being a visual root and be at the initial column after the\n+\t\t * first wrap.\n+\t\t *\n+\t\t * In case of being a non-visual-root the next, stop the\n+\t\t * cascading to get the commit indented.\n+\t\t */\n+\t\tif (!flags.is_next_visual_root &&\n+\t\t    graph->visual_root_depth &&\n+\t\t    !(graph->visual_root_depth % 4))\n+\t\t\tgraph->visual_root_cascade = 0;\n+\n \t\tgraph->visual_root_depth++;\n \t} else {\n \t\tgraph->visual_root_depth = 0;\n@@ -1328,8 +1345,11 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \t\t\t\t * Each visual column is 2 characters wide.\n \t\t\t\t * Omit the indentation for the first visual\n \t\t\t\t * root in cascade mode.\n+\t\t\t\t *\n+\t\t\t\t * Have a max of 4 columns when cascading, after\n+\t\t\t\t * that wrap it and repeat.\n \t\t\t\t */\n-\t\t\t\tint padding = (depth - graph->visual_root_cascade) * 2;\n+\t\t\t\tint padding = ((depth - graph->visual_root_cascade) % 4) * 2;\n \t\t\t\tgraph_line_addchars(line, ' ', padding);\n \t\t\t\tgraph->width += padding;\n \t\t\t}\ndiff --git a/t/t4218-log-graph-indentation.sh b/t/t4218-log-graph-indentation.sh\nindex 60c7d84af7..d4c850c0d4 100755\n--- a/t/t4218-log-graph-indentation.sh\n+++ b/t/t4218-log-graph-indentation.sh\n@@ -511,4 +511,33 @@ test_expect_success '--grep skipped parent makes a visual root' '\n \tEOF\n '\n \n+# The cascading wraps after 4 columns and when wraping (column % 4 == 0) if the\n+# next is a non visual-root, force indentation to avoid an ambiguous graph\n+# (commit 59_A is forcefully indented)\n+test_expect_success 'visual root cascading gets wrapped after 4 columns' '\n+\tcreate_orphan _58 && test_commit 58_A && test_commit 58_B &&\n+\tcreate_orphan _59 && test_commit 59_A &&\n+\tcreate_orphan _60 && test_commit 60_A &&\n+\tcreate_orphan _61 && test_commit 61_A &&\n+\tcreate_orphan _62 && test_commit 62_A &&\n+\tcreate_orphan _63 && test_commit 63_A &&\n+\tcreate_orphan _64 && test_commit 64_A &&\n+\tcreate_orphan _65 && test_commit 65_A &&\n+\tcreate_orphan _66 && test_commit 66_A &&\n+\tcreate_orphan _67 && test_commit 67_A &&\n+\tlib_test_check_graph _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\\EOF\n+\t* 67_A\n+\t  * 66_A\n+\t    * 65_A\n+\t      * 64_A\n+\t* 63_A\n+\t  * 62_A\n+\t    * 61_A\n+\t      * 60_A\n+\t  * 59_A\n+\t* 58_B\n+\t* 58_A\n+\tEOF\n+'\n+\n test_done\n\n-- \n2.54.0\n"},{"id":"547971","messageId":"20260713-ps-pre-commit-indent-v10-6-82ddab26bc96@gmail.com","threadId":"65419","inReplyTo":"20260713-ps-pre-commit-indent-v10-0-82ddab26bc96@gmail.com","subject":"[PATCH v10 6/7] graph: move config reading into graph_read_config()","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-13T10:44:41Z","receivedAt":"2026-07-13T10:45:05Z","isPatch":true,"body":"Move the repo_config_get_string() call out of graph_init() and into\ngraph_read_config(). This simplifies graph_init() and provides a\nfunction for future graph-related config opt.\n\nThis commit is a preparatory commit for a subsequent one.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c | 13 +++++++++----\n 1 file changed, 9 insertions(+), 4 deletions(-)\n\ndiff --git a/graph.c b/graph.c\nindex e3e206170c..c14be934a0 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -417,13 +417,11 @@ void graph_setup_line_prefix(struct diff_options *diffopt)\n \t\tdiffopt->output_prefix = diff_output_prefix_callback;\n }\n \n-struct git_graph *graph_init(struct rev_info *opt)\n+static void graph_read_config(struct rev_info *revs)\n {\n-\tstruct git_graph *graph = xmalloc(sizeof(struct git_graph));\n-\n \tif (!column_colors) {\n \t\tchar *string;\n-\t\tif (repo_config_get_string(opt->repo, \"log.graphcolors\", &string)) {\n+\t\tif (repo_config_get_string(revs->repo, \"log.graphcolors\", &string)) {\n \t\t\t/* not configured -- use default */\n \t\t\tgraph_set_column_colors(column_colors_ansi,\n \t\t\t\t\t\tcolumn_colors_ansi_max);\n@@ -437,6 +435,13 @@ struct git_graph *graph_init(struct rev_info *opt)\n \t\t\t\t\t\tcustom_colors.nr - 1);\n \t\t}\n \t}\n+}\n+\n+struct git_graph *graph_init(struct rev_info *opt)\n+{\n+\tstruct git_graph *graph = xmalloc(sizeof(struct git_graph));\n+\n+\tgraph_read_config(opt);\n \n \tgraph->commit = NULL;\n \tgraph->revs = opt;\n\n-- \n2.54.0\n"},{"id":"547972","messageId":"20260713-ps-pre-commit-indent-v10-7-82ddab26bc96@gmail.com","threadId":"65419","inReplyTo":"20260713-ps-pre-commit-indent-v10-0-82ddab26bc96@gmail.com","subject":"[PATCH v10 7/7] graph: add --[no-]graph-indent and log.graphIndent","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-13T10:44:42Z","receivedAt":"2026-07-13T10:45:06Z","isPatch":true,"body":"Some users may prefer to not have graph indentation.\n\nAdd \"log.graphIndent\" config variable to graph_read_config() to read the\ndefault preference. By default is graph indentation is true.\n\nAdd --graph-indent and --no-graph-indent options to overwrite the\ndefault preference.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n Documentation/config/log.adoc       |  4 +++\n Documentation/rev-list-options.adoc |  8 ++++++\n graph.c                             | 10 +++++--\n revision.c                          |  9 +++++++\n revision.h                          |  2 ++\n t/t4218-log-graph-indentation.sh    | 52 +++++++++++++++++++++++++++++++++++++\n 6 files changed, 83 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/config/log.adoc b/Documentation/config/log.adoc\nindex 757a7be196..f7dfce69b5 100644\n--- a/Documentation/config/log.adoc\n+++ b/Documentation/config/log.adoc\n@@ -59,6 +59,10 @@ This is the same as the `--decorate` option of the `git log`.\n \tA list of colors, separated by commas, that can be used to draw\n \thistory lines in `git log --graph`.\n \n+`log.graphIndent`::\n+\tIf `true`, indent visual roots when rendering the graphs with `--graph`.\n+\tSet true by default. It can be overriden with `--[no-]graph-indent`.\n+\n `log.showRoot`::\n \tIf true, the initial commit will be shown as a big creation event.\n \tThis is equivalent to a diff against an empty tree.\ndiff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc\nindex eaee6ee839..af74f10bb4 100644\n--- a/Documentation/rev-list-options.adoc\n+++ b/Documentation/rev-list-options.adoc\n@@ -1269,6 +1269,14 @@ This implies the `--topo-order` option by default, but the\n \tBy default it is set to 0 (no limit), zero and negative values\n \tare ignored and treated as no limit.\n \n+`--no-graph-indent`::\n+`--graph-indent`::\n+\tWhen used with `--graph`, indent visual roots (commits with no parents\n+\tor whose parents are not shown) to differentiate them from commits that\n+\tare vertically adjacent but unrelated. Enabled by default. Use\n+\t`--no-graph-indent` to disable or set `graph.indent` to set a deafault\n+\tpreference.\n+\n ifdef::git-rev-list[]\n `--count`::\n \tPrint a number stating how many commits would have been\ndiff --git a/graph.c b/graph.c\nindex c14be934a0..28bef1b88f 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -419,6 +419,8 @@ void graph_setup_line_prefix(struct diff_options *diffopt)\n \n static void graph_read_config(struct rev_info *revs)\n {\n+\tint val;\n+\n \tif (!column_colors) {\n \t\tchar *string;\n \t\tif (repo_config_get_string(revs->repo, \"log.graphcolors\", &string)) {\n@@ -435,6 +437,9 @@ static void graph_read_config(struct rev_info *revs)\n \t\t\t\t\t\tcustom_colors.nr - 1);\n \t\t}\n \t}\n+\n+\tif (!repo_config_get_bool(revs->repo, \"log.graphIndent\", &val))\n+\t\trevs->no_graph_indent = !val;\n }\n \n struct git_graph *graph_init(struct rev_info *opt)\n@@ -999,7 +1004,8 @@ static void graph_peek_next_visible(struct git_graph *graph,\n static int graph_needs_pre_root_line(struct git_graph *graph)\n {\n \treturn graph->commit_in_columns && graph->is_visual_root &&\n-\t       graph->num_columns > 0 && !graph->visual_root_cascade;\n+\t       graph->num_columns > 0 && !graph->visual_root_cascade &&\n+\t       !graph->revs->no_graph_indent;\n }\n \n void graph_update(struct git_graph *graph, struct commit *commit)\n@@ -1344,7 +1350,7 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \n \t\tif (col_commit == graph->commit) {\n \t\t\tseen_this = 1;\n-\t\t\tif (graph->is_visual_root) {\n+\t\t\tif (graph->is_visual_root && !graph->revs->no_graph_indent) {\n \t\t\t\tint depth = graph->visual_root_depth;\n \t\t\t\t/*\n \t\t\t\t * Each visual column is 2 characters wide.\ndiff --git a/revision.c b/revision.c\nindex 258c3cf782..215cf11071 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -2627,6 +2627,12 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg\n \t\trevs->graph = NULL;\n \t} else if (skip_prefix(arg, \"--graph-lane-limit=\", &optarg)) {\n \t\trevs->graph_max_lanes = parse_count(optarg);\n+\t} else if (!strcmp(arg, \"--graph-indent\")) {\n+\t\trevs->no_graph_indent = 0;\n+\t\trevs->graph_indent_set = 1;\n+\t} else if (!strcmp(arg, \"--no-graph-indent\")) {\n+\t\trevs->no_graph_indent = 1;\n+\t\trevs->graph_indent_set = 1;\n \t} else if (!strcmp(arg, \"--encode-email-headers\")) {\n \t\trevs->encode_email_headers = 1;\n \t} else if (!strcmp(arg, \"--no-encode-email-headers\")) {\n@@ -3201,6 +3207,9 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s\n \tif (revs->graph_max_lanes > 0 && !revs->graph)\n \t\tdie(_(\"the option '%s' requires '%s'\"), \"--graph-lane-limit\", \"--graph\");\n \n+\tif (revs->graph_indent_set > 0 && !revs->graph)\n+\t\tdie(_(\"the option '%s' requires '%s'\"), \"--[no-]graph-indent\", \"--graph\");\n+\n \tif (!revs->reflog_info && revs->grep_filter.use_reflog_filter)\n \t\tdie(_(\"the option '%s' requires '%s'\"), \"--grep-reflog\", \"--walk-reflogs\");\n \ndiff --git a/revision.h b/revision.h\nindex 569b3fa1cb..49e1380b80 100644\n--- a/revision.h\n+++ b/revision.h\n@@ -314,6 +314,8 @@ struct rev_info {\n \t/* Display history graph */\n \tstruct git_graph *graph;\n \tint graph_max_lanes;\n+\tint no_graph_indent;\n+\tunsigned int graph_indent_set;\n \n \t/* special limits */\n \tint skip_count;\ndiff --git a/t/t4218-log-graph-indentation.sh b/t/t4218-log-graph-indentation.sh\nindex d4c850c0d4..b69730e7ba 100755\n--- a/t/t4218-log-graph-indentation.sh\n+++ b/t/t4218-log-graph-indentation.sh\n@@ -540,4 +540,56 @@ test_expect_success 'visual root cascading gets wrapped after 4 columns' '\n \tEOF\n '\n \n+test_expect_success '--no-graph-indent disables indentation' '\n+\tlib_test_check_graph --no-graph-indent _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\\EOF\n+\t* 67_A\n+\t* 66_A\n+\t* 65_A\n+\t* 64_A\n+\t* 63_A\n+\t* 62_A\n+\t* 61_A\n+\t* 60_A\n+\t* 59_A\n+\t* 58_B\n+\t* 58_A\n+\tEOF\n+'\n+\n+test_expect_success 'log.graphIndent config disables indentation' '\n+\ttest_config log.graphIndent false &&\n+\tlib_test_check_graph _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\\EOF\n+\t* 67_A\n+\t* 66_A\n+\t* 65_A\n+\t* 64_A\n+\t* 63_A\n+\t* 62_A\n+\t* 61_A\n+\t* 60_A\n+\t* 59_A\n+\t* 58_B\n+\t* 58_A\n+\tEOF\n+'\n+\n+test_expect_success '--graph-indent forces indentation when graph.indent is unset' '\n+\ttest_config log.graphIndent false &&\n+\tlib_test_check_graph --graph-indent _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\\EOF\n+\t* 67_A\n+\t  * 66_A\n+\t    * 65_A\n+\t      * 64_A\n+\t* 63_A\n+\t  * 62_A\n+\t    * 61_A\n+\t      * 60_A\n+\t  * 59_A\n+\t* 58_B\n+\t* 58_A\n+\tEOF\n+'\n+\n+# graph.indent true and no --option is the default state.\n+\n test_done\n\n-- \n2.54.0\n"},{"id":"547996","messageId":"alTuevrGiK3bwh31@exploit","threadId":"65419","inReplyTo":"20260713-ps-pre-commit-indent-v10-7-82ddab26bc96@gmail.com","subject":"Re: [PATCH v10 7/7] graph: add --[no-]graph-indent and log.graphIndent","fromName":"Mirko Faina","fromEmail":"mroik@delayed.space","sentAt":"2026-07-13T14:06:50Z","receivedAt":"2026-07-13T14:07:00Z","isPatch":true,"body":"On Mon, Jul 13, 2026 at 12:44:42PM +0200, Pablo Sabater wrote:\n> Some users may prefer to not have graph indentation.\n> \n> Add \"log.graphIndent\" config variable to graph_read_config() to read the\n> default preference. By default is graph indentation is true.\n> \n> Add --graph-indent and --no-graph-indent options to overwrite the\n> default preference.\n> \n> Signed-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n> ---\n>  Documentation/config/log.adoc       |  4 +++\n>  Documentation/rev-list-options.adoc |  8 ++++++\n>  graph.c                             | 10 +++++--\n>  revision.c                          |  9 +++++++\n>  revision.h                          |  2 ++\n>  t/t4218-log-graph-indentation.sh    | 52 +++++++++++++++++++++++++++++++++++++\n>  6 files changed, 83 insertions(+), 2 deletions(-)\n\n[snip]\n\n> diff --git a/revision.h b/revision.h\n> index 569b3fa1cb..49e1380b80 100644\n> --- a/revision.h\n> +++ b/revision.h\n> @@ -314,6 +314,8 @@ struct rev_info {\n>  \t/* Display history graph */\n>  \tstruct git_graph *graph;\n>  \tint graph_max_lanes;\n> +\tint no_graph_indent;\n> +\tunsigned int graph_indent_set;\n\nThese are both boolean values and could be set to be 1 bit wide.\n\nOther than that LGTM.\n\nThank you for the changes.\n"},{"id":"548016","messageId":"DJXKTG2TLTUO.2XCFIBA3ZAN2W@gmail.com","threadId":"65419","inReplyTo":"alTuevrGiK3bwh31@exploit","subject":"Re: [PATCH v10 7/7] graph: add --[no-]graph-indent and log.graphIndent","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-13T16:16:08Z","receivedAt":"2026-07-13T16:16:15Z","isPatch":true,"body":"On Mon Jul 13, 2026 at 4:06 PM CEST, Mirko Faina wrote:\n> On Mon, Jul 13, 2026 at 12:44:42PM +0200, Pablo Sabater wrote:\n>> Some users may prefer to not have graph indentation.\n>>\n>> Add \"log.graphIndent\" config variable to graph_read_config() to read the\n>> default preference. By default is graph indentation is true.\n>>\n>> Add --graph-indent and --no-graph-indent options to overwrite the\n>> default preference.\n>>\n>> Signed-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n>> ---\n>>  Documentation/config/log.adoc       |  4 +++\n>>  Documentation/rev-list-options.adoc |  8 ++++++\n>>  graph.c                             | 10 +++++--\n>>  revision.c                          |  9 +++++++\n>>  revision.h                          |  2 ++\n>>  t/t4218-log-graph-indentation.sh    | 52 +++++++++++++++++++++++++++++++++++++\n>>  6 files changed, 83 insertions(+), 2 deletions(-)\n>\n> [snip]\n>\n>> diff --git a/revision.h b/revision.h\n>> index 569b3fa1cb..49e1380b80 100644\n>> --- a/revision.h\n>> +++ b/revision.h\n>> @@ -314,6 +314,8 @@ struct rev_info {\n>>  \t/* Display history graph */\n>>  \tstruct git_graph *graph;\n>>  \tint graph_max_lanes;\n>> +\tint no_graph_indent;\n>> +\tunsigned int graph_indent_set;\n>\n> These are both boolean values and could be set to be 1 bit wide.\n\nTrue, I'll change it.\n\n>\n> Other than that LGTM.\n>\n> Thank you for the changes.\n\nThanks,\nPablo\n"},{"id":"548025","messageId":"20260713-ps-pre-commit-indent-v11-0-dcb65bc4ba99@gmail.com","threadId":"65419","inReplyTo":"20260713-ps-pre-commit-indent-v10-0-82ddab26bc96@gmail.com","subject":"[PATCH v11 0/7] graph: indent visual roots in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-13T16:43:57Z","receivedAt":"2026-07-13T16:44:09Z","isPatch":true,"body":"When rendering a graph, if the history contains multiple \"visual roots\",\nactual roots or commits that look like roots (i.e. have their parents\nfiltered out) can end up being vertically adjacent to unrelated commits,\nfalsely appearing to be related.\n\nA fix for this issue was already attempted [1] a while ago.\n\nThis series adds indentation to the visual root commits, so they cannot be\nvertically adjacent anymore making it easier to identify them.\n\nBefore indentation:\n\n\t* A\n\t* B1\n\t* B2\n\t* C1\n\t* C2\n\nAfter indentation:\n\n\t  * A\n\t* B1\n\t \\\n\t  * B2\n\t* C1\n\t* C2\n\nIndents the visual root commits that have still commits to show after\nthem, and if they have children it connects them with an edge at a new\nrow.\n\nIf there are multiple visual roots adjacent in history, the indentation\nstarts with the second one, avoiding redundant indentation of the first\none and cascades after the second.\n\n\t* A\n\t  * B\n\t    * C\n\t      * D\n\t* E\n\t  * F\n\t    * G\n\t      * H\n\t  * I\n\t* J1\n\t* J2\n\nThe indentation wraps after cascading columns and when wrapping back to\nthe initial column if the next commit is a non-visual-root commit, force\nthe indentation one extra level.\n\nSeries explanation:\n\n- Cleanup to bring a common function from t4215 and t6016 that will be\n  used in t4218.\n\n- Logic extraction of the chose of from where the commit source comes\n  from.\n\n- Add a buffer for lookahead purposes.\n\n- Principal commit. Implement the logic to get the visual roots\n  indented.\n\n- Make visual root cascading wrap after 4 columns\n\n- Add --[no-]graph-indent and log.graphIndent options.\n\nGitHub CI: https://github.com/pabloosabaterr/git/actions/runs/29266560903\n\n[1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n\nV9 DIFF:\n\n- Changed boolean variables to be bit fields.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\nPablo Sabater (7):\n      lib-log-graph: move check_graph function\n      revision: add next_commit_to_show()\n      graph: add a 2 commit buffer for lookahead\n      graph: indent visual root in graph\n      graph: wrap cascading commits after 4 columns\n      graph: move config reading into graph_read_config()\n      graph: add --[no-]graph-indent and log.graphIndent\n\n Documentation/config/log.adoc              |   4 +\n Documentation/rev-list-options.adoc        |   8 +\n graph.c                                    | 332 +++++++++++++++-\n graph.h                                    |  17 +\n revision.c                                 |  57 ++-\n revision.h                                 |   2 +\n t/lib-log-graph.sh                         |   5 +\n t/meson.build                              |   1 +\n t/t4215-log-skewed-merges.sh               |  33 +-\n t/t4218-log-graph-indentation.sh           | 595 +++++++++++++++++++++++++++++\n t/t6016-rev-list-graph-simplify-history.sh |  25 +-\n 11 files changed, 1031 insertions(+), 48 deletions(-)\n\nRange-diff versus v10:\n\n1:  9541b410b7 = 1:  dd0bb0d215 lib-log-graph: move check_graph function\n2:  4f8fb2cc1d = 2:  07e239533d revision: add next_commit_to_show()\n3:  b50574bbe1 = 3:  4d71f674a1 graph: add a 2 commit buffer for lookahead\n4:  fc3a8253fd = 4:  48ad2562f0 graph: indent visual root in graph\n5:  204aae5061 = 5:  45be69d11b graph: wrap cascading commits after 4 columns\n6:  1b42ed86a1 = 6:  8ce53ae21b graph: move config reading into graph_read_config()\n7:  737331b68d ! 7:  c1fa81022e graph: add --[no-]graph-indent and log.graphIndent\n    @@ revision.h: struct rev_info {\n      \t/* Display history graph */\n      \tstruct git_graph *graph;\n      \tint graph_max_lanes;\n    -+\tint no_graph_indent;\n    -+\tunsigned int graph_indent_set;\n    ++\tunsigned int no_graph_indent:1;\n    ++\tunsigned int graph_indent_set:1;\n\n      \t/* special limits */\n      \tint skip_count;\n\n---\nbase-commit: f60db8d575adb79761d363e026fb49bddf330c73\n"},{"id":"548026","messageId":"20260713-ps-pre-commit-indent-v11-1-dcb65bc4ba99@gmail.com","threadId":"65419","inReplyTo":"20260713-ps-pre-commit-indent-v11-0-dcb65bc4ba99@gmail.com","subject":"[PATCH v11 1/7] lib-log-graph: move check_graph function","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-13T16:43:58Z","receivedAt":"2026-07-13T16:44:10Z","isPatch":true,"body":"check_graph is a function shared in the test files t4215 and t6016 used\nto format the output graph, but instead of being in a file called by\nboth test, the function code is repeated in each file.\n\nMove check_graph to lib-log-graph.sh file which both tests already\nimport graph functions from, renaming it to lib_test_check_graph.\n\nThis function is needed for the following commit which includes graph\ntests in a new file and requires check_graph.\n\nMentored-by: Karthik Nayak <karthik.188@gmail.com>\nMentored-by: Chandra Pratap <chandrapratap3519@gmail.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n t/lib-log-graph.sh                         |  5 +++++\n t/t4215-log-skewed-merges.sh               | 33 +++++++++++++-----------------\n t/t6016-rev-list-graph-simplify-history.sh | 25 +++++++++-------------\n 3 files changed, 29 insertions(+), 34 deletions(-)\n\ndiff --git a/t/lib-log-graph.sh b/t/lib-log-graph.sh\nindex bf952ef920..1eae8f60c2 100644\n--- a/t/lib-log-graph.sh\n+++ b/t/lib-log-graph.sh\n@@ -26,3 +26,8 @@ lib_test_cmp_colored_graph () {\n \ttest_decode_color <output.colors.raw | sed \"s/ *\\$//\" >output.colors &&\n \ttest_cmp expect.colors output.colors\n }\n+\n+lib_test_check_graph () {\n+\tcat >expect &&\n+\tlib_test_cmp_graph --format=%s \"$@\"\n+}\ndiff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh\nindex 1612f05f1b..eebab71039 100755\n--- a/t/t4215-log-skewed-merges.sh\n+++ b/t/t4215-log-skewed-merges.sh\n@@ -5,11 +5,6 @@ test_description='git log --graph of skewed merges'\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-log-graph.sh\n \n-check_graph () {\n-\tcat >expect &&\n-\tlib_test_cmp_graph --format=%s \"$@\"\n-}\n-\n test_expect_success 'log --graph with merge fusing with its left and right neighbors' '\n \tgit checkout --orphan _p &&\n \ttest_commit A &&\n@@ -21,7 +16,7 @@ test_expect_success 'log --graph with merge fusing with its left and right neigh\n \tgit checkout _p && git merge --no-ff _r -m G &&\n \tgit checkout @^^ && git merge --no-ff _p -m H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   H\n \t|\\\n \t| *   G\n@@ -49,7 +44,7 @@ test_expect_success 'log --graph with left-skewed merge' '\n \tgit checkout 0_p && git merge --no-ff 0_s -m 0_G &&\n \tgit checkout @^ && git merge --no-ff 0_q 0_r 0_t 0_p -m 0_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*-----.   0_H\n \t|\\ \\ \\ \\\n \t| | | | * 0_G\n@@ -83,7 +78,7 @@ test_expect_success 'log --graph with nested left-skewed merge' '\n \tgit checkout 1_p && git merge --no-ff 1_r -m 1_G &&\n \tgit checkout @^^ && git merge --no-ff 1_p -m 1_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   1_H\n \t|\\\n \t| *   1_G\n@@ -115,7 +110,7 @@ test_expect_success 'log --graph with nested left-skewed merge following normal\n \tgit checkout -b 2_s @^^ && git merge --no-ff 2_q -m 2_J &&\n \tgit checkout 2_p && git merge --no-ff 2_s -m 2_K &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   2_K\n \t|\\\n \t| *   2_J\n@@ -151,7 +146,7 @@ test_expect_success 'log --graph with nested right-skewed merge following left-s\n \tgit checkout 3_p && git merge --no-ff 3_r -m 3_H &&\n \tgit checkout @^^ && git merge --no-ff 3_p -m 3_J &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   3_J\n \t|\\\n \t| *   3_H\n@@ -182,7 +177,7 @@ test_expect_success 'log --graph with right-skewed merge following a left-skewed\n \tgit merge --no-ff 4_p -m 4_G &&\n \tgit checkout @^^ && git merge --no-ff 4_s -m 4_H &&\n \n-\tcheck_graph --date-order <<-\\EOF\n+\tlib_test_check_graph --date-order <<-\\EOF\n \t*   4_H\n \t|\\\n \t| *   4_G\n@@ -218,7 +213,7 @@ test_expect_success 'log --graph with octopus merge with column joining its penu\n \tgit checkout 5_r &&\n \tgit merge --no-ff 5_s -m 5_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   5_H\n \t|\\\n \t| *-.   5_G\n@@ -257,7 +252,7 @@ test_expect_success 'log --graph with multiple tips' '\n \tgit checkout 6_1 &&\n \tgit merge --no-ff 6_2 -m 6_I &&\n \n-\tcheck_graph 6_1 6_3 6_5 <<-\\EOF\n+\tlib_test_check_graph 6_1 6_3 6_5 <<-\\EOF\n \t*   6_I\n \t|\\\n \t| | *   6_H\n@@ -334,7 +329,7 @@ test_expect_success 'log --graph with multiple tips' '\n \tgit checkout -b M_7 7_1 &&\n \tgit merge --no-ff 7_2 7_3 -m 7_M4 &&\n \n-\tcheck_graph M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -371,7 +366,7 @@ test_expect_success 'log --graph with multiple tips' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' '\n-\tcheck_graph --graph-lane-limit=2 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=2 M_7 <<-\\EOF\n \t*-.   7_M4\n \t|\\ \\\n \t| | * 7_G\n@@ -388,7 +383,7 @@ test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=1 truncate mid octopus merge' '\n-\tcheck_graph --graph-lane-limit=1 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=1 M_7 <<-\\EOF\n \t*-~  7_M4\n \t|\\~\n \t| ~ 7_G\n@@ -405,7 +400,7 @@ test_expect_success 'log --graph --graph-lane-limit=1 truncate mid octopus merge\n '\n \n test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' '\n-\tcheck_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -441,7 +436,7 @@ test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=6 check if it only shows first of 3 parent merge' '\n-\tcheck_graph --graph-lane-limit=6 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=6 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -478,7 +473,7 @@ test_expect_success 'log --graph --graph-lane-limit=6 check if it only shows fir\n '\n \n test_expect_success 'log --graph --graph-lane-limit=7 check if it shows all 3 parent merge' '\n-\tcheck_graph --graph-lane-limit=7 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=7 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\ndiff --git a/t/t6016-rev-list-graph-simplify-history.sh b/t/t6016-rev-list-graph-simplify-history.sh\nindex 54b0a6f5f8..e0d9c3c1ac 100755\n--- a/t/t6016-rev-list-graph-simplify-history.sh\n+++ b/t/t6016-rev-list-graph-simplify-history.sh\n@@ -13,11 +13,6 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-log-graph.sh\n \n-check_graph () {\n-\tcat >expect &&\n-\tlib_test_cmp_graph --format=%s \"$@\"\n-}\n-\n test_expect_success 'set up rev-list --graph test' '\n \t# 3 commits on branch A\n \ttest_commit A1 foo.txt &&\n@@ -54,7 +49,7 @@ test_expect_success 'set up rev-list --graph test' '\n '\n \n test_expect_success '--graph --all' '\n-\tcheck_graph --all <<-\\EOF\n+\tlib_test_check_graph --all <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -82,7 +77,7 @@ test_expect_success '--graph --all' '\n # that undecorated merges are interesting, even with --simplify-by-decoration\n test_expect_success '--graph --simplify-by-decoration' '\n \tgit tag -d A4 &&\n-\tcheck_graph --all --simplify-by-decoration <<-\\EOF\n+\tlib_test_check_graph --all --simplify-by-decoration <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -114,7 +109,7 @@ test_expect_success 'setup: get rid of decorations on B' '\n \n # Graph with branch B simplified away\n test_expect_success '--graph --simplify-by-decoration prune branch B' '\n-\tcheck_graph --simplify-by-decoration --all <<-\\EOF\n+\tlib_test_check_graph --simplify-by-decoration --all <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -133,7 +128,7 @@ test_expect_success '--graph --simplify-by-decoration prune branch B' '\n '\n \n test_expect_success '--graph --full-history -- bar.txt' '\n-\tcheck_graph --full-history --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --full-history --all -- bar.txt <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -148,7 +143,7 @@ test_expect_success '--graph --full-history -- bar.txt' '\n '\n \n test_expect_success '--graph --full-history --simplify-merges -- bar.txt' '\n-\tcheck_graph --full-history --simplify-merges --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --full-history --simplify-merges --all -- bar.txt <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -161,7 +156,7 @@ test_expect_success '--graph --full-history --simplify-merges -- bar.txt' '\n '\n \n test_expect_success '--graph -- bar.txt' '\n-\tcheck_graph --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --all -- bar.txt <<-\\EOF\n \t* A7\n \t* A5\n \t* A3\n@@ -172,7 +167,7 @@ test_expect_success '--graph -- bar.txt' '\n '\n \n test_expect_success '--graph --sparse -- bar.txt' '\n-\tcheck_graph --sparse --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --sparse --all -- bar.txt <<-\\EOF\n \t* A7\n \t* A6\n \t* A5\n@@ -189,7 +184,7 @@ test_expect_success '--graph --sparse -- bar.txt' '\n '\n \n test_expect_success '--graph ^C4' '\n-\tcheck_graph --all ^C4 <<-\\EOF\n+\tlib_test_check_graph --all ^C4 <<-\\EOF\n \t* A7\n \t* A6\n \t* A5\n@@ -202,7 +197,7 @@ test_expect_success '--graph ^C4' '\n '\n \n test_expect_success '--graph ^C3' '\n-\tcheck_graph --all ^C3 <<-\\EOF\n+\tlib_test_check_graph --all ^C3 <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -220,7 +215,7 @@ test_expect_success '--graph ^C3' '\n # that important, but this test depends on it.  If the ordering ever changes\n # in the code, we'll need to update this test.\n test_expect_success '--graph --boundary ^C3' '\n-\tcheck_graph --boundary --all ^C3 <<-\\EOF\n+\tlib_test_check_graph --boundary --all ^C3 <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n\n-- \n2.54.0\n"},{"id":"548027","messageId":"20260713-ps-pre-commit-indent-v11-2-dcb65bc4ba99@gmail.com","threadId":"65419","inReplyTo":"20260713-ps-pre-commit-indent-v11-0-dcb65bc4ba99@gmail.com","subject":"[PATCH v11 2/7] revision: add next_commit_to_show()","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-13T16:43:59Z","receivedAt":"2026-07-13T16:44:11Z","isPatch":true,"body":"get_revision() gets its commits from two sources depending on the mode:\n\n1. Normally it gets the commits from get_revision_internal().\n\n2. --max-count-oldest which was introduced at bb4ce23284 (revision.c:\n   implement --max-count-oldest, 2026-05-19) gets the commits by popping\n   from a saved list at revs->commits marking SHOWN and CHILD_SHOWN on\n   each popped commit.\n\nExtract the choice logic into a helper, next_commit_to_show(), which\nreturns the next commit regardless of the source it comes from.\n\nThis has no change in behavior. The helper is needed in a subsequent\ncommit that pre-fetches two commits into a buffer for lookahead purposes\nand needs to pre-fetch from the same source.\n\nThe --reverse branch keeps its own pop loop. Using the helper for\n--reverse would additionally set SHOWN and CHILD_SHOWN which is not\ndesired and a behavior change.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n revision.c | 36 ++++++++++++++++++++++++------------\n 1 file changed, 24 insertions(+), 12 deletions(-)\n\ndiff --git a/revision.c b/revision.c\nindex 0c95edef59..288935943f 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -4658,12 +4658,34 @@ static void retrieve_oldest_commits(struct rev_info *revs,\n \t\tcommit_list_insert(c, queue);\n }\n \n+/*\n+ * Returns the next commit that will be shown, regardless of whether it comes\n+ * directly from the revision walk or from the list saved by the staged output\n+ * of --max-count-oldest.\n+ */\n+static struct commit *next_commit_to_show(struct rev_info *revs)\n+{\n+\tstruct commit *c;\n+\tstruct commit_list *p;\n+\n+\tif (!revs->max_count_stage)\n+\t\treturn get_revision_internal(revs);\n+\n+\tc = pop_commit(&revs->commits);\n+\tif (c) {\n+\t\tc->object.flags |= SHOWN;\n+\t\tif (!(c->object.flags & BOUNDARY))\n+\t\t\tfor (p = c->parents; p; p = p->next)\n+\t\t\t\tp->item->object.flags |= CHILD_SHOWN;\n+\t}\n+\treturn c;\n+}\n+\n struct commit *get_revision(struct rev_info *revs)\n {\n \tstruct commit *c;\n \tstruct commit_list *reversed;\n \tstruct commit_list *queue = NULL;\n-\tstruct commit_list *p;\n \n \tif (revs->max_count_type == 1 && !revs->max_count_stage) {\n \t\tretrieve_oldest_commits(revs, &queue);\n@@ -4693,17 +4715,7 @@ struct commit *get_revision(struct rev_info *revs)\n \t\treturn c;\n \t}\n \n-\tif (revs->max_count_stage) {\n-\t\tc = pop_commit(&revs->commits);\n-\t\tif (c) {\n-\t\t\tc->object.flags |= SHOWN;\n-\t\t\tif (!(c->object.flags & BOUNDARY))\n-\t\t\t\tfor (p = c->parents; p; p = p->next)\n-\t\t\t\t\tp->item->object.flags |= CHILD_SHOWN;\n-\t\t}\n-\t} else {\n-\t\tc = get_revision_internal(revs);\n-\t}\n+\tc = next_commit_to_show(revs);\n \n \tif (c && revs->graph)\n \t\tgraph_update(revs->graph, c);\n\n-- \n2.54.0\n"},{"id":"548028","messageId":"20260713-ps-pre-commit-indent-v11-3-dcb65bc4ba99@gmail.com","threadId":"65419","inReplyTo":"20260713-ps-pre-commit-indent-v11-0-dcb65bc4ba99@gmail.com","subject":"[PATCH v11 3/7] graph: add a 2 commit buffer for lookahead","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-13T16:44:00Z","receivedAt":"2026-07-13T16:44:12Z","isPatch":true,"body":"In a subsequent commit the graph renderer needs to know if the next\ncommit is a visual root or if it is the last commit to be shown. This\nrequires peeking 2 commits ahead.\n\nCommits are pre-fetched in get_revision() through next_commit_to_show()\nwhere they are also marked as SHOWN, regardless the source they come\nfrom.\n\nUpdate graph_is_interesting() so it considers commits inside the\nlookahead buffer as interesting as well.\n\nHelped-by: Kristofer Karlsson <krka@spotify.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c    | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++\n graph.h    | 17 +++++++++++++++++\n revision.c | 18 ++++++++++++++++--\n 3 files changed, 84 insertions(+), 2 deletions(-)\n\ndiff --git a/graph.c b/graph.c\nindex 842282685f..89ebcf7540 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -315,6 +315,14 @@ struct git_graph {\n \t * diff_output_prefix_callback().\n \t */\n \tstruct strbuf prefix_buf;\n+\n+\t/*\n+\t * Lookahead buffer: up to 2 pre-fetched commits that will be shown.\n+\t * Populated by get_revision() so graph_peek_next_visible() can use\n+\t * actual walk results instead of peeking at rev_info internals.\n+\t */\n+\tstruct commit *lookahead[2];\n+\tint lookahead_nr;\n };\n \n static inline int graph_needs_truncation(struct git_graph *graph, int lane)\n@@ -388,6 +396,9 @@ struct git_graph *graph_init(struct rev_info *opt)\n \tgraph->num_columns = 0;\n \tgraph->num_new_columns = 0;\n \tgraph->mapping_size = 0;\n+\tgraph->lookahead[0] = NULL;\n+\tgraph->lookahead[1] = NULL;\n+\tgraph->lookahead_nr = 0;\n \t/*\n \t * Start the column color at the maximum value, since we'll\n \t * always increment it for the first commit we output.\n@@ -456,6 +467,15 @@ static void graph_ensure_capacity(struct git_graph *graph, int num_columns)\n  */\n static int graph_is_interesting(struct git_graph *graph, struct commit *commit)\n {\n+\t/*\n+\t * Commits in the lookahead buffer have been pre-fetched by\n+\t * get_revision() and will be shown in the future. They already have\n+\t * the SHOWN flag set when they were pre-fetched but the graph still\n+\t * needs to treat them as interesting parents.\n+\t */\n+\tfor (int i = 0; i < graph->lookahead_nr; i++)\n+\t\tif (graph->lookahead[i] == commit)\n+\t\t\treturn 1;\n \t/*\n \t * If revs->boundary is set, commits whose children have\n \t * been shown are always interesting, even if they have the\n@@ -763,6 +783,37 @@ static int graph_needs_pre_commit_line(struct git_graph *graph)\n \t       graph->expansion_row < graph_num_expansion_rows(graph);\n }\n \n+struct commit *graph_pop_lookahead(struct git_graph *graph)\n+{\n+\tstruct commit *c;\n+\n+\tif (!graph->lookahead_nr)\n+\t\treturn NULL;\n+\n+\tc = graph->lookahead[0];\n+\tif (!c)\n+\t\tBUG(\"lookahead buffer has %d entries but the first one is NULL\",\n+\t\t    graph->lookahead_nr);\n+\n+\tgraph->lookahead[0] = graph->lookahead[1];\n+\tgraph->lookahead[1] = NULL;\n+\tgraph->lookahead_nr--;\n+\treturn c;\n+}\n+\n+int graph_get_lookahead_room(struct git_graph *graph)\n+{\n+\treturn (int)ARRAY_SIZE(graph->lookahead) - graph->lookahead_nr;\n+}\n+\n+void graph_push_lookahead(struct git_graph *graph, struct commit *c)\n+{\n+\tif (!graph_get_lookahead_room(graph))\n+\t\tBUG(\"pushing into lookahead buffer when it is already full\");\n+\n+\tgraph->lookahead[graph->lookahead_nr++] = c;\n+}\n+\n void graph_update(struct git_graph *graph, struct commit *commit)\n {\n \tstruct commit_list *parent;\ndiff --git a/graph.h b/graph.h\nindex 3fd1dcb2e9..1193711fb8 100644\n--- a/graph.h\n+++ b/graph.h\n@@ -262,4 +262,21 @@ void graph_show_commit_msg(struct git_graph *graph,\n \t\t\t   FILE *file,\n \t\t\t   struct strbuf const *sb);\n \n+/*\n+ * Pop the first commit from the graph's lookahead buffer.\n+ * Returns NULL if the buffer is empty.\n+ */\n+struct commit *graph_pop_lookahead(struct git_graph *graph);\n+\n+/*\n+ * Returns how many more commits can be added to the lookahead buffer.\n+ */\n+int graph_get_lookahead_room(struct git_graph *graph);\n+\n+/*\n+ * Push a commit into the lookahead buffer. Must only be called when\n+ * graph_get_lookahead_room() returns > 0.\n+ */\n+void graph_push_lookahead(struct git_graph *graph, struct commit *c);\n+\n #endif /* GRAPH_H */\ndiff --git a/revision.c b/revision.c\nindex 288935943f..258c3cf782 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -4715,10 +4715,24 @@ struct commit *get_revision(struct rev_info *revs)\n \t\treturn c;\n \t}\n \n-\tc = next_commit_to_show(revs);\n+\tif (revs->graph) {\n+\t\tc = graph_pop_lookahead(revs->graph);\n+\t\tif (!c)\n+\t\t\tc = next_commit_to_show(revs);\n+\t} else {\n+\t\tc = next_commit_to_show(revs);\n+\t}\n \n-\tif (c && revs->graph)\n+\tif (c && revs->graph) {\n+\t\twhile (graph_get_lookahead_room(revs->graph)) {\n+\t\t\tstruct commit *next = next_commit_to_show(revs);\n+\t\t\tif (!next)\n+\t\t\t\tbreak;\n+\t\t\tgraph_push_lookahead(revs->graph, next);\n+\t\t}\n \t\tgraph_update(revs->graph, c);\n+\t}\n+\n \tif (!c) {\n \t\tfree_saved_parents(revs);\n \t\tcommit_list_free(revs->previous_parents);\n\n-- \n2.54.0\n"},{"id":"548029","messageId":"20260713-ps-pre-commit-indent-v11-4-dcb65bc4ba99@gmail.com","threadId":"65419","inReplyTo":"20260713-ps-pre-commit-indent-v11-0-dcb65bc4ba99@gmail.com","subject":"[PATCH v11 4/7] graph: indent visual root in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-13T16:44:01Z","receivedAt":"2026-07-13T16:44:13Z","isPatch":true,"body":"When rendering a graph, if the history contains multiple \"visual roots\",\nactual roots or commits that look like roots (i.e. have their parents\nfiltered out) can end up being vertically adjacent to unrelated commits,\nfalsely appearing to be related.\n\nA fix for this issue was already attempted [1] a while ago.\n\nThis happens because the commits fill the space from left to right and\nwhen a visual root ends, its column becomes free for the following\ncommit even if they are not related. Once this happens the unrelated\ncommit is rendered below the visual root. Because there is no special\ncharacter or way to identify when a visual root is rendered making the\ngraph confusing.\n\nBy indenting the visual roots when there are still commits to show the\nvertical adjacency can be avoided.\n\nAdd is_visual_root flag to git_graph making it visible in all graph states,\ngive graph_update() a new function, graph_is_visual_root() to know if the\ncurrent commit is a visual root and set is_visual_root.\nThe different handled cases are:\n\n- If a visual root has children: similar to GRAPH_PRE_COMMIT state when\n  octopus merges need space, an edge row needs to be printed to connect\n  the child with the indented visual root. A new state GRAPH_PRE_ROOT is\n  needed to connect the child with the visual root:\n\n    * child of the visual root\n     \\ GRAPH_PRE_ROOT\n      * visual root indented\n\n- If a visual root is child-less we can skip GRAPH_PRE_ROOT state and\n  render the indented commit directly.\n\n      * visual root indented\n    * unrelated commit\n\n- If two or more visual roots are adjacent: by having a lookahead to the\n  next commit that will be rendered, if the next commit is also a visual\n  root and we are on a visual root, meaning two visual root adjacent in\n  the history, the top one can omit the indent, making the one below to\n  indent only once, if there are more adjacent visual commits, the\n  indentation will increase for each adjacent one, cascading.\n\n    * visual root\n      * visual root\n        * visual root\n    * last commit\n\n  Even if the last commit is a root, because there is nothing that will be\n  rendered below we can omit the indentation on purpose.\n\n[1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n\nHelped-by: Kristofer Karlsson <krka@spotify.com>\nMentored-by: Karthik Nayak <karthik.188@gmail.com>\nMentored-by: Chandra Pratap <chandrapratap3519@gmail.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c                          | 244 +++++++++++++++++++\n t/meson.build                    |   1 +\n t/t4218-log-graph-indentation.sh | 514 +++++++++++++++++++++++++++++++++++++++\n 3 files changed, 759 insertions(+)\n\ndiff --git a/graph.c b/graph.c\nindex 89ebcf7540..087094189f 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -60,12 +60,23 @@ struct column {\n \t * index into column_colors.\n \t */\n \tunsigned short color;\n+\t/*\n+\t * Marks if a commit is a non-first parent of a merge. These columns are\n+\t * already visually connected to the merge commit and do not need\n+\t * indentation.\n+\t *\n+\t * The first parent is the one that inherits the column and it can need\n+\t * indentation if turns out to be a visual root and there's still\n+\t * commits to render.\n+\t */\n+\tunsigned int is_merge_parent:1;\n };\n \n enum graph_state {\n \tGRAPH_PADDING,\n \tGRAPH_SKIP,\n \tGRAPH_PRE_COMMIT,\n+\tGRAPH_PRE_ROOT,\n \tGRAPH_COMMIT,\n \tGRAPH_POST_MERGE,\n \tGRAPH_COLLAPSING\n@@ -323,6 +334,51 @@ struct git_graph {\n \t */\n \tstruct commit *lookahead[2];\n \tint lookahead_nr;\n+\n+\t/*\n+\t * If a commit is a visual root, we need to indent it to prevent\n+\t * unrelated commits from being vertically adjacent to it.\n+\t */\n+\tunsigned int is_visual_root:1;\n+\n+\t/*\n+\t * Indentation increases for each visual root adjacent to another visual\n+\t * root, making visual root commits indentation cascade.\n+\t */\n+\tunsigned int visual_root_depth;\n+\n+\t/*\n+\t * When a visual root is adjacent to other visual roots, the first one\n+\t * can avoid indentation and the rest cascades, increasing the indentation\n+\t * for each one.\n+\t */\n+\tunsigned int visual_root_cascade:1;\n+\n+\t/*\n+\t * Set when the current commit was already present in graph->columns\n+\t * before being processed.\n+\t */\n+\tunsigned int commit_in_columns:1;\n+};\n+\n+struct graph_lookahead_flags {\n+\n+\t/*\n+\t * Set when there will be a commit after the current one that will be\n+\t * rendered.\n+\t */\n+\tunsigned int is_next_visible:1;\n+\n+\t/*\n+\t * Set when the next visible commit is candidate to be a visual root.\n+\t */\n+\tunsigned int is_next_visual_root:1;\n+\n+\t/*\n+\t * Set when the next visible commit will be rendered under the current\n+\t * commit.\n+\t */\n+\tunsigned int next_has_column:1;\n };\n \n static inline int graph_needs_truncation(struct git_graph *graph, int lane)\n@@ -399,6 +455,8 @@ struct git_graph *graph_init(struct rev_info *opt)\n \tgraph->lookahead[0] = NULL;\n \tgraph->lookahead[1] = NULL;\n \tgraph->lookahead_nr = 0;\n+\tgraph->visual_root_depth = 0;\n+\tgraph->visual_root_cascade = 0;\n \t/*\n \t * Start the column color at the maximum value, since we'll\n \t * always increment it for the first commit we output.\n@@ -581,6 +639,11 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t\t\t\t\t  struct commit *commit,\n \t\t\t\t\t  int idx)\n {\n+\t/*\n+\t * Get the initial merge_layout before it's modified to know if this\n+\t * is a merge.\n+\t */\n+\tint initial_merge_layout = graph->merge_layout;\n \tint i = graph_find_new_column_by_commit(graph, commit);\n \tint mapping_idx;\n \n@@ -592,6 +655,7 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t\ti = graph->num_new_columns++;\n \t\tgraph->new_columns[i].commit = commit;\n \t\tgraph->new_columns[i].color = graph_find_commit_color(graph, commit);\n+\t\tgraph->new_columns[i].is_merge_parent = 0;\n \t}\n \n \tif (graph->num_parents > 1 && idx > -1 && graph->merge_layout == -1) {\n@@ -630,6 +694,12 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t}\n \n \tgraph->mapping[mapping_idx] = i;\n+\n+\t/*\n+\t * Mark non-first parents of a merge.\n+\t */\n+\tif (graph->num_parents > 1 && initial_merge_layout >= 0 && idx > -1)\n+\t\tgraph->new_columns[i].is_merge_parent = 1;\n }\n \n static void graph_update_columns(struct git_graph *graph)\n@@ -721,10 +791,20 @@ static void graph_update_columns(struct git_graph *graph)\n \t\t\tif (graph->num_parents == 0)\n \t\t\t\tgraph->width += 2;\n \t\t} else {\n+\t\t\tint j;\n \t\t\tgraph_insert_into_new_columns(graph, col_commit, -1);\n+\t\t\t/*\n+\t\t\t * This column is not the current commit, but we need to\n+\t\t\t * propagate the flag until the commit is processed.\n+\t\t\t */\n+\t\t\tj = graph_find_new_column_by_commit(graph, col_commit);\n+\t\t\tif (j >= 0 && graph->columns[i].is_merge_parent)\n+\t\t\t\tgraph->new_columns[j].is_merge_parent = 1;\n \t\t}\n \t}\n \n+\tgraph->commit_in_columns = is_commit_in_columns;\n+\n \t/*\n \t * If graph_max_lanes is set, cap the width\n \t */\n@@ -814,9 +894,113 @@ void graph_push_lookahead(struct git_graph *graph, struct commit *c)\n \tgraph->lookahead[graph->lookahead_nr++] = c;\n }\n \n+/*\n+ * A commit can be a visual root when:\n+ *\n+ * - It has no parents.\n+ *\n+ * - It has parents but they are all filtered out and\n+ *   commit->parents arrives NULL.\n+ *\n+ * - Its parents are uninteresting.\n+ *\n+ * - It is not a boundary commit. Boundary commits also have no visible\n+ *   parents, but they are not selected as visual roots because they cannot\n+ *   cause the ambiguity of being vertically adjacent because:\n+ *\n+ *   1. A boundary only appears because an included commit is its child.\n+ *      Children are always above, and the renderer draws an edge down to\n+ *      the boundary from that child. Rather than starting a column like a\n+ *      visual root would do, it inherits its child column.\n+ *\n+ *   2. Included commits cannot appear below a boundary. Boundaries are\n+ *      ancestors of the exclusion point; if an included commit were an\n+ *      ancestor of the boundary it would be excluded and not rendered.\n+ *      Boundaries therefore always sink to the bottom.\n+ */\n+static int graph_is_visual_root_candidate(struct commit *c, struct git_graph *graph)\n+{\n+\tstruct commit_list *p;\n+\n+\tif (c->object.flags & BOUNDARY)\n+\t\treturn 0;\n+\tfor (p = c->parents; p; p = p->next)\n+\t\tif (graph_is_interesting(graph, p->item))\n+\t\t\treturn 0;\n+\treturn 1;\n+}\n+\n+static int graph_is_visual_root(struct git_graph *graph,\n+\t\t\t\tstruct graph_lookahead_flags *flags)\n+{\n+\t/*\n+\t * This must be only called for the current commit as graph contains\n+\t * the state for the current commit only.\n+\t *\n+\t * To check if a commit is a visual root, call graph_is_visual_root_candidate()\n+\t * but we won't know if it is really a visual root until we get to the\n+\t * next commit state.\n+\t *\n+\t * The current commit is an actual visual root if it is a candidate and\n+\t * the commit is not a non-first parent of a merge.\n+\t *\n+\t *   *\n+\t *   |\\\n+\t *   | *    <- it is a visual root candidate but it shouldn't be indented\n+\t *   *         because it is already connected by an edge.\n+\t *   ^         if commit_in_columns && is_merge_parent means the commit\n+\t *   |         was put by a merge and is connected.\n+\t *   |\n+\t *   `-------- if !is_next_visible means we're on the last commit, avoid\n+\t *             indentation unless the one before is a visual root, then\n+\t *             we need to differentiate from the one above.\n+\t *\n+\t * If next_has_columns means that the next commit has\n+\t * already a column, so it will not be rendered below, the\n+\t * current commit has to act as the last commit and omit\n+\t * indentation.\n+\t */\n+\treturn graph_is_visual_root_candidate(graph->commit, graph) &&\n+\t       !(graph->commit_in_columns &&\n+\t\t graph->columns[graph->commit_index].is_merge_parent) &&\n+\t       flags->is_next_visible &&\n+\t       (!flags->next_has_column || graph->visual_root_depth > 0);\n+}\n+\n+/*\n+ * Peeks the next commits via the lookahead buffer and sets the lookahead flags.\n+ */\n+static void graph_peek_next_visible(struct git_graph *graph,\n+\t\t\t\t    struct graph_lookahead_flags *flags)\n+{\n+\tflags->is_next_visible = 0;\n+\tflags->is_next_visual_root = 0;\n+\tflags->next_has_column = 0;\n+\n+\tif (!graph->lookahead_nr)\n+\t\treturn;\n+\n+\tflags->is_next_visible = 1;\n+\tflags->next_has_column =\n+\t\tgraph_find_new_column_by_commit(graph, graph->lookahead[0]) >= 0;\n+\n+\tif (!graph_is_visual_root_candidate(graph->lookahead[0], graph))\n+\t\treturn;\n+\n+\tif (graph->lookahead_nr >= 2)\n+\t\tflags->is_next_visual_root = 1;\n+}\n+\n+static int graph_needs_pre_root_line(struct git_graph *graph)\n+{\n+\treturn graph->commit_in_columns && graph->is_visual_root &&\n+\t       graph->num_columns > 0 && !graph->visual_root_cascade;\n+}\n+\n void graph_update(struct git_graph *graph, struct commit *commit)\n {\n \tstruct commit_list *parent;\n+\tstruct graph_lookahead_flags flags;\n \n \t/*\n \t * Set the new commit\n@@ -847,6 +1031,23 @@ void graph_update(struct git_graph *graph, struct commit *commit)\n \t */\n \tgraph_update_columns(graph);\n \n+\tgraph_peek_next_visible(graph, &flags);\n+\n+\tgraph->is_visual_root = graph_is_visual_root(graph, &flags);\n+\n+\tif (graph->is_visual_root) {\n+\t\t/*\n+\t\t * If next is a visual root we can omit the indent for the first\n+\t\t * visual root and start cascading.\n+\t\t */\n+\t\tif (!graph->visual_root_depth && flags.is_next_visual_root)\n+\t\t\tgraph->visual_root_cascade = 1;\n+\t\tgraph->visual_root_depth++;\n+\t} else {\n+\t\tgraph->visual_root_depth = 0;\n+\t\tgraph->visual_root_cascade = 0;\n+\t}\n+\n \tgraph->expansion_row = 0;\n \n \t/*\n@@ -864,11 +1065,16 @@ void graph_update(struct git_graph *graph, struct commit *commit)\n \t * room for it.  We need to do this only if there is a branch row\n \t * (or more) to the right of this commit.\n \t *\n+\t * If it is a visual root, we need to print an extra row to\n+\t * connect the indentation.\n+\t *\n \t * If there are less than 3 parents, we can immediately print the\n \t * commit line.\n \t */\n \tif (graph->state != GRAPH_PADDING)\n \t\tgraph->state = GRAPH_SKIP;\n+\telse if (graph_needs_pre_root_line(graph))\n+\t\tgraph->state = GRAPH_PRE_ROOT;\n \telse if (graph_needs_pre_commit_line(graph))\n \t\tgraph->state = GRAPH_PRE_COMMIT;\n \telse\n@@ -1116,6 +1322,17 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \n \t\tif (col_commit == graph->commit) {\n \t\t\tseen_this = 1;\n+\t\t\tif (graph->is_visual_root) {\n+\t\t\t\tint depth = graph->visual_root_depth;\n+\t\t\t\t/*\n+\t\t\t\t * Each visual column is 2 characters wide.\n+\t\t\t\t * Omit the indentation for the first visual\n+\t\t\t\t * root in cascade mode.\n+\t\t\t\t */\n+\t\t\t\tint padding = (depth - graph->visual_root_cascade) * 2;\n+\t\t\t\tgraph_line_addchars(line, ' ', padding);\n+\t\t\t\tgraph->width += padding;\n+\t\t\t}\n \t\t\tgraph_output_commit_char(graph, line);\n \n \t\t\tif (graph_needs_truncation(graph, i)) {\n@@ -1487,6 +1704,30 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n \t\tgraph_update_state(graph, GRAPH_PADDING);\n }\n \n+static void graph_output_pre_root_line(struct git_graph *graph, struct graph_line *line)\n+{\n+\t/*\n+\t * This function adds a row before a visual root, to connect the\n+\t * branch to the indented commit. It must only be called on a\n+\t * visual root.\n+\t */\n+\tif (!graph->is_visual_root)\n+\t\tBUG(\"commit must be a visual root to call pre_root_line\");\n+\n+\tfor (int i = 0; i < graph->num_columns; i++) {\n+\t\tstruct column *col = &graph->columns[i];\n+\t\tif (col->commit == graph->commit) {\n+\t\t\tgraph_line_addch(line, ' ');\n+\t\t\tgraph_line_write_column(line, col, '\\\\');\n+\t\t} else {\n+\t\t\tgraph_line_write_column(line, col, '|');\n+\t\t}\n+\t\tgraph_line_addch(line, ' ');\n+\t}\n+\n+\tgraph_update_state(graph, GRAPH_COMMIT);\n+}\n+\n int graph_next_line(struct git_graph *graph, struct strbuf *sb)\n {\n \tint shown_commit_line = 0;\n@@ -1512,6 +1753,9 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb)\n \tcase GRAPH_PRE_COMMIT:\n \t\tgraph_output_pre_commit_line(graph, &line);\n \t\tbreak;\n+\tcase GRAPH_PRE_ROOT:\n+\t\tgraph_output_pre_root_line(graph, &line);\n+\t\tbreak;\n \tcase GRAPH_COMMIT:\n \t\tgraph_output_commit_line(graph, &line);\n \t\tshown_commit_line = 1;\ndiff --git a/t/meson.build b/t/meson.build\nindex 7c3c070426..cce5ba71f9 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -577,6 +577,7 @@ integration_tests = [\n   't4215-log-skewed-merges.sh',\n   't4216-log-bloom.sh',\n   't4217-log-limit.sh',\n+  't4218-log-graph-indentation.sh',\n   't4219-log-follow-merge.sh',\n   't4252-am-options.sh',\n   't4253-am-keep-cr-dos.sh',\ndiff --git a/t/t4218-log-graph-indentation.sh b/t/t4218-log-graph-indentation.sh\nnew file mode 100755\nindex 0000000000..60c7d84af7\n--- /dev/null\n+++ b/t/t4218-log-graph-indentation.sh\n@@ -0,0 +1,514 @@\n+#!/bin/sh\n+\n+test_description='git log --graph visual root indentations'\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-log-graph.sh\n+\n+check_graph_with_description () {\n+\tcat >expect &&\n+\tlib_test_cmp_graph --format=\"%s%ndescription%nsecond-line\" \"$@\"\n+}\n+\n+create_orphan () {\n+\tgit checkout --orphan \"$1\" &&\n+\ttest_might_fail git rm -rf .\n+}\n+\n+# disable commit-graph topo order to have the graph to render in different\n+# ways (used in --first-parent tests to have multiple visual roots while a\n+# column is active at the same time).\n+unset_commit_graph () {\n+\tsane_unset GIT_TEST_COMMIT_GRAPH &&\n+\trm -f .git/objects/info/commit-graph &&\n+\trm -rf .git/objects/info/commit-graphs\n+}\n+\n+test_expect_success 'single root commit is not indented' '\n+\tcreate_orphan _1 && test_commit 1_A &&\n+\tlib_test_check_graph _1 <<-\\EOF\n+\t* 1_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indented before unrelated branch' '\n+\tcreate_orphan _2 && test_commit 2_A && test_commit 2_B &&\n+\tcreate_orphan _3 && test_commit 3_A &&\n+\tlib_test_check_graph _2 _3 <<-\\EOF\n+\t  * 3_A\n+\t* 2_B\n+\t* 2_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indentation with --left-right' '\n+\tlib_test_check_graph --left-right _2..._3 <<-\\EOF\n+\t  > 3_A\n+\t< 2_B\n+\t< 2_A\n+\tEOF\n+'\n+\n+# A better case of why indentation is still needed with '--left-right' flag is\n+# that unrelated branches can be on the same side, so it's needed to\n+# differentiate visual roots on the same side.\n+test_expect_success 'visual root indentation with --left-right having unrelated commits on the same side' '\n+\tlib_test_check_graph --left-right _2..._3 _1 <<-\\EOF\n+\t  > 3_A\n+\t< 2_B\n+\t \\\n+\t  < 2_A\n+\t> 1_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indents the description also' '\n+\tcheck_graph_with_description _2 _3 <<-\\EOF\n+\t  * 3_A\n+\t    description\n+\t    second-line\n+\t* 2_B\n+\t| description\n+\t| second-line\n+\t* 2_A\n+\t  description\n+\t  second-line\n+\tEOF\n+'\n+\n+test_expect_success 'indented visual root parent gets connected to its child' '\n+\tcreate_orphan _4 && test_commit 4_A && test_commit 4_B &&\n+\tcreate_orphan _5 && test_commit 5_A && test_commit 5_B &&\n+\tlib_test_check_graph _4 _5 <<-\\EOF\n+\t* 5_B\n+\t \\\n+\t  * 5_A\n+\t* 4_B\n+\t* 4_A\n+\tEOF\n+'\n+\n+test_expect_success 'indented visual root parent gets connected to its child with description' '\n+\tcheck_graph_with_description _4 _5 <<-\\EOF\n+\t* 5_B\n+\t| description\n+\t| second-line\n+\t \\\n+\t  * 5_A\n+\t    description\n+\t    second-line\n+\t* 4_B\n+\t| description\n+\t| second-line\n+\t* 4_A\n+\t  description\n+\t  second-line\n+\tEOF\n+'\n+\n+test_expect_success 'visual roots cascade and last root does not' '\n+\tcreate_orphan _7 && test_commit 7_A && test_commit 7_B &&\n+\tcreate_orphan _8 && test_commit 8_A &&\n+\tcreate_orphan _9 && test_commit 9_A &&\n+\tcreate_orphan _10 && test_commit 10_A &&\n+\tlib_test_check_graph _7 _8 _9 _10 <<-\\EOF\n+\t* 10_A\n+\t  * 9_A\n+\t    * 8_A\n+\t* 7_B\n+\t* 7_A\n+\tEOF\n+'\n+\n+test_expect_success 'last root does not cascade' '\n+\tlib_test_check_graph _8 _9 _10 <<-\\EOF\n+\t* 10_A\n+\t  * 9_A\n+\t* 8_A\n+\tEOF\n+'\n+\n+test_expect_success 'merge parents are roots between them but they do not indent' '\n+\tcreate_orphan _11 && test_commit 11_A &&\n+\tcreate_orphan _12 && test_commit 12_A &&\n+\tcreate_orphan _13 && test_commit 13_A &&\n+\tgit checkout _11 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _11 -p _12 -p _13 -m 11_octopus) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _11 <<-\\EOF\n+\t*-.   11_octopus\n+\t|\\ \\\n+\t| | * 13_A\n+\t| * 12_A\n+\t* 11_A\n+\tEOF\n+'\n+\n+# The last parent of a merge can be indented if nothing related to it needs to\n+# be rendered after, if it's another visual root, merge parent must not get\n+# indented but rather activate cascading.\n+test_expect_success 'merge then unrelated visual root and unrelated branch' '\n+\tcreate_orphan _16 && test_commit 16_A && test_commit 16_B &&\n+\tcreate_orphan _17 && test_commit 17_A &&\n+\tcreate_orphan _18 && test_commit 18_A &&\n+\tcreate_orphan _19 && test_commit 19_A &&\n+\tcreate_orphan _20 && test_commit 20_A &&\n+\tgit checkout _18 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _18 -p _19 -p _20 -m 18_octopus) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _18 _17 _16 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t* 18_A\n+\t  * 17_A\n+\t* 16_B\n+\t* 16_A\n+\tEOF\n+'\n+\n+# The last commit root does not get indented, if the next thing after the root\n+# merge parent is the last commit, indent the merge parent.\n+test_expect_success 'merge then unrelated root indents merge parent' '\n+\tlib_test_check_graph _18 _17 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t \\\n+\t  * 18_A\n+\t* 17_A\n+\tEOF\n+'\n+\n+test_expect_success 'merge then unrelated branch indents merge parent' '\n+\tlib_test_check_graph _18 _16 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t \\\n+\t  * 18_A\n+\t* 16_B\n+\t* 16_A\n+\tEOF\n+'\n+\n+test_expect_success 'two-parent merge of orphans' '\n+\tcreate_orphan _21 && test_commit 21_A &&\n+\tcreate_orphan _22 && test_commit 22_A &&\n+\tgit checkout _21 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _21 -p _22 -m 21_merge) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _21 <<-\\EOF\n+\t*   21_merge\n+\t|\\\n+\t| * 22_A\n+\t* 21_A\n+\tEOF\n+'\n+\n+test_expect_success 'commit with filtered parent becomes a visual root' '\n+\tcreate_orphan _23 &&\n+\techo test >other.txt &&\n+\tgit add other.txt &&\n+\tgit commit -m \"23_A\" &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"23_B\" &&\n+\tcreate_orphan _24 &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"24_A\" &&\n+\tlib_test_check_graph _23 _24 -- foo.txt <<-\\EOF\n+\t  * 23_B\n+\t* 24_A\n+\tEOF\n+'\n+\n+test_expect_success 'filtered parent cascading edge case' '\n+\tcreate_orphan _27 &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\ttest_tick &&\n+\tgit commit -m \"D (last)\" &&\n+\n+\tcreate_orphan _25 &&\n+\techo test >other.txt &&\n+\tgit add other.txt &&\n+\ttest_tick &&\n+\tgit commit -m \"C-filtered\" &&\n+\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\ttest_tick &&\n+\tgit commit -m \"B (child of filtered)\" &&\n+\n+\tcreate_orphan _26 &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\ttest_tick &&\n+\tgit commit -m \"A (visual root)\" &&\n+\n+\tlib_test_check_graph _25 _26 _27 -- foo.txt <<-\\EOF\n+\t* A (visual root)\n+\t  * B (child of filtered)\n+\t* D (last)\n+\tEOF\n+'\n+\n+test_expect_success 'multiple filtered parents in sequence' '\n+\tcreate_orphan _44 &&\n+\techo a >other.txt && git add other.txt && git commit -m \"44_F\" &&\n+\techo b >foo.txt && git add foo.txt && git commit -m \"44_C\" &&\n+\n+\tcreate_orphan _45 &&\n+\techo c >other.txt && git add other.txt && git commit -m \"45_F\" &&\n+\techo d >foo.txt && git add foo.txt && git commit -m \"45_C\" &&\n+\n+\tcreate_orphan _46 &&\n+\techo e >foo.txt && git add foo.txt && git commit -m \"46_A\" &&\n+\n+\tlib_test_check_graph _44 _45 _46 -- foo.txt <<-\\EOF\n+\t* 44_C\n+\t  * 45_C\n+\t* 46_A\n+\tEOF\n+'\n+\n+# These tests prove why there is no need to have indentation for boundary\n+# commits.\n+#\n+# Boundary commits rather than starting a column they 'inherit' the one of\n+# its child so there will always be an edge that connects it removing the\n+# ambiguity.\n+test_expect_success 'unrelated boundaries are not ambiguous' '\n+\tcreate_orphan _28 && test_commit 28_A && test_commit 28_B &&\n+\ttest_commit 28_C &&\n+\tcreate_orphan _29 && test_commit 29_A && test_commit 29_B &&\n+\tlib_test_check_graph --boundary 28_A.._28 29_A.._29 <<-\\EOF\n+\t* 29_B\n+\t| * 28_C\n+\t| * 28_B\n+\t| o 28_A\n+\to 29_A\n+\tEOF\n+'\n+\n+# Same structure as t6016\n+test_expect_success 'boundary commits big test' '\n+\t# 3 commits on branch _30\n+\tcreate_orphan _30 &&\n+\ttest_commit 30_A &&\n+\ttest_commit 30_B &&\n+\ttest_commit 30_C &&\n+\n+\t# 2 commits on branch _31, started from 30_A\n+\tgit checkout -b _31 30_A &&\n+\ttest_commit 31_A &&\n+\ttest_commit 31_B &&\n+\n+\t# 2 commits on branch _32, started from 30_B\n+\tgit checkout -b _32 30_B &&\n+\ttest_commit 32_A &&\n+\ttest_commit 32_B &&\n+\n+\t# Octopus merge _31 and _32 into -30\n+\tgit checkout _30 &&\n+\tgit merge _31 _32 -m 30_D &&\n+\tgit tag 30_D &&\n+\ttest_commit 30_E &&\n+\n+\t# More commits on _32, then merge _32 into _30\n+\tgit checkout _32 &&\n+\ttest_commit 32_C &&\n+\ttest_commit 32_D &&\n+\tgit checkout _30 &&\n+\tgit merge -s ours _32 -m 30_F &&\n+\tgit tag 30_F &&\n+\ttest_commit 30_G &&\n+\tlib_test_check_graph --boundary _30 _31 _32 ^32_C <<-\\EOF\n+\t* 30_G\n+\t*   30_F\n+\t|\\\n+\t| * 32_D\n+\t* | 30_E\n+\t| |\n+\t|  \\\n+\t*-. \\   30_D\n+\t|\\ \\ \\\n+\t| * | | 31_B\n+\t| * | | 31_A\n+\t* | | | 30_C\n+\to | | | 30_B\n+\t|/ / /\n+\to / / 30_A\n+\t / /\n+\t| o 32_C\n+\t|/\n+\to 32_B\n+\tEOF\n+'\n+\n+# Filter by --first-parent and then forcing the filtered parents to be shown.\n+test_expect_success '--first-parent flag with the filtered parents' '\n+\t(\n+\t\tunset_commit_graph &&\n+\t\tcreate_orphan _35 && test_commit 35_A && test_commit 35_B &&\n+\t\tcreate_orphan _36 && test_commit 36_A &&\n+\t\tcreate_orphan _37 && test_commit 37_A &&\n+\t\tgit checkout _35 &&\n+\t\tTREE=$(git write-tree) &&\n+\t\tMERGE=$(git commit-tree $TREE -p _35 -p _36 -p _37 -m 35_octopus) &&\n+\t\tgit reset --hard $MERGE &&\n+\t\tlib_test_check_graph --first-parent _35 _36 _37 <<-\\EOF\n+\t\t* 35_octopus\n+\t\t| * 37_A\n+\t\t|   * 36_A\n+\t\t* 35_B\n+\t\t* 35_A\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success '--first-parent with filtered parents but one has a child' '\n+\t(\n+\t\tunset_commit_graph &&\n+\t\tcreate_orphan _38 && test_commit 38_A && test_commit 38_B &&\n+\t\tcreate_orphan _39 && test_commit 39_A &&\n+\t\tcreate_orphan _40 && test_commit 40_A && test_commit 40_B &&\n+\t\tgit checkout _38 &&\n+\t\tTREE=$(git write-tree) &&\n+\t\tMERGE=$(git commit-tree $TREE -p _38 -p _39 -p _40 -m 38_octopus) &&\n+\t\tgit reset --hard $MERGE &&\n+\t\tlib_test_check_graph --first-parent _38 _39 _40 <<-\\EOF\n+\t\t* 38_octopus\n+\t\t| * 40_B\n+\t\t| * 40_A\n+\t\t|   * 39_A\n+\t\t* 38_B\n+\t\t* 38_A\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success '--first-parent with filtered parents but both have children' '\n+\t(\n+\t\tunset_commit_graph &&\n+\t\tcreate_orphan _41 && test_commit 41_A && test_commit 41_B &&\n+\t\tcreate_orphan _42 && test_commit 42_A && test_commit 42_B &&\n+\t\tcreate_orphan _43 && test_commit 43_A && test_commit 43_B &&\n+\t\tgit checkout _41 &&\n+\t\tTREE=$(git write-tree) &&\n+\t\tMERGE=$(git commit-tree $TREE -p _41 -p _42 -p _43 -m 41_octopus) &&\n+\t\tgit reset --hard $MERGE &&\n+\t\tlib_test_check_graph --first-parent _41 _42 _43 <<-\\EOF\n+\t\t* 41_octopus\n+\t\t| * 43_B\n+\t\t|  \\\n+\t\t|   * 43_A\n+\t\t| * 42_B\n+\t\t| * 42_A\n+\t\t* 41_B\n+\t\t* 41_A\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'two unrelated merges' '\n+\tcreate_orphan _50 && test_commit 50_A &&\n+\tgit checkout -b _51 &&\n+\ttest_commit 51_A && test_commit 51_B &&\n+\tgit checkout _50 &&\n+\tgit merge --no-ff _51 -m 50_B &&\n+\n+\tcreate_orphan _52 && test_commit 52_A &&\n+\tgit checkout -b _53 &&\n+\ttest_commit 53_A && test_commit 53_B &&\n+\tgit checkout _52 &&\n+\tgit merge --no-ff _53 -m 52_B &&\n+\n+\tlib_test_check_graph _52 _50 <<-\\EOF\n+\t*   52_B\n+\t|\\\n+\t| * 53_B\n+\t| * 53_A\n+\t|/\n+\t \\\n+\t  * 52_A\n+\t*   50_B\n+\t|\\\n+\t| * 51_B\n+\t| * 51_A\n+\t|/\n+\t* 50_A\n+\tEOF\n+'\n+\n+test_expect_success '--max-count treats the last visible commit as the last commit' '\n+\tlib_test_check_graph --max-count=2 _8 _9 _10 <<-\\EOF\n+\t  * 10_A\n+\t* 9_A\n+\tEOF\n+'\n+\n+test_expect_success '--max-count=1 shows a single root without indentation' '\n+\tlib_test_check_graph --max-count=1 _8 _9 _10 <<-\\EOF\n+\t* 10_A\n+\tEOF\n+'\n+\n+test_expect_success '--max-count-oldest indents visual roots' '\n+\tlib_test_check_graph --max-count-oldest=3 _8 _9 _10 <<-\\EOF\n+\t* 10_A\n+\t  * 9_A\n+\t* 8_A\n+\tEOF\n+'\n+\n+# when the graph commits are filtered with regex options like --author, the\n+# commit parents do not come NULL so it is needed to check if the parents are\n+# interesting.\n+test_expect_success '--author skipped parent makes a visual root' '\n+\tcreate_orphan _55 &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 55_A &&\n+\tcreate_orphan _54 &&\n+\ttest_tick &&\n+\tgit commit --allow-empty --author=\"Other <other@example.com>\" -m 54_A &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 54_B &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 54_C &&\n+\tlib_test_check_graph --author=\"A U Thor\" _54 _55 <<-\\EOF\n+\t* 54_C\n+\t \\\n+\t  * 54_B\n+\t* 55_A\n+\tEOF\n+'\n+\n+test_expect_success '--grep skipped parent makes a visual root' '\n+\tcreate_orphan _57 &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 57_keep_A &&\n+\tcreate_orphan _56 &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 56_skip &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 56_keep_A &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 56_keep_B &&\n+\tlib_test_check_graph --grep=keep _56 _57 <<-\\EOF\n+\t* 56_keep_B\n+\t \\\n+\t  * 56_keep_A\n+\t* 57_keep_A\n+\tEOF\n+'\n+\n+test_done\n\n-- \n2.54.0\n"},{"id":"548030","messageId":"20260713-ps-pre-commit-indent-v11-6-dcb65bc4ba99@gmail.com","threadId":"65419","inReplyTo":"20260713-ps-pre-commit-indent-v11-0-dcb65bc4ba99@gmail.com","subject":"[PATCH v11 6/7] graph: move config reading into graph_read_config()","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-13T16:44:03Z","receivedAt":"2026-07-13T16:44:15Z","isPatch":true,"body":"Move the repo_config_get_string() call out of graph_init() and into\ngraph_read_config(). This simplifies graph_init() and provides a\nfunction for future graph-related config opt.\n\nThis commit is a preparatory commit for a subsequent one.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c | 13 +++++++++----\n 1 file changed, 9 insertions(+), 4 deletions(-)\n\ndiff --git a/graph.c b/graph.c\nindex e3e206170c..c14be934a0 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -417,13 +417,11 @@ void graph_setup_line_prefix(struct diff_options *diffopt)\n \t\tdiffopt->output_prefix = diff_output_prefix_callback;\n }\n \n-struct git_graph *graph_init(struct rev_info *opt)\n+static void graph_read_config(struct rev_info *revs)\n {\n-\tstruct git_graph *graph = xmalloc(sizeof(struct git_graph));\n-\n \tif (!column_colors) {\n \t\tchar *string;\n-\t\tif (repo_config_get_string(opt->repo, \"log.graphcolors\", &string)) {\n+\t\tif (repo_config_get_string(revs->repo, \"log.graphcolors\", &string)) {\n \t\t\t/* not configured -- use default */\n \t\t\tgraph_set_column_colors(column_colors_ansi,\n \t\t\t\t\t\tcolumn_colors_ansi_max);\n@@ -437,6 +435,13 @@ struct git_graph *graph_init(struct rev_info *opt)\n \t\t\t\t\t\tcustom_colors.nr - 1);\n \t\t}\n \t}\n+}\n+\n+struct git_graph *graph_init(struct rev_info *opt)\n+{\n+\tstruct git_graph *graph = xmalloc(sizeof(struct git_graph));\n+\n+\tgraph_read_config(opt);\n \n \tgraph->commit = NULL;\n \tgraph->revs = opt;\n\n-- \n2.54.0\n"},{"id":"548031","messageId":"20260713-ps-pre-commit-indent-v11-7-dcb65bc4ba99@gmail.com","threadId":"65419","inReplyTo":"20260713-ps-pre-commit-indent-v11-0-dcb65bc4ba99@gmail.com","subject":"[PATCH v11 7/7] graph: add --[no-]graph-indent and log.graphIndent","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-13T16:44:04Z","receivedAt":"2026-07-13T16:44:17Z","isPatch":true,"body":"Some users may prefer to not have graph indentation.\n\nAdd \"log.graphIndent\" config variable to graph_read_config() to read the\ndefault preference. By default is graph indentation is true.\n\nAdd --graph-indent and --no-graph-indent options to overwrite the\ndefault preference.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n Documentation/config/log.adoc       |  4 +++\n Documentation/rev-list-options.adoc |  8 ++++++\n graph.c                             | 10 +++++--\n revision.c                          |  9 +++++++\n revision.h                          |  2 ++\n t/t4218-log-graph-indentation.sh    | 52 +++++++++++++++++++++++++++++++++++++\n 6 files changed, 83 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/config/log.adoc b/Documentation/config/log.adoc\nindex 757a7be196..f7dfce69b5 100644\n--- a/Documentation/config/log.adoc\n+++ b/Documentation/config/log.adoc\n@@ -59,6 +59,10 @@ This is the same as the `--decorate` option of the `git log`.\n \tA list of colors, separated by commas, that can be used to draw\n \thistory lines in `git log --graph`.\n \n+`log.graphIndent`::\n+\tIf `true`, indent visual roots when rendering the graphs with `--graph`.\n+\tSet true by default. It can be overriden with `--[no-]graph-indent`.\n+\n `log.showRoot`::\n \tIf true, the initial commit will be shown as a big creation event.\n \tThis is equivalent to a diff against an empty tree.\ndiff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc\nindex eaee6ee839..af74f10bb4 100644\n--- a/Documentation/rev-list-options.adoc\n+++ b/Documentation/rev-list-options.adoc\n@@ -1269,6 +1269,14 @@ This implies the `--topo-order` option by default, but the\n \tBy default it is set to 0 (no limit), zero and negative values\n \tare ignored and treated as no limit.\n \n+`--no-graph-indent`::\n+`--graph-indent`::\n+\tWhen used with `--graph`, indent visual roots (commits with no parents\n+\tor whose parents are not shown) to differentiate them from commits that\n+\tare vertically adjacent but unrelated. Enabled by default. Use\n+\t`--no-graph-indent` to disable or set `graph.indent` to set a deafault\n+\tpreference.\n+\n ifdef::git-rev-list[]\n `--count`::\n \tPrint a number stating how many commits would have been\ndiff --git a/graph.c b/graph.c\nindex c14be934a0..28bef1b88f 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -419,6 +419,8 @@ void graph_setup_line_prefix(struct diff_options *diffopt)\n \n static void graph_read_config(struct rev_info *revs)\n {\n+\tint val;\n+\n \tif (!column_colors) {\n \t\tchar *string;\n \t\tif (repo_config_get_string(revs->repo, \"log.graphcolors\", &string)) {\n@@ -435,6 +437,9 @@ static void graph_read_config(struct rev_info *revs)\n \t\t\t\t\t\tcustom_colors.nr - 1);\n \t\t}\n \t}\n+\n+\tif (!repo_config_get_bool(revs->repo, \"log.graphIndent\", &val))\n+\t\trevs->no_graph_indent = !val;\n }\n \n struct git_graph *graph_init(struct rev_info *opt)\n@@ -999,7 +1004,8 @@ static void graph_peek_next_visible(struct git_graph *graph,\n static int graph_needs_pre_root_line(struct git_graph *graph)\n {\n \treturn graph->commit_in_columns && graph->is_visual_root &&\n-\t       graph->num_columns > 0 && !graph->visual_root_cascade;\n+\t       graph->num_columns > 0 && !graph->visual_root_cascade &&\n+\t       !graph->revs->no_graph_indent;\n }\n \n void graph_update(struct git_graph *graph, struct commit *commit)\n@@ -1344,7 +1350,7 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \n \t\tif (col_commit == graph->commit) {\n \t\t\tseen_this = 1;\n-\t\t\tif (graph->is_visual_root) {\n+\t\t\tif (graph->is_visual_root && !graph->revs->no_graph_indent) {\n \t\t\t\tint depth = graph->visual_root_depth;\n \t\t\t\t/*\n \t\t\t\t * Each visual column is 2 characters wide.\ndiff --git a/revision.c b/revision.c\nindex 258c3cf782..215cf11071 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -2627,6 +2627,12 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg\n \t\trevs->graph = NULL;\n \t} else if (skip_prefix(arg, \"--graph-lane-limit=\", &optarg)) {\n \t\trevs->graph_max_lanes = parse_count(optarg);\n+\t} else if (!strcmp(arg, \"--graph-indent\")) {\n+\t\trevs->no_graph_indent = 0;\n+\t\trevs->graph_indent_set = 1;\n+\t} else if (!strcmp(arg, \"--no-graph-indent\")) {\n+\t\trevs->no_graph_indent = 1;\n+\t\trevs->graph_indent_set = 1;\n \t} else if (!strcmp(arg, \"--encode-email-headers\")) {\n \t\trevs->encode_email_headers = 1;\n \t} else if (!strcmp(arg, \"--no-encode-email-headers\")) {\n@@ -3201,6 +3207,9 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s\n \tif (revs->graph_max_lanes > 0 && !revs->graph)\n \t\tdie(_(\"the option '%s' requires '%s'\"), \"--graph-lane-limit\", \"--graph\");\n \n+\tif (revs->graph_indent_set > 0 && !revs->graph)\n+\t\tdie(_(\"the option '%s' requires '%s'\"), \"--[no-]graph-indent\", \"--graph\");\n+\n \tif (!revs->reflog_info && revs->grep_filter.use_reflog_filter)\n \t\tdie(_(\"the option '%s' requires '%s'\"), \"--grep-reflog\", \"--walk-reflogs\");\n \ndiff --git a/revision.h b/revision.h\nindex 569b3fa1cb..acf6d06b24 100644\n--- a/revision.h\n+++ b/revision.h\n@@ -314,6 +314,8 @@ struct rev_info {\n \t/* Display history graph */\n \tstruct git_graph *graph;\n \tint graph_max_lanes;\n+\tunsigned int no_graph_indent:1;\n+\tunsigned int graph_indent_set:1;\n \n \t/* special limits */\n \tint skip_count;\ndiff --git a/t/t4218-log-graph-indentation.sh b/t/t4218-log-graph-indentation.sh\nindex d4c850c0d4..b69730e7ba 100755\n--- a/t/t4218-log-graph-indentation.sh\n+++ b/t/t4218-log-graph-indentation.sh\n@@ -540,4 +540,56 @@ test_expect_success 'visual root cascading gets wrapped after 4 columns' '\n \tEOF\n '\n \n+test_expect_success '--no-graph-indent disables indentation' '\n+\tlib_test_check_graph --no-graph-indent _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\\EOF\n+\t* 67_A\n+\t* 66_A\n+\t* 65_A\n+\t* 64_A\n+\t* 63_A\n+\t* 62_A\n+\t* 61_A\n+\t* 60_A\n+\t* 59_A\n+\t* 58_B\n+\t* 58_A\n+\tEOF\n+'\n+\n+test_expect_success 'log.graphIndent config disables indentation' '\n+\ttest_config log.graphIndent false &&\n+\tlib_test_check_graph _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\\EOF\n+\t* 67_A\n+\t* 66_A\n+\t* 65_A\n+\t* 64_A\n+\t* 63_A\n+\t* 62_A\n+\t* 61_A\n+\t* 60_A\n+\t* 59_A\n+\t* 58_B\n+\t* 58_A\n+\tEOF\n+'\n+\n+test_expect_success '--graph-indent forces indentation when graph.indent is unset' '\n+\ttest_config log.graphIndent false &&\n+\tlib_test_check_graph --graph-indent _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\\EOF\n+\t* 67_A\n+\t  * 66_A\n+\t    * 65_A\n+\t      * 64_A\n+\t* 63_A\n+\t  * 62_A\n+\t    * 61_A\n+\t      * 60_A\n+\t  * 59_A\n+\t* 58_B\n+\t* 58_A\n+\tEOF\n+'\n+\n+# graph.indent true and no --option is the default state.\n+\n test_done\n\n-- \n2.54.0\n"},{"id":"548032","messageId":"20260713-ps-pre-commit-indent-v11-5-dcb65bc4ba99@gmail.com","threadId":"65419","inReplyTo":"20260713-ps-pre-commit-indent-v11-0-dcb65bc4ba99@gmail.com","subject":"[PATCH v11 5/7] graph: wrap cascading commits after 4 columns","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-13T16:44:02Z","receivedAt":"2026-07-13T16:51:55Z","isPatch":true,"body":"Currently the visual root commits in a graph cascade indefinitely until\na commit which is not a visual root or the last commit appears.\nOn filters like --author where one author might contribute mostly on\nsingle patches this can become a visual issue.\n\nMake the cascading wrap after 4 columns.\n\nThere are two possible cases of the wrap:\n\n1. No ambiguity:\n\n* A\n  * B\n    * C\n      * D\n* E\n  * F\n\n2. Ambiguous conflict:\n\nIf F happens to not be a visual root and E gets wrapped back to the\ninitial column then E and F would be vertically adjacent. The solution\nis to forcefully indent E one level:\n\n* A\n  * B\n    * C\n      * D\n  * E\n* F\n* F\n\nThe magic number 4 comes as the minimum number of columns to wrap where\nthe output shows clearly the commits are unrelated and doesn't cause too\nmuch \"pyramid\" effects\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c                          | 22 +++++++++++++++++++++-\n t/t4218-log-graph-indentation.sh | 29 +++++++++++++++++++++++++++++\n 2 files changed, 50 insertions(+), 1 deletion(-)\n\ndiff --git a/graph.c b/graph.c\nindex 087094189f..e3e206170c 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -1042,6 +1042,23 @@ void graph_update(struct git_graph *graph, struct commit *commit)\n \t\t */\n \t\tif (!graph->visual_root_depth && flags.is_next_visual_root)\n \t\t\tgraph->visual_root_cascade = 1;\n+\n+\t\t/*\n+\t\t * We wrap the cascading at a max of four columns at most, after\n+\t\t * that we wrap it back to the initial column.\n+\t\t *\n+\t\t * This could cause ambiguity in case of the next commit not\n+\t\t * being a visual root and be at the initial column after the\n+\t\t * first wrap.\n+\t\t *\n+\t\t * In case of being a non-visual-root the next, stop the\n+\t\t * cascading to get the commit indented.\n+\t\t */\n+\t\tif (!flags.is_next_visual_root &&\n+\t\t    graph->visual_root_depth &&\n+\t\t    !(graph->visual_root_depth % 4))\n+\t\t\tgraph->visual_root_cascade = 0;\n+\n \t\tgraph->visual_root_depth++;\n \t} else {\n \t\tgraph->visual_root_depth = 0;\n@@ -1328,8 +1345,11 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \t\t\t\t * Each visual column is 2 characters wide.\n \t\t\t\t * Omit the indentation for the first visual\n \t\t\t\t * root in cascade mode.\n+\t\t\t\t *\n+\t\t\t\t * Have a max of 4 columns when cascading, after\n+\t\t\t\t * that wrap it and repeat.\n \t\t\t\t */\n-\t\t\t\tint padding = (depth - graph->visual_root_cascade) * 2;\n+\t\t\t\tint padding = ((depth - graph->visual_root_cascade) % 4) * 2;\n \t\t\t\tgraph_line_addchars(line, ' ', padding);\n \t\t\t\tgraph->width += padding;\n \t\t\t}\ndiff --git a/t/t4218-log-graph-indentation.sh b/t/t4218-log-graph-indentation.sh\nindex 60c7d84af7..d4c850c0d4 100755\n--- a/t/t4218-log-graph-indentation.sh\n+++ b/t/t4218-log-graph-indentation.sh\n@@ -511,4 +511,33 @@ test_expect_success '--grep skipped parent makes a visual root' '\n \tEOF\n '\n \n+# The cascading wraps after 4 columns and when wraping (column % 4 == 0) if the\n+# next is a non visual-root, force indentation to avoid an ambiguous graph\n+# (commit 59_A is forcefully indented)\n+test_expect_success 'visual root cascading gets wrapped after 4 columns' '\n+\tcreate_orphan _58 && test_commit 58_A && test_commit 58_B &&\n+\tcreate_orphan _59 && test_commit 59_A &&\n+\tcreate_orphan _60 && test_commit 60_A &&\n+\tcreate_orphan _61 && test_commit 61_A &&\n+\tcreate_orphan _62 && test_commit 62_A &&\n+\tcreate_orphan _63 && test_commit 63_A &&\n+\tcreate_orphan _64 && test_commit 64_A &&\n+\tcreate_orphan _65 && test_commit 65_A &&\n+\tcreate_orphan _66 && test_commit 66_A &&\n+\tcreate_orphan _67 && test_commit 67_A &&\n+\tlib_test_check_graph _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\\EOF\n+\t* 67_A\n+\t  * 66_A\n+\t    * 65_A\n+\t      * 64_A\n+\t* 63_A\n+\t  * 62_A\n+\t    * 61_A\n+\t      * 60_A\n+\t  * 59_A\n+\t* 58_B\n+\t* 58_A\n+\tEOF\n+'\n+\n test_done\n\n-- \n2.54.0\n"},{"id":"548037","messageId":"xmqqy0fews69.fsf@gitster.g","threadId":"65419","inReplyTo":"20260713-ps-pre-commit-indent-v11-0-dcb65bc4ba99@gmail.com","subject":"Re: [PATCH v11 0/7] graph: indent visual roots in graph","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-07-13T20:28:14Z","receivedAt":"2026-07-13T20:28:17Z","isPatch":true,"body":"Pablo Sabater <pabloosabaterr@gmail.com> writes:\n\n> V9 DIFF:\n>\n> - Changed boolean variables to be bit fields.\n\nv11???\n\n>\n> 7:  737331b68d ! 7:  c1fa81022e graph: add --[no-]graph-indent and log.graphIndent\n>     @@ revision.h: struct rev_info {\n>       \t/* Display history graph */\n>       \tstruct git_graph *graph;\n>       \tint graph_max_lanes;\n>     -+\tint no_graph_indent;\n>     -+\tunsigned int graph_indent_set;\n>     ++\tunsigned int no_graph_indent:1;\n>     ++\tunsigned int graph_indent_set:1;\n\nOK.  References to these occur primarily in a boolean context, and\nall assignments to them are either 0 or 1.\n\ngraph.c:442:\t\trevs->no_graph_indent = !val;\ngraph.c:1008:\t       !graph->revs->no_graph_indent;\ngraph.c:1353:\t\t\tif (graph->is_visual_root && !graph->revs->no_graph_indent) {\nrevision.c:2630:\t\trevs->no_graph_indent = 0;\nrevision.c:2631:\t\trevs->graph_indent_set = 1;\nrevision.c:2633:\t\trevs->no_graph_indent = 1;\nrevision.c:2634:\t\trevs->graph_indent_set = 1;\nrevision.c:3209:\tif (revs->graph_indent_set > 0 && !revs->graph)\n\nYou may want to rewrite the last conditional check to:\n\n\tif (revs->graph_indent_set && !revs->graph)\n\nThis avoids confusing readers into thinking the member can be set\nto 2 or greater.\n\nThanks.\n"},{"id":"548040","messageId":"DJXQO504VCLC.10N8335V7Z1LY@gmail.com","threadId":"65419","inReplyTo":"xmqqy0fews69.fsf@gitster.g","subject":"Re: [PATCH v11 0/7] graph: indent visual roots in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-13T20:51:19Z","receivedAt":"2026-07-13T20:51:22Z","isPatch":true,"body":"On Mon Jul 13, 2026 at 10:28 PM CEST, Junio C Hamano wrote:\n> Pablo Sabater <pabloosabaterr@gmail.com> writes:\n>\n>> V9 DIFF:\n>>\n>> - Changed boolean variables to be bit fields.\n>\n> v11???\n>\n\nMy bad, I updated it manually and I forgot to.\n\n>>\n>> 7:  737331b68d ! 7:  c1fa81022e graph: add --[no-]graph-indent and log.graphIndent\n>>     @@ revision.h: struct rev_info {\n>>       \t/* Display history graph */\n>>       \tstruct git_graph *graph;\n>>       \tint graph_max_lanes;\n>>     -+\tint no_graph_indent;\n>>     -+\tunsigned int graph_indent_set;\n>>     ++\tunsigned int no_graph_indent:1;\n>>     ++\tunsigned int graph_indent_set:1;\n>\n> OK.  References to these occur primarily in a boolean context, and\n> all assignments to them are either 0 or 1.\n>\n> graph.c:442:\t\trevs->no_graph_indent = !val;\n> graph.c:1008:\t       !graph->revs->no_graph_indent;\n> graph.c:1353:\t\t\tif (graph->is_visual_root && !graph->revs->no_graph_indent) {\n> revision.c:2630:\t\trevs->no_graph_indent = 0;\n> revision.c:2631:\t\trevs->graph_indent_set = 1;\n> revision.c:2633:\t\trevs->no_graph_indent = 1;\n> revision.c:2634:\t\trevs->graph_indent_set = 1;\n> revision.c:3209:\tif (revs->graph_indent_set > 0 && !revs->graph)\n>\n> You may want to rewrite the last conditional check to:\n>\n> \tif (revs->graph_indent_set && !revs->graph)\n>\n> This avoids confusing readers into thinking the member can be set\n> to 2 or greater.\n\nI'll do that. Thanks.\n\n>\n> Thanks.\n\nRegards,\nPablo.\n\n"},{"id":"548096","messageId":"CA+J6zkRXbW=bLQ8nDcbPwocetdi2JpyM_R5Gff6sMK-Gb_JGhw@mail.gmail.com","threadId":"65419","inReplyTo":"20260713-ps-pre-commit-indent-v11-7-dcb65bc4ba99@gmail.com","subject":"Re: [PATCH v11 7/7] graph: add --[no-]graph-indent and log.graphIndent","fromName":"Chandra Pratap","fromEmail":"chandrapratap3519@gmail.com","sentAt":"2026-07-14T10:19:28Z","receivedAt":"2026-07-14T10:19:58Z","isPatch":true,"body":"On Mon, 13 Jul 2026 at 22:14, Pablo Sabater <pabloosabaterr@gmail.com> wrote:\n>\n> Some users may prefer to not have graph indentation.\n>\n> Add \"log.graphIndent\" config variable to graph_read_config() to read the\n> default preference. By default is graph indentation is true.\n>\n> Add --graph-indent and --no-graph-indent options to overwrite the\n> default preference.\n>\n> Signed-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n> ---\n>  Documentation/config/log.adoc       |  4 +++\n>  Documentation/rev-list-options.adoc |  8 ++++++\n>  graph.c                             | 10 +++++--\n>  revision.c                          |  9 +++++++\n>  revision.h                          |  2 ++\n>  t/t4218-log-graph-indentation.sh    | 52 +++++++++++++++++++++++++++++++++++++\n>  6 files changed, 83 insertions(+), 2 deletions(-)\n>\n> diff --git a/Documentation/config/log.adoc b/Documentation/config/log.adoc\n> index 757a7be196..f7dfce69b5 100644\n> --- a/Documentation/config/log.adoc\n> +++ b/Documentation/config/log.adoc\n> @@ -59,6 +59,10 @@ This is the same as the `--decorate` option of the `git log`.\n>         A list of colors, separated by commas, that can be used to draw\n>         history lines in `git log --graph`.\n>\n> +`log.graphIndent`::\n> +       If `true`, indent visual roots when rendering the graphs with `--graph`.\n> +       Set true by default. It can be overriden with `--[no-]graph-indent`.\n> +\n>  `log.showRoot`::\n>         If true, the initial commit will be shown as a big creation event.\n>         This is equivalent to a diff against an empty tree.\n> diff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc\n> index eaee6ee839..af74f10bb4 100644\n> --- a/Documentation/rev-list-options.adoc\n> +++ b/Documentation/rev-list-options.adoc\n> @@ -1269,6 +1269,14 @@ This implies the `--topo-order` option by default, but the\n>         By default it is set to 0 (no limit), zero and negative values\n>         are ignored and treated as no limit.\n>\n> +`--no-graph-indent`::\n> +`--graph-indent`::\n> +       When used with `--graph`, indent visual roots (commits with no parents\n> +       or whose parents are not shown) to differentiate them from commits that\n> +       are vertically adjacent but unrelated. Enabled by default. Use\n> +       `--no-graph-indent` to disable or set `graph.indent` to set a deafault\n\ns/deafault/default\n\nAlso, I think you meant log.graphIndent instead of graph.indent here.\n\n[snip]\n> +test_expect_success '--no-graph-indent disables indentation' '\n> +       lib_test_check_graph --no-graph-indent _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\\EOF\n> +       * 67_A\n> +       * 66_A\n> +       * 65_A\n> +       * 64_A\n> +       * 63_A\n> +       * 62_A\n> +       * 61_A\n> +       * 60_A\n> +       * 59_A\n> +       * 58_B\n> +       * 58_A\n> +       EOF\n> +'\n> +\n> +test_expect_success 'log.graphIndent config disables indentation' '\n> +       test_config log.graphIndent false &&\n> +       lib_test_check_graph _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\\EOF\n> +       * 67_A\n> +       * 66_A\n> +       * 65_A\n> +       * 64_A\n> +       * 63_A\n> +       * 62_A\n> +       * 61_A\n> +       * 60_A\n> +       * 59_A\n> +       * 58_B\n> +       * 58_A\n> +       EOF\n> +'\n> +\n> +test_expect_success '--graph-indent forces indentation when graph.indent is unset' '\n> +       test_config log.graphIndent false &&\n> +       lib_test_check_graph --graph-indent _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\\EOF\n> +       * 67_A\n> +         * 66_A\n> +           * 65_A\n> +             * 64_A\n> +       * 63_A\n> +         * 62_A\n> +           * 61_A\n> +             * 60_A\n> +         * 59_A\n> +       * 58_B\n> +       * 58_A\n> +       EOF\n> +'\n> +\n> +# graph.indent true and no --option is the default state.\n\nSame thing here.\n"},{"id":"548112","messageId":"DJY9Q16CVG2G.GT6U9PD2CRD9@gmail.com","threadId":"65419","inReplyTo":"CA+J6zkRXbW=bLQ8nDcbPwocetdi2JpyM_R5Gff6sMK-Gb_JGhw@mail.gmail.com","subject":"Re: [PATCH v11 7/7] graph: add --[no-]graph-indent and log.graphIndent","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-14T11:47:08Z","receivedAt":"2026-07-14T11:47:12Z","isPatch":true,"body":"On Tue Jul 14, 2026 at 12:19 PM CEST, Chandra Pratap wrote:\n> On Mon, 13 Jul 2026 at 22:14, Pablo Sabater <pabloosabaterr@gmail.com> wrote:\n>>\n>> Some users may prefer to not have graph indentation.\n>>\n>> Add \"log.graphIndent\" config variable to graph_read_config() to read the\n>> default preference. By default is graph indentation is true.\n>>\n>> Add --graph-indent and --no-graph-indent options to overwrite the\n>> default preference.\n>>\n>> Signed-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n>> ---\n>>  Documentation/config/log.adoc       |  4 +++\n>>  Documentation/rev-list-options.adoc |  8 ++++++\n>>  graph.c                             | 10 +++++--\n>>  revision.c                          |  9 +++++++\n>>  revision.h                          |  2 ++\n>>  t/t4218-log-graph-indentation.sh    | 52 +++++++++++++++++++++++++++++++++++++\n>>  6 files changed, 83 insertions(+), 2 deletions(-)\n>>\n>> diff --git a/Documentation/config/log.adoc b/Documentation/config/log.adoc\n>> index 757a7be196..f7dfce69b5 100644\n>> --- a/Documentation/config/log.adoc\n>> +++ b/Documentation/config/log.adoc\n>> @@ -59,6 +59,10 @@ This is the same as the `--decorate` option of the `git log`.\n>>         A list of colors, separated by commas, that can be used to draw\n>>         history lines in `git log --graph`.\n>>\n>> +`log.graphIndent`::\n>> +       If `true`, indent visual roots when rendering the graphs with `--graph`.\n>> +       Set true by default. It can be overriden with `--[no-]graph-indent`.\n>> +\n>>  `log.showRoot`::\n>>         If true, the initial commit will be shown as a big creation event.\n>>         This is equivalent to a diff against an empty tree.\n>> diff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc\n>> index eaee6ee839..af74f10bb4 100644\n>> --- a/Documentation/rev-list-options.adoc\n>> +++ b/Documentation/rev-list-options.adoc\n>> @@ -1269,6 +1269,14 @@ This implies the `--topo-order` option by default, but the\n>>         By default it is set to 0 (no limit), zero and negative values\n>>         are ignored and treated as no limit.\n>>\n>> +`--no-graph-indent`::\n>> +`--graph-indent`::\n>> +       When used with `--graph`, indent visual roots (commits with no parents\n>> +       or whose parents are not shown) to differentiate them from commits that\n>> +       are vertically adjacent but unrelated. Enabled by default. Use\n>> +       `--no-graph-indent` to disable or set `graph.indent` to set a deafault\n>\n> s/deafault/default\n>\n> Also, I think you meant log.graphIndent instead of graph.indent here.\n\nYes, thanks, I'll fix it.\n\n>\n> [snip]\n>> +test_expect_success '--no-graph-indent disables indentation' '\n>> +       lib_test_check_graph --no-graph-indent _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\\EOF\n>> +       * 67_A\n>> +       * 66_A\n>> +       * 65_A\n>> +       * 64_A\n>> +       * 63_A\n>> +       * 62_A\n>> +       * 61_A\n>> +       * 60_A\n>> +       * 59_A\n>> +       * 58_B\n>> +       * 58_A\n>> +       EOF\n>> +'\n>> +\n>> +test_expect_success 'log.graphIndent config disables indentation' '\n>> +       test_config log.graphIndent false &&\n>> +       lib_test_check_graph _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\\EOF\n>> +       * 67_A\n>> +       * 66_A\n>> +       * 65_A\n>> +       * 64_A\n>> +       * 63_A\n>> +       * 62_A\n>> +       * 61_A\n>> +       * 60_A\n>> +       * 59_A\n>> +       * 58_B\n>> +       * 58_A\n>> +       EOF\n>> +'\n>> +\n>> +test_expect_success '--graph-indent forces indentation when graph.indent is unset' '\n>> +       test_config log.graphIndent false &&\n>> +       lib_test_check_graph --graph-indent _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\\EOF\n>> +       * 67_A\n>> +         * 66_A\n>> +           * 65_A\n>> +             * 64_A\n>> +       * 63_A\n>> +         * 62_A\n>> +           * 61_A\n>> +             * 60_A\n>> +         * 59_A\n>> +       * 58_B\n>> +       * 58_A\n>> +       EOF\n>> +'\n>> +\n>> +# graph.indent true and no --option is the default state.\n>\n> Same thing here.\n\nWill fix it.\n\nThanks for the review,\nPablo\n\n"},{"id":"548113","messageId":"20260714-ps-pre-commit-indent-v12-0-d50938e006df@gmail.com","threadId":"65419","inReplyTo":"20260713-ps-pre-commit-indent-v11-0-dcb65bc4ba99@gmail.com","subject":"[PATCH v12 0/7] graph: indent visual roots in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-14T12:09:31Z","receivedAt":"2026-07-14T12:09:43Z","isPatch":true,"body":"When rendering a graph, if the history contains multiple \"visual roots\",\nactual roots or commits that look like roots (i.e. have their parents\nfiltered out) can end up being vertically adjacent to unrelated commits,\nfalsely appearing to be related.\n\nA fix for this issue was already attempted [1] a while ago.\n\nThis series adds indentation to the visual root commits, so they cannot be\nvertically adjacent anymore making it easier to identify them.\n\nBefore indentation:\n\n\t* A\n\t* B1\n\t* B2\n\t* C1\n\t* C2\n\nAfter indentation:\n\n\t  * A\n\t* B1\n\t \\\n\t  * B2\n\t* C1\n\t* C2\n\nIndents the visual root commits that have still commits to show after\nthem, and if they have children it connects them with an edge at a new\nrow.\n\nIf there are multiple visual roots adjacent in history, the indentation\nstarts with the second one, avoiding redundant indentation of the first\none and cascades after the second.\n\n\t* A\n\t  * B\n\t    * C\n\t      * D\n\t* E\n\t  * F\n\t    * G\n\t      * H\n\t  * I\n\t* J1\n\t* J2\n\nThe indentation wraps after cascading columns and when wrapping back to\nthe initial column if the next commit is a non-visual-root commit, force\nthe indentation one extra level.\n\nSeries explanation:\n\n1. Cleanup to bring a common function from t4215 and t6016 that will be\n   used in t4218.\n\n2. Logic extraction of the chose of from where the commit source comes\n   from.\n\n3. Add a buffer for lookahead purposes.\n\n4. Principal commit. Implement the logic to get the visual roots\n   indented.\n\n5. Make visual root cascading wrap after 4 columns\n\n6. Add --[no-]graph-indent and log.graphIndent options.\n\nGitHub CI: https://github.com/pabloosabaterr/git/actions/runs/29331144667\n\n[1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n\nV11 DIFF:\n\n- Changed the check that required graph, to not confuse because it is a\n  boolean value.\n\n- Typos\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\nPablo Sabater (7):\n      lib-log-graph: move check_graph function\n      revision: add next_commit_to_show()\n      graph: add a 2 commit buffer for lookahead\n      graph: indent visual root in graph\n      graph: wrap cascading commits after 4 columns\n      graph: move config reading into graph_read_config()\n      graph: add --[no-]graph-indent and log.graphIndent\n\n Documentation/config/log.adoc              |   4 +\n Documentation/rev-list-options.adoc        |   8 +\n graph.c                                    | 332 +++++++++++++++-\n graph.h                                    |  17 +\n revision.c                                 |  57 ++-\n revision.h                                 |   2 +\n t/lib-log-graph.sh                         |   5 +\n t/meson.build                              |   1 +\n t/t4215-log-skewed-merges.sh               |  33 +-\n t/t4218-log-graph-indentation.sh           | 596 +++++++++++++++++++++++++++++\n t/t6016-rev-list-graph-simplify-history.sh |  25 +-\n 11 files changed, 1032 insertions(+), 48 deletions(-)\n\nRange-diff versus v11:\n\n1:  dd0bb0d215 = 1:  d754392142 lib-log-graph: move check_graph function\n2:  07e239533d = 2:  c93c2c0771 revision: add next_commit_to_show()\n3:  4d71f674a1 = 3:  70fe612ae1 graph: add a 2 commit buffer for lookahead\n4:  48ad2562f0 = 4:  e1ac06c4ea graph: indent visual root in graph\n5:  45be69d11b = 5:  ce52b41527 graph: wrap cascading commits after 4 columns\n6:  8ce53ae21b = 6:  9b7bb2cebc graph: move config reading into graph_read_config()\n7:  c1fa81022e ! 7:  13e830725f graph: add --[no-]graph-indent and log.graphIndent\n    @@ Documentation/rev-list-options.adoc: This implies the `--topo-order` option by d\n     +\tWhen used with `--graph`, indent visual roots (commits with no parents\n     +\tor whose parents are not shown) to differentiate them from commits that\n     +\tare vertically adjacent but unrelated. Enabled by default. Use\n    -+\t`--no-graph-indent` to disable or set `graph.indent` to set a deafault\n    -+\tpreference.\n    ++\t`--no-graph-indent` to disable or set `log.graphIndent` to set a\n    ++\tdefault preference.\n     +\n      ifdef::git-rev-list[]\n      `--count`::\n    @@ revision.c: int setup_revisions(int argc, const char **argv, struct rev_info *re\n      \tif (revs->graph_max_lanes > 0 && !revs->graph)\n      \t\tdie(_(\"the option '%s' requires '%s'\"), \"--graph-lane-limit\", \"--graph\");\n\n    -+\tif (revs->graph_indent_set > 0 && !revs->graph)\n    ++\tif (revs->graph_indent_set && !revs->graph)\n     +\t\tdie(_(\"the option '%s' requires '%s'\"), \"--[no-]graph-indent\", \"--graph\");\n     +\n      \tif (!revs->reflog_info && revs->grep_filter.use_reflog_filter)\n    @@ t/t4218-log-graph-indentation.sh: test_expect_success 'visual root cascading get\n     +\tEOF\n     +'\n     +\n    -+# graph.indent true and no --option is the default state.\n    ++# log.graphIndent unset and no --option (which activates graph indentation) is\n    ++# the default state.\n     +\n      test_done\n\n---\nbase-commit: f60db8d575adb79761d363e026fb49bddf330c73\n"},{"id":"548114","messageId":"20260714-ps-pre-commit-indent-v12-1-d50938e006df@gmail.com","threadId":"65419","inReplyTo":"20260714-ps-pre-commit-indent-v12-0-d50938e006df@gmail.com","subject":"[PATCH v12 1/7] lib-log-graph: move check_graph function","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-14T12:09:32Z","receivedAt":"2026-07-14T12:09:44Z","isPatch":true,"body":"check_graph is a function shared in the test files t4215 and t6016 used\nto format the output graph, but instead of being in a file called by\nboth test, the function code is repeated in each file.\n\nMove check_graph to lib-log-graph.sh file which both tests already\nimport graph functions from, renaming it to lib_test_check_graph.\n\nThis function is needed for the following commit which includes graph\ntests in a new file and requires check_graph.\n\nMentored-by: Karthik Nayak <karthik.188@gmail.com>\nMentored-by: Chandra Pratap <chandrapratap3519@gmail.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n t/lib-log-graph.sh                         |  5 +++++\n t/t4215-log-skewed-merges.sh               | 33 +++++++++++++-----------------\n t/t6016-rev-list-graph-simplify-history.sh | 25 +++++++++-------------\n 3 files changed, 29 insertions(+), 34 deletions(-)\n\ndiff --git a/t/lib-log-graph.sh b/t/lib-log-graph.sh\nindex bf952ef920..1eae8f60c2 100644\n--- a/t/lib-log-graph.sh\n+++ b/t/lib-log-graph.sh\n@@ -26,3 +26,8 @@ lib_test_cmp_colored_graph () {\n \ttest_decode_color <output.colors.raw | sed \"s/ *\\$//\" >output.colors &&\n \ttest_cmp expect.colors output.colors\n }\n+\n+lib_test_check_graph () {\n+\tcat >expect &&\n+\tlib_test_cmp_graph --format=%s \"$@\"\n+}\ndiff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh\nindex 1612f05f1b..eebab71039 100755\n--- a/t/t4215-log-skewed-merges.sh\n+++ b/t/t4215-log-skewed-merges.sh\n@@ -5,11 +5,6 @@ test_description='git log --graph of skewed merges'\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-log-graph.sh\n \n-check_graph () {\n-\tcat >expect &&\n-\tlib_test_cmp_graph --format=%s \"$@\"\n-}\n-\n test_expect_success 'log --graph with merge fusing with its left and right neighbors' '\n \tgit checkout --orphan _p &&\n \ttest_commit A &&\n@@ -21,7 +16,7 @@ test_expect_success 'log --graph with merge fusing with its left and right neigh\n \tgit checkout _p && git merge --no-ff _r -m G &&\n \tgit checkout @^^ && git merge --no-ff _p -m H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   H\n \t|\\\n \t| *   G\n@@ -49,7 +44,7 @@ test_expect_success 'log --graph with left-skewed merge' '\n \tgit checkout 0_p && git merge --no-ff 0_s -m 0_G &&\n \tgit checkout @^ && git merge --no-ff 0_q 0_r 0_t 0_p -m 0_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*-----.   0_H\n \t|\\ \\ \\ \\\n \t| | | | * 0_G\n@@ -83,7 +78,7 @@ test_expect_success 'log --graph with nested left-skewed merge' '\n \tgit checkout 1_p && git merge --no-ff 1_r -m 1_G &&\n \tgit checkout @^^ && git merge --no-ff 1_p -m 1_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   1_H\n \t|\\\n \t| *   1_G\n@@ -115,7 +110,7 @@ test_expect_success 'log --graph with nested left-skewed merge following normal\n \tgit checkout -b 2_s @^^ && git merge --no-ff 2_q -m 2_J &&\n \tgit checkout 2_p && git merge --no-ff 2_s -m 2_K &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   2_K\n \t|\\\n \t| *   2_J\n@@ -151,7 +146,7 @@ test_expect_success 'log --graph with nested right-skewed merge following left-s\n \tgit checkout 3_p && git merge --no-ff 3_r -m 3_H &&\n \tgit checkout @^^ && git merge --no-ff 3_p -m 3_J &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   3_J\n \t|\\\n \t| *   3_H\n@@ -182,7 +177,7 @@ test_expect_success 'log --graph with right-skewed merge following a left-skewed\n \tgit merge --no-ff 4_p -m 4_G &&\n \tgit checkout @^^ && git merge --no-ff 4_s -m 4_H &&\n \n-\tcheck_graph --date-order <<-\\EOF\n+\tlib_test_check_graph --date-order <<-\\EOF\n \t*   4_H\n \t|\\\n \t| *   4_G\n@@ -218,7 +213,7 @@ test_expect_success 'log --graph with octopus merge with column joining its penu\n \tgit checkout 5_r &&\n \tgit merge --no-ff 5_s -m 5_H &&\n \n-\tcheck_graph <<-\\EOF\n+\tlib_test_check_graph <<-\\EOF\n \t*   5_H\n \t|\\\n \t| *-.   5_G\n@@ -257,7 +252,7 @@ test_expect_success 'log --graph with multiple tips' '\n \tgit checkout 6_1 &&\n \tgit merge --no-ff 6_2 -m 6_I &&\n \n-\tcheck_graph 6_1 6_3 6_5 <<-\\EOF\n+\tlib_test_check_graph 6_1 6_3 6_5 <<-\\EOF\n \t*   6_I\n \t|\\\n \t| | *   6_H\n@@ -334,7 +329,7 @@ test_expect_success 'log --graph with multiple tips' '\n \tgit checkout -b M_7 7_1 &&\n \tgit merge --no-ff 7_2 7_3 -m 7_M4 &&\n \n-\tcheck_graph M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -371,7 +366,7 @@ test_expect_success 'log --graph with multiple tips' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' '\n-\tcheck_graph --graph-lane-limit=2 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=2 M_7 <<-\\EOF\n \t*-.   7_M4\n \t|\\ \\\n \t| | * 7_G\n@@ -388,7 +383,7 @@ test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=1 truncate mid octopus merge' '\n-\tcheck_graph --graph-lane-limit=1 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=1 M_7 <<-\\EOF\n \t*-~  7_M4\n \t|\\~\n \t| ~ 7_G\n@@ -405,7 +400,7 @@ test_expect_success 'log --graph --graph-lane-limit=1 truncate mid octopus merge\n '\n \n test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' '\n-\tcheck_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -441,7 +436,7 @@ test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' '\n '\n \n test_expect_success 'log --graph --graph-lane-limit=6 check if it only shows first of 3 parent merge' '\n-\tcheck_graph --graph-lane-limit=6 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=6 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\n@@ -478,7 +473,7 @@ test_expect_success 'log --graph --graph-lane-limit=6 check if it only shows fir\n '\n \n test_expect_success 'log --graph --graph-lane-limit=7 check if it shows all 3 parent merge' '\n-\tcheck_graph --graph-lane-limit=7 M_1 M_3 M_5 M_7 <<-\\EOF\n+\tlib_test_check_graph --graph-lane-limit=7 M_1 M_3 M_5 M_7 <<-\\EOF\n \t*   7_M1\n \t|\\\n \t| | *   7_M2\ndiff --git a/t/t6016-rev-list-graph-simplify-history.sh b/t/t6016-rev-list-graph-simplify-history.sh\nindex 54b0a6f5f8..e0d9c3c1ac 100755\n--- a/t/t6016-rev-list-graph-simplify-history.sh\n+++ b/t/t6016-rev-list-graph-simplify-history.sh\n@@ -13,11 +13,6 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n . ./test-lib.sh\n . \"$TEST_DIRECTORY\"/lib-log-graph.sh\n \n-check_graph () {\n-\tcat >expect &&\n-\tlib_test_cmp_graph --format=%s \"$@\"\n-}\n-\n test_expect_success 'set up rev-list --graph test' '\n \t# 3 commits on branch A\n \ttest_commit A1 foo.txt &&\n@@ -54,7 +49,7 @@ test_expect_success 'set up rev-list --graph test' '\n '\n \n test_expect_success '--graph --all' '\n-\tcheck_graph --all <<-\\EOF\n+\tlib_test_check_graph --all <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -82,7 +77,7 @@ test_expect_success '--graph --all' '\n # that undecorated merges are interesting, even with --simplify-by-decoration\n test_expect_success '--graph --simplify-by-decoration' '\n \tgit tag -d A4 &&\n-\tcheck_graph --all --simplify-by-decoration <<-\\EOF\n+\tlib_test_check_graph --all --simplify-by-decoration <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -114,7 +109,7 @@ test_expect_success 'setup: get rid of decorations on B' '\n \n # Graph with branch B simplified away\n test_expect_success '--graph --simplify-by-decoration prune branch B' '\n-\tcheck_graph --simplify-by-decoration --all <<-\\EOF\n+\tlib_test_check_graph --simplify-by-decoration --all <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -133,7 +128,7 @@ test_expect_success '--graph --simplify-by-decoration prune branch B' '\n '\n \n test_expect_success '--graph --full-history -- bar.txt' '\n-\tcheck_graph --full-history --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --full-history --all -- bar.txt <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -148,7 +143,7 @@ test_expect_success '--graph --full-history -- bar.txt' '\n '\n \n test_expect_success '--graph --full-history --simplify-merges -- bar.txt' '\n-\tcheck_graph --full-history --simplify-merges --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --full-history --simplify-merges --all -- bar.txt <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -161,7 +156,7 @@ test_expect_success '--graph --full-history --simplify-merges -- bar.txt' '\n '\n \n test_expect_success '--graph -- bar.txt' '\n-\tcheck_graph --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --all -- bar.txt <<-\\EOF\n \t* A7\n \t* A5\n \t* A3\n@@ -172,7 +167,7 @@ test_expect_success '--graph -- bar.txt' '\n '\n \n test_expect_success '--graph --sparse -- bar.txt' '\n-\tcheck_graph --sparse --all -- bar.txt <<-\\EOF\n+\tlib_test_check_graph --sparse --all -- bar.txt <<-\\EOF\n \t* A7\n \t* A6\n \t* A5\n@@ -189,7 +184,7 @@ test_expect_success '--graph --sparse -- bar.txt' '\n '\n \n test_expect_success '--graph ^C4' '\n-\tcheck_graph --all ^C4 <<-\\EOF\n+\tlib_test_check_graph --all ^C4 <<-\\EOF\n \t* A7\n \t* A6\n \t* A5\n@@ -202,7 +197,7 @@ test_expect_success '--graph ^C4' '\n '\n \n test_expect_success '--graph ^C3' '\n-\tcheck_graph --all ^C3 <<-\\EOF\n+\tlib_test_check_graph --all ^C3 <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n@@ -220,7 +215,7 @@ test_expect_success '--graph ^C3' '\n # that important, but this test depends on it.  If the ordering ever changes\n # in the code, we'll need to update this test.\n test_expect_success '--graph --boundary ^C3' '\n-\tcheck_graph --boundary --all ^C3 <<-\\EOF\n+\tlib_test_check_graph --boundary --all ^C3 <<-\\EOF\n \t* A7\n \t*   A6\n \t|\\\n\n-- \n2.54.0\n"},{"id":"548115","messageId":"20260714-ps-pre-commit-indent-v12-2-d50938e006df@gmail.com","threadId":"65419","inReplyTo":"20260714-ps-pre-commit-indent-v12-0-d50938e006df@gmail.com","subject":"[PATCH v12 2/7] revision: add next_commit_to_show()","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-14T12:09:33Z","receivedAt":"2026-07-14T12:09:45Z","isPatch":true,"body":"get_revision() gets its commits from two sources depending on the mode:\n\n1. Normally it gets the commits from get_revision_internal().\n\n2. --max-count-oldest which was introduced at bb4ce23284 (revision.c:\n   implement --max-count-oldest, 2026-05-19) gets the commits by popping\n   from a saved list at revs->commits marking SHOWN and CHILD_SHOWN on\n   each popped commit.\n\nExtract the choice logic into a helper, next_commit_to_show(), which\nreturns the next commit regardless of the source it comes from.\n\nThis has no change in behavior. The helper is needed in a subsequent\ncommit that pre-fetches two commits into a buffer for lookahead purposes\nand needs to pre-fetch from the same source.\n\nThe --reverse branch keeps its own pop loop. Using the helper for\n--reverse would additionally set SHOWN and CHILD_SHOWN which is not\ndesired and a behavior change.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n revision.c | 36 ++++++++++++++++++++++++------------\n 1 file changed, 24 insertions(+), 12 deletions(-)\n\ndiff --git a/revision.c b/revision.c\nindex 0c95edef59..288935943f 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -4658,12 +4658,34 @@ static void retrieve_oldest_commits(struct rev_info *revs,\n \t\tcommit_list_insert(c, queue);\n }\n \n+/*\n+ * Returns the next commit that will be shown, regardless of whether it comes\n+ * directly from the revision walk or from the list saved by the staged output\n+ * of --max-count-oldest.\n+ */\n+static struct commit *next_commit_to_show(struct rev_info *revs)\n+{\n+\tstruct commit *c;\n+\tstruct commit_list *p;\n+\n+\tif (!revs->max_count_stage)\n+\t\treturn get_revision_internal(revs);\n+\n+\tc = pop_commit(&revs->commits);\n+\tif (c) {\n+\t\tc->object.flags |= SHOWN;\n+\t\tif (!(c->object.flags & BOUNDARY))\n+\t\t\tfor (p = c->parents; p; p = p->next)\n+\t\t\t\tp->item->object.flags |= CHILD_SHOWN;\n+\t}\n+\treturn c;\n+}\n+\n struct commit *get_revision(struct rev_info *revs)\n {\n \tstruct commit *c;\n \tstruct commit_list *reversed;\n \tstruct commit_list *queue = NULL;\n-\tstruct commit_list *p;\n \n \tif (revs->max_count_type == 1 && !revs->max_count_stage) {\n \t\tretrieve_oldest_commits(revs, &queue);\n@@ -4693,17 +4715,7 @@ struct commit *get_revision(struct rev_info *revs)\n \t\treturn c;\n \t}\n \n-\tif (revs->max_count_stage) {\n-\t\tc = pop_commit(&revs->commits);\n-\t\tif (c) {\n-\t\t\tc->object.flags |= SHOWN;\n-\t\t\tif (!(c->object.flags & BOUNDARY))\n-\t\t\t\tfor (p = c->parents; p; p = p->next)\n-\t\t\t\t\tp->item->object.flags |= CHILD_SHOWN;\n-\t\t}\n-\t} else {\n-\t\tc = get_revision_internal(revs);\n-\t}\n+\tc = next_commit_to_show(revs);\n \n \tif (c && revs->graph)\n \t\tgraph_update(revs->graph, c);\n\n-- \n2.54.0\n"},{"id":"548116","messageId":"20260714-ps-pre-commit-indent-v12-3-d50938e006df@gmail.com","threadId":"65419","inReplyTo":"20260714-ps-pre-commit-indent-v12-0-d50938e006df@gmail.com","subject":"[PATCH v12 3/7] graph: add a 2 commit buffer for lookahead","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-14T12:09:34Z","receivedAt":"2026-07-14T12:09:47Z","isPatch":true,"body":"In a subsequent commit the graph renderer needs to know if the next\ncommit is a visual root or if it is the last commit to be shown. This\nrequires peeking 2 commits ahead.\n\nCommits are pre-fetched in get_revision() through next_commit_to_show()\nwhere they are also marked as SHOWN, regardless the source they come\nfrom.\n\nUpdate graph_is_interesting() so it considers commits inside the\nlookahead buffer as interesting as well.\n\nHelped-by: Kristofer Karlsson <krka@spotify.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c    | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++\n graph.h    | 17 +++++++++++++++++\n revision.c | 18 ++++++++++++++++--\n 3 files changed, 84 insertions(+), 2 deletions(-)\n\ndiff --git a/graph.c b/graph.c\nindex 842282685f..89ebcf7540 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -315,6 +315,14 @@ struct git_graph {\n \t * diff_output_prefix_callback().\n \t */\n \tstruct strbuf prefix_buf;\n+\n+\t/*\n+\t * Lookahead buffer: up to 2 pre-fetched commits that will be shown.\n+\t * Populated by get_revision() so graph_peek_next_visible() can use\n+\t * actual walk results instead of peeking at rev_info internals.\n+\t */\n+\tstruct commit *lookahead[2];\n+\tint lookahead_nr;\n };\n \n static inline int graph_needs_truncation(struct git_graph *graph, int lane)\n@@ -388,6 +396,9 @@ struct git_graph *graph_init(struct rev_info *opt)\n \tgraph->num_columns = 0;\n \tgraph->num_new_columns = 0;\n \tgraph->mapping_size = 0;\n+\tgraph->lookahead[0] = NULL;\n+\tgraph->lookahead[1] = NULL;\n+\tgraph->lookahead_nr = 0;\n \t/*\n \t * Start the column color at the maximum value, since we'll\n \t * always increment it for the first commit we output.\n@@ -456,6 +467,15 @@ static void graph_ensure_capacity(struct git_graph *graph, int num_columns)\n  */\n static int graph_is_interesting(struct git_graph *graph, struct commit *commit)\n {\n+\t/*\n+\t * Commits in the lookahead buffer have been pre-fetched by\n+\t * get_revision() and will be shown in the future. They already have\n+\t * the SHOWN flag set when they were pre-fetched but the graph still\n+\t * needs to treat them as interesting parents.\n+\t */\n+\tfor (int i = 0; i < graph->lookahead_nr; i++)\n+\t\tif (graph->lookahead[i] == commit)\n+\t\t\treturn 1;\n \t/*\n \t * If revs->boundary is set, commits whose children have\n \t * been shown are always interesting, even if they have the\n@@ -763,6 +783,37 @@ static int graph_needs_pre_commit_line(struct git_graph *graph)\n \t       graph->expansion_row < graph_num_expansion_rows(graph);\n }\n \n+struct commit *graph_pop_lookahead(struct git_graph *graph)\n+{\n+\tstruct commit *c;\n+\n+\tif (!graph->lookahead_nr)\n+\t\treturn NULL;\n+\n+\tc = graph->lookahead[0];\n+\tif (!c)\n+\t\tBUG(\"lookahead buffer has %d entries but the first one is NULL\",\n+\t\t    graph->lookahead_nr);\n+\n+\tgraph->lookahead[0] = graph->lookahead[1];\n+\tgraph->lookahead[1] = NULL;\n+\tgraph->lookahead_nr--;\n+\treturn c;\n+}\n+\n+int graph_get_lookahead_room(struct git_graph *graph)\n+{\n+\treturn (int)ARRAY_SIZE(graph->lookahead) - graph->lookahead_nr;\n+}\n+\n+void graph_push_lookahead(struct git_graph *graph, struct commit *c)\n+{\n+\tif (!graph_get_lookahead_room(graph))\n+\t\tBUG(\"pushing into lookahead buffer when it is already full\");\n+\n+\tgraph->lookahead[graph->lookahead_nr++] = c;\n+}\n+\n void graph_update(struct git_graph *graph, struct commit *commit)\n {\n \tstruct commit_list *parent;\ndiff --git a/graph.h b/graph.h\nindex 3fd1dcb2e9..1193711fb8 100644\n--- a/graph.h\n+++ b/graph.h\n@@ -262,4 +262,21 @@ void graph_show_commit_msg(struct git_graph *graph,\n \t\t\t   FILE *file,\n \t\t\t   struct strbuf const *sb);\n \n+/*\n+ * Pop the first commit from the graph's lookahead buffer.\n+ * Returns NULL if the buffer is empty.\n+ */\n+struct commit *graph_pop_lookahead(struct git_graph *graph);\n+\n+/*\n+ * Returns how many more commits can be added to the lookahead buffer.\n+ */\n+int graph_get_lookahead_room(struct git_graph *graph);\n+\n+/*\n+ * Push a commit into the lookahead buffer. Must only be called when\n+ * graph_get_lookahead_room() returns > 0.\n+ */\n+void graph_push_lookahead(struct git_graph *graph, struct commit *c);\n+\n #endif /* GRAPH_H */\ndiff --git a/revision.c b/revision.c\nindex 288935943f..258c3cf782 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -4715,10 +4715,24 @@ struct commit *get_revision(struct rev_info *revs)\n \t\treturn c;\n \t}\n \n-\tc = next_commit_to_show(revs);\n+\tif (revs->graph) {\n+\t\tc = graph_pop_lookahead(revs->graph);\n+\t\tif (!c)\n+\t\t\tc = next_commit_to_show(revs);\n+\t} else {\n+\t\tc = next_commit_to_show(revs);\n+\t}\n \n-\tif (c && revs->graph)\n+\tif (c && revs->graph) {\n+\t\twhile (graph_get_lookahead_room(revs->graph)) {\n+\t\t\tstruct commit *next = next_commit_to_show(revs);\n+\t\t\tif (!next)\n+\t\t\t\tbreak;\n+\t\t\tgraph_push_lookahead(revs->graph, next);\n+\t\t}\n \t\tgraph_update(revs->graph, c);\n+\t}\n+\n \tif (!c) {\n \t\tfree_saved_parents(revs);\n \t\tcommit_list_free(revs->previous_parents);\n\n-- \n2.54.0\n"},{"id":"548118","messageId":"20260714-ps-pre-commit-indent-v12-4-d50938e006df@gmail.com","threadId":"65419","inReplyTo":"20260714-ps-pre-commit-indent-v12-0-d50938e006df@gmail.com","subject":"[PATCH v12 4/7] graph: indent visual root in graph","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-14T12:09:35Z","receivedAt":"2026-07-14T12:09:48Z","isPatch":true,"body":"When rendering a graph, if the history contains multiple \"visual roots\",\nactual roots or commits that look like roots (i.e. have their parents\nfiltered out) can end up being vertically adjacent to unrelated commits,\nfalsely appearing to be related.\n\nA fix for this issue was already attempted [1] a while ago.\n\nThis happens because the commits fill the space from left to right and\nwhen a visual root ends, its column becomes free for the following\ncommit even if they are not related. Once this happens the unrelated\ncommit is rendered below the visual root. Because there is no special\ncharacter or way to identify when a visual root is rendered making the\ngraph confusing.\n\nBy indenting the visual roots when there are still commits to show the\nvertical adjacency can be avoided.\n\nAdd is_visual_root flag to git_graph making it visible in all graph states,\ngive graph_update() a new function, graph_is_visual_root() to know if the\ncurrent commit is a visual root and set is_visual_root.\nThe different handled cases are:\n\n- If a visual root has children: similar to GRAPH_PRE_COMMIT state when\n  octopus merges need space, an edge row needs to be printed to connect\n  the child with the indented visual root. A new state GRAPH_PRE_ROOT is\n  needed to connect the child with the visual root:\n\n    * child of the visual root\n     \\ GRAPH_PRE_ROOT\n      * visual root indented\n\n- If a visual root is child-less we can skip GRAPH_PRE_ROOT state and\n  render the indented commit directly.\n\n      * visual root indented\n    * unrelated commit\n\n- If two or more visual roots are adjacent: by having a lookahead to the\n  next commit that will be rendered, if the next commit is also a visual\n  root and we are on a visual root, meaning two visual root adjacent in\n  the history, the top one can omit the indent, making the one below to\n  indent only once, if there are more adjacent visual commits, the\n  indentation will increase for each adjacent one, cascading.\n\n    * visual root\n      * visual root\n        * visual root\n    * last commit\n\n  Even if the last commit is a root, because there is nothing that will be\n  rendered below we can omit the indentation on purpose.\n\n[1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n\nHelped-by: Kristofer Karlsson <krka@spotify.com>\nMentored-by: Karthik Nayak <karthik.188@gmail.com>\nMentored-by: Chandra Pratap <chandrapratap3519@gmail.com>\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c                          | 244 +++++++++++++++++++\n t/meson.build                    |   1 +\n t/t4218-log-graph-indentation.sh | 514 +++++++++++++++++++++++++++++++++++++++\n 3 files changed, 759 insertions(+)\n\ndiff --git a/graph.c b/graph.c\nindex 89ebcf7540..087094189f 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -60,12 +60,23 @@ struct column {\n \t * index into column_colors.\n \t */\n \tunsigned short color;\n+\t/*\n+\t * Marks if a commit is a non-first parent of a merge. These columns are\n+\t * already visually connected to the merge commit and do not need\n+\t * indentation.\n+\t *\n+\t * The first parent is the one that inherits the column and it can need\n+\t * indentation if turns out to be a visual root and there's still\n+\t * commits to render.\n+\t */\n+\tunsigned int is_merge_parent:1;\n };\n \n enum graph_state {\n \tGRAPH_PADDING,\n \tGRAPH_SKIP,\n \tGRAPH_PRE_COMMIT,\n+\tGRAPH_PRE_ROOT,\n \tGRAPH_COMMIT,\n \tGRAPH_POST_MERGE,\n \tGRAPH_COLLAPSING\n@@ -323,6 +334,51 @@ struct git_graph {\n \t */\n \tstruct commit *lookahead[2];\n \tint lookahead_nr;\n+\n+\t/*\n+\t * If a commit is a visual root, we need to indent it to prevent\n+\t * unrelated commits from being vertically adjacent to it.\n+\t */\n+\tunsigned int is_visual_root:1;\n+\n+\t/*\n+\t * Indentation increases for each visual root adjacent to another visual\n+\t * root, making visual root commits indentation cascade.\n+\t */\n+\tunsigned int visual_root_depth;\n+\n+\t/*\n+\t * When a visual root is adjacent to other visual roots, the first one\n+\t * can avoid indentation and the rest cascades, increasing the indentation\n+\t * for each one.\n+\t */\n+\tunsigned int visual_root_cascade:1;\n+\n+\t/*\n+\t * Set when the current commit was already present in graph->columns\n+\t * before being processed.\n+\t */\n+\tunsigned int commit_in_columns:1;\n+};\n+\n+struct graph_lookahead_flags {\n+\n+\t/*\n+\t * Set when there will be a commit after the current one that will be\n+\t * rendered.\n+\t */\n+\tunsigned int is_next_visible:1;\n+\n+\t/*\n+\t * Set when the next visible commit is candidate to be a visual root.\n+\t */\n+\tunsigned int is_next_visual_root:1;\n+\n+\t/*\n+\t * Set when the next visible commit will be rendered under the current\n+\t * commit.\n+\t */\n+\tunsigned int next_has_column:1;\n };\n \n static inline int graph_needs_truncation(struct git_graph *graph, int lane)\n@@ -399,6 +455,8 @@ struct git_graph *graph_init(struct rev_info *opt)\n \tgraph->lookahead[0] = NULL;\n \tgraph->lookahead[1] = NULL;\n \tgraph->lookahead_nr = 0;\n+\tgraph->visual_root_depth = 0;\n+\tgraph->visual_root_cascade = 0;\n \t/*\n \t * Start the column color at the maximum value, since we'll\n \t * always increment it for the first commit we output.\n@@ -581,6 +639,11 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t\t\t\t\t  struct commit *commit,\n \t\t\t\t\t  int idx)\n {\n+\t/*\n+\t * Get the initial merge_layout before it's modified to know if this\n+\t * is a merge.\n+\t */\n+\tint initial_merge_layout = graph->merge_layout;\n \tint i = graph_find_new_column_by_commit(graph, commit);\n \tint mapping_idx;\n \n@@ -592,6 +655,7 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t\ti = graph->num_new_columns++;\n \t\tgraph->new_columns[i].commit = commit;\n \t\tgraph->new_columns[i].color = graph_find_commit_color(graph, commit);\n+\t\tgraph->new_columns[i].is_merge_parent = 0;\n \t}\n \n \tif (graph->num_parents > 1 && idx > -1 && graph->merge_layout == -1) {\n@@ -630,6 +694,12 @@ static void graph_insert_into_new_columns(struct git_graph *graph,\n \t}\n \n \tgraph->mapping[mapping_idx] = i;\n+\n+\t/*\n+\t * Mark non-first parents of a merge.\n+\t */\n+\tif (graph->num_parents > 1 && initial_merge_layout >= 0 && idx > -1)\n+\t\tgraph->new_columns[i].is_merge_parent = 1;\n }\n \n static void graph_update_columns(struct git_graph *graph)\n@@ -721,10 +791,20 @@ static void graph_update_columns(struct git_graph *graph)\n \t\t\tif (graph->num_parents == 0)\n \t\t\t\tgraph->width += 2;\n \t\t} else {\n+\t\t\tint j;\n \t\t\tgraph_insert_into_new_columns(graph, col_commit, -1);\n+\t\t\t/*\n+\t\t\t * This column is not the current commit, but we need to\n+\t\t\t * propagate the flag until the commit is processed.\n+\t\t\t */\n+\t\t\tj = graph_find_new_column_by_commit(graph, col_commit);\n+\t\t\tif (j >= 0 && graph->columns[i].is_merge_parent)\n+\t\t\t\tgraph->new_columns[j].is_merge_parent = 1;\n \t\t}\n \t}\n \n+\tgraph->commit_in_columns = is_commit_in_columns;\n+\n \t/*\n \t * If graph_max_lanes is set, cap the width\n \t */\n@@ -814,9 +894,113 @@ void graph_push_lookahead(struct git_graph *graph, struct commit *c)\n \tgraph->lookahead[graph->lookahead_nr++] = c;\n }\n \n+/*\n+ * A commit can be a visual root when:\n+ *\n+ * - It has no parents.\n+ *\n+ * - It has parents but they are all filtered out and\n+ *   commit->parents arrives NULL.\n+ *\n+ * - Its parents are uninteresting.\n+ *\n+ * - It is not a boundary commit. Boundary commits also have no visible\n+ *   parents, but they are not selected as visual roots because they cannot\n+ *   cause the ambiguity of being vertically adjacent because:\n+ *\n+ *   1. A boundary only appears because an included commit is its child.\n+ *      Children are always above, and the renderer draws an edge down to\n+ *      the boundary from that child. Rather than starting a column like a\n+ *      visual root would do, it inherits its child column.\n+ *\n+ *   2. Included commits cannot appear below a boundary. Boundaries are\n+ *      ancestors of the exclusion point; if an included commit were an\n+ *      ancestor of the boundary it would be excluded and not rendered.\n+ *      Boundaries therefore always sink to the bottom.\n+ */\n+static int graph_is_visual_root_candidate(struct commit *c, struct git_graph *graph)\n+{\n+\tstruct commit_list *p;\n+\n+\tif (c->object.flags & BOUNDARY)\n+\t\treturn 0;\n+\tfor (p = c->parents; p; p = p->next)\n+\t\tif (graph_is_interesting(graph, p->item))\n+\t\t\treturn 0;\n+\treturn 1;\n+}\n+\n+static int graph_is_visual_root(struct git_graph *graph,\n+\t\t\t\tstruct graph_lookahead_flags *flags)\n+{\n+\t/*\n+\t * This must be only called for the current commit as graph contains\n+\t * the state for the current commit only.\n+\t *\n+\t * To check if a commit is a visual root, call graph_is_visual_root_candidate()\n+\t * but we won't know if it is really a visual root until we get to the\n+\t * next commit state.\n+\t *\n+\t * The current commit is an actual visual root if it is a candidate and\n+\t * the commit is not a non-first parent of a merge.\n+\t *\n+\t *   *\n+\t *   |\\\n+\t *   | *    <- it is a visual root candidate but it shouldn't be indented\n+\t *   *         because it is already connected by an edge.\n+\t *   ^         if commit_in_columns && is_merge_parent means the commit\n+\t *   |         was put by a merge and is connected.\n+\t *   |\n+\t *   `-------- if !is_next_visible means we're on the last commit, avoid\n+\t *             indentation unless the one before is a visual root, then\n+\t *             we need to differentiate from the one above.\n+\t *\n+\t * If next_has_columns means that the next commit has\n+\t * already a column, so it will not be rendered below, the\n+\t * current commit has to act as the last commit and omit\n+\t * indentation.\n+\t */\n+\treturn graph_is_visual_root_candidate(graph->commit, graph) &&\n+\t       !(graph->commit_in_columns &&\n+\t\t graph->columns[graph->commit_index].is_merge_parent) &&\n+\t       flags->is_next_visible &&\n+\t       (!flags->next_has_column || graph->visual_root_depth > 0);\n+}\n+\n+/*\n+ * Peeks the next commits via the lookahead buffer and sets the lookahead flags.\n+ */\n+static void graph_peek_next_visible(struct git_graph *graph,\n+\t\t\t\t    struct graph_lookahead_flags *flags)\n+{\n+\tflags->is_next_visible = 0;\n+\tflags->is_next_visual_root = 0;\n+\tflags->next_has_column = 0;\n+\n+\tif (!graph->lookahead_nr)\n+\t\treturn;\n+\n+\tflags->is_next_visible = 1;\n+\tflags->next_has_column =\n+\t\tgraph_find_new_column_by_commit(graph, graph->lookahead[0]) >= 0;\n+\n+\tif (!graph_is_visual_root_candidate(graph->lookahead[0], graph))\n+\t\treturn;\n+\n+\tif (graph->lookahead_nr >= 2)\n+\t\tflags->is_next_visual_root = 1;\n+}\n+\n+static int graph_needs_pre_root_line(struct git_graph *graph)\n+{\n+\treturn graph->commit_in_columns && graph->is_visual_root &&\n+\t       graph->num_columns > 0 && !graph->visual_root_cascade;\n+}\n+\n void graph_update(struct git_graph *graph, struct commit *commit)\n {\n \tstruct commit_list *parent;\n+\tstruct graph_lookahead_flags flags;\n \n \t/*\n \t * Set the new commit\n@@ -847,6 +1031,23 @@ void graph_update(struct git_graph *graph, struct commit *commit)\n \t */\n \tgraph_update_columns(graph);\n \n+\tgraph_peek_next_visible(graph, &flags);\n+\n+\tgraph->is_visual_root = graph_is_visual_root(graph, &flags);\n+\n+\tif (graph->is_visual_root) {\n+\t\t/*\n+\t\t * If next is a visual root we can omit the indent for the first\n+\t\t * visual root and start cascading.\n+\t\t */\n+\t\tif (!graph->visual_root_depth && flags.is_next_visual_root)\n+\t\t\tgraph->visual_root_cascade = 1;\n+\t\tgraph->visual_root_depth++;\n+\t} else {\n+\t\tgraph->visual_root_depth = 0;\n+\t\tgraph->visual_root_cascade = 0;\n+\t}\n+\n \tgraph->expansion_row = 0;\n \n \t/*\n@@ -864,11 +1065,16 @@ void graph_update(struct git_graph *graph, struct commit *commit)\n \t * room for it.  We need to do this only if there is a branch row\n \t * (or more) to the right of this commit.\n \t *\n+\t * If it is a visual root, we need to print an extra row to\n+\t * connect the indentation.\n+\t *\n \t * If there are less than 3 parents, we can immediately print the\n \t * commit line.\n \t */\n \tif (graph->state != GRAPH_PADDING)\n \t\tgraph->state = GRAPH_SKIP;\n+\telse if (graph_needs_pre_root_line(graph))\n+\t\tgraph->state = GRAPH_PRE_ROOT;\n \telse if (graph_needs_pre_commit_line(graph))\n \t\tgraph->state = GRAPH_PRE_COMMIT;\n \telse\n@@ -1116,6 +1322,17 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \n \t\tif (col_commit == graph->commit) {\n \t\t\tseen_this = 1;\n+\t\t\tif (graph->is_visual_root) {\n+\t\t\t\tint depth = graph->visual_root_depth;\n+\t\t\t\t/*\n+\t\t\t\t * Each visual column is 2 characters wide.\n+\t\t\t\t * Omit the indentation for the first visual\n+\t\t\t\t * root in cascade mode.\n+\t\t\t\t */\n+\t\t\t\tint padding = (depth - graph->visual_root_cascade) * 2;\n+\t\t\t\tgraph_line_addchars(line, ' ', padding);\n+\t\t\t\tgraph->width += padding;\n+\t\t\t}\n \t\t\tgraph_output_commit_char(graph, line);\n \n \t\t\tif (graph_needs_truncation(graph, i)) {\n@@ -1487,6 +1704,30 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n \t\tgraph_update_state(graph, GRAPH_PADDING);\n }\n \n+static void graph_output_pre_root_line(struct git_graph *graph, struct graph_line *line)\n+{\n+\t/*\n+\t * This function adds a row before a visual root, to connect the\n+\t * branch to the indented commit. It must only be called on a\n+\t * visual root.\n+\t */\n+\tif (!graph->is_visual_root)\n+\t\tBUG(\"commit must be a visual root to call pre_root_line\");\n+\n+\tfor (int i = 0; i < graph->num_columns; i++) {\n+\t\tstruct column *col = &graph->columns[i];\n+\t\tif (col->commit == graph->commit) {\n+\t\t\tgraph_line_addch(line, ' ');\n+\t\t\tgraph_line_write_column(line, col, '\\\\');\n+\t\t} else {\n+\t\t\tgraph_line_write_column(line, col, '|');\n+\t\t}\n+\t\tgraph_line_addch(line, ' ');\n+\t}\n+\n+\tgraph_update_state(graph, GRAPH_COMMIT);\n+}\n+\n int graph_next_line(struct git_graph *graph, struct strbuf *sb)\n {\n \tint shown_commit_line = 0;\n@@ -1512,6 +1753,9 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb)\n \tcase GRAPH_PRE_COMMIT:\n \t\tgraph_output_pre_commit_line(graph, &line);\n \t\tbreak;\n+\tcase GRAPH_PRE_ROOT:\n+\t\tgraph_output_pre_root_line(graph, &line);\n+\t\tbreak;\n \tcase GRAPH_COMMIT:\n \t\tgraph_output_commit_line(graph, &line);\n \t\tshown_commit_line = 1;\ndiff --git a/t/meson.build b/t/meson.build\nindex 7c3c070426..cce5ba71f9 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -577,6 +577,7 @@ integration_tests = [\n   't4215-log-skewed-merges.sh',\n   't4216-log-bloom.sh',\n   't4217-log-limit.sh',\n+  't4218-log-graph-indentation.sh',\n   't4219-log-follow-merge.sh',\n   't4252-am-options.sh',\n   't4253-am-keep-cr-dos.sh',\ndiff --git a/t/t4218-log-graph-indentation.sh b/t/t4218-log-graph-indentation.sh\nnew file mode 100755\nindex 0000000000..60c7d84af7\n--- /dev/null\n+++ b/t/t4218-log-graph-indentation.sh\n@@ -0,0 +1,514 @@\n+#!/bin/sh\n+\n+test_description='git log --graph visual root indentations'\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-log-graph.sh\n+\n+check_graph_with_description () {\n+\tcat >expect &&\n+\tlib_test_cmp_graph --format=\"%s%ndescription%nsecond-line\" \"$@\"\n+}\n+\n+create_orphan () {\n+\tgit checkout --orphan \"$1\" &&\n+\ttest_might_fail git rm -rf .\n+}\n+\n+# disable commit-graph topo order to have the graph to render in different\n+# ways (used in --first-parent tests to have multiple visual roots while a\n+# column is active at the same time).\n+unset_commit_graph () {\n+\tsane_unset GIT_TEST_COMMIT_GRAPH &&\n+\trm -f .git/objects/info/commit-graph &&\n+\trm -rf .git/objects/info/commit-graphs\n+}\n+\n+test_expect_success 'single root commit is not indented' '\n+\tcreate_orphan _1 && test_commit 1_A &&\n+\tlib_test_check_graph _1 <<-\\EOF\n+\t* 1_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indented before unrelated branch' '\n+\tcreate_orphan _2 && test_commit 2_A && test_commit 2_B &&\n+\tcreate_orphan _3 && test_commit 3_A &&\n+\tlib_test_check_graph _2 _3 <<-\\EOF\n+\t  * 3_A\n+\t* 2_B\n+\t* 2_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indentation with --left-right' '\n+\tlib_test_check_graph --left-right _2..._3 <<-\\EOF\n+\t  > 3_A\n+\t< 2_B\n+\t< 2_A\n+\tEOF\n+'\n+\n+# A better case of why indentation is still needed with '--left-right' flag is\n+# that unrelated branches can be on the same side, so it's needed to\n+# differentiate visual roots on the same side.\n+test_expect_success 'visual root indentation with --left-right having unrelated commits on the same side' '\n+\tlib_test_check_graph --left-right _2..._3 _1 <<-\\EOF\n+\t  > 3_A\n+\t< 2_B\n+\t \\\n+\t  < 2_A\n+\t> 1_A\n+\tEOF\n+'\n+\n+test_expect_success 'visual root indents the description also' '\n+\tcheck_graph_with_description _2 _3 <<-\\EOF\n+\t  * 3_A\n+\t    description\n+\t    second-line\n+\t* 2_B\n+\t| description\n+\t| second-line\n+\t* 2_A\n+\t  description\n+\t  second-line\n+\tEOF\n+'\n+\n+test_expect_success 'indented visual root parent gets connected to its child' '\n+\tcreate_orphan _4 && test_commit 4_A && test_commit 4_B &&\n+\tcreate_orphan _5 && test_commit 5_A && test_commit 5_B &&\n+\tlib_test_check_graph _4 _5 <<-\\EOF\n+\t* 5_B\n+\t \\\n+\t  * 5_A\n+\t* 4_B\n+\t* 4_A\n+\tEOF\n+'\n+\n+test_expect_success 'indented visual root parent gets connected to its child with description' '\n+\tcheck_graph_with_description _4 _5 <<-\\EOF\n+\t* 5_B\n+\t| description\n+\t| second-line\n+\t \\\n+\t  * 5_A\n+\t    description\n+\t    second-line\n+\t* 4_B\n+\t| description\n+\t| second-line\n+\t* 4_A\n+\t  description\n+\t  second-line\n+\tEOF\n+'\n+\n+test_expect_success 'visual roots cascade and last root does not' '\n+\tcreate_orphan _7 && test_commit 7_A && test_commit 7_B &&\n+\tcreate_orphan _8 && test_commit 8_A &&\n+\tcreate_orphan _9 && test_commit 9_A &&\n+\tcreate_orphan _10 && test_commit 10_A &&\n+\tlib_test_check_graph _7 _8 _9 _10 <<-\\EOF\n+\t* 10_A\n+\t  * 9_A\n+\t    * 8_A\n+\t* 7_B\n+\t* 7_A\n+\tEOF\n+'\n+\n+test_expect_success 'last root does not cascade' '\n+\tlib_test_check_graph _8 _9 _10 <<-\\EOF\n+\t* 10_A\n+\t  * 9_A\n+\t* 8_A\n+\tEOF\n+'\n+\n+test_expect_success 'merge parents are roots between them but they do not indent' '\n+\tcreate_orphan _11 && test_commit 11_A &&\n+\tcreate_orphan _12 && test_commit 12_A &&\n+\tcreate_orphan _13 && test_commit 13_A &&\n+\tgit checkout _11 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _11 -p _12 -p _13 -m 11_octopus) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _11 <<-\\EOF\n+\t*-.   11_octopus\n+\t|\\ \\\n+\t| | * 13_A\n+\t| * 12_A\n+\t* 11_A\n+\tEOF\n+'\n+\n+# The last parent of a merge can be indented if nothing related to it needs to\n+# be rendered after, if it's another visual root, merge parent must not get\n+# indented but rather activate cascading.\n+test_expect_success 'merge then unrelated visual root and unrelated branch' '\n+\tcreate_orphan _16 && test_commit 16_A && test_commit 16_B &&\n+\tcreate_orphan _17 && test_commit 17_A &&\n+\tcreate_orphan _18 && test_commit 18_A &&\n+\tcreate_orphan _19 && test_commit 19_A &&\n+\tcreate_orphan _20 && test_commit 20_A &&\n+\tgit checkout _18 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _18 -p _19 -p _20 -m 18_octopus) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _18 _17 _16 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t* 18_A\n+\t  * 17_A\n+\t* 16_B\n+\t* 16_A\n+\tEOF\n+'\n+\n+# The last commit root does not get indented, if the next thing after the root\n+# merge parent is the last commit, indent the merge parent.\n+test_expect_success 'merge then unrelated root indents merge parent' '\n+\tlib_test_check_graph _18 _17 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t \\\n+\t  * 18_A\n+\t* 17_A\n+\tEOF\n+'\n+\n+test_expect_success 'merge then unrelated branch indents merge parent' '\n+\tlib_test_check_graph _18 _16 <<-\\EOF\n+\t*-.   18_octopus\n+\t|\\ \\\n+\t| | * 20_A\n+\t| * 19_A\n+\t \\\n+\t  * 18_A\n+\t* 16_B\n+\t* 16_A\n+\tEOF\n+'\n+\n+test_expect_success 'two-parent merge of orphans' '\n+\tcreate_orphan _21 && test_commit 21_A &&\n+\tcreate_orphan _22 && test_commit 22_A &&\n+\tgit checkout _21 &&\n+\tTREE=$(git write-tree) &&\n+\tMERGE=$(git commit-tree $TREE -p _21 -p _22 -m 21_merge) &&\n+\tgit reset --hard $MERGE &&\n+\tlib_test_check_graph _21 <<-\\EOF\n+\t*   21_merge\n+\t|\\\n+\t| * 22_A\n+\t* 21_A\n+\tEOF\n+'\n+\n+test_expect_success 'commit with filtered parent becomes a visual root' '\n+\tcreate_orphan _23 &&\n+\techo test >other.txt &&\n+\tgit add other.txt &&\n+\tgit commit -m \"23_A\" &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"23_B\" &&\n+\tcreate_orphan _24 &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\tgit commit -m \"24_A\" &&\n+\tlib_test_check_graph _23 _24 -- foo.txt <<-\\EOF\n+\t  * 23_B\n+\t* 24_A\n+\tEOF\n+'\n+\n+test_expect_success 'filtered parent cascading edge case' '\n+\tcreate_orphan _27 &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\ttest_tick &&\n+\tgit commit -m \"D (last)\" &&\n+\n+\tcreate_orphan _25 &&\n+\techo test >other.txt &&\n+\tgit add other.txt &&\n+\ttest_tick &&\n+\tgit commit -m \"C-filtered\" &&\n+\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\ttest_tick &&\n+\tgit commit -m \"B (child of filtered)\" &&\n+\n+\tcreate_orphan _26 &&\n+\techo test >foo.txt &&\n+\tgit add foo.txt &&\n+\ttest_tick &&\n+\tgit commit -m \"A (visual root)\" &&\n+\n+\tlib_test_check_graph _25 _26 _27 -- foo.txt <<-\\EOF\n+\t* A (visual root)\n+\t  * B (child of filtered)\n+\t* D (last)\n+\tEOF\n+'\n+\n+test_expect_success 'multiple filtered parents in sequence' '\n+\tcreate_orphan _44 &&\n+\techo a >other.txt && git add other.txt && git commit -m \"44_F\" &&\n+\techo b >foo.txt && git add foo.txt && git commit -m \"44_C\" &&\n+\n+\tcreate_orphan _45 &&\n+\techo c >other.txt && git add other.txt && git commit -m \"45_F\" &&\n+\techo d >foo.txt && git add foo.txt && git commit -m \"45_C\" &&\n+\n+\tcreate_orphan _46 &&\n+\techo e >foo.txt && git add foo.txt && git commit -m \"46_A\" &&\n+\n+\tlib_test_check_graph _44 _45 _46 -- foo.txt <<-\\EOF\n+\t* 44_C\n+\t  * 45_C\n+\t* 46_A\n+\tEOF\n+'\n+\n+# These tests prove why there is no need to have indentation for boundary\n+# commits.\n+#\n+# Boundary commits rather than starting a column they 'inherit' the one of\n+# its child so there will always be an edge that connects it removing the\n+# ambiguity.\n+test_expect_success 'unrelated boundaries are not ambiguous' '\n+\tcreate_orphan _28 && test_commit 28_A && test_commit 28_B &&\n+\ttest_commit 28_C &&\n+\tcreate_orphan _29 && test_commit 29_A && test_commit 29_B &&\n+\tlib_test_check_graph --boundary 28_A.._28 29_A.._29 <<-\\EOF\n+\t* 29_B\n+\t| * 28_C\n+\t| * 28_B\n+\t| o 28_A\n+\to 29_A\n+\tEOF\n+'\n+\n+# Same structure as t6016\n+test_expect_success 'boundary commits big test' '\n+\t# 3 commits on branch _30\n+\tcreate_orphan _30 &&\n+\ttest_commit 30_A &&\n+\ttest_commit 30_B &&\n+\ttest_commit 30_C &&\n+\n+\t# 2 commits on branch _31, started from 30_A\n+\tgit checkout -b _31 30_A &&\n+\ttest_commit 31_A &&\n+\ttest_commit 31_B &&\n+\n+\t# 2 commits on branch _32, started from 30_B\n+\tgit checkout -b _32 30_B &&\n+\ttest_commit 32_A &&\n+\ttest_commit 32_B &&\n+\n+\t# Octopus merge _31 and _32 into -30\n+\tgit checkout _30 &&\n+\tgit merge _31 _32 -m 30_D &&\n+\tgit tag 30_D &&\n+\ttest_commit 30_E &&\n+\n+\t# More commits on _32, then merge _32 into _30\n+\tgit checkout _32 &&\n+\ttest_commit 32_C &&\n+\ttest_commit 32_D &&\n+\tgit checkout _30 &&\n+\tgit merge -s ours _32 -m 30_F &&\n+\tgit tag 30_F &&\n+\ttest_commit 30_G &&\n+\tlib_test_check_graph --boundary _30 _31 _32 ^32_C <<-\\EOF\n+\t* 30_G\n+\t*   30_F\n+\t|\\\n+\t| * 32_D\n+\t* | 30_E\n+\t| |\n+\t|  \\\n+\t*-. \\   30_D\n+\t|\\ \\ \\\n+\t| * | | 31_B\n+\t| * | | 31_A\n+\t* | | | 30_C\n+\to | | | 30_B\n+\t|/ / /\n+\to / / 30_A\n+\t / /\n+\t| o 32_C\n+\t|/\n+\to 32_B\n+\tEOF\n+'\n+\n+# Filter by --first-parent and then forcing the filtered parents to be shown.\n+test_expect_success '--first-parent flag with the filtered parents' '\n+\t(\n+\t\tunset_commit_graph &&\n+\t\tcreate_orphan _35 && test_commit 35_A && test_commit 35_B &&\n+\t\tcreate_orphan _36 && test_commit 36_A &&\n+\t\tcreate_orphan _37 && test_commit 37_A &&\n+\t\tgit checkout _35 &&\n+\t\tTREE=$(git write-tree) &&\n+\t\tMERGE=$(git commit-tree $TREE -p _35 -p _36 -p _37 -m 35_octopus) &&\n+\t\tgit reset --hard $MERGE &&\n+\t\tlib_test_check_graph --first-parent _35 _36 _37 <<-\\EOF\n+\t\t* 35_octopus\n+\t\t| * 37_A\n+\t\t|   * 36_A\n+\t\t* 35_B\n+\t\t* 35_A\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success '--first-parent with filtered parents but one has a child' '\n+\t(\n+\t\tunset_commit_graph &&\n+\t\tcreate_orphan _38 && test_commit 38_A && test_commit 38_B &&\n+\t\tcreate_orphan _39 && test_commit 39_A &&\n+\t\tcreate_orphan _40 && test_commit 40_A && test_commit 40_B &&\n+\t\tgit checkout _38 &&\n+\t\tTREE=$(git write-tree) &&\n+\t\tMERGE=$(git commit-tree $TREE -p _38 -p _39 -p _40 -m 38_octopus) &&\n+\t\tgit reset --hard $MERGE &&\n+\t\tlib_test_check_graph --first-parent _38 _39 _40 <<-\\EOF\n+\t\t* 38_octopus\n+\t\t| * 40_B\n+\t\t| * 40_A\n+\t\t|   * 39_A\n+\t\t* 38_B\n+\t\t* 38_A\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success '--first-parent with filtered parents but both have children' '\n+\t(\n+\t\tunset_commit_graph &&\n+\t\tcreate_orphan _41 && test_commit 41_A && test_commit 41_B &&\n+\t\tcreate_orphan _42 && test_commit 42_A && test_commit 42_B &&\n+\t\tcreate_orphan _43 && test_commit 43_A && test_commit 43_B &&\n+\t\tgit checkout _41 &&\n+\t\tTREE=$(git write-tree) &&\n+\t\tMERGE=$(git commit-tree $TREE -p _41 -p _42 -p _43 -m 41_octopus) &&\n+\t\tgit reset --hard $MERGE &&\n+\t\tlib_test_check_graph --first-parent _41 _42 _43 <<-\\EOF\n+\t\t* 41_octopus\n+\t\t| * 43_B\n+\t\t|  \\\n+\t\t|   * 43_A\n+\t\t| * 42_B\n+\t\t| * 42_A\n+\t\t* 41_B\n+\t\t* 41_A\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'two unrelated merges' '\n+\tcreate_orphan _50 && test_commit 50_A &&\n+\tgit checkout -b _51 &&\n+\ttest_commit 51_A && test_commit 51_B &&\n+\tgit checkout _50 &&\n+\tgit merge --no-ff _51 -m 50_B &&\n+\n+\tcreate_orphan _52 && test_commit 52_A &&\n+\tgit checkout -b _53 &&\n+\ttest_commit 53_A && test_commit 53_B &&\n+\tgit checkout _52 &&\n+\tgit merge --no-ff _53 -m 52_B &&\n+\n+\tlib_test_check_graph _52 _50 <<-\\EOF\n+\t*   52_B\n+\t|\\\n+\t| * 53_B\n+\t| * 53_A\n+\t|/\n+\t \\\n+\t  * 52_A\n+\t*   50_B\n+\t|\\\n+\t| * 51_B\n+\t| * 51_A\n+\t|/\n+\t* 50_A\n+\tEOF\n+'\n+\n+test_expect_success '--max-count treats the last visible commit as the last commit' '\n+\tlib_test_check_graph --max-count=2 _8 _9 _10 <<-\\EOF\n+\t  * 10_A\n+\t* 9_A\n+\tEOF\n+'\n+\n+test_expect_success '--max-count=1 shows a single root without indentation' '\n+\tlib_test_check_graph --max-count=1 _8 _9 _10 <<-\\EOF\n+\t* 10_A\n+\tEOF\n+'\n+\n+test_expect_success '--max-count-oldest indents visual roots' '\n+\tlib_test_check_graph --max-count-oldest=3 _8 _9 _10 <<-\\EOF\n+\t* 10_A\n+\t  * 9_A\n+\t* 8_A\n+\tEOF\n+'\n+\n+# when the graph commits are filtered with regex options like --author, the\n+# commit parents do not come NULL so it is needed to check if the parents are\n+# interesting.\n+test_expect_success '--author skipped parent makes a visual root' '\n+\tcreate_orphan _55 &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 55_A &&\n+\tcreate_orphan _54 &&\n+\ttest_tick &&\n+\tgit commit --allow-empty --author=\"Other <other@example.com>\" -m 54_A &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 54_B &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 54_C &&\n+\tlib_test_check_graph --author=\"A U Thor\" _54 _55 <<-\\EOF\n+\t* 54_C\n+\t \\\n+\t  * 54_B\n+\t* 55_A\n+\tEOF\n+'\n+\n+test_expect_success '--grep skipped parent makes a visual root' '\n+\tcreate_orphan _57 &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 57_keep_A &&\n+\tcreate_orphan _56 &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 56_skip &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 56_keep_A &&\n+\ttest_tick &&\n+\tgit commit --allow-empty -m 56_keep_B &&\n+\tlib_test_check_graph --grep=keep _56 _57 <<-\\EOF\n+\t* 56_keep_B\n+\t \\\n+\t  * 56_keep_A\n+\t* 57_keep_A\n+\tEOF\n+'\n+\n+test_done\n\n-- \n2.54.0\n"},{"id":"548117","messageId":"20260714-ps-pre-commit-indent-v12-5-d50938e006df@gmail.com","threadId":"65419","inReplyTo":"20260714-ps-pre-commit-indent-v12-0-d50938e006df@gmail.com","subject":"[PATCH v12 5/7] graph: wrap cascading commits after 4 columns","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-14T12:09:36Z","receivedAt":"2026-07-14T12:09:49Z","isPatch":true,"body":"Currently the visual root commits in a graph cascade indefinitely until\na commit which is not a visual root or the last commit appears.\nOn filters like --author where one author might contribute mostly on\nsingle patches this can become a visual issue.\n\nMake the cascading wrap after 4 columns.\n\nThere are two possible cases of the wrap:\n\n1. No ambiguity:\n\n* A\n  * B\n    * C\n      * D\n* E\n  * F\n\n2. Ambiguous conflict:\n\nIf F happens to not be a visual root and E gets wrapped back to the\ninitial column then E and F would be vertically adjacent. The solution\nis to forcefully indent E one level:\n\n* A\n  * B\n    * C\n      * D\n  * E\n* F\n* F\n\nThe magic number 4 comes as the minimum number of columns to wrap where\nthe output shows clearly the commits are unrelated and doesn't cause too\nmuch \"pyramid\" effects\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c                          | 22 +++++++++++++++++++++-\n t/t4218-log-graph-indentation.sh | 29 +++++++++++++++++++++++++++++\n 2 files changed, 50 insertions(+), 1 deletion(-)\n\ndiff --git a/graph.c b/graph.c\nindex 087094189f..e3e206170c 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -1042,6 +1042,23 @@ void graph_update(struct git_graph *graph, struct commit *commit)\n \t\t */\n \t\tif (!graph->visual_root_depth && flags.is_next_visual_root)\n \t\t\tgraph->visual_root_cascade = 1;\n+\n+\t\t/*\n+\t\t * We wrap the cascading at a max of four columns at most, after\n+\t\t * that we wrap it back to the initial column.\n+\t\t *\n+\t\t * This could cause ambiguity in case of the next commit not\n+\t\t * being a visual root and be at the initial column after the\n+\t\t * first wrap.\n+\t\t *\n+\t\t * In case of being a non-visual-root the next, stop the\n+\t\t * cascading to get the commit indented.\n+\t\t */\n+\t\tif (!flags.is_next_visual_root &&\n+\t\t    graph->visual_root_depth &&\n+\t\t    !(graph->visual_root_depth % 4))\n+\t\t\tgraph->visual_root_cascade = 0;\n+\n \t\tgraph->visual_root_depth++;\n \t} else {\n \t\tgraph->visual_root_depth = 0;\n@@ -1328,8 +1345,11 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \t\t\t\t * Each visual column is 2 characters wide.\n \t\t\t\t * Omit the indentation for the first visual\n \t\t\t\t * root in cascade mode.\n+\t\t\t\t *\n+\t\t\t\t * Have a max of 4 columns when cascading, after\n+\t\t\t\t * that wrap it and repeat.\n \t\t\t\t */\n-\t\t\t\tint padding = (depth - graph->visual_root_cascade) * 2;\n+\t\t\t\tint padding = ((depth - graph->visual_root_cascade) % 4) * 2;\n \t\t\t\tgraph_line_addchars(line, ' ', padding);\n \t\t\t\tgraph->width += padding;\n \t\t\t}\ndiff --git a/t/t4218-log-graph-indentation.sh b/t/t4218-log-graph-indentation.sh\nindex 60c7d84af7..d4c850c0d4 100755\n--- a/t/t4218-log-graph-indentation.sh\n+++ b/t/t4218-log-graph-indentation.sh\n@@ -511,4 +511,33 @@ test_expect_success '--grep skipped parent makes a visual root' '\n \tEOF\n '\n \n+# The cascading wraps after 4 columns and when wraping (column % 4 == 0) if the\n+# next is a non visual-root, force indentation to avoid an ambiguous graph\n+# (commit 59_A is forcefully indented)\n+test_expect_success 'visual root cascading gets wrapped after 4 columns' '\n+\tcreate_orphan _58 && test_commit 58_A && test_commit 58_B &&\n+\tcreate_orphan _59 && test_commit 59_A &&\n+\tcreate_orphan _60 && test_commit 60_A &&\n+\tcreate_orphan _61 && test_commit 61_A &&\n+\tcreate_orphan _62 && test_commit 62_A &&\n+\tcreate_orphan _63 && test_commit 63_A &&\n+\tcreate_orphan _64 && test_commit 64_A &&\n+\tcreate_orphan _65 && test_commit 65_A &&\n+\tcreate_orphan _66 && test_commit 66_A &&\n+\tcreate_orphan _67 && test_commit 67_A &&\n+\tlib_test_check_graph _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\\EOF\n+\t* 67_A\n+\t  * 66_A\n+\t    * 65_A\n+\t      * 64_A\n+\t* 63_A\n+\t  * 62_A\n+\t    * 61_A\n+\t      * 60_A\n+\t  * 59_A\n+\t* 58_B\n+\t* 58_A\n+\tEOF\n+'\n+\n test_done\n\n-- \n2.54.0\n"},{"id":"548119","messageId":"20260714-ps-pre-commit-indent-v12-6-d50938e006df@gmail.com","threadId":"65419","inReplyTo":"20260714-ps-pre-commit-indent-v12-0-d50938e006df@gmail.com","subject":"[PATCH v12 6/7] graph: move config reading into graph_read_config()","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-14T12:09:37Z","receivedAt":"2026-07-14T12:09:50Z","isPatch":true,"body":"Move the repo_config_get_string() call out of graph_init() and into\ngraph_read_config(). This simplifies graph_init() and provides a\nfunction for future graph-related config opt.\n\nThis commit is a preparatory commit for a subsequent one.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c | 13 +++++++++----\n 1 file changed, 9 insertions(+), 4 deletions(-)\n\ndiff --git a/graph.c b/graph.c\nindex e3e206170c..c14be934a0 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -417,13 +417,11 @@ void graph_setup_line_prefix(struct diff_options *diffopt)\n \t\tdiffopt->output_prefix = diff_output_prefix_callback;\n }\n \n-struct git_graph *graph_init(struct rev_info *opt)\n+static void graph_read_config(struct rev_info *revs)\n {\n-\tstruct git_graph *graph = xmalloc(sizeof(struct git_graph));\n-\n \tif (!column_colors) {\n \t\tchar *string;\n-\t\tif (repo_config_get_string(opt->repo, \"log.graphcolors\", &string)) {\n+\t\tif (repo_config_get_string(revs->repo, \"log.graphcolors\", &string)) {\n \t\t\t/* not configured -- use default */\n \t\t\tgraph_set_column_colors(column_colors_ansi,\n \t\t\t\t\t\tcolumn_colors_ansi_max);\n@@ -437,6 +435,13 @@ struct git_graph *graph_init(struct rev_info *opt)\n \t\t\t\t\t\tcustom_colors.nr - 1);\n \t\t}\n \t}\n+}\n+\n+struct git_graph *graph_init(struct rev_info *opt)\n+{\n+\tstruct git_graph *graph = xmalloc(sizeof(struct git_graph));\n+\n+\tgraph_read_config(opt);\n \n \tgraph->commit = NULL;\n \tgraph->revs = opt;\n\n-- \n2.54.0\n"},{"id":"548120","messageId":"20260714-ps-pre-commit-indent-v12-7-d50938e006df@gmail.com","threadId":"65419","inReplyTo":"20260714-ps-pre-commit-indent-v12-0-d50938e006df@gmail.com","subject":"[PATCH v12 7/7] graph: add --[no-]graph-indent and log.graphIndent","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-07-14T12:09:38Z","receivedAt":"2026-07-14T12:09:52Z","isPatch":true,"body":"Some users may prefer to not have graph indentation.\n\nAdd \"log.graphIndent\" config variable to graph_read_config() to read the\ndefault preference. By default is graph indentation is true.\n\nAdd --graph-indent and --no-graph-indent options to overwrite the\ndefault preference.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n Documentation/config/log.adoc       |  4 +++\n Documentation/rev-list-options.adoc |  8 ++++++\n graph.c                             | 10 +++++--\n revision.c                          |  9 +++++++\n revision.h                          |  2 ++\n t/t4218-log-graph-indentation.sh    | 53 +++++++++++++++++++++++++++++++++++++\n 6 files changed, 84 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/config/log.adoc b/Documentation/config/log.adoc\nindex 757a7be196..f7dfce69b5 100644\n--- a/Documentation/config/log.adoc\n+++ b/Documentation/config/log.adoc\n@@ -59,6 +59,10 @@ This is the same as the `--decorate` option of the `git log`.\n \tA list of colors, separated by commas, that can be used to draw\n \thistory lines in `git log --graph`.\n \n+`log.graphIndent`::\n+\tIf `true`, indent visual roots when rendering the graphs with `--graph`.\n+\tSet true by default. It can be overriden with `--[no-]graph-indent`.\n+\n `log.showRoot`::\n \tIf true, the initial commit will be shown as a big creation event.\n \tThis is equivalent to a diff against an empty tree.\ndiff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc\nindex eaee6ee839..fd831f0ec6 100644\n--- a/Documentation/rev-list-options.adoc\n+++ b/Documentation/rev-list-options.adoc\n@@ -1269,6 +1269,14 @@ This implies the `--topo-order` option by default, but the\n \tBy default it is set to 0 (no limit), zero and negative values\n \tare ignored and treated as no limit.\n \n+`--no-graph-indent`::\n+`--graph-indent`::\n+\tWhen used with `--graph`, indent visual roots (commits with no parents\n+\tor whose parents are not shown) to differentiate them from commits that\n+\tare vertically adjacent but unrelated. Enabled by default. Use\n+\t`--no-graph-indent` to disable or set `log.graphIndent` to set a\n+\tdefault preference.\n+\n ifdef::git-rev-list[]\n `--count`::\n \tPrint a number stating how many commits would have been\ndiff --git a/graph.c b/graph.c\nindex c14be934a0..28bef1b88f 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -419,6 +419,8 @@ void graph_setup_line_prefix(struct diff_options *diffopt)\n \n static void graph_read_config(struct rev_info *revs)\n {\n+\tint val;\n+\n \tif (!column_colors) {\n \t\tchar *string;\n \t\tif (repo_config_get_string(revs->repo, \"log.graphcolors\", &string)) {\n@@ -435,6 +437,9 @@ static void graph_read_config(struct rev_info *revs)\n \t\t\t\t\t\tcustom_colors.nr - 1);\n \t\t}\n \t}\n+\n+\tif (!repo_config_get_bool(revs->repo, \"log.graphIndent\", &val))\n+\t\trevs->no_graph_indent = !val;\n }\n \n struct git_graph *graph_init(struct rev_info *opt)\n@@ -999,7 +1004,8 @@ static void graph_peek_next_visible(struct git_graph *graph,\n static int graph_needs_pre_root_line(struct git_graph *graph)\n {\n \treturn graph->commit_in_columns && graph->is_visual_root &&\n-\t       graph->num_columns > 0 && !graph->visual_root_cascade;\n+\t       graph->num_columns > 0 && !graph->visual_root_cascade &&\n+\t       !graph->revs->no_graph_indent;\n }\n \n void graph_update(struct git_graph *graph, struct commit *commit)\n@@ -1344,7 +1350,7 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \n \t\tif (col_commit == graph->commit) {\n \t\t\tseen_this = 1;\n-\t\t\tif (graph->is_visual_root) {\n+\t\t\tif (graph->is_visual_root && !graph->revs->no_graph_indent) {\n \t\t\t\tint depth = graph->visual_root_depth;\n \t\t\t\t/*\n \t\t\t\t * Each visual column is 2 characters wide.\ndiff --git a/revision.c b/revision.c\nindex 258c3cf782..37f7ea45d1 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -2627,6 +2627,12 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg\n \t\trevs->graph = NULL;\n \t} else if (skip_prefix(arg, \"--graph-lane-limit=\", &optarg)) {\n \t\trevs->graph_max_lanes = parse_count(optarg);\n+\t} else if (!strcmp(arg, \"--graph-indent\")) {\n+\t\trevs->no_graph_indent = 0;\n+\t\trevs->graph_indent_set = 1;\n+\t} else if (!strcmp(arg, \"--no-graph-indent\")) {\n+\t\trevs->no_graph_indent = 1;\n+\t\trevs->graph_indent_set = 1;\n \t} else if (!strcmp(arg, \"--encode-email-headers\")) {\n \t\trevs->encode_email_headers = 1;\n \t} else if (!strcmp(arg, \"--no-encode-email-headers\")) {\n@@ -3201,6 +3207,9 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s\n \tif (revs->graph_max_lanes > 0 && !revs->graph)\n \t\tdie(_(\"the option '%s' requires '%s'\"), \"--graph-lane-limit\", \"--graph\");\n \n+\tif (revs->graph_indent_set && !revs->graph)\n+\t\tdie(_(\"the option '%s' requires '%s'\"), \"--[no-]graph-indent\", \"--graph\");\n+\n \tif (!revs->reflog_info && revs->grep_filter.use_reflog_filter)\n \t\tdie(_(\"the option '%s' requires '%s'\"), \"--grep-reflog\", \"--walk-reflogs\");\n \ndiff --git a/revision.h b/revision.h\nindex 569b3fa1cb..acf6d06b24 100644\n--- a/revision.h\n+++ b/revision.h\n@@ -314,6 +314,8 @@ struct rev_info {\n \t/* Display history graph */\n \tstruct git_graph *graph;\n \tint graph_max_lanes;\n+\tunsigned int no_graph_indent:1;\n+\tunsigned int graph_indent_set:1;\n \n \t/* special limits */\n \tint skip_count;\ndiff --git a/t/t4218-log-graph-indentation.sh b/t/t4218-log-graph-indentation.sh\nindex d4c850c0d4..24dc9b497d 100755\n--- a/t/t4218-log-graph-indentation.sh\n+++ b/t/t4218-log-graph-indentation.sh\n@@ -540,4 +540,57 @@ test_expect_success 'visual root cascading gets wrapped after 4 columns' '\n \tEOF\n '\n \n+test_expect_success '--no-graph-indent disables indentation' '\n+\tlib_test_check_graph --no-graph-indent _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\\EOF\n+\t* 67_A\n+\t* 66_A\n+\t* 65_A\n+\t* 64_A\n+\t* 63_A\n+\t* 62_A\n+\t* 61_A\n+\t* 60_A\n+\t* 59_A\n+\t* 58_B\n+\t* 58_A\n+\tEOF\n+'\n+\n+test_expect_success 'log.graphIndent config disables indentation' '\n+\ttest_config log.graphIndent false &&\n+\tlib_test_check_graph _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\\EOF\n+\t* 67_A\n+\t* 66_A\n+\t* 65_A\n+\t* 64_A\n+\t* 63_A\n+\t* 62_A\n+\t* 61_A\n+\t* 60_A\n+\t* 59_A\n+\t* 58_B\n+\t* 58_A\n+\tEOF\n+'\n+\n+test_expect_success '--graph-indent forces indentation when graph.indent is unset' '\n+\ttest_config log.graphIndent false &&\n+\tlib_test_check_graph --graph-indent _58 _59 _60 _61 _62 _63 _64 _65 _66 _67 <<-\\EOF\n+\t* 67_A\n+\t  * 66_A\n+\t    * 65_A\n+\t      * 64_A\n+\t* 63_A\n+\t  * 62_A\n+\t    * 61_A\n+\t      * 60_A\n+\t  * 59_A\n+\t* 58_B\n+\t* 58_A\n+\tEOF\n+'\n+\n+# log.graphIndent unset and no --option (which activates graph indentation) is\n+# the default state.\n+\n test_done\n\n-- \n2.54.0\n"},{"id":"548244","messageId":"CA+J6zkQNzEAhhY74qDrOwfFVrshEF7YFxWRRkwE3ttJo15ZbAg@mail.gmail.com","threadId":"65419","inReplyTo":"20260714-ps-pre-commit-indent-v12-0-d50938e006df@gmail.com","subject":"Re: [PATCH v12 0/7] graph: indent visual roots in graph","fromName":"Chandra Pratap","fromEmail":"chandrapratap3519@gmail.com","sentAt":"2026-07-15T08:59:08Z","receivedAt":"2026-07-15T08:59:38Z","isPatch":true,"body":"On Tue, 14 Jul 2026 at 17:39, Pablo Sabater <pabloosabaterr@gmail.com> wrote:\n>\n> When rendering a graph, if the history contains multiple \"visual roots\",\n> actual roots or commits that look like roots (i.e. have their parents\n> filtered out) can end up being vertically adjacent to unrelated commits,\n> falsely appearing to be related.\n>\n> A fix for this issue was already attempted [1] a while ago.\n>\n> This series adds indentation to the visual root commits, so they cannot be\n> vertically adjacent anymore making it easier to identify them.\n>\n> Before indentation:\n>\n>         * A\n>         * B1\n>         * B2\n>         * C1\n>         * C2\n>\n> After indentation:\n>\n>           * A\n>         * B1\n>          \\\n>           * B2\n>         * C1\n>         * C2\n>\n> Indents the visual root commits that have still commits to show after\n> them, and if they have children it connects them with an edge at a new\n> row.\n>\n> If there are multiple visual roots adjacent in history, the indentation\n> starts with the second one, avoiding redundant indentation of the first\n> one and cascades after the second.\n>\n>         * A\n>           * B\n>             * C\n>               * D\n>         * E\n>           * F\n>             * G\n>               * H\n>           * I\n>         * J1\n>         * J2\n>\n> The indentation wraps after cascading columns and when wrapping back to\n> the initial column if the next commit is a non-visual-root commit, force\n> the indentation one extra level.\n>\n> Series explanation:\n>\n> 1. Cleanup to bring a common function from t4215 and t6016 that will be\n>    used in t4218.\n>\n> 2. Logic extraction of the chose of from where the commit source comes\n>    from.\n>\n> 3. Add a buffer for lookahead purposes.\n>\n> 4. Principal commit. Implement the logic to get the visual roots\n>    indented.\n>\n> 5. Make visual root cascading wrap after 4 columns\n>\n> 6. Add --[no-]graph-indent and log.graphIndent options.\n>\n> GitHub CI: https://github.com/pabloosabaterr/git/actions/runs/29331144667\n>\n> [1]: https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n>\n> V11 DIFF:\n>\n> - Changed the check that required graph, to not confuse because it is a\n>   boolean value.\n>\n> - Typos\n>\n> Signed-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n> ---\n> Pablo Sabater (7):\n>       lib-log-graph: move check_graph function\n>       revision: add next_commit_to_show()\n>       graph: add a 2 commit buffer for lookahead\n>       graph: indent visual root in graph\n>       graph: wrap cascading commits after 4 columns\n>       graph: move config reading into graph_read_config()\n>       graph: add --[no-]graph-indent and log.graphIndent\n>\n>  Documentation/config/log.adoc              |   4 +\n>  Documentation/rev-list-options.adoc        |   8 +\n>  graph.c                                    | 332 +++++++++++++++-\n>  graph.h                                    |  17 +\n>  revision.c                                 |  57 ++-\n>  revision.h                                 |   2 +\n>  t/lib-log-graph.sh                         |   5 +\n>  t/meson.build                              |   1 +\n>  t/t4215-log-skewed-merges.sh               |  33 +-\n>  t/t4218-log-graph-indentation.sh           | 596 +++++++++++++++++++++++++++++\n>  t/t6016-rev-list-graph-simplify-history.sh |  25 +-\n>  11 files changed, 1032 insertions(+), 48 deletions(-)\n>\n> Range-diff versus v11:\n>\n> 1:  dd0bb0d215 = 1:  d754392142 lib-log-graph: move check_graph function\n> 2:  07e239533d = 2:  c93c2c0771 revision: add next_commit_to_show()\n> 3:  4d71f674a1 = 3:  70fe612ae1 graph: add a 2 commit buffer for lookahead\n> 4:  48ad2562f0 = 4:  e1ac06c4ea graph: indent visual root in graph\n> 5:  45be69d11b = 5:  ce52b41527 graph: wrap cascading commits after 4 columns\n> 6:  8ce53ae21b = 6:  9b7bb2cebc graph: move config reading into graph_read_config()\n> 7:  c1fa81022e ! 7:  13e830725f graph: add --[no-]graph-indent and log.graphIndent\n>     @@ Documentation/rev-list-options.adoc: This implies the `--topo-order` option by d\n>      +  When used with `--graph`, indent visual roots (commits with no parents\n>      +  or whose parents are not shown) to differentiate them from commits that\n>      +  are vertically adjacent but unrelated. Enabled by default. Use\n>     -+  `--no-graph-indent` to disable or set `graph.indent` to set a deafault\n>     -+  preference.\n>     ++  `--no-graph-indent` to disable or set `log.graphIndent` to set a\n>     ++  default preference.\n>      +\n>       ifdef::git-rev-list[]\n>       `--count`::\n>     @@ revision.c: int setup_revisions(int argc, const char **argv, struct rev_info *re\n>         if (revs->graph_max_lanes > 0 && !revs->graph)\n>                 die(_(\"the option '%s' requires '%s'\"), \"--graph-lane-limit\", \"--graph\");\n>\n>     -+  if (revs->graph_indent_set > 0 && !revs->graph)\n>     ++  if (revs->graph_indent_set && !revs->graph)\n>      +          die(_(\"the option '%s' requires '%s'\"), \"--[no-]graph-indent\", \"--graph\");\n>      +\n>         if (!revs->reflog_info && revs->grep_filter.use_reflog_filter)\n>     @@ t/t4218-log-graph-indentation.sh: test_expect_success 'visual root cascading get\n>      +  EOF\n>      +'\n>      +\n>     -+# graph.indent true and no --option is the default state.\n>     ++# log.graphIndent unset and no --option (which activates graph indentation) is\n>     ++# the default state.\n>      +\n>       test_done\n>\n> ---\n> base-commit: f60db8d575adb79761d363e026fb49bddf330c73\n\nThis version looks fine to me.\n\nThanks,\nChandra.\n"}]}