{"thread":{"id":"65267","subject":"[GSoC RFC PATCH] graph: add --graph-max option to limit displayed columns","startedAt":"2026-03-16T13:34:38Z","lastAt":"2026-04-03T20:15:47Z","messageCount":48,"participants":["Pablo Sabater","Karthik Nayak","Pablo","Junio C Hamano","Johannes Sixt","SZEDER Gábor","Tian Yuchen"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"539107","messageId":"20260316133426.117684-1-pabloosabaterr@gmail.com","threadId":"65267","inReplyTo":null,"subject":"[GSoC RFC PATCH] graph: add --graph-max option to limit displayed columns","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-16T13:34:26Z","receivedAt":"2026-03-16T13:34:38Z","isPatch":true,"body":"When there are multiple branches, --graph-max modifies the maximum\namount of columns that will be displayed.\n\nAdd \"--graph-max=<n>\" option to cap how many columns will be shown,\ncolumns after the limit are replaced with a single '.'. Changes only\nthe output rendering.\n\nDefine MINIMUM_GRAPH_COLUMNS constant to validate the option value.\n\nThe commit character '*' is always shown no matter what the limit is.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n\nThis addresses the TODO at graph.c:\n\n  TODO:\n      - Limit the number of columns, similar to the way gitk does.\n        If we reach more than a specified number of columns, omit\n        sections of some columns.\n\nAbout the design of how this would have to be:\n\n- Should '--graph-max' by itself be enough to implicitly work like '--graph' so \n  'git log --graph-max=3' works without needing to write '--graph'?\n- graph_max_columns by default is set to 0, meaning no limit, and any other \n  positive value becomes a limit. Is this a good design? it cannot be negative,\n  shouldn't it be a uint32_t instead, I left it as a int because of the other\n  variables like this that are int. like skip_count, max_count, etc.\n- Is '--graph-max' a good name?\n- Is '.' a good char for truncation?\n- Should '/' to outside branches be shown?\n- What should it be done when a commit is in a column that is truncated?\n\nknown limitations:\n\n- Post merge lines have some trouble with the padding.\n\nI added two tests for example, but I will add better test coverage as design \nchoices are more clear. testing on the Git repo itself is a good example also.\n\n graph.c                      | 52 +++++++++++++++++++++++++++------\n graph.h                      |  2 ++\n revision.c                   |  7 +++++\n revision.h                   |  1 +\n t/t4215-log-skewed-merges.sh | 56 ++++++++++++++++++++++++++++++++++++\n 5 files changed, 109 insertions(+), 9 deletions(-)\n\ndiff --git a/graph.c b/graph.c\nindex 26f6fbf000..7ae0ab61b7 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -42,14 +42,6 @@ static void graph_padding_line(struct git_graph *graph, struct strbuf *sb);\n static void graph_show_strbuf(struct git_graph *graph,\n \t\t\t      FILE *file,\n \t\t\t      struct strbuf const *sb);\n-\n-/*\n- * TODO:\n- * - Limit the number of columns, similar to the way gitk does.\n- *   If we reach more than a specified number of columns, omit\n- *   sections of some columns.\n- */\n-\n struct column {\n \t/*\n \t * The parent commit of this column.\n@@ -317,6 +309,12 @@ struct git_graph {\n \tstruct strbuf prefix_buf;\n };\n \n+static int graph_is_truncated(struct git_graph *graph, int col)\n+{\n+\tint max = graph->revs->graph_max_columns;\n+\treturn max > 0 && col >= max;\n+}\n+\n static const char *diff_output_prefix_callback(struct diff_options *opt, void *data)\n {\n \tstruct git_graph *graph = data;\n@@ -846,6 +844,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\tif (graph_is_truncated(graph, i)) {\n+\t\t\tgraph_line_addstr(line, \". \");\n+\t\t\tbreak;\n+\t\t}\n \t\tgraph_line_write_column(line, &graph->new_columns[i], '|');\n \t\tgraph_line_addch(line, ' ');\n \t}\n@@ -903,6 +905,9 @@ static void graph_output_pre_commit_line(struct git_graph *graph,\n \t\t\tseen_this = 1;\n \t\t\tgraph_line_write_column(line, col, '|');\n \t\t\tgraph_line_addchars(line, ' ', graph->expansion_row);\n+\t\t} else if (seen_this && graph_is_truncated(graph, i)) {\n+\t\t\tgraph_line_addstr(line, \". \");\n+\t\t\tbreak;\n \t\t} else if (seen_this && (graph->expansion_row == 0)) {\n \t\t\t/*\n \t\t\t * This is the first line of the pre-commit output.\n@@ -1013,6 +1018,7 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \t * children that we have already processed.)\n \t */\n \tseen_this = 0;\n+\n \tfor (i = 0; i <= graph->num_columns; i++) {\n \t\tstruct column *col = &graph->columns[i];\n \t\tstruct commit *col_commit;\n@@ -1028,8 +1034,14 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \t\t\tseen_this = 1;\n \t\t\tgraph_output_commit_char(graph, line);\n \n+\t\t\tif (graph_is_truncated(graph, i))\n+\t\t\t\tbreak;\n+\n \t\t\tif (graph->num_parents > 2)\n \t\t\t\tgraph_draw_octopus_merge(graph, line);\n+\t\t} else if (seen_this && graph_is_truncated(graph, i)) {\n+\t\t\tgraph_line_addstr(line, \". \");\n+\t\t\tbreak;\n \t\t} else if (seen_this && (graph->edges_added > 1)) {\n \t\t\tgraph_line_write_column(line, col, '\\\\');\n \t\t} else if (seen_this && (graph->edges_added == 1)) {\n@@ -1109,9 +1121,15 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n \t\t\tint par_column;\n \t\t\tint idx = graph->merge_layout;\n \t\t\tchar c;\n+\t\t\tint truncated = 0;\n \t\t\tseen_this = 1;\n \n \t\t\tfor (j = 0; j < graph->num_parents; j++) {\n+\t\t\t\tif (graph_is_truncated(graph, i + j)) {\n+\t\t\t\t\tgraph_line_addstr(line, \". \");\n+\t\t\t\t\ttruncated = 1;\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n \t\t\t\tpar_column = graph_find_new_column_by_commit(graph, parents->item);\n \t\t\t\tassert(par_column >= 0);\n \n@@ -1125,10 +1143,15 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n \t\t\t\t}\n \t\t\t\tparents = next_interesting_parent(graph, parents);\n \t\t\t}\n+\t\t\tif (truncated)\n+\t\t\t\tbreak;\n \t\t\tif (graph->edges_added == 0)\n \t\t\t\tgraph_line_addch(line, ' ');\n-\n \t\t} else if (seen_this) {\n+\t\t\tif (graph_is_truncated(graph, i)) {\n+\t\t\t\tgraph_line_addstr(line, \". \");\n+\t\t\t\tbreak;\n+\t\t\t}\n \t\t\tif (graph->edges_added > 0)\n \t\t\t\tgraph_line_write_column(line, col, '\\\\');\n \t\t\telse\n@@ -1279,6 +1302,12 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n \t */\n \tfor (i = 0; i < graph->mapping_size; i++) {\n \t\tint target = graph->mapping[i];\n+\n+\t\tif (graph_is_truncated(graph, i / 2)) {\n+\t\t\tgraph_line_addstr(line, \". \");\n+\t\t\tbreak;\n+\t\t}\n+\n \t\tif (target < 0)\n \t\t\tgraph_line_addch(line, ' ');\n \t\telse if (target * 2 == i)\n@@ -1372,6 +1401,11 @@ static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)\n \tfor (i = 0; i < graph->num_columns; i++) {\n \t\tstruct column *col = &graph->columns[i];\n \n+\t\tif (graph_is_truncated(graph, i)) {\n+\t\t\tgraph_line_addch(&line, '.');\n+\t\t\tbreak;\n+\t\t}\n+\n \t\tgraph_line_write_column(&line, col, '|');\n \n \t\tif (col->commit == graph->commit && graph->num_parents > 2) {\ndiff --git a/graph.h b/graph.h\nindex 3fd1dcb2e9..9a4551dd29 100644\n--- a/graph.h\n+++ b/graph.h\n@@ -262,4 +262,6 @@ 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+#define MINIMUM_GRAPH_COLUMNS 1\n+\n #endif /* GRAPH_H */\ndiff --git a/revision.c b/revision.c\nindex 31808e3df0..ba5088be14 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -2605,6 +2605,13 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg\n \t} else if (!strcmp(arg, \"--no-graph\")) {\n \t\tgraph_clear(revs->graph);\n \t\trevs->graph = NULL;\n+\t} else if (skip_prefix(arg, \"--graph-max=\", &optarg)) {\n+\t\trevs->graph_max_columns = strtoul(optarg, NULL, 10);\n+\t\tif (revs->graph_max_columns < MINIMUM_GRAPH_COLUMNS) {\n+\t\t\tdie(_(\"minimum columns is %d, unable to set below %d\"),\n+\t\t\tMINIMUM_GRAPH_COLUMNS,\n+\t\t\trevs->graph_max_columns);\n+\t\t}\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\")) {\ndiff --git a/revision.h b/revision.h\nindex 69242ecb18..6442129c14 100644\n--- a/revision.h\n+++ b/revision.h\n@@ -304,6 +304,7 @@ struct rev_info {\n \n \t/* Display history graph */\n \tstruct git_graph *graph;\n+\tint graph_max_columns;\n \n \t/* special limits */\n \tint skip_count;\ndiff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh\nindex 28d0779a8c..6266de4e2b 100755\n--- a/t/t4215-log-skewed-merges.sh\n+++ b/t/t4215-log-skewed-merges.sh\n@@ -370,4 +370,60 @@ test_expect_success 'log --graph with multiple tips' '\n \tEOF\n '\n \n+test_expect_success 'log --graph --graph-max=2 only two columns' '\n+\tcheck_graph --graph-max=2 M_7 <<-\\EOF\n+\t*-.   7_M4\n+\t|\\ .\n+\t| | * 7_G\n+\t| | * 7_F\n+\t| * . 7_E\n+\t| * . 7_D\n+\t* | . 7_C\n+\t| |/\n+\t|/|\n+\t* | 7_B\n+\t|/\n+\t* 7_A\n+\tEOF\n+'\n+\n+test_expect_success 'log --graph --graph-max=3 only three columns' '\n+\tcheck_graph --graph-max=3 M_1 M_3 M_5 M_7 <<-\\EOF\n+\t*   7_M1\n+\t|\\\n+\t| | *   7_M2\n+\t| | |.\n+\t| | | * 7_H\n+\t| | | | *   7_M3\n+\t| | | | .\n+\t| | | | | * 7_J\n+\t| | | | *   7_I\n+\t| | | | | | *   7_M4\n+\t| |_|_|_|_|.\n+\t|/| | .\n+\t| | |_.\n+\t| |/|_.\n+\t| |/|_.\n+\t| |/| .\n+\t| | |/.\n+\t| | * .     7_G\n+\t| | | .\n+\t| | |/.\n+\t| | |/.\n+\t| | * .   7_F\n+\t| * | .   7_E\n+\t| | |/.\n+\t| |/| .\n+\t| * | . 7_D\n+\t| | |/\n+\t| |/|\n+\t* | | 7_C\n+\t| |/\n+\t|/|\n+\t* | 7_B\n+\t|/\n+\t* 7_A\n+\tEOF\n+'\n+\n test_done\n-- \n2.43.0\n\n"},{"id":"539135","messageId":"CAOLa=ZSsC7zpfpRx8pShcqGEv_2_NMrKzJHCgTSaO=0Dg0xakg@mail.gmail.com","threadId":"65267","inReplyTo":"20260316133426.117684-1-pabloosabaterr@gmail.com","subject":"Re: [GSoC RFC PATCH] graph: add --graph-max option to limit displayed columns","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2026-03-16T17:04:18Z","receivedAt":"2026-03-16T17:04:21Z","isPatch":true,"body":"Pablo Sabater <pabloosabaterr@gmail.com> writes:\n\n> When there are multiple branches, --graph-max modifies the maximum\n> amount of columns that will be displayed.\n>\n> Add \"--graph-max=<n>\" option to cap how many columns will be shown,\n> columns after the limit are replaced with a single '.'. Changes only\n> the output rendering.\n>\n\nThe first sentence seems to talk about the option like it already\nexists, when the second para introduces it. It would be nice if the\nfirst para explained the problem we're trying to solve and why and the\nsecond para then dove into the solution space.\n\nDo you think '--graph-max' signifies that we're talking about the\nmaximum columns to display?\n\n> Define MINIMUM_GRAPH_COLUMNS constant to validate the option value.\n\nWhat does this mean? Validate how?\n\n> The commit character '*' is always shown no matter what the limit is.\n\nI think overall a little more explanation in the commit message makes it\neasier to understand the context and also helps reviewers!\n\nShouldn't we also talk about the todo and the commit (c12172d2ea (Add\nhistory graph API, 2008-05-04)) in which it was added?\n\n>\n> Signed-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n> ---\n>\n> This addresses the TODO at graph.c:\n>\n>   TODO:\n>       - Limit the number of columns, similar to the way gitk does.\n>         If we reach more than a specified number of columns, omit\n>         sections of some columns.\n>\n\nOne question to ask is, is this even needed anymore and does it really\nmake sense to add it?\n\nThe TODO was added back in 2008, that's ~16 years ago and was not\ntouched till now. So perhaps no one needs it? If so, maybe the smarter\noption is to simply remove the TODO?\n\nOr do you see a usecase where this is useful? If so, it would be nice to\ntalk about that in the commit message.\n\n> About the design of how this would have to be:\n>\n> - Should '--graph-max' by itself be enough to implicitly work like '--graph' so\n>   'git log --graph-max=3' works without needing to write '--graph'?\n\nMy preference would be not to have implicit behavior but also at the\nsame time guide the user in the right path, so:\n\n     $ git log --graph-max=4\n     fatal: --graph-max used without --graph\n\n> - graph_max_columns by default is set to 0, meaning no limit, and any other\n>   positive value becomes a limit. Is this a good design? it cannot be negative,\n>   shouldn't it be a uint32_t instead, I left it as a int because of the other\n>   variables like this that are int. like skip_count, max_count, etc.\n> - Is '--graph-max' a good name?\n\nI would argue against it. Perhaps '--graph-col-limit'? It's a bit handy though.\n\n> - Is '.' a good char for truncation?\n\nTrying it out:\n\n$ git log --graph --oneline --graph-max=3\n\n| * ba1c21d343 odb: split `struct odb_source` into separate header\n| *   b1af291b4a Merge branch 'ps/object-info-bits-cleanup' into ps/odb-sources\n| |\\\n| * \\   703c97519d Merge branch 'ps/odb-for-each-object' into ps/odb-sources\n| |\\ \\\n* | \\ .   d0413b31dd Merge branch 'hn/status-compare-with-push'\n|\\ \\ \\ .\n| * | .   68791d7506 status: clarify how status.compareBranches deduplicates\n| * | .   3ea95ac9c5 (gitster/hn/status-compare-with-push) status: add\nstatus.compareBranches config for multiple branch comparisons\n| * | .   04f47265c1 refactor format_branch_comparison in preparation\n| * | .     2aa9b75b43 Merge branch 'jk/remote-tracking-ref-leakfix'\ninto hn/status-compare-with-push\n| |\\ \\ .\n* | \\ .       03161747b4 Merge branch 'ds/for-each-repo-w-worktree'\n|\\ \\ \\ .\n| * | .       e87493b9b4 for-each-repo: simplify passing of parameters\n| * | .       2ef539bcee for-each-repo: work correctly in a worktree\n| * | .       5f031fe4f1 run-command: extract sanitize_repo_env helper\n| * | .       c5e62e1aa0 for-each-repo: test outside of repo context\n* | | .       67006b9db8 The 15th batch\n* | | .         99da934835 Merge branch 'sp/send-email-validate-charset'\n|\\ \\ \\ .\n| * | .         c52f085a47 (gitster/sp/send-email-validate-charset)\nsend-email: validate charset name in 8bit encoding prompt\n\nvs\n\n$ git log --graph --oneline\n\n| * ba1c21d343 odb: split `struct odb_source` into separate header\n| *   b1af291b4a Merge branch 'ps/object-info-bits-cleanup' into ps/odb-sources\n| |\\\n| * \\   703c97519d Merge branch 'ps/odb-for-each-object' into ps/odb-sources\n| |\\ \\\n* | \\ \\   d0413b31dd Merge branch 'hn/status-compare-with-push'\n|\\ \\ \\ \\\n| * | | | 68791d7506 status: clarify how status.compareBranches deduplicates\n| * | | | 3ea95ac9c5 (gitster/hn/status-compare-with-push) status: add\nstatus.compareBranches config for multiple branch comparisons\n| * | | | 04f47265c1 refactor format_branch_comparison in preparation\n| * | | |   2aa9b75b43 Merge branch 'jk/remote-tracking-ref-leakfix'\ninto hn/status-compare-with-push\n| |\\ \\ \\ \\\n* | \\ \\ \\ \\   03161747b4 Merge branch 'ds/for-each-repo-w-worktree'\n|\\ \\ \\ \\ \\ \\\n| * | | | | | e87493b9b4 for-each-repo: simplify passing of parameters\n| * | | | | | 2ef539bcee for-each-repo: work correctly in a worktree\n| * | | | | | 5f031fe4f1 run-command: extract sanitize_repo_env helper\n| * | | | | | c5e62e1aa0 for-each-repo: test outside of repo context\n* | | | | | | 67006b9db8 The 15th batch\n* | | | | | |   99da934835 Merge branch 'sp/send-email-validate-charset'\n|\\ \\ \\ \\ \\ \\ \\\n| * | | | | | | c52f085a47 (gitster/sp/send-email-validate-charset)\nsend-email: validate charset name in 8bit encoding prompt\n\nSo we still keep the spaces, but only remove the column indicator\n\n> - Should '/' to outside branches be shown?\n> - What should it be done when a commit is in a column that is truncated?\n>\n> known limitations:\n>\n> - Post merge lines have some trouble with the padding.\n>\n> I added two tests for example, but I will add better test coverage as design\n> choices are more clear. testing on the Git repo itself is a good example also.\n>\n>  graph.c                      | 52 +++++++++++++++++++++++++++------\n>  graph.h                      |  2 ++\n>  revision.c                   |  7 +++++\n>  revision.h                   |  1 +\n>  t/t4215-log-skewed-merges.sh | 56 ++++++++++++++++++++++++++++++++++++\n>  5 files changed, 109 insertions(+), 9 deletions(-)\n>\n> diff --git a/graph.c b/graph.c\n> index 26f6fbf000..7ae0ab61b7 100644\n> --- a/graph.c\n> +++ b/graph.c\n> @@ -42,14 +42,6 @@ static void graph_padding_line(struct git_graph *graph, struct strbuf *sb);\n>  static void graph_show_strbuf(struct git_graph *graph,\n>  \t\t\t      FILE *file,\n>  \t\t\t      struct strbuf const *sb);\n> -\n> -/*\n> - * TODO:\n> - * - Limit the number of columns, similar to the way gitk does.\n> - *   If we reach more than a specified number of columns, omit\n> - *   sections of some columns.\n> - */\n> -\n>  struct column {\n>  \t/*\n>  \t * The parent commit of this column.\n> @@ -317,6 +309,12 @@ struct git_graph {\n>  \tstruct strbuf prefix_buf;\n>  };\n>\n> +static int graph_is_truncated(struct git_graph *graph, int col)\n\nIsn't this more of `graphs_needs_truncation()`?\n\n> +{\n> +\tint max = graph->revs->graph_max_columns;\n> +\treturn max > 0 && col >= max;\n> +}\n> +\n>  static const char *diff_output_prefix_callback(struct diff_options *opt, void *data)\n>  {\n>  \tstruct git_graph *graph = data;\n> @@ -846,6 +844,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\tif (graph_is_truncated(graph, i)) {\n> +\t\t\tgraph_line_addstr(line, \". \");\n> +\t\t\tbreak;\n> +\t\t}\n>  \t\tgraph_line_write_column(line, &graph->new_columns[i], '|');\n>  \t\tgraph_line_addch(line, ' ');\n>  \t}\n> @@ -903,6 +905,9 @@ static void graph_output_pre_commit_line(struct git_graph *graph,\n>  \t\t\tseen_this = 1;\n>  \t\t\tgraph_line_write_column(line, col, '|');\n>  \t\t\tgraph_line_addchars(line, ' ', graph->expansion_row);\n> +\t\t} else if (seen_this && graph_is_truncated(graph, i)) {\n> +\t\t\tgraph_line_addstr(line, \". \");\n> +\t\t\tbreak;\n>  \t\t} else if (seen_this && (graph->expansion_row == 0)) {\n>  \t\t\t/*\n>  \t\t\t * This is the first line of the pre-commit output.\n> @@ -1013,6 +1018,7 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n>  \t * children that we have already processed.)\n>  \t */\n>  \tseen_this = 0;\n> +\n>  \tfor (i = 0; i <= graph->num_columns; i++) {\n>  \t\tstruct column *col = &graph->columns[i];\n>  \t\tstruct commit *col_commit;\n> @@ -1028,8 +1034,14 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n>  \t\t\tseen_this = 1;\n>  \t\t\tgraph_output_commit_char(graph, line);\n>\n> +\t\t\tif (graph_is_truncated(graph, i))\n> +\t\t\t\tbreak;\n> +\n>  \t\t\tif (graph->num_parents > 2)\n>  \t\t\t\tgraph_draw_octopus_merge(graph, line);\n> +\t\t} else if (seen_this && graph_is_truncated(graph, i)) {\n> +\t\t\tgraph_line_addstr(line, \". \");\n> +\t\t\tbreak;\n>  \t\t} else if (seen_this && (graph->edges_added > 1)) {\n>  \t\t\tgraph_line_write_column(line, col, '\\\\');\n>  \t\t} else if (seen_this && (graph->edges_added == 1)) {\n> @@ -1109,9 +1121,15 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n>  \t\t\tint par_column;\n>  \t\t\tint idx = graph->merge_layout;\n>  \t\t\tchar c;\n> +\t\t\tint truncated = 0;\n>  \t\t\tseen_this = 1;\n>\n>  \t\t\tfor (j = 0; j < graph->num_parents; j++) {\n> +\t\t\t\tif (graph_is_truncated(graph, i + j)) {\n> +\t\t\t\t\tgraph_line_addstr(line, \". \");\n> +\t\t\t\t\ttruncated = 1;\n> +\t\t\t\t\tbreak;\n> +\t\t\t\t}\n>  \t\t\t\tpar_column = graph_find_new_column_by_commit(graph, parents->item);\n>  \t\t\t\tassert(par_column >= 0);\n>\n> @@ -1125,10 +1143,15 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n>  \t\t\t\t}\n>  \t\t\t\tparents = next_interesting_parent(graph, parents);\n>  \t\t\t}\n> +\t\t\tif (truncated)\n> +\t\t\t\tbreak;\n>  \t\t\tif (graph->edges_added == 0)\n>  \t\t\t\tgraph_line_addch(line, ' ');\n> -\n>  \t\t} else if (seen_this) {\n> +\t\t\tif (graph_is_truncated(graph, i)) {\n> +\t\t\t\tgraph_line_addstr(line, \". \");\n> +\t\t\t\tbreak;\n> +\t\t\t}\n>  \t\t\tif (graph->edges_added > 0)\n>  \t\t\t\tgraph_line_write_column(line, col, '\\\\');\n>  \t\t\telse\n> @@ -1279,6 +1302,12 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n>  \t */\n>  \tfor (i = 0; i < graph->mapping_size; i++) {\n>  \t\tint target = graph->mapping[i];\n> +\n> +\t\tif (graph_is_truncated(graph, i / 2)) {\n> +\t\t\tgraph_line_addstr(line, \". \");\n> +\t\t\tbreak;\n> +\t\t}\n> +\n>  \t\tif (target < 0)\n>  \t\t\tgraph_line_addch(line, ' ');\n>  \t\telse if (target * 2 == i)\n> @@ -1372,6 +1401,11 @@ static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)\n>  \tfor (i = 0; i < graph->num_columns; i++) {\n>  \t\tstruct column *col = &graph->columns[i];\n>\n> +\t\tif (graph_is_truncated(graph, i)) {\n> +\t\t\tgraph_line_addch(&line, '.');\n> +\t\t\tbreak;\n> +\t\t}\n> +\n>  \t\tgraph_line_write_column(&line, col, '|');\n>\n>  \t\tif (col->commit == graph->commit && graph->num_parents > 2) {\n> diff --git a/graph.h b/graph.h\n> index 3fd1dcb2e9..9a4551dd29 100644\n> --- a/graph.h\n> +++ b/graph.h\n> @@ -262,4 +262,6 @@ 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> +#define MINIMUM_GRAPH_COLUMNS 1\n> +\n>  #endif /* GRAPH_H */\n> diff --git a/revision.c b/revision.c\n> index 31808e3df0..ba5088be14 100644\n> --- a/revision.c\n> +++ b/revision.c\n> @@ -2605,6 +2605,13 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg\n>  \t} else if (!strcmp(arg, \"--no-graph\")) {\n>  \t\tgraph_clear(revs->graph);\n>  \t\trevs->graph = NULL;\n> +\t} else if (skip_prefix(arg, \"--graph-max=\", &optarg)) {\n> +\t\trevs->graph_max_columns = strtoul(optarg, NULL, 10);\n> +\t\tif (revs->graph_max_columns < MINIMUM_GRAPH_COLUMNS) {\n> +\t\t\tdie(_(\"minimum columns is %d, unable to set below %d\"),\n> +\t\t\tMINIMUM_GRAPH_COLUMNS,\n> +\t\t\trevs->graph_max_columns);\n\nShouldn't we allow users to set 0? That combined with an unsigned int\nwould:\n1. remove the need for MINIMUM_GRAPH_COLUMNS\n2. allow users to specify that they do not want a column limit\n\n> +\t\t}\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> diff --git a/revision.h b/revision.h\n> index 69242ecb18..6442129c14 100644\n> --- a/revision.h\n> +++ b/revision.h\n> @@ -304,6 +304,7 @@ struct rev_info {\n>\n>  \t/* Display history graph */\n>  \tstruct git_graph *graph;\n> +\tint graph_max_columns;\n>\n\nI think it makes sense to make this an unsigned int.\n\n>  \t/* special limits */\n>  \tint skip_count;\n> diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh\n> index 28d0779a8c..6266de4e2b 100755\n> --- a/t/t4215-log-skewed-merges.sh\n> +++ b/t/t4215-log-skewed-merges.sh\n> @@ -370,4 +370,60 @@ test_expect_success 'log --graph with multiple tips' '\n>  \tEOF\n>  '\n>\n> +test_expect_success 'log --graph --graph-max=2 only two columns' '\n> +\tcheck_graph --graph-max=2 M_7 <<-\\EOF\n> +\t*-.   7_M4\n> +\t|\\ .\n> +\t| | * 7_G\n> +\t| | * 7_F\n> +\t| * . 7_E\n> +\t| * . 7_D\n> +\t* | . 7_C\n> +\t| |/\n> +\t|/|\n> +\t* | 7_B\n> +\t|/\n> +\t* 7_A\n> +\tEOF\n> +'\n> +\n> +test_expect_success 'log --graph --graph-max=3 only three columns' '\n> +\tcheck_graph --graph-max=3 M_1 M_3 M_5 M_7 <<-\\EOF\n> +\t*   7_M1\n> +\t|\\\n> +\t| | *   7_M2\n> +\t| | |.\n> +\t| | | * 7_H\n> +\t| | | | *   7_M3\n> +\t| | | | .\n> +\t| | | | | * 7_J\n> +\t| | | | *   7_I\n> +\t| | | | | | *   7_M4\n> +\t| |_|_|_|_|.\n> +\t|/| | .\n> +\t| | |_.\n> +\t| |/|_.\n> +\t| |/|_.\n> +\t| |/| .\n> +\t| | |/.\n> +\t| | * .     7_G\n> +\t| | | .\n> +\t| | |/.\n> +\t| | |/.\n> +\t| | * .   7_F\n> +\t| * | .   7_E\n> +\t| | |/.\n> +\t| |/| .\n> +\t| * | . 7_D\n> +\t| | |/\n> +\t| |/|\n> +\t* | | 7_C\n> +\t| |/\n> +\t|/|\n> +\t* | 7_B\n> +\t|/\n> +\t* 7_A\n> +\tEOF\n> +'\n> +\n>  test_done\n> --\n> 2.43.0\nv\n"},{"id":"539149","messageId":"CAN5EUNSzC9C3Sn3OP3df7Hur-S64khV7VVJoas138CfD4dcKpg@mail.gmail.com","threadId":"65267","inReplyTo":"CAOLa=ZSsC7zpfpRx8pShcqGEv_2_NMrKzJHCgTSaO=0Dg0xakg@mail.gmail.com","subject":"Re: [GSoC RFC PATCH] graph: add --graph-max option to limit displayed columns","fromName":"Pablo","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-16T19:48:25Z","receivedAt":"2026-03-16T19:48:40Z","isPatch":true,"body":"Thanks for the feedback,\n\n> Do you think '--graph-max' signifies that we're talking about the\n> maximum columns to display?\n\nYeah, doesn't  seem clear enough, but --graph-col-limit seems too\nverbose. What do you think about --graph-max-cols ? It's just one char\nless but I feel it is more readable.\n\n> One question to ask is, is this even needed anymore and does it really\n> make sense to add it?\n\n> The TODO was added back in 2008, that's ~16 years ago and was not\n> touched till now. So perhaps no one needs it? If so, maybe the smarter\n> option is to simply remove the TODO?\n\n> Or do you see a usecase where this is useful? If so, it would be nice to\n> talk about that in the commit message.\n\nI've run 'git log --graph --all' on the Git repo itself  on 'next' and\njust scrolling a bit\ndown up to March 5 there are already +35 branches, which isn't very readable.\n\nIt's been 16 years but I believe that is still a good thing to add, even if\na lot of graph viewing happens with third parties, adding this makes\nGit a bit more self sufficient. This is even more useful in a case where the\nthird party is not an option.\n\nThat said, if this still doesn't seem useful I would remove the TODO\nto avoid further confusion in the future.\n\n> My preference would be not to have implicit behavior but also at the\n> same time guide the user in the right path, so:\n\n>      $ git log --graph-max=4\n>      fatal: --graph-max used without --graph\n\nOk, will add this to v2.\n\n> Trying it out:\n> ...\n> So we still keep the spaces, but only remove the column indicator\n\nPadding needs to be adjusted to the columns truncated. Will add it to v2.\n\n> Isn't this more of `graphs_needs_truncation()`?\n\nYes, I'll rename it in v2.\n\n> Shouldn't we allow users to set 0? That combined with an unsigned int\n> would:\n> 1. remove the need for MINIMUM_GRAPH_COLUMNS\n> 2. allow users to specify that they do not want a column limit\n\nI think it's better not to let 0 be a good input, it's the same as not using it\nmaking it redundant. Is set to 0 by default from the memset().\nOther \"max\" options I've tried that allow 0, don't share the \"no limit\"\nbehaviour.\n  git log --graph --all --max-parents=0\n  git log --max-count=0\nThat's why I wouldn't let it be valid.\nThe unsigned int is better for clarity.\n\n> I think overall a little more explanation in the commit message makes it\n> easier to understand the context and also helps reviewers!\n>\n> Shouldn't we also talk about the todo and the commit (c12172d2ea (Add\n> history graph API, 2008-05-04)) in which it was added?\n\n> The first sentence seems to talk about the option like it already\n> exists, when the second para introduces it. It would be nice if the\n> first para explained the problem we're trying to solve and why and the\n> second para then dove into the solution space.\n\nMy bad, I'll improve the commit msg on v2 to be more clear about the\nproblem, what it does and where it comes from.\n\n> What does this mean? Validate how?\n\nIt validated user input to avoid them to set 0. In case of letting them\nset 0 (no limit) this would be removed.\n\nI'll wait for the decision about whether this TODO from 2008 is worth\ndoing before a v2.\n\nThanks,\nPablo\n"},{"id":"539265","messageId":"20260317220929.120746-1-pabloosabaterr@gmail.com","threadId":"65267","inReplyTo":"20260316133426.117684-1-pabloosabaterr@gmail.com","subject":"[GSoC RFC PATCH v2] graph: add --max-columns option to limit displayed columns","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-17T22:09:29Z","receivedAt":"2026-03-17T22:10:11Z","isPatch":true,"body":"Repositories that have many active branches produce very wide\noutputs with 'git log --graph --all', makes it difficult to read.\n\nAdd '--max-columns=<n>' to limit the columns shown. Columns over\nthe limit are replaced with a '.' truncation indicator. This only\naffects the visual rendering.\n\nThe commit mark '*' is only shown in two cases:\n- The commit is in a branch inside the limit.\n- The commit is in the first hidden branch, in this case '.'\n  is replaced by '*'.\n\nCommits on deeper hidden branches do not show '*' but the commit\nsubject is still shown, so no information is lost.\n\nThe original idea to limit columns was noted as a TODO in\nc12172d2ea (Add history graph API, 2008-05-04), which mentions\ngitk's behavior. This does not implement gitk-style column\nrearrangement; it only truncates the visual output.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n\n> I'll wait for the decision about whether this TODO from 2008 is worth\n> doing before a v2.\n\nI believe that it is better to send an improved version instead of waiting\nto show more clearly what I want to do, sorry for the confusion.\n\n> It's been 16 years but I believe that is still a good thing to add, even if\n> a lot of graph viewing happens with third parties, adding this makes\n> Git a bit more self sufficient. This is even more useful in a case where the\n> third party is not an option.\n\n> That said, if this still doesn't seem useful I would remove the TODO\n> to avoid further confusion in the future.\n\nI revert what I've said here. This doesn't actually reflects gitk behaviour, \nit only truncates the visual output and prettifies it. To actually fullfill the\nTODO, it should rearrange the columns what would mean to change the whole\nrendering algorithm of --graph. This prob why it has been a TODO for so long.\n\nI don't think the TODO should be removed with what I bring here. Therefore, I\nwont't remove it.\n\nThe actual TODO seems still a good project and a good improvement.\n\nexample:\n\ngit log --oneline --graph --all\n\n*   b81d099 (M_1) 7_M1\n|\\\n| | *   2d96ba0 (M_3) 7_M2\n| | |\\\n| | | * 993fbcd (7_4) 7_H\n| | | | *   d5b56db (M_5) 7_M3\n| | | | |\\\n| | | | | * b45d68d (7_6) 7_J\n| | | | * | 3f9ab79 (7_5) 7_I\n| | | | | | *   f89bbdc (HEAD -> M_7) 7_M4\n| |_|_|_|_|/|\\\n|/| | | | |/ /\n| | |_|_|/| /\n| |/| | | |/\n| | | |_|/|\n| | |/| | |\n| | * | | | f5cbb18 (7_3) 7_G\n| | | |_|/\n| | |/| |\n| | * | | 6ee728a 7_F\n| * | | | d0c5a67 (7_2) 7_E\n| | |/ /\n| |/| |\n| * | | c3bffce 7_D\n| | |/\n| |/|\n* | | df1a24f (7_1) 7_C\n| |/\n|/|\n* | 5d3f777 7_B\n|/\n* d309d64 7_A\n\nvs \n\ngit log --graph --all --oneline --max-columns=2\n\n*   b81d099 (M_1) 7_M1\n|\\\n| | * 2d96ba0 (M_3) 7_M2\n| | .\n| | . 993fbcd (7_4) 7_H\n| | . d5b56db (M_5) 7_M3\n| | .\n| | . b45d68d (7_6) 7_J\n| | . 3f9ab79 (7_5) 7_I\n| | . f89bbdc (HEAD -> M_7) 7_M4\n| |_.\n|/| .\n| | .\n| |/.\n| |/.\n| |/.\n| | .\n| | * f5cbb18 (7_3) 7_G\n| | .\n| | .\n| | .\n| | * 6ee728a 7_F\n| * . d0c5a67 (7_2) 7_E\n| | .\n| |/.\n| * . c3bffce 7_D\n| | .\n| |/.\n* | . df1a24f (7_1) 7_C\n| |/\n|/|\n* | 5d3f777 7_B\n|/\n* d309d64 7_A\n\nChanges since v1:\n- Renamed option from --graph-max to --max-columns\n- Require --graph for --max-columns, die without it\n- Fixed padding so commit text aligns correctly after truncation\n- Added pre-commit truncation so lines before the commit column\n  are also truncated when they exceed the limit\n- Fixed post-merge line spacing inconsistency\n- Used parse_long_opt/parse_count for input validation, matching\n  existing revision.c patterns\n- Reject negative and zero values with clear error messages\n- Renamed graph_is_truncated to graph_needs_truncation\n\nRFC :\n- I think --max-columns is a good name, but it does not have --graph\n  prefix, bcs it can only be on --graph I think it's clear enough.\n  I added checks in setup_revisions().\n- Is '.' fine as delimiter ? other options could be \"~\" or \"-\".\n\n graph.c                      | 57 +++++++++++++++++++++++++++++++++++-\n graph.h                      |  2 ++\n revision.c                   | 11 +++++++\n revision.h                   |  1 +\n t/t4215-log-skewed-merges.sh | 56 +++++++++++++++++++++++++++++++++++\n 5 files changed, 126 insertions(+), 1 deletion(-)\n\ndiff --git a/graph.c b/graph.c\nindex 26f6fbf000..6227c4f22f 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -317,6 +317,12 @@ struct git_graph {\n \tstruct strbuf prefix_buf;\n };\n \n+static int graph_needs_truncation(struct git_graph *graph, int col)\n+{\n+\tint max = graph->revs->graph_max_columns;\n+\treturn max > 0 && col >= max;\n+}\n+\n static const char *diff_output_prefix_callback(struct diff_options *opt, void *data)\n {\n \tstruct git_graph *graph = data;\n@@ -696,6 +702,15 @@ static void graph_update_columns(struct git_graph *graph)\n \t\t}\n \t}\n \n+\t/*\n+\t * If graph_max_columns is set, cap the padding from the branches\n+\t */\n+\tif (graph->revs->graph_max_columns > 0) {\n+\t\tint truncation = graph->revs->graph_max_columns * 2 + 2;\n+\t\tif (graph->width > truncation)\n+\t\t\tgraph->width = truncation;\n+\t}\n+\n \t/*\n \t * Shrink mapping_size to be the minimum necessary\n \t */\n@@ -846,6 +861,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\tif (graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addstr(line, \". \");\n+\t\t\tbreak;\n+\t\t}\n \t\tgraph_line_write_column(line, &graph->new_columns[i], '|');\n \t\tgraph_line_addch(line, ' ');\n \t}\n@@ -903,6 +922,9 @@ static void graph_output_pre_commit_line(struct git_graph *graph,\n \t\t\tseen_this = 1;\n \t\t\tgraph_line_write_column(line, col, '|');\n \t\t\tgraph_line_addchars(line, ' ', graph->expansion_row);\n+\t\t} else if (seen_this && graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addstr(line, \". \");\n+\t\t\tbreak;\n \t\t} else if (seen_this && (graph->expansion_row == 0)) {\n \t\t\t/*\n \t\t\t * This is the first line of the pre-commit output.\n@@ -1013,6 +1035,7 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \t * children that we have already processed.)\n \t */\n \tseen_this = 0;\n+\n \tfor (i = 0; i <= graph->num_columns; i++) {\n \t\tstruct column *col = &graph->columns[i];\n \t\tstruct commit *col_commit;\n@@ -1028,8 +1051,17 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \t\t\tseen_this = 1;\n \t\t\tgraph_output_commit_char(graph, line);\n \n+\t\t\tif (graph_needs_truncation(graph, i)) {\n+\t\t\t\tgraph_line_addch(line, ' ');\n+\t\t\t\tbreak;\n+\t\t\t}\n+\n \t\t\tif (graph->num_parents > 2)\n \t\t\t\tgraph_draw_octopus_merge(graph, line);\n+\t\t} else if (graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addstr(line, \". \");\n+\t\t\tseen_this = 1;\n+\t\t\tbreak;\n \t\t} else if (seen_this && (graph->edges_added > 1)) {\n \t\t\tgraph_line_write_column(line, col, '\\\\');\n \t\t} else if (seen_this && (graph->edges_added == 1)) {\n@@ -1109,9 +1141,17 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n \t\t\tint par_column;\n \t\t\tint idx = graph->merge_layout;\n \t\t\tchar c;\n+\t\t\tint truncated = 0;\n \t\t\tseen_this = 1;\n \n \t\t\tfor (j = 0; j < graph->num_parents; j++) {\n+\t\t\t\tif (graph_needs_truncation(graph, i + j)) {\n+\t\t\t\t\tif (j > 0)\n+\t\t\t\t\t\tgraph_line_addch(line, ' ');\n+\t\t\t\t\tgraph_line_addstr(line, \". \");\n+\t\t\t\t\ttruncated = 1;\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n \t\t\t\tpar_column = graph_find_new_column_by_commit(graph, parents->item);\n \t\t\t\tassert(par_column >= 0);\n \n@@ -1125,9 +1165,13 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n \t\t\t\t}\n \t\t\t\tparents = next_interesting_parent(graph, parents);\n \t\t\t}\n+\t\t\tif (truncated)\n+\t\t\t\tbreak;\n \t\t\tif (graph->edges_added == 0)\n \t\t\t\tgraph_line_addch(line, ' ');\n-\n+\t\t} else if (graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addstr(line, \". \");\n+\t\t\tbreak;\n \t\t} else if (seen_this) {\n \t\t\tif (graph->edges_added > 0)\n \t\t\t\tgraph_line_write_column(line, col, '\\\\');\n@@ -1279,6 +1323,12 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n \t */\n \tfor (i = 0; i < graph->mapping_size; i++) {\n \t\tint target = graph->mapping[i];\n+\n+\t\tif (graph_needs_truncation(graph, i / 2)) {\n+\t\t\tgraph_line_addstr(line, \". \");\n+\t\t\tbreak;\n+\t\t}\n+\n \t\tif (target < 0)\n \t\t\tgraph_line_addch(line, ' ');\n \t\telse if (target * 2 == i)\n@@ -1372,6 +1422,11 @@ static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)\n \tfor (i = 0; i < graph->num_columns; i++) {\n \t\tstruct column *col = &graph->columns[i];\n \n+\t\tif (graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addch(&line, '.');\n+\t\t\tbreak;\n+\t\t}\n+\n \t\tgraph_line_write_column(&line, col, '|');\n \n \t\tif (col->commit == graph->commit && graph->num_parents > 2) {\ndiff --git a/graph.h b/graph.h\nindex 3fd1dcb2e9..9a4551dd29 100644\n--- a/graph.h\n+++ b/graph.h\n@@ -262,4 +262,6 @@ 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+#define MINIMUM_GRAPH_COLUMNS 1\n+\n #endif /* GRAPH_H */\ndiff --git a/revision.c b/revision.c\nindex 31808e3df0..e8d38cb2a1 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -2605,6 +2605,13 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg\n \t} else if (!strcmp(arg, \"--no-graph\")) {\n \t\tgraph_clear(revs->graph);\n \t\trevs->graph = NULL;\n+\t} else if ((argcount = parse_long_opt(\"max-columns\", argv, &optarg))) {\n+\t\tint val = parse_count(optarg);\n+\t\tif (val < MINIMUM_GRAPH_COLUMNS)\n+\t\t\tdie(_(\"minimum columns is %d, cannot be set to %d\"),\n+\t\t\t    MINIMUM_GRAPH_COLUMNS, val);\n+\t\trevs->graph_max_columns = val;\n+\t\treturn argcount;\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@@ -3172,6 +3179,10 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s\n \n \tif (revs->no_walk && revs->graph)\n \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--no-walk\", \"--graph\");\n+\n+\tif (revs->graph_max_columns > 0 && !revs->graph)\n+\t\tdie(_(\"option '%s' requires '%s'\"), \"--max-columns\", \"--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 69242ecb18..f15b390289 100644\n--- a/revision.h\n+++ b/revision.h\n@@ -304,6 +304,7 @@ struct rev_info {\n \n \t/* Display history graph */\n \tstruct git_graph *graph;\n+\tunsigned int graph_max_columns;\n \n \t/* special limits */\n \tint skip_count;\ndiff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh\nindex 28d0779a8c..ca2b224cc4 100755\n--- a/t/t4215-log-skewed-merges.sh\n+++ b/t/t4215-log-skewed-merges.sh\n@@ -370,4 +370,60 @@ test_expect_success 'log --graph with multiple tips' '\n \tEOF\n '\n \n+test_expect_success 'log --graph --max-columns=2 only two columns' '\n+\tcheck_graph --max-columns=2 M_7 <<-\\EOF\n+\t*-.   7_M4\n+\t|\\  .\n+\t| | * 7_G\n+\t| | * 7_F\n+\t| * . 7_E\n+\t| * . 7_D\n+\t* | . 7_C\n+\t| |/\n+\t|/|\n+\t* | 7_B\n+\t|/\n+\t* 7_A\n+\tEOF\n+'\n+\n+test_expect_success 'log --graph --max-columns=3 only three columns' '\n+\tcheck_graph --max-columns=3 M_1 M_3 M_5 M_7 <<-\\EOF\n+\t*   7_M1\n+\t|\\\n+\t| | *   7_M2\n+\t| | | .\n+\t| | | * 7_H\n+\t| | | . 7_M3\n+\t| | | .\n+\t| | | . 7_J\n+\t| | | . 7_I\n+\t| | | . 7_M4\n+\t| |_|_.\n+\t|/| | .\n+\t| | |_.\n+\t| |/|_.\n+\t| |/|_.\n+\t| |/| .\n+\t| | |/.\n+\t| | * . 7_G\n+\t| | | .\n+\t| | |/.\n+\t| | |/.\n+\t| | * . 7_F\n+\t| * | . 7_E\n+\t| | |/.\n+\t| |/| .\n+\t| * | . 7_D\n+\t| | |/\n+\t| |/|\n+\t* | | 7_C\n+\t| |/\n+\t|/|\n+\t* | 7_B\n+\t|/\n+\t* 7_A\n+\tEOF\n+'\n+\n test_done\n-- \n2.43.0\n\n"},{"id":"539292","messageId":"xmqqzf45gm2q.fsf@gitster.g","threadId":"65267","inReplyTo":"20260317220929.120746-1-pabloosabaterr@gmail.com","subject":"Re: [GSoC RFC PATCH v2] graph: add --max-columns option to limit displayed columns","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-18T16:05:17Z","receivedAt":"2026-03-18T16:05:22Z","isPatch":true,"body":"Pablo Sabater <pabloosabaterr@gmail.com> writes:\n\n> Repositories that have many active branches produce very wide\n> outputs with 'git log --graph --all', makes it difficult to read.\n\n\"making it difficult\"?\n\n> Add '--max-columns=<n>' to limit the columns shown. Columns over\n> the limit are replaced with a '.' truncation indicator. This only\n> affects the visual rendering.\n\nBeing an option to \"git log\", I expect that readers would naturally\ntake \"--max-columns=<n>\" to refer to the total display width\nconsumed by the entire output including the log messages and\npossibly patches when the command is run with the \"-p\" option,\nwhether the ancestry graph is shown or not.\n\nBut somehow I suspect that it is not what is going on here.\n\nIf this <n> refers to the width of graph part only, the option name\nshould hint that fact somehow.  Perhaps include the word \"graph\" in\nit, or something.  As you use the verb \"limit\" below, perhaps\n\"--limit-graph-columns=<n>\"?\n\n> The commit mark '*' is only shown in two cases:\n> - The commit is in a branch inside the limit.\n> - The commit is in the first hidden branch, in this case '.'\n>   is replaced by '*'.\n\n> Commits on deeper hidden branches do not show '*' but the commit\n> subject is still shown, so no information is lost.\n\nIsn't \"no information is lost\" a huge exaggeration?  We are losing\nthe ancestry information by not drawing graph lines, aren't we?\n\n> The original idea to limit columns was noted as a TODO in\n> c12172d2ea (Add history graph API, 2008-05-04), which mentions\n> gitk's behavior. This does not implement gitk-style column\n> rearrangement; it only truncates the visual output.\n\nTrue.  \n\nIIRC, Gitk also allows you to click the chopped arrow-head to jump\nto the other end of the omitted ancestry line, which is very useful\nbut is hard to do on a terminal output that is not interactive.\n\n> git log --graph --all --oneline --max-columns=2\n> *   b81d099 (M_1) 7_M1\n> |\\\n> | | * 2d96ba0 (M_3) 7_M2\n> | | .\n\nI would call this output consuming 5 columns for graph part, not 2.\nFor end users, being able to specify 2, i.e., being able to say \"I\ntolerate wasting display columns to show up to two ancestry lines\"\n(or \"two lanes of ancestry information\"), is indeed a lot more\nintuitive than having to say \"You are allowed to use up to 5 display\ncolumns\", so I do not object to an option that takes \"2\" as its\nvalue and produces the above output, but I am not sure if we want to\nhave \"columns\" in the name of such an option; \"--limit-graph-lanes=2\"?\n\n> - I think --max-columns is a good name, but it does not have --graph\n>   prefix, bcs it can only be on --graph I think it's clear enough.\n\nIf \"git log --max-columns=77\" ignores the option because \"--graph\"\nis not given, it would be confusing to the users.\n\n> +\t/*\n> +\t * If graph_max_columns is set, cap the padding from the branches\n> +\t */\n> +\tif (graph->revs->graph_max_columns > 0) {\n> +\t\tint truncation = graph->revs->graph_max_columns * 2 + 2;\n\nThis needs a bit more commenting to explain where these magic\nnumbers come from; they are of the same value 2 but have different\nmeanings, right?  Like this (only to illustrate the shape, not\nsuggesting what the contents should read):\n\n\t\t/*\n\t\t * Each ancestry \"lane\" occupies 2 columns, and\n\t\t * we leave two columns before drawing the commit\n\t\t * title and log message part.\n\t\t */\n\t\tint max_column_width = \n\t\t\tgraph->revs->graph_limit_lanes * 2 + 2;\n\n> +\n> +\tif (revs->graph_max_columns > 0 && !revs->graph)\n> +\t\tdie(_(\"option '%s' requires '%s'\"), \"--max-columns\", \"--graph\");\n\nThe naming is so selfish.  Among \"git log\" options that exists and\nthat will be added in the future, this design decision declares that\n\"--graph\" is and will remain to be the only one that may want to\nspecify the maximum number of columns to spend.\n\nIf the option is named clearly to be related to the \"--graph\"\nfeature, another way to go is to make it imply \"--graph\".  If the\nuser says \"I want to limit the graph output to consume no more than\n10 leftmost columns\", it is clear that the user expects the graph to\nbe shown.\n"},{"id":"539305","messageId":"CAN5EUNT7co=ucbBRykXdLJDUdewvoh+cMVbbOOUuRTrv7j2u5A@mail.gmail.com","threadId":"65267","inReplyTo":"xmqqzf45gm2q.fsf@gitster.g","subject":"Re: [GSoC RFC PATCH v2] graph: add --max-columns option to limit displayed columns","fromName":"Pablo","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-18T18:20:02Z","receivedAt":"2026-03-18T18:20:19Z","isPatch":true,"body":"Thanks for the feedback!\n\nJunio C Hamano (<gitster@pobox.com>) writes:\n\n> \"making it difficult\"?\n\nI'll correct the typo\n\n> If this <n> refers to the width of graph part only, the option name\n> should hint that fact somehow.  Perhaps include the word \"graph\" in\n> it, or something.  As you use the verb \"limit\" below, perhaps\n> \"--limit-graph-columns=<n>\"?\n\n> I would call this output consuming 5 columns for graph part, not 2.\n> For end users, being able to specify 2, i.e., being able to say \"I\n> tolerate wasting display columns to show up to two ancestry lines\"\n> (or \"two lanes of ancestry information\"), is indeed a lot more\n> intuitive than having to say \"You are allowed to use up to 5 display\n> columns\", so I do not object to an option that takes \"2\" as its\n> value and produces the above output, but I am not sure if we want to\n> have \"columns\" in the name of such an option; \"--limit-graph-lanes=2\"?\n\nYes, it only affects the graph rendering, I'll change the name to be\nmore clear about it is for the graph only and that what is being\nmodified are the lanes, \"|\" + \" \" not actual single char columns.\nWhat about --graph-limit-lanes and make it imply --graph as you said\nif it's clear enough?\n\n> Isn't \"no information is lost\" a huge exaggeration?  We are losing\n> the ancestry information by not drawing graph lines, aren't we?\n\nYeah, I meant that most of the commit information is not lost, only\nwhere they come from if it's a deeper hidden lane. But what I wanted\nto say is that internally I'm not removing any information about the\ngraph and that every commit will show no matter the lane limit.\n\n> IIRC, Gitk also allows you to click the chopped arrow-head to jump\n> to the other end of the omitted ancestry line, which is very useful\n> but is hard to do on a terminal output that is not interactive.\n\nYes, that's why it's been sooo long without being done prob. But I\nthink this approach is still useful, it can be extended later for more\ncustomization like limiting the lines from the left side or ranges.\nFor lane rearrangement I would need to study graph.c more and look\nforward to refactoring a lot in multiple patches.\n\n> If \"git log --max-columns=77\" ignores the option because \"--graph\"\n> is not given, it would be confusing to the users.\n\nWith the check on 'setup_revision()' users won't be able to\n'max-columns=77' if there's no '--graph'\n\n> This needs a bit more commenting to explain where these magic\n> numbers come from; they are of the same value 2 but have different\n> meanings, right?  Like this (only to illustrate the shape, not\n> suggesting what the contents should read):\n>\n>                 /*\n>                  * Each ancestry \"lane\" occupies 2 columns, and\n>                  * we leave two columns before drawing the commit\n>                  * title and log message part.\n>                  */\n>                 int max_column_width =\n>                         graph->revs->graph_limit_lanes * 2 + 2;\n\nthe magic numbers are because, a lane is two columns '|' + ' ', and\nthe +2 comes from the truncation mark '.' + ' ', so for a 3 lanes\nlimit, the padding from the graph should be 3 * 2 + 2 = 8. I'll add a\ncomment to doc the magic numbers.\n\n> > +\n> > +     if (revs->graph_max_columns > 0 && !revs->graph)\n> > +             die(_(\"option '%s' requires '%s'\"), \"--max-columns\", \"--graph\");\n>\n> The naming is so selfish.  Among \"git log\" options that exists and\n> that will be added in the future, this design decision declares that\n> \"--graph\" is and will remain to be the only one that may want to\n> specify the maximum number of columns to spend.\n>\n> If the option is named clearly to be related to the \"--graph\"\n> feature, another way to go is to make it imply \"--graph\".  If the\n> user says \"I want to limit the graph output to consume no more than\n> 10 leftmost columns\", it is clear that the user expects the graph to\n> be shown.\n\nI'll make the name clearer about what it does and less selfish. I like\nthe idea about --graph-limit-lanes to imply --graph directly and not\nforce it to be explicit.\nMaking --graph-limit-lanes imply --graph removes the check at revision_setup()\n\nI'll send a v3\n"},{"id":"539368","messageId":"42b146ed-1c53-4b4b-9ead-99d924bec501@kdbg.org","threadId":"65267","inReplyTo":"CAN5EUNT7co=ucbBRykXdLJDUdewvoh+cMVbbOOUuRTrv7j2u5A@mail.gmail.com","subject":"Re: [GSoC RFC PATCH v2] graph: add --max-columns option to limit displayed columns","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2026-03-19T07:07:33Z","receivedAt":"2026-03-19T07:07:50Z","isPatch":true,"body":"Am 18.03.26 um 19:20 schrieb Pablo:\n> Junio C Hamano (<gitster@pobox.com>) writes:>> If the option is named clearly to be related to the \"--graph\"\n>> feature, another way to go is to make it imply \"--graph\".  If the\n>> user says \"I want to limit the graph output to consume no more than\n>> 10 leftmost columns\", it is clear that the user expects the graph to\n>> be shown.\n> \n> I'll make the name clearer about what it does and less selfish. I like\n> the idea about --graph-limit-lanes to imply --graph directly and not\n> force it to be explicit.\nDon't let this option imply --graph. It specifies a parameter that could\nalso reasonably be specified via a configuration. But we don't want that\nthe existence of the hypothetical configuration implies --graph.\n\nHow about --edge-limit or --lane-limit?\n\n-- Hannes\n\n"},{"id":"539668","messageId":"20260322195406.108280-1-pabloosabaterr@gmail.com","threadId":"65267","inReplyTo":"20260317220929.120746-1-pabloosabaterr@gmail.com","subject":"[GSoC PATCH WIP RFC v3 0/3] graph: add --graph-lane-limit option","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-22T19:54:03Z","receivedAt":"2026-03-22T19:54:23Z","isPatch":true,"body":"When viewing the history of a repository that has many branches, the graph output\ncan become very wide easily, making it difficult to read and sometimes pushing the \ncommit messages to the right and breaking the format. \n\nIntroduce --graph-lane-limit option to truncate the graph at n + 1/2 lanes (n lanes\nplus the column between the lane and the truncation mark). Lanes over the limit\nare replaced with '.' keeping the graph readable and shows all the information\nabout the visible lanes.\n\ncommit mark '*' is shown in two cases:\n\n- When the commit lives on a visible lane.\n- When the commit lives on the first hidden lane (n+1) this case instead of\n  '.' it will show '*' to show that there is a commit in the first hidden lane.\n\nAny commit on deeper branches won't show any mark, but the commit subject will \nstill be shown.\n\nComing from graph_output_commit_line, merge-lanes with no relevant information\non the visible lanes (neither commit or parent lives on a visible lane) are\nskipped making the graph more compact.\n\nThe original idea to limit columns was noted as a TODO in\nc12172d2ea (Add history graph API, 2008-05-04), which mentions\ngitk's behavior. This does not implement gitk-style column\nrearrangement; it only truncates the visual output.\n\nRFC and WIP:\n\n1. On lane-limited graphs, there are collapsing, merge and padding lanes that don't\n   add any information to the graph.\n\n     - graph_output_collapsing_line mixes state handling and graph rendering, skipping\n       this function entirely, as is done with post-merge, would corrupt the mapping\n       so the function needs to be run but returning an empty buffer is not expected\n       by the callers and produces blank lanes.\n\n     - Suppressing redundant lanes like merges or collapses that come from graph_next_line().\n       graph_show_remainder() and graph_show_commit_msg() emits newlines hoping that\n       all lines will have content to print, returning an empty buffer to them\n       results in blank lines because of newlines being duplicated. graph_show_remainder\n       would be called from two places with different newlines needs, to make\n       this work graph_show_remainder() needs to be refactored. Doing this will result\n       in shorter graphs with less redundant lines on lane-limited graphs. Given that\n       the graph.c code is 17 years old is this desired ?\n\n2. To keep the most information rendered on lane-limited graphs, collapsing and\n   merges are rendered as usual but when the collapse or merge to hidden lanes, \n   the information about from where and to where it goes it lost, are they still\n   useful ? if not, it could be desirable to remove them like in point 1.\n\nPablo Sabater (3):\n  graph: add --graph-lane-limit option\n  graph: truncate graph visual output\n  graph: add documentation and testing about --graph-lane-limit\n\n Documentation/rev-list-options.adoc |   5 ++\n graph.c                             | 130 ++++++++++++++++++++++++----\n graph.h                             |   2 +\n revision.c                          |  11 +++\n revision.h                          |   1 +\n t/t4215-log-skewed-merges.sh        |  53 ++++++++++++\n 6 files changed, 185 insertions(+), 17 deletions(-)\n\n\nbase-commit: dc6ecd5354dca88d51b6d6562777fc8fc10d77e1\n-- \n2.43.0\n\n"},{"id":"539669","messageId":"20260322203801.637769-1-pabloosabaterr@gmail.com","threadId":"65267","inReplyTo":"20260322195406.108280-1-pabloosabaterr@gmail.com","subject":"[GSoC PATCH WIP RFC v3 1/3] graph: add --graph-lane-limit option","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-22T20:37:59Z","receivedAt":"2026-03-22T20:38:11Z","isPatch":true,"body":"Repositories that have many active branches produce very wide outputs\nwith 'git log --graph --all' making it difficult to read.\n\nAdd MINIMUM_GRAPH_COLUMNS = 1\n\nAdd '--graph-lane-limit=<n>' to the revision options, this option\nneeds --graph explicitly and rejects values under MINIMUM_GRAPH_COLUMNS.\n\nAdd graph_max_lanes to rev_info and store what the user set\n\nAdd graph_needs_truncation() and teach it to know when a column is\nover the limit following graph_max_lanes, if the limit is 0, treat it like\nno limit.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c    |  6 ++++++\n graph.h    |  2 ++\n revision.c | 11 +++++++++++\n revision.h |  1 +\n 4 files changed, 20 insertions(+)\n\ndiff --git a/graph.c b/graph.c\nindex 26f6fbf000..a95c0a9a73 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -317,6 +317,12 @@ struct git_graph {\n \tstruct strbuf prefix_buf;\n };\n \n+static int graph_needs_truncation(struct git_graph *graph, int col)\n+{\n+\tint max = graph->revs->graph_max_lanes;\n+\treturn max > 0 && col >= max;\n+}\n+\n static const char *diff_output_prefix_callback(struct diff_options *opt, void *data)\n {\n \tstruct git_graph *graph = data;\ndiff --git a/graph.h b/graph.h\nindex 3fd1dcb2e9..9a4551dd29 100644\n--- a/graph.h\n+++ b/graph.h\n@@ -262,4 +262,6 @@ 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+#define MINIMUM_GRAPH_COLUMNS 1\n+\n #endif /* GRAPH_H */\ndiff --git a/revision.c b/revision.c\nindex 31808e3df0..aeddf2d166 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -2605,6 +2605,13 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg\n \t} else if (!strcmp(arg, \"--no-graph\")) {\n \t\tgraph_clear(revs->graph);\n \t\trevs->graph = NULL;\n+\t} else if ((argcount = parse_long_opt(\"graph-lane-limit\", argv, &optarg))) {\n+\t\tint max_lanes = parse_count(optarg);\n+\t\tif (max_lanes < MINIMUM_GRAPH_COLUMNS)\n+\t\t\tdie(_(\"minimum lanes is %d, cannot be set to %d\"),\n+\t\t\t    MINIMUM_GRAPH_COLUMNS, max_lanes);\n+\t\trevs->graph_max_lanes = max_lanes;\n+\t\treturn argcount;\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@@ -3172,6 +3179,10 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s\n \n \tif (revs->no_walk && revs->graph)\n \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--no-walk\", \"--graph\");\n+\n+\tif (revs->graph_max_lanes > 0 && !revs->graph)\n+\t\tdie(_(\"option '%s' requires '%s'\"), \"--graph-lane-limit\", \"--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 69242ecb18..597116f885 100644\n--- a/revision.h\n+++ b/revision.h\n@@ -304,6 +304,7 @@ struct rev_info {\n \n \t/* Display history graph */\n \tstruct git_graph *graph;\n+\tunsigned int graph_max_lanes;\n \n \t/* special limits */\n \tint skip_count;\n-- \n2.43.0\n\n"},{"id":"539670","messageId":"20260322203801.637769-2-pabloosabaterr@gmail.com","threadId":"65267","inReplyTo":"20260322203801.637769-1-pabloosabaterr@gmail.com","subject":"[GSoC PATCH WIP RFC v3 2/3] graph: truncate graph visual output","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-22T20:38:00Z","receivedAt":"2026-03-22T20:38:13Z","isPatch":true,"body":"Teach graph statuses to stop rendering and add the truncation mark\n'.' once they are writing over the lane limit, following\ngraph_needs_truncation().\n\nTeach graph_output_commit_line to skip the POST_MERGE status when\nneither the commit nor the parent is on a visible lane but keep it\nwhen either commit or parent lives in a visible lane.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c | 124 ++++++++++++++++++++++++++++++++++++++++++++++++--------\n 1 file changed, 107 insertions(+), 17 deletions(-)\n\ndiff --git a/graph.c b/graph.c\nindex a95c0a9a73..ab0a008af5 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -702,6 +702,20 @@ static void graph_update_columns(struct git_graph *graph)\n \t\t}\n \t}\n \n+\t/*\n+\t * If graph_max_lanes is set, cap the padding from the branches\n+\t */\n+\tif (graph->revs->graph_max_lanes > 0) {\n+\t\t/*\n+\t\t * Get the maximum width by multiplying the maximum number of\n+\t\t * lanes by the size of the lane \"| \" and adds the truncation\n+\t\t * mark \". \"\n+\t\t */\n+\t\tint max_columns_width = graph->revs->graph_max_lanes * 2 + 2;\n+\t\tif (graph->width > max_columns_width)\n+\t\t\tgraph->width = max_columns_width;\n+\t}\n+\n \t/*\n \t * Shrink mapping_size to be the minimum necessary\n \t */\n@@ -852,6 +866,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\tif (graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addstr(line, \". \");\n+\t\t\tbreak;\n+\t\t}\n \t\tgraph_line_write_column(line, &graph->new_columns[i], '|');\n \t\tgraph_line_addch(line, ' ');\n \t}\n@@ -909,6 +927,9 @@ static void graph_output_pre_commit_line(struct git_graph *graph,\n \t\t\tseen_this = 1;\n \t\t\tgraph_line_write_column(line, col, '|');\n \t\t\tgraph_line_addchars(line, ' ', graph->expansion_row);\n+\t\t} else if (seen_this && graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addstr(line, \". \");\n+\t\t\tbreak;\n \t\t} else if (seen_this && (graph->expansion_row == 0)) {\n \t\t\t/*\n \t\t\t * This is the first line of the pre-commit output.\n@@ -1019,6 +1040,7 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \t * children that we have already processed.)\n \t */\n \tseen_this = 0;\n+\n \tfor (i = 0; i <= graph->num_columns; i++) {\n \t\tstruct column *col = &graph->columns[i];\n \t\tstruct commit *col_commit;\n@@ -1034,8 +1056,17 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \t\t\tseen_this = 1;\n \t\t\tgraph_output_commit_char(graph, line);\n \n+\t\t\tif (graph_needs_truncation(graph, i)) {\n+\t\t\t\tgraph_line_addch(line, ' ');\n+\t\t\t\tbreak;\n+\t\t\t}\n+\n \t\t\tif (graph->num_parents > 2)\n \t\t\t\tgraph_draw_octopus_merge(graph, line);\n+\t\t} else if (graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addstr(line, \". \");\n+\t\t\tseen_this = 1;\n+\t\t\tbreak;\n \t\t} else if (seen_this && (graph->edges_added > 1)) {\n \t\t\tgraph_line_write_column(line, col, '\\\\');\n \t\t} else if (seen_this && (graph->edges_added == 1)) {\n@@ -1071,10 +1102,32 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \n \t/*\n \t * Update graph->state\n-\t */\n-\tif (graph->num_parents > 1)\n-\t\tgraph_update_state(graph, GRAPH_POST_MERGE);\n-\telse if (graph_is_mapping_correct(graph))\n+\t *\n+\t * If the commit is a merge and the first parent is in a visible lane,\n+\t * then the GRAPH_POST_MERGE is needed to draw the merge lane.\n+\t * \n+\t * If the commit is over the truncation limit, but the first parent is on\n+\t * a visible lane, then we still need the merge lane but truncated.\n+\t * \n+\t * If both commit and first parent are over the truncation limit, then\n+\t * there's no need to draw the merge lane because it would work as a\n+\t * padding lane.\n+\t */\n+\tif (graph->num_parents > 1) {\n+\t\tif (!graph_needs_truncation(graph, graph->commit_index)) {\n+\t\t\tgraph_update_state(graph, GRAPH_POST_MERGE);\n+\t\t} else {\n+\t\t\tstruct commit_list *first_parent = first_interesting_parent(graph);\n+\t\t\tint first_parent_col = graph_find_new_column_by_commit(graph, first_parent->item);\n+\t\t\t\n+\t\t\tif (!graph_needs_truncation(graph, first_parent_col))\n+\t\t\t\tgraph_update_state(graph, GRAPH_POST_MERGE);\n+\t\t\telse if (graph_is_mapping_correct(graph))\n+\t\t\t\tgraph_update_state(graph, GRAPH_PADDING);\n+\t\t\telse\n+\t\t\t\tgraph_update_state(graph, GRAPH_COLLAPSING);\n+\t\t}\n+\t} else if (graph_is_mapping_correct(graph))\n \t\tgraph_update_state(graph, GRAPH_PADDING);\n \telse\n \t\tgraph_update_state(graph, GRAPH_COLLAPSING);\n@@ -1115,14 +1168,28 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n \t\t\tint par_column;\n \t\t\tint idx = graph->merge_layout;\n \t\t\tchar c;\n+\t\t\tint truncated = 0;\n \t\t\tseen_this = 1;\n \n \t\t\tfor (j = 0; j < graph->num_parents; j++) {\n+\t\t\t\tunsigned int truncation_max = i + (j > 1 ? j - 1 : 0);\n \t\t\t\tpar_column = graph_find_new_column_by_commit(graph, parents->item);\n \t\t\t\tassert(par_column >= 0);\n \n \t\t\t\tc = merge_chars[idx];\n \t\t\t\tgraph_line_write_column(line, &graph->new_columns[par_column], c);\n+\n+\t\t\t\tif (j >= 2)\n+\t\t\t\t\ttruncation_max -= 1;\n+\n+\t\t\t\tif (graph_needs_truncation(graph, truncation_max)) {\n+\t\t\t\t\tif (j > 0 && !(graph->edges_added > 0))\n+\t\t\t\t\t\tgraph_line_addch(line, ' ');\n+\t\t\t\t\tgraph_line_addstr(line, \". \");\n+\t\t\t\t\ttruncated = 1;\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\n \t\t\t\tif (idx == 2) {\n \t\t\t\t\tif (graph->edges_added > 0 || j < graph->num_parents - 1)\n \t\t\t\t\t\tgraph_line_addch(line, ' ');\n@@ -1131,15 +1198,24 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n \t\t\t\t}\n \t\t\t\tparents = next_interesting_parent(graph, parents);\n \t\t\t}\n+\t\t\tif (truncated)\n+\t\t\t\tbreak;\n \t\t\tif (graph->edges_added == 0)\n \t\t\t\tgraph_line_addch(line, ' ');\n-\n+\t\t} else if (graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addstr(line, \". \");\n+\t\t\tbreak;\n \t\t} else if (seen_this) {\n \t\t\tif (graph->edges_added > 0)\n \t\t\t\tgraph_line_write_column(line, col, '\\\\');\n \t\t\telse\n \t\t\t\tgraph_line_write_column(line, col, '|');\n-\t\t\tgraph_line_addch(line, ' ');\n+\t\t\t/*\n+\t\t\t * If it's between two lanes and next would be truncated,\n+\t\t\t * don't add space padding.\n+\t\t\t */\n+\t\t\tif (!graph_needs_truncation(graph, i + 1)) \n+\t\t\t\tgraph_line_addch(line, ' ');\n \t\t} else {\n \t\t\tgraph_line_write_column(line, col, '|');\n \t\t\tif (graph->merge_layout != 0 || i != graph->commit_index - 1) {\n@@ -1170,6 +1246,7 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n \tshort used_horizontal = 0;\n \tint horizontal_edge = -1;\n \tint horizontal_edge_target = -1;\n+\tint truncated = 0;\n \n \t/*\n \t * Swap the mapping and old_mapping arrays\n@@ -1285,12 +1362,20 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n \t */\n \tfor (i = 0; i < graph->mapping_size; i++) {\n \t\tint target = graph->mapping[i];\n-\t\tif (target < 0)\n-\t\t\tgraph_line_addch(line, ' ');\n-\t\telse if (target * 2 == i)\n-\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '|');\n-\t\telse if (target == horizontal_edge_target &&\n-\t\t\t i != horizontal_edge - 1) {\n+\n+\t\tif (!truncated && graph_needs_truncation(graph, i / 2)) {\n+\t\t\tgraph_line_addstr(line, \". \");\n+\t\t\ttruncated = 1;\n+\t\t}\n+\n+\t\tif (target < 0) {\n+\t\t\tif (!truncated)\n+\t\t\t\tgraph_line_addch(line, ' ');\n+\t\t} else if (target * 2 == i) {\n+\t\t\tif (!truncated)\n+\t\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '|');\n+\t\t} else if (target == horizontal_edge_target &&\n+\t\t\t   i != horizontal_edge - 1) {\n \t\t\t\t/*\n \t\t\t\t * Set the mappings for all but the\n \t\t\t\t * first segment to -1 so that they\n@@ -1298,13 +1383,14 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n \t\t\t\t */\n \t\t\t\tif (i != (target * 2)+3)\n \t\t\t\t\tgraph->mapping[i] = -1;\n-\t\t\t\tused_horizontal = 1;\n-\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '_');\n+\t\t\tused_horizontal = 1;\n+\t\t\tif (!truncated)\n+\t\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '_');\n \t\t} else {\n \t\t\tif (used_horizontal && i < horizontal_edge)\n \t\t\t\tgraph->mapping[i] = -1;\n-\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '/');\n-\n+\t\t\tif (!truncated)\n+\t\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '/');\n \t\t}\n \t}\n \n@@ -1353,7 +1439,6 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb)\n \t\tgraph_output_collapsing_line(graph, &line);\n \t\tbreak;\n \t}\n-\n \tgraph_pad_horizontally(graph, &line);\n \treturn shown_commit_line;\n }\n@@ -1378,6 +1463,11 @@ static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)\n \tfor (i = 0; i < graph->num_columns; i++) {\n \t\tstruct column *col = &graph->columns[i];\n \n+\t\tif (graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addch(&line, '.');\n+\t\t\tbreak;\n+\t\t}\n+\n \t\tgraph_line_write_column(&line, col, '|');\n \n \t\tif (col->commit == graph->commit && graph->num_parents > 2) {\n-- \n2.43.0\n\n"},{"id":"539671","messageId":"20260322203801.637769-3-pabloosabaterr@gmail.com","threadId":"65267","inReplyTo":"20260322203801.637769-1-pabloosabaterr@gmail.com","subject":"[GSoC PATCH WIP RFC v3 3/3] graph: add documentation and testing about --graph-lane-limit","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-22T20:38:01Z","receivedAt":"2026-03-22T20:38:15Z","isPatch":true,"body":"Document --graph-lane-limit option in rev-list-options.adoc with\n--graph option.\n\nAdd two tests in t4215 reusing last test graph structure.\n\n- --graph-lane-limit=2 on one tip showing only two rendered\n  lanes and the rest replaced with the truncation\n  marker.\n\n- --graph-lane-limit=3 with multiple tips, showing only three\n  rendered lanes.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n Documentation/rev-list-options.adoc |  5 +++\n t/t4215-log-skewed-merges.sh        | 53 +++++++++++++++++++++++++++++\n 2 files changed, 58 insertions(+)\n\ndiff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc\nindex 2d195a1474..1819228b60 100644\n--- a/Documentation/rev-list-options.adoc\n+++ b/Documentation/rev-list-options.adoc\n@@ -1259,6 +1259,11 @@ This implies the `--topo-order` option by default, but the\n \tin between them in that case. If _<barrier>_ is specified, it\n \tis the string that will be shown instead of the default one.\n \n+`graph-lane-limit=<n>`::\n+\tWhen `--graph` is used, limit the number of graph lanes to be shown.\n+\tLanes over the limit are replaced with a truncation mark '.'. By default\n+\tthere is no limit.\n+\n ifdef::git-rev-list[]\n `--count`::\n \tPrint a number stating how many commits would have been\ndiff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh\nindex 28d0779a8c..657e3ff2a5 100755\n--- a/t/t4215-log-skewed-merges.sh\n+++ b/t/t4215-log-skewed-merges.sh\n@@ -370,4 +370,57 @@ test_expect_success 'log --graph with multiple tips' '\n \tEOF\n '\n \n+test_expect_success 'log --graph --graph-lane-limit=2 limited to two columns' '\n+\tcheck_graph --graph-lane-limit=2 M_7 <<-\\EOF\n+\t*-.   7_M4\n+\t|\\ \\\n+\t| | * 7_G\n+\t| | * 7_F\n+\t| * . 7_E\n+\t| * . 7_D\n+\t* | . 7_C\n+\t| |/\n+\t|/|\n+\t* | 7_B\n+\t|/\n+\t* 7_A\n+\tEOF\n+'\n+\n+test_expect_success 'log --graph --graph-lane-limit=3 limited to three columns' '\n+\tcheck_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+\t| | |\\\n+\t| | | * 7_H\n+\t| | | . 7_M3\n+\t| | | . 7_J\n+\t| | | . 7_I\n+\t| | | . 7_M4\n+\t| |_|_.\n+\t|/| | .\n+\t| | |_.\n+\t| |/| .\n+\t| | | .\n+\t| | |/.\n+\t| | * . 7_G\n+\t| | | .\n+\t| | |/.\n+\t| | * . 7_F\n+\t| * | . 7_E\n+\t| | |/.\n+\t| |/| .\n+\t| * | . 7_D\n+\t| | |/\n+\t| |/|\n+\t* | | 7_C\n+\t| |/\n+\t|/|\n+\t* | 7_B\n+\t|/\n+\t* 7_A\n+\tEOF\n+'\n+\n test_done\n-- \n2.43.0\n\n"},{"id":"539677","messageId":"xmqqeclb7byr.fsf@gitster.g","threadId":"65267","inReplyTo":"20260322203801.637769-1-pabloosabaterr@gmail.com","subject":"Re: [GSoC PATCH WIP RFC v3 1/3] graph: add --graph-lane-limit option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-22T22:09:48Z","receivedAt":"2026-03-22T22:09:50Z","isPatch":true,"body":"Pablo Sabater <pabloosabaterr@gmail.com> writes:\n\n> Repositories that have many active branches produce very wide outputs\n> with 'git log --graph --all' making it difficult to read.\n\nIf you have active branches, whether they are merged to some very\nsmall number of integration branches or they are left updating\nwithout getting merged for a long time, you'd end up getting very\nwide output from \"log --all --graph\"?  Or is this a problem only\nwhen these active branches are merged and having to show merge\ncommits create the need for wider output?  I cannot quite see which\nfrom the above description.\n\n>\n> Add MINIMUM_GRAPH_COLUMNS = 1\n\nThis unfinished sentence looks a bit out of place.  It is unclear\nhow that variable about \"columns\" related to \"lane\" that is\nintroduced in the next paratraph.\n\n> Add '--graph-lane-limit=<n>' to the revision options, this option\n> needs --graph explicitly and rejects values under MINIMUM_GRAPH_COLUMNS.\n\n\"and rejects values under ...\" -> \"has to be at least 1\".  And the\nprevious paragraph that consists of a single unfinished sentence can\nbe discarded.\n\nThe implementation detail that you happened to choose a C\nproprocessor macro instead of hardcoded constatnt to write the lower\nlimit is not something readers of the log message needs to know.\nThey can see that in the \"log -p\" output easily.  \n\nWhat is more helpful for readers to know is what you mean by\n\"graph-lane\", what you are counting, and why you want its lower\nlimit to 1 (instead of 0 or 2).  These reasoning behind the design\nis much more important to record to help future developers who want\nto fix bugs in this code or who want to extend the feature this code\nadds, without violating the underlying assumption and design goals\nof the original author (i.e., you).\n\n> Add graph_max_lanes to rev_info and store what the user set\n>\n> Add graph_needs_truncation() and teach it to know when a column is\n> over the limit following graph_max_lanes, if the limit is 0, treat it like\n> no limit.\n>\n> Signed-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n> ---\n>  graph.c    |  6 ++++++\n>  graph.h    |  2 ++\n>  revision.c | 11 +++++++++++\n>  revision.h |  1 +\n>  4 files changed, 20 insertions(+)\n>\n> diff --git a/graph.c b/graph.c\n> index 26f6fbf000..a95c0a9a73 100644\n> --- a/graph.c\n> +++ b/graph.c\n> @@ -317,6 +317,12 @@ struct git_graph {\n>  \tstruct strbuf prefix_buf;\n>  };\n>  \n> +static int graph_needs_truncation(struct git_graph *graph, int col)\n> +{\n> +\tint max = graph->revs->graph_max_lanes;\n> +\treturn max > 0 && col >= max;\n> +}\n> +\n>  static const char *diff_output_prefix_callback(struct diff_options *opt, void *data)\n>  {\n>  \tstruct git_graph *graph = data;\n> diff --git a/graph.h b/graph.h\n> index 3fd1dcb2e9..9a4551dd29 100644\n> --- a/graph.h\n> +++ b/graph.h\n> @@ -262,4 +262,6 @@ 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> +#define MINIMUM_GRAPH_COLUMNS 1\n> +\n>  #endif /* GRAPH_H */\n> diff --git a/revision.c b/revision.c\n> index 31808e3df0..aeddf2d166 100644\n> --- a/revision.c\n> +++ b/revision.c\n> @@ -2605,6 +2605,13 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg\n>  \t} else if (!strcmp(arg, \"--no-graph\")) {\n>  \t\tgraph_clear(revs->graph);\n>  \t\trevs->graph = NULL;\n> +\t} else if ((argcount = parse_long_opt(\"graph-lane-limit\", argv, &optarg))) {\n> +\t\tint max_lanes = parse_count(optarg);\n> +\t\tif (max_lanes < MINIMUM_GRAPH_COLUMNS)\n> +\t\t\tdie(_(\"minimum lanes is %d, cannot be set to %d\"),\n> +\t\t\t    MINIMUM_GRAPH_COLUMNS, max_lanes);\n> +\t\trevs->graph_max_lanes = max_lanes;\n> +\t\treturn argcount;\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> @@ -3172,6 +3179,10 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s\n>  \n>  \tif (revs->no_walk && revs->graph)\n>  \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--no-walk\", \"--graph\");\n> +\n> +\tif (revs->graph_max_lanes > 0 && !revs->graph)\n> +\t\tdie(_(\"option '%s' requires '%s'\"), \"--graph-lane-limit\", \"--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>  \n> diff --git a/revision.h b/revision.h\n> index 69242ecb18..597116f885 100644\n> --- a/revision.h\n> +++ b/revision.h\n> @@ -304,6 +304,7 @@ struct rev_info {\n>  \n>  \t/* Display history graph */\n>  \tstruct git_graph *graph;\n> +\tunsigned int graph_max_lanes;\n\nThis is \"unsigned int\"; don't we want the other places (like the\non-stack local variable handle_revision_opt() uses to parse the\nvalue from the command line) and the parameter used in\ngraph_needs_truncation() helper function all consistently use the\nsame type?\n\n>  \t/* special limits */\n>  \tint skip_count;\n"},{"id":"539685","messageId":"CAN5EUNQ8rnBAezRLgATwotnw4EC--EAa3p+52nWb4KCtB7uySQ@mail.gmail.com","threadId":"65267","inReplyTo":"xmqqeclb7byr.fsf@gitster.g","subject":"Re: [GSoC PATCH WIP RFC v3 1/3] graph: add --graph-lane-limit option","fromName":"Pablo","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-23T02:33:25Z","receivedAt":"2026-03-23T02:33:40Z","isPatch":true,"body":"Junio C Hamano (<gitster@pobox.com>) writes:\n\n> If you have active branches, whether they are merged to some very\n> small number of integration branches or they are left updating\n> without getting merged for a long time, you'd end up getting very\n> wide output from \"log --all --graph\"?  Or is this a problem only\n> when these active branches are merged and having to show merge\n> commits create the need for wider output?  I cannot quite see which\n> from the above description.\n\nThe graph gets wide based on the branches active at the same time,\neach one occupies two columns, merges don't create new lanes.\nI'll improve the problem description.\n\n> The implementation detail that you happened to choose a C\n> proprocessor macro instead of hardcoded constatnt to write the lower\n> limit is not something readers of the log message needs to know.\n> They can see that in the \"log -p\" output easily.\n>\n> What is more helpful for readers to know is what you mean by\n> \"graph-lane\", what you are counting, and why you want its lower\n> limit to 1 (instead of 0 or 2).  These reasoning behind the design\n> is much more important to record to help future developers who want\n> to fix bugs in this code or who want to extend the feature this code\n> adds, without violating the underlying assumption and design goals\n> of the original author (i.e., you).\n\nOk, I'll focus more on why, rather than how.\nthe minimum is set to 1 to have at least 1 visible lane, even though it could\naccept 0 it's the same as not using this option, 0 it's treated as no limit\nand I found it better to not give the users the option to place it\nbecause no other option\nseems to behave like this, so I thought the best would be to force the\ninput to be >= 1 to be valid.\nin v4 I'll make sure that this is clear for others.\n\n> This is \"unsigned int\"; don't we want the other places (like the\n> on-stack local variable handle_revision_opt() uses to parse the\n> value from the command line) and the parameter used in\n> graph_needs_truncation() helper function all consistently use the\n> same type?\n\nI saw that other options like max_count, min/max_parents use int instead\nof unsigned int, so yes the most consistent would be to have it as\nint also, but it would make no sense to have -1 visible lanes. I thought\nit would be a good idea to keep it explicit that it can't be negative.\nthis examples, max_count, etc were the closest examples I saw, but\nparse_count does return an int so i can't cast it to unsigned without\nchecking if its neg that's why this\n\n> int max_lanes = parse_count(optarg);\n> if (max_lanes < MINIMUM_GRAPH_COLUMNS)\n>         die(_(\"minimum lanes is %d, cannot be set to %d\"),\n>                   MINIMUM_GRAPH_COLUMNS, max_lanes);\n> revs->graph_max_lanes = max_lanes;\n\nwhere it checks if it's < 1, now max_lanes has to be > 1 and it fits in an\nunsigned int. But i do understand to keep the consistency and\nthe coding guidelines, i'll make it an int.\n\nThanks for the feedback!\n"},{"id":"539784","messageId":"20260323215935.74486-1-pabloosabaterr@gmail.com","threadId":"65267","inReplyTo":"20260322195406.108280-1-pabloosabaterr@gmail.com","subject":"[GSoC PATCH v4 0/3] graph: add --graph-lane-limit option","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-23T21:59:32Z","receivedAt":"2026-03-23T21:59:42Z","isPatch":true,"body":"Repositories that have many active branches at the same time produce\nwide graphs. A lane consists of two columns, the edge and the space\npadding, each branch takes a lane in the graph and there is no way\nto limit how many can be shown.\n\nAdd '--graph-lane-limit=<n>' option that caps the visible lanes to n. Lanes over\nthis limit are replaced with '.' truncation mark.\n\nThe '*' commit mark is visible when it lives on a visible lane or the first\nhidden lane, any deeper lane doesn't show the commit mark but keeps the commit\nmessage visible. \n\nMerges where neither the commit nor its parents live on a visible lane are skipped\nbecause they dont carry any visible information.\n\nThe original idea to limit columns was noted as a TODO in c12172d2ea\n(Add history graph API, 2008-05-04).  This does not implement\ngitk-style column rearrangement, it only truncates the visual output.\n\nPossible future improvements:\n\n- When all branches involved in collapsing or padding are over the limit, the\n  truncated lane doesn't show any information, this lane could be removed to\n  make the graph more compact. Currently these lanes still appear because\n  graph_output_collapsing_line() mixes state handling with rendering, so it can't\n  be skipped but callers always expect a non empty buffer. Fixing it would need\n  to refactor the callers to handle empty buffers instead of expecting them to\n  always have content.\n\n- Collapsing and merges lanes that start on visible lanes but end on hidden ones\n  are kept to maintain the most information possible on the visible lanes, but\n  the information about where they go is lost. They can be kept, removed or think\n  of a way to show that information without showing the lanes.\n\nChanges since v3:\n\n- Rewrote commit messages to focus on reasoning rather than the\n  implementation details.\n- Changed graph_max_lanes to int for consistency with other\n  rev_info fields like max_count.\n- Zero and negative values are now accepted and silently treated\n  as \"no limit\", following what --max-parents does with negative values.\n- Removed the MINIMUM_GRAPH_COLUMNS macro.\n- Fixed missing \"--\" prefix in the documentation.\n\nPablo Sabater (3):\n  graph: add --graph-lane-limit option\n  graph: truncate graph visual output\n  graph: add documentation and tests about --graph-lane-limit\n\n Documentation/rev-list-options.adoc |   5 ++\n graph.c                             | 131 ++++++++++++++++++++++++----\n revision.c                          |   6 ++\n revision.h                          |   1 +\n t/t4215-log-skewed-merges.sh        |  53 +++++++++++\n 5 files changed, 180 insertions(+), 16 deletions(-)\n\n\nbase-commit: dc6ecd5354dca88d51b6d6562777fc8fc10d77e1\n-- \n2.43.0\n\n"},{"id":"539785","messageId":"20260323215935.74486-2-pabloosabaterr@gmail.com","threadId":"65267","inReplyTo":"20260323215935.74486-1-pabloosabaterr@gmail.com","subject":"[GSoC PATCH v4 1/3] graph: add --graph-lane-limit option","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-23T21:59:33Z","receivedAt":"2026-03-23T21:59:43Z","isPatch":true,"body":"Repositories that have many active branches at the same time produce\nwide graphs. A lane consists of two columns, the edge and the space\npadding, each branch takes a lane in the graph and there is no way\nto limit how many can be shown.\n\nAdd '--graph-lane-limit=<n>' revision option that caps the number\nof visible lanes to n. This option requires '--graph', without it\na limit to the graph has no meaning, in this case error out.\n\nZero and negative values are valid inputs but silently ignored\ntreating them as \"no limit\", the same as not using the option.\nThis follows what '--max-parents' does with negative values.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c    | 9 +++++++++\n revision.c | 6 ++++++\n revision.h | 1 +\n 3 files changed, 16 insertions(+)\n\ndiff --git a/graph.c b/graph.c\nindex 26f6fbf000..e7c1151ac0 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -317,6 +317,15 @@ struct git_graph {\n \tstruct strbuf prefix_buf;\n };\n \n+static int graph_needs_truncation(struct git_graph *graph, int lane)\n+{\n+\tint max = graph->revs->graph_max_lanes;\n+\t/*\n+\t * Ignore values <= 0, meaning no limit.\n+\t */\n+\treturn max > 0 && lane >= max;\n+}\n+\n static const char *diff_output_prefix_callback(struct diff_options *opt, void *data)\n {\n \tstruct git_graph *graph = data;\ndiff --git a/revision.c b/revision.c\nindex 31808e3df0..952edb031e 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -2605,6 +2605,8 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg\n \t} else if (!strcmp(arg, \"--no-graph\")) {\n \t\tgraph_clear(revs->graph);\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, \"--encode-email-headers\")) {\n \t\trevs->encode_email_headers = 1;\n \t} else if (!strcmp(arg, \"--no-encode-email-headers\")) {\n@@ -3172,6 +3174,10 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s\n \n \tif (revs->no_walk && revs->graph)\n \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--no-walk\", \"--graph\");\n+\n+\tif (revs->graph_max_lanes > 0 && !revs->graph)\n+\t\tdie(_(\"option '%s' requires '%s'\"), \"--graph-lane-limit\", \"--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 69242ecb18..874ccce625 100644\n--- a/revision.h\n+++ b/revision.h\n@@ -304,6 +304,7 @@ struct rev_info {\n \n \t/* Display history graph */\n \tstruct git_graph *graph;\n+\tint graph_max_lanes;\n \n \t/* special limits */\n \tint skip_count;\n-- \n2.43.0\n\n"},{"id":"539786","messageId":"20260323215935.74486-3-pabloosabaterr@gmail.com","threadId":"65267","inReplyTo":"20260323215935.74486-1-pabloosabaterr@gmail.com","subject":"[GSoC PATCH v4 2/3] graph: truncate graph visual output","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-23T21:59:34Z","receivedAt":"2026-03-23T21:59:45Z","isPatch":true,"body":"When '--graph-lane-limit' is set, lanes over the limit should\nnot be drawn. Teach each graph state to stop rendering at the\nlane limit and print a '.' truncation mark, so users know that\nthere are hidden lanes.\n\nOn the commit line, if the commit lives on a visible lane, show\nthe normal commit mark and truncate after it, if the commit lives\non the first hidden lane (the truncation mark lane) show the \"*\"\ninstead of the truncation mark so it is known that this commit sits\non the first hidden lane. Commits on deeper lanes don't leave\na mark.\n\nFor merges, the post-merge lane is only needed when the commit or\nthe first parent lives on a visible lane (to draw the connection\nbetween them), when both are on hidden lanes, post-merge carries no\nuseful information, skip it and go to collapsing or padding state.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c | 122 ++++++++++++++++++++++++++++++++++++++++++++++++--------\n 1 file changed, 106 insertions(+), 16 deletions(-)\n\ndiff --git a/graph.c b/graph.c\nindex e7c1151ac0..886ef7cede 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -705,6 +705,20 @@ static void graph_update_columns(struct git_graph *graph)\n \t\t}\n \t}\n \n+\t/*\n+\t * If graph_max_lanes is set, cap the padding from the branches\n+\t */\n+\tif (graph->revs->graph_max_lanes > 0) {\n+\t\t/*\n+\t\t * Get the maximum width by multiplying the maximum number of\n+\t\t * lanes by the size of the lane \"| \" and adds the truncation\n+\t\t * mark \". \"\n+\t\t */\n+\t\tint max_columns_width = graph->revs->graph_max_lanes * 2 + 2;\n+\t\tif (graph->width > max_columns_width)\n+\t\t\tgraph->width = max_columns_width;\n+\t}\n+\n \t/*\n \t * Shrink mapping_size to be the minimum necessary\n \t */\n@@ -855,6 +869,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\tif (graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addstr(line, \". \");\n+\t\t\tbreak;\n+\t\t}\n \t\tgraph_line_write_column(line, &graph->new_columns[i], '|');\n \t\tgraph_line_addch(line, ' ');\n \t}\n@@ -912,6 +930,9 @@ static void graph_output_pre_commit_line(struct git_graph *graph,\n \t\t\tseen_this = 1;\n \t\t\tgraph_line_write_column(line, col, '|');\n \t\t\tgraph_line_addchars(line, ' ', graph->expansion_row);\n+\t\t} else if (seen_this && graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addstr(line, \". \");\n+\t\t\tbreak;\n \t\t} else if (seen_this && (graph->expansion_row == 0)) {\n \t\t\t/*\n \t\t\t * This is the first line of the pre-commit output.\n@@ -1022,6 +1043,7 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \t * children that we have already processed.)\n \t */\n \tseen_this = 0;\n+\n \tfor (i = 0; i <= graph->num_columns; i++) {\n \t\tstruct column *col = &graph->columns[i];\n \t\tstruct commit *col_commit;\n@@ -1037,8 +1059,17 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \t\t\tseen_this = 1;\n \t\t\tgraph_output_commit_char(graph, line);\n \n+\t\t\tif (graph_needs_truncation(graph, i)) {\n+\t\t\t\tgraph_line_addch(line, ' ');\n+\t\t\t\tbreak;\n+\t\t\t}\n+\n \t\t\tif (graph->num_parents > 2)\n \t\t\t\tgraph_draw_octopus_merge(graph, line);\n+\t\t} else if (graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addstr(line, \". \");\n+\t\t\tseen_this = 1;\n+\t\t\tbreak;\n \t\t} else if (seen_this && (graph->edges_added > 1)) {\n \t\t\tgraph_line_write_column(line, col, '\\\\');\n \t\t} else if (seen_this && (graph->edges_added == 1)) {\n@@ -1074,10 +1105,32 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \n \t/*\n \t * Update graph->state\n+\t *\n+\t * If the commit is a merge and the first parent is in a visible lane,\n+\t * then the GRAPH_POST_MERGE is needed to draw the merge lane.\n+\t *\n+\t * If the commit is over the truncation limit, but the first parent is on\n+\t * a visible lane, then we still need the merge lane but truncated.\n+\t *\n+\t * If both commit and first parent are over the truncation limit, then\n+\t * there's no need to draw the merge lane because it would work as a\n+\t * padding lane.\n \t */\n-\tif (graph->num_parents > 1)\n-\t\tgraph_update_state(graph, GRAPH_POST_MERGE);\n-\telse if (graph_is_mapping_correct(graph))\n+\tif (graph->num_parents > 1) {\n+\t\tif (!graph_needs_truncation(graph, graph->commit_index)) {\n+\t\t\tgraph_update_state(graph, GRAPH_POST_MERGE);\n+\t\t} else {\n+\t\t\tstruct commit_list *first_parent = first_interesting_parent(graph);\n+\t\t\tint first_parent_col = graph_find_new_column_by_commit(graph, first_parent->item);\n+\n+\t\t\tif (!graph_needs_truncation(graph, first_parent_col))\n+\t\t\t\tgraph_update_state(graph, GRAPH_POST_MERGE);\n+\t\t\telse if (graph_is_mapping_correct(graph))\n+\t\t\t\tgraph_update_state(graph, GRAPH_PADDING);\n+\t\t\telse\n+\t\t\t\tgraph_update_state(graph, GRAPH_COLLAPSING);\n+\t\t}\n+\t} else if (graph_is_mapping_correct(graph))\n \t\tgraph_update_state(graph, GRAPH_PADDING);\n \telse\n \t\tgraph_update_state(graph, GRAPH_COLLAPSING);\n@@ -1118,14 +1171,28 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n \t\t\tint par_column;\n \t\t\tint idx = graph->merge_layout;\n \t\t\tchar c;\n+\t\t\tint truncated = 0;\n \t\t\tseen_this = 1;\n \n \t\t\tfor (j = 0; j < graph->num_parents; j++) {\n+\t\t\t\tunsigned int truncation_max = i + (j > 1 ? j - 1 : 0);\n \t\t\t\tpar_column = graph_find_new_column_by_commit(graph, parents->item);\n \t\t\t\tassert(par_column >= 0);\n \n \t\t\t\tc = merge_chars[idx];\n \t\t\t\tgraph_line_write_column(line, &graph->new_columns[par_column], c);\n+\n+\t\t\t\tif (j >= 2)\n+\t\t\t\t\ttruncation_max -= 1;\n+\n+\t\t\t\tif (graph_needs_truncation(graph, truncation_max)) {\n+\t\t\t\t\tif (j > 0 && !(graph->edges_added > 0))\n+\t\t\t\t\t\tgraph_line_addch(line, ' ');\n+\t\t\t\t\tgraph_line_addstr(line, \". \");\n+\t\t\t\t\ttruncated = 1;\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\n \t\t\t\tif (idx == 2) {\n \t\t\t\t\tif (graph->edges_added > 0 || j < graph->num_parents - 1)\n \t\t\t\t\t\tgraph_line_addch(line, ' ');\n@@ -1134,15 +1201,24 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n \t\t\t\t}\n \t\t\t\tparents = next_interesting_parent(graph, parents);\n \t\t\t}\n+\t\t\tif (truncated)\n+\t\t\t\tbreak;\n \t\t\tif (graph->edges_added == 0)\n \t\t\t\tgraph_line_addch(line, ' ');\n-\n+\t\t} else if (graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addstr(line, \". \");\n+\t\t\tbreak;\n \t\t} else if (seen_this) {\n \t\t\tif (graph->edges_added > 0)\n \t\t\t\tgraph_line_write_column(line, col, '\\\\');\n \t\t\telse\n \t\t\t\tgraph_line_write_column(line, col, '|');\n-\t\t\tgraph_line_addch(line, ' ');\n+\t\t\t/*\n+\t\t\t * If it's between two lanes and next would be truncated,\n+\t\t\t * don't add space padding.\n+\t\t\t */\n+\t\t\tif (!graph_needs_truncation(graph, i + 1))\n+\t\t\t\tgraph_line_addch(line, ' ');\n \t\t} else {\n \t\t\tgraph_line_write_column(line, col, '|');\n \t\t\tif (graph->merge_layout != 0 || i != graph->commit_index - 1) {\n@@ -1173,6 +1249,7 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n \tshort used_horizontal = 0;\n \tint horizontal_edge = -1;\n \tint horizontal_edge_target = -1;\n+\tint truncated = 0;\n \n \t/*\n \t * Swap the mapping and old_mapping arrays\n@@ -1288,12 +1365,20 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n \t */\n \tfor (i = 0; i < graph->mapping_size; i++) {\n \t\tint target = graph->mapping[i];\n-\t\tif (target < 0)\n-\t\t\tgraph_line_addch(line, ' ');\n-\t\telse if (target * 2 == i)\n-\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '|');\n-\t\telse if (target == horizontal_edge_target &&\n-\t\t\t i != horizontal_edge - 1) {\n+\n+\t\tif (!truncated && graph_needs_truncation(graph, i / 2)) {\n+\t\t\tgraph_line_addstr(line, \". \");\n+\t\t\ttruncated = 1;\n+\t\t}\n+\n+\t\tif (target < 0) {\n+\t\t\tif (!truncated)\n+\t\t\t\tgraph_line_addch(line, ' ');\n+\t\t} else if (target * 2 == i) {\n+\t\t\tif (!truncated)\n+\t\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '|');\n+\t\t} else if (target == horizontal_edge_target &&\n+\t\t\t   i != horizontal_edge - 1) {\n \t\t\t\t/*\n \t\t\t\t * Set the mappings for all but the\n \t\t\t\t * first segment to -1 so that they\n@@ -1301,13 +1386,14 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n \t\t\t\t */\n \t\t\t\tif (i != (target * 2)+3)\n \t\t\t\t\tgraph->mapping[i] = -1;\n-\t\t\t\tused_horizontal = 1;\n-\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '_');\n+\t\t\tused_horizontal = 1;\n+\t\t\tif (!truncated)\n+\t\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '_');\n \t\t} else {\n \t\t\tif (used_horizontal && i < horizontal_edge)\n \t\t\t\tgraph->mapping[i] = -1;\n-\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '/');\n-\n+\t\t\tif (!truncated)\n+\t\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '/');\n \t\t}\n \t}\n \n@@ -1356,7 +1442,6 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb)\n \t\tgraph_output_collapsing_line(graph, &line);\n \t\tbreak;\n \t}\n-\n \tgraph_pad_horizontally(graph, &line);\n \treturn shown_commit_line;\n }\n@@ -1381,6 +1466,11 @@ static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)\n \tfor (i = 0; i < graph->num_columns; i++) {\n \t\tstruct column *col = &graph->columns[i];\n \n+\t\tif (graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addch(&line, '.');\n+\t\t\tbreak;\n+\t\t}\n+\n \t\tgraph_line_write_column(&line, col, '|');\n \n \t\tif (col->commit == graph->commit && graph->num_parents > 2) {\n-- \n2.43.0\n\n"},{"id":"539787","messageId":"20260323215935.74486-4-pabloosabaterr@gmail.com","threadId":"65267","inReplyTo":"20260323215935.74486-1-pabloosabaterr@gmail.com","subject":"[GSoC PATCH v4 3/3] graph: add documentation and tests about --graph-lane-limit","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-23T21:59:35Z","receivedAt":"2026-03-23T21:59:47Z","isPatch":true,"body":"Document --graph-lane-limit option in rev-list-options.adoc with\n--graph option.\n\nAdd two tests in t4215 reusing existing graphs. The first test\nlimits to two lanes on a single tip and the second one limits to\nthree lanes on multiple tips. Both tests check that everything\nis truncated correctly.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n Documentation/rev-list-options.adoc |  5 +++\n t/t4215-log-skewed-merges.sh        | 53 +++++++++++++++++++++++++++++\n 2 files changed, 58 insertions(+)\n\ndiff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc\nindex 2d195a1474..2b5a1794cd 100644\n--- a/Documentation/rev-list-options.adoc\n+++ b/Documentation/rev-list-options.adoc\n@@ -1259,6 +1259,11 @@ This implies the `--topo-order` option by default, but the\n \tin between them in that case. If _<barrier>_ is specified, it\n \tis the string that will be shown instead of the default one.\n \n+`--graph-lane-limit=<n>`::\n+\tWhen `--graph` is used, limit the number of graph lanes to be shown.\n+\tLanes over the limit are replaced with a truncation mark '.'. By default\n+\tthere is no limit.\n+\n ifdef::git-rev-list[]\n `--count`::\n \tPrint a number stating how many commits would have been\ndiff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh\nindex 28d0779a8c..650701df42 100755\n--- a/t/t4215-log-skewed-merges.sh\n+++ b/t/t4215-log-skewed-merges.sh\n@@ -370,4 +370,57 @@ test_expect_success 'log --graph with multiple tips' '\n \tEOF\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+\t*-.   7_M4\n+\t|\\ \\\n+\t| | * 7_G\n+\t| | * 7_F\n+\t| * . 7_E\n+\t| * . 7_D\n+\t* | . 7_C\n+\t| |/\n+\t|/|\n+\t* | 7_B\n+\t|/\n+\t* 7_A\n+\tEOF\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+\t*   7_M1\n+\t|\\\n+\t| | *   7_M2\n+\t| | |\\\n+\t| | | * 7_H\n+\t| | | . 7_M3\n+\t| | | . 7_J\n+\t| | | . 7_I\n+\t| | | . 7_M4\n+\t| |_|_.\n+\t|/| | .\n+\t| | |_.\n+\t| |/| .\n+\t| | | .\n+\t| | |/.\n+\t| | * . 7_G\n+\t| | | .\n+\t| | |/.\n+\t| | * . 7_F\n+\t| * | . 7_E\n+\t| | |/.\n+\t| |/| .\n+\t| * | . 7_D\n+\t| | |/\n+\t| |/|\n+\t* | | 7_C\n+\t| |/\n+\t|/|\n+\t* | 7_B\n+\t|/\n+\t* 7_A\n+\tEOF\n+'\n+\n test_done\n-- \n2.43.0\n\n"},{"id":"539910","messageId":"acOIjm2KHXvopSQ/@szeder.dev","threadId":"65267","inReplyTo":"20260323215935.74486-2-pabloosabaterr@gmail.com","subject":"Re: [GSoC PATCH v4 1/3] graph: add --graph-lane-limit option","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2026-03-25T07:02:38Z","receivedAt":"2026-03-25T07:02:42Z","isPatch":true,"body":"On Mon, Mar 23, 2026 at 10:59:33PM +0100, Pablo Sabater wrote:\n> Repositories that have many active branches at the same time produce\n> wide graphs. A lane consists of two columns, the edge and the space\n> padding, each branch takes a lane in the graph and there is no way\n> to limit how many can be shown.\n> \n> Add '--graph-lane-limit=<n>' revision option that caps the number\n> of visible lanes to n. This option requires '--graph', without it\n> a limit to the graph has no meaning, in this case error out.\n> \n> Zero and negative values are valid inputs but silently ignored\n> treating them as \"no limit\", the same as not using the option.\n> This follows what '--max-parents' does with negative values.\n> \n> Signed-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n> ---\n>  graph.c    | 9 +++++++++\n>  revision.c | 6 ++++++\n>  revision.h | 1 +\n>  3 files changed, 16 insertions(+)\n> \n> diff --git a/graph.c b/graph.c\n> index 26f6fbf000..e7c1151ac0 100644\n> --- a/graph.c\n> +++ b/graph.c\n> @@ -317,6 +317,15 @@ struct git_graph {\n>  \tstruct strbuf prefix_buf;\n>  };\n>  \n> +static int graph_needs_truncation(struct git_graph *graph, int lane)\n> +{\n> +\tint max = graph->revs->graph_max_lanes;\n> +\t/*\n> +\t * Ignore values <= 0, meaning no limit.\n> +\t */\n> +\treturn max > 0 && lane >= max;\n> +}\n\nThis patch adds this static function, but it doesn't add any callers.\nThis breaks the build with DEVELOPER=1:\n\n  $ make DEVELOPER=1 graph.o\n      CC graph.o\n  graph.c:320:12: error: ‘graph_needs_truncation’ defined but not used [-Werror=unused-function]\n    320 | static int graph_needs_truncation(struct git_graph *graph, int lane)\n        |            ^~~~~~~~~~~~~~~~~~~~~~\n  cc1: all warnings being treated as errors\n  make: *** [Makefile:2923: graph.o] Error 1\n\n>  static const char *diff_output_prefix_callback(struct diff_options *opt, void *data)\n>  {\n>  \tstruct git_graph *graph = data;\n> diff --git a/revision.c b/revision.c\n> index 31808e3df0..952edb031e 100644\n> --- a/revision.c\n> +++ b/revision.c\n> @@ -2605,6 +2605,8 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg\n>  \t} else if (!strcmp(arg, \"--no-graph\")) {\n>  \t\tgraph_clear(revs->graph);\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, \"--encode-email-headers\")) {\n>  \t\trevs->encode_email_headers = 1;\n>  \t} else if (!strcmp(arg, \"--no-encode-email-headers\")) {\n> @@ -3172,6 +3174,10 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s\n>  \n>  \tif (revs->no_walk && revs->graph)\n>  \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--no-walk\", \"--graph\");\n> +\n> +\tif (revs->graph_max_lanes > 0 && !revs->graph)\n> +\t\tdie(_(\"option '%s' requires '%s'\"), \"--graph-lane-limit\", \"--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>  \n> diff --git a/revision.h b/revision.h\n> index 69242ecb18..874ccce625 100644\n> --- a/revision.h\n> +++ b/revision.h\n> @@ -304,6 +304,7 @@ struct rev_info {\n>  \n>  \t/* Display history graph */\n>  \tstruct git_graph *graph;\n> +\tint graph_max_lanes;\n>  \n>  \t/* special limits */\n>  \tint skip_count;\n> -- \n> 2.43.0\n> \n"},{"id":"539924","messageId":"fae2f8e3-029a-43c7-aa6e-45a452026853@kdbg.org","threadId":"65267","inReplyTo":"20260323215935.74486-1-pabloosabaterr@gmail.com","subject":"Re: [GSoC PATCH v4 0/3] graph: add --graph-lane-limit option","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2026-03-25T10:02:56Z","receivedAt":"2026-03-25T10:03:13Z","isPatch":true,"body":"Am 23.03.26 um 22:59 schrieb Pablo Sabater:\n> Repositories that have many active branches at the same time produce\n> wide graphs. A lane consists of two columns, the edge and the space\n> padding, each branch takes a lane in the graph and there is no way\n> to limit how many can be shown.\n\nGenerally, I like the goal of this patch series. However, the way in\nwhich it is presented and justified can be improved substantially, IMO.\n\nIt begins with the statement of what this patch series wants to achieve.\nIt is \"limit the width of the graph\", isn't it? It is not \"add\n--graph-lane-limit\"; that is just a tool to achieve the goal.\n\nTo help reviewers, you should present an example chart in the cover\nletter that shows the before- and after-state (with and without the user\nof the new option).\n\nAs far as the separation into patches is concerned, I see a few\nproblems. With the current separation is difficult to justify the\npatches. For example, the first patch adds prerequisites for a later\npatch, but it is unclear how these are used. The answer to the question\n\"Why do we need this?\" is simply \"because the next patch uses them\", but\nthis is a very weak justification, because the next questions are \"how\nare they used and why didn't you squash this into the next patch?\"\n\nLet me suggest a different separation.\n\n1. The first patch limits the graph width with a hard-coded limit, say\n15 lanes. It limits the graph *always*. Choose a limit that is large\nenough to pass all tests.\n\n2. The next patch adds --graph-lane-limit and its documentation. Let it\ndo its thing. Revert to the default limit value 0, i.e., unlimited.\n\n3. Next, add additional eye-candy. I am alluding to the line that marks\nwhere a graph lane was truncated.\n\n(4. If more detailed document is warranted, e.g., an example chart, do\nthis as a separate patch that can now show all bells and whistles that\nthe earlier commits have implemented. Whether this makes sense as a\nseparate step, or whether documentation grows with the earlier patches,\nis a judgement call.)\n\nAs far as commit messages are concerned, always, always provide an\nanswer to \"Why?\" for every detail.\n\n- Why do we want to limit the graph width?\n- Why is the hard-coded limit 15? (because it lets tests pass and is\nstill a useful limit; we'll make it dynamic later.)\n- Why do we always limit the graph width? (Because it makes this patch\nsimpler; we'll fix this later.)\n- Why does 0 mean unlimited? (Consistency with --max-parents.)\n- Why is the truncation marked with a fullstop \".\"? (...)\n\nI'll also look over the patches, but I don't do C code, so I can provide\nonly superficial comments, if any.\n\n-- Hannes\n\n"},{"id":"539925","messageId":"6b299cf5-acfd-4a56-87e7-db26743a3271@kdbg.org","threadId":"65267","inReplyTo":"20260323215935.74486-2-pabloosabaterr@gmail.com","subject":"Re: [GSoC PATCH v4 1/3] graph: add --graph-lane-limit option","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2026-03-25T10:03:44Z","receivedAt":"2026-03-25T10:03:47Z","isPatch":true,"body":"Am 23.03.26 um 22:59 schrieb Pablo Sabater:\n> @@ -3172,6 +3174,10 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s\n>  \n>  \tif (revs->no_walk && revs->graph)\n>  \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--no-walk\", \"--graph\");\n> +\n> +\tif (revs->graph_max_lanes > 0 && !revs->graph)\n> +\t\tdie(_(\"option '%s' requires '%s'\"), \"--graph-lane-limit\", \"--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\nYou help translators if you make the new error message format string\nexactly identical to the one that we see in the post-context.\n\n-- Hannes\n\n"},{"id":"539926","messageId":"19cef686-6287-4916-8fec-a9ffe33f7889@kdbg.org","threadId":"65267","inReplyTo":"20260323215935.74486-3-pabloosabaterr@gmail.com","subject":"Re: [GSoC PATCH v4 2/3] graph: truncate graph visual output","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2026-03-25T10:04:48Z","receivedAt":"2026-03-25T10:04:52Z","isPatch":true,"body":"Am 23.03.26 um 22:59 schrieb Pablo Sabater:\n> +\t/*\n> +\t * If graph_max_lanes is set, cap the padding from the branches\n> +\t */\n> +\tif (graph->revs->graph_max_lanes > 0) {\n> +\t\t/*\n> +\t\t * Get the maximum width by multiplying the maximum number of\n> +\t\t * lanes by the size of the lane \"| \" and adds the truncation\n> +\t\t * mark \". \"\n> +\t\t */\n> +\t\tint max_columns_width = graph->revs->graph_max_lanes * 2 + 2;\n\nPlease be to the point in the code comments. That there is a\nmultiplication and an addition, we can see in the code. Perhaps:\n\n\t\t/* width of \"| \" per lane plus truncation mark \". \" */\n\n> +\t\tif (graph->width > max_columns_width)\n> +\t\t\tgraph->width = max_columns_width;\n> +\t}\n> +\n>  \t/*\n>  \t * Shrink mapping_size to be the minimum necessary\n>  \t */\n\n> @@ -1022,6 +1043,7 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n>  \t * children that we have already processed.)\n>  \t */\n>  \tseen_this = 0;\n> +\n>  \tfor (i = 0; i <= graph->num_columns; i++) {\n>  \t\tstruct column *col = &graph->columns[i];\n>  \t\tstruct commit *col_commit;\n\nIs this empty line really needed?\n\n> +\t\t\t\tif (j >= 2)\n> +\t\t\t\t\ttruncation_max -= 1;\n\nI think it is more idiomatic way to write this as truncation_max--.\n\n> @@ -1288,12 +1365,20 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n>  \t */\n>  \tfor (i = 0; i < graph->mapping_size; i++) {\n>  \t\tint target = graph->mapping[i];\n> -\t\tif (target < 0)\n> -\t\t\tgraph_line_addch(line, ' ');\n> -\t\telse if (target * 2 == i)\n> -\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '|');\n> -\t\telse if (target == horizontal_edge_target &&\n> -\t\t\t i != horizontal_edge - 1) {\n> +\n> +\t\tif (!truncated && graph_needs_truncation(graph, i / 2)) {\n> +\t\t\tgraph_line_addstr(line, \". \");\n> +\t\t\ttruncated = 1;\n> +\t\t}\n> +\n> +\t\tif (target < 0) {\n> +\t\t\tif (!truncated)\n> +\t\t\t\tgraph_line_addch(line, ' ');\n> +\t\t} else if (target * 2 == i) {\n> +\t\t\tif (!truncated)\n> +\t\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '|');\n> +\t\t} else if (target == horizontal_edge_target &&\n> +\t\t\t   i != horizontal_edge - 1) {\n>  \t\t\t\t/*\n>  \t\t\t\t * Set the mappings for all but the\n>  \t\t\t\t * first segment to -1 so that they\n> @@ -1301,13 +1386,14 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n>  \t\t\t\t */\n>  \t\t\t\tif (i != (target * 2)+3)\n>  \t\t\t\t\tgraph->mapping[i] = -1;\n> -\t\t\t\tused_horizontal = 1;\n> -\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '_');\n> +\t\t\tused_horizontal = 1;\n> +\t\t\tif (!truncated)\n> +\t\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '_');\n\nHuh? The indentation of \"used_horizontal...\" changed. The reason is that\nthis whole if-branch is indented too far by one tab. Perhaps an initial\nclean-up commit that only fixes this indentation?\n\n>  \t\t} else {\n>  \t\t\tif (used_horizontal && i < horizontal_edge)\n>  \t\t\t\tgraph->mapping[i] = -1;\n> -\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '/');\n> -\n> +\t\t\tif (!truncated)\n> +\t\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '/');\n>  \t\t}\n>  \t}\n>  \n> @@ -1356,7 +1442,6 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb)\n>  \t\tgraph_output_collapsing_line(graph, &line);\n>  \t\tbreak;\n>  \t}\n> -\n>  \tgraph_pad_horizontally(graph, &line);\n>  \treturn shown_commit_line;\n>  }\n\nThis removal of an empty line isn't warranted, I think.\n\n-- Hannes\n\n"},{"id":"539927","messageId":"6cdcece0-8cc5-4c87-8727-6d3e17424a9e@kdbg.org","threadId":"65267","inReplyTo":"20260323215935.74486-4-pabloosabaterr@gmail.com","subject":"Re: [GSoC PATCH v4 3/3] graph: add documentation and tests about --graph-lane-limit","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2026-03-25T10:07:06Z","receivedAt":"2026-03-25T10:07:15Z","isPatch":true,"body":"Am 23.03.26 um 22:59 schrieb Pablo Sabater:\n> @@ -1259,6 +1259,11 @@ This implies the `--topo-order` option by default, but the\n>  \tin between them in that case. If _<barrier>_ is specified, it\n>  \tis the string that will be shown instead of the default one.\n>  \n> +`--graph-lane-limit=<n>`::\n> +\tWhen `--graph` is used, limit the number of graph lanes to be shown.\n> +\tLanes over the limit are replaced with a truncation mark '.'. By default\n> +\tthere is no limit.\n\nThis should probably mention that 0 means no limit.\n\n> +\n>  ifdef::git-rev-list[]\n>  `--count`::\n>  \tPrint a number stating how many commits would have been\n> diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh\n> index 28d0779a8c..650701df42 100755\n> --- a/t/t4215-log-skewed-merges.sh\n> +++ b/t/t4215-log-skewed-merges.sh\n> @@ -370,4 +370,57 @@ test_expect_success 'log --graph with multiple tips' '\n>  \tEOF\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> +\t*-.   7_M4\n> +\t|\\ \\\n> +\t| | * 7_G\n> +\t| | * 7_F\n> +\t| * . 7_E\n> +\t| * . 7_D\n> +\t* | . 7_C\n> +\t| |/\n> +\t|/|\n> +\t* | 7_B\n> +\t|/\n> +\t* 7_A\n\nI'm confused. If the lane limit is 2, why do we have actually have 3 lanes?\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> +\t*   7_M1\n> +\t|\\\n> +\t| | *   7_M2\n> +\t| | |\\\n> +\t| | | * 7_H\n> +\t| | | . 7_M3\n> +\t| | | . 7_J\n> +\t| | | . 7_I\n> +\t| | | . 7_M4\n> +\t| |_|_.\n> +\t|/| | .\n> +\t| | |_.\n> +\t| |/| .\n> +\t| | | .\n> +\t| | |/.\n> +\t| | * . 7_G\n> +\t| | | .\n> +\t| | |/.\n> +\t| | * . 7_F\n> +\t| * | . 7_E\n> +\t| | |/.\n> +\t| |/| .\n> +\t| * | . 7_D\n> +\t| | |/\n> +\t| |/|\n> +\t* | | 7_C\n> +\t| |/\n> +\t|/|\n> +\t* | 7_B\n> +\t|/\n> +\t* 7_A\n\nSame here. Why is there a fourth lane?\n\nOh! \"Truncation\" here does not mean that the vertial lines are cut off\nand are supposed to continue sometime later in the chart. It literally\nmeans that the *line* is truncated and just some stuff *on that line* is\nomitted.\n\nOuch! That was not what I was expecting. I thought that truncation means\nthat when the eye follows a line vertically, it finds the truncation\npoint of the line at some point, and then the continuation of that line\nis again some time later down the chart. The only clue which lanes are\nthe same would be the color, which would have to be remedied somehow.\n\nI don't know what to make of it. I have to reconsider.\n\n-- Hannes\n\n"},{"id":"539928","messageId":"CAN5EUNTTVB6Ou5H3_M2AYUTX4mi+CLrDS=W8tWv-hfgwTctrZg@mail.gmail.com","threadId":"65267","inReplyTo":"19cef686-6287-4916-8fec-a9ffe33f7889@kdbg.org","subject":"Re: [GSoC PATCH v4 2/3] graph: truncate graph visual output","fromName":"Pablo","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-25T11:19:45Z","receivedAt":"2026-03-25T11:20:02Z","isPatch":true,"body":"Johannes Sixt (<j6t@kdbg.org>) writes:\n>\n> Am 23.03.26 um 22:59 schrieb Pablo Sabater:\n> > +     /*\n> > +      * If graph_max_lanes is set, cap the padding from the branches\n> > +      */\n> > +     if (graph->revs->graph_max_lanes > 0) {\n> > +             /*\n> > +              * Get the maximum width by multiplying the maximum number of\n> > +              * lanes by the size of the lane \"| \" and adds the truncation\n> > +              * mark \". \"\n> > +              */\n> > +             int max_columns_width = graph->revs->graph_max_lanes * 2 + 2;\n>\n> Please be to the point in the code comments. That there is a\n> multiplication and an addition, we can see in the code. Perhaps:\n>\n>                 /* width of \"| \" per lane plus truncation mark \". \" */\n\nI'll use this one instead, thanks.\n\n> > +             if (graph->width > max_columns_width)\n> > +                     graph->width = max_columns_width;\n> > +     }\n> > +\n> >       /*\n> >        * Shrink mapping_size to be the minimum necessary\n> >        */\n>\n> > @@ -1022,6 +1043,7 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n> >        * children that we have already processed.)\n> >        */\n> >       seen_this = 0;\n> > +\n> >       for (i = 0; i <= graph->num_columns; i++) {\n> >               struct column *col = &graph->columns[i];\n> >               struct commit *col_commit;\n>\n> Is this empty line really needed?\n\nNo, I'll drop it.\n\n>\n> > +                             if (j >= 2)\n> > +                                     truncation_max -= 1;\n>\n> I think it is more idiomatic way to write this as truncation_max--.\n\nI'll change it.\n\n>\n> > @@ -1288,12 +1365,20 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n> >        */\n> >       for (i = 0; i < graph->mapping_size; i++) {\n> >               int target = graph->mapping[i];\n> > -             if (target < 0)\n> > -                     graph_line_addch(line, ' ');\n> > -             else if (target * 2 == i)\n> > -                     graph_line_write_column(line, &graph->new_columns[target], '|');\n> > -             else if (target == horizontal_edge_target &&\n> > -                      i != horizontal_edge - 1) {\n> > +\n> > +             if (!truncated && graph_needs_truncation(graph, i / 2)) {\n> > +                     graph_line_addstr(line, \". \");\n> > +                     truncated = 1;\n> > +             }\n> > +\n> > +             if (target < 0) {\n> > +                     if (!truncated)\n> > +                             graph_line_addch(line, ' ');\n> > +             } else if (target * 2 == i) {\n> > +                     if (!truncated)\n> > +                             graph_line_write_column(line, &graph->new_columns[target], '|');\n> > +             } else if (target == horizontal_edge_target &&\n> > +                        i != horizontal_edge - 1) {\n> >                               /*\n> >                                * Set the mappings for all but the\n> >                                * first segment to -1 so that they\n> > @@ -1301,13 +1386,14 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n> >                                */\n> >                               if (i != (target * 2)+3)\n> >                                       graph->mapping[i] = -1;\n> > -                             used_horizontal = 1;\n> > -                     graph_line_write_column(line, &graph->new_columns[target], '_');\n> > +                     used_horizontal = 1;\n> > +                     if (!truncated)\n> > +                             graph_line_write_column(line, &graph->new_columns[target], '_');\n>\n> Huh? The indentation of \"used_horizontal...\" changed. The reason is that\n> this whole if-branch is indented too far by one tab. Perhaps an initial\n> clean-up commit that only fixes this indentation?\n\nI'll fix it, but I think it will be better to have it on the patch\nwith the truncation logic because\nit is a tiny cleanup and doesn't really justify a whole commit for it.\n\n> >               } else {\n> >                       if (used_horizontal && i < horizontal_edge)\n> >                               graph->mapping[i] = -1;\n> > -                     graph_line_write_column(line, &graph->new_columns[target], '/');\n> > -\n> > +                     if (!truncated)\n> > +                             graph_line_write_column(line, &graph->new_columns[target], '/');\n> >               }\n> >       }\n> >\n> > @@ -1356,7 +1442,6 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb)\n> >               graph_output_collapsing_line(graph, &line);\n> >               break;\n> >       }\n> > -\n> >       graph_pad_horizontally(graph, &line);\n> >       return shown_commit_line;\n> >  }\n>\n> This removal of an empty line isn't warranted, I think.\n\nNo, I'll revert that.\n\n>\n> -- Hannes\n>\n"},{"id":"539931","messageId":"CAN5EUNT+x=OmQb0JqcTYn_6xNSpKKTz71aZOfdM-CShjSbAyPw@mail.gmail.com","threadId":"65267","inReplyTo":"6cdcece0-8cc5-4c87-8727-6d3e17424a9e@kdbg.org","subject":"Re: [GSoC PATCH v4 3/3] graph: add documentation and tests about --graph-lane-limit","fromName":"Pablo","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-25T11:49:37Z","receivedAt":"2026-03-25T11:49:53Z","isPatch":true,"body":"Johannes Sixt (<j6t@kdbg.org>) writes:\n>\n> Am 23.03.26 um 22:59 schrieb Pablo Sabater:\n> > @@ -1259,6 +1259,11 @@ This implies the `--topo-order` option by default, but the\n> >       in between them in that case. If _<barrier>_ is specified, it\n> >       is the string that will be shown instead of the default one.\n> >\n> > +`--graph-lane-limit=<n>`::\n> > +     When `--graph` is used, limit the number of graph lanes to be shown.\n> > +     Lanes over the limit are replaced with a truncation mark '.'. By default\n> > +     there is no limit.\n>\n> This should probably mention that 0 means no limit.\n\nI'll add that, it is for zero or any negative number.\n\n>\n> > +\n> >  ifdef::git-rev-list[]\n> >  `--count`::\n> >       Print a number stating how many commits would have been\n> > diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh\n> > index 28d0779a8c..650701df42 100755\n> > --- a/t/t4215-log-skewed-merges.sh\n> > +++ b/t/t4215-log-skewed-merges.sh\n> > @@ -370,4 +370,57 @@ test_expect_success 'log --graph with multiple tips' '\n> >       EOF\n> >  '\n> >\n> > +test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' '\n> > +     check_graph --graph-lane-limit=2 M_7 <<-\\EOF\n> > +     *-.   7_M4\n> > +     |\\ \\\n> > +     | | * 7_G\n> > +     | | * 7_F\n> > +     | * . 7_E\n> > +     | * . 7_D\n> > +     * | . 7_C\n> > +     | |/\n> > +     |/|\n> > +     * | 7_B\n> > +     |/\n> > +     * 7_A\n>\n> I'm confused. If the lane limit is 2, why do we have actually have 3 lanes?\n>\n> > +test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' '\n> > +     check_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\\EOF\n> > +     *   7_M1\n> > +     |\\\n> > +     | | *   7_M2\n> > +     | | |\\\n> > +     | | | * 7_H\n> > +     | | | . 7_M3\n> > +     | | | . 7_J\n> > +     | | | . 7_I\n> > +     | | | . 7_M4\n> > +     | |_|_.\n> > +     |/| | .\n> > +     | | |_.\n> > +     | |/| .\n> > +     | | | .\n> > +     | | |/.\n> > +     | | * . 7_G\n> > +     | | | .\n> > +     | | |/.\n> > +     | | * . 7_F\n> > +     | * | . 7_E\n> > +     | | |/.\n> > +     | |/| .\n> > +     | * | . 7_D\n> > +     | | |/\n> > +     | |/|\n> > +     * | | 7_C\n> > +     | |/\n> > +     |/|\n> > +     * | 7_B\n> > +     |/\n> > +     * 7_A\n>\n> Same here. Why is there a fourth lane?\n\nThe extra column is the truncation marker that shows in every lane that\nhad to be truncated horizontally, not an actual lane, even tho that by\nkeeping the right side edge might seem confusing.\n\n>\n> Oh! \"Truncation\" here does not mean that the vertical lines are cut off\n> and are supposed to continue sometime later in the chart. It literally\n> means that the *line* is truncated and just some stuff *on that line* is\n> omitted.\n>\n> Ouch! That was not what I was expecting. I thought that truncation means\n> that when the eye follows a line vertically, it finds the truncation\n> point of the line at some point, and then the continuation of that line\n> is again some time later down the chart. The only clue which lanes are\n> the same would be the color, which would have to be remedied somehow.\n>\n> I don't know what to make of it. I have to reconsider.\n\nYes, the truncation is horizontal, each lane is cut at the lane limit. I went\nwith this because it is more accessible starting to work at the graph and\nunderstanding it (it is a 17 years old code).\n\nVertical truncation needs rearranging which lanes are visible and which\nones come and go, similar to gitk (This is the TODO idea that is present\non the graph.c code) I think it will be hard to do it even if there is a way\nof not having to rewrite the whole graph rendering code. prob if this is done\nit won't replace the actual --graph but actually become something like\n--rgraph as a different graph rendering option.\n\n>\n> -- Hannes\n>\n\nI hope I a made my intentions with horizontal truncation more clear.\nI'll send a v5 with all the changes talked about.\n\nThanks for the feedback throughout all the patches!\nPablo\n"},{"id":"539935","messageId":"CAN5EUNTXy+cFyHApdrhGKUqrvBGO0bb9X-=MaAWgp4DWOAkA-A@mail.gmail.com","threadId":"65267","inReplyTo":"fae2f8e3-029a-43c7-aa6e-45a452026853@kdbg.org","subject":"Re: [GSoC PATCH v4 0/3] graph: add --graph-lane-limit option","fromName":"Pablo","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-25T12:28:36Z","receivedAt":"2026-03-25T12:28:53Z","isPatch":true,"body":"Johannes Sixt (<j6t@kdbg.org>) writes:\n\n> Generally, I like the goal of this patch series. However, the way in\n> which it is presented and justified can be improved substantially, IMO.\n>\n> It begins with the statement of what this patch series wants to achieve.\n> It is \"limit the width of the graph\", isn't it? It is not \"add\n> --graph-lane-limit\"; that is just a tool to achieve the goal.\n>\n> To help reviewers, you should present an example chart in the cover\n> letter that shows the before- and after-state (with and without the user\n> of the new option).\n\nTrue, I'll do that. Because it seems on your last review that I didn't explain\nmyself correctly, the idea was to:\n\nWithout --graph-lane-limit:\n\n| | | | | | | * commit message\n| | | | | |/\n\nWith --graph-lane-limit=3:\n\n| | | . commit message\n| | | .\n\nIt truncates the lanes horizontally at the lane limit, the \".\" replaces\neverything over the limit (n+1).\n\n> As far as the separation into patches is concerned, I see a few\n> problems. With the current separation is difficult to justify the\n> patches. For example, the first patch adds prerequisites for a later\n> patch, but it is unclear how these are used. The answer to the question\n> \"Why do we need this?\" is simply \"because the next patch uses them\", but\n> this is a very weak justification, because the next questions are \"how\n> are they used and why didn't you squash this into the next patch?\"\n>\n> Let me suggest a different separation.\n\nI'll merge 1st and 2nd patch together into a single one, adding the option\ntogether with the actual logic that does it. This fixes what SZEDER said about\nthe first patch alone breaking the build.\n\nAnd the documentation + tests on a separate commit.\n\n> 1. The first patch limits the graph width with a hard-coded limit, say\n> 15 lanes. It limits the graph *always*. Choose a limit that is large\n> enough to pass all tests.\n>\n> 2. The next patch adds --graph-lane-limit and its documentation. Let it\n> do its thing. Revert to the default limit value 0, i.e., unlimited.\n>\n> 3. Next, add additional eye-candy. I am alluding to the line that marks\n> where a graph lane was truncated.\n>\n> (4. If more detailed document is warranted, e.g., an example chart, do\n> this as a separate patch that can now show all bells and whistles that\n> the earlier commits have implemented. Whether this makes sense as a\n> separate step, or whether documentation grows with the earlier patches,\n> is a judgement call.)\n>\n> As far as commit messages are concerned, always, always provide an\n> answer to \"Why?\" for every detail.\n>\n> - Why do we want to limit the graph width?\n> - Why is the hard-coded limit 15? (because it lets tests pass and is\n> still a useful limit; we'll make it dynamic later.)\n> - Why do we always limit the graph width? (Because it makes this patch\n> simpler; we'll fix this later.)\n> - Why does 0 mean unlimited? (Consistency with --max-parents.)\n> - Why is the truncation marked with a fullstop \".\"? (...)\n\nOk, I'll make sure to answer clearly all the WHY's on the commits.\n\n\n> I'll also look over the patches, but I don't do C code, so I can provide\n> only superficial comments, if any.\n>\n> -- Hannes\n>\n"},{"id":"539936","messageId":"CAN5EUNQBRp2OQHQ32FFW5vPKUO9jHu5chijA3FTatR7jnyzO1g@mail.gmail.com","threadId":"65267","inReplyTo":"6b299cf5-acfd-4a56-87e7-db26743a3271@kdbg.org","subject":"Re: [GSoC PATCH v4 1/3] graph: add --graph-lane-limit option","fromName":"Pablo","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-25T12:29:37Z","receivedAt":"2026-03-25T12:29:53Z","isPatch":true,"body":"Johannes Sixt (<j6t@kdbg.org>) writes:\n>\n> Am 23.03.26 um 22:59 schrieb Pablo Sabater:\n> > @@ -3172,6 +3174,10 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s\n> >\n> >       if (revs->no_walk && revs->graph)\n> >               die(_(\"options '%s' and '%s' cannot be used together\"), \"--no-walk\", \"--graph\");\n> > +\n> > +     if (revs->graph_max_lanes > 0 && !revs->graph)\n> > +             die(_(\"option '%s' requires '%s'\"), \"--graph-lane-limit\", \"--graph\");\n> > +\n> >       if (!revs->reflog_info && revs->grep_filter.use_reflog_filter)\n> >               die(_(\"the option '%s' requires '%s'\"), \"--grep-reflog\", \"--walk-reflogs\");\n>\n> You help translators if you make the new error message format string\n> exactly identical to the one that we see in the post-context.\n>\n\nTrue, I'll make the messages the same in v5.\n\n> -- Hannes\n>\n"},{"id":"539969","messageId":"20260325174401.217577-1-pabloosabaterr@gmail.com","threadId":"65267","inReplyTo":"20260323215935.74486-1-pabloosabaterr@gmail.com","subject":"[GSoC PATCH v5 0/2] graph: add --graph-lane-limit option","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-25T17:43:59Z","receivedAt":"2026-03-25T17:44:16Z","isPatch":true,"body":"Repositories that have many active branches at the same time produce\nwide graphs. A lane consists of two columns, the edge and the space\npadding, each branch takes a lane in the graph and there is no way\nto limit how many can be shown.\n\nThe limit is a horizontal truncation, each lane is cut at the lane limit:\n\n  Without --graph-lane-limit:\n\n  *   7_M1\n  |\\  \n  | * 7_E\n  * | 7_C\n  | | *   7_M2\n  | | |\\  \n  | | | * 7_H\n  | | |/  \n  | |/|   \n  | * | 7_D\n  | | * 7_G\n  | | * 7_F\n  | |/  \n  |/|   \n  * | 7_B\n  |/  \n  * 7_A\n\n  With --graph-lane-limit=1:\n\n  *   7_M1\n  |\\  \n  | * 7_E\n  * ~ 7_C\n  | ~ 7_M2\n  | ~ 7_H\n  | ~ \n  | ~ \n  | * 7_D\n  | ~ 7_G\n  | ~ 7_F\n  | ~ \n  |/~ \n  * ~ 7_B\n  |/  \n  * 7_A\n\nThe '~' is the truncation mark, not an actual lane. It was chosen because '.' is\nalready used in octopus merges and '~' is not used elsewhere in the graph. Yet\nthe edges between the last visible lane and the truncation mark are still\nconserved.\n\nThe '*' commit mark is visible when it lives on a visible lane or the first\nhidden lane, any deeper lane doesn't show the commit mark but keeps the commit\nmessage visible. \n\nMerges where neither the commit nor its parents live on a visible lane are skipped\nbecause they don't carry any visible information.\n\nThe original idea to limit columns was noted as a TODO in c12172d2ea\n(Add history graph API, 2008-05-04).  This does not implement\ngitk-style column rearrangement, it only truncates the visual output.\n\nPossible future improvements:\n\n- When all branches involved in collapsing or padding are over the limit, the\n  truncated lane doesn't show any information, this lane could be removed to\n  make the graph more compact. Currently these lanes still appear because\n  graph_output_collapsing_line() mixes state handling with rendering, so it can't\n  be skipped but callers always expect a non empty buffer. Fixing it would need\n  to refactor the callers to handle empty buffers instead of expecting them to\n  always have content.\n\n- Collapsing and merges lanes that start on visible lanes but end on hidden ones\n  are kept to maintain the most information possible on the visible lanes, but\n  the information about where they go is lost. They can be kept, removed or think\n  of a way to show that information without showing the lanes.\n\nChanges since v4:\n\n- Merged the option parsing and the truncation logic into a single\n  patch, fixing the DEVELOPER=1 build break when the first patch was alone.\n- Added before/after example to clarify horizontal truncation.\n- Fixed error message to match existing format strings.\n- Shortened code comment.\n- Changed truncation_max -= 1 to truncation_max--.\n- Fixed pre-existing indentation in graph_output_collapsing_line().\n- Fixed unnecessary blank line changes.\n- Added that zero and negative values mean no limit in the docs.\n- Changed truncation mark from '.' to '~' because '.' is already\n  used in octopus merges.\n- Added more tests.\n\nPablo Sabater (2):\n  graph: add --graph-lane-limit option\n  graph: add documentation and tests about --graph-lane-limit\n\n Documentation/rev-list-options.adoc |   6 ++\n graph.c                             | 149 +++++++++++++++++++++++-----\n revision.c                          |   6 ++\n revision.h                          |   1 +\n t/t4215-log-skewed-merges.sh        | 144 +++++++++++++++++++++++++++\n 5 files changed, 283 insertions(+), 23 deletions(-)\n\n\nbase-commit: ce74208c2fa13943fffa58f168ac27a76d0eb789\n-- \n2.43.0\n\n"},{"id":"539970","messageId":"20260325174401.217577-2-pabloosabaterr@gmail.com","threadId":"65267","inReplyTo":"20260325174401.217577-1-pabloosabaterr@gmail.com","subject":"[GSoC PATCH v5 1/2] graph: add --graph-lane-limit option","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-25T17:44:00Z","receivedAt":"2026-03-25T17:44:18Z","isPatch":true,"body":"Repositories that have many active branches at the same time produce\nwide graphs. A lane consists of two columns, the edge and the space\npadding, each branch takes a lane in the graph and there is no way\nto limit how many can be shown.\n\nAdd '--graph-lane-limit=<n>' revision option that caps the number\nof visible lanes to n. This option requires '--graph', without it\na limit to the graph has no meaning, in this case error out.\n\nZero and negative values are valid inputs but silently ignored\ntreating them as \"no limit\", the same as not using the option.\nThis follows what '--max-parents' does with negative values.\n\nWhen the limit is set, lanes over the limit are not drawn.\nTeach each graph state to stop rendering at the lane limit\nand print a \"~\" truncation mark, so users know that\nthere are hidden lanes. The \"~\" was chosen because it was not used\nelsewhere in the graph and it is discrete.\n\nOn the commit line, if the commit lives on a visible lane, show\nthe normal commit mark and truncate after it, if the commit lives\non the first hidden lane show the \"*\" instead of the truncation mark\nso it is known that this commit sits on the first hidden lane.\nCommits on deeper lanes don't leave a mark.\n\nFor merges, the post-merge lane is only needed when the commit or\nthe first parent lives on a visible lane (to draw the connection\nbetween them), when both are on hidden lanes, post-merge carries no\nuseful information, skip it and go to collapsing or padding state.\n\nAlso fix a pre-existing indentation issue.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c    | 150 +++++++++++++++++++++++++++++++++++++++++++++--------\n revision.c |   6 +++\n revision.h |   1 +\n 3 files changed, 134 insertions(+), 23 deletions(-)\n\ndiff --git a/graph.c b/graph.c\nindex 26f6fbf000..2218f00c40 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -317,6 +317,15 @@ struct git_graph {\n \tstruct strbuf prefix_buf;\n };\n \n+static int graph_needs_truncation(struct git_graph *graph, int lane)\n+{\n+\tint max = graph->revs->graph_max_lanes;\n+\t/*\n+\t * Ignore values <= 0, meaning no limit.\n+\t */\n+\treturn max > 0 && lane >= max;\n+}\n+\n static const char *diff_output_prefix_callback(struct diff_options *opt, void *data)\n {\n \tstruct git_graph *graph = data;\n@@ -696,6 +705,18 @@ static void graph_update_columns(struct git_graph *graph)\n \t\t}\n \t}\n \n+\t/*\n+\t * If graph_max_lanes is set, cap the padding from the branches\n+\t */\n+\tif (graph->revs->graph_max_lanes > 0) {\n+\t\t/*\n+\t\t * width of \"| \" per lanes plus truncation mark \"~ \".\n+\t\t */\n+\t\tint max_columns_width = graph->revs->graph_max_lanes * 2 + 2;\n+\t\tif (graph->width > max_columns_width)\n+\t\t\tgraph->width = max_columns_width;\n+\t}\n+\n \t/*\n \t * Shrink mapping_size to be the minimum necessary\n \t */\n@@ -846,6 +867,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\tif (graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addstr(line, \"~ \");\n+\t\t\tbreak;\n+\t\t}\n \t\tgraph_line_write_column(line, &graph->new_columns[i], '|');\n \t\tgraph_line_addch(line, ' ');\n \t}\n@@ -903,6 +928,9 @@ static void graph_output_pre_commit_line(struct git_graph *graph,\n \t\t\tseen_this = 1;\n \t\t\tgraph_line_write_column(line, col, '|');\n \t\t\tgraph_line_addchars(line, ' ', graph->expansion_row);\n+\t\t} else if (seen_this && graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addstr(line, \"~ \");\n+\t\t\tbreak;\n \t\t} else if (seen_this && (graph->expansion_row == 0)) {\n \t\t\t/*\n \t\t\t * This is the first line of the pre-commit output.\n@@ -994,6 +1022,12 @@ static void graph_draw_octopus_merge(struct git_graph *graph, struct graph_line\n \t\tcol = &graph->new_columns[j];\n \n \t\tgraph_line_write_column(line, col, '-');\n+\n+\t\tif (graph_needs_truncation(graph, j / 2 + i)) {\n+\t\t\tgraph_line_addstr(line, \"~ \");\n+\t\t\tbreak;\n+\t\t}\n+\n \t\tgraph_line_write_column(line, col, (i == dashed_parents - 1) ? '.' : '-');\n \t}\n \n@@ -1028,8 +1062,17 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \t\t\tseen_this = 1;\n \t\t\tgraph_output_commit_char(graph, line);\n \n+\t\t\tif (graph_needs_truncation(graph, i)) {\n+\t\t\t\tgraph_line_addch(line, ' ');\n+\t\t\t\tbreak;\n+\t\t\t}\n+\n \t\t\tif (graph->num_parents > 2)\n \t\t\t\tgraph_draw_octopus_merge(graph, line);\n+\t\t} else if (graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addstr(line, \"~ \");\n+\t\t\tseen_this = 1;\n+\t\t\tbreak;\n \t\t} else if (seen_this && (graph->edges_added > 1)) {\n \t\t\tgraph_line_write_column(line, col, '\\\\');\n \t\t} else if (seen_this && (graph->edges_added == 1)) {\n@@ -1065,10 +1108,32 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \n \t/*\n \t * Update graph->state\n+\t *\n+\t * If the commit is a merge and the first parent is in a visible lane,\n+\t * then the GRAPH_POST_MERGE is needed to draw the merge lane.\n+\t *\n+\t * If the commit is over the truncation limit, but the first parent is on\n+\t * a visible lane, then we still need the merge lane but truncated.\n+\t *\n+\t * If both commit and first parent are over the truncation limit, then\n+\t * there's no need to draw the merge lane because it would work as a\n+\t * padding lane.\n \t */\n-\tif (graph->num_parents > 1)\n-\t\tgraph_update_state(graph, GRAPH_POST_MERGE);\n-\telse if (graph_is_mapping_correct(graph))\n+\tif (graph->num_parents > 1) {\n+\t\tif (!graph_needs_truncation(graph, graph->commit_index)) {\n+\t\t\tgraph_update_state(graph, GRAPH_POST_MERGE);\n+\t\t} else {\n+\t\t\tstruct commit_list *first_parent = first_interesting_parent(graph);\n+\t\t\tint first_parent_col = graph_find_new_column_by_commit(graph, first_parent->item);\n+\n+\t\t\tif (!graph_needs_truncation(graph, first_parent_col))\n+\t\t\t\tgraph_update_state(graph, GRAPH_POST_MERGE);\n+\t\t\telse if (graph_is_mapping_correct(graph))\n+\t\t\t\tgraph_update_state(graph, GRAPH_PADDING);\n+\t\t\telse\n+\t\t\t\tgraph_update_state(graph, GRAPH_COLLAPSING);\n+\t\t}\n+\t} else if (graph_is_mapping_correct(graph))\n \t\tgraph_update_state(graph, GRAPH_PADDING);\n \telse\n \t\tgraph_update_state(graph, GRAPH_COLLAPSING);\n@@ -1109,6 +1174,7 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n \t\t\tint par_column;\n \t\t\tint idx = graph->merge_layout;\n \t\t\tchar c;\n+\t\t\tint truncated = 0;\n \t\t\tseen_this = 1;\n \n \t\t\tfor (j = 0; j < graph->num_parents; j++) {\n@@ -1117,23 +1183,46 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n \n \t\t\t\tc = merge_chars[idx];\n \t\t\t\tgraph_line_write_column(line, &graph->new_columns[par_column], c);\n+\t\t\t\tif (graph_needs_truncation(graph, j / 2 + i) &&\n+\t\t\t\t    j / 2 + i <= graph->num_columns) {\n+\t\t\t\t\tif ((j + i * 2) % 2 != 0)\n+\t\t\t\t\t\tgraph_line_addch(line, ' ');\n+\t\t\t\t\tgraph_line_addstr(line, \"~ \");\n+\t\t\t\t\ttruncated = 1;\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\n \t\t\t\tif (idx == 2) {\n-\t\t\t\t\tif (graph->edges_added > 0 || j < graph->num_parents - 1)\n+\t\t\t\t\tif (graph_needs_truncation(graph, (j + 1) / 2 + i) &&\n+\t\t\t\t\t    j < graph->num_parents - 1) {\n+\t\t\t\t\t\tgraph_line_addstr(line, \"~ \");\n+\t\t\t\t\t\ttruncated = 1;\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t\t} else if (graph->edges_added > 0 || j < graph->num_parents - 1)\n \t\t\t\t\t\tgraph_line_addch(line, ' ');\n \t\t\t\t} else {\n \t\t\t\t\tidx++;\n \t\t\t\t}\n \t\t\t\tparents = next_interesting_parent(graph, parents);\n \t\t\t}\n+\t\t\tif (truncated)\n+\t\t\t\tbreak;\n \t\t\tif (graph->edges_added == 0)\n \t\t\t\tgraph_line_addch(line, ' ');\n-\n+\t\t} else if (graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addstr(line, \"~ \");\n+\t\t\tbreak;\n \t\t} else if (seen_this) {\n \t\t\tif (graph->edges_added > 0)\n \t\t\t\tgraph_line_write_column(line, col, '\\\\');\n \t\t\telse\n \t\t\t\tgraph_line_write_column(line, col, '|');\n-\t\t\tgraph_line_addch(line, ' ');\n+\t\t\t/*\n+\t\t\t * If it's between two lanes and next would be truncated,\n+\t\t\t * don't add space padding.\n+\t\t\t */\n+\t\t\tif (!graph_needs_truncation(graph, i + 1))\n+\t\t\t\tgraph_line_addch(line, ' ');\n \t\t} else {\n \t\t\tgraph_line_write_column(line, col, '|');\n \t\t\tif (graph->merge_layout != 0 || i != graph->commit_index - 1) {\n@@ -1164,6 +1253,7 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n \tshort used_horizontal = 0;\n \tint horizontal_edge = -1;\n \tint horizontal_edge_target = -1;\n+\tint truncated = 0;\n \n \t/*\n \t * Swap the mapping and old_mapping arrays\n@@ -1279,26 +1369,35 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n \t */\n \tfor (i = 0; i < graph->mapping_size; i++) {\n \t\tint target = graph->mapping[i];\n-\t\tif (target < 0)\n-\t\t\tgraph_line_addch(line, ' ');\n-\t\telse if (target * 2 == i)\n-\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '|');\n-\t\telse if (target == horizontal_edge_target &&\n-\t\t\t i != horizontal_edge - 1) {\n-\t\t\t\t/*\n-\t\t\t\t * Set the mappings for all but the\n-\t\t\t\t * first segment to -1 so that they\n-\t\t\t\t * won't continue into the next line.\n-\t\t\t\t */\n-\t\t\t\tif (i != (target * 2)+3)\n-\t\t\t\t\tgraph->mapping[i] = -1;\n-\t\t\t\tused_horizontal = 1;\n-\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '_');\n+\n+\t\tif (!truncated && graph_needs_truncation(graph, i / 2)) {\n+\t\t\tgraph_line_addstr(line, \"~ \");\n+\t\t\ttruncated = 1;\n+\t\t}\n+\n+\t\tif (target < 0) {\n+\t\t\tif (!truncated)\n+\t\t\t\tgraph_line_addch(line, ' ');\n+\t\t} else if (target * 2 == i) {\n+\t\t\tif (!truncated)\n+\t\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '|');\n+\t\t} else if (target == horizontal_edge_target &&\n+\t\t\t   i != horizontal_edge - 1) {\n+\t\t\t/*\n+\t\t\t * Set the mappings for all but the\n+\t\t\t * first segment to -1 so that they\n+\t\t\t * won't continue into the next line.\n+\t\t\t */\n+\t\t\tif (i != (target * 2)+3)\n+\t\t\t\tgraph->mapping[i] = -1;\n+\t\t\tused_horizontal = 1;\n+\t\t\tif (!truncated)\n+\t\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '_');\n \t\t} else {\n \t\t\tif (used_horizontal && i < horizontal_edge)\n \t\t\t\tgraph->mapping[i] = -1;\n-\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '/');\n-\n+\t\t\tif (!truncated)\n+\t\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '/');\n \t\t}\n \t}\n \n@@ -1372,6 +1471,11 @@ static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)\n \tfor (i = 0; i < graph->num_columns; i++) {\n \t\tstruct column *col = &graph->columns[i];\n \n+\t\tif (graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addch(&line, '~');\n+\t\t\tbreak;\n+\t\t}\n+\n \t\tgraph_line_write_column(&line, col, '|');\n \n \t\tif (col->commit == graph->commit && graph->num_parents > 2) {\ndiff --git a/revision.c b/revision.c\nindex 31808e3df0..81b67682a8 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -2605,6 +2605,8 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg\n \t} else if (!strcmp(arg, \"--no-graph\")) {\n \t\tgraph_clear(revs->graph);\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, \"--encode-email-headers\")) {\n \t\trevs->encode_email_headers = 1;\n \t} else if (!strcmp(arg, \"--no-encode-email-headers\")) {\n@@ -3172,6 +3174,10 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s\n \n \tif (revs->no_walk && revs->graph)\n \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--no-walk\", \"--graph\");\n+\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->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 69242ecb18..874ccce625 100644\n--- a/revision.h\n+++ b/revision.h\n@@ -304,6 +304,7 @@ struct rev_info {\n \n \t/* Display history graph */\n \tstruct git_graph *graph;\n+\tint graph_max_lanes;\n \n \t/* special limits */\n \tint skip_count;\n\nbase-commit: ce74208c2fa13943fffa58f168ac27a76d0eb789\n-- \n2.43.0\n\n"},{"id":"539971","messageId":"20260325174401.217577-3-pabloosabaterr@gmail.com","threadId":"65267","inReplyTo":"20260325174401.217577-1-pabloosabaterr@gmail.com","subject":"[GSoC PATCH v5 2/2] graph: add documentation and tests about --graph-lane-limit","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-25T17:44:01Z","receivedAt":"2026-03-25T17:44:20Z","isPatch":true,"body":"Document --graph-lane-limit option in rev-list-options.adoc with\n--graph option.\n\nAdd multiple tests in t4215 reusing existing last graph, test\nfor different scenarios.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n Documentation/rev-list-options.adoc |   6 ++\n t/t4215-log-skewed-merges.sh        | 144 ++++++++++++++++++++++++++++\n 2 files changed, 150 insertions(+)\n\ndiff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc\nindex 2d195a1474..56590f4e95 100644\n--- a/Documentation/rev-list-options.adoc\n+++ b/Documentation/rev-list-options.adoc\n@@ -1259,6 +1259,12 @@ This implies the `--topo-order` option by default, but the\n \tin between them in that case. If _<barrier>_ is specified, it\n \tis the string that will be shown instead of the default one.\n \n+`--graph-lane-limit=<n>`::\n+\tWhen `--graph` is used, limit the number of graph lanes to be shown.\n+\tLanes over the limit are replaced with a truncation mark '~'. By default\n+\tit is set to 0 (no limit), zero and negative values are ignored and\n+\ttreated as no limit.\n+\n ifdef::git-rev-list[]\n `--count`::\n \tPrint a number stating how many commits would have been\ndiff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh\nindex 28d0779a8c..1612f05f1b 100755\n--- a/t/t4215-log-skewed-merges.sh\n+++ b/t/t4215-log-skewed-merges.sh\n@@ -370,4 +370,148 @@ test_expect_success 'log --graph with multiple tips' '\n \tEOF\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+\t*-.   7_M4\n+\t|\\ \\\n+\t| | * 7_G\n+\t| | * 7_F\n+\t| * ~ 7_E\n+\t| * ~ 7_D\n+\t* | ~ 7_C\n+\t| |/\n+\t|/|\n+\t* | 7_B\n+\t|/\n+\t* 7_A\n+\tEOF\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+\t*-~  7_M4\n+\t|\\~\n+\t| ~ 7_G\n+\t| ~ 7_F\n+\t| * 7_E\n+\t| * 7_D\n+\t* ~ 7_C\n+\t| ~\n+\t|/~\n+\t* ~ 7_B\n+\t|/\n+\t* 7_A\n+\tEOF\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+\t*   7_M1\n+\t|\\\n+\t| | *   7_M2\n+\t| | |\\\n+\t| | | * 7_H\n+\t| | | ~ 7_M3\n+\t| | | ~ 7_J\n+\t| | | ~ 7_I\n+\t| | | ~ 7_M4\n+\t| |_|_~\n+\t|/| | ~\n+\t| | |_~\n+\t| |/| ~\n+\t| | | ~\n+\t| | |/~\n+\t| | * ~ 7_G\n+\t| | | ~\n+\t| | |/~\n+\t| | * ~ 7_F\n+\t| * | ~ 7_E\n+\t| | |/~\n+\t| |/| ~\n+\t| * | ~ 7_D\n+\t| | |/\n+\t| |/|\n+\t* | | 7_C\n+\t| |/\n+\t|/|\n+\t* | 7_B\n+\t|/\n+\t* 7_A\n+\tEOF\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+\t*   7_M1\n+\t|\\\n+\t| | *   7_M2\n+\t| | |\\\n+\t| | | * 7_H\n+\t| | | | *   7_M3\n+\t| | | | |\\\n+\t| | | | | * 7_J\n+\t| | | | * | 7_I\n+\t| | | | | | * 7_M4\n+\t| |_|_|_|_|/~\n+\t|/| | | | |/~\n+\t| | |_|_|/| ~\n+\t| |/| | | |/\n+\t| | | |_|/|\n+\t| | |/| | |\n+\t| | * | | | 7_G\n+\t| | | |_|/\n+\t| | |/| |\n+\t| | * | | 7_F\n+\t| * | | | 7_E\n+\t| | |/ /\n+\t| |/| |\n+\t| * | | 7_D\n+\t| | |/\n+\t| |/|\n+\t* | | 7_C\n+\t| |/\n+\t|/|\n+\t* | 7_B\n+\t|/\n+\t* 7_A\n+\tEOF\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+\t*   7_M1\n+\t|\\\n+\t| | *   7_M2\n+\t| | |\\\n+\t| | | * 7_H\n+\t| | | | *   7_M3\n+\t| | | | |\\\n+\t| | | | | * 7_J\n+\t| | | | * | 7_I\n+\t| | | | | | *   7_M4\n+\t| |_|_|_|_|/|\\\n+\t|/| | | | |/ /\n+\t| | |_|_|/| /\n+\t| |/| | | |/\n+\t| | | |_|/|\n+\t| | |/| | |\n+\t| | * | | | 7_G\n+\t| | | |_|/\n+\t| | |/| |\n+\t| | * | | 7_F\n+\t| * | | | 7_E\n+\t| | |/ /\n+\t| |/| |\n+\t| * | | 7_D\n+\t| | |/\n+\t| |/|\n+\t* | | 7_C\n+\t| |/\n+\t|/|\n+\t* | 7_B\n+\t|/\n+\t* 7_A\n+\tEOF\n+'\n+\n test_done\n-- \n2.43.0\n\n"},{"id":"539972","messageId":"251cbdd8-26ca-4569-9801-5eb278de7e0c@kdbg.org","threadId":"65267","inReplyTo":"CAN5EUNTXy+cFyHApdrhGKUqrvBGO0bb9X-=MaAWgp4DWOAkA-A@mail.gmail.com","subject":"Re: [GSoC PATCH v4 0/3] graph: add --graph-lane-limit option","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2026-03-25T17:44:20Z","receivedAt":"2026-03-25T17:44:30Z","isPatch":true,"body":"Am 25.03.26 um 13:28 schrieb Pablo:\n> Johannes Sixt (<j6t@kdbg.org>) writes:\n>> Let me suggest a different separation.\n> \n> I'll merge 1st and 2nd patch together into a single one, adding the option\n> together with the actual logic that does it. This fixes what SZEDER said about\n> the first patch alone breaking the build.\n> \n> And the documentation + tests on a separate commit.\n\nIt is better to add documentation and tests in the same commit that add\nthe feature, because both serve as a specification what the code is\nsupposed to do. This way reviewers can decide whether the code does\nindeed work as designed. On top of that, when the code has to be\ninspected later, the commit that introduced the code shows immediately\nwhether a certain behavior was intentional or not.\n\nSo, you would end up with a single patch.\n\nBut to make reviewing easier, I proposed a different split:\n\n>> 1. The first patch limits the graph width with a hard-coded limit, say\n>> 15 lanes. It limits the graph *always*. Choose a limit that is large\n>> enough to pass all tests.\n\nThis change will touch the graphing engine, but almost nothing else.\n\n>> 2. The next patch adds --graph-lane-limit and its documentation. Let it\n>> do its thing. Revert to the default limit value 0, i.e., unlimited.\n\nThis change now introduces all the plumbing that passes the user's\noption through to the engine.\n\n>> 3. Next, add additional eye-candy. I am alluding to the line that marks\n>> where a graph lane was truncated.\n\nIf possible, this change provides final touches that can reasonably be\nleft out from the first patch without compromising its basic functionality.\n\n>> (4. If more detailed document is warranted, e.g., an example chart, do\n>> this as a separate patch that can now show all bells and whistles that\n>> the earlier commits have implemented. Whether this makes sense as a\n>> separate step, or whether documentation grows with the earlier patches,\n>> is a judgement call.)\n\nThis could be a new paragraph in the manuals with example charts if\ndoing so makes sense.\n\n-- Hannes\n\n"},{"id":"539974","messageId":"CAN5EUNQdq7Eg+yd9ZqVGbYuKSYOhAB5rc2np7SeQT-Zc10aqDw@mail.gmail.com","threadId":"65267","inReplyTo":"251cbdd8-26ca-4569-9801-5eb278de7e0c@kdbg.org","subject":"Re: [GSoC PATCH v4 0/3] graph: add --graph-lane-limit option","fromName":"Pablo","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-25T17:58:52Z","receivedAt":"2026-03-25T17:59:11Z","isPatch":true,"body":"Johannes Sixt (<j6t@kdbg.org>) writes:\n>\n> Am 25.03.26 um 13:28 schrieb Pablo:\n> > Johannes Sixt (<j6t@kdbg.org>) writes:\n> >> Let me suggest a different separation.\n> >\n> > I'll merge 1st and 2nd patch together into a single one, adding the option\n> > together with the actual logic that does it. This fixes what SZEDER said about\n> > the first patch alone breaking the build.\n> >\n> > And the documentation + tests on a separate commit.\n>\n> It is better to add documentation and tests in the same commit that add\n> the feature, because both serve as a specification what the code is\n> supposed to do. This way reviewers can decide whether the code does\n> indeed work as designed. On top of that, when the code has to be\n> inspected later, the commit that introduced the code shows immediately\n> whether a certain behavior was intentional or not.\n>\n> So, you would end up with a single patch.\n>\n> But to make reviewing easier, I proposed a different split:\n>\n> >> 1. The first patch limits the graph width with a hard-coded limit, say\n> >> 15 lanes. It limits the graph *always*. Choose a limit that is large\n> >> enough to pass all tests.\n>\n> This change will touch the graphing engine, but almost nothing else.\n>\n> >> 2. The next patch adds --graph-lane-limit and its documentation. Let it\n> >> do its thing. Revert to the default limit value 0, i.e., unlimited.\n>\n> This change now introduces all the plumbing that passes the user's\n> option through to the engine.\n>\n> >> 3. Next, add additional eye-candy. I am alluding to the line that marks\n> >> where a graph lane was truncated.\n>\n> If possible, this change provides final touches that can reasonably be\n> left out from the first patch without compromising its basic functionality.\n>\n> >> (4. If more detailed document is warranted, e.g., an example chart, do\n> >> this as a separate patch that can now show all bells and whistles that\n> >> the earlier commits have implemented. Whether this makes sense as a\n> >> separate step, or whether documentation grows with the earlier patches,\n> >> is a judgement call.)\n>\n> This could be a new paragraph in the manuals with example charts if\n> doing so makes sense.\n\nOk, I'll do that to make it better for reviewing, thanks. I've just\nsent the v5 at the same\ntime with this morning's feedback so it has the 2 patch split I talked\nabout but I'll\ndo a v6 following this.\n\n>\n> -- Hannes\n>\n\nThanks for explaining everything, the patience and the feedback.\nPablo\n"},{"id":"540019","messageId":"xmqqh5q3sgnm.fsf@gitster.g","threadId":"65267","inReplyTo":"20260325174401.217577-2-pabloosabaterr@gmail.com","subject":"Re: [GSoC PATCH v5 1/2] graph: add --graph-lane-limit option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-25T22:11:57Z","receivedAt":"2026-03-25T22:11:59Z","isPatch":true,"body":"Pablo Sabater <pabloosabaterr@gmail.com> writes:\n\n> +static int graph_needs_truncation(struct git_graph *graph, int lane)\n> +{\n> +\tint max = graph->revs->graph_max_lanes;\n> +\t/*\n> +\t * Ignore values <= 0, meaning no limit.\n> +\t */\n> +\treturn max > 0 && lane >= max;\n> +}\n\nMake a mental note that this helper function works on number of\nlanes, not display columns (which is roughly twice the number of\nlanes).\n\n> @@ -696,6 +705,18 @@ static void graph_update_columns(struct git_graph *graph)\n>  \t\t}\n>  \t}\n>  \n> +\t/*\n> +\t * If graph_max_lanes is set, cap the padding from the branches\n> +\t */\n> +\tif (graph->revs->graph_max_lanes > 0) {\n> +\t\t/*\n> +\t\t * width of \"| \" per lanes plus truncation mark \"~ \".\n> +\t\t */\n> +\t\tint max_columns_width = graph->revs->graph_max_lanes * 2 + 2;\n> +\t\tif (graph->width > max_columns_width)\n> +\t\t\tgraph->width = max_columns_width;\n> +\t}\n> +\n>  \t/*\n>  \t * Shrink mapping_size to be the minimum necessary\n>  \t */\n> @@ -846,6 +867,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\tif (graph_needs_truncation(graph, i)) {\n> +\t\t\tgraph_line_addstr(line, \"~ \");\n> +\t\t\tbreak;\n> +\t\t}\n\nAnd that mental note helps to convince us this loop makes sense, as\nit increments 'i' one by one ;-)\n\n> @@ -903,6 +928,9 @@ static void graph_output_pre_commit_line(struct git_graph *graph,\n>  \t\t\tseen_this = 1;\n>  \t\t\tgraph_line_write_column(line, col, '|');\n>  \t\t\tgraph_line_addchars(line, ' ', graph->expansion_row);\n> +\t\t} else if (seen_this && graph_needs_truncation(graph, i)) {\n> +\t\t\tgraph_line_addstr(line, \"~ \");\n> +\t\t\tbreak;\n>  \t\t} else if (seen_this && (graph->expansion_row == 0)) {\n>  \t\t\t/*\n>  \t\t\t * This is the first line of the pre-commit output.\n> @@ -994,6 +1022,12 @@ static void graph_draw_octopus_merge(struct git_graph *graph, struct graph_line\n>  \t\tcol = &graph->new_columns[j];\n>  \n>  \t\tgraph_line_write_column(line, col, '-');\n\nAnd here, 'j' comes from graph->mapping[] array.  Does that count in\ndisplay columns or lanes?\n\n> +\t\tif (graph_needs_truncation(graph, j / 2 + i)) {\n\nThis makes it look as if 'j' counts in columns and needs to be\ndivided by 2 to make it comparable to lanes.\n\n> +\t\t\tgraph_line_addstr(line, \"~ \");\n> +\t\t\tbreak;\n> +\t\t}\n> +\n>  \t\tgraph_line_write_column(line, col, (i == dashed_parents - 1) ? '.' : '-');\n>  \t}\n>  \n\n> +\tif (graph->num_parents > 1) {\n> +\t\tif (!graph_needs_truncation(graph, graph->commit_index)) {\n> +\t\t\tgraph_update_state(graph, GRAPH_POST_MERGE);\n> +\t\t} else {\n> +\t\t\tstruct commit_list *first_parent = first_interesting_parent(graph);\n> +\t\t\tint first_parent_col = graph_find_new_column_by_commit(graph, first_parent->item);\n\nAre we sure that first_interesting_parent() will always give us a\nnon-NULL pointer?\n\nCan we use a bit shorter identifier names to deal with these overly\nlong lines?  The lifetime of these two variables is very short so they\ndo not have to be so descriptive.\n\n\t\t\tstruct commit *p = first_interesting_parent(graph)->item;\n\t\t\tint lane = graph_find_new_column_by_commit(graph, p);\n\n> +\t\t\tif (!graph_needs_truncation(graph, first_parent_col))\n> +\t\t\t\tgraph_update_state(graph, GRAPH_POST_MERGE);\n> +\t\t\telse if (graph_is_mapping_correct(graph))\n> +\t\t\t\tgraph_update_state(graph, GRAPH_PADDING);\n> +\t\t\telse\n> +\t\t\t\tgraph_update_state(graph, GRAPH_COLLAPSING);\n> +\t\t}\n> +\t} else if (graph_is_mapping_correct(graph))\n\n"},{"id":"540178","messageId":"CAN5EUNSyBjpZHHAAd1YGVRjkLwzgGzpafhBJVTTcHJCLKNU2gQ@mail.gmail.com","threadId":"65267","inReplyTo":"xmqqh5q3sgnm.fsf@gitster.g","subject":"Re: [GSoC PATCH v5 1/2] graph: add --graph-lane-limit option","fromName":"Pablo","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-27T14:22:06Z","receivedAt":"2026-03-27T14:22:23Z","isPatch":true,"body":"Junio C Hamano (<gitster@pobox.com>) writes:\n>\n> Pablo Sabater <pabloosabaterr@gmail.com> writes:\n>\n> > +static int graph_needs_truncation(struct git_graph *graph, int lane)\n> > +{\n> > +     int max = graph->revs->graph_max_lanes;\n> > +     /*\n> > +      * Ignore values <= 0, meaning no limit.\n> > +      */\n> > +     return max > 0 && lane >= max;\n> > +}\n>\n> Make a mental note that this helper function works on number of\n> lanes, not display columns (which is roughly twice the number of\n> lanes).\n>\n> > @@ -696,6 +705,18 @@ static void graph_update_columns(struct git_graph *graph)\n> >               }\n> >       }\n> >\n> > +     /*\n> > +      * If graph_max_lanes is set, cap the padding from the branches\n> > +      */\n> > +     if (graph->revs->graph_max_lanes > 0) {\n> > +             /*\n> > +              * width of \"| \" per lanes plus truncation mark \"~ \".\n> > +              */\n> > +             int max_columns_width = graph->revs->graph_max_lanes * 2 + 2;\n> > +             if (graph->width > max_columns_width)\n> > +                     graph->width = max_columns_width;\n> > +     }\n> > +\n> >       /*\n> >        * Shrink mapping_size to be the minimum necessary\n> >        */\n> > @@ -846,6 +867,10 @@ static void graph_output_padding_line(struct git_graph *graph,\n> >        * Output a padding row, that leaves all branch lines unchanged\n> >        */\n> >       for (i = 0; i < graph->num_new_columns; i++) {\n> > +             if (graph_needs_truncation(graph, i)) {\n> > +                     graph_line_addstr(line, \"~ \");\n> > +                     break;\n> > +             }\n>\n> And that mental note helps to convince us this loop makes sense, as\n> it increments 'i' one by one ;-)\n\nOk, I'll add the note to graph_needs_truncation() and any other places\nthat might need to be more clear about if it handles columns or lanes.\n\n>\n> > @@ -903,6 +928,9 @@ static void graph_output_pre_commit_line(struct git_graph *graph,\n> >                       seen_this = 1;\n> >                       graph_line_write_column(line, col, '|');\n> >                       graph_line_addchars(line, ' ', graph->expansion_row);\n> > +             } else if (seen_this && graph_needs_truncation(graph, i)) {\n> > +                     graph_line_addstr(line, \"~ \");\n> > +                     break;\n> >               } else if (seen_this && (graph->expansion_row == 0)) {\n> >                       /*\n> >                        * This is the first line of the pre-commit output.\n> > @@ -994,6 +1022,12 @@ static void graph_draw_octopus_merge(struct git_graph *graph, struct graph_line\n> >               col = &graph->new_columns[j];\n> >\n> >               graph_line_write_column(line, col, '-');\n>\n> And here, 'j' comes from graph->mapping[] array.  Does that count in\n> display columns or lanes?\n>\n> > +             if (graph_needs_truncation(graph, j / 2 + i)) {\n>\n> This makes it look as if 'j' counts in columns and needs to be\n> divided by 2 to make it comparable to lanes.\n\nActually, no, because there are other parts like\ngraph_output_post_merge_line handling i and j  like that and it is a\nmore mechanical thing that logical I didn't double checked it, it\nshould be something like commit_index + 1 + i similar to what j is,\nimma check to be sure and add another test for this to be sure because\ncurrent ones pass this and that's why I thought it was ok in the first\nplace.\n\n>\n> > +                     graph_line_addstr(line, \"~ \");\n> > +                     break;\n> > +             }\n> > +\n> >               graph_line_write_column(line, col, (i == dashed_parents - 1) ? '.' : '-');\n> >       }\n> >\n>\n> > +     if (graph->num_parents > 1) {\n> > +             if (!graph_needs_truncation(graph, graph->commit_index)) {\n> > +                     graph_update_state(graph, GRAPH_POST_MERGE);\n> > +             } else {\n> > +                     struct commit_list *first_parent = first_interesting_parent(graph);\n> > +                     int first_parent_col = graph_find_new_column_by_commit(graph, first_parent->item);\n>\n> Are we sure that first_interesting_parent() will always give us a\n> non-NULL pointer?\n\nmy bad, first_interestign_parent() can be a NULL, will add a check for that\n\n>\n> Can we use a bit shorter identifier names to deal with these overly\n> long lines?  The lifetime of these two variables is very short so they\n> do not have to be so descriptive.\n>\n>                         struct commit *p = first_interesting_parent(graph)->item;\n>                         int lane = graph_find_new_column_by_commit(graph, p);\n>\n> > +                     if (!graph_needs_truncation(graph, first_parent_col))\n> > +                             graph_update_state(graph, GRAPH_POST_MERGE);\n> > +                     else if (graph_is_mapping_correct(graph))\n> > +                             graph_update_state(graph, GRAPH_PADDING);\n> > +                     else\n> > +                             graph_update_state(graph, GRAPH_COLLAPSING);\n> > +             }\n> > +     } else if (graph_is_mapping_correct(graph))\n>\n\nsure\n\nThanks for the feedback I'll start with the v6,\nPablo.\n"},{"id":"540188","messageId":"CAN5EUNSuVNPfC5bChw7ocBJD5_ObsAvVv9Q=jaD6v_go4e9nyg@mail.gmail.com","threadId":"65267","inReplyTo":"CAN5EUNSyBjpZHHAAd1YGVRjkLwzgGzpafhBJVTTcHJCLKNU2gQ@mail.gmail.com","subject":"Re: [GSoC PATCH v5 1/2] graph: add --graph-lane-limit option","fromName":"Pablo","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-27T16:07:34Z","receivedAt":"2026-03-27T16:07:52Z","isPatch":true,"body":"Pablo (<pabloosabaterr@gmail.com>) writes:\n\n> > > +                     graph_line_addstr(line, \"~ \");\n> > > +                     break;\n> > > +             }\n> > > +\n> > >               graph_line_write_column(line, col, (i == dashed_parents - 1) ? '.' : '-');\n> > >       }\n> > >\n> >\n> > > +     if (graph->num_parents > 1) {\n> > > +             if (!graph_needs_truncation(graph, graph->commit_index)) {\n> > > +                     graph_update_state(graph, GRAPH_POST_MERGE);\n> > > +             } else {\n> > > +                     struct commit_list *first_parent = first_interesting_parent(graph);\n> > > +                     int first_parent_col = graph_find_new_column_by_commit(graph, first_parent->item);\n> >\n> > Are we sure that first_interesting_parent() will always give us a\n> > non-NULL pointer?\n>\n> my bad, first_interestign_parent() can be a NULL, will add a check for that\n\nActually, I've been looking and first_interesting_parent() can't\nreturn NULL here because: num_parents is counted using\nfirst_interesting_parent()/next_interesting_parent() so if num_parents\n> 1 it guarantees that first_interesting_parent() is non-NULL. I'll\nadd a BUG() to have it reflected on the code.\n\nPablo\n"},{"id":"540198","messageId":"xmqqmrztkz9j.fsf@gitster.g","threadId":"65267","inReplyTo":"CAN5EUNSyBjpZHHAAd1YGVRjkLwzgGzpafhBJVTTcHJCLKNU2gQ@mail.gmail.com","subject":"Re: [GSoC PATCH v5 1/2] graph: add --graph-lane-limit option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-27T16:34:00Z","receivedAt":"2026-03-27T16:34:02Z","isPatch":true,"body":"Pablo <pabloosabaterr@gmail.com> writes:\n\n>> Make a mental note that this helper function works on number of\n>> lanes, not display columns (which is roughly twice the number of\n>> lanes).\n>> ...\n>> And that mental note helps to convince us this loop makes sense, as\n>> it increments 'i' one by one ;-)\n>\n> Ok, I'll add the note to graph_needs_truncation() and any other places\n> that might need to be more clear about if it handles columns or lanes.\n\nSorry, I should have taken into account that you are new around\nhere.  My \"mental note\" comment wasn't meant to suggest adding extra\ncomments in the code.  Rather, it is \"readers would make a mental\nnote here after reading this piece of code---and then what they\nlater see this other piece of code, what it does is consistent with\nwhat they remember from the earlier piece code did, which is good\"\n(if they are inconsistent, you'd see a similar \"make a mental note\nhere\" followed later by \"but this contradicts what we saw\nearlier. what is going on?!?!\" instead).\n\n"},{"id":"540247","messageId":"20260328001113.1275291-1-pabloosabaterr@gmail.com","threadId":"65267","inReplyTo":"20260325174401.217577-1-pabloosabaterr@gmail.com","subject":"[GSoC PATCH v6 0/3] graph: add --graph-lane-limit option","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-28T00:11:10Z","receivedAt":"2026-03-28T00:11:20Z","isPatch":true,"body":"Repositories that have many active branches at the same time produce\nwide graphs. A lane consists of two columns, the edge and the space\npadding, each branch takes a lane in the graph and there is no way\nto limit how many can be shown.\n\nThe limit is a horizontal truncation, each lane is cut at the lane limit:\n\n  Without --graph-lane-limit:\n\n  *   7_M1\n  |\\  \n  | * 7_E\n  * | 7_C\n  | | *   7_M2\n  | | |\\  \n  | | | * 7_H\n  | | |/  \n  | |/|   \n  | * | 7_D\n  | | * 7_G\n  | | * 7_F\n  | |/  \n  |/|   \n  * | 7_B\n  |/  \n  * 7_A\n\n  With --graph-lane-limit=1:\n\n  *   7_M1\n  |\\  \n  | * 7_E\n  * ~ 7_C\n  | ~ 7_M2\n  | ~ 7_H\n  | ~ \n  | ~ \n  | * 7_D\n  | ~ 7_G\n  | ~ 7_F\n  | ~ \n  |/~ \n  * ~ 7_B\n  |/  \n  * 7_A\n\nThe '~' is the truncation mark, not an actual lane. It was chosen because '.' is\nalready used in octopus merges and '~' is not used elsewhere in the graph. Yet\nthe edges between the last visible lane and the truncation mark are still\nconserved.\n\nThe '*' commit mark is visible when it lives on a visible lane or the first\nhidden lane, any deeper lane doesn't show the commit mark but keeps the commit\nmessage visible. \n\nMerges where neither the commit nor its parents live on a visible lane are skipped\nbecause they don't carry any visible information.\n\nThe original idea to limit columns was noted as a TODO in c12172d2ea\n(Add history graph API, 2008-05-04).  This does not implement\ngitk-style column rearrangement, it only truncates the visual output.\n\nPossible future improvements:\n\n- When all branches involved in collapsing or padding are over the limit, the\n  truncated lane doesn't show any information, this lane could be removed to\n  make the graph more compact. Currently these lanes still appear because\n  graph_output_collapsing_line() mixes state handling with rendering, so it can't\n  be skipped but callers always expect a non empty buffer. Fixing it would need\n  to refactor the callers to handle empty buffers instead of expecting them to\n  always have content.\n\n- Collapsing and merges lanes that start on visible lanes but end on hidden ones\n  are kept to maintain the most information possible on the visible lanes, but\n  the information about where they go or come from is lost. They can be kept,\n  removed or think of a way to show that information without showing the lanes.\n\nChanges since v5:\n\n- Changed patch structure\n- Fixed octopus merge truncation check to commit_index + 1 + i.\n- Added check for first_interesting_parent() NULL check.\n- Shortened variable names.\n- Added clarifications when converting between lanes and columns.\n\nPablo Sabater (3):\n  graph: limit the graph width to a hard-coded max\n  graph: add --graph-lane-limit option\n  graph: add truncation mark to capped lanes\n\n Documentation/rev-list-options.adoc |   6 +\n graph.c                             | 178 ++++++++++++++++++++++++----\n revision.c                          |   6 +\n revision.h                          |   1 +\n t/t4215-log-skewed-merges.sh        | 144 ++++++++++++++++++++++\n 5 files changed, 311 insertions(+), 24 deletions(-)\n\n\nbase-commit: 5361983c075154725be47b65cca9a2421789e410\n-- \n2.43.0\n\n"},{"id":"540248","messageId":"20260328001113.1275291-2-pabloosabaterr@gmail.com","threadId":"65267","inReplyTo":"20260328001113.1275291-1-pabloosabaterr@gmail.com","subject":"[GSoC PATCH v6 1/3] graph: limit the graph width to a hard-coded max","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-28T00:11:11Z","receivedAt":"2026-03-28T00:11:22Z","isPatch":true,"body":"Repositories that have many active branches at the same time\nproduce wide graphs. A lane consists of two columns, the edge and the\npadding (or another edge), each branch takes a lane in the graph and\nthere is no way to limit how many can be shown.\n\nLimit the graph engine to draw at most 15 lanes. Lanes over the limit are\nnot rendered.\n\nOn the commit line, if the commit lives on a visible lane, show the\nnormal commit mark and stop rendering. If the commit lives on the\nfirst hidden lane, show the \"*\" commit mark so it is known that\nthis commit lives in the first hidden lane. Commits on deeper lanes\naren't rendered, but the commit subject will always remain.\n\nFor merges, the post-merge lane is only needed when the commit or\nthe first parent lives on a visible lane (to draw the connection\nbetween them), when both are on hidden lanes, post-merge carries\nno useful information, skip it and go to collapsing or padding state.\n\nAlso fix a pre-existing indentation issue.\n\nThe hard-coded limit will be replaced by a user-facing option\non a subsequent commit.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n graph.c | 161 +++++++++++++++++++++++++++++++++++++++++++++++---------\n 1 file changed, 136 insertions(+), 25 deletions(-)\n\ndiff --git a/graph.c b/graph.c\nindex 26f6fbf000..70458cf323 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -82,6 +82,8 @@ static void graph_show_line_prefix(const struct diff_options *diffopt)\n static const char **column_colors;\n static unsigned short column_colors_max;\n \n+static unsigned int max_lanes = 15;\n+\n static void parse_graph_colors_config(struct strvec *colors, const char *string)\n {\n \tconst char *end, *start;\n@@ -317,6 +319,11 @@ struct git_graph {\n \tstruct strbuf prefix_buf;\n };\n \n+static inline int graph_needs_truncation(int lane)\n+{\n+\treturn lane >= max_lanes;\n+}\n+\n static const char *diff_output_prefix_callback(struct diff_options *opt, void *data)\n {\n \tstruct git_graph *graph = data;\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, max;\n \n \t/*\n \t * Swap graph->columns with graph->new_columns\n@@ -696,6 +703,14 @@ static void graph_update_columns(struct git_graph *graph)\n \t\t}\n \t}\n \n+\t/*\n+\t * Cap to the hard-coded limit.\n+\t * Allow commits from merges to align to the merged lane.\n+\t */\n+\tmax = max_lanes * 2 + 2;\n+\tif (graph->width > max)\n+\t\tgraph->width = max;\n+\n \t/*\n \t * Shrink mapping_size to be the minimum necessary\n \t */\n@@ -846,6 +861,8 @@ 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\tif (graph_needs_truncation(i))\n+\t\t\tbreak;\n \t\tgraph_line_write_column(line, &graph->new_columns[i], '|');\n \t\tgraph_line_addch(line, ' ');\n \t}\n@@ -903,6 +920,8 @@ static void graph_output_pre_commit_line(struct git_graph *graph,\n \t\t\tseen_this = 1;\n \t\t\tgraph_line_write_column(line, col, '|');\n \t\t\tgraph_line_addchars(line, ' ', graph->expansion_row);\n+\t\t} else if (seen_this && graph_needs_truncation(i)) {\n+\t\t\tbreak;\n \t\t} else if (seen_this && (graph->expansion_row == 0)) {\n \t\t\t/*\n \t\t\t * This is the first line of the pre-commit output.\n@@ -994,6 +1013,14 @@ static void graph_draw_octopus_merge(struct git_graph *graph, struct graph_line\n \t\tcol = &graph->new_columns[j];\n \n \t\tgraph_line_write_column(line, col, '-');\n+\n+\t\t/*\n+\t\t * Commit is at commit_index, each iteration move one lane to\n+\t\t * the right from the commit.\n+\t\t */\n+\t\tif (graph_needs_truncation(graph->commit_index + 1 + i))\n+\t\t\tbreak;\n+\n \t\tgraph_line_write_column(line, col, (i == dashed_parents - 1) ? '.' : '-');\n \t}\n \n@@ -1028,8 +1055,16 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \t\t\tseen_this = 1;\n \t\t\tgraph_output_commit_char(graph, line);\n \n+\t\t\tif (graph_needs_truncation(i)) {\n+\t\t\t\tgraph_line_addch(line, ' ');\n+\t\t\t\tbreak;\n+\t\t\t}\n+\n \t\t\tif (graph->num_parents > 2)\n \t\t\t\tgraph_draw_octopus_merge(graph, line);\n+\t\t} else if (graph_needs_truncation(i)) {\n+\t\t\tseen_this = 1;\n+\t\t\tbreak;\n \t\t} else if (seen_this && (graph->edges_added > 1)) {\n \t\t\tgraph_line_write_column(line, col, '\\\\');\n \t\t} else if (seen_this && (graph->edges_added == 1)) {\n@@ -1065,13 +1100,46 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \n \t/*\n \t * Update graph->state\n+\t *\n+\t * If the commit is a merge and the first parent is in a visible lane,\n+\t * then the GRAPH_POST_MERGE is needed to draw the merge lane.\n+\t *\n+\t * If the commit is over the truncation limit, but the first parent is on\n+\t * a visible lane, then we still need the merge lane but truncated.\n+\t *\n+\t * If both commit and first parent are over the truncation limit, then\n+\t * there's no need to draw the merge lane because it would work as a\n+\t * padding lane.\n \t */\n-\tif (graph->num_parents > 1)\n-\t\tgraph_update_state(graph, GRAPH_POST_MERGE);\n-\telse if (graph_is_mapping_correct(graph))\n+\tif (graph->num_parents > 1) {\n+\t\tif (!graph_needs_truncation(graph->commit_index)) {\n+\t\t\tgraph_update_state(graph, GRAPH_POST_MERGE);\n+\t\t} else {\n+\t\t\tstruct commit_list *p = first_interesting_parent(graph);\n+\t\t\tint lane;\n+\n+\t\t\t/*\n+\t\t\t * graph->num_parents are found using first_interesting_parent\n+\t\t\t * and next_interesting_parent so it can't be a scenario\n+\t\t\t * where num_parents > 1 and there are no interesting parents\n+\t\t\t */\n+\t\t\tif (!p)\n+\t\t\t\tBUG(\"num_parents > 1 but no interesting parent\");\n+\n+\t\t\tlane = graph_find_new_column_by_commit(graph, p->item);\n+\n+\t\t\tif (!graph_needs_truncation(lane))\n+\t\t\t\tgraph_update_state(graph, GRAPH_POST_MERGE);\n+\t\t\telse if (graph_is_mapping_correct(graph))\n+\t\t\t\tgraph_update_state(graph, GRAPH_PADDING);\n+\t\t\telse\n+\t\t\t\tgraph_update_state(graph, GRAPH_COLLAPSING);\n+\t\t}\n+\t} else if (graph_is_mapping_correct(graph)) {\n \t\tgraph_update_state(graph, GRAPH_PADDING);\n-\telse\n+\t} else {\n \t\tgraph_update_state(graph, GRAPH_COLLAPSING);\n+\t}\n }\n \n static const char merge_chars[] = {'/', '|', '\\\\'};\n@@ -1109,6 +1177,7 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n \t\t\tint par_column;\n \t\t\tint idx = graph->merge_layout;\n \t\t\tchar c;\n+\t\t\tint truncated = 0;\n \t\t\tseen_this = 1;\n \n \t\t\tfor (j = 0; j < graph->num_parents; j++) {\n@@ -1117,23 +1186,53 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n \n \t\t\t\tc = merge_chars[idx];\n \t\t\t\tgraph_line_write_column(line, &graph->new_columns[par_column], c);\n+\n+\t\t\t\t/*\n+\t\t\t\t * j counts parents, it needs to be halved to be\n+\t\t\t\t * comparable with i. Don't truncate if there are\n+\t\t\t\t * no more lanes to print (end of the lane)\n+\t\t\t\t */\n+\t\t\t\tif (graph_needs_truncation(j / 2 + i) &&\n+\t\t\t\t    j / 2 + i <= graph->num_columns) {\n+\t\t\t\t\tif ((j + i * 2) % 2 != 0)\n+\t\t\t\t\t\tgraph_line_addch(line, ' ');\n+\t\t\t\t\ttruncated = 1;\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\n \t\t\t\tif (idx == 2) {\n-\t\t\t\t\tif (graph->edges_added > 0 || j < graph->num_parents - 1)\n+\t\t\t\t\t/*\n+\t\t\t\t\t * Check if the next lane needs truncation\n+\t\t\t\t\t * to avoid having the padding doubled\n+\t\t\t\t\t */\n+\t\t\t\t\tif (graph_needs_truncation((j + 1) / 2 + i) &&\n+\t\t\t\t\t    j < graph->num_parents - 1) {\n+\t\t\t\t\t\ttruncated = 1;\n+\t\t\t\t\t\tbreak;\n+\t\t\t\t\t} else if (graph->edges_added > 0 || j < graph->num_parents - 1)\n \t\t\t\t\t\tgraph_line_addch(line, ' ');\n \t\t\t\t} else {\n \t\t\t\t\tidx++;\n \t\t\t\t}\n \t\t\t\tparents = next_interesting_parent(graph, parents);\n \t\t\t}\n+\t\t\tif (truncated)\n+\t\t\t\tbreak;\n \t\t\tif (graph->edges_added == 0)\n \t\t\t\tgraph_line_addch(line, ' ');\n-\n+\t\t} else if (graph_needs_truncation(i)) {\n+\t\t\tbreak;\n \t\t} else if (seen_this) {\n \t\t\tif (graph->edges_added > 0)\n \t\t\t\tgraph_line_write_column(line, col, '\\\\');\n \t\t\telse\n \t\t\t\tgraph_line_write_column(line, col, '|');\n-\t\t\tgraph_line_addch(line, ' ');\n+\t\t\t/*\n+\t\t\t * If it's between two lanes and next would be truncated,\n+\t\t\t * don't add space padding.\n+\t\t\t */\n+\t\t\tif (!graph_needs_truncation(i + 1))\n+\t\t\t\tgraph_line_addch(line, ' ');\n \t\t} else {\n \t\t\tgraph_line_write_column(line, col, '|');\n \t\t\tif (graph->merge_layout != 0 || i != graph->commit_index - 1) {\n@@ -1164,6 +1263,7 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n \tshort used_horizontal = 0;\n \tint horizontal_edge = -1;\n \tint horizontal_edge_target = -1;\n+\tint truncated = 0;\n \n \t/*\n \t * Swap the mapping and old_mapping arrays\n@@ -1279,26 +1379,34 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n \t */\n \tfor (i = 0; i < graph->mapping_size; i++) {\n \t\tint target = graph->mapping[i];\n-\t\tif (target < 0)\n-\t\t\tgraph_line_addch(line, ' ');\n-\t\telse if (target * 2 == i)\n-\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '|');\n-\t\telse if (target == horizontal_edge_target &&\n-\t\t\t i != horizontal_edge - 1) {\n-\t\t\t\t/*\n-\t\t\t\t * Set the mappings for all but the\n-\t\t\t\t * first segment to -1 so that they\n-\t\t\t\t * won't continue into the next line.\n-\t\t\t\t */\n-\t\t\t\tif (i != (target * 2)+3)\n-\t\t\t\t\tgraph->mapping[i] = -1;\n-\t\t\t\tused_horizontal = 1;\n-\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '_');\n+\n+\t\tif (!truncated && graph_needs_truncation(i / 2)) {\n+\t\t\ttruncated = 1;\n+\t\t}\n+\n+\t\tif (target < 0) {\n+\t\t\tif (!truncated)\n+\t\t\t\tgraph_line_addch(line, ' ');\n+\t\t} else if (target * 2 == i) {\n+\t\t\tif (!truncated)\n+\t\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '|');\n+\t\t} else if (target == horizontal_edge_target &&\n+\t\t\t   i != horizontal_edge - 1) {\n+\t\t\t/*\n+\t\t\t * Set the mappings for all but the\n+\t\t\t * first segment to -1 so that they\n+\t\t\t * won't continue into the next line.\n+\t\t\t */\n+\t\t\tif (i != (target * 2)+3)\n+\t\t\t\tgraph->mapping[i] = -1;\n+\t\t\tused_horizontal = 1;\n+\t\t\tif (!truncated)\n+\t\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '_');\n \t\t} else {\n \t\t\tif (used_horizontal && i < horizontal_edge)\n \t\t\t\tgraph->mapping[i] = -1;\n-\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '/');\n-\n+\t\t\tif (!truncated)\n+\t\t\t\tgraph_line_write_column(line, &graph->new_columns[target], '/');\n \t\t}\n \t}\n \n@@ -1372,6 +1480,9 @@ static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)\n \tfor (i = 0; i < graph->num_columns; i++) {\n \t\tstruct column *col = &graph->columns[i];\n \n+\t\tif (graph_needs_truncation(i))\n+\t\t\tbreak;\n+\n \t\tgraph_line_write_column(&line, col, '|');\n \n \t\tif (col->commit == graph->commit && graph->num_parents > 2) {\n-- \n2.43.0\n\n"},{"id":"540249","messageId":"20260328001113.1275291-3-pabloosabaterr@gmail.com","threadId":"65267","inReplyTo":"20260328001113.1275291-1-pabloosabaterr@gmail.com","subject":"[GSoC PATCH v6 2/3] graph: add --graph-lane-limit option","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-28T00:11:12Z","receivedAt":"2026-03-28T00:11:23Z","isPatch":true,"body":"Replace the hard-coded lane limit with a user-facing\noption '--graph-lane-limit=<n>'. It caps the number of\nvisible lanes to n. This option requires '--graph', without\nit, limiting the graph has no meaning, in this case error out.\n\nZero and negative values are valid inputs but silently\nignored treating them as \"no limit\", the same as not using\nthe option. This follows what '--max-parents' does with\nnegative values.\n\nThe default is 0, same as not being used.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n Documentation/rev-list-options.adoc |   5 +\n graph.c                             |  53 +++++-----\n revision.c                          |   6 ++\n revision.h                          |   1 +\n t/t4215-log-skewed-merges.sh        | 144 ++++++++++++++++++++++++++++\n 5 files changed, 186 insertions(+), 23 deletions(-)\n\ndiff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc\nindex 2d195a1474..1b6ea89a63 100644\n--- a/Documentation/rev-list-options.adoc\n+++ b/Documentation/rev-list-options.adoc\n@@ -1259,6 +1259,11 @@ This implies the `--topo-order` option by default, but the\n \tin between them in that case. If _<barrier>_ is specified, it\n \tis the string that will be shown instead of the default one.\n \n+`--graph-lane-limit=<n>`::\n+\tWhen `--graph` is used, limit the number of graph lanes to be shown.\n+\tLanes over the limit are not shown. By default it is set to 0 \n+\t(no limit), zero and negative values are ignored and treated as no limit.\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 70458cf323..ee1f9e2d2d 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -82,8 +82,6 @@ static void graph_show_line_prefix(const struct diff_options *diffopt)\n static const char **column_colors;\n static unsigned short column_colors_max;\n \n-static unsigned int max_lanes = 15;\n-\n static void parse_graph_colors_config(struct strvec *colors, const char *string)\n {\n \tconst char *end, *start;\n@@ -319,9 +317,13 @@ struct git_graph {\n \tstruct strbuf prefix_buf;\n };\n \n-static inline int graph_needs_truncation(int lane)\n+static inline int graph_needs_truncation(struct git_graph *graph, int lane)\n {\n-\treturn lane >= max_lanes;\n+\tint max = graph->revs->graph_max_lanes;\n+\t/*\n+\t * Ignore values <= 0, meaning no limit.\n+\t */\n+\treturn max > 0 && lane >= max;\n }\n \n static const char *diff_output_prefix_callback(struct diff_options *opt, void *data)\n@@ -614,7 +616,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, max;\n+\tint i, seen_this, is_commit_in_columns;\n \n \t/*\n \t * Swap graph->columns with graph->new_columns\n@@ -704,12 +706,17 @@ static void graph_update_columns(struct git_graph *graph)\n \t}\n \n \t/*\n-\t * Cap to the hard-coded limit.\n-\t * Allow commits from merges to align to the merged lane.\n+\t *  If graph_max_lanes is set, cap the width\n \t */\n-\tmax = max_lanes * 2 + 2;\n-\tif (graph->width > max)\n-\t\tgraph->width = max;\n+\tif (graph->revs->graph_max_lanes > 0) {\n+\t\t/*\n+\t\t * Width is column index while a lane is half that.\n+\t\t * Allow commits from merges to align to the merged lane.\n+\t\t */\n+\t\tint max_width = graph->revs->graph_max_lanes * 2 + 2;\n+\t\tif (graph->width > max_width)\n+\t\t\tgraph->width = max_width;\n+\t}\n \n \t/*\n \t * Shrink mapping_size to be the minimum necessary\n@@ -861,7 +868,7 @@ 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\tif (graph_needs_truncation(i))\n+\t\tif (graph_needs_truncation(graph, i))\n \t\t\tbreak;\n \t\tgraph_line_write_column(line, &graph->new_columns[i], '|');\n \t\tgraph_line_addch(line, ' ');\n@@ -920,7 +927,7 @@ static void graph_output_pre_commit_line(struct git_graph *graph,\n \t\t\tseen_this = 1;\n \t\t\tgraph_line_write_column(line, col, '|');\n \t\t\tgraph_line_addchars(line, ' ', graph->expansion_row);\n-\t\t} else if (seen_this && graph_needs_truncation(i)) {\n+\t\t} else if (seen_this && graph_needs_truncation(graph, i)) {\n \t\t\tbreak;\n \t\t} else if (seen_this && (graph->expansion_row == 0)) {\n \t\t\t/*\n@@ -1018,7 +1025,7 @@ static void graph_draw_octopus_merge(struct git_graph *graph, struct graph_line\n \t\t * Commit is at commit_index, each iteration move one lane to\n \t\t * the right from the commit.\n \t\t */\n-\t\tif (graph_needs_truncation(graph->commit_index + 1 + i))\n+\t\tif (graph_needs_truncation(graph, graph->commit_index + 1 + i))\n \t\t\tbreak;\n \n \t\tgraph_line_write_column(line, col, (i == dashed_parents - 1) ? '.' : '-');\n@@ -1055,14 +1062,14 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \t\t\tseen_this = 1;\n \t\t\tgraph_output_commit_char(graph, line);\n \n-\t\t\tif (graph_needs_truncation(i)) {\n+\t\t\tif (graph_needs_truncation(graph, i)) {\n \t\t\t\tgraph_line_addch(line, ' ');\n \t\t\t\tbreak;\n \t\t\t}\n \n \t\t\tif (graph->num_parents > 2)\n \t\t\t\tgraph_draw_octopus_merge(graph, line);\n-\t\t} else if (graph_needs_truncation(i)) {\n+\t\t} else if (graph_needs_truncation(graph, i)) {\n \t\t\tseen_this = 1;\n \t\t\tbreak;\n \t\t} else if (seen_this && (graph->edges_added > 1)) {\n@@ -1112,7 +1119,7 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \t * padding lane.\n \t */\n \tif (graph->num_parents > 1) {\n-\t\tif (!graph_needs_truncation(graph->commit_index)) {\n+\t\tif (!graph_needs_truncation(graph, graph->commit_index)) {\n \t\t\tgraph_update_state(graph, GRAPH_POST_MERGE);\n \t\t} else {\n \t\t\tstruct commit_list *p = first_interesting_parent(graph);\n@@ -1128,7 +1135,7 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \n \t\t\tlane = graph_find_new_column_by_commit(graph, p->item);\n \n-\t\t\tif (!graph_needs_truncation(lane))\n+\t\t\tif (!graph_needs_truncation(graph, lane))\n \t\t\t\tgraph_update_state(graph, GRAPH_POST_MERGE);\n \t\t\telse if (graph_is_mapping_correct(graph))\n \t\t\t\tgraph_update_state(graph, GRAPH_PADDING);\n@@ -1192,7 +1199,7 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n \t\t\t\t * comparable with i. Don't truncate if there are\n \t\t\t\t * no more lanes to print (end of the lane)\n \t\t\t\t */\n-\t\t\t\tif (graph_needs_truncation(j / 2 + i) &&\n+\t\t\t\tif (graph_needs_truncation(graph, j / 2 + i) &&\n \t\t\t\t    j / 2 + i <= graph->num_columns) {\n \t\t\t\t\tif ((j + i * 2) % 2 != 0)\n \t\t\t\t\t\tgraph_line_addch(line, ' ');\n@@ -1205,7 +1212,7 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n \t\t\t\t\t * Check if the next lane needs truncation\n \t\t\t\t\t * to avoid having the padding doubled\n \t\t\t\t\t */\n-\t\t\t\t\tif (graph_needs_truncation((j + 1) / 2 + i) &&\n+\t\t\t\t\tif (graph_needs_truncation(graph, (j + 1) / 2 + i) &&\n \t\t\t\t\t    j < graph->num_parents - 1) {\n \t\t\t\t\t\ttruncated = 1;\n \t\t\t\t\t\tbreak;\n@@ -1220,7 +1227,7 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n \t\t\t\tbreak;\n \t\t\tif (graph->edges_added == 0)\n \t\t\t\tgraph_line_addch(line, ' ');\n-\t\t} else if (graph_needs_truncation(i)) {\n+\t\t} else if (graph_needs_truncation(graph, i)) {\n \t\t\tbreak;\n \t\t} else if (seen_this) {\n \t\t\tif (graph->edges_added > 0)\n@@ -1231,7 +1238,7 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n \t\t\t * If it's between two lanes and next would be truncated,\n \t\t\t * don't add space padding.\n \t\t\t */\n-\t\t\tif (!graph_needs_truncation(i + 1))\n+\t\t\tif (!graph_needs_truncation(graph, i + 1))\n \t\t\t\tgraph_line_addch(line, ' ');\n \t\t} else {\n \t\t\tgraph_line_write_column(line, col, '|');\n@@ -1380,7 +1387,7 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n \tfor (i = 0; i < graph->mapping_size; i++) {\n \t\tint target = graph->mapping[i];\n \n-\t\tif (!truncated && graph_needs_truncation(i / 2)) {\n+\t\tif (!truncated && graph_needs_truncation(graph, i / 2)) {\n \t\t\ttruncated = 1;\n \t\t}\n \n@@ -1480,7 +1487,7 @@ static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)\n \tfor (i = 0; i < graph->num_columns; i++) {\n \t\tstruct column *col = &graph->columns[i];\n \n-\t\tif (graph_needs_truncation(i))\n+\t\tif (graph_needs_truncation(graph, i))\n \t\t\tbreak;\n \n \t\tgraph_line_write_column(&line, col, '|');\ndiff --git a/revision.c b/revision.c\nindex 31808e3df0..81b67682a8 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -2605,6 +2605,8 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg\n \t} else if (!strcmp(arg, \"--no-graph\")) {\n \t\tgraph_clear(revs->graph);\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, \"--encode-email-headers\")) {\n \t\trevs->encode_email_headers = 1;\n \t} else if (!strcmp(arg, \"--no-encode-email-headers\")) {\n@@ -3172,6 +3174,10 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s\n \n \tif (revs->no_walk && revs->graph)\n \t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--no-walk\", \"--graph\");\n+\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->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 69242ecb18..874ccce625 100644\n--- a/revision.h\n+++ b/revision.h\n@@ -304,6 +304,7 @@ struct rev_info {\n \n \t/* Display history graph */\n \tstruct git_graph *graph;\n+\tint graph_max_lanes;\n \n \t/* special limits */\n \tint skip_count;\ndiff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh\nindex 28d0779a8c..d7524e9366 100755\n--- a/t/t4215-log-skewed-merges.sh\n+++ b/t/t4215-log-skewed-merges.sh\n@@ -370,4 +370,148 @@ test_expect_success 'log --graph with multiple tips' '\n \tEOF\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+\t*-.   7_M4\n+\t|\\ \\\n+\t| | * 7_G\n+\t| | * 7_F\n+\t| *   7_E\n+\t| *   7_D\n+\t* |   7_C\n+\t| |/\n+\t|/|\n+\t* | 7_B\n+\t|/\n+\t* 7_A\n+\tEOF\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+\t*-  7_M4\n+\t|\\\n+\t|   7_G\n+\t|   7_F\n+\t| * 7_E\n+\t| * 7_D\n+\t*   7_C\n+\t|\n+\t|/\n+\t*   7_B\n+\t|/\n+\t* 7_A\n+\tEOF\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+\t*   7_M1\n+\t|\\\n+\t| | *   7_M2\n+\t| | |\\\n+\t| | | * 7_H\n+\t| | |   7_M3\n+\t| | |   7_J\n+\t| | |   7_I\n+\t| | |   7_M4\n+\t| |_|_\n+\t|/| |\n+\t| | |_\n+\t| |/|\n+\t| | |\n+\t| | |/\n+\t| | *   7_G\n+\t| | |\n+\t| | |/\n+\t| | *   7_F\n+\t| * |   7_E\n+\t| | |/\n+\t| |/|\n+\t| * |   7_D\n+\t| | |/\n+\t| |/|\n+\t* | | 7_C\n+\t| |/\n+\t|/|\n+\t* | 7_B\n+\t|/\n+\t* 7_A\n+\tEOF\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+\t*   7_M1\n+\t|\\\n+\t| | *   7_M2\n+\t| | |\\\n+\t| | | * 7_H\n+\t| | | | *   7_M3\n+\t| | | | |\\\n+\t| | | | | * 7_J\n+\t| | | | * | 7_I\n+\t| | | | | | * 7_M4\n+\t| |_|_|_|_|/\n+\t|/| | | | |/\n+\t| | |_|_|/|\n+\t| |/| | | |/\n+\t| | | |_|/|\n+\t| | |/| | |\n+\t| | * | | | 7_G\n+\t| | | |_|/\n+\t| | |/| |\n+\t| | * | | 7_F\n+\t| * | | | 7_E\n+\t| | |/ /\n+\t| |/| |\n+\t| * | | 7_D\n+\t| | |/\n+\t| |/|\n+\t* | | 7_C\n+\t| |/\n+\t|/|\n+\t* | 7_B\n+\t|/\n+\t* 7_A\n+\tEOF\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+\t*   7_M1\n+\t|\\\n+\t| | *   7_M2\n+\t| | |\\\n+\t| | | * 7_H\n+\t| | | | *   7_M3\n+\t| | | | |\\\n+\t| | | | | * 7_J\n+\t| | | | * | 7_I\n+\t| | | | | | *   7_M4\n+\t| |_|_|_|_|/|\\\n+\t|/| | | | |/ /\n+\t| | |_|_|/| /\n+\t| |/| | | |/\n+\t| | | |_|/|\n+\t| | |/| | |\n+\t| | * | | | 7_G\n+\t| | | |_|/\n+\t| | |/| |\n+\t| | * | | 7_F\n+\t| * | | | 7_E\n+\t| | |/ /\n+\t| |/| |\n+\t| * | | 7_D\n+\t| | |/\n+\t| |/|\n+\t* | | 7_C\n+\t| |/\n+\t|/|\n+\t* | 7_B\n+\t|/\n+\t* 7_A\n+\tEOF\n+'\n+\n test_done\n-- \n2.43.0\n\n"},{"id":"540250","messageId":"20260328001113.1275291-4-pabloosabaterr@gmail.com","threadId":"65267","inReplyTo":"20260328001113.1275291-1-pabloosabaterr@gmail.com","subject":"[GSoC PATCH v6 3/3] graph: add truncation mark to capped lanes","fromName":"Pablo Sabater","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-03-28T00:11:13Z","receivedAt":"2026-03-28T00:11:26Z","isPatch":true,"body":"When lanes are hidden by --graph-lane-limit, show a \"~\"\ntruncation mark, so users know that there are lanes\nbeing truncated. The \"~\" is chosen because it is not\nused elsewhere in the graph and it is discrete.\n\nSigned-off-by: Pablo Sabater <pabloosabaterr@gmail.com>\n---\n Documentation/rev-list-options.adoc |  5 ++-\n graph.c                             | 22 +++++++---\n t/t4215-log-skewed-merges.sh        | 64 ++++++++++++++---------------\n 3 files changed, 52 insertions(+), 39 deletions(-)\n\ndiff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc\nindex 1b6ea89a63..937ffc6195 100644\n--- a/Documentation/rev-list-options.adoc\n+++ b/Documentation/rev-list-options.adoc\n@@ -1261,8 +1261,9 @@ This implies the `--topo-order` option by default, but the\n \n `--graph-lane-limit=<n>`::\n \tWhen `--graph` is used, limit the number of graph lanes to be shown.\n-\tLanes over the limit are not shown. By default it is set to 0 \n-\t(no limit), zero and negative values are ignored and treated as no limit.\n+\tLanes over the limit are replaced with a truncation mark '~'. \n+\tBy default it is set to 0 (no limit), zero and negative values\n+\tare ignored and treated as no limit.\n \n ifdef::git-rev-list[]\n `--count`::\ndiff --git a/graph.c b/graph.c\nindex ee1f9e2d2d..842282685f 100644\n--- a/graph.c\n+++ b/graph.c\n@@ -706,11 +706,11 @@ static void graph_update_columns(struct git_graph *graph)\n \t}\n \n \t/*\n-\t *  If graph_max_lanes is set, cap the width\n+\t * If graph_max_lanes is set, cap the width\n \t */\n \tif (graph->revs->graph_max_lanes > 0) {\n \t\t/*\n-\t\t * Width is column index while a lane is half that.\n+\t\t * width of \"| \" per lanes plus truncation mark \"~ \".\n \t\t * Allow commits from merges to align to the merged lane.\n \t\t */\n \t\tint max_width = graph->revs->graph_max_lanes * 2 + 2;\n@@ -868,8 +868,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\tif (graph_needs_truncation(graph, i))\n+\t\tif (graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addstr(line, \"~ \");\n \t\t\tbreak;\n+\t\t}\n \t\tgraph_line_write_column(line, &graph->new_columns[i], '|');\n \t\tgraph_line_addch(line, ' ');\n \t}\n@@ -928,6 +930,7 @@ static void graph_output_pre_commit_line(struct git_graph *graph,\n \t\t\tgraph_line_write_column(line, col, '|');\n \t\t\tgraph_line_addchars(line, ' ', graph->expansion_row);\n \t\t} else if (seen_this && graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addstr(line, \"~ \");\n \t\t\tbreak;\n \t\t} else if (seen_this && (graph->expansion_row == 0)) {\n \t\t\t/*\n@@ -1025,8 +1028,10 @@ static void graph_draw_octopus_merge(struct git_graph *graph, struct graph_line\n \t\t * Commit is at commit_index, each iteration move one lane to\n \t\t * the right from the commit.\n \t\t */\n-\t\tif (graph_needs_truncation(graph, graph->commit_index + 1 + i))\n+\t\tif (graph_needs_truncation(graph, graph->commit_index + 1 + i)) {\n+\t\t\tgraph_line_addstr(line, \"~ \");\n \t\t\tbreak;\n+\t\t}\n \n \t\tgraph_line_write_column(line, col, (i == dashed_parents - 1) ? '.' : '-');\n \t}\n@@ -1070,6 +1075,7 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line\n \t\t\tif (graph->num_parents > 2)\n \t\t\t\tgraph_draw_octopus_merge(graph, line);\n \t\t} else if (graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addstr(line, \"~ \");\n \t\t\tseen_this = 1;\n \t\t\tbreak;\n \t\t} else if (seen_this && (graph->edges_added > 1)) {\n@@ -1203,6 +1209,7 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n \t\t\t\t    j / 2 + i <= graph->num_columns) {\n \t\t\t\t\tif ((j + i * 2) % 2 != 0)\n \t\t\t\t\t\tgraph_line_addch(line, ' ');\n+\t\t\t\t\tgraph_line_addstr(line, \"~ \");\n \t\t\t\t\ttruncated = 1;\n \t\t\t\t\tbreak;\n \t\t\t\t}\n@@ -1214,6 +1221,7 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n \t\t\t\t\t */\n \t\t\t\t\tif (graph_needs_truncation(graph, (j + 1) / 2 + i) &&\n \t\t\t\t\t    j < graph->num_parents - 1) {\n+\t\t\t\t\t\tgraph_line_addstr(line, \"~ \");\n \t\t\t\t\t\ttruncated = 1;\n \t\t\t\t\t\tbreak;\n \t\t\t\t\t} else if (graph->edges_added > 0 || j < graph->num_parents - 1)\n@@ -1228,6 +1236,7 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l\n \t\t\tif (graph->edges_added == 0)\n \t\t\t\tgraph_line_addch(line, ' ');\n \t\t} else if (graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addstr(line, \"~ \");\n \t\t\tbreak;\n \t\t} else if (seen_this) {\n \t\t\tif (graph->edges_added > 0)\n@@ -1388,6 +1397,7 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l\n \t\tint target = graph->mapping[i];\n \n \t\tif (!truncated && graph_needs_truncation(graph, i / 2)) {\n+\t\t\tgraph_line_addstr(line, \"~ \");\n \t\t\ttruncated = 1;\n \t\t}\n \n@@ -1487,8 +1497,10 @@ static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)\n \tfor (i = 0; i < graph->num_columns; i++) {\n \t\tstruct column *col = &graph->columns[i];\n \n-\t\tif (graph_needs_truncation(graph, i))\n+\t\tif (graph_needs_truncation(graph, i)) {\n+\t\t\tgraph_line_addstr(&line, \"~ \");\n \t\t\tbreak;\n+\t\t}\n \n \t\tgraph_line_write_column(&line, col, '|');\n \ndiff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh\nindex d7524e9366..1612f05f1b 100755\n--- a/t/t4215-log-skewed-merges.sh\n+++ b/t/t4215-log-skewed-merges.sh\n@@ -376,9 +376,9 @@ test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' '\n \t|\\ \\\n \t| | * 7_G\n \t| | * 7_F\n-\t| *   7_E\n-\t| *   7_D\n-\t* |   7_C\n+\t| * ~ 7_E\n+\t| * ~ 7_D\n+\t* | ~ 7_C\n \t| |/\n \t|/|\n \t* | 7_B\n@@ -389,16 +389,16 @@ test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' '\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-\t*-  7_M4\n-\t|\\\n-\t|   7_G\n-\t|   7_F\n+\t*-~  7_M4\n+\t|\\~\n+\t| ~ 7_G\n+\t| ~ 7_F\n \t| * 7_E\n \t| * 7_D\n-\t*   7_C\n-\t|\n-\t|/\n-\t*   7_B\n+\t* ~ 7_C\n+\t| ~\n+\t|/~\n+\t* ~ 7_B\n \t|/\n \t* 7_A\n \tEOF\n@@ -411,24 +411,24 @@ test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' '\n \t| | *   7_M2\n \t| | |\\\n \t| | | * 7_H\n-\t| | |   7_M3\n-\t| | |   7_J\n-\t| | |   7_I\n-\t| | |   7_M4\n-\t| |_|_\n-\t|/| |\n-\t| | |_\n-\t| |/|\n-\t| | |\n-\t| | |/\n-\t| | *   7_G\n-\t| | |\n-\t| | |/\n-\t| | *   7_F\n-\t| * |   7_E\n-\t| | |/\n-\t| |/|\n-\t| * |   7_D\n+\t| | | ~ 7_M3\n+\t| | | ~ 7_J\n+\t| | | ~ 7_I\n+\t| | | ~ 7_M4\n+\t| |_|_~\n+\t|/| | ~\n+\t| | |_~\n+\t| |/| ~\n+\t| | | ~\n+\t| | |/~\n+\t| | * ~ 7_G\n+\t| | | ~\n+\t| | |/~\n+\t| | * ~ 7_F\n+\t| * | ~ 7_E\n+\t| | |/~\n+\t| |/| ~\n+\t| * | ~ 7_D\n \t| | |/\n \t| |/|\n \t* | | 7_C\n@@ -452,9 +452,9 @@ test_expect_success 'log --graph --graph-lane-limit=6 check if it only shows fir\n \t| | | | | * 7_J\n \t| | | | * | 7_I\n \t| | | | | | * 7_M4\n-\t| |_|_|_|_|/\n-\t|/| | | | |/\n-\t| | |_|_|/|\n+\t| |_|_|_|_|/~\n+\t|/| | | | |/~\n+\t| | |_|_|/| ~\n \t| |/| | | |/\n \t| | | |_|/|\n \t| | |/| | |\n-- \n2.43.0\n\n"},{"id":"540572","messageId":"xmqqpl4jwss5.fsf@gitster.g","threadId":"65267","inReplyTo":"20260328001113.1275291-1-pabloosabaterr@gmail.com","subject":"Re: [GSoC PATCH v6 0/3] graph: add --graph-lane-limit option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-31T22:14:34Z","receivedAt":"2026-03-31T22:14:37Z","isPatch":true,"body":"Pablo Sabater <pabloosabaterr@gmail.com> writes:\n\n> Changes since v5:\n>\n> - Changed patch structure\n> - Fixed octopus merge truncation check to commit_index + 1 + i.\n> - Added check for first_interesting_parent() NULL check.\n> - Shortened variable names.\n> - Added clarifications when converting between lanes and columns.\n\nThe updated series structure is quite unique.  While it is a bit\ncounter-intuitive to first hardcode the limit and then start lifting\nit, it does make the presentation really easy to understand.\n\nAnybody spotted problems in the series?  I couldn't find any but I\nadmit I did not look very hard.\n\nThanks.\n"},{"id":"540624","messageId":"bdff0a5d-b738-4053-9b72-08eba88156de@kdbg.org","threadId":"65267","inReplyTo":"20260328001113.1275291-1-pabloosabaterr@gmail.com","subject":"Re: [GSoC PATCH v6 0/3] graph: add --graph-lane-limit option","fromName":"Johannes Sixt","fromEmail":"j6t@kdbg.org","sentAt":"2026-04-01T08:36:18Z","receivedAt":"2026-04-01T08:36:28Z","isPatch":true,"body":"Am 28.03.26 um 01:11 schrieb Pablo Sabater:\n> Repositories that have many active branches at the same time produce\n> wide graphs. A lane consists of two columns, the edge and the space\n> padding, each branch takes a lane in the graph and there is no way\n> to limit how many can be shown.\n> \n> The limit is a horizontal truncation, each lane is cut at the lane limit:\n> \n>   Without --graph-lane-limit:\n> \n>   *   7_M1\n>   |\\  \n>   | * 7_E\n>   * | 7_C\n>   | | *   7_M2\n>   | | |\\  \n>   | | | * 7_H\n>   | | |/  \n>   | |/|   \n>   | * | 7_D\n>   | | * 7_G\n>   | | * 7_F\n>   | |/  \n>   |/|   \n>   * | 7_B\n>   |/  \n>   * 7_A\n> \n>   With --graph-lane-limit=1:\n> \n>   *   7_M1\n>   |\\  \n>   | * 7_E\n>   * ~ 7_C\n>   | ~ 7_M2\n>   | ~ 7_H\n>   | ~ \n>   | ~ \n>   | * 7_D\n>   | ~ 7_G\n>   | ~ 7_F\n>   | ~ \n>   |/~ \n>   * ~ 7_B\n>   |/  \n>   * 7_A\n\nAfter seeing this example, my first reaction was that this\n--graph-lane-limit option would not be useful for me. The relationship\namong the commits is apparently obfuscated to such a degree that the\ngraph is not a lot better than a plain listing without --graph.\n\nBut then I tried on a few real-world examples, and the result turned out\nto be a lot better. The commits (asterisks) typically occur in the\nleft-most lanes, and the lanes to the right are usually just connections\nwithout commits. This makes it more practical to just truncate the graph\npart, i.e., hide the connecting lanes.\n\nIn conclusion, I regard the way the option works as useful, even though\nit is not the way of truncation I had envisioned originally.\n\nI discovered a small glitch, though. If you download today's gitk\nrepository https://github.com/j6t/gitk.git, run\n\n  git log --graph --oneline --decorate --boundary \\\n     --graph-lane-limit=4 465f03869ae11acd0..origin/j6t-testing\n\n(j6t-testing is a volatile branch and is 86848fe40b60ae58f today).\nScroll down to line 166 and you see the '~' at the wrong place:\n\n| | * | ~ 9f0d1c2 gitk: sanitize 'exec' arguments: simple cases\n| | * | ~ 6eb797f gitk: have callers of diffcmd supply pipe symbol...\n| | * | ~ b966b73 gitk: treat file names beginning with \"|\" as...\n* | | | ~ 0c8be6f Merge branch 'ah/fix-open-with-stdin'\n|\\| | |~           <-- this is line 166\n| * | | ~ 8e3070a (...) gitk: encode arguments correctly with \"open\"\n* | | | ~ bfb0fa7 Merge branch 'top-panel-search-highlight' of ...\n|\\ \\ \\ \\~\n| * | | ~ 9cad4a9 gitk: do not hard-code color of search results...\n\nI haven't tried to find out what is going wrong here or to simplify the\nreproducer.\n\n-- Hannes\n\n"},{"id":"540647","messageId":"CAN5EUNR_yfkv_hC4wg-nHNg=3FnkYdvFm6FcOUNG2A=MdGs7ZQ@mail.gmail.com","threadId":"65267","inReplyTo":"bdff0a5d-b738-4053-9b72-08eba88156de@kdbg.org","subject":"Re: [GSoC PATCH v6 0/3] graph: add --graph-lane-limit option","fromName":"Pablo","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-04-01T14:42:26Z","receivedAt":"2026-04-01T14:42:39Z","isPatch":true,"body":"El mié, 1 abr 2026 a las 10:36, Johannes Sixt (<j6t@kdbg.org>) escribió:\n>\n> Am 28.03.26 um 01:11 schrieb Pablo Sabater:\n> > Repositories that have many active branches at the same time produce\n> > wide graphs. A lane consists of two columns, the edge and the space\n> > padding, each branch takes a lane in the graph and there is no way\n> > to limit how many can be shown.\n> >\n> > The limit is a horizontal truncation, each lane is cut at the lane limit:\n> >\n> >   Without --graph-lane-limit:\n> >\n> >   *   7_M1\n> >   |\\\n> >   | * 7_E\n> >   * | 7_C\n> >   | | *   7_M2\n> >   | | |\\\n> >   | | | * 7_H\n> >   | | |/\n> >   | |/|\n> >   | * | 7_D\n> >   | | * 7_G\n> >   | | * 7_F\n> >   | |/\n> >   |/|\n> >   * | 7_B\n> >   |/\n> >   * 7_A\n> >\n> >   With --graph-lane-limit=1:\n> >\n> >   *   7_M1\n> >   |\\\n> >   | * 7_E\n> >   * ~ 7_C\n> >   | ~ 7_M2\n> >   | ~ 7_H\n> >   | ~\n> >   | ~\n> >   | * 7_D\n> >   | ~ 7_G\n> >   | ~ 7_F\n> >   | ~\n> >   |/~\n> >   * ~ 7_B\n> >   |/\n> >   * 7_A\n>\n> After seeing this example, my first reaction was that this\n> --graph-lane-limit option would not be useful for me. The relationship\n> among the commits is apparently obfuscated to such a degree that the\n> graph is not a lot better than a plain listing without --graph.\n>\n> But then I tried on a few real-world examples, and the result turned out\n> to be a lot better. The commits (asterisks) typically occur in the\n> left-most lanes, and the lanes to the right are usually just connections\n> without commits. This makes it more practical to just truncate the graph\n> part, i.e., hide the connecting lanes.\n>\n> In conclusion, I regard the way the option works as useful, even though\n> it is not the way of truncation I had envisioned originally.\n\nThanks for testing it on real-world examples and I'm happy that it\nseems more useful than expected.\n\nWhile working on this I spent most of the time with graph.c and\nI got to understand well how the rendering engine works. I think\nI have an idea about how to tackle the column rearrangement\nlike gitk, which I believe is what you thought it was about at the\nstart (and the\nTODO that's been on graph.c for 16 years c12172d2ea).\n\nFWIW, I'd like to send an RFC about the column rearrangement\nbecause it would be better overall, no information is lost, you can\nstill limit the number of visible columns which would replace this\nin most cases (only scenario I can think of where you still\nwant to keep the truncation would be if you want to keep the\nbranches going straight vertically).\n\nI'd like to hold this series and send the RFC with the idea for the\nrearrangement. If it ends up not being viable I would come back\nhere and add a 4th patch to remove the extra padding lines\n(merge and collapsing lines truncated) to make it more useful\nmaking the graph more compact vertically as well.\n\nI'm sorry if this ends up not being merged and I've wasted your time.\n\n>\n> I discovered a small glitch, though. If you download today's gitk\n> repository https://github.com/j6t/gitk.git, run\n>\n>   git log --graph --oneline --decorate --boundary \\\n>      --graph-lane-limit=4 465f03869ae11acd0..origin/j6t-testing\n>\n> (j6t-testing is a volatile branch and is 86848fe40b60ae58f today).\n> Scroll down to line 166 and you see the '~' at the wrong place:\n>\n> | | * | ~ 9f0d1c2 gitk: sanitize 'exec' arguments: simple cases\n> | | * | ~ 6eb797f gitk: have callers of diffcmd supply pipe symbol...\n> | | * | ~ b966b73 gitk: treat file names beginning with \"|\" as...\n> * | | | ~ 0c8be6f Merge branch 'ah/fix-open-with-stdin'\n> |\\| | |~           <-- this is line 166\n> | * | | ~ 8e3070a (...) gitk: encode arguments correctly with \"open\"\n> * | | | ~ bfb0fa7 Merge branch 'top-panel-search-highlight' of ...\n> |\\ \\ \\ \\~\n> | * | | ~ 9cad4a9 gitk: do not hard-code color of search results...\n>\n> I haven't tried to find out what is going wrong here or to simplify the\n> reproducer.\n\nIIRC there's one place where the ~ padding comes from if it's in an uneven\ncolumn, I think it might be what's failing. Thanks.\n\n>\n> -- Hannes\n>\n\nI'm still new to Git and even though I've read the documentation\nI may have missed something, if something about what\nI'm saying to do (The RFC, holding this) is wrong please let me know.\n\nThanks for the feedback and the reviews,\nPablo.\n"},{"id":"540653","messageId":"xmqqikaawrpx.fsf@gitster.g","threadId":"65267","inReplyTo":"CAN5EUNR_yfkv_hC4wg-nHNg=3FnkYdvFm6FcOUNG2A=MdGs7ZQ@mail.gmail.com","subject":"Re: [GSoC PATCH v6 0/3] graph: add --graph-lane-limit option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-01T16:49:46Z","receivedAt":"2026-04-01T16:49:48Z","isPatch":true,"body":"Pablo <pabloosabaterr@gmail.com> writes:\n\n> While working on this I spent most of the time with graph.c and\n> I got to understand well how the rendering engine works. I think\n> I have an idea about how to tackle the column rearrangement\n> like gitk, which I believe is what you thought it was about at the\n> start (and the\n> TODO that's been on graph.c for 16 years c12172d2ea).\n>\n> FWIW, I'd like to send an RFC about the column rearrangement\n> because it would be better overall, no information is lost, you can\n> still limit the number of visible columns which would replace this\n> in most cases (only scenario I can think of where you still\n> want to keep the truncation would be if you want to keep the\n> branches going straight vertically).\n>\n> I'd like to hold this series and send the RFC with the idea for the\n> rearrangement. If it ends up not being viable I would come back\n> here and add a 4th patch to remove the extra padding lines\n> (merge and collapsing lines truncated) to make it more useful\n> making the graph more compact vertically as well.\n\nOh, I may have found a volunteer to fix one of my pet peeves ;-)\n\nImagine a history with multiple root commits and you are drawing the\nhistory near one of the roots.  Immediately fater pacing that root\ncommit, the graphing engine seems to say \"ah, the next display row\nimmediately below this commit '*' is vacant because it does not have\nany parent.  We can draw a commit right there\" and draws a commit\nthat is unrelated to that root commit it just has drawn.\n\nWhich of course makes it impossible to tell that the commit on the\nearlier row is a root, if we draw a commit immediately below it.\nWe'd want to leave that column/lane open for at least one row.\n\nInstead of \n\n    * a child of the root commit below\n    * one of the root commits\n    * an unrelated commit X\n    * the parent of X\n    * the other root commit that is a grandparent of X\n\nwe could probably draw\n\n    * a child of the root commit below\n    * one of the root commits\n      * an unrelated commit X\n     /\n    * the parent of X\n    * the other root commit that is a grandparent of X\n\nor you or somebody who stared at the graph engine much longer than I\nhave may have even better ideas to draw such a history.\n"},{"id":"540703","messageId":"CAN5EUNRvsUgZPQhk4vj-QY8k+iCkTHQsgO8RJj1gNkYBDChsZg@mail.gmail.com","threadId":"65267","inReplyTo":"xmqqikaawrpx.fsf@gitster.g","subject":"Re: [GSoC PATCH v6 0/3] graph: add --graph-lane-limit option","fromName":"Pablo","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-04-02T05:53:32Z","receivedAt":"2026-04-02T05:53:45Z","isPatch":true,"body":"El mié, 1 abr 2026 a las 18:49, Junio C Hamano (<gitster@pobox.com>) escribió:\n\n> Oh, I may have found a volunteer to fix one of my pet peeves ;-)\n>\n> Imagine a history with multiple root commits and you are drawing the\n> history near one of the roots.  Immediately fater pacing that root\n> commit, the graphing engine seems to say \"ah, the next display row\n> immediately below this commit '*' is vacant because it does not have\n> any parent.  We can draw a commit right there\" and draws a commit\n> that is unrelated to that root commit it just has drawn.\n>\n> Which of course makes it impossible to tell that the commit on the\n> earlier row is a root, if we draw a commit immediately below it.\n> We'd want to leave that column/lane open for at least one row.\n>\n> Instead of\n>\n>     * a child of the root commit below\n>     * one of the root commits\n>     * an unrelated commit X\n>     * the parent of X\n>     * the other root commit that is a grandparent of X\n>\n> we could probably draw\n>\n>     * a child of the root commit below\n>     * one of the root commits\n>       * an unrelated commit X\n>      /\n>     * the parent of X\n>     * the other root commit that is a grandparent of X\n>\n> or you or somebody who stared at the graph engine much longer than I\n> have may have even better ideas to draw such a history.\n\nOk, I like the idea and I think it should be relatively easy,\nsomething like if the last commit had no interesting parent to keep\nthe padding like if it is still there for at least a row, then\ncollapse the commit (unrelated X) back to the first column. So far\nI've got this:\n\n  * B\n  | * A2\n  * A1\n  * A\n\nOnce I've got something more close to what you said I'll send a RFC\nPATCH on a clean thread and CC you.\n\nAbout other ways to draw it, I actually like yours, other way I can\nthink of is to make a hard separation row something like\n\n>     * a child of the root commit below\n>     * one of the root commits\n       ---\n>     * an unrelated commit X\n>     * the parent of X\n>     * the other root commit that is a grandparent of X\n\nBut I think yours is more minimal, with this it would be a new type of\nrow to handle, etc. yours is to pretend that there is something, hide\nit and let it naturally collapse.\n"},{"id":"540791","messageId":"xmqqeckxp264.fsf@gitster.g","threadId":"65267","inReplyTo":"CAN5EUNRvsUgZPQhk4vj-QY8k+iCkTHQsgO8RJj1gNkYBDChsZg@mail.gmail.com","subject":"Re: [GSoC PATCH v6 0/3] graph: add --graph-lane-limit option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-02T19:55:47Z","receivedAt":"2026-04-02T19:55:49Z","isPatch":true,"body":"Pablo <pabloosabaterr@gmail.com> writes:\n\n> About other ways to draw it, I actually like yours, other way I can\n> think of is to make a hard separation row something like\n>\n>>     * a child of the root commit below\n>>     * one of the root commits\n>        ---\n>>     * an unrelated commit X\n>>     * the parent of X\n>>     * the other root commit that is a grandparent of X\n\nYes, this is rather an easy way out and its variants have been\nattempted over the years for a few times, I think.  Marking a root\ncommit differently from others, like the hard break line immediately\nbelow it, drawing it with something other than '*', or painting '*'\nin red---any of these approaches will let you tell that the commit\ndoes not have a parent-child relationship with the commit that\nappears on the next line.\n\nBut the reason why the user asks for \"--graph\" is because they want\nto see the parent-child relashionships in the graph layout itself by\nlaying commits out on the 2-D plane, and drawing the root so\ndifferently from others is failing that task.\n\n"},{"id":"540864","messageId":"dc134cdb-cdc3-4c54-a97e-993a26900d0d@gmail.com","threadId":"65267","inReplyTo":"xmqqikaawrpx.fsf@gitster.g","subject":"Re: [GSoC PATCH v6 0/3] graph: add --graph-lane-limit option","fromName":"Tian Yuchen","fromEmail":"a3205153416@gmail.com","sentAt":"2026-04-03T18:56:28Z","receivedAt":"2026-04-03T18:56:36Z","isPatch":true,"body":"On 4/2/26 00:49, Junio C Hamano wrote:\n> Pablo <pabloosabaterr@gmail.com> writes:\n> \n>> While working on this I spent most of the time with graph.c and\n>> I got to understand well how the rendering engine works. I think\n>> I have an idea about how to tackle the column rearrangement\n>> like gitk, which I believe is what you thought it was about at the\n>> start (and the\n>> TODO that's been on graph.c for 16 years c12172d2ea).\n>>\n>> FWIW, I'd like to send an RFC about the column rearrangement\n>> because it would be better overall, no information is lost, you can\n>> still limit the number of visible columns which would replace this\n>> in most cases (only scenario I can think of where you still\n>> want to keep the truncation would be if you want to keep the\n>> branches going straight vertically).\n>>\n>> I'd like to hold this series and send the RFC with the idea for the\n>> rearrangement. If it ends up not being viable I would come back\n>> here and add a 4th patch to remove the extra padding lines\n>> (merge and collapsing lines truncated) to make it more useful\n>> making the graph more compact vertically as well.\n> \n> Oh, I may have found a volunteer to fix one of my pet peeves ;-)\n> \n> Imagine a history with multiple root commits and you are drawing the\n> history near one of the roots.  Immediately fater pacing that root\n> commit, the graphing engine seems to say \"ah, the next display row\n> immediately below this commit '*' is vacant because it does not have\n> any parent.  We can draw a commit right there\" and draws a commit\n> that is unrelated to that root commit it just has drawn.\n> \n> Which of course makes it impossible to tell that the commit on the\n> earlier row is a root, if we draw a commit immediately below it.\n> We'd want to leave that column/lane open for at least one row.\n> \n> Instead of\n> \n>      * a child of the root commit below\n>      * one of the root commits\n>      * an unrelated commit X\n>      * the parent of X\n>      * the other root commit that is a grandparent of X\n> \n> we could probably draw\n> \n>      * a child of the root commit below\n>      * one of the root commits\n>        * an unrelated commit X\n>       /\n>      * the parent of X\n>      * the other root commit that is a grandparent of X\n> \n> or you or somebody who stared at the graph engine much longer than I\n> have may have even better ideas to draw such a history.\n\nI have some reservations about this idea, although I know little about \nthe graph engine. ;)\n\nIn terms of user conventions, in --graph, '/' and '\\' represent \nbranching and merging respectively. Here, however, the '/' used to break \nup between different root commits is merely a placeholder and has no \nspecific meaning. I feel that this not only goes against user intuition \nbut also creates visual confusion: (I just drew this off the cuff, but I \nreckon it should look something like this?)\n\n> * (main) commit 4\n> | * (feature) commit 4\n> | | * (doc1) commit\n> | | * (doc1) root\n> | |   * (main) commit 3\n> | |  /| \n> | * | (feature) commit 3\n> | |/\n> | * (doc2) root\n> |   * (main) commit 2 \n> |  /\n> * (feature) commit 2\n> | * (main) commit 1\n\nI don’t know about you, but I find this rather difficult to grasp. At \nthe very least, I can’t tell at a glance where the actual merges and \nbranches are. I don’t think a good solution should involve shifting left \nor right. Perhaps it would be better to use a special symbol in the root \ncommit (such as the ■ symbol, which resembles a full stop? People would \ninstinctively recognise it as a terminator):\n\n|* (main) commit 2\n| * (feature) commit 2\n| ■ (doc1) ROOT COMMIT\n* | (main) commit 1\n\n(I wonder if there might be any character compatibility issues. Anyway, \nI like square characters. Square things are all rather cute.)\n\nRegards, Yuchen\n"},{"id":"540865","messageId":"CAN5EUNQbdoymmJcqzqzUy2aEKg-qUBqe_7bSmWwboJ4PoCfFJQ@mail.gmail.com","threadId":"65267","inReplyTo":"dc134cdb-cdc3-4c54-a97e-993a26900d0d@gmail.com","subject":"Re: [GSoC PATCH v6 0/3] graph: add --graph-lane-limit option","fromName":"Pablo","fromEmail":"pabloosabaterr@gmail.com","sentAt":"2026-04-03T19:13:45Z","receivedAt":"2026-04-03T19:13:59Z","isPatch":true,"body":"El vie, 3 abr 2026 a las 20:56, Tian Yuchen (<a3205153416@gmail.com>) escribió:\n>\n>\n> I have some reservations about this idea, although I know little about\n> the graph engine. ;)\n>\n> In terms of user conventions, in --graph, '/' and '\\' represent\n> branching and merging respectively. Here, however, the '/' used to break\n> up between different root commits is merely a placeholder and has no\n> specific meaning. I feel that this not only goes against user intuition\n> but also creates visual confusion: (I just drew this off the cuff, but I\n> reckon it should look something like this?)\n>\n> > * (main) commit 4\n> > | * (feature) commit 4\n> > | | * (doc1) commit\n> > | | * (doc1) root\n> > | |   * (main) commit 3\n> > | |  /|\n> > | * | (feature) commit 3\n> > | |/\n> > | * (doc2) root\n> > |   * (main) commit 2\n> > |  /\n> > * (feature) commit 2\n> > | * (main) commit 1\n>\n> I don’t know about you, but I find this rather difficult to grasp. At\n> the very least, I can’t tell at a glance where the actual merges and\n> branches are. I don’t think a good solution should involve shifting left\n> or right. Perhaps it would be better to use a special symbol in the root\n> commit (such as the ■ symbol, which resembles a full stop? People would\n> instinctively recognise it as a terminator):\n>\n> |* (main) commit 2\n> | * (feature) commit 2\n> | ■ (doc1) ROOT COMMIT\n> * | (main) commit 1\n>\n> (I wonder if there might be any character compatibility issues. Anyway,\n> I like square characters. Square things are all rather cute.)\n>\n> Regards, Yuchen\n\nHi Yuchen,\neven tho we have diverged a little, this thread is for a new option to\nlimit the graph horizontally, see the cover letter.\nIt's currently being held, waiting for a RFC about column\nrearrangements. But I'm attempting the root commits issue before.\n\nBut what Junio mentioned is actually interesting and it's being\ndiscussed currently in a new thread, to avoid having two different\ntopics here.\nat :\n  https://lore.kernel.org/git/20260402211717.3604688-1-pabloosabaterr@gmail.com/\n\nThis seems to have been attempted multiple times and the idea to have\na new symbol was thought of already.\nat:\n  https://lore.kernel.org/git/191201d6eaa3$4b585fa0$e2091ee0$@pdinc.us/\nfrom the same thread, but remarkable (part of the review from junio in\nthe new thread):\n  https://lore.kernel.org/git/xmqqwnwajbuj.fsf@gitster.c.googlers.com/\n\nI already have some feedback and I'll send a v2 later today or maybe\ntomorrow, BUT testing and thoughts about it are very welcome, so feel\nfree.\nPablo.\n"},{"id":"540866","messageId":"xmqqbjfzn6ku.fsf@gitster.g","threadId":"65267","inReplyTo":"dc134cdb-cdc3-4c54-a97e-993a26900d0d@gmail.com","subject":"Re: [GSoC PATCH v6 0/3] graph: add --graph-lane-limit option","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-04-03T20:15:45Z","receivedAt":"2026-04-03T20:15:47Z","isPatch":true,"body":"Tian Yuchen <a3205153416@gmail.com> writes:\n\n> I don’t know about you, but I find this rather difficult to grasp. At \n> the very least, I can’t tell at a glance where the actual merges and \n> branches are. I don’t think a good solution should involve shifting left \n> or right. Perhaps it would be better to use a special symbol in the root \n> commit (such as the ■ symbol, which resembles a full stop? People would \n> instinctively recognise it as a terminator):\n\nNot only just a symbol for \"root\", but you'd need to a set of\nalternative \"root\" symbols, so that you can also show roots that\nplay special roles in --left-right and --boundary output.\n\nDifferent charactrers were ruled out years ago and had to be shot\ndown at least twice in the past mostly due to this problem.\n\nAlso there will be another question, should the bottom end of a\nrange and a root commit be shown the same way or differently?\n"}]}