Subject: [GSoC RFC PATCH v2] graph: add --max-columns option to limit displayed columns

From: Pablo Sabater <[email-masked]>
Date: Tue, 17 Mar 2026 23:09:29 +0100

Repositories that have many active branches produce very wide
outputs with 'git log --graph --all', makes it difficult to read.

Add '--max-columns=<n>' to limit the columns shown. Columns over
the limit are replaced with a '.' truncation indicator. This only
affects the visual rendering.

The commit mark '*' is only shown in two cases:
- The commit is in a branch inside the limit.
- The commit is in the first hidden branch, in this case '.'
  is replaced by '*'.

Commits on deeper hidden branches do not show '*' but the commit
subject is still shown, so no information is lost.

The original idea to limit columns was noted as a TODO in
c12172d2ea (Add history graph API, 2008-05-04), which mentions
gitk's behavior. This does not implement gitk-style column
rearrangement; it only truncates the visual output.

Signed-off-by: Pablo Sabater <[email-masked]>
---

> I'll wait for the decision about whether this TODO from 2008 is worth
> doing before a v2.

I believe that it is better to send an improved version instead of waiting
to show more clearly what I want to do, sorry for the confusion.

> It's been 16 years but I believe that is still a good thing to add, even if
> a lot of graph viewing happens with third parties, adding this makes
> Git a bit more self sufficient. This is even more useful in a case where the
> third party is not an option.

> That said, if this still doesn't seem useful I would remove the TODO
> to avoid further confusion in the future.

I revert what I've said here. This doesn't actually reflects gitk behaviour, 
it only truncates the visual output and prettifies it. To actually fullfill the
TODO, it should rearrange the columns what would mean to change the whole
rendering algorithm of --graph. This prob why it has been a TODO for so long.

I don't think the TODO should be removed with what I bring here. Therefore, I
wont't remove it.

The actual TODO seems still a good project and a good improvement.

example:

git log --oneline --graph --all

*   b81d099 (M_1) 7_M1
|\
| | *   2d96ba0 (M_3) 7_M2
| | |\
| | | * 993fbcd (7_4) 7_H
| | | | *   d5b56db (M_5) 7_M3
| | | | |\
| | | | | * b45d68d (7_6) 7_J
| | | | * | 3f9ab79 (7_5) 7_I
| | | | | | *   f89bbdc (HEAD -> M_7) 7_M4
| |_|_|_|_|/|\
|/| | | | |/ /
| | |_|_|/| /
| |/| | | |/
| | | |_|/|
| | |/| | |
| | * | | | f5cbb18 (7_3) 7_G
| | | |_|/
| | |/| |
| | * | | 6ee728a 7_F
| * | | | d0c5a67 (7_2) 7_E
| | |/ /
| |/| |
| * | | c3bffce 7_D
| | |/
| |/|
* | | df1a24f (7_1) 7_C
| |/
|/|
* | 5d3f777 7_B
|/
* d309d64 7_A

vs 

git log --graph --all --oneline --max-columns=2

*   b81d099 (M_1) 7_M1
|\
| | * 2d96ba0 (M_3) 7_M2
| | .
| | . 993fbcd (7_4) 7_H
| | . d5b56db (M_5) 7_M3
| | .
| | . b45d68d (7_6) 7_J
| | . 3f9ab79 (7_5) 7_I
| | . f89bbdc (HEAD -> M_7) 7_M4
| |_.
|/| .
| | .
| |/.
| |/.
| |/.
| | .
| | * f5cbb18 (7_3) 7_G
| | .
| | .
| | .
| | * 6ee728a 7_F
| * . d0c5a67 (7_2) 7_E
| | .
| |/.
| * . c3bffce 7_D
| | .
| |/.
* | . df1a24f (7_1) 7_C
| |/
|/|
* | 5d3f777 7_B
|/
* d309d64 7_A

Changes since v1:
- Renamed option from --graph-max to --max-columns
- Require --graph for --max-columns, die without it
- Fixed padding so commit text aligns correctly after truncation
- Added pre-commit truncation so lines before the commit column
  are also truncated when they exceed the limit
- Fixed post-merge line spacing inconsistency
- Used parse_long_opt/parse_count for input validation, matching
  existing revision.c patterns
- Reject negative and zero values with clear error messages
- Renamed graph_is_truncated to graph_needs_truncation

RFC :
- I think --max-columns is a good name, but it does not have --graph
  prefix, bcs it can only be on --graph I think it's clear enough.
  I added checks in setup_revisions().
