graph: add --graph-lane-limit option

Replace the hard-coded lane limit with a user-facing option '--graph-lane-limit=<n>'. It caps the number of visible lanes to n. This option requires '--graph', without it, limiting the graph has no meaning, in this case error out. Zero and negative values are valid inputs but silently ignored treating them as "no limit", the same as not using the option. This follows what '--max-parents' does with negative values. The default is 0, same as not being used. Signed-off-by: Pablo Sabater <pabloosabaterr@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Pablo Sabater committed Mar 28, 2026 at 01:11 UTC f756a3c78d4d88af1701996394650e6df6f66170
5 files changed +186 -23
Documentation/rev-list-options.adoc
+5
@@ -1259,6 +1259,11 @@ This implies the `--topo-order` option by default, but the
1259 in between them in that case. If _<barrier>_ is specified, it
1260 is the string that will be shown instead of the default one.
1261
1262 +`--graph-lane-limit=<n>`::
1263 + When `--graph` is used, limit the number of graph lanes to be shown.
1264 + Lanes over the limit are not shown. By default it is set to 0
1265 + (no limit), zero and negative values are ignored and treated as no limit.
1266 +
1267 ifdef::git-rev-list[]
1268 `--count`::
1269 Print a number stating how many commits would have been
graph.c
+30 -23
@@ -82,8 +82,6 @@ static void graph_show_line_prefix(const struct diff_options *diffopt)
82 static const char **column_colors;
83 static unsigned short column_colors_max;
84
85 -static unsigned int max_lanes = 15;
86 -
85 static void parse_graph_colors_config(struct strvec *colors, const char *string)
86 {
87 const char *end, *start;
@@ -319,9 +317,13 @@ struct git_graph {
317 struct strbuf prefix_buf;
318 };
319
322 -static inline int graph_needs_truncation(int lane)
320 +static inline int graph_needs_truncation(struct git_graph *graph, int lane)
321 {
324 - return lane >= max_lanes;
322 + int max = graph->revs->graph_max_lanes;
323 + /*
324 + * Ignore values <= 0, meaning no limit.
325 + */
326 + return max > 0 && lane >= max;
327 }
328
329 static const char *diff_output_prefix_callback(struct diff_options *opt, void *data)
@@ -614,7 +616,7 @@ static void graph_update_columns(struct git_graph *graph)
616 {
617 struct commit_list *parent;
618 int max_new_columns;
617 - int i, seen_this, is_commit_in_columns, max;
619 + int i, seen_this, is_commit_in_columns;
620
621 /*
622 * Swap graph->columns with graph->new_columns
@@ -704,12 +706,17 @@ static void graph_update_columns(struct git_graph *graph)
706 }
707
708 /*
707 - * Cap to the hard-coded limit.
708 - * Allow commits from merges to align to the merged lane.
709 + * If graph_max_lanes is set, cap the width
710 */
710 - max = max_lanes * 2 + 2;
711 - if (graph->width > max)
712 - graph->width = max;
711 + if (graph->revs->graph_max_lanes > 0) {
712 + /*
713 + * Width is column index while a lane is half that.
714 + * Allow commits from merges to align to the merged lane.
715 + */
716 + int max_width = graph->revs->graph_max_lanes * 2 + 2;
717 + if (graph->width > max_width)
718 + graph->width = max_width;
719 + }
720
721 /*
722 * Shrink mapping_size to be the minimum necessary
@@ -861,7 +868,7 @@ static void graph_output_padding_line(struct git_graph *graph,
868 * Output a padding row, that leaves all branch lines unchanged
869 */
870 for (i = 0; i < graph->num_new_columns; i++) {
864 - if (graph_needs_truncation(i))
871 + if (graph_needs_truncation(graph, i))
872 break;
873 graph_line_write_column(line, &graph->new_columns[i], '|');
874 graph_line_addch(line, ' ');
@@ -920,7 +927,7 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
927 seen_this = 1;
928 graph_line_write_column(line, col, '|');
929 graph_line_addchars(line, ' ', graph->expansion_row);
923 - } else if (seen_this && graph_needs_truncation(i)) {
930 + } else if (seen_this && graph_needs_truncation(graph, i)) {
931 break;
932 } else if (seen_this && (graph->expansion_row == 0)) {
933 /*
@@ -1018,7 +1025,7 @@ static void graph_draw_octopus_merge(struct git_graph *graph, struct graph_line
1025 * Commit is at commit_index, each iteration move one lane to
1026 * the right from the commit.
1027 */
1021 - if (graph_needs_truncation(graph->commit_index + 1 + i))
1028 + if (graph_needs_truncation(graph, graph->commit_index + 1 + i))
1029 break;
1030
1031 graph_line_write_column(line, col, (i == dashed_parents - 1) ? '.' : '-');
@@ -1055,14 +1062,14 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line
1062 seen_this = 1;
1063 graph_output_commit_char(graph, line);
1064
1058 - if (graph_needs_truncation(i)) {
1065 + if (graph_needs_truncation(graph, i)) {
1066 graph_line_addch(line, ' ');
1067 break;
1068 }
1069
1070 if (graph->num_parents > 2)
1071 graph_draw_octopus_merge(graph, line);
1065 - } else if (graph_needs_truncation(i)) {
1072 + } else if (graph_needs_truncation(graph, i)) {
1073 seen_this = 1;
1074 break;
1075 } else if (seen_this && (graph->edges_added > 1)) {
@@ -1112,7 +1119,7 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line
1119 * padding lane.
1120 */
1121 if (graph->num_parents > 1) {
1115 - if (!graph_needs_truncation(graph->commit_index)) {
1122 + if (!graph_needs_truncation(graph, graph->commit_index)) {
1123 graph_update_state(graph, GRAPH_POST_MERGE);
1124 } else {
1125 struct commit_list *p = first_interesting_parent(graph);
@@ -1128,7 +1135,7 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line
1135
1136 lane = graph_find_new_column_by_commit(graph, p->item);
1137
1131 - if (!graph_needs_truncation(lane))
1138 + if (!graph_needs_truncation(graph, lane))
1139 graph_update_state(graph, GRAPH_POST_MERGE);
1140 else if (graph_is_mapping_correct(graph))
1141 graph_update_state(graph, GRAPH_PADDING);
@@ -1192,7 +1199,7 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l
1199 * comparable with i. Don't truncate if there are
1200 * no more lanes to print (end of the lane)
1201 */
1195 - if (graph_needs_truncation(j / 2 + i) &&
1202 + if (graph_needs_truncation(graph, j / 2 + i) &&
1203 j / 2 + i <= graph->num_columns) {
1204 if ((j + i * 2) % 2 != 0)
1205 graph_line_addch(line, ' ');
@@ -1205,7 +1212,7 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l
1212 * Check if the next lane needs truncation
1213 * to avoid having the padding doubled
1214 */
1208 - if (graph_needs_truncation((j + 1) / 2 + i) &&
1215 + if (graph_needs_truncation(graph, (j + 1) / 2 + i) &&
1216 j < graph->num_parents - 1) {
1217 truncated = 1;
1218 break;
@@ -1220,7 +1227,7 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l
1227 break;
1228 if (graph->edges_added == 0)
1229 graph_line_addch(line, ' ');
1223 - } else if (graph_needs_truncation(i)) {
1230 + } else if (graph_needs_truncation(graph, i)) {
1231 break;
1232 } else if (seen_this) {
1233 if (graph->edges_added > 0)
@@ -1231,7 +1238,7 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l
1238 * If it's between two lanes and next would be truncated,
1239 * don't add space padding.
1240 */
1234 - if (!graph_needs_truncation(i + 1))
1241 + if (!graph_needs_truncation(graph, i + 1))
1242 graph_line_addch(line, ' ');
1243 } else {
1244 graph_line_write_column(line, col, '|');
@@ -1380,7 +1387,7 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
1387 for (i = 0; i < graph->mapping_size; i++) {
1388 int target = graph->mapping[i];
1389
1383 - if (!truncated && graph_needs_truncation(i / 2)) {
1390 + if (!truncated && graph_needs_truncation(graph, i / 2)) {
1391 truncated = 1;
1392 }
1393
@@ -1480,7 +1487,7 @@ static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)
1487 for (i = 0; i < graph->num_columns; i++) {
1488 struct column *col = &graph->columns[i];
1489
1483 - if (graph_needs_truncation(i))
1490 + if (graph_needs_truncation(graph, i))
1491 break;
1492
1493 graph_line_write_column(&line, col, '|');
revision.c
+6
@@ -2605,6 +2605,8 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
2605 } else if (!strcmp(arg, "--no-graph")) {
2606 graph_clear(revs->graph);
2607 revs->graph = NULL;
2608 + } else if (skip_prefix(arg, "--graph-lane-limit=", &optarg)) {
2609 + revs->graph_max_lanes = parse_count(optarg);
2610 } else if (!strcmp(arg, "--encode-email-headers")) {
2611 revs->encode_email_headers = 1;
2612 } else if (!strcmp(arg, "--no-encode-email-headers")) {
@@ -3172,6 +3174,10 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s
3174
3175 if (revs->no_walk && revs->graph)
3176 die(_("options '%s' and '%s' cannot be used together"), "--no-walk", "--graph");
3177 +
3178 + if (revs->graph_max_lanes > 0 && !revs->graph)
3179 + die(_("the option '%s' requires '%s'"), "--graph-lane-limit", "--graph");
3180 +
3181 if (!revs->reflog_info && revs->grep_filter.use_reflog_filter)
3182 die(_("the option '%s' requires '%s'"), "--grep-reflog", "--walk-reflogs");
3183
revision.h
+1
@@ -304,6 +304,7 @@ struct rev_info {
304
305 /* Display history graph */
306 struct git_graph *graph;
307 + int graph_max_lanes;
308
309 /* special limits */
310 int skip_count;
t/t4215-log-skewed-merges.sh
+144
@@ -370,4 +370,148 @@ test_expect_success 'log --graph with multiple tips' '
370 EOF
371 '
372
373 +test_expect_success 'log --graph --graph-lane-limit=2 limited to two lanes' '
374 + check_graph --graph-lane-limit=2 M_7 <<-\EOF
375 + *-. 7_M4
376 + |\ \
377 + | | * 7_G
378 + | | * 7_F
379 + | * 7_E
380 + | * 7_D
381 + * | 7_C
382 + | |/
383 + |/|
384 + * | 7_B
385 + |/
386 + * 7_A
387 + EOF
388 +'
389 +
390 +test_expect_success 'log --graph --graph-lane-limit=1 truncate mid octopus merge' '
391 + check_graph --graph-lane-limit=1 M_7 <<-\EOF
392 + *- 7_M4
393 + |\
394 + | 7_G
395 + | 7_F
396 + | * 7_E
397 + | * 7_D
398 + * 7_C
399 + |
400 + |/
401 + * 7_B
402 + |/
403 + * 7_A
404 + EOF
405 +'
406 +
407 +test_expect_success 'log --graph --graph-lane-limit=3 limited to three lanes' '
408 + check_graph --graph-lane-limit=3 M_1 M_3 M_5 M_7 <<-\EOF
409 + * 7_M1
410 + |\
411 + | | * 7_M2
412 + | | |\
413 + | | | * 7_H
414 + | | | 7_M3
415 + | | | 7_J
416 + | | | 7_I
417 + | | | 7_M4
418 + | |_|_
419 + |/| |
420 + | | |_
421 + | |/|
422 + | | |
423 + | | |/
424 + | | * 7_G
425 + | | |
426 + | | |/
427 + | | * 7_F
428 + | * | 7_E
429 + | | |/
430 + | |/|
431 + | * | 7_D
432 + | | |/
433 + | |/|
434 + * | | 7_C
435 + | |/
436 + |/|
437 + * | 7_B
438 + |/
439 + * 7_A
440 + EOF
441 +'
442 +
443 +test_expect_success 'log --graph --graph-lane-limit=6 check if it only shows first of 3 parent merge' '
444 + check_graph --graph-lane-limit=6 M_1 M_3 M_5 M_7 <<-\EOF
445 + * 7_M1
446 + |\
447 + | | * 7_M2
448 + | | |\
449 + | | | * 7_H
450 + | | | | * 7_M3
451 + | | | | |\
452 + | | | | | * 7_J
453 + | | | | * | 7_I
454 + | | | | | | * 7_M4
455 + | |_|_|_|_|/
456 + |/| | | | |/
457 + | | |_|_|/|
458 + | |/| | | |/
459 + | | | |_|/|
460 + | | |/| | |
461 + | | * | | | 7_G
462 + | | | |_|/
463 + | | |/| |
464 + | | * | | 7_F
465 + | * | | | 7_E
466 + | | |/ /
467 + | |/| |
468 + | * | | 7_D
469 + | | |/
470 + | |/|
471 + * | | 7_C
472 + | |/
473 + |/|
474 + * | 7_B
475 + |/
476 + * 7_A
477 + EOF
478 +'
479 +
480 +test_expect_success 'log --graph --graph-lane-limit=7 check if it shows all 3 parent merge' '
481 + check_graph --graph-lane-limit=7 M_1 M_3 M_5 M_7 <<-\EOF
482 + * 7_M1
483 + |\
484 + | | * 7_M2
485 + | | |\
486 + | | | * 7_H
487 + | | | | * 7_M3
488 + | | | | |\
489 + | | | | | * 7_J
490 + | | | | * | 7_I
491 + | | | | | | * 7_M4
492 + | |_|_|_|_|/|\
493 + |/| | | | |/ /
494 + | | |_|_|/| /
495 + | |/| | | |/
496 + | | | |_|/|
497 + | | |/| | |
498 + | | * | | | 7_G
499 + | | | |_|/
500 + | | |/| |
501 + | | * | | 7_F
502 + | * | | | 7_E
503 + | | |/ /
504 + | |/| |
505 + | * | | 7_D
506 + | | |/
507 + | |/|
508 + * | | 7_C
509 + | |/
510 + |/|
511 + * | 7_B
512 + |/
513 + * 7_A
514 + EOF
515 +'
516 +
517 test_done