- Is '.' fine as delimiter ? other options could be "~" or "-".

 graph.c                      | 57 +++++++++++++++++++++++++++++++++++-
 graph.h                      |  2 ++
 revision.c                   | 11 +++++++
 revision.h                   |  1 +
 t/t4215-log-skewed-merges.sh | 56 +++++++++++++++++++++++++++++++++++
 5 files changed, 126 insertions(+), 1 deletion(-)

diff --git a/graph.c b/graph.c
index 26f6fbf000..6227c4f22f 100644
--- a/graph.c
+++ b/graph.c
@@ -317,6 +317,12 @@ struct git_graph {
 	struct strbuf prefix_buf;
 };
 
+static int graph_needs_truncation(struct git_graph *graph, int col)
+{
+	int max = graph->revs->graph_max_columns;
+	return max > 0 && col >= max;
+}
+
 static const char *diff_output_prefix_callback(struct diff_options *opt, void *data)
 {
 	struct git_graph *graph = data;
@@ -696,6 +702,15 @@ static void graph_update_columns(struct git_graph *graph)
 		}
 	}
 
+	/*
+	 * If graph_max_columns is set, cap the padding from the branches
+	 */
+	if (graph->revs->graph_max_columns > 0) {
+		int truncation = graph->revs->graph_max_columns * 2 + 2;
+		if (graph->width > truncation)
+			graph->width = truncation;
+	}
+
 	/*
 	 * Shrink mapping_size to be the minimum necessary
 	 */
@@ -846,6 +861,10 @@ static void graph_output_padding_line(struct git_graph *graph,
 	 * Output a padding row, that leaves all branch lines unchanged
 	 */
 	for (i = 0; i < graph->num_new_columns; i++) {
+		if (graph_needs_truncation(graph, i)) {
+			graph_line_addstr(line, ". ");
+			break;
+		}
 		graph_line_write_column(line, &graph->new_columns[i], '|');
 		graph_line_addch(line, ' ');
 	}
@@ -903,6 +922,9 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 			seen_this = 1;
 			graph_line_write_column(line, col, '|');
 			graph_line_addchars(line, ' ', graph->expansion_row);
+		} else if (seen_this && graph_needs_truncation(graph, i)) {
+			graph_line_addstr(line, ". ");
+			break;
 		} else if (seen_this && (graph->expansion_row == 0)) {
 			/*
 			 * This is the first line of the pre-commit output.
@@ -1013,6 +1035,7 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line
 	 * children that we have already processed.)
 	 */
 	seen_this = 0;
+
 	for (i = 0; i <= graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		struct commit *col_commit;
@@ -1028,8 +1051,17 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line
 			seen_this = 1;
 			graph_output_commit_char(graph, line);
 
+			if (graph_needs_truncation(graph, i)) {
+				graph_line_addch(line, ' ');
+				break;
+			}
+
 			if (graph->num_parents > 2)
 				graph_draw_octopus_merge(graph, line);
+		} else if (graph_needs_truncation(graph, i)) {
+			graph_line_addstr(line, ". ");
+			seen_this = 1;
+			break;
 		} else if (seen_this && (graph->edges_added > 1)) {
 			graph_line_write_column(line, col, '\\');
 		} else if (seen_this && (graph->edges_added == 1)) {
@@ -1109,9 +1141,17 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l
 			int par_column;
 			int idx = graph->merge_layout;
 			char c;
+			int truncated = 0;
 			seen_this = 1;
 
 			for (j = 0; j < graph->num_parents; j++) {
+				if (graph_needs_truncation(graph, i + j)) {
+					if (j > 0)
+						graph_line_addch(line, ' ');
+					graph_line_addstr(line, ". ");
+					truncated = 1;
+					break;
+				}
 				par_column = graph_find_new_column_by_commit(graph, parents->item);
 				assert(par_column >= 0);
 
@@ -1125,9 +1165,13 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l
 				}
 				parents = next_interesting_parent(graph, parents);
 			}
+			if (truncated)
+				break;
 			if (graph->edges_added == 0)
 				graph_line_addch(line, ' ');
-
+		} else if (graph_needs_truncation(graph, i)) {
+			graph_line_addstr(line, ". ");
+			break;
 		} else if (seen_this) {
 			if (graph->edges_added > 0)
 				graph_line_write_column(line, col, '\\');
@@ -1279,6 +1323,12 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
 	 */
 	for (i = 0; i < graph->mapping_size; i++) {
 		int target = graph->mapping[i];
+
+		if (graph_needs_truncation(graph, i / 2)) {
+			graph_line_addstr(line, ". ");
+			break;
+		}
+
 		if (target < 0)
 			graph_line_addch(line, ' ');
 		else if (target * 2 == i)
@@ -1372,6 +1422,11 @@ static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)
 	for (i = 0; i < graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 
+		if (graph_needs_truncation(graph, i)) {
+			graph_line_addch(&line, '.');
+			break;
+		}
+
 		graph_line_write_column(&line, col, '|');
 
 		if (col->commit == graph->commit && graph->num_parents > 2) {
diff --git a/graph.h b/graph.h
index 3fd1dcb2e9..9a4551dd29 100644
--- a/graph.h
+++ b/graph.h
@@ -262,4 +262,6 @@ void graph_show_commit_msg(struct git_graph *graph,
 			   FILE *file,
 			   struct strbuf const *sb);
 
+#define MINIMUM_GRAPH_COLUMNS 1
+
 #endif /* GRAPH_H */
diff --git a/revision.c b/revision.c
index 31808e3df0..e8d38cb2a1 100644
--- a/revision.c
+++ b/revision.c
@@ -2605,6 +2605,13 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
 	} else if (!strcmp(arg, "--no-graph")) {
 		graph_clear(revs->graph);
 		revs->graph = NULL;
+	} else if ((argcount = parse_long_opt("max-columns", argv, &optarg))) {
+		int val = parse_count(optarg);
+		if (val < MINIMUM_GRAPH_COLUMNS)
+			die(_("minimum columns is %d, cannot be set to %d"),
+			    MINIMUM_GRAPH_COLUMNS, val);
+		revs->graph_max_columns = val;
+		return argcount;
 	} else if (!strcmp(arg, "--encode-email-headers")) {
 		revs->encode_email_headers = 1;
 	} else if (!strcmp(arg, "--no-encode-email-headers")) {
@@ -3172,6 +3179,10 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s
 
 	if (revs->no_walk && revs->graph)
 		die(_("options '%s' and '%s' cannot be used together"), "--no-walk", "--graph");
+
+	if (revs->graph_max_columns > 0 && !revs->graph)
+		die(_("option '%s' requires '%s'"), "--max-columns", "--graph");
+
 	if (!revs->reflog_info && revs->grep_filter.use_reflog_filter)
 		die(_("the option '%s' requires '%s'"), "--grep-reflog", "--walk-reflogs");
 
diff --git a/revision.h b/revision.h
index 69242ecb18..f15b390289 100644
--- a/revision.h
+++ b/revision.h
@@ -304,6 +304,7 @@ struct rev_info {
 
 	/* Display history graph */
 	struct git_graph *graph;
+	unsigned int graph_max_columns;
 
 	/* special limits */
 	int skip_count;
diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
index 28d0779a8c..ca2b224cc4 100755
--- a/t/t4215-log-skewed-merges.sh
+++ b/t/t4215-log-skewed-merges.sh
@@ -370,4 +370,60 @@ test_expect_success 'log --graph with multiple tips' '
 	EOF
 '
 
+test_expect_success 'log --graph --max-columns=2 only two columns' '
+	check_graph --max-columns=2 M_7 <<-\EOF
+	*-.   7_M4
+	|\  .
+	| | * 7_G
+	| | * 7_F
+	| * . 7_E
+	| * . 7_D
+	* | . 7_C
+	| |/
+	|/|
+	* | 7_B
+	|/
+	* 7_A
+	EOF
+'
+
+test_expect_success 'log --graph --max-columns=3 only three columns' '
+	check_graph --max-columns=3 M_1 M_3 M_5 M_7 <<-\EOF
+	*   7_M1
+	|\
+	| | *   7_M2
+	| | | .
+	| | | * 7_H
+	| | | . 7_M3
+	| | | .
+	| | | . 7_J
+	| | | . 7_I
+	| | | . 7_M4
+	| |_|_.
+	|/| | .
+	| | |_.
+	| |/|_.
+	| |/|_.
+	| |/| .
+	| | |/.
+	| | * . 7_G
+	| | | .
+	| | |/.
+	| | |/.
+	| | * . 7_F
+	| * | . 7_E
+	| | |/.
+	| |/| .
+	| * | . 7_D
+	| | |/
+	| |/|
+	* | | 7_C
+	| |/
+	|/|
+	* | 7_B
+	|/
+	* 7_A
+	EOF
+'
+
 test_done
-- 
2.43.0