Skip to contentSkip to conversation

Improve the readability of log --graph output

PATCH Superseded James Coglan via GitGitGadget 8 participants 83 messages Oct 10, 2019 – Nov 13, 2019

lore
Share & tools
lore
#0 lore raw

cover Improve the readability of log --graph output

This series of patches are designed to improve the output of the log --graph command; their effect can be summed up in the following diagram:

    Before                    After
    ------                    -----

    *
    |\
    | *                       *
    | |\                      |\
    | | *                     | *
    | | |                     | |\
    | |  \                    | | *
    | *-. \                   | * |
    | |\ \ \                  |/|\|
    |/ / / /                  | | *
    | | | /                   | * |
    | | |/                    | |/
    | | *                     * /
    | * |                     |/
    | |/                      *
    * |
    |/
    *

These changes aim to make the edges in graph diagrams easier to read, by straightening lines and making certain kinds of topologies display more compactly. Three distinct changes are included.

First, if the first parent of a merge fuses with an edge to the left of the commit, then we display that by making the edges fuse immediately rather than by drawing a line straight down and then having it track to the left. That is, where we currently display these graphs:

    | *             | | | *
    | |\            | | | |\
    |/ /            | |_|/ /
    | |             |/| | |

We will now display these merges as follows:

    | *             | | | *
    |/|             | |_|/|
    | |             |/| | |

This transformation is applied to merges with any number of parents, for example we currently display 3-parent merges like this:

    | *-.           | | | *-.
    | |\ \          | | | |\ \
    |/ / /          | |_|/ / /
    | | |           |/| | | |

And we will now display them like this:

    | *             | | | *
    |/|\            | |_|/|\
    | | |           |/| | | |

If the edge the first parent fuses with is separated from the commit by multiple columns, a horizontal edge is drawn just as we currently do in the 'collapsing' state. This change also affects the display of commit and post-merge lines in subtle ways that are more thoroughly described in the relevant commits.

The second change is that if the final parent of a merge fuses with the edge to the right of the commit, then we can remove the zig-zag effect that currently results. We currently display these merges like this:

    * |
    |\ \
    | |/
    | *

After these changes, this merge will now be displayed like so:

    * |
    |\|
    | *

If the final parent fuses with an edge that's further to the right, its display is unchanged and it will still display like this:

    * | | |
    |\ \ \ \
    | | |_|/
    | |/| |
    | * | |

The final structural change smooths out lines that are collapsing through commit lines. For example, consider the following history:

    *-. \
    |\ \ \
    | | * |
    | * | |
    | |/ /
    * | |
    |/ /
    * |
    |/
    *

This is now rendered so that commit lines display an edge using / instead of |, if that edge is tracking to the left both above and below the commit line. That results in this improved display:

    *-. \
    |\ \ \
    | | * |
    | * | |
    | |/ /
    * / /
    |/ /
    * /
    |/
    *

Taken together, these changes produce the change shown in the first diagram above, with the current rendering on the left and the new rendering on the right.

A final addition to that set of changes fixes the coloring of dashes that are drawn next to octopus merges, in a manner compatible with all these changes. The early commits in this set are refactorings that make the functional changes easier to introduce.

James Coglan (11):
graph: automatically track visible width of strbuf
graph: reuse find_new_column_by_commit()
graph: reduce duplication in graph_insert_into_new_columns()
graph: remove mapping_idx and graph_update_width()
graph: extract logic for moving to GRAPH_PRE_COMMIT state
graph: tidy up display of left-skewed merges
graph: commit and post-merge lines for left-skewed merges
graph: rename new_mapping to old_mapping
graph: smooth appearance of collapsing edges on commit lines
graph: flatten edges that join to their right neighbor
graph: fix coloring of octopus dashes

 graph.c                                    | 499 ++++++++++++---------
 strbuf.h                                   |   8 +-
 t/t3430-rebase-merges.sh                   |   2 +-
 t/t4202-log.sh                             |   2 +-
 t/t4214-log-graph-octopus.sh               |  85 +++-
 t/t4215-log-skewed-merges.sh               | 267 +++++++++++
 t/t6016-rev-list-graph-simplify-history.sh |  30 +-
 7 files changed, 649 insertions(+), 244 deletions(-)
 create mode 100755 t/t4215-log-skewed-merges.sh

base-commit: 5fa0f5238b0cd46cfe7f6fa76c3f526ea98148d9
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-383%2Fjcoglan%2Fjc%2Fsimplify-graph-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-383/jcoglan/jc/simplify-graph-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/383

Signature · 1 line
gitgitgadget
#1 lore raw

PATCH 1/11 graph: automatically track visible width of `strbuf`

From: James Coglan <[email protected]>

All the output functions in graph.c currently keep track of how many printable chars they've written to the buffer, before calling graph_pad_horizontally() to pad the line with spaces. Some functions do this by incrementing a counter whenever they write to the buffer, and others do it by encoding an assumption about how many chars are written, as in:

    graph_pad_horizontally(graph, sb, graph->num_columns * 2);

This adds a fair amount of noise to the functions' logic and is easily broken if one forgets to increment the right counter or update the calculations used for padding.

To make this easier to use, I'm adding a width field to strbuf that tracks the number of printing characters added after the line prefix. It's set to 0 at the start of graph_next_line(), and then various strbuf functions update it as follows:

- strbuf_write_column() increments width by 1

- strbuf_setlen() changes width by the amount added to len if len is increased, or makes width and len the same if it's decreased

- strbuf_addch() increments width by 1

This is enough to ensure that the functions used by graph.c update strbuf->width correctly, and graph_pad_horizontally() can then use this field instead of taking chars_written as a parameter.

---
 graph.c  | 68 ++++++++++++++++++++++----------------------------------
 strbuf.h |  8 ++++++-
 2 files changed, 33 insertions(+), 43 deletions(-)
graph.c +26 -42
index f53135485f..c56fdec1fc 100644
@@ -115,11 +115,20 @@ static const char *column_get_color_code(unsigned short color)
 static void strbuf_write_column(struct strbuf *sb, const struct column *c,
 				char col_char)
 {
+	/*
+	 * Remember the buffer's width as we're about to add non-printing
+	 * content to it, and we want to avoid counting the byte length
+	 * of this content towards the buffer's visible width
+	 */
+	size_t prev_width = sb->width;
+
 	if (c->color < column_colors_max)
 		strbuf_addstr(sb, column_get_color_code(c->color));
 	strbuf_addch(sb, col_char);
 	if (c->color < column_colors_max)
 		strbuf_addstr(sb, column_get_color_code(column_colors_max));
+
+	sb->width = prev_width + 1;
 }
 
 struct git_graph {
@@ -686,8 +695,7 @@ static int graph_is_mapping_correct(struct git_graph *graph)
 	return 1;
 }
 
-static void graph_pad_horizontally(struct git_graph *graph, struct strbuf *sb,
-				   int chars_written)
+static void graph_pad_horizontally(struct git_graph *graph, struct strbuf *sb)
 {
 	/*
 	 * Add additional spaces to the end of the strbuf, so that all
@@ -696,8 +704,8 @@ static void graph_pad_horizontally(struct git_graph *graph, struct strbuf *sb,
 	 * This way, fields printed to the right of the graph will remain
 	 * aligned for the entire commit.
 	 */
-	if (chars_written < graph->width)
-		strbuf_addchars(sb, ' ', graph->width - chars_written);
+	if (sb->width < graph->width)
+		strbuf_addchars(sb, ' ', graph->width - sb->width);
 }
 
 static void graph_output_padding_line(struct git_graph *graph,
@@ -723,7 +731,7 @@ static void graph_output_padding_line(struct git_graph *graph,
 		strbuf_addch(sb, ' ');
 	}
 
-	graph_pad_horizontally(graph, sb, graph->num_new_columns * 2);
+	graph_pad_horizontally(graph, sb);
 }
 
 
@@ -740,7 +748,7 @@ static void graph_output_skip_line(struct git_graph *graph, struct strbuf *sb)
 	 * of the graph is missing.
 	 */
 	strbuf_addstr(sb, "...");
-	graph_pad_horizontally(graph, sb, 3);
+	graph_pad_horizontally(graph, sb);
 
 	if (graph->num_parents >= 3 &&
 	    graph->commit_index < (graph->num_columns - 1))
@@ -754,7 +762,6 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 {
 	int num_expansion_rows;
 	int i, seen_this;
-	int chars_written;
 
 	/*
 	 * This function formats a row that increases the space around a commit
@@ -777,14 +784,12 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 	 * Output the row
 	 */
 	seen_this = 0;
-	chars_written = 0;
 	for (i = 0; i < graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		if (col->commit == graph->commit) {
 			seen_this = 1;
 			strbuf_write_column(sb, col, '|');
 			strbuf_addchars(sb, ' ', graph->expansion_row);
-			chars_written += 1 + graph->expansion_row;
 		} else if (seen_this && (graph->expansion_row == 0)) {
 			/*
 			 * This is the first line of the pre-commit output.
@@ -800,19 +805,15 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 				strbuf_write_column(sb, col, '\\');
 			else
 				strbuf_write_column(sb, col, '|');
-			chars_written++;
 		} else if (seen_this && (graph->expansion_row > 0)) {
 			strbuf_write_column(sb, col, '\\');
-			chars_written++;
 		} else {
 			strbuf_write_column(sb, col, '|');
-			chars_written++;
 		}
 		strbuf_addch(sb, ' ');
-		chars_written++;
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, sb);
 
 	/*
 	 * Increment graph->expansion_row,
@@ -842,11 +843,9 @@ static void graph_output_commit_char(struct git_graph *graph, struct strbuf *sb)
 }
 
 /*
- * Draw the horizontal dashes of an octopus merge and return the number of
- * characters written.
+ * Draw the horizontal dashes of an octopus merge.
  */
-static int graph_draw_octopus_merge(struct git_graph *graph,
-				    struct strbuf *sb)
+static void graph_draw_octopus_merge(struct git_graph *graph, struct strbuf *sb)
 {
 	/*
 	 * Here dashless_parents represents the number of parents which don't
@@ -890,13 +889,12 @@ static int graph_draw_octopus_merge(struct git_graph *graph,
 		strbuf_write_column(sb, &graph->new_columns[i+first_col],
 				    i == dashful_parents-1 ? '.' : '-');
 	}
-	return 2 * dashful_parents;
 }
 
 static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 {
 	int seen_this = 0;
-	int i, chars_written;
+	int i;
 
 	/*
 	 * Output the row containing this commit
@@ -906,7 +904,6 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 	 * children that we have already processed.)
 	 */
 	seen_this = 0;
-	chars_written = 0;
 	for (i = 0; i <= graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		struct commit *col_commit;
@@ -921,14 +918,11 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 		if (col_commit == graph->commit) {
 			seen_this = 1;
 			graph_output_commit_char(graph, sb);
-			chars_written++;
 
 			if (graph->num_parents > 2)
-				chars_written += graph_draw_octopus_merge(graph,
-									  sb);
+				graph_draw_octopus_merge(graph, sb);
 		} else if (seen_this && (graph->num_parents > 2)) {
 			strbuf_write_column(sb, col, '\\');
-			chars_written++;
 		} else if (seen_this && (graph->num_parents == 2)) {
 			/*
 			 * This is a 2-way merge commit.
@@ -948,16 +942,13 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 				strbuf_write_column(sb, col, '\\');
 			else
 				strbuf_write_column(sb, col, '|');
-			chars_written++;
 		} else {
 			strbuf_write_column(sb, col, '|');
-			chars_written++;
 		}
 		strbuf_addch(sb, ' ');
-		chars_written++;
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, sb);
 
 	/*
 	 * Update graph->state
@@ -984,12 +975,11 @@ static struct column *find_new_column_by_commit(struct git_graph *graph,
 static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf *sb)
 {
 	int seen_this = 0;
-	int i, j, chars_written;
+	int i, j;
 
 	/*
 	 * Output the post-merge row
 	 */
-	chars_written = 0;
 	for (i = 0; i <= graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		struct commit *col_commit;
@@ -1017,7 +1007,6 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf
 			assert(par_column);
 
 			strbuf_write_column(sb, par_column, '|');
-			chars_written++;
 			for (j = 0; j < graph->num_parents - 1; j++) {
 				parents = next_interesting_parent(graph, parents);
 				assert(parents);
@@ -1026,19 +1015,16 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf
 				strbuf_write_column(sb, par_column, '\\');
 				strbuf_addch(sb, ' ');
 			}
-			chars_written += j * 2;
 		} else if (seen_this) {
 			strbuf_write_column(sb, col, '\\');
 			strbuf_addch(sb, ' ');
-			chars_written += 2;
 		} else {
 			strbuf_write_column(sb, col, '|');
 			strbuf_addch(sb, ' ');
-			chars_written += 2;
 		}
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, sb);
 
 	/*
 	 * Update graph->state
@@ -1181,7 +1167,7 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 		}
 	}
 
-	graph_pad_horizontally(graph, sb, graph->mapping_size);
+	graph_pad_horizontally(graph, sb);
 
 	/*
 	 * Swap mapping and new_mapping
@@ -1199,6 +1185,8 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 
 int graph_next_line(struct git_graph *graph, struct strbuf *sb)
 {
+	sb->width = 0;
+
 	switch (graph->state) {
 	case GRAPH_PADDING:
 		graph_output_padding_line(graph, sb);
@@ -1227,7 +1215,6 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb)
 static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)
 {
 	int i;
-	int chars_written = 0;
 
 	if (graph->state != GRAPH_COMMIT) {
 		graph_next_line(graph, sb);
@@ -1245,19 +1232,16 @@ static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)
 		struct column *col = &graph->columns[i];
 
 		strbuf_write_column(sb, col, '|');
-		chars_written++;
 
 		if (col->commit == graph->commit && graph->num_parents > 2) {
 			int len = (graph->num_parents - 2) * 2;
 			strbuf_addchars(sb, ' ', len);
-			chars_written += len;
 		} else {
 			strbuf_addch(sb, ' ');
-			chars_written++;
 		}
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, sb);
 
 	/*
 	 * Update graph->prev_state since we have output a padding line
strbuf.h +7 -1
index f62278a0be..3a98147321 100644
@@ -66,11 +66,12 @@ struct string_list;
 struct strbuf {
 	size_t alloc;
 	size_t len;
+	size_t width;
 	char *buf;
 };
 
 extern char strbuf_slopbuf[];
-#define STRBUF_INIT  { .alloc = 0, .len = 0, .buf = strbuf_slopbuf }
+#define STRBUF_INIT  { .alloc = 0, .len = 0, .width = 0, .buf = strbuf_slopbuf }
 
 /*
  * Predeclare this here, since cache.h includes this file before it defines the
@@ -161,6 +162,10 @@ static inline void strbuf_setlen(struct strbuf *sb, size_t len)
 {
 	if (len > (sb->alloc ? sb->alloc - 1 : 0))
 		die("BUG: strbuf_setlen() beyond buffer");
+	if (len > sb->len)
+		sb->width += len - sb->len;
+	else
+		sb->width = len;
 	sb->len = len;
 	if (sb->buf != strbuf_slopbuf)
 		sb->buf[len] = '\0';
@@ -231,6 +236,7 @@ static inline void strbuf_addch(struct strbuf *sb, int c)
 		strbuf_grow(sb, 1);
 	sb->buf[sb->len++] = c;
 	sb->buf[sb->len] = '\0';
+	sb->width++;
 }
 
 /**
Signature · 1 line
gitgitgadget
#2 lore raw

Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`

Hi James,

Quoted 9 lines · James Coglan via GitGitGadget

From: James Coglan <[email protected]>

All the output functions in graph.c currently keep track of how many printable chars they've written to the buffer, before calling graph_pad_horizontally() to pad the line with spaces. Some functions do this by incrementing a counter whenever they write to the buffer, and others do it by encoding an assumption about how many chars are written, as in:

Quoted 8 lines
    graph_pad_horizontally(graph, sb, graph->num_columns * 2);

This adds a fair amount of noise to the functions' logic and is easily broken if one forgets to increment the right counter or update the calculations used for padding.

To make this easier to use, I'm adding a width field to strbuf that tracks the number of printing characters added after the line prefix.

This is a big heavy-handed: adding a width field to struct strbuf and maintaining it _just_ for the purpose of graph.c puts an unnecssary load on every other strbuf user (of which there are a _lot_).

So my obvious question is: what makes width different from len? Since we exclusively use ASCII characters for the graph part, we should be able to use the already-existing len, for free, no?

I could imagine that the strbuf might receive more than one line, but then we still would only need to remember the offset of the last newline character in that strbuf, no?

Ciao,
Johannes

Quoted 331 lines

It's set to 0 at the start of graph_next_line(), and then various strbuf functions update it as follows:

- strbuf_write_column() increments width by 1

- strbuf_setlen() changes width by the amount added to len if len is increased, or makes width and len the same if it's decreased

- strbuf_addch() increments width by 1

This is enough to ensure that the functions used by graph.c update strbuf->width correctly, and graph_pad_horizontally() can then use this field instead of taking chars_written as a parameter.

---
 graph.c  | 68 ++++++++++++++++++++++----------------------------------
 strbuf.h |  8 ++++++-
 2 files changed, 33 insertions(+), 43 deletions(-)
graph.c +26 -42
index f53135485f..c56fdec1fc 100644
@@ -115,11 +115,20 @@ static const char *column_get_color_code(unsigned short color)
 static void strbuf_write_column(struct strbuf *sb, const struct column *c,
 				char col_char)
 {
+	/*
+	 * Remember the buffer's width as we're about to add non-printing
+	 * content to it, and we want to avoid counting the byte length
+	 * of this content towards the buffer's visible width
+	 */
+	size_t prev_width = sb->width;
+
 	if (c->color < column_colors_max)
 		strbuf_addstr(sb, column_get_color_code(c->color));
 	strbuf_addch(sb, col_char);
 	if (c->color < column_colors_max)
 		strbuf_addstr(sb, column_get_color_code(column_colors_max));
+
+	sb->width = prev_width + 1;
 }
 
 struct git_graph {
@@ -686,8 +695,7 @@ static int graph_is_mapping_correct(struct git_graph *graph)
 	return 1;
 }
 
-static void graph_pad_horizontally(struct git_graph *graph, struct strbuf *sb,
-				   int chars_written)
+static void graph_pad_horizontally(struct git_graph *graph, struct strbuf *sb)
 {
 	/*
 	 * Add additional spaces to the end of the strbuf, so that all
@@ -696,8 +704,8 @@ static void graph_pad_horizontally(struct git_graph *graph, struct strbuf *sb,
 	 * This way, fields printed to the right of the graph will remain
 	 * aligned for the entire commit.
 	 */
-	if (chars_written < graph->width)
-		strbuf_addchars(sb, ' ', graph->width - chars_written);
+	if (sb->width < graph->width)
+		strbuf_addchars(sb, ' ', graph->width - sb->width);
 }
 
 static void graph_output_padding_line(struct git_graph *graph,
@@ -723,7 +731,7 @@ static void graph_output_padding_line(struct git_graph *graph,
 		strbuf_addch(sb, ' ');
 	}
 
-	graph_pad_horizontally(graph, sb, graph->num_new_columns * 2);
+	graph_pad_horizontally(graph, sb);
 }
 
 
@@ -740,7 +748,7 @@ static void graph_output_skip_line(struct git_graph *graph, struct strbuf *sb)
 	 * of the graph is missing.
 	 */
 	strbuf_addstr(sb, "...");
-	graph_pad_horizontally(graph, sb, 3);
+	graph_pad_horizontally(graph, sb);
 
 	if (graph->num_parents >= 3 &&
 	    graph->commit_index < (graph->num_columns - 1))
@@ -754,7 +762,6 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 {
 	int num_expansion_rows;
 	int i, seen_this;
-	int chars_written;
 
 	/*
 	 * This function formats a row that increases the space around a commit
@@ -777,14 +784,12 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 	 * Output the row
 	 */
 	seen_this = 0;
-	chars_written = 0;
 	for (i = 0; i < graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		if (col->commit == graph->commit) {
 			seen_this = 1;
 			strbuf_write_column(sb, col, '|');
 			strbuf_addchars(sb, ' ', graph->expansion_row);
-			chars_written += 1 + graph->expansion_row;
 		} else if (seen_this && (graph->expansion_row == 0)) {
 			/*
 			 * This is the first line of the pre-commit output.
@@ -800,19 +805,15 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 				strbuf_write_column(sb, col, '\\');
 			else
 				strbuf_write_column(sb, col, '|');
-			chars_written++;
 		} else if (seen_this && (graph->expansion_row > 0)) {
 			strbuf_write_column(sb, col, '\\');
-			chars_written++;
 		} else {
 			strbuf_write_column(sb, col, '|');
-			chars_written++;
 		}
 		strbuf_addch(sb, ' ');
-		chars_written++;
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, sb);
 
 	/*
 	 * Increment graph->expansion_row,
@@ -842,11 +843,9 @@ static void graph_output_commit_char(struct git_graph *graph, struct strbuf *sb)
 }
 
 /*
- * Draw the horizontal dashes of an octopus merge and return the number of
- * characters written.
+ * Draw the horizontal dashes of an octopus merge.
  */
-static int graph_draw_octopus_merge(struct git_graph *graph,
-				    struct strbuf *sb)
+static void graph_draw_octopus_merge(struct git_graph *graph, struct strbuf *sb)
 {
 	/*
 	 * Here dashless_parents represents the number of parents which don't
@@ -890,13 +889,12 @@ static int graph_draw_octopus_merge(struct git_graph *graph,
 		strbuf_write_column(sb, &graph->new_columns[i+first_col],
 				    i == dashful_parents-1 ? '.' : '-');
 	}
-	return 2 * dashful_parents;
 }
 
 static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 {
 	int seen_this = 0;
-	int i, chars_written;
+	int i;
 
 	/*
 	 * Output the row containing this commit
@@ -906,7 +904,6 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 	 * children that we have already processed.)
 	 */
 	seen_this = 0;
-	chars_written = 0;
 	for (i = 0; i <= graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		struct commit *col_commit;
@@ -921,14 +918,11 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 		if (col_commit == graph->commit) {
 			seen_this = 1;
 			graph_output_commit_char(graph, sb);
-			chars_written++;
 
 			if (graph->num_parents > 2)
-				chars_written += graph_draw_octopus_merge(graph,
-									  sb);
+				graph_draw_octopus_merge(graph, sb);
 		} else if (seen_this && (graph->num_parents > 2)) {
 			strbuf_write_column(sb, col, '\\');
-			chars_written++;
 		} else if (seen_this && (graph->num_parents == 2)) {
 			/*
 			 * This is a 2-way merge commit.
@@ -948,16 +942,13 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 				strbuf_write_column(sb, col, '\\');
 			else
 				strbuf_write_column(sb, col, '|');
-			chars_written++;
 		} else {
 			strbuf_write_column(sb, col, '|');
-			chars_written++;
 		}
 		strbuf_addch(sb, ' ');
-		chars_written++;
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, sb);
 
 	/*
 	 * Update graph->state
@@ -984,12 +975,11 @@ static struct column *find_new_column_by_commit(struct git_graph *graph,
 static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf *sb)
 {
 	int seen_this = 0;
-	int i, j, chars_written;
+	int i, j;
 
 	/*
 	 * Output the post-merge row
 	 */
-	chars_written = 0;
 	for (i = 0; i <= graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		struct commit *col_commit;
@@ -1017,7 +1007,6 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf
 			assert(par_column);
 
 			strbuf_write_column(sb, par_column, '|');
-			chars_written++;
 			for (j = 0; j < graph->num_parents - 1; j++) {
 				parents = next_interesting_parent(graph, parents);
 				assert(parents);
@@ -1026,19 +1015,16 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf
 				strbuf_write_column(sb, par_column, '\\');
 				strbuf_addch(sb, ' ');
 			}
-			chars_written += j * 2;
 		} else if (seen_this) {
 			strbuf_write_column(sb, col, '\\');
 			strbuf_addch(sb, ' ');
-			chars_written += 2;
 		} else {
 			strbuf_write_column(sb, col, '|');
 			strbuf_addch(sb, ' ');
-			chars_written += 2;
 		}
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, sb);
 
 	/*
 	 * Update graph->state
@@ -1181,7 +1167,7 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 		}
 	}
 
-	graph_pad_horizontally(graph, sb, graph->mapping_size);
+	graph_pad_horizontally(graph, sb);
 
 	/*
 	 * Swap mapping and new_mapping
@@ -1199,6 +1185,8 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 
 int graph_next_line(struct git_graph *graph, struct strbuf *sb)
 {
+	sb->width = 0;
+
 	switch (graph->state) {
 	case GRAPH_PADDING:
 		graph_output_padding_line(graph, sb);
@@ -1227,7 +1215,6 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb)
 static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)
 {
 	int i;
-	int chars_written = 0;
 
 	if (graph->state != GRAPH_COMMIT) {
 		graph_next_line(graph, sb);
@@ -1245,19 +1232,16 @@ static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)
 		struct column *col = &graph->columns[i];
 
 		strbuf_write_column(sb, col, '|');
-		chars_written++;
 
 		if (col->commit == graph->commit && graph->num_parents > 2) {
 			int len = (graph->num_parents - 2) * 2;
 			strbuf_addchars(sb, ' ', len);
-			chars_written += len;
 		} else {
 			strbuf_addch(sb, ' ');
-			chars_written++;
 		}
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, sb);
 
 	/*
 	 * Update graph->prev_state since we have output a padding line
strbuf.h +7 -2
index f62278a0be..3a98147321 100644
@@ -66,11 +66,12 @@ struct string_list;
 struct strbuf {
 	size_t alloc;
 	size_t len;
+	size_t width;
 	char *buf;
 };
 
 extern char strbuf_slopbuf[];
-#define STRBUF_INIT  { .alloc = 0, .len = 0, .buf = strbuf_slopbuf }
+#define STRBUF_INIT  { .alloc = 0, .len = 0, .width = 0, .buf = strbuf_slopbuf }
 
 /*
  * Predeclare this here, since cache.h includes this file before it defines the
@@ -161,6 +162,10 @@ static inline void strbuf_setlen(struct strbuf *sb, size_t len)
 {
 	if (len > (sb->alloc ? sb->alloc - 1 : 0))
 		die("BUG: strbuf_setlen() beyond buffer");
+	if (len > sb->len)
+		sb->width += len - sb->len;
+	else
+		sb->width = len;
 	sb->len = len;
 	if (sb->buf != strbuf_slopbuf)
 		sb->buf[len] = '\0';
@@ -231,6 +236,7 @@ static inline void strbuf_addch(struct strbuf *sb, int c)
 		strbuf_grow(sb, 1);
 	sb->buf[sb->len++] = c;
 	sb->buf[sb->len] = '\0';
+	sb->width++;
 }
 
 /**
--

gitgitgadget

#3 lore raw

Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`

Hi Dscho,

Quoted 22 lines · Johannes Schindelin

Hi James,

Quoted 9 lines · James Coglan via GitGitGadget

From: James Coglan <[email protected]>

All the output functions in graph.c currently keep track of how many printable chars they've written to the buffer, before calling graph_pad_horizontally() to pad the line with spaces. Some functions do this by incrementing a counter whenever they write to the buffer, and others do it by encoding an assumption about how many chars are written, as in:

Quoted 8 lines
    graph_pad_horizontally(graph, sb, graph->num_columns * 2);

This adds a fair amount of noise to the functions' logic and is easily broken if one forgets to increment the right counter or update the calculations used for padding.

To make this easier to use, I'm adding a width field to strbuf that tracks the number of printing characters added after the line prefix.

Quoted 8 lines

This is a big heavy-handed: adding a width field to struct strbuf and maintaining it _just_ for the purpose of graph.c puts an unnecssary load on every other strbuf user (of which there are a _lot_).

So my obvious question is: what makes width different from len? Since we exclusively use ASCII characters for the graph part, we should be able to use the already-existing len, for free, no?

From what I can gleam from looking at the code, width is different from len because when we're printing with colours, there'll be a bunch of termcodes that don't actually count for the width.

I think that we should either leave the chars_written variable as is or maybe calculate it after the fact. Here's an untested and uncompiled implementation of something that might do that:

	static int calculate_width(const struct strbuf *row)
	{
		int in_termcode = 0;
		int width = 0;
		int i;

		for (i = 0; i < row.len; i++) {
			if (row.buf[i] == '\033')
				in_termcode = 1;

			if (!in_termcode)
				width++;
			else if (row.buf[i] == 'm')
				in_termcode = 0;
		}
	}

If we include this, I'm not sure what kind of performance hit we might take if the graph we're generating is particularly big, though.

Quoted 7 lines

I could imagine that the strbuf might receive more than one line, but then we still would only need to remember the offset of the last newline character in that strbuf, no?

Ciao,
Johannes

#4 lore raw

Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`

Quoted 29 lines · Denton Liu

Hi Dscho,

Quoted 26 lines · Johannes Schindelin

Hi James,

Quoted 9 lines · James Coglan via GitGitGadget

From: James Coglan <[email protected]>

All the output functions in graph.c currently keep track of how many printable chars they've written to the buffer, before calling graph_pad_horizontally() to pad the line with spaces. Some functions do this by incrementing a counter whenever they write to the buffer, and others do it by encoding an assumption about how many chars are written, as in:

Quoted 8 lines
    graph_pad_horizontally(graph, sb, graph->num_columns * 2);

This adds a fair amount of noise to the functions' logic and is easily broken if one forgets to increment the right counter or update the calculations used for padding.

To make this easier to use, I'm adding a width field to strbuf that tracks the number of printing characters added after the line prefix.

This is a big heavy-handed: adding a width field to struct strbuf and maintaining it _just_ for the purpose of graph.c puts an unnecssary load on every other strbuf user (of which there are a _lot_).

I was concerned about this, too.

Quoted 29 lines
Quoted 3 lines

So my obvious question is: what makes width different from len? Since we exclusively use ASCII characters for the graph part, we should be able to use the already-existing len, for free, no?

From what I can gleam from looking at the code, width is different from len because when we're printing with colours, there'll be a bunch of termcodes that don't actually count for the width.

I think that we should either leave the chars_written variable as is or maybe calculate it after the fact. Here's an untested and uncompiled implementation of something that might do that:

	static int calculate_width(const struct strbuf *row)
	{
		int in_termcode = 0;
		int width = 0;
		int i;

		for (i = 0; i < row.len; i++) {
			if (row.buf[i] == '\033')
				in_termcode = 1;

			if (!in_termcode)
				width++;
			else if (row.buf[i] == 'm')
				in_termcode = 0;
		}
	}
Quoted 2 lines

If we include this, I'm not sure what kind of performance hit we might take if the graph we're generating is particularly big, though.

This is worth trying. In terms of CPU time, this should be a mere blip compared to all of the commit walking that is going on. (Unless we get to thousands of columns, but by then the output is useless.)

Of course, we should measure the impact, but it is worth a try.

-Stolee

#5 lore raw

Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`

In reply to Denton Liu
Quoted 8 lines · Denton Liu
	static int calculate_width(const struct strbuf *row)
	{
		int in_termcode = 0;
		int width = 0;
		int i;

		for (i = 0; i < row.len; i++) {
			if (row.buf[i] == '\033')
Quoted 8 lines
				in_termcode = 1;

			if (!in_termcode)
				width++;
			else if (row.buf[i] == 'm')
				in_termcode = 0;
		}
	}

Not every byte that is outside the escape sequence contributes to one display columns. You would want to take a look at utf8_width() for inspiration.

#6 lore raw

Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`

Quoted 19 lines · Junio C Hamano
Quoted 8 lines · Denton Liu
	static int calculate_width(const struct strbuf *row)
	{
		int in_termcode = 0;
		int width = 0;
		int i;

		for (i = 0; i < row.len; i++) {
			if (row.buf[i] == '\033')
Quoted 8 lines
				in_termcode = 1;

			if (!in_termcode)
				width++;
			else if (row.buf[i] == 'm')
				in_termcode = 0;
		}
	}
Quoted 4 lines

Not every byte that is outside the escape sequence contributes to one display columns. You would want to take a look at utf8_width() for inspiration.

Heh, I guess you're right. Looking right below the definition of utf8_width, I realised we have the utf8_strnwidth function. We should be able to just call

	utf8_strnwidth(row.buf, row.len, 1);
#7 lore raw

Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`

Hi,

Quoted 25 lines · Denton Liu
Quoted 19 lines · Junio C Hamano
Quoted 8 lines · Denton Liu
	static int calculate_width(const struct strbuf *row)
	{
		int in_termcode = 0;
		int width = 0;
		int i;

		for (i = 0; i < row.len; i++) {
			if (row.buf[i] == '\033')
Quoted 8 lines
				in_termcode = 1;

			if (!in_termcode)
				width++;
			else if (row.buf[i] == 'm')
				in_termcode = 0;
		}
	}
Quoted 4 lines

Not every byte that is outside the escape sequence contributes to one display columns. You would want to take a look at utf8_width() for inspiration.

Quoted 5 lines

Heh, I guess you're right. Looking right below the definition of utf8_width, I realised we have the utf8_strnwidth function. We should be able to just call

	utf8_strnwidth(row.buf, row.len, 1);

Correct me if I'm wrong, but don't we want to keep track of the columns *only* in the part with the actual line graph, i.e. we're not at all interested in the onelines' widths?

If so, I could imagine that a good idea would be to introduce

	struct graphbuf {
		struct strbuf buf;
		int width;
	};

and then introduce wrappers for _addch() and whatever else is used in graph.c, these wrappers will increment the width together with the buf.len field, and one additional helper that adds color sequences to that graphbuf that leaves width unchanged.

Ciao,
Dscho

#8 lore raw

Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`

Quoted 46 lines · Johannes Schindelin

Hi,

Quoted 25 lines · Denton Liu
Quoted 19 lines · Junio C Hamano
Quoted 8 lines · Denton Liu
	static int calculate_width(const struct strbuf *row)
	{
		int in_termcode = 0;
		int width = 0;
		int i;

		for (i = 0; i < row.len; i++) {
			if (row.buf[i] == '\033')
Quoted 8 lines
				in_termcode = 1;

			if (!in_termcode)
				width++;
			else if (row.buf[i] == 'm')
				in_termcode = 0;
		}
	}
Quoted 4 lines

Not every byte that is outside the escape sequence contributes to one display columns. You would want to take a look at utf8_width() for inspiration.

Quoted 5 lines

Heh, I guess you're right. Looking right below the definition of utf8_width, I realised we have the utf8_strnwidth function. We should be able to just call

	utf8_strnwidth(row.buf, row.len, 1);

Correct me if I'm wrong, but don't we want to keep track of the columns *only* in the part with the actual line graph, i.e. we're not at all interested in the onelines' widths?

If so, I could imagine that a good idea would be to introduce

	struct graphbuf {
		struct strbuf buf;
		int width;
	};
Quoted 4 lines

and then introduce wrappers for _addch() and whatever else is used in graph.c, these wrappers will increment the width together with the buf.len field, and one additional helper that adds color sequences to that graphbuf that leaves width unchanged.

That's exactly what I've spent this afternoon doing, in response to the feedback here. I took into consideration that:

- We don't want a general solution to this problem for everything strbuf could be used for; it only needs to address the graph padding problem.

- We only want to count printing characters, not color formatting sequences.

- We only need to consider the width of a small set of characters: { | / \ _ - . * }. We don't need to worry about Unicode, and the simple character counting in graph.c was working fine.

- Attempting to count the characters in a strbuf after the fact is problematic, because sometimes the line prefix (diff_options.line_prefix) is included when graph_pad_horizontally() is called, and sometimes the prefix has already been sent to stdout and is not included in the strbuf. We need to know how many printing characters were added only during graph_next_line().

To this end I've prepared a different implementation that introduces the indirection described above, and does not modify strbuf:

graph.c +94 -94
index f53135485f..24bf1f4fe1 100644
@@ -112,14 +112,38 @@ static const char *column_get_color_code(unsigned short color)
 	return column_colors[color];
 }
 
-static void strbuf_write_column(struct strbuf *sb, const struct column *c,
-				char col_char)
+struct graph_strbuf {
+	struct strbuf *buf;
+	size_t width;
+};
+
+static inline void graph_strbuf_addch(struct graph_strbuf *sb, int c)
+{
+	strbuf_addch(sb->buf, c);
+	sb->width++;
+}
+
+void graph_strbuf_addchars(struct graph_strbuf *sb, int c, size_t n)
+{
+	strbuf_addchars(sb->buf, c, n);
+	sb->width += n;
+}
+
+static inline void graph_strbuf_addstr(struct graph_strbuf *sb, const char *s)
+{
+	strbuf_addstr(sb->buf, s);
+	sb->width += strlen(s);
+}
+
+static void graph_strbuf_write_column(struct graph_strbuf *sb, const struct column *c,
+				      char col_char)
 {
 	if (c->color < column_colors_max)
-		strbuf_addstr(sb, column_get_color_code(c->color));
-	strbuf_addch(sb, col_char);
+		strbuf_addstr(sb->buf, column_get_color_code(c->color));
+	strbuf_addch(sb->buf, col_char);
+	sb->width++;
 	if (c->color < column_colors_max)
-		strbuf_addstr(sb, column_get_color_code(column_colors_max));
+		strbuf_addstr(sb->buf, column_get_color_code(column_colors_max));
 }
 
 struct git_graph {
@@ -686,8 +710,7 @@ static int graph_is_mapping_correct(struct git_graph *graph)
 	return 1;
 }
 
-static void graph_pad_horizontally(struct git_graph *graph, struct strbuf *sb,
-				   int chars_written)
+static void graph_pad_horizontally(struct git_graph *graph, struct graph_strbuf *sb)
 {
 	/*
 	 * Add additional spaces to the end of the strbuf, so that all
@@ -696,12 +719,12 @@ static void graph_pad_horizontally(struct git_graph *graph, struct strbuf *sb,
 	 * This way, fields printed to the right of the graph will remain
 	 * aligned for the entire commit.
 	 */
-	if (chars_written < graph->width)
-		strbuf_addchars(sb, ' ', graph->width - chars_written);
+	if (sb->width < graph->width)
+		strbuf_addchars(sb->buf, ' ', graph->width - sb->width);
 }
 
 static void graph_output_padding_line(struct git_graph *graph,
-				      struct strbuf *sb)
+				      struct graph_strbuf *sb)
 {
 	int i;
 
@@ -719,11 +742,11 @@ 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++) {
-		strbuf_write_column(sb, &graph->new_columns[i], '|');
-		strbuf_addch(sb, ' ');
+		graph_strbuf_write_column(sb, &graph->new_columns[i], '|');
+		graph_strbuf_addch(sb, ' ');
 	}
 
-	graph_pad_horizontally(graph, sb, graph->num_new_columns * 2);
+	graph_pad_horizontally(graph, sb);
 }
 
 
@@ -733,14 +756,14 @@ int graph_width(struct git_graph *graph)
 }
 
 
-static void graph_output_skip_line(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_skip_line(struct git_graph *graph, struct graph_strbuf *sb)
 {
 	/*
 	 * Output an ellipsis to indicate that a portion
 	 * of the graph is missing.
 	 */
-	strbuf_addstr(sb, "...");
-	graph_pad_horizontally(graph, sb, 3);
+	graph_strbuf_addstr(sb, "...");
+	graph_pad_horizontally(graph, sb);
 
 	if (graph->num_parents >= 3 &&
 	    graph->commit_index < (graph->num_columns - 1))
@@ -750,11 +773,10 @@ static void graph_output_skip_line(struct git_graph *graph, struct strbuf *sb)
 }
 
 static void graph_output_pre_commit_line(struct git_graph *graph,
-					 struct strbuf *sb)
+					 struct graph_strbuf *sb)
 {
 	int num_expansion_rows;
 	int i, seen_this;
-	int chars_written;
 
 	/*
 	 * This function formats a row that increases the space around a commit
@@ -777,14 +799,12 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 	 * Output the row
 	 */
 	seen_this = 0;
-	chars_written = 0;
 	for (i = 0; i < graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		if (col->commit == graph->commit) {
 			seen_this = 1;
-			strbuf_write_column(sb, col, '|');
-			strbuf_addchars(sb, ' ', graph->expansion_row);
-			chars_written += 1 + graph->expansion_row;
+			graph_strbuf_write_column(sb, col, '|');
+			graph_strbuf_addchars(sb, ' ', graph->expansion_row);
 		} else if (seen_this && (graph->expansion_row == 0)) {
 			/*
 			 * This is the first line of the pre-commit output.
@@ -797,22 +817,18 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 			 */
 			if (graph->prev_state == GRAPH_POST_MERGE &&
 			    graph->prev_commit_index < i)
-				strbuf_write_column(sb, col, '\\');
+				graph_strbuf_write_column(sb, col, '\\');
 			else
-				strbuf_write_column(sb, col, '|');
-			chars_written++;
+				graph_strbuf_write_column(sb, col, '|');
 		} else if (seen_this && (graph->expansion_row > 0)) {
-			strbuf_write_column(sb, col, '\\');
-			chars_written++;
+			graph_strbuf_write_column(sb, col, '\\');
 		} else {
-			strbuf_write_column(sb, col, '|');
-			chars_written++;
+			graph_strbuf_write_column(sb, col, '|');
 		}
-		strbuf_addch(sb, ' ');
-		chars_written++;
+		graph_strbuf_addch(sb, ' ');
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, sb);
 
 	/*
 	 * Increment graph->expansion_row,
@@ -823,7 +839,7 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 		graph_update_state(graph, GRAPH_COMMIT);
 }
 
-static void graph_output_commit_char(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_commit_char(struct git_graph *graph, struct graph_strbuf *sb)
 {
 	/*
 	 * For boundary commits, print 'o'
@@ -831,22 +847,20 @@ static void graph_output_commit_char(struct git_graph *graph, struct strbuf *sb)
 	 */
 	if (graph->commit->object.flags & BOUNDARY) {
 		assert(graph->revs->boundary);
-		strbuf_addch(sb, 'o');
+		graph_strbuf_addch(sb, 'o');
 		return;
 	}
 
 	/*
 	 * get_revision_mark() handles all other cases without assert()
 	 */
-	strbuf_addstr(sb, get_revision_mark(graph->revs, graph->commit));
+	graph_strbuf_addstr(sb, get_revision_mark(graph->revs, graph->commit));
 }
 
 /*
- * Draw the horizontal dashes of an octopus merge and return the number of
- * characters written.
+ * Draw the horizontal dashes of an octopus merge.
  */
-static int graph_draw_octopus_merge(struct git_graph *graph,
-				    struct strbuf *sb)
+static void graph_draw_octopus_merge(struct git_graph *graph, struct graph_strbuf *sb)
 {
 	/*
 	 * Here dashless_parents represents the number of parents which don't
@@ -886,17 +900,16 @@ static int graph_draw_octopus_merge(struct git_graph *graph,
 
 	int i;
 	for (i = 0; i < dashful_parents; i++) {
-		strbuf_write_column(sb, &graph->new_columns[i+first_col], '-');
-		strbuf_write_column(sb, &graph->new_columns[i+first_col],
-				    i == dashful_parents-1 ? '.' : '-');
+		graph_strbuf_write_column(sb, &graph->new_columns[i+first_col], '-');
+		graph_strbuf_write_column(sb, &graph->new_columns[i+first_col],
+					  i == dashful_parents-1 ? '.' : '-');
 	}
-	return 2 * dashful_parents;
 }
 
-static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_commit_line(struct git_graph *graph, struct graph_strbuf *sb)
 {
 	int seen_this = 0;
-	int i, chars_written;
+	int i;
 
 	/*
 	 * Output the row containing this commit
@@ -906,7 +919,6 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 	 * children that we have already processed.)
 	 */
 	seen_this = 0;
-	chars_written = 0;
 	for (i = 0; i <= graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		struct commit *col_commit;
@@ -921,14 +933,11 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 		if (col_commit == graph->commit) {
 			seen_this = 1;
 			graph_output_commit_char(graph, sb);
-			chars_written++;
 
 			if (graph->num_parents > 2)
-				chars_written += graph_draw_octopus_merge(graph,
-									  sb);
+				graph_draw_octopus_merge(graph, sb);
 		} else if (seen_this && (graph->num_parents > 2)) {
-			strbuf_write_column(sb, col, '\\');
-			chars_written++;
+			graph_strbuf_write_column(sb, col, '\\');
 		} else if (seen_this && (graph->num_parents == 2)) {
 			/*
 			 * This is a 2-way merge commit.
@@ -945,19 +954,16 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 			 */
 			if (graph->prev_state == GRAPH_POST_MERGE &&
 			    graph->prev_commit_index < i)
-				strbuf_write_column(sb, col, '\\');
+				graph_strbuf_write_column(sb, col, '\\');
 			else
-				strbuf_write_column(sb, col, '|');
-			chars_written++;
+				graph_strbuf_write_column(sb, col, '|');
 		} else {
-			strbuf_write_column(sb, col, '|');
-			chars_written++;
+			graph_strbuf_write_column(sb, col, '|');
 		}
-		strbuf_addch(sb, ' ');
-		chars_written++;
+		graph_strbuf_addch(sb, ' ');
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, sb);
 
 	/*
 	 * Update graph->state
@@ -981,15 +987,14 @@ static struct column *find_new_column_by_commit(struct git_graph *graph,
 	return NULL;
 }
 
-static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_post_merge_line(struct git_graph *graph, struct graph_strbuf *sb)
 {
 	int seen_this = 0;
-	int i, j, chars_written;
+	int i, j;
 
 	/*
 	 * Output the post-merge row
 	 */
-	chars_written = 0;
 	for (i = 0; i <= graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		struct commit *col_commit;
@@ -1016,29 +1021,25 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf
 			par_column = find_new_column_by_commit(graph, parents->item);
 			assert(par_column);
 
-			strbuf_write_column(sb, par_column, '|');
-			chars_written++;
+			graph_strbuf_write_column(sb, par_column, '|');
 			for (j = 0; j < graph->num_parents - 1; j++) {
 				parents = next_interesting_parent(graph, parents);
 				assert(parents);
 				par_column = find_new_column_by_commit(graph, parents->item);
 				assert(par_column);
-				strbuf_write_column(sb, par_column, '\\');
-				strbuf_addch(sb, ' ');
+				graph_strbuf_write_column(sb, par_column, '\\');
+				graph_strbuf_addch(sb, ' ');
 			}
-			chars_written += j * 2;
 		} else if (seen_this) {
-			strbuf_write_column(sb, col, '\\');
-			strbuf_addch(sb, ' ');
-			chars_written += 2;
+			graph_strbuf_write_column(sb, col, '\\');
+			graph_strbuf_addch(sb, ' ');
 		} else {
-			strbuf_write_column(sb, col, '|');
-			strbuf_addch(sb, ' ');
-			chars_written += 2;
+			graph_strbuf_write_column(sb, col, '|');
+			graph_strbuf_addch(sb, ' ');
 		}
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, sb);
 
 	/*
 	 * Update graph->state
@@ -1049,7 +1050,7 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf
 		graph_update_state(graph, GRAPH_COLLAPSING);
 }
 
-static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_collapsing_line(struct git_graph *graph, struct graph_strbuf *sb)
 {
 	int i;
 	short used_horizontal = 0;
@@ -1159,9 +1160,9 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 	for (i = 0; i < graph->mapping_size; i++) {
 		int target = graph->new_mapping[i];
 		if (target < 0)
-			strbuf_addch(sb, ' ');
+			graph_strbuf_addch(sb, ' ');
 		else if (target * 2 == i)
-			strbuf_write_column(sb, &graph->new_columns[target], '|');
+			graph_strbuf_write_column(sb, &graph->new_columns[target], '|');
 		else if (target == horizontal_edge_target &&
 			 i != horizontal_edge - 1) {
 				/*
@@ -1172,16 +1173,16 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 				if (i != (target * 2)+3)
 					graph->new_mapping[i] = -1;
 				used_horizontal = 1;
-			strbuf_write_column(sb, &graph->new_columns[target], '_');
+			graph_strbuf_write_column(sb, &graph->new_columns[target], '_');
 		} else {
 			if (used_horizontal && i < horizontal_edge)
 				graph->new_mapping[i] = -1;
-			strbuf_write_column(sb, &graph->new_columns[target], '/');
+			graph_strbuf_write_column(sb, &graph->new_columns[target], '/');
 
 		}
 	}
 
-	graph_pad_horizontally(graph, sb, graph->mapping_size);
+	graph_pad_horizontally(graph, sb);
 
 	/*
 	 * Swap mapping and new_mapping
@@ -1199,24 +1200,26 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 
 int graph_next_line(struct git_graph *graph, struct strbuf *sb)
 {
+	struct graph_strbuf gsb = { .buf = sb, .width = 0 };
+
 	switch (graph->state) {
 	case GRAPH_PADDING:
-		graph_output_padding_line(graph, sb);
+		graph_output_padding_line(graph, &gsb);
 		return 0;
 	case GRAPH_SKIP:
-		graph_output_skip_line(graph, sb);
+		graph_output_skip_line(graph, &gsb);
 		return 0;
 	case GRAPH_PRE_COMMIT:
-		graph_output_pre_commit_line(graph, sb);
+		graph_output_pre_commit_line(graph, &gsb);
 		return 0;
 	case GRAPH_COMMIT:
-		graph_output_commit_line(graph, sb);
+		graph_output_commit_line(graph, &gsb);
 		return 1;
 	case GRAPH_POST_MERGE:
-		graph_output_post_merge_line(graph, sb);
+		graph_output_post_merge_line(graph, &gsb);
 		return 0;
 	case GRAPH_COLLAPSING:
-		graph_output_collapsing_line(graph, sb);
+		graph_output_collapsing_line(graph, &gsb);
 		return 0;
 	}
 
@@ -1227,7 +1230,7 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb)
 static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)
 {
 	int i;
-	int chars_written = 0;
+	struct graph_strbuf gsb = { .buf = sb, .width = 0 };
 
 	if (graph->state != GRAPH_COMMIT) {
 		graph_next_line(graph, sb);
@@ -1244,20 +1247,17 @@ 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];
 
-		strbuf_write_column(sb, col, '|');
-		chars_written++;
+		graph_strbuf_write_column(&gsb, col, '|');
 
 		if (col->commit == graph->commit && graph->num_parents > 2) {
 			int len = (graph->num_parents - 2) * 2;
-			strbuf_addchars(sb, ' ', len);
-			chars_written += len;
+			graph_strbuf_addchars(&gsb, ' ', len);
 		} else {
-			strbuf_addch(sb, ' ');
-			chars_written++;
+			graph_strbuf_addch(&gsb, ' ');
 		}
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, &gsb);
 
 	/*
 	 * Update graph->prev_state since we have output a padding line
#9 lore raw

Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`

Quoted 3 lines · James Coglan

- We don't want a general solution to this problem for everything strbuf could be used for; it only needs to address the graph padding problem.

Of course. Somebody may use strbuf to hold rows of a table and you do not want to contaminate strbuf with fields like width of each column etc, that are very specific to the application. IOW, strbuf is merely _one_ component of a larger solution to each specific problem, and the latter may be things like "struct graphbuf" like Dscho suggested, which might use strbuf as an underlying <byte-string, length> tuple mechanism, but that is an implementation detail that should not be exposed to the users of the struct (and that is why he did not call, and you should not call, the data structure "graph-strbuf" or anything with "strbuf").

Quoted 1 line

- We only want to count printing characters, not color formatting sequences.

OK. But I'd phrase "count printing characters" as "measure display width" for at least two reasons. Whitespaces are typically counted as non-printing, but you do want to take them into account for this application. Also the graph may not be limited to ASCII graphics forever, and byte- or character-count may not match display width on a fixed-width display.

Quoted 3 lines

- We only need to consider the width of a small set of characters:
{ | / \ _ - . * }. We don't need to worry about Unicode, and the simple character counting in graph.c was working fine.

I have to warn you that we saw attempts to step outside these ASCII graphics and use Unicode characters for prettier output in the past. If you can do so without too much complexity, I'd suggest you try not to close the door to those people who follow your footsteps to further improve the system by pursuing the avenue further.

graph.c static const char *column_get_color_code(unsigned short color) · line 118

Quoted 8 lines

To this end I've prepared a different implementation that introduces the indirection described above, and does not modify strbuf:

graph.c +4 -0
+struct graph_strbuf {
+	struct strbuf *buf;
+	size_t width;
+};

Is there a reason why you need a pointer to a strbuf that is allocated separately? E.g. would it make it harder to manage if the above were

	struct graphbuf {
		struct strbuf buf;
		int width; /* display width in columns */
	};

which is essentially what Dscho suggested?

Quoted 7 lines
graph.c +7 -0
+static inline void graph_strbuf_addch(struct graph_strbuf *sb, int c)
+{
+	strbuf_addch(sb->buf, c);
+	sb->width++;
+}
+
+void graph_strbuf_addchars(struct graph_strbuf *sb, int c, size_t n)

graph.c static const char *column_get_color_code(unsigned short color) · line 136

Quoted 10 lines
graph.c +10 -0
+{
+	strbuf_addchars(sb->buf, c, n);
+	sb->width += n;
+}
+
+static inline void graph_strbuf_addstr(struct graph_strbuf *sb, const char *s)
+{
+	strbuf_addstr(sb->buf, s);
+	sb->width += strlen(s);
+}

I'd probably introduce another helper that takes color code and graphbuf (also notice how I name the variables and types; calling something sb that is not a strbuf is misleading and inviting unnecessary bugs):

    static inline void graphbuf_addcolor(struct graphbuf *gb, unsigned short color)
    {
            strbuf_addstr(gb->buf, column_get_color_code(color));
    }

if I were writing this code. The goal is to make any strbuf_add*() that is done directly on gb->buf outside these helpers a bug--that way, grepping for strbuf_add* in this file would become a very easy way to catch such a bug that bypasses the graphbuf_add*() layer and potentially screw up the column counting.

Other than these three points (i.e. naming without "strbuf" that is an implementation detail, embedded vs pointer of strbuf in the graphbuf, and making sure everything can be done via graphbuf_add*() wrappers to make it easier to spot bugs), it seems you are going in the right direction. Thanks for working on this.

#10 lore raw

Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`

Hi,

I just realized that I completely failed to offer my enthusiasm not only for the idea of this patch series and the resulting graphs, but also at the really high quality of the contribution. Thanks, James, for working on this!

And now just quickly...

Quoted 11 lines · Junio C Hamano
Quoted 3 lines · James Coglan

- We only need to consider the width of a small set of characters:
{ | / \ _ - . * }. We don't need to worry about Unicode, and the simple character counting in graph.c was working fine.

I have to warn you that we saw attempts to step outside these ASCII graphics and use Unicode characters for prettier output in the past. If you can do so without too much complexity, I'd suggest you try not to close the door to those people who follow your footsteps to further improve the system by pursuing the avenue further.

That is a very valid point, I think. There are really pretty Unicode characters out there that I could totally see in a nice commit graph.

At that stage, we would have to use int utf8_strnwidth(const char *string, int len, int skip_ansi) anyway (because of the skip_ansi that saves us _a lot_ of work). In this case, I doubt that it makes sense to keep a running tally of the width. It would be faster, or at least similarly fast, to just run utf8_strnwidth() on the final string that we want to measure.

Ciao,
Dscho

#11 lore raw

Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`

In reply to Junio C Hamano
Quoted 6 lines · Junio C Hamano
Quoted 3 lines · James Coglan

- We don't want a general solution to this problem for everything strbuf could be used for; it only needs to address the graph padding problem.

Quoted 10 lines

Of course. Somebody may use strbuf to hold rows of a table and you do not want to contaminate strbuf with fields like width of each column etc, that are very specific to the application. IOW, strbuf is merely _one_ component of a larger solution to each specific problem, and the latter may be things like "struct graphbuf" like Dscho suggested, which might use strbuf as an underlying <byte-string, length> tuple mechanism, but that is an implementation detail that should not be exposed to the users of the struct (and that is why he did not call, and you should not call, the data structure "graph-strbuf" or anything with "strbuf").

Thank you for the clarification. I've now modified my patch to call the wrapper struct graph_line to better reflect its use. This was informed by uncertainty on this thread about whether the width calculation needs to take line breaks into account, so I wanted to name it to indicate it contains a single display line.

Quoted 13 lines
Quoted 1 line

- We only want to count printing characters, not color formatting sequences.

OK. But I'd phrase "count printing characters" as "measure display width" for at least two reasons. Whitespaces are typically counted as non-printing, but you do want to take them into account for this application. Also the graph may not be limited to ASCII graphics forever, and byte- or character-count may not match display width on a fixed-width display.

Quoted 3 lines

- We only need to consider the width of a small set of characters:
{ | / \ _ - . * }. We don't need to worry about Unicode, and the simple character counting in graph.c was working fine.

Quoted 5 lines

I have to warn you that we saw attempts to step outside these ASCII graphics and use Unicode characters for prettier output in the past. If you can do so without too much complexity, I'd suggest you try not to close the door to those people who follow your footsteps to further improve the system by pursuing the avenue further.

That makes sense. All I've done for now is to put the calculations that were already being done behind an interface, consisting of just the operations graph.c actually uses, namely:

void graph_line_addch(struct graph_line *line, int c);

void graph_line_addchars(struct graph_line *line, int c, size_t n);

void graph_line_addstr(struct graph_line *line, const char *s);

void graph_line_write_column(struct graph_line *line, const struct column *c, char col_char);

Having this interface in place should not preclude extending this functionality to Unicode later, and may make it simpler as it puts all the disply width calculations in one place, rather than its current state where it's distributed across all the output functions and makes the assumption that all chars contribute one display column. It especially removes the need for statements like this that encode an assumption about what was printed:

    graph_pad_horizontally(graph, sb, graph->num_columns * 2);
Quoted 13 lines

Review

Quoted 8 lines

To this end I've prepared a different implementation that introduces the indirection described above, and does not modify strbuf:

(quoted diff) +4 -0
+struct graph_strbuf {
+	struct strbuf *buf;
+	size_t width;
+};

Is there a reason why you need a pointer to a strbuf that is allocated separately? E.g. would it make it harder to manage if the above were

Quoted 6 lines
	struct graphbuf {
		struct strbuf buf;
		int width; /* display width in columns */
	};

which is essentially what Dscho suggested?

I used a pointer here because I create the wrapper struct in graph_next_line(), which is an external interface that takes a struct strbuf *:

int graph_next_line(struct git_graph *graph, struct strbuf *sb)

{
	struct graph_line line = { .buf = sb, .width = 0 };
	// ...
}

So I'm not allocating the strbuf here, just wrapping a pointer to it. I would prefer to define graph_line as having a strbuf inline but I've not found a way to do that without breaking the other functions that call graph_next_line(). Although, as far as I know both this wrapper struct and every strbuf it points to are stack-allocated so I'm more concerned about the extra dereference rather than allocation cost.

If there's a way to do this I'm open to suggestions.

Quoted 28 lines
Quoted 7 lines
(quoted diff) +7 -0
+static inline void graph_strbuf_addch(struct graph_strbuf *sb, int c)
+{
+	strbuf_addch(sb->buf, c);
+	sb->width++;
+}
+
+void graph_strbuf_addchars(struct graph_strbuf *sb, int c, size_t n)

Review

Quoted 10 lines
(quoted diff) +10 -0
+{
+	strbuf_addchars(sb->buf, c, n);
+	sb->width += n;
+}
+
+static inline void graph_strbuf_addstr(struct graph_strbuf *sb, const char *s)
+{
+	strbuf_addstr(sb->buf, s);
+	sb->width += strlen(s);
+}

I'd probably introduce another helper that takes color code and graphbuf (also notice how I name the variables and types; calling something sb that is not a strbuf is misleading and inviting unnecessary bugs):

    static inline void graphbuf_addcolor(struct graphbuf *gb, unsigned short color)
    {
            strbuf_addstr(gb->buf, column_get_color_code(color));
    }
Quoted 5 lines

if I were writing this code. The goal is to make any strbuf_add*() that is done directly on gb->buf outside these helpers a bug--that way, grepping for strbuf_add* in this file would become a very easy way to catch such a bug that bypasses the graphbuf_add*() layer and potentially screw up the column counting.

That would be great, I'm just not sure how to fully decouple graph.c from strbuf so that I can properly block it from using the strbuf interface. What I have done is got a better separation between uses of strbuf and graph_line, so that the only functions that still use the strbuf interface are the graph_line interface, and functions that create a strbuf themselves (e.g. graph_show_commit()). Hopefully that separation makes the intent clearer and is a stepping stone to fully decoupling these interfaces.

Quoted 5 lines

Other than these three points (i.e. naming without "strbuf" that is an implementation detail, embedded vs pointer of strbuf in the graphbuf, and making sure everything can be done via graphbuf_add*() wrappers to make it easier to spot bugs), it seems you are going in the right direction. Thanks for working on this.

Thanks! For completeness, here is the current state of this patch after I've been through the feedback up to this point:

From 241180570fbaae04a2263c5ff1ab3b92f54f8004 Mon Sep 17 00:00:00 2001
From: James Coglan <[email protected]>
Date: Thu, 22 Aug 2019 09:30:08 +0100
Subject: [PATCH] graph: automatically track display width of graph lines

All the output functions called by graph_next_line() currently keep track of how many printable chars they've written to the buffer, before calling graph_pad_horizontally() to pad the line with spaces. Some functions do this by incrementing a counter whenever they write to the buffer, and others do it by encoding an assumption about how many chars are written, as in:

    graph_pad_horizontally(graph, sb, graph->num_columns * 2);

This adds a fair amount of noise to the functions' logic and is easily broken if one forgets to increment the right counter or update the calculations used for padding.

To make this easier to use, I'm introducing a new struct called graph_line that wraps a strbuf and keeps count of its display width implicitly. graph_next_line() wraps this around the struct strbuf * it's given and passes a struct graph_line * to the output functions, which use its interface.

The graph_line interface wraps the strbuf_addch(), strbuf_addchars() and strbuf_addstr() functions, and adds the graph_line_write_column() function for adding a single character with color formatting. The graph_pad_horizontally() function can then use the width field from the struct rather than taking a character count as a parameter.

---
 graph.c | 194 +++++++++++++++++++++++++++++---------------------------
 1 file changed, 99 insertions(+), 95 deletions(-)
graph.c +99 -95
index f53135485f..2f81a5d23d 100644
@@ -112,14 +112,42 @@ static const char *column_get_color_code(unsigned short color)
 	return column_colors[color];
 }
 
-static void strbuf_write_column(struct strbuf *sb, const struct column *c,
-				char col_char)
+struct graph_line {
+	struct strbuf *buf;
+	size_t width;
+};
+
+static inline void graph_line_addch(struct graph_line *line, int c)
+{
+	strbuf_addch(line->buf, c);
+	line->width++;
+}
+
+static inline void graph_line_addchars(struct graph_line *line, int c, size_t n)
+{
+	strbuf_addchars(line->buf, c, n);
+	line->width += n;
+}
+
+static inline void graph_line_addstr(struct graph_line *line, const char *s)
+{
+	strbuf_addstr(line->buf, s);
+	line->width += strlen(s);
+}
+
+static inline void graph_line_addcolor(struct graph_line *line, unsigned short color)
+{
+	strbuf_addstr(line->buf, column_get_color_code(color));
+}
+
+static void graph_line_write_column(struct graph_line *line, const struct column *c,
+				    char col_char)
 {
 	if (c->color < column_colors_max)
-		strbuf_addstr(sb, column_get_color_code(c->color));
-	strbuf_addch(sb, col_char);
+		graph_line_addcolor(line, c->color);
+	graph_line_addch(line, col_char);
 	if (c->color < column_colors_max)
-		strbuf_addstr(sb, column_get_color_code(column_colors_max));
+		graph_line_addcolor(line, column_colors_max);
 }
 
 struct git_graph {
@@ -686,8 +714,7 @@ static int graph_is_mapping_correct(struct git_graph *graph)
 	return 1;
 }
 
-static void graph_pad_horizontally(struct git_graph *graph, struct strbuf *sb,
-				   int chars_written)
+static void graph_pad_horizontally(struct git_graph *graph, struct graph_line *line)
 {
 	/*
 	 * Add additional spaces to the end of the strbuf, so that all
@@ -696,12 +723,12 @@ static void graph_pad_horizontally(struct git_graph *graph, struct strbuf *sb,
 	 * This way, fields printed to the right of the graph will remain
 	 * aligned for the entire commit.
 	 */
-	if (chars_written < graph->width)
-		strbuf_addchars(sb, ' ', graph->width - chars_written);
+	if (line->width < graph->width)
+		graph_line_addchars(line, ' ', graph->width - line->width);
 }
 
 static void graph_output_padding_line(struct git_graph *graph,
-				      struct strbuf *sb)
+				      struct graph_line *line)
 {
 	int i;
 
@@ -719,11 +746,11 @@ 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++) {
-		strbuf_write_column(sb, &graph->new_columns[i], '|');
-		strbuf_addch(sb, ' ');
+		graph_line_write_column(line, &graph->new_columns[i], '|');
+		graph_line_addch(line, ' ');
 	}
 
-	graph_pad_horizontally(graph, sb, graph->num_new_columns * 2);
+	graph_pad_horizontally(graph, line);
 }
 
 
@@ -733,14 +760,14 @@ int graph_width(struct git_graph *graph)
 }
 
 
-static void graph_output_skip_line(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_skip_line(struct git_graph *graph, struct graph_line *line)
 {
 	/*
 	 * Output an ellipsis to indicate that a portion
 	 * of the graph is missing.
 	 */
-	strbuf_addstr(sb, "...");
-	graph_pad_horizontally(graph, sb, 3);
+	graph_line_addstr(line, "...");
+	graph_pad_horizontally(graph, line);
 
 	if (graph->num_parents >= 3 &&
 	    graph->commit_index < (graph->num_columns - 1))
@@ -750,11 +777,10 @@ static void graph_output_skip_line(struct git_graph *graph, struct strbuf *sb)
 }
 
 static void graph_output_pre_commit_line(struct git_graph *graph,
-					 struct strbuf *sb)
+					 struct graph_line *line)
 {
 	int num_expansion_rows;
 	int i, seen_this;
-	int chars_written;
 
 	/*
 	 * This function formats a row that increases the space around a commit
@@ -777,14 +803,12 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 	 * Output the row
 	 */
 	seen_this = 0;
-	chars_written = 0;
 	for (i = 0; i < graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		if (col->commit == graph->commit) {
 			seen_this = 1;
-			strbuf_write_column(sb, col, '|');
-			strbuf_addchars(sb, ' ', graph->expansion_row);
-			chars_written += 1 + graph->expansion_row;
+			graph_line_write_column(line, col, '|');
+			graph_line_addchars(line, ' ', graph->expansion_row);
 		} else if (seen_this && (graph->expansion_row == 0)) {
 			/*
 			 * This is the first line of the pre-commit output.
@@ -797,22 +821,18 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 			 */
 			if (graph->prev_state == GRAPH_POST_MERGE &&
 			    graph->prev_commit_index < i)
-				strbuf_write_column(sb, col, '\\');
+				graph_line_write_column(line, col, '\\');
 			else
-				strbuf_write_column(sb, col, '|');
-			chars_written++;
+				graph_line_write_column(line, col, '|');
 		} else if (seen_this && (graph->expansion_row > 0)) {
-			strbuf_write_column(sb, col, '\\');
-			chars_written++;
+			graph_line_write_column(line, col, '\\');
 		} else {
-			strbuf_write_column(sb, col, '|');
-			chars_written++;
+			graph_line_write_column(line, col, '|');
 		}
-		strbuf_addch(sb, ' ');
-		chars_written++;
+		graph_line_addch(line, ' ');
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, line);
 
 	/*
 	 * Increment graph->expansion_row,
@@ -823,7 +843,7 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 		graph_update_state(graph, GRAPH_COMMIT);
 }
 
-static void graph_output_commit_char(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_commit_char(struct git_graph *graph, struct graph_line *line)
 {
 	/*
 	 * For boundary commits, print 'o'
@@ -831,22 +851,20 @@ static void graph_output_commit_char(struct git_graph *graph, struct strbuf *sb)
 	 */
 	if (graph->commit->object.flags & BOUNDARY) {
 		assert(graph->revs->boundary);
-		strbuf_addch(sb, 'o');
+		graph_line_addch(line, 'o');
 		return;
 	}
 
 	/*
 	 * get_revision_mark() handles all other cases without assert()
 	 */
-	strbuf_addstr(sb, get_revision_mark(graph->revs, graph->commit));
+	graph_line_addstr(line, get_revision_mark(graph->revs, graph->commit));
 }
 
 /*
- * Draw the horizontal dashes of an octopus merge and return the number of
- * characters written.
+ * Draw the horizontal dashes of an octopus merge.
  */
-static int graph_draw_octopus_merge(struct git_graph *graph,
-				    struct strbuf *sb)
+static void graph_draw_octopus_merge(struct git_graph *graph, struct graph_line *line)
 {
 	/*
 	 * Here dashless_parents represents the number of parents which don't
@@ -886,17 +904,16 @@ static int graph_draw_octopus_merge(struct git_graph *graph,
 
 	int i;
 	for (i = 0; i < dashful_parents; i++) {
-		strbuf_write_column(sb, &graph->new_columns[i+first_col], '-');
-		strbuf_write_column(sb, &graph->new_columns[i+first_col],
-				    i == dashful_parents-1 ? '.' : '-');
+		graph_line_write_column(line, &graph->new_columns[i+first_col], '-');
+		graph_line_write_column(line, &graph->new_columns[i+first_col],
+					  i == dashful_parents-1 ? '.' : '-');
 	}
-	return 2 * dashful_parents;
 }
 
-static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_commit_line(struct git_graph *graph, struct graph_line *line)
 {
 	int seen_this = 0;
-	int i, chars_written;
+	int i;
 
 	/*
 	 * Output the row containing this commit
@@ -906,7 +923,6 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 	 * children that we have already processed.)
 	 */
 	seen_this = 0;
-	chars_written = 0;
 	for (i = 0; i <= graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		struct commit *col_commit;
@@ -920,15 +936,12 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 
 		if (col_commit == graph->commit) {
 			seen_this = 1;
-			graph_output_commit_char(graph, sb);
-			chars_written++;
+			graph_output_commit_char(graph, line);
 
 			if (graph->num_parents > 2)
-				chars_written += graph_draw_octopus_merge(graph,
-									  sb);
+				graph_draw_octopus_merge(graph, line);
 		} else if (seen_this && (graph->num_parents > 2)) {
-			strbuf_write_column(sb, col, '\\');
-			chars_written++;
+			graph_line_write_column(line, col, '\\');
 		} else if (seen_this && (graph->num_parents == 2)) {
 			/*
 			 * This is a 2-way merge commit.
@@ -945,19 +958,16 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 			 */
 			if (graph->prev_state == GRAPH_POST_MERGE &&
 			    graph->prev_commit_index < i)
-				strbuf_write_column(sb, col, '\\');
+				graph_line_write_column(line, col, '\\');
 			else
-				strbuf_write_column(sb, col, '|');
-			chars_written++;
+				graph_line_write_column(line, col, '|');
 		} else {
-			strbuf_write_column(sb, col, '|');
-			chars_written++;
+			graph_line_write_column(line, col, '|');
 		}
-		strbuf_addch(sb, ' ');
-		chars_written++;
+		graph_line_addch(line, ' ');
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, line);
 
 	/*
 	 * Update graph->state
@@ -981,15 +991,14 @@ static struct column *find_new_column_by_commit(struct git_graph *graph,
 	return NULL;
 }
 
-static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_post_merge_line(struct git_graph *graph, struct graph_line *line)
 {
 	int seen_this = 0;
-	int i, j, chars_written;
+	int i, j;
 
 	/*
 	 * Output the post-merge row
 	 */
-	chars_written = 0;
 	for (i = 0; i <= graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		struct commit *col_commit;
@@ -1016,29 +1025,25 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf
 			par_column = find_new_column_by_commit(graph, parents->item);
 			assert(par_column);
 
-			strbuf_write_column(sb, par_column, '|');
-			chars_written++;
+			graph_line_write_column(line, par_column, '|');
 			for (j = 0; j < graph->num_parents - 1; j++) {
 				parents = next_interesting_parent(graph, parents);
 				assert(parents);
 				par_column = find_new_column_by_commit(graph, parents->item);
 				assert(par_column);
-				strbuf_write_column(sb, par_column, '\\');
-				strbuf_addch(sb, ' ');
+				graph_line_write_column(line, par_column, '\\');
+				graph_line_addch(line, ' ');
 			}
-			chars_written += j * 2;
 		} else if (seen_this) {
-			strbuf_write_column(sb, col, '\\');
-			strbuf_addch(sb, ' ');
-			chars_written += 2;
+			graph_line_write_column(line, col, '\\');
+			graph_line_addch(line, ' ');
 		} else {
-			strbuf_write_column(sb, col, '|');
-			strbuf_addch(sb, ' ');
-			chars_written += 2;
+			graph_line_write_column(line, col, '|');
+			graph_line_addch(line, ' ');
 		}
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, line);
 
 	/*
 	 * Update graph->state
@@ -1049,7 +1054,7 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf
 		graph_update_state(graph, GRAPH_COLLAPSING);
 }
 
-static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_collapsing_line(struct git_graph *graph, struct graph_line *line)
 {
 	int i;
 	short used_horizontal = 0;
@@ -1159,9 +1164,9 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 	for (i = 0; i < graph->mapping_size; i++) {
 		int target = graph->new_mapping[i];
 		if (target < 0)
-			strbuf_addch(sb, ' ');
+			graph_line_addch(line, ' ');
 		else if (target * 2 == i)
-			strbuf_write_column(sb, &graph->new_columns[target], '|');
+			graph_line_write_column(line, &graph->new_columns[target], '|');
 		else if (target == horizontal_edge_target &&
 			 i != horizontal_edge - 1) {
 				/*
@@ -1172,16 +1177,16 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 				if (i != (target * 2)+3)
 					graph->new_mapping[i] = -1;
 				used_horizontal = 1;
-			strbuf_write_column(sb, &graph->new_columns[target], '_');
+			graph_line_write_column(line, &graph->new_columns[target], '_');
 		} else {
 			if (used_horizontal && i < horizontal_edge)
 				graph->new_mapping[i] = -1;
-			strbuf_write_column(sb, &graph->new_columns[target], '/');
+			graph_line_write_column(line, &graph->new_columns[target], '/');
 
 		}
 	}
 
-	graph_pad_horizontally(graph, sb, graph->mapping_size);
+	graph_pad_horizontally(graph, line);
 
 	/*
 	 * Swap mapping and new_mapping
@@ -1199,24 +1204,26 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 
 int graph_next_line(struct git_graph *graph, struct strbuf *sb)
 {
+	struct graph_line line = { .buf = sb, .width = 0 };
+
 	switch (graph->state) {
 	case GRAPH_PADDING:
-		graph_output_padding_line(graph, sb);
+		graph_output_padding_line(graph, &line);
 		return 0;
 	case GRAPH_SKIP:
-		graph_output_skip_line(graph, sb);
+		graph_output_skip_line(graph, &line);
 		return 0;
 	case GRAPH_PRE_COMMIT:
-		graph_output_pre_commit_line(graph, sb);
+		graph_output_pre_commit_line(graph, &line);
 		return 0;
 	case GRAPH_COMMIT:
-		graph_output_commit_line(graph, sb);
+		graph_output_commit_line(graph, &line);
 		return 1;
 	case GRAPH_POST_MERGE:
-		graph_output_post_merge_line(graph, sb);
+		graph_output_post_merge_line(graph, &line);
 		return 0;
 	case GRAPH_COLLAPSING:
-		graph_output_collapsing_line(graph, sb);
+		graph_output_collapsing_line(graph, &line);
 		return 0;
 	}
 
@@ -1227,7 +1234,7 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb)
 static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)
 {
 	int i;
-	int chars_written = 0;
+	struct graph_line line = { .buf = sb, .width = 0 };
 
 	if (graph->state != GRAPH_COMMIT) {
 		graph_next_line(graph, sb);
@@ -1244,20 +1251,17 @@ 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];
 
-		strbuf_write_column(sb, col, '|');
-		chars_written++;
+		graph_line_write_column(&line, col, '|');
 
 		if (col->commit == graph->commit && graph->num_parents > 2) {
 			int len = (graph->num_parents - 2) * 2;
-			strbuf_addchars(sb, ' ', len);
-			chars_written += len;
+			graph_line_addchars(&line, ' ', len);
 		} else {
-			strbuf_addch(sb, ' ');
-			chars_written++;
+			graph_line_addch(&line, ' ');
 		}
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, &line);
 
 	/*
 	 * Update graph->prev_state since we have output a padding line
Signature · 1 line
2.23.0
#12 lore raw

Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`

Quoted 473 lines · James Coglan

Thanks! For completeness, here is the current state of this patch after I've been through the feedback up to this point:

From 241180570fbaae04a2263c5ff1ab3b92f54f8004 Mon Sep 17 00:00:00 2001
From: James Coglan <[email protected]>
Date: Thu, 22 Aug 2019 09:30:08 +0100
Subject: [PATCH] graph: automatically track display width of graph lines

All the output functions called by graph_next_line() currently keep track of how many printable chars they've written to the buffer, before calling graph_pad_horizontally() to pad the line with spaces. Some functions do this by incrementing a counter whenever they write to the buffer, and others do it by encoding an assumption about how many chars are written, as in:

    graph_pad_horizontally(graph, sb, graph->num_columns * 2);

This adds a fair amount of noise to the functions' logic and is easily broken if one forgets to increment the right counter or update the calculations used for padding.

To make this easier to use, I'm introducing a new struct called graph_line that wraps a strbuf and keeps count of its display width implicitly. graph_next_line() wraps this around the struct strbuf * it's given and passes a struct graph_line * to the output functions, which use its interface.

The graph_line interface wraps the strbuf_addch(), strbuf_addchars() and strbuf_addstr() functions, and adds the graph_line_write_column() function for adding a single character with color formatting. The graph_pad_horizontally() function can then use the width field from the struct rather than taking a character count as a parameter.

---
 graph.c | 194 +++++++++++++++++++++++++++++---------------------------
 1 file changed, 99 insertions(+), 95 deletions(-)
graph.c +97 -93
index f53135485f..2f81a5d23d 100644
@@ -112,14 +112,42 @@ static const char *column_get_color_code(unsigned short color)
 	return column_colors[color];
 }
 
-static void strbuf_write_column(struct strbuf *sb, const struct column *c,
-				char col_char)
+struct graph_line {
+	struct strbuf *buf;
+	size_t width;
+};
+
+static inline void graph_line_addch(struct graph_line *line, int c)
+{
+	strbuf_addch(line->buf, c);
+	line->width++;
+}
+
+static inline void graph_line_addchars(struct graph_line *line, int c, size_t n)
+{
+	strbuf_addchars(line->buf, c, n);
+	line->width += n;
+}
+
+static inline void graph_line_addstr(struct graph_line *line, const char *s)
+{
+	strbuf_addstr(line->buf, s);
+	line->width += strlen(s);
+}
+
+static inline void graph_line_addcolor(struct graph_line *line, unsigned short color)
+{
+	strbuf_addstr(line->buf, column_get_color_code(color));
+}
+
+static void graph_line_write_column(struct graph_line *line, const struct column *c,
+				    char col_char)
 {
 	if (c->color < column_colors_max)
-		strbuf_addstr(sb, column_get_color_code(c->color));
-	strbuf_addch(sb, col_char);
+		graph_line_addcolor(line, c->color);
+	graph_line_addch(line, col_char);
 	if (c->color < column_colors_max)
-		strbuf_addstr(sb, column_get_color_code(column_colors_max));
+		graph_line_addcolor(line, column_colors_max);
 }
 
 struct git_graph {
@@ -686,8 +714,7 @@ static int graph_is_mapping_correct(struct git_graph *graph)
 	return 1;
 }
 
-static void graph_pad_horizontally(struct git_graph *graph, struct strbuf *sb,
-				   int chars_written)
+static void graph_pad_horizontally(struct git_graph *graph, struct graph_line *line)
 {
 	/*
 	 * Add additional spaces to the end of the strbuf, so that all
@@ -696,12 +723,12 @@ static void graph_pad_horizontally(struct git_graph *graph, struct strbuf *sb,
 	 * This way, fields printed to the right of the graph will remain
 	 * aligned for the entire commit.
 	 */
-	if (chars_written < graph->width)
-		strbuf_addchars(sb, ' ', graph->width - chars_written);
+	if (line->width < graph->width)
+		graph_line_addchars(line, ' ', graph->width - line->width);
 }
 
 static void graph_output_padding_line(struct git_graph *graph,
-				      struct strbuf *sb)
+				      struct graph_line *line)
 {
 	int i;
 
@@ -719,11 +746,11 @@ 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++) {
-		strbuf_write_column(sb, &graph->new_columns[i], '|');
-		strbuf_addch(sb, ' ');
+		graph_line_write_column(line, &graph->new_columns[i], '|');
+		graph_line_addch(line, ' ');
 	}
 
-	graph_pad_horizontally(graph, sb, graph->num_new_columns * 2);
+	graph_pad_horizontally(graph, line);
 }
 
 
@@ -733,14 +760,14 @@ int graph_width(struct git_graph *graph)
 }
 
 
-static void graph_output_skip_line(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_skip_line(struct git_graph *graph, struct graph_line *line)
 {
 	/*
 	 * Output an ellipsis to indicate that a portion
 	 * of the graph is missing.
 	 */
-	strbuf_addstr(sb, "...");
-	graph_pad_horizontally(graph, sb, 3);
+	graph_line_addstr(line, "...");
+	graph_pad_horizontally(graph, line);
 
 	if (graph->num_parents >= 3 &&
 	    graph->commit_index < (graph->num_columns - 1))
@@ -750,11 +777,10 @@ static void graph_output_skip_line(struct git_graph *graph, struct strbuf *sb)
 }
 
 static void graph_output_pre_commit_line(struct git_graph *graph,
-					 struct strbuf *sb)
+					 struct graph_line *line)
 {
 	int num_expansion_rows;
 	int i, seen_this;
-	int chars_written;
 
 	/*
 	 * This function formats a row that increases the space around a commit
@@ -777,14 +803,12 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 	 * Output the row
 	 */
 	seen_this = 0;
-	chars_written = 0;
 	for (i = 0; i < graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		if (col->commit == graph->commit) {
 			seen_this = 1;
-			strbuf_write_column(sb, col, '|');
-			strbuf_addchars(sb, ' ', graph->expansion_row);
-			chars_written += 1 + graph->expansion_row;
+			graph_line_write_column(line, col, '|');
+			graph_line_addchars(line, ' ', graph->expansion_row);
 		} else if (seen_this && (graph->expansion_row == 0)) {
 			/*
 			 * This is the first line of the pre-commit output.
@@ -797,22 +821,18 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 			 */
 			if (graph->prev_state == GRAPH_POST_MERGE &&
 			    graph->prev_commit_index < i)
-				strbuf_write_column(sb, col, '\\');
+				graph_line_write_column(line, col, '\\');
 			else
-				strbuf_write_column(sb, col, '|');
-			chars_written++;
+				graph_line_write_column(line, col, '|');
 		} else if (seen_this && (graph->expansion_row > 0)) {
-			strbuf_write_column(sb, col, '\\');
-			chars_written++;
+			graph_line_write_column(line, col, '\\');
 		} else {
-			strbuf_write_column(sb, col, '|');
-			chars_written++;
+			graph_line_write_column(line, col, '|');
 		}
-		strbuf_addch(sb, ' ');
-		chars_written++;
+		graph_line_addch(line, ' ');
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, line);
 
 	/*
 	 * Increment graph->expansion_row,
@@ -823,7 +843,7 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 		graph_update_state(graph, GRAPH_COMMIT);
 }
 
-static void graph_output_commit_char(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_commit_char(struct git_graph *graph, struct graph_line *line)
 {
 	/*
 	 * For boundary commits, print 'o'
@@ -831,22 +851,20 @@ static void graph_output_commit_char(struct git_graph *graph, struct strbuf *sb)
 	 */
 	if (graph->commit->object.flags & BOUNDARY) {
 		assert(graph->revs->boundary);
-		strbuf_addch(sb, 'o');
+		graph_line_addch(line, 'o');
 		return;
 	}
 
 	/*
 	 * get_revision_mark() handles all other cases without assert()
 	 */
-	strbuf_addstr(sb, get_revision_mark(graph->revs, graph->commit));
+	graph_line_addstr(line, get_revision_mark(graph->revs, graph->commit));
 }
 
 /*
- * Draw the horizontal dashes of an octopus merge and return the number of
- * characters written.
+ * Draw the horizontal dashes of an octopus merge.
  */
-static int graph_draw_octopus_merge(struct git_graph *graph,
-				    struct strbuf *sb)
+static void graph_draw_octopus_merge(struct git_graph *graph, struct graph_line *line)
 {
 	/*
 	 * Here dashless_parents represents the number of parents which don't
@@ -886,17 +904,16 @@ static int graph_draw_octopus_merge(struct git_graph *graph,
 
 	int i;
 	for (i = 0; i < dashful_parents; i++) {
-		strbuf_write_column(sb, &graph->new_columns[i+first_col], '-');
-		strbuf_write_column(sb, &graph->new_columns[i+first_col],
-				    i == dashful_parents-1 ? '.' : '-');
+		graph_line_write_column(line, &graph->new_columns[i+first_col], '-');
+		graph_line_write_column(line, &graph->new_columns[i+first_col],
+					  i == dashful_parents-1 ? '.' : '-');
 	}
-	return 2 * dashful_parents;
 }
 
-static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_commit_line(struct git_graph *graph, struct graph_line *line)
 {
 	int seen_this = 0;
-	int i, chars_written;
+	int i;
 
 	/*
 	 * Output the row containing this commit
@@ -906,7 +923,6 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 	 * children that we have already processed.)
 	 */
 	seen_this = 0;
-	chars_written = 0;
 	for (i = 0; i <= graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		struct commit *col_commit;
@@ -920,15 +936,12 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 
 		if (col_commit == graph->commit) {
 			seen_this = 1;
-			graph_output_commit_char(graph, sb);
-			chars_written++;
+			graph_output_commit_char(graph, line);
 
 			if (graph->num_parents > 2)
-				chars_written += graph_draw_octopus_merge(graph,
-									  sb);
+				graph_draw_octopus_merge(graph, line);
 		} else if (seen_this && (graph->num_parents > 2)) {
-			strbuf_write_column(sb, col, '\\');
-			chars_written++;
+			graph_line_write_column(line, col, '\\');
 		} else if (seen_this && (graph->num_parents == 2)) {
 			/*
 			 * This is a 2-way merge commit.
@@ -945,19 +958,16 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 			 */
 			if (graph->prev_state == GRAPH_POST_MERGE &&
 			    graph->prev_commit_index < i)
-				strbuf_write_column(sb, col, '\\');
+				graph_line_write_column(line, col, '\\');
 			else
-				strbuf_write_column(sb, col, '|');
-			chars_written++;
+				graph_line_write_column(line, col, '|');
 		} else {
-			strbuf_write_column(sb, col, '|');
-			chars_written++;
+			graph_line_write_column(line, col, '|');
 		}
-		strbuf_addch(sb, ' ');
-		chars_written++;
+		graph_line_addch(line, ' ');
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, line);
 
 	/*
 	 * Update graph->state
@@ -981,15 +991,14 @@ static struct column *find_new_column_by_commit(struct git_graph *graph,
 	return NULL;
 }
 
-static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_post_merge_line(struct git_graph *graph, struct graph_line *line)
 {
 	int seen_this = 0;
-	int i, j, chars_written;
+	int i, j;
 
 	/*
 	 * Output the post-merge row
 	 */
-	chars_written = 0;
 	for (i = 0; i <= graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		struct commit *col_commit;
@@ -1016,29 +1025,25 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf
 			par_column = find_new_column_by_commit(graph, parents->item);
 			assert(par_column);
 
-			strbuf_write_column(sb, par_column, '|');
-			chars_written++;
+			graph_line_write_column(line, par_column, '|');
 			for (j = 0; j < graph->num_parents - 1; j++) {
 				parents = next_interesting_parent(graph, parents);
 				assert(parents);
 				par_column = find_new_column_by_commit(graph, parents->item);
 				assert(par_column);
-				strbuf_write_column(sb, par_column, '\\');
-				strbuf_addch(sb, ' ');
+				graph_line_write_column(line, par_column, '\\');
+				graph_line_addch(line, ' ');
 			}
-			chars_written += j * 2;
 		} else if (seen_this) {
-			strbuf_write_column(sb, col, '\\');
-			strbuf_addch(sb, ' ');
-			chars_written += 2;
+			graph_line_write_column(line, col, '\\');
+			graph_line_addch(line, ' ');
 		} else {
-			strbuf_write_column(sb, col, '|');
-			strbuf_addch(sb, ' ');
-			chars_written += 2;
+			graph_line_write_column(line, col, '|');
+			graph_line_addch(line, ' ');
 		}
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, line);
 
 	/*
 	 * Update graph->state
@@ -1049,7 +1054,7 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf
 		graph_update_state(graph, GRAPH_COLLAPSING);
 }
 
-static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_collapsing_line(struct git_graph *graph, struct graph_line *line)
 {
 	int i;
 	short used_horizontal = 0;
@@ -1159,9 +1164,9 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 	for (i = 0; i < graph->mapping_size; i++) {
 		int target = graph->new_mapping[i];
 		if (target < 0)
-			strbuf_addch(sb, ' ');
+			graph_line_addch(line, ' ');
 		else if (target * 2 == i)
-			strbuf_write_column(sb, &graph->new_columns[target], '|');
+			graph_line_write_column(line, &graph->new_columns[target], '|');
 		else if (target == horizontal_edge_target &&
 			 i != horizontal_edge - 1) {
 				/*
@@ -1172,16 +1177,16 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 				if (i != (target * 2)+3)
 					graph->new_mapping[i] = -1;
 				used_horizontal = 1;
-			strbuf_write_column(sb, &graph->new_columns[target], '_');
+			graph_line_write_column(line, &graph->new_columns[target], '_');
 		} else {
 			if (used_horizontal && i < horizontal_edge)
 				graph->new_mapping[i] = -1;
-			strbuf_write_column(sb, &graph->new_columns[target], '/');
+			graph_line_write_column(line, &graph->new_columns[target], '/');
 
 		}
 	}
 
-	graph_pad_horizontally(graph, sb, graph->mapping_size);
+	graph_pad_horizontally(graph, line);
 
 	/*
 	 * Swap mapping and new_mapping
@@ -1199,24 +1204,26 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 
 int graph_next_line(struct git_graph *graph, struct strbuf *sb)
 {
+	struct graph_line line = { .buf = sb, .width = 0 };
+
 	switch (graph->state) {
 	case GRAPH_PADDING:
-		graph_output_padding_line(graph, sb);
+		graph_output_padding_line(graph, &line);
 		return 0;
 	case GRAPH_SKIP:
-		graph_output_skip_line(graph, sb);
+		graph_output_skip_line(graph, &line);
 		return 0;
 	case GRAPH_PRE_COMMIT:
-		graph_output_pre_commit_line(graph, sb);
+		graph_output_pre_commit_line(graph, &line);
 		return 0;
 	case GRAPH_COMMIT:
-		graph_output_commit_line(graph, sb);
+		graph_output_commit_line(graph, &line);
 		return 1;
 	case GRAPH_POST_MERGE:
-		graph_output_post_merge_line(graph, sb);
+		graph_output_post_merge_line(graph, &line);
 		return 0;
 	case GRAPH_COLLAPSING:
-		graph_output_collapsing_line(graph, sb);
+		graph_output_collapsing_line(graph, &line);
 		return 0;
 	}
 
@@ -1227,7 +1234,7 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb)
 static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)
 {
 	int i;
-	int chars_written = 0;
+	struct graph_line line = { .buf = sb, .width = 0 };
 
 	if (graph->state != GRAPH_COMMIT) {
 		graph_next_line(graph, sb);
@@ -1244,20 +1251,17 @@ 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];
 
-		strbuf_write_column(sb, col, '|');
-		chars_written++;
+		graph_line_write_column(&line, col, '|');
 
 		if (col->commit == graph->commit && graph->num_parents > 2) {
 			int len = (graph->num_parents - 2) * 2;
-			strbuf_addchars(sb, ' ', len);
-			chars_written += len;
+			graph_line_addchars(&line, ' ', len);
 		} else {
-			strbuf_addch(sb, ' ');

graph.c static void graph_padding_line(struct git_graph *graph, struct strbuf *sb) · line 1267

Quoted 10 lines
graph.c +2 -2
-			chars_written++;
+			graph_line_addch(&line, ' ');
 		}
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, &line);
 
 	/*
 	 * Update graph->prev_state since we have output a padding line

In addition to this, I'd like to include the following patch that moves the graph_pad_horizontally() calls. By the way, what's the best way for me to submit the whole updated patch series after responding to all feedback?

From 97d230c8a8d9b677b331e37482039384589b096a Mon Sep 17 00:00:00 2001
From: James Coglan <[email protected]>
Date: Mon, 14 Oct 2019 12:10:06 +0100
Subject: [PATCH] graph: handle line padding in graph_next_line()

Now that the display width of graph lines is implicitly tracked via the graph_line interface, the calls to graph_pad_horizontally() no longer need to be located inside the individual output functions, where the character counting was previously being done.

All the functions called by graph_next_line() generate a line of output, then call graph_pad_horizontally(), and finally change the graph state if necessary. As padding is the final change to the output done by all these functions, it can be removed from all of them and done in graph_next_line() instead.

I've also moved the guard in graph_output_padding_line() that checks the graph has a commit; this function is only called by graph_next_line() and we must not pad the graph_line if no commit is set.

---
 graph.c | 49 ++++++++++++++++++++-----------------------------
 1 file changed, 20 insertions(+), 29 deletions(-)
graph.c +20 -29
index 2f81a5d23d..4c68557b17 100644
@@ -732,16 +732,6 @@ static void graph_output_padding_line(struct git_graph *graph,
 {
 	int i;
 
-	/*
-	 * We could conceivable be called with a NULL commit
-	 * if our caller has a bug, and invokes graph_next_line()
-	 * immediately after graph_init(), without first calling
-	 * graph_update().  Return without outputting anything in this
-	 * case.
-	 */
-	if (!graph->commit)
-		return;
-
 	/*
 	 * Output a padding row, that leaves all branch lines unchanged
 	 */
@@ -749,8 +739,6 @@ static void graph_output_padding_line(struct git_graph *graph,
 		graph_line_write_column(line, &graph->new_columns[i], '|');
 		graph_line_addch(line, ' ');
 	}
-
-	graph_pad_horizontally(graph, line);
 }
 
 
@@ -767,7 +755,6 @@ static void graph_output_skip_line(struct git_graph *graph, struct graph_line *l
 	 * of the graph is missing.
 	 */
 	graph_line_addstr(line, "...");
-	graph_pad_horizontally(graph, line);
 
 	if (graph->num_parents >= 3 &&
 	    graph->commit_index < (graph->num_columns - 1))
@@ -832,8 +819,6 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 		graph_line_addch(line, ' ');
 	}
 
-	graph_pad_horizontally(graph, line);
-
 	/*
 	 * Increment graph->expansion_row,
 	 * and move to state GRAPH_COMMIT if necessary
@@ -967,8 +952,6 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line
 		graph_line_addch(line, ' ');
 	}
 
-	graph_pad_horizontally(graph, line);
-
 	/*
 	 * Update graph->state
 	 */
@@ -1043,8 +1026,6 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l
 		}
 	}
 
-	graph_pad_horizontally(graph, line);
-
 	/*
 	 * Update graph->state
 	 */
@@ -1186,8 +1167,6 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
 		}
 	}
 
-	graph_pad_horizontally(graph, line);
-
 	/*
 	 * Swap mapping and new_mapping
 	 */
@@ -1204,31 +1183,43 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
 
 int graph_next_line(struct git_graph *graph, struct strbuf *sb)
 {
+	int shown_commit_line = 0;
 	struct graph_line line = { .buf = sb, .width = 0 };
 
+	/*
+	 * We could conceivable be called with a NULL commit
+	 * if our caller has a bug, and invokes graph_next_line()
+	 * immediately after graph_init(), without first calling
+	 * graph_update().  Return without outputting anything in this
+	 * case.
+	 */
+	if (!graph->commit)
+		return -1;
+
 	switch (graph->state) {
 	case GRAPH_PADDING:
 		graph_output_padding_line(graph, &line);
-		return 0;
+		break;
 	case GRAPH_SKIP:
 		graph_output_skip_line(graph, &line);
-		return 0;
+		break;
 	case GRAPH_PRE_COMMIT:
 		graph_output_pre_commit_line(graph, &line);
-		return 0;
+		break;
 	case GRAPH_COMMIT:
 		graph_output_commit_line(graph, &line);
-		return 1;
+		shown_commit_line = 1;
+		break;
 	case GRAPH_POST_MERGE:
 		graph_output_post_merge_line(graph, &line);
-		return 0;
+		break;
 	case GRAPH_COLLAPSING:
 		graph_output_collapsing_line(graph, &line);
-		return 0;
+		break;
 	}
 
-	assert(0);
-	return 0;
+	graph_pad_horizontally(graph, &line);
+	return shown_commit_line;
 }
 
 static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)
Signature · 1 line
2.23.0
#13 lore raw

Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`

In reply to James Coglan
Quoted 15 lines · James Coglan
Quoted 10 lines

Is there a reason why you need a pointer to a strbuf that is allocated separately? E.g. would it make it harder to manage if the above were

	struct graphbuf {
		struct strbuf buf;
		int width; /* display width in columns */
	};

which is essentially what Dscho suggested?

I used a pointer here because I create the wrapper struct in graph_next_line(), which is an external interface that takes a struct strbuf *:

Quoted 8 lines

int graph_next_line(struct git_graph *graph, struct strbuf *sb)

{
	struct graph_line line = { .buf = sb, .width = 0 };
	// ...
}

So I'm not allocating the strbuf here, just wrapping a pointer to it.

OK, so existing callers allocate strbuf, and you are merely adding a wrapper structure to keep track of the width. The management of the lifetime of the strbuf is not your business so there is no reason to inline the structure in graph_line. Makes sense. Thanks.

#14 lore raw

Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`

In reply to Johannes Schindelin
Quoted 8 lines · Johannes Schindelin

This is a big heavy-handed: adding a width field to struct strbuf and maintaining it _just_ for the purpose of graph.c puts an unnecssary load on every other strbuf user (of which there are a _lot_).

So my obvious question is: what makes width different from len? Since we exclusively use ASCII characters for the graph part, we should be able to use the already-existing len, for free, no?

A red-colored piece on the line consumes <RED><RESET> bytes more than the payload. Which is counted as part of "len". These bytes do not consume any display width.

When the payload is a basic CJK char in UTF-8 it may typically use 3 byte, while consuming only two display columns.

So I can understand that this application may want to keep track of <byte sequence, byte sequence length, display width> tuple.

I also understand that a programmer inexperienced/unfamiliar with our codebase may find it an easy way to satisfiy the need to add an extra field to strbuf. But as you pointed out, that is a hack unacceptable in the larger picture. Use of strbuf as "auto resizing byte array, represented as a <byte sequence, byte sequence length> tuple" is everywhere and we do not want to bloat it.

Thanks for spotting and raising this unfortunate show-stopper issue. The problem being solved is worth solving, but it needs to be done without butchering a basic data structure used elsewhere.

#15 lore raw

Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`

In reply to Johannes Schindelin

Hi Johannes,

Quoted 19 lines · Johannes Schindelin

Hi James,

Quoted 15 lines · James Coglan via GitGitGadget

From: James Coglan <[email protected]>

All the output functions in graph.c currently keep track of how many printable chars they've written to the buffer, before calling graph_pad_horizontally() to pad the line with spaces. Some functions do this by incrementing a counter whenever they write to the buffer, and others do it by encoding an assumption about how many chars are written, as in:

    graph_pad_horizontally(graph, sb, graph->num_columns * 2);

This adds a fair amount of noise to the functions' logic and is easily broken if one forgets to increment the right counter or update the calculations used for padding.

Quoted 7 lines
Quoted 2 lines

To make this easier to use, I'm adding a width field to strbuf that tracks the number of printing characters added after the line prefix.

This is a big heavy-handed: adding a width field to struct strbuf and maintaining it _just_ for the purpose of graph.c puts an unnecssary load on every other strbuf user (of which there are a _lot_).

I was anticipating there might be objections to modifying a widely used struct. There are other idea I had for solving this that I can share -- I'll post an alternative to this patch shortly that does not involve changing strbuf.

Quoted 3 lines

So my obvious question is: what makes width different from len? Since we exclusively use ASCII characters for the graph part, we should be able to use the already-existing len, for free, no?

len counts the number of bytes in the buffer, which may include non-printing bytes used to apply color formatting. For padding graph lines, we need to know the number of display columns the buffer will use, and that's what width was added for.

Quoted 3 lines

I could imagine that the strbuf might receive more than one line, but then we still would only need to remember the offset of the last newline character in that strbuf, no?

As far as I know, it does not contain multiple lines when used in graph.c, but I agree the implementation I've submitted here is obviously wrong if you wanted to use it to get the width of multi-line text. Putting code in strbuf that only works in graph.c and not more broadly was probably a mistake on my part.

Quoted 332 lines
Quoted 332 lines

It's set to 0 at the start of graph_next_line(), and then various strbuf functions update it as follows:

- strbuf_write_column() increments width by 1

- strbuf_setlen() changes width by the amount added to len if len is increased, or makes width and len the same if it's decreased

- strbuf_addch() increments width by 1

This is enough to ensure that the functions used by graph.c update strbuf->width correctly, and graph_pad_horizontally() can then use this field instead of taking chars_written as a parameter.

---
 graph.c  | 68 ++++++++++++++++++++++----------------------------------
 strbuf.h |  8 ++++++-
 2 files changed, 33 insertions(+), 43 deletions(-)
graph.c +26 -42
index f53135485f..c56fdec1fc 100644
@@ -115,11 +115,20 @@ static const char *column_get_color_code(unsigned short color)
 static void strbuf_write_column(struct strbuf *sb, const struct column *c,
 				char col_char)
 {
+	/*
+	 * Remember the buffer's width as we're about to add non-printing
+	 * content to it, and we want to avoid counting the byte length
+	 * of this content towards the buffer's visible width
+	 */
+	size_t prev_width = sb->width;
+
 	if (c->color < column_colors_max)
 		strbuf_addstr(sb, column_get_color_code(c->color));
 	strbuf_addch(sb, col_char);
 	if (c->color < column_colors_max)
 		strbuf_addstr(sb, column_get_color_code(column_colors_max));
+
+	sb->width = prev_width + 1;
 }
 
 struct git_graph {
@@ -686,8 +695,7 @@ static int graph_is_mapping_correct(struct git_graph *graph)
 	return 1;
 }
 
-static void graph_pad_horizontally(struct git_graph *graph, struct strbuf *sb,
-				   int chars_written)
+static void graph_pad_horizontally(struct git_graph *graph, struct strbuf *sb)
 {
 	/*
 	 * Add additional spaces to the end of the strbuf, so that all
@@ -696,8 +704,8 @@ static void graph_pad_horizontally(struct git_graph *graph, struct strbuf *sb,
 	 * This way, fields printed to the right of the graph will remain
 	 * aligned for the entire commit.
 	 */
-	if (chars_written < graph->width)
-		strbuf_addchars(sb, ' ', graph->width - chars_written);
+	if (sb->width < graph->width)
+		strbuf_addchars(sb, ' ', graph->width - sb->width);
 }
 
 static void graph_output_padding_line(struct git_graph *graph,
@@ -723,7 +731,7 @@ static void graph_output_padding_line(struct git_graph *graph,
 		strbuf_addch(sb, ' ');
 	}
 
-	graph_pad_horizontally(graph, sb, graph->num_new_columns * 2);
+	graph_pad_horizontally(graph, sb);
 }
 
 
@@ -740,7 +748,7 @@ static void graph_output_skip_line(struct git_graph *graph, struct strbuf *sb)
 	 * of the graph is missing.
 	 */
 	strbuf_addstr(sb, "...");
-	graph_pad_horizontally(graph, sb, 3);
+	graph_pad_horizontally(graph, sb);
 
 	if (graph->num_parents >= 3 &&
 	    graph->commit_index < (graph->num_columns - 1))
@@ -754,7 +762,6 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 {
 	int num_expansion_rows;
 	int i, seen_this;
-	int chars_written;
 
 	/*
 	 * This function formats a row that increases the space around a commit
@@ -777,14 +784,12 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 	 * Output the row
 	 */
 	seen_this = 0;
-	chars_written = 0;
 	for (i = 0; i < graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		if (col->commit == graph->commit) {
 			seen_this = 1;
 			strbuf_write_column(sb, col, '|');
 			strbuf_addchars(sb, ' ', graph->expansion_row);
-			chars_written += 1 + graph->expansion_row;
 		} else if (seen_this && (graph->expansion_row == 0)) {
 			/*
 			 * This is the first line of the pre-commit output.
@@ -800,19 +805,15 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 				strbuf_write_column(sb, col, '\\');
 			else
 				strbuf_write_column(sb, col, '|');
-			chars_written++;
 		} else if (seen_this && (graph->expansion_row > 0)) {
 			strbuf_write_column(sb, col, '\\');
-			chars_written++;
 		} else {
 			strbuf_write_column(sb, col, '|');
-			chars_written++;
 		}
 		strbuf_addch(sb, ' ');
-		chars_written++;
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, sb);
 
 	/*
 	 * Increment graph->expansion_row,
@@ -842,11 +843,9 @@ static void graph_output_commit_char(struct git_graph *graph, struct strbuf *sb)
 }
 
 /*
- * Draw the horizontal dashes of an octopus merge and return the number of
- * characters written.
+ * Draw the horizontal dashes of an octopus merge.
  */
-static int graph_draw_octopus_merge(struct git_graph *graph,
-				    struct strbuf *sb)
+static void graph_draw_octopus_merge(struct git_graph *graph, struct strbuf *sb)
 {
 	/*
 	 * Here dashless_parents represents the number of parents which don't
@@ -890,13 +889,12 @@ static int graph_draw_octopus_merge(struct git_graph *graph,
 		strbuf_write_column(sb, &graph->new_columns[i+first_col],
 				    i == dashful_parents-1 ? '.' : '-');
 	}
-	return 2 * dashful_parents;
 }
 
 static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 {
 	int seen_this = 0;
-	int i, chars_written;
+	int i;
 
 	/*
 	 * Output the row containing this commit
@@ -906,7 +904,6 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 	 * children that we have already processed.)
 	 */
 	seen_this = 0;
-	chars_written = 0;
 	for (i = 0; i <= graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		struct commit *col_commit;
@@ -921,14 +918,11 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 		if (col_commit == graph->commit) {
 			seen_this = 1;
 			graph_output_commit_char(graph, sb);
-			chars_written++;
 
 			if (graph->num_parents > 2)
-				chars_written += graph_draw_octopus_merge(graph,
-									  sb);
+				graph_draw_octopus_merge(graph, sb);
 		} else if (seen_this && (graph->num_parents > 2)) {
 			strbuf_write_column(sb, col, '\\');
-			chars_written++;
 		} else if (seen_this && (graph->num_parents == 2)) {
 			/*
 			 * This is a 2-way merge commit.
@@ -948,16 +942,13 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 				strbuf_write_column(sb, col, '\\');
 			else
 				strbuf_write_column(sb, col, '|');
-			chars_written++;
 		} else {
 			strbuf_write_column(sb, col, '|');
-			chars_written++;
 		}
 		strbuf_addch(sb, ' ');
-		chars_written++;
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, sb);
 
 	/*
 	 * Update graph->state
@@ -984,12 +975,11 @@ static struct column *find_new_column_by_commit(struct git_graph *graph,
 static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf *sb)
 {
 	int seen_this = 0;
-	int i, j, chars_written;
+	int i, j;
 
 	/*
 	 * Output the post-merge row
 	 */
-	chars_written = 0;
 	for (i = 0; i <= graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		struct commit *col_commit;
@@ -1017,7 +1007,6 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf
 			assert(par_column);
 
 			strbuf_write_column(sb, par_column, '|');
-			chars_written++;
 			for (j = 0; j < graph->num_parents - 1; j++) {
 				parents = next_interesting_parent(graph, parents);
 				assert(parents);
@@ -1026,19 +1015,16 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf
 				strbuf_write_column(sb, par_column, '\\');
 				strbuf_addch(sb, ' ');
 			}
-			chars_written += j * 2;
 		} else if (seen_this) {
 			strbuf_write_column(sb, col, '\\');
 			strbuf_addch(sb, ' ');
-			chars_written += 2;
 		} else {
 			strbuf_write_column(sb, col, '|');
 			strbuf_addch(sb, ' ');
-			chars_written += 2;
 		}
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, sb);
 
 	/*
 	 * Update graph->state
@@ -1181,7 +1167,7 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 		}
 	}
 
-	graph_pad_horizontally(graph, sb, graph->mapping_size);
+	graph_pad_horizontally(graph, sb);
 
 	/*
 	 * Swap mapping and new_mapping
@@ -1199,6 +1185,8 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 
 int graph_next_line(struct git_graph *graph, struct strbuf *sb)
 {
+	sb->width = 0;
+
 	switch (graph->state) {
 	case GRAPH_PADDING:
 		graph_output_padding_line(graph, sb);
@@ -1227,7 +1215,6 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb)
 static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)
 {
 	int i;
-	int chars_written = 0;
 
 	if (graph->state != GRAPH_COMMIT) {
 		graph_next_line(graph, sb);
@@ -1245,19 +1232,16 @@ static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)
 		struct column *col = &graph->columns[i];
 
 		strbuf_write_column(sb, col, '|');
-		chars_written++;
 
 		if (col->commit == graph->commit && graph->num_parents > 2) {
 			int len = (graph->num_parents - 2) * 2;
 			strbuf_addchars(sb, ' ', len);
-			chars_written += len;
 		} else {
 			strbuf_addch(sb, ' ');
-			chars_written++;
 		}
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, sb);
 
 	/*
 	 * Update graph->prev_state since we have output a padding line
strbuf.h +7 -2
index f62278a0be..3a98147321 100644
@@ -66,11 +66,12 @@ struct string_list;
 struct strbuf {
 	size_t alloc;
 	size_t len;
+	size_t width;
 	char *buf;
 };
 
 extern char strbuf_slopbuf[];
-#define STRBUF_INIT  { .alloc = 0, .len = 0, .buf = strbuf_slopbuf }
+#define STRBUF_INIT  { .alloc = 0, .len = 0, .width = 0, .buf = strbuf_slopbuf }
 
 /*
  * Predeclare this here, since cache.h includes this file before it defines the
@@ -161,6 +162,10 @@ static inline void strbuf_setlen(struct strbuf *sb, size_t len)
 {
 	if (len > (sb->alloc ? sb->alloc - 1 : 0))
 		die("BUG: strbuf_setlen() beyond buffer");
+	if (len > sb->len)
+		sb->width += len - sb->len;
+	else
+		sb->width = len;
 	sb->len = len;
 	if (sb->buf != strbuf_slopbuf)
 		sb->buf[len] = '\0';
@@ -231,6 +236,7 @@ static inline void strbuf_addch(struct strbuf *sb, int c)
 		strbuf_grow(sb, 1);
 	sb->buf[sb->len++] = c;
 	sb->buf[sb->len] = '\0';
+	sb->width++;
 }
 
 /**
--

gitgitgadget

#16 lore raw

PATCH 2/11 graph: reuse `find_new_column_by_commit()`

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

I will shortly be making some changes to graph_insert_into_new_columns() and so am trying to simplify it. One possible simplification is that we can extract the loop for finding the element in new_columns containing the given commit.

find_new_column_by_commit() contains a very similar loop but it returns a struct column * rather than an int offset into the array. Here I'm introducing a version that returns int and using that in graph_insert_into_new_columns() and graph_output_post_merge_line().

---
 graph.c | 48 +++++++++++++++++++++++-------------------------
 1 file changed, 23 insertions(+), 25 deletions(-)
graph.c +23 -25
index c56fdec1fc..a890c9d8bb 100644
@@ -441,22 +441,31 @@ static unsigned short graph_find_commit_color(const struct git_graph *graph,
 	return graph_get_current_column_color(graph);
 }
 
+static int graph_find_new_column_by_commit(struct git_graph *graph,
+					   struct commit *commit)
+{
+	int i;
+	for (i = 0; i < graph->num_new_columns; i++) {
+		if (graph->new_columns[i].commit == commit)
+			return i;
+	}
+	return -1;
+}
+
 static void graph_insert_into_new_columns(struct git_graph *graph,
 					  struct commit *commit,
 					  int *mapping_index)
 {
-	int i;
+	int i = graph_find_new_column_by_commit(graph, commit);
 
 	/*
 	 * If the commit is already in the new_columns list, we don't need to
 	 * add it.  Just update the mapping correctly.
 	 */
-	for (i = 0; i < graph->num_new_columns; i++) {
-		if (graph->new_columns[i].commit == commit) {
-			graph->mapping[*mapping_index] = i;
-			*mapping_index += 2;
-			return;
-		}
+	if (i >= 0) {
+		graph->mapping[*mapping_index] = i;
+		*mapping_index += 2;
+		return;
 	}
 
 	/*
@@ -961,17 +970,6 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 		graph_update_state(graph, GRAPH_COLLAPSING);
 }
 
-static struct column *find_new_column_by_commit(struct git_graph *graph,
-						struct commit *commit)
-{
-	int i;
-	for (i = 0; i < graph->num_new_columns; i++) {
-		if (graph->new_columns[i].commit == commit)
-			return &graph->new_columns[i];
-	}
-	return NULL;
-}
-
 static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf *sb)
 {
 	int seen_this = 0;
@@ -999,20 +997,20 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf
 			 * edges.
 			 */
 			struct commit_list *parents = NULL;
-			struct column *par_column;
+			int par_column;
 			seen_this = 1;
 			parents = first_interesting_parent(graph);
 			assert(parents);
-			par_column = find_new_column_by_commit(graph, parents->item);
-			assert(par_column);
+			par_column = graph_find_new_column_by_commit(graph, parents->item);
+			assert(par_column >= 0);
 
-			strbuf_write_column(sb, par_column, '|');
+			strbuf_write_column(sb, &graph->new_columns[par_column], '|');
 			for (j = 0; j < graph->num_parents - 1; j++) {
 				parents = next_interesting_parent(graph, parents);
 				assert(parents);
-				par_column = find_new_column_by_commit(graph, parents->item);
-				assert(par_column);
-				strbuf_write_column(sb, par_column, '\\');
+				par_column = graph_find_new_column_by_commit(graph, parents->item);
+				assert(par_column >= 0);
+				strbuf_write_column(sb, &graph->new_columns[par_column], '\\');
 				strbuf_addch(sb, ' ');
 			}
 		} else if (seen_this) {
Signature · 1 line
gitgitgadget
#17 lore raw

PATCH 3/11 graph: reduce duplication in `graph_insert_into_new_columns()`

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

I will shortly be making some changes to this function and so am trying to simplify it. It currently contains some duplicated logic; both branches the function can take assign the commit's column index into the mapping array and increment mapping_index.

Here I change the function so that the only conditional behaviour is that it appends the commit to new_columns if it's not present. All manipulation of mapping now happens on a single code path.

---
 graph.c | 20 +++++++-------------
 1 file changed, 7 insertions(+), 13 deletions(-)
graph.c +7 -13
index a890c9d8bb..1cd30f80e6 100644
@@ -459,23 +459,17 @@ static void graph_insert_into_new_columns(struct git_graph *graph,
 	int i = graph_find_new_column_by_commit(graph, commit);
 
 	/*
-	 * If the commit is already in the new_columns list, we don't need to
-	 * add it.  Just update the mapping correctly.
+	 * If the commit is not already in the new_columns array, then add it
+	 * and record it as being in the final column.
 	 */
-	if (i >= 0) {
-		graph->mapping[*mapping_index] = i;
-		*mapping_index += 2;
-		return;
+	if (i < 0) {
+		i = graph->num_new_columns++;
+		graph->new_columns[i].commit = commit;
+		graph->new_columns[i].color = graph_find_commit_color(graph, commit);
 	}
 
-	/*
-	 * This commit isn't already in new_columns.  Add it.
-	 */
-	graph->new_columns[graph->num_new_columns].commit = commit;
-	graph->new_columns[graph->num_new_columns].color = graph_find_commit_color(graph, commit);
-	graph->mapping[*mapping_index] = graph->num_new_columns;
+	graph->mapping[*mapping_index] = i;
 	*mapping_index += 2;
-	graph->num_new_columns++;
 }
 
 static void graph_update_width(struct git_graph *graph,
Signature · 1 line
gitgitgadget
#18 lore raw

PATCH 4/11 graph: remove `mapping_idx` and `graph_update_width()`

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

There's a duplication of logic between graph_insert_into_new_columns() and graph_update_width(). graph_insert_into_new_columns() is called repeatedly by graph_update_columns() with an int * that tracks the offset into the mapping array where we should write the next value. Each call to graph_insert_into_new_columns() effectively pushes one column index and one "null" value (-1) onto the mapping array and therefore increments mapping_idx by 2.

graph_update_width() duplicates this process: the width of the graph is essentially the initial width of the mapping array before edges begin collapsing. The graph_update_width() function's logic effectively works out how many times graph_insert_into_new_columns() was called based on the relationship of the current commit to the rest of the graph.

I'm about to make some changes that make the assignment of values into the mapping array more complicated. Rather than make graph_update_width() more complicated at the same time, we can simply remove this function and use graph->width to track the offset into the mapping array as we're building it. This removes the duplication and makes sure that graph->width is the same as the visual width of the mapping array once graph_update_columns() is complete.

---
 graph.c | 65 +++++++++------------------------------------------------
 1 file changed, 10 insertions(+), 55 deletions(-)
graph.c +10 -55
index 1cd30f80e6..a71ae5cd61 100644
@@ -453,8 +453,7 @@ static int graph_find_new_column_by_commit(struct git_graph *graph,
 }
 
 static void graph_insert_into_new_columns(struct git_graph *graph,
-					  struct commit *commit,
-					  int *mapping_index)
+					  struct commit *commit)
 {
 	int i = graph_find_new_column_by_commit(graph, commit);
 
@@ -468,50 +467,14 @@ static void graph_insert_into_new_columns(struct git_graph *graph,
 		graph->new_columns[i].color = graph_find_commit_color(graph, commit);
 	}
 
-	graph->mapping[*mapping_index] = i;
-	*mapping_index += 2;
-}
-
-static void graph_update_width(struct git_graph *graph,
-			       int is_commit_in_existing_columns)
-{
-	/*
-	 * Compute the width needed to display the graph for this commit.
-	 * This is the maximum width needed for any row.  All other rows
-	 * will be padded to this width.
-	 *
-	 * Compute the number of columns in the widest row:
-	 * Count each existing column (graph->num_columns), and each new
-	 * column added by this commit.
-	 */
-	int max_cols = graph->num_columns + graph->num_parents;
-
-	/*
-	 * Even if the current commit has no parents to be printed, it
-	 * still takes up a column for itself.
-	 */
-	if (graph->num_parents < 1)
-		max_cols++;
-
-	/*
-	 * We added a column for the current commit as part of
-	 * graph->num_parents.  If the current commit was already in
-	 * graph->columns, then we have double counted it.
-	 */
-	if (is_commit_in_existing_columns)
-		max_cols--;
-
-	/*
-	 * Each column takes up 2 spaces
-	 */
-	graph->width = max_cols * 2;
+	graph->mapping[graph->width] = i;
+	graph->width += 2;
 }
 
 static void graph_update_columns(struct git_graph *graph)
 {
 	struct commit_list *parent;
 	int max_new_columns;
-	int mapping_idx;
 	int i, seen_this, is_commit_in_columns;
 
 	/*
@@ -544,6 +507,8 @@ static void graph_update_columns(struct git_graph *graph)
 	for (i = 0; i < graph->mapping_size; i++)
 		graph->mapping[i] = -1;
 
+	graph->width = 0;
+
 	/*
 	 * Populate graph->new_columns and graph->mapping
 	 *
@@ -554,7 +519,6 @@ static void graph_update_columns(struct git_graph *graph)
 	 * supposed to end up after the collapsing is performed.
 	 */
 	seen_this = 0;
-	mapping_idx = 0;
 	is_commit_in_columns = 1;
 	for (i = 0; i <= graph->num_columns; i++) {
 		struct commit *col_commit;
@@ -568,7 +532,6 @@ static void graph_update_columns(struct git_graph *graph)
 		}
 
 		if (col_commit == graph->commit) {
-			int old_mapping_idx = mapping_idx;
 			seen_this = 1;
 			graph->commit_index = i;
 			for (parent = first_interesting_parent(graph);
@@ -583,21 +546,18 @@ static void graph_update_columns(struct git_graph *graph)
 				    !is_commit_in_columns) {
 					graph_increment_column_color(graph);
 				}
-				graph_insert_into_new_columns(graph,
-							      parent->item,
-							      &mapping_idx);
+				graph_insert_into_new_columns(graph, parent->item);
 			}
 			/*
-			 * We always need to increment mapping_idx by at
+			 * We always need to increment graph->width by at
 			 * least 2, even if it has no interesting parents.
 			 * The current commit always takes up at least 2
 			 * spaces.
 			 */
-			if (mapping_idx == old_mapping_idx)
-				mapping_idx += 2;
+			if (graph->num_parents == 0)
+				graph->width += 2;
 		} else {
-			graph_insert_into_new_columns(graph, col_commit,
-						      &mapping_idx);
+			graph_insert_into_new_columns(graph, col_commit);
 		}
 	}
 
@@ -607,11 +567,6 @@ static void graph_update_columns(struct git_graph *graph)
 	while (graph->mapping_size > 1 &&
 	       graph->mapping[graph->mapping_size - 1] < 0)
 		graph->mapping_size--;
-
-	/*
-	 * Compute graph->width for this commit
-	 */
-	graph_update_width(graph, is_commit_in_columns);
 }
 
 void graph_update(struct git_graph *graph, struct commit *commit)
Signature · 1 line
gitgitgadget
#19 lore raw

PATCH 5/11 graph: extract logic for moving to GRAPH_PRE_COMMIT state

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

This computation is repeated in a couple of places and I need to add another condition to it to implement a further improvement to the graph rendering, so I'm extracting this into a function.

---
 graph.c | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)
graph.c +8 -4
index a71ae5cd61..98e8777db4 100644
@@ -569,6 +569,12 @@ static void graph_update_columns(struct git_graph *graph)
 		graph->mapping_size--;
 }
 
+static int graph_needs_pre_commit_line(struct git_graph *graph)
+{
+	return graph->num_parents >= 3 &&
+	       graph->commit_index < (graph->num_columns - 1);
+}
+
 void graph_update(struct git_graph *graph, struct commit *commit)
 {
 	struct commit_list *parent;
@@ -624,8 +630,7 @@ void graph_update(struct git_graph *graph, struct commit *commit)
 	 */
 	if (graph->state != GRAPH_PADDING)
 		graph->state = GRAPH_SKIP;
-	else if (graph->num_parents >= 3 &&
-		 graph->commit_index < (graph->num_columns - 1))
+	else if (graph_needs_pre_commit_line(graph))
 		graph->state = GRAPH_PRE_COMMIT;
 	else
 		graph->state = GRAPH_COMMIT;
@@ -708,8 +713,7 @@ static void graph_output_skip_line(struct git_graph *graph, struct strbuf *sb)
 	strbuf_addstr(sb, "...");
 	graph_pad_horizontally(graph, sb);
 
-	if (graph->num_parents >= 3 &&
-	    graph->commit_index < (graph->num_columns - 1))
+	if (graph_needs_pre_commit_line(graph))
 		graph_update_state(graph, GRAPH_PRE_COMMIT);
 	else
 		graph_update_state(graph, GRAPH_COMMIT);
Signature · 1 line
gitgitgadget
#20 lore raw

PATCH 6/11 graph: tidy up display of left-skewed merges

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

Currently, when we display a merge whose first parent is already present in a column to the left of the merge commit, we display the first parent as a veritcal pipe | in the GRAPH_POST_MERGE line and then immediately enter the GRAPH_COLLAPSING state. The first-parent line tracks to the left and all the other parent lines follow it; this creates a "kink" in those lines:

        | *---.
        | |\ \ \
        |/ / / /
        | | | *

This change tidies the display of such commits such that if the first parent appears to the left of the merge, we render it as a / and the second parent as a |. This reduces the horizontal and vertical space needed to render the merge, and makes the resulting lines easier to read.

        | *-.
        |/|\ \
        | | | *

If the first parent is separated from the merge by several columns, a horizontal line is drawn in a similar manner to how the GRAPH_COLLAPSING state displays the line.

        | | | *-.
        | |_|/|\ \
        |/| | | | *

This effect is applied to both "normal" two-parent merges, and to octopus merges. It also reduces the vertical space needed for pre-commit lines, as the merge occupies one less column than usual.

        Before:         After:

        | *             | *
        | |\            | |\
        | | \           | * \
        | |  \          |/|\ \
        | *-. \
        | |\ \ \
---
 graph.c                      | 125 +++++++++++++++++++++++++++--------
 t/t4214-log-graph-octopus.sh |  10 ++-
 t/t4215-log-skewed-merges.sh |  42 ++++++++++++
 3 files changed, 143 insertions(+), 34 deletions(-)
 create mode 100755 t/t4215-log-skewed-merges.sh
graph.c +97 -28
index 98e8777db4..9136173e03 100644
@@ -183,6 +183,20 @@ struct git_graph {
 	 * previous commit.
 	 */
 	int prev_commit_index;
+	/*
+	 * Which layout variant to use to display merge commits. If the
+	 * commit's first parent is known to be in a column to the left of the
+	 * merge, then this value is 0 and we use the layout on the left.
+	 * Otherwise, the value is 1 and the layout on the right is used. This
+	 * field tells us how many columns the first parent occupies.
+	 *
+	 * 		0)			1)
+	 *
+	 * 		| | | *-.		| | *---.
+	 * 		| |_|/|\ \		| | |\ \ \
+	 * 		|/| | | | |		| | | | | *
+	 */
+	int merge_layout;
 	/*
 	 * The maximum number of columns that can be stored in the columns
 	 * and new_columns arrays.  This is also half the number of entries
@@ -294,6 +308,7 @@ struct git_graph *graph_init(struct rev_info *opt)
 	graph->prev_state = GRAPH_PADDING;
 	graph->commit_index = 0;
 	graph->prev_commit_index = 0;
+	graph->merge_layout = 0;
 	graph->num_columns = 0;
 	graph->num_new_columns = 0;
 	graph->mapping_size = 0;
@@ -453,9 +468,11 @@ static int graph_find_new_column_by_commit(struct git_graph *graph,
 }
 
 static void graph_insert_into_new_columns(struct git_graph *graph,
-					  struct commit *commit)
+					  struct commit *commit,
+					  int idx)
 {
 	int i = graph_find_new_column_by_commit(graph, commit);
+	int mapping_idx;
 
 	/*
 	 * If the commit is not already in the new_columns array, then add it
@@ -467,8 +484,26 @@ static void graph_insert_into_new_columns(struct git_graph *graph,
 		graph->new_columns[i].color = graph_find_commit_color(graph, commit);
 	}
 
-	graph->mapping[graph->width] = i;
-	graph->width += 2;
+	if (graph->num_parents > 1 && idx > -1 && graph->merge_layout == -1) {
+		/*
+		 * If this is the first parent of a merge, choose a layout for
+		 * the merge line based on whether the parent appears in a
+		 * column to the left of the merge
+		 */
+		int dist, shift;
+
+		dist = idx - i;
+		shift = (dist > 1) ? 2 * dist - 3 : 1;
+
+		graph->merge_layout = (dist > 0) ? 0 : 1;
+		mapping_idx = graph->width + (graph->merge_layout - 1) * shift;
+		graph->width += 2 * graph->merge_layout;
+	} else {
+		mapping_idx = graph->width;
+		graph->width += 2;
+	}
+
+	graph->mapping[mapping_idx] = i;
 }
 
 static void graph_update_columns(struct git_graph *graph)
@@ -534,6 +569,7 @@ static void graph_update_columns(struct git_graph *graph)
 		if (col_commit == graph->commit) {
 			seen_this = 1;
 			graph->commit_index = i;
+			graph->merge_layout = -1;
 			for (parent = first_interesting_parent(graph);
 			     parent;
 			     parent = next_interesting_parent(graph, parent)) {
@@ -546,7 +582,7 @@ static void graph_update_columns(struct git_graph *graph)
 				    !is_commit_in_columns) {
 					graph_increment_column_color(graph);
 				}
-				graph_insert_into_new_columns(graph, parent->item);
+				graph_insert_into_new_columns(graph, parent->item, i);
 			}
 			/*
 			 * We always need to increment graph->width by at
@@ -557,7 +593,7 @@ static void graph_update_columns(struct git_graph *graph)
 			if (graph->num_parents == 0)
 				graph->width += 2;
 		} else {
-			graph_insert_into_new_columns(graph, col_commit);
+			graph_insert_into_new_columns(graph, col_commit, -1);
 		}
 	}
 
@@ -569,10 +605,36 @@ static void graph_update_columns(struct git_graph *graph)
 		graph->mapping_size--;
 }
 
+static int graph_num_expansion_rows(struct git_graph *graph)
+{
+	/*
+	 * Normally, we need two expansion rows for each dashed parent line from
+	 * an octopus merge:
+	 *
+	 * 		| *
+	 * 		| |\
+	 * 		| | \
+	 * 		| |  \
+	 * 		| *-. \
+	 * 		| |\ \ \
+	 *
+	 * If the merge is skewed to the left, then its parents occupy one less
+	 * column, and we don't need as many expansion rows to route around it;
+	 * in some cases that means we don't need any expansion rows at all:
+	 *
+	 * 		| *
+	 * 		| |\
+	 * 		| * \
+	 * 		|/|\ \
+	 */
+	return (graph->num_parents + graph->merge_layout - 3) * 2;
+}
+
 static int graph_needs_pre_commit_line(struct git_graph *graph)
 {
 	return graph->num_parents >= 3 &&
-	       graph->commit_index < (graph->num_columns - 1);
+	       graph->commit_index < (graph->num_columns - 1) &&
+	       graph->expansion_row < graph_num_expansion_rows(graph);
 }
 
 void graph_update(struct git_graph *graph, struct commit *commit)
@@ -722,7 +784,6 @@ static void graph_output_skip_line(struct git_graph *graph, struct strbuf *sb)
 static void graph_output_pre_commit_line(struct git_graph *graph,
 					 struct strbuf *sb)
 {
-	int num_expansion_rows;
 	int i, seen_this;
 
 	/*
@@ -733,14 +794,13 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 	 * We need 2 extra rows for every parent over 2.
 	 */
 	assert(graph->num_parents >= 3);
-	num_expansion_rows = (graph->num_parents - 2) * 2;
 
 	/*
 	 * graph->expansion_row tracks the current expansion row we are on.
 	 * It should be in the range [0, num_expansion_rows - 1]
 	 */
 	assert(0 <= graph->expansion_row &&
-	       graph->expansion_row < num_expansion_rows);
+	       graph->expansion_row < graph_num_expansion_rows(graph));
 
 	/*
 	 * Output the row
@@ -782,7 +842,7 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 	 * and move to state GRAPH_COMMIT if necessary
 	 */
 	graph->expansion_row++;
-	if (graph->expansion_row >= num_expansion_rows)
+	if (!graph_needs_pre_commit_line(graph))
 		graph_update_state(graph, GRAPH_COMMIT);
 }
 
@@ -820,7 +880,7 @@ static void graph_draw_octopus_merge(struct git_graph *graph, struct strbuf *sb)
 	 * x 0 1 2 3
 	 *
 	 */
-	const int dashless_parents = 2;
+	const int dashless_parents = 3 - graph->merge_layout;
 	int dashful_parents = graph->num_parents - dashless_parents;
 
 	/*
@@ -828,9 +888,9 @@ static void graph_draw_octopus_merge(struct git_graph *graph, struct strbuf *sb)
 	 * above) but sometimes the first parent goes into an existing column,
 	 * like this:
 	 *
-	 * | *---.
-	 * | |\ \ \
-	 * |/ / / /
+	 * | *-.
+	 * |/|\ \
+	 * | | | |
 	 * x 0 1 2
 	 *
 	 * In which case the number of parents will be one greater than the
@@ -923,10 +983,15 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 		graph_update_state(graph, GRAPH_COLLAPSING);
 }
 
+const char merge_chars[] = {'/', '|', '\\'};
+
 static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf *sb)
 {
 	int seen_this = 0;
-	int i, j;
+	int i;
+
+	struct commit_list *first_parent = first_interesting_parent(graph);
+	int seen_parent = 0;
 
 	/*
 	 * Output the post-merge row
@@ -949,30 +1014,34 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf
 			 * new_columns and use those to format the
 			 * edges.
 			 */
-			struct commit_list *parents = NULL;
+			struct commit_list *parents = first_parent;
 			int par_column;
+			int idx = graph->merge_layout;
+			char c;
 			seen_this = 1;
-			parents = first_interesting_parent(graph);
-			assert(parents);
-			par_column = graph_find_new_column_by_commit(graph, parents->item);
-			assert(par_column >= 0);
-
-			strbuf_write_column(sb, &graph->new_columns[par_column], '|');
-			for (j = 0; j < graph->num_parents - 1; j++) {
-				parents = next_interesting_parent(graph, parents);
-				assert(parents);
+
+			for (; parents; parents = next_interesting_parent(graph, parents)) {
 				par_column = graph_find_new_column_by_commit(graph, parents->item);
 				assert(par_column >= 0);
-				strbuf_write_column(sb, &graph->new_columns[par_column], '\\');
-				strbuf_addch(sb, ' ');
+
+				c = merge_chars[idx];
+				strbuf_write_column(sb, &graph->new_columns[par_column], c);
+				if (idx == 2)
+					strbuf_addch(sb, ' ');
+				else
+					idx++;
 			}
 		} else if (seen_this) {
 			strbuf_write_column(sb, col, '\\');
 			strbuf_addch(sb, ' ');
 		} else {
 			strbuf_write_column(sb, col, '|');
-			strbuf_addch(sb, ' ');
+			if (graph->merge_layout != 0 || i != graph->commit_index - 1)
+				strbuf_addch(sb, seen_parent ? '_' : ' ');
 		}
+
+		if (col_commit == first_parent->item)
+			seen_parent = 1;
 	}
 
 	graph_pad_horizontally(graph, sb);
t/t4214-log-graph-octopus.sh +4 -6
index dab96c89aa..40f420877b 100755
@@ -7,9 +7,8 @@ test_description='git log --graph of skewed left octopus merge.'
 test_expect_success 'set up merge history' '
 	cat >expect.uncolored <<-\EOF &&
 	* left
-	| *---.   octopus-merge
-	| |\ \ \
-	|/ / / /
+	| *-.   octopus-merge
+	|/|\ \
 	| | | * 4
 	| | * | 3
 	| | |/
@@ -21,9 +20,8 @@ test_expect_success 'set up merge history' '
 	EOF
 	cat >expect.colors <<-\EOF &&
 	* left
-	<RED>|<RESET> *<BLUE>-<RESET><BLUE>-<RESET><MAGENTA>-<RESET><MAGENTA>.<RESET>   octopus-merge
-	<RED>|<RESET> <RED>|<RESET><YELLOW>\<RESET> <BLUE>\<RESET> <MAGENTA>\<RESET>
-	<RED>|<RESET><RED>/<RESET> <YELLOW>/<RESET> <BLUE>/<RESET> <MAGENTA>/<RESET>
+	<RED>|<RESET> *<MAGENTA>-<RESET><MAGENTA>.<RESET>   octopus-merge
+	<RED>|<RESET><RED>/<RESET><YELLOW>|<RESET><BLUE>\<RESET> <MAGENTA>\<RESET>
 	<RED>|<RESET> <YELLOW>|<RESET> <BLUE>|<RESET> * 4
 	<RED>|<RESET> <YELLOW>|<RESET> * <MAGENTA>|<RESET> 3
 	<RED>|<RESET> <YELLOW>|<RESET> <MAGENTA>|<RESET><MAGENTA>/<RESET>
t/t4215-log-skewed-merges.sh new file +42 -0

42 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#21 lore raw

Re: [PATCH 06/11] graph: tidy up display of left-skewed merges

Quoted 36 lines · James Coglan via GitGitGadget

From: James Coglan <[email protected]>

Currently, when we display a merge whose first parent is already present in a column to the left of the merge commit, we display the first parent as a veritcal pipe | in the GRAPH_POST_MERGE line and then immediately enter the GRAPH_COLLAPSING state. The first-parent line tracks to the left and all the other parent lines follow it; this creates a "kink" in those lines:

        | *---.
        | |\ \ \
        |/ / / /
        | | | *

This change tidies the display of such commits such that if the first parent appears to the left of the merge, we render it as a / and the second parent as a |. This reduces the horizontal and vertical space needed to render the merge, and makes the resulting lines easier to read.

        | *-.
        |/|\ \
        | | | *

If the first parent is separated from the merge by several columns, a horizontal line is drawn in a similar manner to how the GRAPH_COLLAPSING state displays the line.

        | | | *-.
        | |_|/|\ \
        |/| | | | *

This effect is applied to both "normal" two-parent merges, and to octopus merges. It also reduces the vertical space needed for pre-commit lines, as the merge occupies one less column than usual.

Quoted 9 lines
        Before:         After:

        | *             | *
        | |\            | |\
        | | \           | * \
        | |  \          |/|\ \
        | *-. \
        | |\ \ \

Thank you for adding these careful diagrams both to the message and the code. These concepts are hard to track without a visual aid.

[snip]

t/t4215-log-skewed-merges.sh line 7

Quoted 9 lines

+++ b/t/t4215-log-skewed-merges.sh

t/t4215-log-skewed-merges.sh new file +7 -0
@@ -0,0 +1,42 @@
+#!/bin/sh
+
+test_description='git log --graph of skewed merges'
+
+. ./test-lib.sh
+
+test_expect_success 'setup left-skewed merge' '

Could you skew this example to include a left-skewed octopus merge (and use fewer Git processes) with the following:

	git checkout --orphan _a && test_commit A &&
	git switch -c _b _a && test_commit B &&
	git switch -c _c _a && test_commit C &&
	git switch -c _d _a && test_commit D &&	git switch -c _e _b && git merge --no-ff _c _d E &&
	git switch -c _f _a && git merge --no-ff _d -m F &&	git checkout _a && git merge --no-ff _b _c _e _f -m G

and I think the resulting output will be:

*-----. G

|\ \ \ \
| | | | * F
| |_|_|/|
|/| | | |
| | | * | E
| |_|/|\|
|/| | | |
| | |/  * D
| |_|__/
|/| |
| | * C
| |/
|/|
| * B
|/

* A

#22 lore raw

Re: [PATCH 06/11] graph: tidy up display of left-skewed merges

Quoted 38 lines · Derrick Stolee

t/t4215-log-skewed-merges.sh line 7

Quoted 9 lines · James Coglan via GitGitGadget

+++ b/t/t4215-log-skewed-merges.sh

t/t4215-log-skewed-merges.sh new file +7 -0
@@ -0,0 +1,42 @@
+#!/bin/sh
+
+test_description='git log --graph of skewed merges'
+
+. ./test-lib.sh
+
+test_expect_success 'setup left-skewed merge' '

Could you skew this example to include a left-skewed octopus merge (and use fewer Git processes) with the following:

	git checkout --orphan _a && test_commit A &&
	git switch -c _b _a && test_commit B &&
	git switch -c _c _a && test_commit C &&
	git switch -c _d _a && test_commit D &&	git switch -c _e _b && git merge --no-ff _c _d E &&
	git switch -c _f _a && git merge --no-ff _d -m F &&	git checkout _a && git merge --no-ff _b _c _e _f -m G

and I think the resulting output will be:

*-----. G

|\ \ \ \
| | | | * F
| |_|_|/|
|/| | | |
| | | * | E
| |_|/|\|
|/| | | |
| | |/  * D
| |_|__/
|/| |
| | * C
| |/
|/|
| * B
|/
Quoted 1 line

* A

At this point in the history, commit E won't render like that -- this is before the change that flattens edges that fuse with the merge's last parent. I think the display of this history at this point will be:

	*-----.   G
	|\ \ \ \
	| | | | * F
	| |_|_|/|
	|/| | | |
	| | | * |   E
	| |_|/|\ \
	|/| |/ / /
	| | | | /
	| | | |/
	| | | * D
	| |_|/
	|/| |
	| | * C
	| |/
	|/|
	| * B
	|/
	* A

Is there a particular reason for wanting to include this test case? What particular combination of states is it designed to test? (My guess is that it includes an octopus merge where the original test does not.) I'd be happy to add it at the appropriate point in the history if it's adding coverage not provided by the other tests.

#23 lore raw

Re: [PATCH 06/11] graph: tidy up display of left-skewed merges

Quoted 63 lines · James Coglan
Quoted 38 lines · Derrick Stolee

t/t4215-log-skewed-merges.sh line 7

Quoted 9 lines · James Coglan via GitGitGadget

+++ b/t/t4215-log-skewed-merges.sh

t/t4215-log-skewed-merges.sh new file +7 -0
@@ -0,0 +1,42 @@
+#!/bin/sh
+
+test_description='git log --graph of skewed merges'
+
+. ./test-lib.sh
+
+test_expect_success 'setup left-skewed merge' '

Could you skew this example to include a left-skewed octopus merge (and use fewer Git processes) with the following:

	git checkout --orphan _a && test_commit A &&
	git switch -c _b _a && test_commit B &&
	git switch -c _c _a && test_commit C &&
	git switch -c _d _a && test_commit D &&	git switch -c _e _b && git merge --no-ff _c _d E &&
	git switch -c _f _a && git merge --no-ff _d -m F &&	git checkout _a && git merge --no-ff _b _c _e _f -m G

and I think the resulting output will be:

*-----. G

|\ \ \ \
| | | | * F
| |_|_|/|
|/| | | |
| | | * | E
| |_|/|\|
|/| | | |
| | |/  * D
| |_|__/
|/| |
| | * C
| |/
|/|
| * B
|/
Quoted 1 line

* A

At this point in the history, commit E won't render like that -- this is before the change that flattens edges that fuse with the merge's last parent. I think the display of this history at this point will be:

	*-----.   G
	|\ \ \ \
	| | | | * F
	| |_|_|/|
	|/| | | |
	| | | * |   E
	| |_|/|\ \
	|/| |/ / /
	| | | | /
	| | | |/
	| | | * D
	| |_|/
	|/| |
	| | * C
	| |/
	|/|
	| * B
	|/
	* A
Quoted 1 line

Is there a particular reason for wanting to include this test case? What particular combination of states is it designed to test? (My guess is that it includes an octopus merge where the original test does not.) I'd be happy to add it at the appropriate point in the history if it's adding coverage not provided by the other tests.

Thanks for correcting my test case. It also helps that you would show the change in behavior in your later commits.

My reason to include this test is because it includes a regular merge and an octopus merge, both of which have a skewed render. Many times logic that applies to a normal merge breaks with octopus merges, so I try to include them whenever possible.

Thanks,
-Stolee

#24 lore raw

Re: [PATCH 06/11] graph: tidy up display of left-skewed merges

Quoted 64 lines · Derrick Stolee
Quoted 63 lines · James Coglan
Quoted 38 lines · Derrick Stolee

t/t4215-log-skewed-merges.sh line 7

Quoted 9 lines · James Coglan via GitGitGadget

+++ b/t/t4215-log-skewed-merges.sh

t/t4215-log-skewed-merges.sh new file +7 -0
@@ -0,0 +1,42 @@
+#!/bin/sh
+
+test_description='git log --graph of skewed merges'
+
+. ./test-lib.sh
+
+test_expect_success 'setup left-skewed merge' '

Could you skew this example to include a left-skewed octopus merge (and use fewer Git processes) with the following:

	git checkout --orphan _a && test_commit A &&
	git switch -c _b _a && test_commit B &&
	git switch -c _c _a && test_commit C &&
	git switch -c _d _a && test_commit D &&	git switch -c _e _b && git merge --no-ff _c _d E &&
	git switch -c _f _a && git merge --no-ff _d -m F &&	git checkout _a && git merge --no-ff _b _c _e _f -m G

and I think the resulting output will be:

*-----. G

|\ \ \ \
| | | | * F
| |_|_|/|
|/| | | |
| | | * | E
| |_|/|\|
|/| | | |
| | |/  * D
| |_|__/
|/| |
| | * C
| |/
|/|
| * B
|/
Quoted 1 line

* A

At this point in the history, commit E won't render like that -- this is before the change that flattens edges that fuse with the merge's last parent. I think the display of this history at this point will be:

	*-----.   G
	|\ \ \ \
	| | | | * F
	| |_|_|/|
	|/| | | |
	| | | * |   E
	| |_|/|\ \
	|/| |/ / /
	| | | | /
	| | | |/
	| | | * D
	| |_|/
	|/| |
	| | * C
	| |/
	|/|
	| * B
	|/
	* A
Quoted 5 lines
Quoted 1 line

Is there a particular reason for wanting to include this test case? What particular combination of states is it designed to test? (My guess is that it includes an octopus merge where the original test does not.) I'd be happy to add it at the appropriate point in the history if it's adding coverage not provided by the other tests.

Thanks for correcting my test case. It also helps that you would show the change in behavior in your later commits.

My reason to include this test is because it includes a regular merge and an octopus merge, both of which have a skewed render. Many times logic that applies to a normal merge breaks with octopus merges, so I try to include them whenever possible.

Thanks, I've now incorporated your suggested test into my patch. I had to amend it slightly as it turns out the above history is not valid; G is not a possible merge because one of its parents (C) is an ancestor of another (E). The actual example I've added is this:

	*-----.   0_H
	|\ \ \ \
	| | | | * 0_G
	| |_|_|/|
	|/| | | |
	| | | * \   0_F
	| |_|/|\ \
	|/| | | |/
	| | | | * 0_E
	| |_|_|/
	|/| | |
	| | * | 0_D
	| | |/
	| | * 0_C
	| |/
	|/|
	| * 0_B
	|/
	* 0_A

I've also added another commit before beginning this work that adds the example from the cover letter, so you can see it changing with each commit, namely this history:

	*   H
	|\
	| *   G
	| |\
	| | * F
	| | |
	| |  \
	| *-. \   E
	| |\ \ \
	|/ / / /
	| | | /
	| | |/
	| | * D
	| * | C
	| |/
	* | B
	|/
	* A
#25 lore raw

PATCH 7/11 graph: commit and post-merge lines for left-skewed merges

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

Following the introduction of "left-skewed" merges, which are merges whose first parent fuses with another edge to its left, we have some more edge cases to deal with in the display of commit and post-merge lines.

The current graph code handles the following cases for edges appearing to the right of the commit (*) on commit lines. A 2-way merge is usually followed by vertical lines:

        | | |
        | * |
        | |\ \

An octopus merge (more than two parents) is always followed by edges sloping to the right:

        | |  \          | |    \
        | *-. \         | *---. \
        | |\ \ \        | |\ \ \ \

A 2-way merge is followed by a right-sloping edge if the commit line immediately follows a post-merge line for a commit that appears in the same column as the current commit, or any column to the left of that:

        | *             | * |
        | |\            | |\ \
        | * \           | | * \
        | |\ \          | | |\ \

This commit introduces the following new cases for commit lines. If a 2-way merge skews to the left, then the edges to its right are always vertical lines, even if the commit follows a post-merge line:

        | | |           | |\
        | * |           | * |
        |/| |           |/| |

A commit with 3 parents that skews left is followed by vertical edges:

        | | |
        | * |
        |/|\ \

If a 3-way left-skewed merge commit appears immediately after a post-merge line, then it may be followed the right-sloping edges, just like a 2-way merge that is not skewed.

        | |\
        | * \
        |/|\ \

Octopus merges with 4 or more parents that skew to the left will always be followed by right-sloping edges, because the existing columns need to expand around the merge.

        | |  \
        | *-. \
        |/|\ \ \

On post-merge lines, usually all edges following the current commit slope to the right:

        | * | |
        | |\ \ \

However, if the commit is a left-skewed 2-way merge, the edges to its right remain vertical. We also need to display a space after the vertical line descending from the commit marker, whereas this line would normally be followed by a backslash.

        | * | |
        |/| | |

If a left-skewed merge has more than 2 parents, then the edges to its right are still sloped as they bend around the edges introduced by the merge.

        | * | |
        |/|\ \ \

To handle these new cases, we need to know not just how many parents each commit has, but how many new columns it adds to the display; this quantity is recorded in the edges_added field for the current commit, and prev_edges_added field for the previous commit.

Here, "column" refers to visual columns, not the logical columns of the columns array. This is because even if all the commit's parents end up fusing with existing edges, they initially introduce distinct edges in the commit and post-merge lines before those edges collapse. For example, a 3-way merge whose 2nd and 3rd parents fuse with existing edges still introduces 2 visual columns that affect the display of edges to their right.

        | | |  \
        | | *-. \
        | | |\ \ \
        | |_|/ / /
        |/| | / /
        | | |/ /
        | |/| |
        | | | |

This merge does not introduce any *logical* columns; there are 4 edges before and after this commit once all edges have collapsed. But it does initially introduce 2 new edges that need to be accommodated by the edges to their right.

---
 graph.c                      |  63 +++++++++++++--
 t/t4215-log-skewed-merges.sh | 151 +++++++++++++++++++++++++++++++++++
 2 files changed, 209 insertions(+), 5 deletions(-)
graph.c +58 -5
index 9136173e03..fb2e42850f 100644
@@ -197,6 +197,46 @@ struct git_graph {
 	 * 		|/| | | | |		| | | | | *
 	 */
 	int merge_layout;
+	/*
+	 * The number of columns added to the graph by the current commit. For
+	 * 2-way and octopus merges, this is is usually one less than the
+	 * number of parents:
+	 *
+	 * 		| | |			| |    \
+	 *		| * |			| *---. \
+	 *		| |\ \			| |\ \ \ \
+	 *		| | | |         	| | | | | |
+	 *
+	 *		num_parents: 2		num_parents: 4
+	 *		edges_added: 1		edges_added: 3
+	 *
+	 * For left-skewed merges, the first parent fuses with its neighbor and
+	 * so one less column is added:
+	 *
+	 *		| | |			| |  \
+	 *		| * |			| *-. \
+	 *		|/| |			|/|\ \ \
+	 *		| | |			| | | | |
+	 *
+	 *		num_parents: 2		num_parents: 4
+	 *		edges_added: 0		edges_added: 2
+	 *
+	 * This number determines how edges to the right of the merge are
+	 * displayed in commit and post-merge lines; if no columns have been
+	 * added then a vertical line should be used where a right-tracking
+	 * line would otherwise be used.
+	 *
+	 *		| * \			| * |
+	 *		| |\ \			|/| |
+	 *		| | * \			| * |
+	 */
+	int edges_added;
+	/*
+	 * The number of columns added by the previous commit, which is used to
+	 * smooth edges appearing to the right of a commit in a commit line
+	 * following a post-merge line.
+	 */
+	int prev_edges_added;
 	/*
 	 * The maximum number of columns that can be stored in the columns
 	 * and new_columns arrays.  This is also half the number of entries
@@ -309,6 +349,8 @@ struct git_graph *graph_init(struct rev_info *opt)
 	graph->commit_index = 0;
 	graph->prev_commit_index = 0;
 	graph->merge_layout = 0;
+	graph->edges_added = 0;
+	graph->prev_edges_added = 0;
 	graph->num_columns = 0;
 	graph->num_new_columns = 0;
 	graph->mapping_size = 0;
@@ -670,6 +712,9 @@ void graph_update(struct git_graph *graph, struct commit *commit)
 	 */
 	graph_update_columns(graph);
 
+	graph->prev_edges_added = graph->edges_added;
+	graph->edges_added = graph->num_parents + graph->merge_layout - 2;
+
 	graph->expansion_row = 0;
 
 	/*
@@ -943,12 +988,13 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 
 			if (graph->num_parents > 2)
 				graph_draw_octopus_merge(graph, sb);
-		} else if (seen_this && (graph->num_parents > 2)) {
+		} else if (seen_this && (graph->edges_added > 1)) {
 			strbuf_write_column(sb, col, '\\');
-		} else if (seen_this && (graph->num_parents == 2)) {
+		} else if (seen_this && (graph->edges_added == 1)) {
 			/*
-			 * This is a 2-way merge commit.
-			 * There is no GRAPH_PRE_COMMIT stage for 2-way
+			 * This is either a right-skewed 2-way merge
+			 * commit, or a left-skewed 3-way merge.
+			 * There is no GRAPH_PRE_COMMIT stage for such
 			 * merges, so this is the first line of output
 			 * for this commit.  Check to see what the previous
 			 * line of output was.
@@ -960,6 +1006,7 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 			 * makes the output look nicer.
 			 */
 			if (graph->prev_state == GRAPH_POST_MERGE &&
+			    graph->prev_edges_added > 0 &&
 			    graph->prev_commit_index < i)
 				strbuf_write_column(sb, col, '\\');
 			else
@@ -1031,8 +1078,14 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf
 				else
 					idx++;
 			}
+			if (graph->edges_added == 0)
+				strbuf_addch(sb, ' ');
+
 		} else if (seen_this) {
-			strbuf_write_column(sb, col, '\\');
+			if (graph->edges_added > 0)
+				strbuf_write_column(sb, col, '\\');
+			else
+				strbuf_write_column(sb, col, '|');
 			strbuf_addch(sb, ' ');
 		} else {
 			strbuf_write_column(sb, col, '|');
t/t4215-log-skewed-merges.sh +151 -0

155 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#26 lore raw

Re: [PATCH 07/11] graph: commit and post-merge lines for left-skewed merges

Quoted 257 lines · James Coglan via GitGitGadget

From: James Coglan <[email protected]>

Following the introduction of "left-skewed" merges, which are merges whose first parent fuses with another edge to its left, we have some more edge cases to deal with in the display of commit and post-merge lines.

The current graph code handles the following cases for edges appearing to the right of the commit (*) on commit lines. A 2-way merge is usually followed by vertical lines:

        | | |
        | * |
        | |\ \

An octopus merge (more than two parents) is always followed by edges sloping to the right:

        | |  \          | |    \
        | *-. \         | *---. \
        | |\ \ \        | |\ \ \ \

A 2-way merge is followed by a right-sloping edge if the commit line immediately follows a post-merge line for a commit that appears in the same column as the current commit, or any column to the left of that:

        | *             | * |
        | |\            | |\ \
        | * \           | | * \
        | |\ \          | | |\ \

This commit introduces the following new cases for commit lines. If a 2-way merge skews to the left, then the edges to its right are always vertical lines, even if the commit follows a post-merge line:

        | | |           | |\
        | * |           | * |
        |/| |           |/| |

A commit with 3 parents that skews left is followed by vertical edges:

        | | |
        | * |
        |/|\ \

If a 3-way left-skewed merge commit appears immediately after a post-merge line, then it may be followed the right-sloping edges, just like a 2-way merge that is not skewed.

        | |\
        | * \
        |/|\ \

Octopus merges with 4 or more parents that skew to the left will always be followed by right-sloping edges, because the existing columns need to expand around the merge.

        | |  \
        | *-. \
        |/|\ \ \

On post-merge lines, usually all edges following the current commit slope to the right:

        | * | |
        | |\ \ \

However, if the commit is a left-skewed 2-way merge, the edges to its right remain vertical. We also need to display a space after the vertical line descending from the commit marker, whereas this line would normally be followed by a backslash.

        | * | |
        |/| | |

If a left-skewed merge has more than 2 parents, then the edges to its right are still sloped as they bend around the edges introduced by the merge.

        | * | |
        |/|\ \ \

To handle these new cases, we need to know not just how many parents each commit has, but how many new columns it adds to the display; this quantity is recorded in the edges_added field for the current commit, and prev_edges_added field for the previous commit.

Here, "column" refers to visual columns, not the logical columns of the columns array. This is because even if all the commit's parents end up fusing with existing edges, they initially introduce distinct edges in the commit and post-merge lines before those edges collapse. For example, a 3-way merge whose 2nd and 3rd parents fuse with existing edges still introduces 2 visual columns that affect the display of edges to their right.

        | | |  \
        | | *-. \
        | | |\ \ \
        | |_|/ / /
        |/| | / /
        | | |/ /
        | |/| |
        | | | |

This merge does not introduce any *logical* columns; there are 4 edges before and after this commit once all edges have collapsed. But it does initially introduce 2 new edges that need to be accommodated by the edges to their right.

---
 graph.c                      |  63 +++++++++++++--
 t/t4215-log-skewed-merges.sh | 151 +++++++++++++++++++++++++++++++++++
 2 files changed, 209 insertions(+), 5 deletions(-)
graph.c +58 -5
index 9136173e03..fb2e42850f 100644
@@ -197,6 +197,46 @@ struct git_graph {
 	 * 		|/| | | | |		| | | | | *
 	 */
 	int merge_layout;
+	/*
+	 * The number of columns added to the graph by the current commit. For
+	 * 2-way and octopus merges, this is is usually one less than the
+	 * number of parents:
+	 *
+	 * 		| | |			| |    \
+	 *		| * |			| *---. \
+	 *		| |\ \			| |\ \ \ \
+	 *		| | | |         	| | | | | |
+	 *
+	 *		num_parents: 2		num_parents: 4
+	 *		edges_added: 1		edges_added: 3
+	 *
+	 * For left-skewed merges, the first parent fuses with its neighbor and
+	 * so one less column is added:
+	 *
+	 *		| | |			| |  \
+	 *		| * |			| *-. \
+	 *		|/| |			|/|\ \ \
+	 *		| | |			| | | | |
+	 *
+	 *		num_parents: 2		num_parents: 4
+	 *		edges_added: 0		edges_added: 2
+	 *
+	 * This number determines how edges to the right of the merge are
+	 * displayed in commit and post-merge lines; if no columns have been
+	 * added then a vertical line should be used where a right-tracking
+	 * line would otherwise be used.
+	 *
+	 *		| * \			| * |
+	 *		| |\ \			|/| |
+	 *		| | * \			| * |
+	 */
+	int edges_added;
+	/*
+	 * The number of columns added by the previous commit, which is used to
+	 * smooth edges appearing to the right of a commit in a commit line
+	 * following a post-merge line.
+	 */
+	int prev_edges_added;
 	/*
 	 * The maximum number of columns that can be stored in the columns
 	 * and new_columns arrays.  This is also half the number of entries
@@ -309,6 +349,8 @@ struct git_graph *graph_init(struct rev_info *opt)
 	graph->commit_index = 0;
 	graph->prev_commit_index = 0;
 	graph->merge_layout = 0;
+	graph->edges_added = 0;
+	graph->prev_edges_added = 0;
 	graph->num_columns = 0;
 	graph->num_new_columns = 0;
 	graph->mapping_size = 0;
@@ -670,6 +712,9 @@ void graph_update(struct git_graph *graph, struct commit *commit)
 	 */
 	graph_update_columns(graph);
 
+	graph->prev_edges_added = graph->edges_added;
+	graph->edges_added = graph->num_parents + graph->merge_layout - 2;
+
 	graph->expansion_row = 0;
 
 	/*
@@ -943,12 +988,13 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 
 			if (graph->num_parents > 2)
 				graph_draw_octopus_merge(graph, sb);
-		} else if (seen_this && (graph->num_parents > 2)) {
+		} else if (seen_this && (graph->edges_added > 1)) {
 			strbuf_write_column(sb, col, '\\');
-		} else if (seen_this && (graph->num_parents == 2)) {
+		} else if (seen_this && (graph->edges_added == 1)) {
 			/*
-			 * This is a 2-way merge commit.
-			 * There is no GRAPH_PRE_COMMIT stage for 2-way
+			 * This is either a right-skewed 2-way merge
+			 * commit, or a left-skewed 3-way merge.
+			 * There is no GRAPH_PRE_COMMIT stage for such
 			 * merges, so this is the first line of output
 			 * for this commit.  Check to see what the previous
 			 * line of output was.
@@ -960,6 +1006,7 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 			 * makes the output look nicer.
 			 */
 			if (graph->prev_state == GRAPH_POST_MERGE &&
+			    graph->prev_edges_added > 0 &&
 			    graph->prev_commit_index < i)
 				strbuf_write_column(sb, col, '\\');
 			else
@@ -1031,8 +1078,14 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf
 				else
 					idx++;
 			}
+			if (graph->edges_added == 0)
+				strbuf_addch(sb, ' ');
+
 		} else if (seen_this) {
-			strbuf_write_column(sb, col, '\\');
+			if (graph->edges_added > 0)
+				strbuf_write_column(sb, col, '\\');
+			else
+				strbuf_write_column(sb, col, '|');
 			strbuf_addch(sb, ' ');
 		} else {
 			strbuf_write_column(sb, col, '|');
t/t4215-log-skewed-merges.sh +22 -0
index cfaba40f97..e479d6e676 100755
@@ -39,4 +39,155 @@ test_expect_success 'log --graph with left-skewed merge' '
 	test_cmp expect actual
 '
 
+test_expect_success 'setup nested left-skewed merge' '
+	git checkout --orphan 1_p &&
+	test_commit 1_A &&
+	test_commit 1_B &&
+	test_commit 1_C &&
+	git checkout -b 1_q @^ && test_commit 1_D &&
+	git checkout 1_p && git merge --no-ff 1_q -m 1_E &&
+	git checkout -b 1_r @~3 && test_commit 1_F &&
+	git checkout 1_p && git merge --no-ff 1_r -m 1_G &&
+	git checkout @^^ && git merge --no-ff 1_p -m 1_H
+'
+
+cat > expect <<\EOF
+*   1_H
+|\
+| *   1_G
+| |\
+| | * 1_F
+| * | 1_E
+|/| |
+| * | 1_D
+* | | 1_C

t/t4215-log-skewed-merges.sh test_expect_success 'log --graph with left-skewed merge' ' · line 73

Quoted 10 lines
t/t4215-log-skewed-merges.sh +10 -0
+|/ /
+* | 1_B
+|/
+* 1_A
+EOF
+
+test_expect_success 'log --graph with nested left-skewed merge' '
+	git log --graph --pretty=tformat:%s | sed "s/ *$//" > actual &&
+	test_cmp expect actual
+'

I should have noticed in your earlier commits, but why don't you keep the output inside the test suite? You can start with "cat >expect <<-EOF" to have it ignore leading whitespace. Sorry if there's something else about this that is causing issues.

Quoted 121 lines
t/t4215-log-skewed-merges.sh +119 -0
+
+test_expect_success 'setup nested left-skewed merge following normal merge' '
+	git checkout --orphan 2_p &&
+	test_commit 2_A &&
+	test_commit 2_B &&
+	test_commit 2_C &&
+	git checkout -b 2_q @^^ &&
+	test_commit 2_D &&
+	test_commit 2_E &&
+	git checkout -b 2_r @^ && test_commit 2_F &&
+	git checkout 2_q &&
+	git merge --no-ff 2_r -m 2_G &&
+	git merge --no-ff 2_p^ -m 2_H &&
+	git checkout -b 2_s @^^ && git merge --no-ff 2_q -m 2_J &&
+	git checkout 2_p && git merge --no-ff 2_s -m 2_K
+'
+
+cat > expect <<\EOF
+*   2_K
+|\
+| *   2_J
+| |\
+| | *   2_H
+| | |\
+| | * | 2_G
+| |/| |
+| | * | 2_F
+| * | | 2_E
+| |/ /
+| * | 2_D
+* | | 2_C
+| |/
+|/|
+* | 2_B
+|/
+* 2_A
+EOF
+
+test_expect_success 'log --graph with nested left-skewed merge following normal merge' '
+	git log --graph --pretty=tformat:%s | sed "s/ *$//" > actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'setup nested right-skewed merge following left-skewed merge' '
+	git checkout --orphan 3_p &&
+	test_commit 3_A &&
+	git checkout -b 3_q &&
+	test_commit 3_B &&
+	test_commit 3_C &&
+	git checkout -b 3_r @^ &&
+	test_commit 3_D &&
+	git checkout 3_q && git merge --no-ff 3_r -m 3_E &&
+	git checkout 3_p && git merge --no-ff 3_q -m 3_F &&
+	git checkout 3_r && test_commit 3_G &&
+	git checkout 3_p && git merge --no-ff 3_r -m 3_H &&
+	git checkout @^^ && git merge --no-ff 3_p -m 3_J
+'
+
+cat > expect <<\EOF
+*   3_J
+|\
+| *   3_H
+| |\
+| | * 3_G
+| * | 3_F
+|/| |
+| * |   3_E
+| |\ \
+| | |/
+| | * 3_D
+| * | 3_C
+| |/
+| * 3_B
+|/
+* 3_A
+EOF
+
+test_expect_success 'log --graph with nested right-skewed merge following left-skewed merge' '
+	git log --graph --pretty=tformat:%s | sed "s/ *$//" > actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'setup right-skewed merge following a left-skewed one' '
+	git checkout --orphan 4_p &&
+	test_commit 4_A &&
+	test_commit 4_B &&
+	test_commit 4_C &&
+	git checkout -b 4_q @^^ && test_commit 4_D &&
+	git checkout -b 4_r 4_p^ && git merge --no-ff 4_q -m 4_E &&
+	git checkout -b 4_s 4_p^^ &&
+	git merge --no-ff 4_r -m 4_F &&
+	git merge --no-ff 4_p -m 4_G &&
+	git checkout @^^ && git merge --no-ff 4_s -m 4_H
+'
+
+cat > expect <<\EOF
+*   4_H
+|\
+| *   4_G
+| |\
+| * | 4_F
+|/| |
+| * |   4_E
+| |\ \
+| | * | 4_D
+| |/ /
+|/| |
+| | * 4_C
+| |/
+| * 4_B
+|/
+* 4_A
+EOF
+
+test_expect_success 'log --graph with right-skewed merge following a left-skewed one' '
+	git log --graph --date-order --pretty=tformat:%s | sed "s/ *$//" > actual &&
+	test_cmp expect actual
+'
+
 test_done
#27 lore raw

Re: [PATCH 07/11] graph: commit and post-merge lines for left-skewed merges

Quoted 265 lines · Derrick Stolee
Quoted 264 lines · James Coglan via GitGitGadget

From: James Coglan <[email protected]>

Following the introduction of "left-skewed" merges, which are merges whose first parent fuses with another edge to its left, we have some more edge cases to deal with in the display of commit and post-merge lines.

The current graph code handles the following cases for edges appearing to the right of the commit (*) on commit lines. A 2-way merge is usually followed by vertical lines:

        | | |
        | * |
        | |\ \

An octopus merge (more than two parents) is always followed by edges sloping to the right:

        | |  \          | |    \
        | *-. \         | *---. \
        | |\ \ \        | |\ \ \ \

A 2-way merge is followed by a right-sloping edge if the commit line immediately follows a post-merge line for a commit that appears in the same column as the current commit, or any column to the left of that:

        | *             | * |
        | |\            | |\ \
        | * \           | | * \
        | |\ \          | | |\ \

This commit introduces the following new cases for commit lines. If a 2-way merge skews to the left, then the edges to its right are always vertical lines, even if the commit follows a post-merge line:

        | | |           | |\
        | * |           | * |
        |/| |           |/| |

A commit with 3 parents that skews left is followed by vertical edges:

        | | |
        | * |
        |/|\ \

If a 3-way left-skewed merge commit appears immediately after a post-merge line, then it may be followed the right-sloping edges, just like a 2-way merge that is not skewed.

        | |\
        | * \
        |/|\ \

Octopus merges with 4 or more parents that skew to the left will always be followed by right-sloping edges, because the existing columns need to expand around the merge.

        | |  \
        | *-. \
        |/|\ \ \

On post-merge lines, usually all edges following the current commit slope to the right:

        | * | |
        | |\ \ \

However, if the commit is a left-skewed 2-way merge, the edges to its right remain vertical. We also need to display a space after the vertical line descending from the commit marker, whereas this line would normally be followed by a backslash.

        | * | |
        |/| | |

If a left-skewed merge has more than 2 parents, then the edges to its right are still sloped as they bend around the edges introduced by the merge.

        | * | |
        |/|\ \ \

To handle these new cases, we need to know not just how many parents each commit has, but how many new columns it adds to the display; this quantity is recorded in the edges_added field for the current commit, and prev_edges_added field for the previous commit.

Here, "column" refers to visual columns, not the logical columns of the columns array. This is because even if all the commit's parents end up fusing with existing edges, they initially introduce distinct edges in the commit and post-merge lines before those edges collapse. For example, a 3-way merge whose 2nd and 3rd parents fuse with existing edges still introduces 2 visual columns that affect the display of edges to their right.

        | | |  \
        | | *-. \
        | | |\ \ \
        | |_|/ / /
        |/| | / /
        | | |/ /
        | |/| |
        | | | |

This merge does not introduce any *logical* columns; there are 4 edges before and after this commit once all edges have collapsed. But it does initially introduce 2 new edges that need to be accommodated by the edges to their right.

---
 graph.c                      |  63 +++++++++++++--
 t/t4215-log-skewed-merges.sh | 151 +++++++++++++++++++++++++++++++++++
 2 files changed, 209 insertions(+), 5 deletions(-)
graph.c +58 -5
index 9136173e03..fb2e42850f 100644
@@ -197,6 +197,46 @@ struct git_graph {
 	 * 		|/| | | | |		| | | | | *
 	 */
 	int merge_layout;
+	/*
+	 * The number of columns added to the graph by the current commit. For
+	 * 2-way and octopus merges, this is is usually one less than the
+	 * number of parents:
+	 *
+	 * 		| | |			| |    \
+	 *		| * |			| *---. \
+	 *		| |\ \			| |\ \ \ \
+	 *		| | | |         	| | | | | |
+	 *
+	 *		num_parents: 2		num_parents: 4
+	 *		edges_added: 1		edges_added: 3
+	 *
+	 * For left-skewed merges, the first parent fuses with its neighbor and
+	 * so one less column is added:
+	 *
+	 *		| | |			| |  \
+	 *		| * |			| *-. \
+	 *		|/| |			|/|\ \ \
+	 *		| | |			| | | | |
+	 *
+	 *		num_parents: 2		num_parents: 4
+	 *		edges_added: 0		edges_added: 2
+	 *
+	 * This number determines how edges to the right of the merge are
+	 * displayed in commit and post-merge lines; if no columns have been
+	 * added then a vertical line should be used where a right-tracking
+	 * line would otherwise be used.
+	 *
+	 *		| * \			| * |
+	 *		| |\ \			|/| |
+	 *		| | * \			| * |
+	 */
+	int edges_added;
+	/*
+	 * The number of columns added by the previous commit, which is used to
+	 * smooth edges appearing to the right of a commit in a commit line
+	 * following a post-merge line.
+	 */
+	int prev_edges_added;
 	/*
 	 * The maximum number of columns that can be stored in the columns
 	 * and new_columns arrays.  This is also half the number of entries
@@ -309,6 +349,8 @@ struct git_graph *graph_init(struct rev_info *opt)
 	graph->commit_index = 0;
 	graph->prev_commit_index = 0;
 	graph->merge_layout = 0;
+	graph->edges_added = 0;
+	graph->prev_edges_added = 0;
 	graph->num_columns = 0;
 	graph->num_new_columns = 0;
 	graph->mapping_size = 0;
@@ -670,6 +712,9 @@ void graph_update(struct git_graph *graph, struct commit *commit)
 	 */
 	graph_update_columns(graph);
 
+	graph->prev_edges_added = graph->edges_added;
+	graph->edges_added = graph->num_parents + graph->merge_layout - 2;
+
 	graph->expansion_row = 0;
 
 	/*
@@ -943,12 +988,13 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 
 			if (graph->num_parents > 2)
 				graph_draw_octopus_merge(graph, sb);
-		} else if (seen_this && (graph->num_parents > 2)) {
+		} else if (seen_this && (graph->edges_added > 1)) {
 			strbuf_write_column(sb, col, '\\');
-		} else if (seen_this && (graph->num_parents == 2)) {
+		} else if (seen_this && (graph->edges_added == 1)) {
 			/*
-			 * This is a 2-way merge commit.
-			 * There is no GRAPH_PRE_COMMIT stage for 2-way
+			 * This is either a right-skewed 2-way merge
+			 * commit, or a left-skewed 3-way merge.
+			 * There is no GRAPH_PRE_COMMIT stage for such
 			 * merges, so this is the first line of output
 			 * for this commit.  Check to see what the previous
 			 * line of output was.
@@ -960,6 +1006,7 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 			 * makes the output look nicer.
 			 */
 			if (graph->prev_state == GRAPH_POST_MERGE &&
+			    graph->prev_edges_added > 0 &&
 			    graph->prev_commit_index < i)
 				strbuf_write_column(sb, col, '\\');
 			else
@@ -1031,8 +1078,14 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf
 				else
 					idx++;
 			}
+			if (graph->edges_added == 0)
+				strbuf_addch(sb, ' ');
+
 		} else if (seen_this) {
-			strbuf_write_column(sb, col, '\\');
+			if (graph->edges_added > 0)
+				strbuf_write_column(sb, col, '\\');
+			else
+				strbuf_write_column(sb, col, '|');
 			strbuf_addch(sb, ' ');
 		} else {
 			strbuf_write_column(sb, col, '|');
t/t4215-log-skewed-merges.sh +29 -0
index cfaba40f97..e479d6e676 100755
@@ -39,4 +39,155 @@ test_expect_success 'log --graph with left-skewed merge' '
 	test_cmp expect actual
 '
 
+test_expect_success 'setup nested left-skewed merge' '
+	git checkout --orphan 1_p &&
+	test_commit 1_A &&
+	test_commit 1_B &&
+	test_commit 1_C &&
+	git checkout -b 1_q @^ && test_commit 1_D &&
+	git checkout 1_p && git merge --no-ff 1_q -m 1_E &&
+	git checkout -b 1_r @~3 && test_commit 1_F &&
+	git checkout 1_p && git merge --no-ff 1_r -m 1_G &&
+	git checkout @^^ && git merge --no-ff 1_p -m 1_H
+'
+
+cat > expect <<\EOF
+*   1_H
+|\
+| *   1_G
+| |\
+| | * 1_F
+| * | 1_E
+|/| |
+| * | 1_D
+* | | 1_C
+|/ /
+* | 1_B
+|/
+* 1_A
+EOF
+
+test_expect_success 'log --graph with nested left-skewed merge' '
Quoted 8 lines

t/t4215-log-skewed-merges.sh test_expect_success 'log --graph with left-skewed merge' ' · line 73

Quoted 3 lines
t/t4215-log-skewed-merges.sh +3 -0
+	git log --graph --pretty=tformat:%s | sed "s/ *$//" > actual &&
+	test_cmp expect actual
+'

I should have noticed in your earlier commits, but why don't you keep the output inside the test suite? You can start with "cat >expect <<-EOF" to have it ignore leading whitespace. Sorry if there's something else about this that is causing issues.

I was following a pattern used in t/t4202-log.sh. I believe it was easier to debug these tests with the setup and expectations split into separate blocks, but I wouldn't be opposed to merging them.

Quoted 122 lines
Quoted 121 lines
t/t4215-log-skewed-merges.sh +119 -0
+
+test_expect_success 'setup nested left-skewed merge following normal merge' '
+	git checkout --orphan 2_p &&
+	test_commit 2_A &&
+	test_commit 2_B &&
+	test_commit 2_C &&
+	git checkout -b 2_q @^^ &&
+	test_commit 2_D &&
+	test_commit 2_E &&
+	git checkout -b 2_r @^ && test_commit 2_F &&
+	git checkout 2_q &&
+	git merge --no-ff 2_r -m 2_G &&
+	git merge --no-ff 2_p^ -m 2_H &&
+	git checkout -b 2_s @^^ && git merge --no-ff 2_q -m 2_J &&
+	git checkout 2_p && git merge --no-ff 2_s -m 2_K
+'
+
+cat > expect <<\EOF
+*   2_K
+|\
+| *   2_J
+| |\
+| | *   2_H
+| | |\
+| | * | 2_G
+| |/| |
+| | * | 2_F
+| * | | 2_E
+| |/ /
+| * | 2_D
+* | | 2_C
+| |/
+|/|
+* | 2_B
+|/
+* 2_A
+EOF
+
+test_expect_success 'log --graph with nested left-skewed merge following normal merge' '
+	git log --graph --pretty=tformat:%s | sed "s/ *$//" > actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'setup nested right-skewed merge following left-skewed merge' '
+	git checkout --orphan 3_p &&
+	test_commit 3_A &&
+	git checkout -b 3_q &&
+	test_commit 3_B &&
+	test_commit 3_C &&
+	git checkout -b 3_r @^ &&
+	test_commit 3_D &&
+	git checkout 3_q && git merge --no-ff 3_r -m 3_E &&
+	git checkout 3_p && git merge --no-ff 3_q -m 3_F &&
+	git checkout 3_r && test_commit 3_G &&
+	git checkout 3_p && git merge --no-ff 3_r -m 3_H &&
+	git checkout @^^ && git merge --no-ff 3_p -m 3_J
+'
+
+cat > expect <<\EOF
+*   3_J
+|\
+| *   3_H
+| |\
+| | * 3_G
+| * | 3_F
+|/| |
+| * |   3_E
+| |\ \
+| | |/
+| | * 3_D
+| * | 3_C
+| |/
+| * 3_B
+|/
+* 3_A
+EOF
+
+test_expect_success 'log --graph with nested right-skewed merge following left-skewed merge' '
+	git log --graph --pretty=tformat:%s | sed "s/ *$//" > actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'setup right-skewed merge following a left-skewed one' '
+	git checkout --orphan 4_p &&
+	test_commit 4_A &&
+	test_commit 4_B &&
+	test_commit 4_C &&
+	git checkout -b 4_q @^^ && test_commit 4_D &&
+	git checkout -b 4_r 4_p^ && git merge --no-ff 4_q -m 4_E &&
+	git checkout -b 4_s 4_p^^ &&
+	git merge --no-ff 4_r -m 4_F &&
+	git merge --no-ff 4_p -m 4_G &&
+	git checkout @^^ && git merge --no-ff 4_s -m 4_H
+'
+
+cat > expect <<\EOF
+*   4_H
+|\
+| *   4_G
+| |\
+| * | 4_F
+|/| |
+| * |   4_E
+| |\ \
+| | * | 4_D
+| |/ /
+|/| |
+| | * 4_C
+| |/
+| * 4_B
+|/
+* 4_A
+EOF
+
+test_expect_success 'log --graph with right-skewed merge following a left-skewed one' '
+	git log --graph --date-order --pretty=tformat:%s | sed "s/ *$//" > actual &&
+	test_cmp expect actual
+'
+
 test_done
#28 lore raw

Re: [PATCH 07/11] graph: commit and post-merge lines for left-skewed merges

Quoted 8 lines · James Coglan
Quoted 4 lines

I should have noticed in your earlier commits, but why don't you keep the output inside the test suite? You can start with "cat >expect <<-EOF" to have it ignore leading whitespace. Sorry if there's something else about this that is causing issues.

I was following a pattern used in t/t4202-log.sh. I believe it was easier to debug these tests with the setup and expectations split into separate blocks, but I wouldn't be opposed to merging them.

Some of the older tests used that style, but we've been slowly modernizing (I know, it's hard to pick up the style by example in such cases!). The usual style these days is making sure everything goes in a test_expect_* block, with "<<-" to indent here-documents.

Another minor style nit that you picked up from t4202:

Quoted 1 line
Quoted 1 line
Quoted 1 line

+cat > expect <<\EOF

We'd omit the space after ">" here.

-Peff

#29 lore raw

Re: [PATCH 07/11] graph: commit and post-merge lines for left-skewed merges

Quoted 16 lines · Jeff King
Quoted 8 lines · James Coglan
Quoted 4 lines

I should have noticed in your earlier commits, but why don't you keep the output inside the test suite? You can start with "cat >expect <<-EOF" to have it ignore leading whitespace. Sorry if there's something else about this that is causing issues.

I was following a pattern used in t/t4202-log.sh. I believe it was easier to debug these tests with the setup and expectations split into separate blocks, but I wouldn't be opposed to merging them.

Some of the older tests used that style, but we've been slowly modernizing (I know, it's hard to pick up the style by example in such cases!). The usual style these days is making sure everything goes in a test_expect_* block, with "<<-" to indent here-documents.

Quoted 5 lines

Another minor style nit that you picked up from t4202:

Quoted 1 line
Quoted 1 line
Quoted 1 line

+cat > expect <<\EOF

We'd omit the space after ">" here.

Thanks, I've now made the style changes you've suggested on my branch. How should I go about sharing the current state of my patch series after I've incorporated all the changes suggested here? Should I post them as replies on this thread, or start a new pull request via GitGitGadget?

#30 lore raw

Re: [PATCH 07/11] graph: commit and post-merge lines for left-skewed merges

Quoted 17 lines · James Coglan
Quoted 16 lines · Jeff King
Quoted 8 lines · James Coglan
Quoted 4 lines

I should have noticed in your earlier commits, but why don't you keep the output inside the test suite? You can start with "cat >expect <<-EOF" to have it ignore leading whitespace. Sorry if there's something else about this that is causing issues.

I was following a pattern used in t/t4202-log.sh. I believe it was easier to debug these tests with the setup and expectations split into separate blocks, but I wouldn't be opposed to merging them.

Some of the older tests used that style, but we've been slowly modernizing (I know, it's hard to pick up the style by example in such cases!). The usual style these days is making sure everything goes in a test_expect_* block, with "<<-" to indent here-documents.

Quoted 7 lines
Quoted 5 lines

Another minor style nit that you picked up from t4202:

Quoted 1 line
Quoted 1 line
Quoted 1 line

+cat > expect <<\EOF

We'd omit the space after ">" here.

Thanks, I've now made the style changes you've suggested on my branch. How should I go about sharing the current state of my patch series after I've incorporated all the changes suggested here? Should I post them as replies on this thread, or start a new pull request via GitGitGadget?

Since you sent v1 via GitGitGadget, all you need to do is add another "/submit" comment on the same PR and it will send a v2 to this thread.

It will auto-generate the range-diff from v1 and append it to your cover letter.

-Stolee

#31 lore raw

Re: [PATCH 07/11] graph: commit and post-merge lines for left-skewed merges

In reply to James Coglan

Hi James,

Quoted 4 lines · James Coglan

[...] How should I go about sharing the current state of my patch series after I've incorporated all the changes suggested here? Should I post them as replies on this thread, or start a new pull request via GitGitGadget?

Just force-push the branch, and tell GitGitGadget to /submit. It will take care of tagging a new iteration, generating a range-diff, and sending it off to the list.

Thanks,
Dscho

#32 lore raw

PATCH 8/11 graph: rename `new_mapping` to `old_mapping`

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

The change I'm about to make requires being able to inspect the mapping array that was used to render the last GRAPH_COLLAPSING line while rendering a GRAPH_COMMIT line. The new_mapping array currently exists as a pre-allocated space for computing the next mapping array during graph_output_collapsing_line(), but we can repurpose it to let us see the previous mapping state.

To support this use it will make more sense if this array is named old_mapping, as it will contain the mapping data for the previous line we rendered, at the point we're rendering a commit line.

---
 graph.c | 54 +++++++++++++++++++++++++++---------------------------
 1 file changed, 27 insertions(+), 27 deletions(-)
graph.c +27 -27
index fb2e42850f..b0e31e23ca 100644
@@ -240,7 +240,7 @@ struct git_graph {
 	/*
 	 * The maximum number of columns that can be stored in the columns
 	 * and new_columns arrays.  This is also half the number of entries
-	 * that can be stored in the mapping and new_mapping arrays.
+	 * that can be stored in the mapping and old_mapping arrays.
 	 */
 	int column_capacity;
 	/*
@@ -283,7 +283,7 @@ struct git_graph {
 	 * of the git_graph simply so we don't have to allocate a new
 	 * temporary array each time we have to output a collapsing line.
 	 */
-	int *new_mapping;
+	int *old_mapping;
 	/*
 	 * The current default column color being used.  This is
 	 * stored as an index into the array column_colors.
@@ -369,7 +369,7 @@ struct git_graph *graph_init(struct rev_info *opt)
 	ALLOC_ARRAY(graph->columns, graph->column_capacity);
 	ALLOC_ARRAY(graph->new_columns, graph->column_capacity);
 	ALLOC_ARRAY(graph->mapping, 2 * graph->column_capacity);
-	ALLOC_ARRAY(graph->new_mapping, 2 * graph->column_capacity);
+	ALLOC_ARRAY(graph->old_mapping, 2 * graph->column_capacity);
 
 	/*
 	 * The diff output prefix callback, with this we can make
@@ -399,7 +399,7 @@ static void graph_ensure_capacity(struct git_graph *graph, int num_columns)
 	REALLOC_ARRAY(graph->columns, graph->column_capacity);
 	REALLOC_ARRAY(graph->new_columns, graph->column_capacity);
 	REALLOC_ARRAY(graph->mapping, graph->column_capacity * 2);
-	REALLOC_ARRAY(graph->new_mapping, graph->column_capacity * 2);
+	REALLOC_ARRAY(graph->old_mapping, graph->column_capacity * 2);
 }
 
 /*
@@ -1116,13 +1116,18 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 	int horizontal_edge_target = -1;
 
 	/*
-	 * Clear out the new_mapping array
+	 * Swap the mapping and old_mapping arrays
+	 */
+	SWAP(graph->mapping, graph->old_mapping);
+
+	/*
+	 * Clear out the mapping array
 	 */
 	for (i = 0; i < graph->mapping_size; i++)
-		graph->new_mapping[i] = -1;
+		graph->mapping[i] = -1;
 
 	for (i = 0; i < graph->mapping_size; i++) {
-		int target = graph->mapping[i];
+		int target = graph->old_mapping[i];
 		if (target < 0)
 			continue;
 
@@ -1143,14 +1148,14 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 			 * This column is already in the
 			 * correct place
 			 */
-			assert(graph->new_mapping[i] == -1);
-			graph->new_mapping[i] = target;
-		} else if (graph->new_mapping[i - 1] < 0) {
+			assert(graph->mapping[i] == -1);
+			graph->mapping[i] = target;
+		} else if (graph->mapping[i - 1] < 0) {
 			/*
 			 * Nothing is to the left.
 			 * Move to the left by one
 			 */
-			graph->new_mapping[i - 1] = target;
+			graph->mapping[i - 1] = target;
 			/*
 			 * If there isn't already an edge moving horizontally
 			 * select this one.
@@ -1166,9 +1171,9 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 				 * line.
 				 */
 				for (j = (target * 2)+3; j < (i - 2); j += 2)
-					graph->new_mapping[j] = target;
+					graph->mapping[j] = target;
 			}
-		} else if (graph->new_mapping[i - 1] == target) {
+		} else if (graph->mapping[i - 1] == target) {
 			/*
 			 * There is a branch line to our left
 			 * already, and it is our target.  We
@@ -1176,7 +1181,7 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 			 * the same parent commit.
 			 *
 			 * We don't have to add anything to the
-			 * output or new_mapping, since the
+			 * output or mapping, since the
 			 * existing branch line has already taken
 			 * care of it.
 			 */
@@ -1192,10 +1197,10 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 			 * The branch to the left of that space
 			 * should be our eventual target.
 			 */
-			assert(graph->new_mapping[i - 1] > target);
-			assert(graph->new_mapping[i - 2] < 0);
-			assert(graph->new_mapping[i - 3] == target);
-			graph->new_mapping[i - 2] = target;
+			assert(graph->mapping[i - 1] > target);
+			assert(graph->mapping[i - 2] < 0);
+			assert(graph->mapping[i - 3] == target);
+			graph->mapping[i - 2] = target;
 			/*
 			 * Mark this branch as the horizontal edge to
 			 * prevent any other edges from moving
@@ -1209,14 +1214,14 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 	/*
 	 * The new mapping may be 1 smaller than the old mapping
 	 */
-	if (graph->new_mapping[graph->mapping_size - 1] < 0)
+	if (graph->mapping[graph->mapping_size - 1] < 0)
 		graph->mapping_size--;
 
 	/*
 	 * Output out a line based on the new mapping info
 	 */
 	for (i = 0; i < graph->mapping_size; i++) {
-		int target = graph->new_mapping[i];
+		int target = graph->mapping[i];
 		if (target < 0)
 			strbuf_addch(sb, ' ');
 		else if (target * 2 == i)
@@ -1229,12 +1234,12 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 				 * won't continue into the next line.
 				 */
 				if (i != (target * 2)+3)
-					graph->new_mapping[i] = -1;
+					graph->mapping[i] = -1;
 				used_horizontal = 1;
 			strbuf_write_column(sb, &graph->new_columns[target], '_');
 		} else {
 			if (used_horizontal && i < horizontal_edge)
-				graph->new_mapping[i] = -1;
+				graph->mapping[i] = -1;
 			strbuf_write_column(sb, &graph->new_columns[target], '/');
 
 		}
@@ -1242,11 +1247,6 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 
 	graph_pad_horizontally(graph, sb);
 
-	/*
-	 * Swap mapping and new_mapping
-	 */
-	SWAP(graph->mapping, graph->new_mapping);
-
 	/*
 	 * If graph->mapping indicates that all of the branch lines
 	 * are already in the correct positions, we are done.
Signature · 1 line
gitgitgadget
#33 lore raw

PATCH 9/11 graph: smooth appearance of collapsing edges on commit lines

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

When a graph contains edges that are in the process of collapsing to the left, but those edges cross a commit line, the effect is that the edges have a jagged appearance:

        *
        |\
        | *
        |  \
        *-. \
        |\ \ \
        | | * |
        | * | |
        | |/ /
        * | |
        |/ /
        * |
        |/
        *

We already takes steps to smooth edges like this when they're expanding; when an edge appears to the right of a merge commit marker on a GRAPH_COMMIT line immediately following a GRAPH_POST_MERGE line, we render it as a \:

        * \
        |\ \
        | * \
        | |\ \

We can make a similar improvement to collapsing edges, making them easier to follow and giving the overall graph a feeling of increased symmetry:

        *
        |\
        | *
        |  \
        *-. \
        |\ \ \
        | | * |
        | * | |
        | |/ /
        * / /
        |/ /
        * /
        |/
        *

To do this, we introduce a new special case for edges on GRAPH_COMMIT lines that immediately follow a GRAPH_COLLAPSING line. By retaining a copy of the mapping array used to render the GRAPH_COLLAPSING line in the old_mapping array, we can determine that an edge is collapsing through the GRAPH_COMMIT line and should be smoothed.

---
 graph.c                                    | 17 +++++++++++++----
 t/t3430-rebase-merges.sh                   |  2 +-
 t/t4202-log.sh                             |  2 +-
 t/t4214-log-graph-octopus.sh               | 16 ++++++++--------
 t/t4215-log-skewed-merges.sh               |  2 +-
 t/t6016-rev-list-graph-simplify-history.sh |  4 ++--
 6 files changed, 26 insertions(+), 17 deletions(-)
graph.c +13 -4
index b0e31e23ca..6391e393ec 100644
@@ -278,10 +278,10 @@ struct git_graph {
 	 */
 	int *mapping;
 	/*
-	 * A temporary array for computing the next mapping state
-	 * while we are outputting a mapping line.  This is stored as part
-	 * of the git_graph simply so we don't have to allocate a new
-	 * temporary array each time we have to output a collapsing line.
+	 * A copy of the contents of the mapping array from the last commit,
+	 * which we use to improve the display of columns that are tracking
+	 * from right to left through a commit line.  We also use this to
+	 * avoid allocating a fresh array when we compute the next mapping.
 	 */
 	int *old_mapping;
 	/*
@@ -1011,6 +1011,10 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 				strbuf_write_column(sb, col, '\\');
 			else
 				strbuf_write_column(sb, col, '|');
+		} else if (graph->prev_state == GRAPH_COLLAPSING &&
+			   graph->old_mapping[2 * i + 1] == i &&
+			   graph->mapping[2 * i] < i) {
+			strbuf_write_column(sb, col, '/');
 		} else {
 			strbuf_write_column(sb, col, '|');
 		}
@@ -1211,6 +1215,11 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 		}
 	}
 
+	/*
+	 * Copy the current mapping array into old_mapping
+	 */
+	COPY_ARRAY(graph->old_mapping, graph->mapping, graph->mapping_size);
+
 	/*
 	 * The new mapping may be 1 smaller than the old mapping
 	 */
t/t3430-rebase-merges.sh +1 -1
index 7b6c4847ad..051351ecdf 100755
@@ -402,7 +402,7 @@ test_expect_success 'octopus merges' '
 	| | * three
 	| * | two
 	| |/
-	* | one
+	* / one
 	|/
 	o before-octopus
 	EOF
t/t4202-log.sh +1 -1
index c20209324c..c7aa0532c1 100755
@@ -667,7 +667,7 @@ cat > expect <<\EOF
 * | | fifth
 * | | fourth
 |/ /
-* | third
+* / third
 |/
 * second
 * initial
t/t4214-log-graph-octopus.sh +8 -8
index 40f420877b..9ada687628 100755
@@ -12,9 +12,9 @@ test_expect_success 'set up merge history' '
 	| | | * 4
 	| | * | 3
 	| | |/
-	| * | 2
+	| * / 2
 	| |/
-	* | 1
+	* / 1
 	|/
 	* initial
 	EOF
@@ -25,9 +25,9 @@ test_expect_success 'set up merge history' '
 	<RED>|<RESET> <YELLOW>|<RESET> <BLUE>|<RESET> * 4
 	<RED>|<RESET> <YELLOW>|<RESET> * <MAGENTA>|<RESET> 3
 	<RED>|<RESET> <YELLOW>|<RESET> <MAGENTA>|<RESET><MAGENTA>/<RESET>
-	<RED>|<RESET> * <MAGENTA>|<RESET> 2
+	<RED>|<RESET> * <MAGENTA>/<RESET> 2
 	<RED>|<RESET> <MAGENTA>|<RESET><MAGENTA>/<RESET>
-	* <MAGENTA>|<RESET> 1
+	* <MAGENTA>/<RESET> 1
 	<MAGENTA>|<RESET><MAGENTA>/<RESET>
 	* initial
 	EOF
@@ -68,9 +68,9 @@ test_expect_success 'log --graph with normal octopus merge, no color' '
 	| | | * 4
 	| | * | 3
 	| | |/
-	| * | 2
+	| * / 2
 	| |/
-	* | 1
+	* / 1
 	|/
 	* initial
 	EOF
@@ -86,9 +86,9 @@ test_expect_success 'log --graph with normal octopus merge with colors' '
 	<RED>|<RESET> <GREEN>|<RESET> <YELLOW>|<RESET> * 4
 	<RED>|<RESET> <GREEN>|<RESET> * <BLUE>|<RESET> 3
 	<RED>|<RESET> <GREEN>|<RESET> <BLUE>|<RESET><BLUE>/<RESET>
-	<RED>|<RESET> * <BLUE>|<RESET> 2
+	<RED>|<RESET> * <BLUE>/<RESET> 2
 	<RED>|<RESET> <BLUE>|<RESET><BLUE>/<RESET>
-	* <BLUE>|<RESET> 1
+	* <BLUE>/<RESET> 1
 	<BLUE>|<RESET><BLUE>/<RESET>
 	* initial
 	EOF
t/t4215-log-skewed-merges.sh +1 -1
index e479d6e676..b739268e5e 100755
@@ -62,7 +62,7 @@ cat > expect <<\EOF
 | * | 1_D
 * | | 1_C
 |/ /
-* | 1_B
+* / 1_B
 |/
 * 1_A
 EOF
t/t6016-rev-list-graph-simplify-history.sh +2 -2
index f7181d1d6a..ca1682f29b 100755
@@ -154,7 +154,7 @@ test_expect_success '--graph --full-history -- bar.txt' '
 	echo "* |   $A4" >> expected &&
 	echo "|\\ \\  " >> expected &&
 	echo "| |/  " >> expected &&
-	echo "* | $A3" >> expected &&
+	echo "* / $A3" >> expected &&
 	echo "|/  " >> expected &&
 	echo "* $A2" >> expected &&
 	git rev-list --graph --full-history --all -- bar.txt > actual &&
@@ -255,7 +255,7 @@ test_expect_success '--graph --boundary ^C3' '
 	echo "* | | | $A3" >> expected &&
 	echo "o | | | $A2" >> expected &&
 	echo "|/ / /  " >> expected &&
-	echo "o | | $A1" >> expected &&
+	echo "o / / $A1" >> expected &&
 	echo " / /  " >> expected &&
 	echo "| o $C3" >> expected &&
 	echo "|/  " >> expected &&
Signature · 1 line
gitgitgadget
#34 lore raw

PATCH 10/11 graph: flatten edges that join to their right neighbor

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

When a merge commit is printed and its final parent is the same commit that occupies the column to the right of the merge, this results in a kink in the displayed edges:

        * |
        |\ \
        | |/
        | *

Graphs containing these shapes can be hard to read, as the expansion to the right followed immediately by collapsing back to the left creates a lot of zig-zagging edges, especially when many columns are present.

We can improve this by eliminating the zig-zag and having the merge's final parent edge fuse immediately with its neighbor:

        * |
        |\|
        | *

This reduces the horizontal width for the current commit by 2, and requires one less row, making the graph display more compact. Taken in combination with other graph-smoothing enhancements, it greatly compresses the space needed to display certain histories:

*
|\
| * *
| |\ |\
| | * | *
| | | | |\
| | \ | | *
| *-. \ | * |
| |\ \ \ => |/|\|
|/ / / / | | *
| | | / | * |
| | |/ | |/
| | * * /
| * | |/
| |/ *
* |
|/
*

One of the test cases here cannot be correctly rendered in Git v2.23.0; it produces this output following commit E:

        | | *-. \   5_E
        | | |\ \ \
        | |/ / / /
        | | | / _
        | |_|/
        |/| |

The new implementation makes sure that the rightmost edge in this history is not left dangling as above.

---
 graph.c                                    | 34 ++++++---
 t/t4215-log-skewed-merges.sh               | 80 +++++++++++++++++++++-
 t/t6016-rev-list-graph-simplify-history.sh | 30 ++++----
 3 files changed, 116 insertions(+), 28 deletions(-)
graph.c +26 -8
index 6391e393ec..7dd2fab625 100644
@@ -538,8 +538,24 @@ static void graph_insert_into_new_columns(struct git_graph *graph,
 		shift = (dist > 1) ? 2 * dist - 3 : 1;
 
 		graph->merge_layout = (dist > 0) ? 0 : 1;
+		graph->edges_added = graph->num_parents + graph->merge_layout  - 2;
+
 		mapping_idx = graph->width + (graph->merge_layout - 1) * shift;
 		graph->width += 2 * graph->merge_layout;
+
+	} else if (graph->edges_added > 0 && i == graph->mapping[graph->width - 2]) {
+		/*
+		 * If some columns have been added by a merge, but this commit
+		 * was found in the last existing column, then adjust the
+		 * numbers so that the two edges immediately join, i.e.:
+		 *
+		 *		* |		* |
+		 *		|\ \	=>	|\|
+		 *		| |/		| *
+		 *		| *
+		 */
+		mapping_idx = graph->width - 2;
+		graph->edges_added = -1;
 	} else {
 		mapping_idx = graph->width;
 		graph->width += 2;
@@ -585,6 +601,8 @@ static void graph_update_columns(struct git_graph *graph)
 		graph->mapping[i] = -1;
 
 	graph->width = 0;
+	graph->prev_edges_added = graph->edges_added;
+	graph->edges_added = 0;
 
 	/*
 	 * Populate graph->new_columns and graph->mapping
@@ -712,9 +730,6 @@ void graph_update(struct git_graph *graph, struct commit *commit)
 	 */
 	graph_update_columns(graph);
 
-	graph->prev_edges_added = graph->edges_added;
-	graph->edges_added = graph->num_parents + graph->merge_layout - 2;
-
 	graph->expansion_row = 0;
 
 	/*
@@ -1039,7 +1054,7 @@ const char merge_chars[] = {'/', '|', '\\'};
 static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf *sb)
 {
 	int seen_this = 0;
-	int i;
+	int i, j;
 
 	struct commit_list *first_parent = first_interesting_parent(graph);
 	int seen_parent = 0;
@@ -1071,16 +1086,19 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf
 			char c;
 			seen_this = 1;
 
-			for (; parents; parents = next_interesting_parent(graph, parents)) {
+			for (j = 0; j < graph->num_parents; j++) {
 				par_column = graph_find_new_column_by_commit(graph, parents->item);
 				assert(par_column >= 0);
 
 				c = merge_chars[idx];
 				strbuf_write_column(sb, &graph->new_columns[par_column], c);
-				if (idx == 2)
-					strbuf_addch(sb, ' ');
-				else
+				if (idx == 2) {
+					if (graph->edges_added > 0 || j < graph->num_parents - 1)
+						strbuf_addch(sb, ' ');
+				} else {
 					idx++;
+				}
+				parents = next_interesting_parent(graph, parents);
 			}
 			if (graph->edges_added == 0)
 				strbuf_addch(sb, ' ');
t/t4215-log-skewed-merges.sh +77 -3

90 lines not on this page · raw message

t/t6016-rev-list-graph-simplify-history.sh +13 -17
index ca1682f29b..f5e6e92f5b 100755
@@ -67,11 +67,10 @@ test_expect_success '--graph --all' '
 	echo "| * $C4" >> expected &&
 	echo "| * $C3" >> expected &&
 	echo "* | $A5" >> expected &&
-	echo "| |     " >> expected &&
-	echo "|  \\    " >> expected &&
-	echo "*-. \\   $A4" >> expected &&
-	echo "|\\ \\ \\  " >> expected &&
-	echo "| | |/  " >> expected &&
+	echo "| |   " >> expected &&
+	echo "|  \\  " >> expected &&
+	echo "*-. | $A4" >> expected &&
+	echo "|\\ \\| " >> expected &&
 	echo "| | * $C2" >> expected &&
 	echo "| | * $C1" >> expected &&
 	echo "| * | $B2" >> expected &&
@@ -97,11 +96,10 @@ test_expect_success '--graph --simplify-by-decoration' '
 	echo "| * $C4" >> expected &&
 	echo "| * $C3" >> expected &&
 	echo "* | $A5" >> expected &&
-	echo "| |     " >> expected &&
-	echo "|  \\    " >> expected &&
-	echo "*-. \\   $A4" >> expected &&
-	echo "|\\ \\ \\  " >> expected &&
-	echo "| | |/  " >> expected &&
+	echo "| |   " >> expected &&
+	echo "|  \\  " >> expected &&
+	echo "*-. | $A4" >> expected &&
+	echo "|\\ \\| " >> expected &&
 	echo "| | * $C2" >> expected &&
 	echo "| | * $C1" >> expected &&
 	echo "| * | $B2" >> expected &&
@@ -131,9 +129,8 @@ test_expect_success '--graph --simplify-by-decoration prune branch B' '
 	echo "| * $C4" >> expected &&
 	echo "| * $C3" >> expected &&
 	echo "* | $A5" >> expected &&
-	echo "* |   $A4" >> expected &&
-	echo "|\\ \\  " >> expected &&
-	echo "| |/  " >> expected &&
+	echo "* | $A4" >> expected &&
+	echo "|\\| " >> expected &&
 	echo "| * $C2" >> expected &&
 	echo "| * $C1" >> expected &&
 	echo "* | $A3" >> expected &&
@@ -151,10 +148,9 @@ test_expect_success '--graph --full-history -- bar.txt' '
 	echo "|\\  " >> expected &&
 	echo "| * $C4" >> expected &&
 	echo "* | $A5" >> expected &&
-	echo "* |   $A4" >> expected &&
-	echo "|\\ \\  " >> expected &&
-	echo "| |/  " >> expected &&
-	echo "* / $A3" >> expected &&
+	echo "* | $A4" >> expected &&
+	echo "|\\| " >> expected &&
+	echo "* | $A3" >> expected &&
 	echo "|/  " >> expected &&
 	echo "* $A2" >> expected &&
 	git rev-list --graph --full-history --all -- bar.txt > actual &&
Signature · 1 line
gitgitgadget
#35 lore raw

PATCH 11/11 graph: fix coloring of octopus dashes

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

In 04005834ed ("log: fix coloring of certain octopus merge shapes", 2018-09-01) there is a fix for the coloring of dashes following an octopus merge. It makes a distinction between the case where all parents introduce a new column, versus the case where the first parent collapses into an existing column:

        | *-.           | *-.
        | |\ \          | |\ \
        | | | |         |/ / /

The latter case means that the columns for the merge parents begin one place to the left in the new_columns array compared to the former case.

However, the implementation only works if the commit's parents are kept in order as they map onto the visual columns, as we get the colors by iterating over new_columns as we print the dashes. In general, the commit's parents can arbitrarily merge with existing columns, and change their ordering in the process.

For example, in the following diagram, the number of each column indicates which commit parent appears in each column.

        | | *---.
        | | |\ \ \
        | | |/ / /
        | |/| | /
        | |_|_|/
        |/| | |
        3 1 0 2

If the columns are colored (red, green, yellow, blue), then the dashes will currently be colored yellow and blue, whereas they should be blue and red.

To fix this, we need to look up each column in the mapping array, which before the GRAPH_COLLAPSING state indicates which logical column is displayed in each visual column. This implementation is simpler as it doesn't have any edge cases, and it also handles how left-skewed first parents are now displayed:

        | *-.
        |/|\ \
        | | | |
        0 1 2 3

The color of the first dashes is always the color found in mapping two columns to the right of the commit symbol. Because commits are displayed after all edges have been collapsed together and the visual columns match the logical ones, we can find the visual offset of the commit symbol using commit_index.

---
 graph.c                      | 71 +++++++++++++++++++-----------------
 t/t4214-log-graph-octopus.sh | 59 ++++++++++++++++++++++++++++--
 2 files changed, 92 insertions(+), 38 deletions(-)
graph.c +37 -34
index 7dd2fab625..908f257018 100644
@@ -665,6 +665,11 @@ static void graph_update_columns(struct git_graph *graph)
 		graph->mapping_size--;
 }
 
+static int graph_num_dashed_parents(struct git_graph *graph)
+{
+	return graph->num_parents + graph->merge_layout - 3;
+}
+
 static int graph_num_expansion_rows(struct git_graph *graph)
 {
 	/*
@@ -687,7 +692,7 @@ static int graph_num_expansion_rows(struct git_graph *graph)
 	 * 		| * \
 	 * 		|/|\ \
 	 */
-	return (graph->num_parents + graph->merge_layout - 3) * 2;
+	return graph_num_dashed_parents(graph) * 2;
 }
 
 static int graph_needs_pre_commit_line(struct git_graph *graph)
@@ -930,47 +935,45 @@ static void graph_output_commit_char(struct git_graph *graph, struct strbuf *sb)
 static void graph_draw_octopus_merge(struct git_graph *graph, struct strbuf *sb)
 {
 	/*
-	 * Here dashless_parents represents the number of parents which don't
-	 * need to have dashes (the edges labeled "0" and "1").  And
-	 * dashful_parents are the remaining ones.
+	 * The parents of a merge commit can be arbitrarily reordered as they
+	 * are mapped onto display columns, for example this is a valid merge:
 	 *
-	 * | *---.
-	 * | |\ \ \
-	 * | | | | |
-	 * x 0 1 2 3
+	 *	| | *---.
+	 *	| | |\ \ \
+	 *	| | |/ / /
+	 *	| |/| | /
+	 *	| |_|_|/
+	 *	|/| | |
+	 *	3 1 0 2
 	 *
-	 */
-	const int dashless_parents = 3 - graph->merge_layout;
-	int dashful_parents = graph->num_parents - dashless_parents;
-
-	/*
-	 * Usually, we add one new column for each parent (like the diagram
-	 * above) but sometimes the first parent goes into an existing column,
-	 * like this:
+	 * The numbers denote which parent of the merge each visual column
+	 * corresponds to; we can't assume that the parents will initially
+	 * display in the order given by new_columns.
 	 *
-	 * | *-.
-	 * |/|\ \
-	 * | | | |
-	 * x 0 1 2
+	 * To find the right color for each dash, we need to consult the
+	 * mapping array, starting from the column 2 places to the right of the
+	 * merge commit, and use that to find out which logical column each
+	 * edge will collapse to.
 	 *
-	 * In which case the number of parents will be one greater than the
-	 * number of added columns.
+	 * Commits are rendered once all edges have collapsed to their correct
+	 * logcial column, so commit_index gives us the right visual offset for
+	 * the merge commit.
 	 */
-	int added_cols = (graph->num_new_columns - graph->num_columns);
-	int parent_in_old_cols = graph->num_parents - added_cols;
 
-	/*
-	 * In both cases, commit_index corresponds to the edge labeled "0".
-	 */
-	int first_col = graph->commit_index + dashless_parents
-	    - parent_in_old_cols;
+	int i, j;
+	struct column *col;
 
-	int i;
-	for (i = 0; i < dashful_parents; i++) {
-		strbuf_write_column(sb, &graph->new_columns[i+first_col], '-');
-		strbuf_write_column(sb, &graph->new_columns[i+first_col],
-				    i == dashful_parents-1 ? '.' : '-');
+	int dashed_parents = graph_num_dashed_parents(graph);
+
+	for (i = 0; i < dashed_parents; i++) {
+		j = graph->mapping[(graph->commit_index + i + 2) * 2];
+		col = &graph->new_columns[j];
+
+		strbuf_write_column(sb, col, '-');
+		strbuf_write_column(sb, col, (i == dashed_parents - 1) ? '.' : '-');
 	}
+
+	return;
 }
 
 static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
t/t4214-log-graph-octopus.sh +55 -4

78 lines not on this page · raw message

Signature · 1 line
gitgitgadget
Tested-by on v1 11/11
#36 lore raw

Re: [PATCH 11/11] graph: fix coloring of octopus dashes

Hi James,

Nicely done! This issue was bugging me for a while!

On Thu, Oct 10, 2019 at 09:13:52AM -0700, James Coglan via GitGitGadget wrote:

[...]

Quoted 73 lines
t/t4214-log-graph-octopus.sh +48 -4
index 9ada687628..fbd485d83a 100755
@@ -42,23 +42,74 @@ test_expect_success 'set up merge history' '
 	test_tick &&
 	git merge -m octopus-merge 1 2 3 4 &&
 	git checkout 1 -b L &&
-	test_commit left
+	test_commit left &&
+	git checkout 4 -b R &&
+	test_commit right
 '
 
 test_expect_success 'log --graph with tricky octopus merge with colors' '
 	test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
-	git log --color=always --graph --date-order --pretty=tformat:%s --all >actual.colors.raw &&
+	git log --color=always --graph --date-order --pretty=tformat:%s L merge >actual.colors.raw &&
 	test_decode_color <actual.colors.raw | sed "s/ *\$//" >actual.colors &&
 	test_cmp expect.colors actual.colors
 '
 
 test_expect_success 'log --graph with tricky octopus merge, no color' '
-	git log --color=never --graph --date-order --pretty=tformat:%s --all >actual.raw &&
+	git log --color=never --graph --date-order --pretty=tformat:%s L merge >actual.raw &&
 	sed "s/ *\$//" actual.raw >actual &&
 	test_cmp expect.uncolored actual
 '
 
-# Repeat the previous two tests with "normal" octopus merge (i.e.,
+# Repeat the previous two tests with an octopus merge whose final parent skews left
+
+test_expect_success 'log --graph with left-skewed final parent, no color' '
+	cat >expect.uncolored <<-\EOF &&
+	* right
+	| *---.   octopus-merge
+	| |\ \ \
+	| |_|_|/
+	|/| | |
+	* | | | 4
+	| | | * 3
+	| |_|/
+	|/| |
+	| | * 2
+	| |/
+	|/|
+	| * 1
+	|/
+	* initial
+	EOF
+	git log --color=never --graph --date-order --pretty=tformat:%s R merge >actual.raw &&
+	sed "s/ *\$//" actual.raw >actual &&
+	test_cmp expect.uncolored actual
+'
+
+test_expect_success 'log --graph with left-skewed final parent with colors' '
+	cat >expect.colors <<-\EOF &&
+	* right
+	<RED>|<RESET> *<BLUE>-<RESET><BLUE>-<RESET><RED>-<RESET><RED>.<RESET>   octopus-merge
+	<RED>|<RESET> <GREEN>|<RESET><YELLOW>\<RESET> <BLUE>\<RESET> <RED>\<RESET>
+	<RED>|<RESET> <GREEN>|<RESET><RED>_<RESET><YELLOW>|<RESET><RED>_<RESET><BLUE>|<RESET><RED>/<RESET>
+	<RED>|<RESET><RED>/<RESET><GREEN>|<RESET> <YELLOW>|<RESET> <BLUE>|<RESET>
+	* <GREEN>|<RESET> <YELLOW>|<RESET> <BLUE>|<RESET> 4
+	<MAGENTA>|<RESET> <GREEN>|<RESET> <YELLOW>|<RESET> * 3
+	<MAGENTA>|<RESET> <GREEN>|<RESET><MAGENTA>_<RESET><YELLOW>|<RESET><MAGENTA>/<RESET>
+	<MAGENTA>|<RESET><MAGENTA>/<RESET><GREEN>|<RESET> <YELLOW>|<RESET>
+	<MAGENTA>|<RESET> <GREEN>|<RESET> * 2
+	<MAGENTA>|<RESET> <GREEN>|<RESET><MAGENTA>/<RESET>
+	<MAGENTA>|<RESET><MAGENTA>/<RESET><GREEN>|<RESET>
+	<MAGENTA>|<RESET> * 1
+	<MAGENTA>|<RESET><MAGENTA>/<RESET>
+	* initial
+	EOF

t/t4214-log-graph-octopus.sh test_expect_success 'set up merge history' ' · line 115

Quoted 10 lines
t/t4214-log-graph-octopus.sh +7 -0
+	test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
+	git log --color=always --graph --date-order --pretty=tformat:%s R merge >actual.colors.raw &&
+	test_decode_color <actual.colors.raw | sed "s/ *\$//" >actual.colors &&
+	test_cmp expect.colors actual.colors
+'
+
+# Repeat the first two tests with "normal" octopus merge (i.e.,
 # without the first parent skewing to the "left" branch column).
 
 test_expect_success 'log --graph with normal octopus merge, no color' '

So, I decided to merge 'dl/octopus-graph-bug' with your branch and I couldn't be happier! I had to make a couple of minor tweaks to the existing test cases but most of them only involved flipping test_expect_failure to test_expect_success.

You can see the results of this by doing:

	$ git fetch https://github.com/Denton-L/git.git testing/graph-output
	$ git diff FETCH_HEAD^2 t/t4214-log-graph-octopus.sh

and the resulting diff is very pleasing imo.

Junio, when you pick this topic up, that branch should contain the correct conflict resolution if that can help you out in any way.

Anyway, thanks for the work, James!

I'll give this patch my:

Quoted 2 lines
Signature · 1 line
gitgitgadget
#37 lore raw

Re: [PATCH 11/11] graph: fix coloring of octopus dashes

Quoted 6 lines · Denton Liu

You can see the results of this by doing:

	$ git fetch https://github.com/Denton-L/git.git testing/graph-output
	$ git diff FETCH_HEAD^2 t/t4214-log-graph-octopus.sh

and the resulting diff is very pleasing imo.

I guess it'd probably be nice if the result of that diff were viewable without the extra work of fetching everything, so here it is:

t/t4214-log-graph-octopus.sh +29 -33
index 3ae8e51e50..40d27db674 100755
@@ -26,15 +26,14 @@ test_expect_success 'set up merge history' '
 test_expect_success 'log --graph with tricky octopus merge, no color' '
 	cat >expect.uncolored <<-\EOF &&
 	* left
-	| *---.   octopus-merge
-	| |\ \ \
-	|/ / / /
+	| *-.   octopus-merge
+	|/|\ \
 	| | | * 4
 	| | * | 3
 	| | |/
-	| * | 2
+	| * / 2
 	| |/
-	* | 1
+	* / 1
 	|/
 	* initial
 	EOF
@@ -47,15 +46,14 @@ test_expect_success 'log --graph with tricky octopus merge with colors' '
 	test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
 	cat >expect.colors <<-\EOF &&
 	* left
-	<RED>|<RESET> *<BLUE>-<RESET><BLUE>-<RESET><MAGENTA>-<RESET><MAGENTA>.<RESET>   octopus-merge
-	<RED>|<RESET> <RED>|<RESET><YELLOW>\<RESET> <BLUE>\<RESET> <MAGENTA>\<RESET>
-	<RED>|<RESET><RED>/<RESET> <YELLOW>/<RESET> <BLUE>/<RESET> <MAGENTA>/<RESET>
+	<RED>|<RESET> *<MAGENTA>-<RESET><MAGENTA>.<RESET>   octopus-merge
+	<RED>|<RESET><RED>/<RESET><YELLOW>|<RESET><BLUE>\<RESET> <MAGENTA>\<RESET>
 	<RED>|<RESET> <YELLOW>|<RESET> <BLUE>|<RESET> * 4
 	<RED>|<RESET> <YELLOW>|<RESET> * <MAGENTA>|<RESET> 3
 	<RED>|<RESET> <YELLOW>|<RESET> <MAGENTA>|<RESET><MAGENTA>/<RESET>
-	<RED>|<RESET> * <MAGENTA>|<RESET> 2
+	<RED>|<RESET> * <MAGENTA>/<RESET> 2
 	<RED>|<RESET> <MAGENTA>|<RESET><MAGENTA>/<RESET>
-	* <MAGENTA>|<RESET> 1
+	* <MAGENTA>/<RESET> 1
 	<MAGENTA>|<RESET><MAGENTA>/<RESET>
 	* initial
 	EOF
@@ -74,9 +72,9 @@ test_expect_success 'log --graph with normal octopus merge, no color' '
 	| | | * 4
 	| | * | 3
 	| | |/
-	| * | 2
+	| * / 2
 	| |/
-	* | 1
+	* / 1
 	|/
 	* initial
 	EOF
@@ -92,9 +90,9 @@ test_expect_success 'log --graph with normal octopus merge with colors' '
 	<RED>|<RESET> <GREEN>|<RESET> <YELLOW>|<RESET> * 4
 	<RED>|<RESET> <GREEN>|<RESET> * <BLUE>|<RESET> 3
 	<RED>|<RESET> <GREEN>|<RESET> <BLUE>|<RESET><BLUE>/<RESET>
-	<RED>|<RESET> * <BLUE>|<RESET> 2
+	<RED>|<RESET> * <BLUE>/<RESET> 2
 	<RED>|<RESET> <BLUE>|<RESET><BLUE>/<RESET>
-	* <BLUE>|<RESET> 1
+	* <BLUE>/<RESET> 1
 	<BLUE>|<RESET><BLUE>/<RESET>
 	* initial
 	EOF
@@ -112,9 +110,9 @@ test_expect_success 'log --graph with normal octopus merge and child, no color'
 	| | | * 4
 	| | * | 3
 	| | |/
-	| * | 2
+	| * / 2
 	| |/
-	* | 1
+	* / 1
 	|/
 	* initial
 	EOF
@@ -123,7 +121,7 @@ test_expect_success 'log --graph with normal octopus merge and child, no color'
 	test_cmp expect.uncolored actual
 '
 
-test_expect_failure 'log --graph with normal octopus and child merge with colors' '
+test_expect_success 'log --graph with normal octopus and child merge with colors' '
 	cat >expect.colors <<-\EOF &&
 	* after-merge
 	*<BLUE>-<RESET><BLUE>-<RESET><MAGENTA>-<RESET><MAGENTA>.<RESET>   octopus-merge
@@ -131,9 +129,9 @@ test_expect_failure 'log --graph with normal octopus and child merge with colors
 	<GREEN>|<RESET> <YELLOW>|<RESET> <BLUE>|<RESET> * 4
 	<GREEN>|<RESET> <YELLOW>|<RESET> * <MAGENTA>|<RESET> 3
 	<GREEN>|<RESET> <YELLOW>|<RESET> <MAGENTA>|<RESET><MAGENTA>/<RESET>
-	<GREEN>|<RESET> * <MAGENTA>|<RESET> 2
+	<GREEN>|<RESET> * <MAGENTA>/<RESET> 2
 	<GREEN>|<RESET> <MAGENTA>|<RESET><MAGENTA>/<RESET>
-	* <MAGENTA>|<RESET> 1
+	* <MAGENTA>/<RESET> 1
 	<MAGENTA>|<RESET><MAGENTA>/<RESET>
 	* initial
 	EOF
@@ -147,15 +145,14 @@ test_expect_success 'log --graph with tricky octopus merge and its child, no col
 	cat >expect.uncolored <<-\EOF &&
 	* left
 	| * after-merge
-	| *---.   octopus-merge
-	| |\ \ \
-	|/ / / /
+	| *-.   octopus-merge
+	|/|\ \
 	| | | * 4
 	| | * | 3
 	| | |/
-	| * | 2
+	| * / 2
 	| |/
-	* | 1
+	* / 1
 	|/
 	* initial
 	EOF
@@ -164,20 +161,19 @@ test_expect_success 'log --graph with tricky octopus merge and its child, no col
 	test_cmp expect.uncolored actual
 '
 
-test_expect_failure 'log --graph with tricky octopus merge and its child with colors' '
+test_expect_success 'log --graph with tricky octopus merge and its child with colors' '
 	test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
 	cat >expect.colors <<-\EOF &&
 	* left
 	<RED>|<RESET> * after-merge
-	<RED>|<RESET> *<MAGENTA>-<RESET><MAGENTA>-<RESET><CYAN>-<RESET><CYAN>.<RESET>   octopus-merge
-	<RED>|<RESET> <RED>|<RESET><BLUE>\<RESET> <MAGENTA>\<RESET> <CYAN>\<RESET>
-	<RED>|<RESET><RED>/<RESET> <BLUE>/<RESET> <MAGENTA>/<RESET> <CYAN>/<RESET>
+	<RED>|<RESET> *<CYAN>-<RESET><CYAN>.<RESET>   octopus-merge
+	<RED>|<RESET><RED>/<RESET><BLUE>|<RESET><MAGENTA>\<RESET> <CYAN>\<RESET>
 	<RED>|<RESET> <BLUE>|<RESET> <MAGENTA>|<RESET> * 4
 	<RED>|<RESET> <BLUE>|<RESET> * <CYAN>|<RESET> 3
 	<RED>|<RESET> <BLUE>|<RESET> <CYAN>|<RESET><CYAN>/<RESET>
-	<RED>|<RESET> * <CYAN>|<RESET> 2
+	<RED>|<RESET> * <CYAN>/<RESET> 2
 	<RED>|<RESET> <CYAN>|<RESET><CYAN>/<RESET>
-	* <CYAN>|<RESET> 1
+	* <CYAN>/<RESET> 1
 	<CYAN>|<RESET><CYAN>/<RESET>
 	* initial
 	EOF
@@ -209,7 +205,7 @@ test_expect_success 'log --graph with crossover in octopus merge, no color' '
 	test_cmp expect.uncolored actual
 '
 
-test_expect_failure 'log --graph with crossover in octopus merge with colors' '
+test_expect_success 'log --graph with crossover in octopus merge with colors' '
 	test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
 	cat >expect.colors <<-\EOF &&
 	* after-4
@@ -257,7 +253,7 @@ test_expect_success 'log --graph with crossover in octopus merge and its child,
 	test_cmp expect.uncolored actual
 '
 
-test_expect_failure 'log --graph with crossover in octopus merge and its child with colors' '
+test_expect_success 'log --graph with crossover in octopus merge and its child with colors' '
 	test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
 	cat >expect.colors <<-\EOF &&
 	* after-4
@@ -353,7 +349,7 @@ test_expect_success 'log --graph with unrelated commit and octopus child, no col
 	test_cmp expect.uncolored actual
 '
 
-test_expect_failure 'log --graph with unrelated commit and octopus child with colors' '
+test_expect_success 'log --graph with unrelated commit and octopus child with colors' '
 	test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
 	cat >expect.colors <<-\EOF &&
 	* after-initial
#38 lore raw

Re: [PATCH 11/11] graph: fix coloring of octopus dashes

In reply to Denton Liu
Quoted 6 lines · Denton Liu
Quoted 1 line

test_expect_success 'log --graph with normal octopus merge, no color' '

So, I decided to merge 'dl/octopus-graph-bug' with your branch and I couldn't be happier! I had to make a couple of minor tweaks to the existing test cases but most of them only involved flipping test_expect_failure to test_expect_success.

Thanks for doing that. I also checked the output on the git.git case discussed in:

https://public-inbox.org/git/[email protected]/

and it looks great. Probably because it's the same as your test cases -- the "non-collapsing type" I called it there (or maybe it's all just the out-of-order thing James brought up; I don't recall discussing that before, but certainly his test case exhibits the same off-by-one coloring weirdness without the code change).

-Peff

#39 lore raw
In reply to James Coglan via GitGitGadget
Quoted 16 lines · James Coglan via GitGitGadget

This series of patches are designed to improve the output of the log --graph command; their effect can be summed up in the following diagram:

    Before                    After
    ------                    -----

    *
    |\
    | *                       *
    | |\                      |\
    | | *                     | *
    | | |                     | |\
    | |  \                    | | *
    | *-. \                   | * |
    | |\ \ \                  |/|\|
    |/ / / /                  | | *
Quoted 8 lines
    | | | /                   | * |
    | | |/                    | |/
    | | *                     * /
    | * |                     |/
    | |/                      *
    * |
    |/
    *

I took a brief look through your series, and I think this is a really cool improvement. I use "git log --graph" all the time and those kinks have bothered me, too.

I'd give you extra bonus points if your first patch added the case on the left as an expected output and we watch it get simpler as you modify the behavior in pieces.

I do really like how you isolated different transformations into different commits. I would have given more specific feedback if I was more familiar with graph.c. I'll need to play with it more to give more substantive feedback.

The only thing I could complain about in the first glance is some test file formatting stuff, like how you split a test case into "setup" "create expected output" and "test the output". That would be better as one test. Also you add a space between your redirect character and your file ("> expect" instead of ">expect").

Those nits aside, I look forward to digging into the code more soon.

Thanks,
-Stolee

#40 lore raw
In reply to James Coglan via GitGitGadget
Quoted 4 lines · James Coglan via GitGitGadget

A final addition to that set of changes fixes the coloring of dashes that are drawn next to octopus merges, in a manner compatible with all these changes. The early commits in this set are refactorings that make the functional changes easier to introduce.

As somebody who has pondered the octopus coloring code (for an embarrassingly long time considering that it still has some bugs!), let me just say thank you for taking this on. :)

Moreover, I'll echo Dscho's comments elsewhere on the quality of this series. It's a tricky topic to explain, and the way you've broken it up, along with the commit messages, comments, and diagrams made it much easier to follow.

Others have already commented on things I saw while reading it, so I'll just add a few more thoughts.

Quoted 16 lines

This series of patches are designed to improve the output of the log --graph command; their effect can be summed up in the following diagram:

    Before                    After
    ------                    -----

    *
    |\
    | *                       *
    | |\                      |\
    | | *                     | *
    | | |                     | |\
    | |  \                    | | *
    | *-. \                   | * |
    | |\ \ \                  |/|\|
    |/ / / /                  | | *
Quoted 8 lines
    | | | /                   | * |
    | | |/                    | |/
    | | *                     * /
    | * |                     |/
    | |/                      *
    * |
    |/
    *

I wondered if anybody would prefer the sparseness of the "before" diagram, and if that would merit having two modes that could selected at runtime. I'm not sure I'd want to carry the code for both types, though; it seems like a recipe for the non-default output format to accrue a bunch of bugs (since the graph code has proven itself to be a magnet for off-by-ones and other weirdness).

Diffing the output of "git diff --color --graph --oneline" on git.git both before and after your patch, the changes all look generally positive to me. The graph above is pretty dense, but that's not really what real-world graphs look like; it was designed to show off the changes.

That plus the fact that you're fixing real bugs (like the octopus coloring) makes me inclined to just move to your suggested output.

-Peff

#41 lore raw
Quoted 44 lines · Jeff King
Quoted 4 lines · James Coglan via GitGitGadget

A final addition to that set of changes fixes the coloring of dashes that are drawn next to octopus merges, in a manner compatible with all these changes. The early commits in this set are refactorings that make the functional changes easier to introduce.

As somebody who has pondered the octopus coloring code (for an embarrassingly long time considering that it still has some bugs!), let me just say thank you for taking this on. :)

Moreover, I'll echo Dscho's comments elsewhere on the quality of this series. It's a tricky topic to explain, and the way you've broken it up, along with the commit messages, comments, and diagrams made it much easier to follow.

Others have already commented on things I saw while reading it, so I'll just add a few more thoughts.

Quoted 16 lines

This series of patches are designed to improve the output of the log --graph command; their effect can be summed up in the following diagram:

    Before                    After
    ------                    -----

    *
    |\
    | *                       *
    | |\                      |\
    | | *                     | *
    | | |                     | |\
    | |  \                    | | *
    | *-. \                   | * |
    | |\ \ \                  |/|\|
    |/ / / /                  | | *
Quoted 8 lines
    | | | /                   | * |
    | | |/                    | |/
    | | *                     * /
    | * |                     |/
    | |/                      *
    * |
    |/
    *
Quoted 6 lines

I wondered if anybody would prefer the sparseness of the "before" diagram, and if that would merit having two modes that could selected at runtime. I'm not sure I'd want to carry the code for both types, though; it seems like a recipe for the non-default output format to accrue a bunch of bugs (since the graph code has proven itself to be a magnet for off-by-ones and other weirdness).

You're probably right about the non-default mode hiding bugs, but if you did want to do this, I believe the original rendering can be preserved by the following small switches:

- always set merge_layout = 1; this will prevent skewing to the left
- do not modify edges_added if the last parent fuses with its neighbor

I believe all the layout changes are driven by these values, so you should be able to set in such a way as to preserve the original rendering by ignoring the special cases that lead to them having different values.

#42 lore raw

v2 cover Improve the readability of log --graph output

In reply to James Coglan via GitGitGadget

This series of patches are designed to improve the output of the log --graph command; their effect can be summed up in the following diagram:

    Before                    After
    ------                    -----

    *
    |\
    | *                       *
    | |\                      |\
    | | *                     | *
    | | |                     | |\
    | |  \                    | | *
    | *-. \                   | * |
    | |\ \ \                  |/|\|
    |/ / / /                  | | *
    | | | /                   | * |
    | | |/                    | |/
    | | *                     * /
    | * |                     |/
    | |/                      *
    * |
    |/
    *

These changes aim to make the edges in graph diagrams easier to read, by straightening lines and making certain kinds of topologies display more compactly. Three distinct changes are included.

First, if the first parent of a merge fuses with an edge to the left of the commit, then we display that by making the edges fuse immediately rather than by drawing a line straight down and then having it track to the left. That is, where we currently display these graphs:

    | *             | | | *
    | |\            | | | |\
    |/ /            | |_|/ /
    | |             |/| | |

We will now display these merges as follows:

    | *             | | | *
    |/|             | |_|/|
    | |             |/| | |

This transformation is applied to merges with any number of parents, for example we currently display 3-parent merges like this:

    | *-.           | | | *-.
    | |\ \          | | | |\ \
    |/ / /          | |_|/ / /
    | | |           |/| | | |

And we will now display them like this:

    | *             | | | *
    |/|\            | |_|/|\
    | | |           |/| | | |

If the edge the first parent fuses with is separated from the commit by multiple columns, a horizontal edge is drawn just as we currently do in the 'collapsing' state. This change also affects the display of commit and post-merge lines in subtle ways that are more thoroughly described in the relevant commits.

The second change is that if the final parent of a merge fuses with the edge to the right of the commit, then we can remove the zig-zag effect that currently results. We currently display these merges like this:

    * |
    |\ \
    | |/
    | *

After these changes, this merge will now be displayed like so:

    * |
    |\|
    | *

If the final parent fuses with an edge that's further to the right, its display is unchanged and it will still display like this:

    * | | |
    |\ \ \ \
    | | |_|/
    | |/| |
    | * | |

The final structural change smooths out lines that are collapsing through commit lines. For example, consider the following history:

    *-. \
    |\ \ \
    | | * |
    | * | |
    | |/ /
    * | |
    |/ /
    * |
    |/
    *

This is now rendered so that commit lines display an edge using / instead of |, if that edge is tracking to the left both above and below the commit line. That results in this improved display:

    *-. \
    |\ \ \
    | | * |
    | * | |
    | |/ /
    * / /
    |/ /
    * /
    |/
    *

Taken together, these changes produce the change shown in the first diagram above, with the current rendering on the left and the new rendering on the right.

A final addition to that set of changes fixes the coloring of dashes that are drawn next to octopus merges, in a manner compatible with all these changes. The early commits in this set are refactorings that make the functional changes easier to introduce.

James Coglan (13):
graph: automatically track display width of graph lines
graph: handle line padding in graph_next_line()
graph: reuse find_new_column_by_commit()
graph: reduce duplication in graph_insert_into_new_columns()
graph: remove mapping_idx and graph_update_width()
graph: extract logic for moving to GRAPH_PRE_COMMIT state
graph: example of graph output that can be simplified
graph: tidy up display of left-skewed merges
graph: commit and post-merge lines for left-skewed merges
graph: rename new_mapping to old_mapping
graph: smooth appearance of collapsing edges on commit lines
graph: flatten edges that fuse with their right neighbor
graph: fix coloring of octopus dashes

 graph.c                                    | 646 ++++++++++++---------
 t/t3430-rebase-merges.sh                   |   2 +-
 t/t4202-log.sh                             |   2 +-
 t/t4214-log-graph-octopus.sh               |  62 +-
 t/t4215-log-skewed-merges.sh               | 257 ++++++++
 t/t6016-rev-list-graph-simplify-history.sh |  30 +-
 6 files changed, 673 insertions(+), 326 deletions(-)
 create mode 100755 t/t4215-log-skewed-merges.sh

base-commit: 108b97dc372828f0e72e56bbb40cae8e1e83ece6
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-383%2Fjcoglan%2Fjc%2Fsimplify-graph-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-383/jcoglan/jc/simplify-graph-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/383

Range-diff vs v1:

1: 4bc0a05961 ! 1: c30f5bb43b graph: automatically track visible width of strbuf

     @@ -1,13 +1,13 @@
      Author: James Coglan <[email protected]>
      
     -    graph: automatically track visible width of `strbuf`
     +    graph: automatically track display width of graph lines
      
     -    All the output functions in `graph.c` currently keep track of how many
     -    printable chars they've written to the buffer, before calling
     -    `graph_pad_horizontally()` to pad the line with spaces. Some functions
     -    do this by incrementing a counter whenever they write to the buffer, and
     -    others do it by encoding an assumption about how many chars are written,
     -    as in:
     +    All the output functions called by `graph_next_line()` currently keep
     +    track of how many printable chars they've written to the buffer, before
     +    calling `graph_pad_horizontally()` to pad the line with spaces. Some
     +    functions do this by incrementing a counter whenever they write to the
     +    buffer, and others do it by encoding an assumption about how many chars
     +    are written, as in:
      
              graph_pad_horizontally(graph, sb, graph->num_columns * 2);
      
     @@ -15,22 +15,18 @@
          broken if one forgets to increment the right counter or update the
          calculations used for padding.
      
     -    To make this easier to use, I'm adding a `width` field to `strbuf` that
     -    tracks the number of printing characters added after the line prefix.
     -    It's set to 0 at the start of `graph_next_line()`, and then various
     -    `strbuf` functions update it as follows:
     +    To make this easier to use, I'm introducing a new struct called
     +    `graph_line` that wraps a `strbuf` and keeps count of its display width
     +    implicitly. `graph_next_line()` wraps this around the `struct strbuf *`
     +    it's given and passes a `struct graph_line *` to the output functions,
     +    which use its interface.
      
     -    - `strbuf_write_column()` increments `width` by 1
     -
     -    - `strbuf_setlen()` changes `width` by the amount added to `len` if
     -      `len` is increased, or makes `width` and `len` the same if it's
     -      decreased
     -
     -    - `strbuf_addch()` increments `width` by 1
     -
     -    This is enough to ensure that the functions used by `graph.c` update
     -    `strbuf->width` correctly, and `graph_pad_horizontally()` can then use
     -    this field instead of taking `chars_written` as a parameter.
     +    The `graph_line` interface wraps the `strbuf_addch()`,
     +    `strbuf_addchars()` and `strbuf_addstr()` functions, and adds the
     +    `graph_line_write_column()` function for adding a single character with
     +    color formatting. The `graph_pad_horizontally()` function can then use
     +    the `width` field from the struct rather than taking a character count
     +    as a parameter.
      
          Signed-off-by: James Coglan <[email protected]>
      
     @@ -38,23 +34,50 @@
       --- a/graph.c
       +++ b/graph.c
      @@
     - static void strbuf_write_column(struct strbuf *sb, const struct column *c,
     - 				char col_char)
     - {
     -+	/*
     -+	 * Remember the buffer's width as we're about to add non-printing
     -+	 * content to it, and we want to avoid counting the byte length
     -+	 * of this content towards the buffer's visible width
     -+	 */
     -+	size_t prev_width = sb->width;
     + 	return column_colors[color];
     + }
     + 
     +-static void strbuf_write_column(struct strbuf *sb, const struct column *c,
     +-				char col_char)
     ++struct graph_line {
     ++	struct strbuf *buf;
     ++	size_t width;
     ++};
     ++
     ++static inline void graph_line_addch(struct graph_line *line, int c)
     ++{
     ++	strbuf_addch(line->buf, c);
     ++	line->width++;
     ++}
      +
     ++static inline void graph_line_addchars(struct graph_line *line, int c, size_t n)
     ++{
     ++	strbuf_addchars(line->buf, c, n);
     ++	line->width += n;
     ++}
     ++
     ++static inline void graph_line_addstr(struct graph_line *line, const char *s)
     ++{
     ++	strbuf_addstr(line->buf, s);
     ++	line->width += strlen(s);
     ++}
     ++
     ++static inline void graph_line_addcolor(struct graph_line *line, unsigned short color)
     ++{
     ++	strbuf_addstr(line->buf, column_get_color_code(color));
     ++}
     ++
     ++static void graph_line_write_column(struct graph_line *line, const struct column *c,
     ++				    char col_char)
     + {
       	if (c->color < column_colors_max)
     - 		strbuf_addstr(sb, column_get_color_code(c->color));
     - 	strbuf_addch(sb, col_char);
     +-		strbuf_addstr(sb, column_get_color_code(c->color));
     +-	strbuf_addch(sb, col_char);
     ++		graph_line_addcolor(line, c->color);
     ++	graph_line_addch(line, col_char);
       	if (c->color < column_colors_max)
     - 		strbuf_addstr(sb, column_get_color_code(column_colors_max));
     -+
     -+	sb->width = prev_width + 1;
     +-		strbuf_addstr(sb, column_get_color_code(column_colors_max));
     ++		graph_line_addcolor(line, column_colors_max);
       }
       
       struct git_graph {
     @@ -64,7 +87,7 @@
       
      -static void graph_pad_horizontally(struct git_graph *graph, struct strbuf *sb,
      -				   int chars_written)
     -+static void graph_pad_horizontally(struct git_graph *graph, struct strbuf *sb)
     ++static void graph_pad_horizontally(struct git_graph *graph, struct graph_line *line)
       {
       	/*
       	 * Add additional spaces to the end of the strbuf, so that all
     @@ -74,30 +97,55 @@
       	 */
      -	if (chars_written < graph->width)
      -		strbuf_addchars(sb, ' ', graph->width - chars_written);
     -+	if (sb->width < graph->width)
     -+		strbuf_addchars(sb, ' ', graph->width - sb->width);
     ++	if (line->width < graph->width)
     ++		graph_line_addchars(line, ' ', graph->width - line->width);
       }
       
       static void graph_output_padding_line(struct git_graph *graph,
     +-				      struct strbuf *sb)
     ++				      struct graph_line *line)
     + {
     + 	int i;
     + 
      @@
     - 		strbuf_addch(sb, ' ');
     + 	 * Output a padding row, that leaves all branch lines unchanged
     + 	 */
     + 	for (i = 0; i < graph->num_new_columns; i++) {
     +-		strbuf_write_column(sb, &graph->new_columns[i], '|');
     +-		strbuf_addch(sb, ' ');
     ++		graph_line_write_column(line, &graph->new_columns[i], '|');
     ++		graph_line_addch(line, ' ');
       	}
       
      -	graph_pad_horizontally(graph, sb, graph->num_new_columns * 2);
     -+	graph_pad_horizontally(graph, sb);
     ++	graph_pad_horizontally(graph, line);
       }
       
       
      @@
     + }
     + 
     + 
     +-static void graph_output_skip_line(struct git_graph *graph, struct strbuf *sb)
     ++static void graph_output_skip_line(struct git_graph *graph, struct graph_line *line)
     + {
     + 	/*
     + 	 * Output an ellipsis to indicate that a portion
       	 * of the graph is missing.
       	 */
     - 	strbuf_addstr(sb, "...");
     +-	strbuf_addstr(sb, "...");
      -	graph_pad_horizontally(graph, sb, 3);
     -+	graph_pad_horizontally(graph, sb);
     ++	graph_line_addstr(line, "...");
     ++	graph_pad_horizontally(graph, line);
       
       	if (graph->num_parents >= 3 &&
       	    graph->commit_index < (graph->num_columns - 1))
      @@
     + }
     + 
     + static void graph_output_pre_commit_line(struct git_graph *graph,
     +-					 struct strbuf *sb)
     ++					 struct graph_line *line)
       {
       	int num_expansion_rows;
       	int i, seen_this;
     @@ -114,34 +162,66 @@
       		struct column *col = &graph->columns[i];
       		if (col->commit == graph->commit) {
       			seen_this = 1;
     - 			strbuf_write_column(sb, col, '|');
     - 			strbuf_addchars(sb, ' ', graph->expansion_row);
     +-			strbuf_write_column(sb, col, '|');
     +-			strbuf_addchars(sb, ' ', graph->expansion_row);
      -			chars_written += 1 + graph->expansion_row;
     ++			graph_line_write_column(line, col, '|');
     ++			graph_line_addchars(line, ' ', graph->expansion_row);
       		} else if (seen_this && (graph->expansion_row == 0)) {
       			/*
       			 * This is the first line of the pre-commit output.
      @@
     - 				strbuf_write_column(sb, col, '\\');
     + 			 */
     + 			if (graph->prev_state == GRAPH_POST_MERGE &&
     + 			    graph->prev_commit_index < i)
     +-				strbuf_write_column(sb, col, '\\');
     ++				graph_line_write_column(line, col, '\\');
       			else
     - 				strbuf_write_column(sb, col, '|');
     +-				strbuf_write_column(sb, col, '|');
      -			chars_written++;
     ++				graph_line_write_column(line, col, '|');
       		} else if (seen_this && (graph->expansion_row > 0)) {
     - 			strbuf_write_column(sb, col, '\\');
     +-			strbuf_write_column(sb, col, '\\');
      -			chars_written++;
     ++			graph_line_write_column(line, col, '\\');
       		} else {
     - 			strbuf_write_column(sb, col, '|');
     +-			strbuf_write_column(sb, col, '|');
      -			chars_written++;
     ++			graph_line_write_column(line, col, '|');
       		}
     - 		strbuf_addch(sb, ' ');
     +-		strbuf_addch(sb, ' ');
      -		chars_written++;
     ++		graph_line_addch(line, ' ');
       	}
       
      -	graph_pad_horizontally(graph, sb, chars_written);
     -+	graph_pad_horizontally(graph, sb);
     ++	graph_pad_horizontally(graph, line);
       
       	/*
       	 * Increment graph->expansion_row,
      @@
     + 		graph_update_state(graph, GRAPH_COMMIT);
     + }
     + 
     +-static void graph_output_commit_char(struct git_graph *graph, struct strbuf *sb)
     ++static void graph_output_commit_char(struct git_graph *graph, struct graph_line *line)
     + {
     + 	/*
     + 	 * For boundary commits, print 'o'
     +@@
     + 	 */
     + 	if (graph->commit->object.flags & BOUNDARY) {
     + 		assert(graph->revs->boundary);
     +-		strbuf_addch(sb, 'o');
     ++		graph_line_addch(line, 'o');
     + 		return;
     + 	}
     + 
     + 	/*
     + 	 * get_revision_mark() handles all other cases without assert()
     + 	 */
     +-	strbuf_addstr(sb, get_revision_mark(graph->revs, graph->commit));
     ++	graph_line_addstr(line, get_revision_mark(graph->revs, graph->commit));
       }
       
       /*
     @@ -151,18 +231,26 @@
        */
      -static int graph_draw_octopus_merge(struct git_graph *graph,
      -				    struct strbuf *sb)
     -+static void graph_draw_octopus_merge(struct git_graph *graph, struct strbuf *sb)
     ++static void graph_draw_octopus_merge(struct git_graph *graph, struct graph_line *line)
       {
       	/*
       	 * Here dashless_parents represents the number of parents which don't
      @@
     - 		strbuf_write_column(sb, &graph->new_columns[i+first_col],
     - 				    i == dashful_parents-1 ? '.' : '-');
     + 
     + 	int i;
     + 	for (i = 0; i < dashful_parents; i++) {
     +-		strbuf_write_column(sb, &graph->new_columns[i+first_col], '-');
     +-		strbuf_write_column(sb, &graph->new_columns[i+first_col],
     +-				    i == dashful_parents-1 ? '.' : '-');
     ++		graph_line_write_column(line, &graph->new_columns[i+first_col], '-');
     ++		graph_line_write_column(line, &graph->new_columns[i+first_col],
     ++					  i == dashful_parents-1 ? '.' : '-');
       	}
      -	return 2 * dashful_parents;
       }
       
     - static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
     +-static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
     ++static void graph_output_commit_line(struct git_graph *graph, struct graph_line *line)
       {
       	int seen_this = 0;
      -	int i, chars_written;
     @@ -179,41 +267,55 @@
       		struct column *col = &graph->columns[i];
       		struct commit *col_commit;
      @@
     + 
       		if (col_commit == graph->commit) {
       			seen_this = 1;
     - 			graph_output_commit_char(graph, sb);
     +-			graph_output_commit_char(graph, sb);
      -			chars_written++;
     ++			graph_output_commit_char(graph, line);
       
       			if (graph->num_parents > 2)
      -				chars_written += graph_draw_octopus_merge(graph,
      -									  sb);
     -+				graph_draw_octopus_merge(graph, sb);
     ++				graph_draw_octopus_merge(graph, line);
       		} else if (seen_this && (graph->num_parents > 2)) {
     - 			strbuf_write_column(sb, col, '\\');
     +-			strbuf_write_column(sb, col, '\\');
      -			chars_written++;
     ++			graph_line_write_column(line, col, '\\');
       		} else if (seen_this && (graph->num_parents == 2)) {
       			/*
       			 * This is a 2-way merge commit.
      @@
     - 				strbuf_write_column(sb, col, '\\');
     + 			 */
     + 			if (graph->prev_state == GRAPH_POST_MERGE &&
     + 			    graph->prev_commit_index < i)
     +-				strbuf_write_column(sb, col, '\\');
     ++				graph_line_write_column(line, col, '\\');
       			else
     - 				strbuf_write_column(sb, col, '|');
     +-				strbuf_write_column(sb, col, '|');
      -			chars_written++;
     ++				graph_line_write_column(line, col, '|');
       		} else {
     - 			strbuf_write_column(sb, col, '|');
     +-			strbuf_write_column(sb, col, '|');
      -			chars_written++;
     ++			graph_line_write_column(line, col, '|');
       		}
     - 		strbuf_addch(sb, ' ');
     +-		strbuf_addch(sb, ' ');
      -		chars_written++;
     ++		graph_line_addch(line, ' ');
       	}
       
      -	graph_pad_horizontally(graph, sb, chars_written);
     -+	graph_pad_horizontally(graph, sb);
     ++	graph_pad_horizontally(graph, line);
       
       	/*
       	 * Update graph->state
      @@
     - static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf *sb)
     + 	return NULL;
     + }
     + 
     +-static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf *sb)
     ++static void graph_output_post_merge_line(struct git_graph *graph, struct graph_line *line)
       {
       	int seen_this = 0;
      -	int i, j, chars_written;
     @@ -227,40 +329,81 @@
       		struct column *col = &graph->columns[i];
       		struct commit *col_commit;
      @@
     + 			par_column = find_new_column_by_commit(graph, parents->item);
       			assert(par_column);
       
     - 			strbuf_write_column(sb, par_column, '|');
     +-			strbuf_write_column(sb, par_column, '|');
      -			chars_written++;
     ++			graph_line_write_column(line, par_column, '|');
       			for (j = 0; j < graph->num_parents - 1; j++) {
       				parents = next_interesting_parent(graph, parents);
       				assert(parents);
     -@@
     - 				strbuf_write_column(sb, par_column, '\\');
     - 				strbuf_addch(sb, ' ');
     + 				par_column = find_new_column_by_commit(graph, parents->item);
     + 				assert(par_column);
     +-				strbuf_write_column(sb, par_column, '\\');
     +-				strbuf_addch(sb, ' ');
     ++				graph_line_write_column(line, par_column, '\\');
     ++				graph_line_addch(line, ' ');
       			}
      -			chars_written += j * 2;
       		} else if (seen_this) {
     - 			strbuf_write_column(sb, col, '\\');
     - 			strbuf_addch(sb, ' ');
     +-			strbuf_write_column(sb, col, '\\');
     +-			strbuf_addch(sb, ' ');
      -			chars_written += 2;
     ++			graph_line_write_column(line, col, '\\');
     ++			graph_line_addch(line, ' ');
       		} else {
     - 			strbuf_write_column(sb, col, '|');
     - 			strbuf_addch(sb, ' ');
     +-			strbuf_write_column(sb, col, '|');
     +-			strbuf_addch(sb, ' ');
      -			chars_written += 2;
     ++			graph_line_write_column(line, col, '|');
     ++			graph_line_addch(line, ' ');
       		}
       	}
       
      -	graph_pad_horizontally(graph, sb, chars_written);
     -+	graph_pad_horizontally(graph, sb);
     ++	graph_pad_horizontally(graph, line);
       
       	/*
       	 * Update graph->state
      @@
     + 		graph_update_state(graph, GRAPH_COLLAPSING);
     + }
     + 
     +-static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf *sb)
     ++static void graph_output_collapsing_line(struct git_graph *graph, struct graph_line *line)
     + {
     + 	int i;
     + 	short used_horizontal = 0;
     +@@
     + 	for (i = 0; i < graph->mapping_size; i++) {
     + 		int target = graph->new_mapping[i];
     + 		if (target < 0)
     +-			strbuf_addch(sb, ' ');
     ++			graph_line_addch(line, ' ');
     + 		else if (target * 2 == i)
     +-			strbuf_write_column(sb, &graph->new_columns[target], '|');
     ++			graph_line_write_column(line, &graph->new_columns[target], '|');
     + 		else if (target == horizontal_edge_target &&
     + 			 i != horizontal_edge - 1) {
     + 				/*
     +@@
     + 				if (i != (target * 2)+3)
     + 					graph->new_mapping[i] = -1;
     + 				used_horizontal = 1;
     +-			strbuf_write_column(sb, &graph->new_columns[target], '_');
     ++			graph_line_write_column(line, &graph->new_columns[target], '_');
     + 		} else {
     + 			if (used_horizontal && i < horizontal_edge)
     + 				graph->new_mapping[i] = -1;
     +-			strbuf_write_column(sb, &graph->new_columns[target], '/');
     ++			graph_line_write_column(line, &graph->new_columns[target], '/');
     + 
       		}
       	}
       
      -	graph_pad_horizontally(graph, sb, graph->mapping_size);
     -+	graph_pad_horizontally(graph, sb);
     ++	graph_pad_horizontally(graph, line);
       
       	/*
       	 * Swap mapping and new_mapping
     @@ -268,74 +411,66 @@
       
       int graph_next_line(struct git_graph *graph, struct strbuf *sb)
       {
     -+	sb->width = 0;
     ++	struct graph_line line = { .buf = sb, .width = 0 };
      +
       	switch (graph->state) {
       	case GRAPH_PADDING:
     - 		graph_output_padding_line(graph, sb);
     +-		graph_output_padding_line(graph, sb);
     ++		graph_output_padding_line(graph, &line);
     + 		return 0;
     + 	case GRAPH_SKIP:
     +-		graph_output_skip_line(graph, sb);
     ++		graph_output_skip_line(graph, &line);
     + 		return 0;
     + 	case GRAPH_PRE_COMMIT:
     +-		graph_output_pre_commit_line(graph, sb);
     ++		graph_output_pre_commit_line(graph, &line);
     + 		return 0;
     + 	case GRAPH_COMMIT:
     +-		graph_output_commit_line(graph, sb);
     ++		graph_output_commit_line(graph, &line);
     + 		return 1;
     + 	case GRAPH_POST_MERGE:
     +-		graph_output_post_merge_line(graph, sb);
     ++		graph_output_post_merge_line(graph, &line);
     + 		return 0;
     + 	case GRAPH_COLLAPSING:
     +-		graph_output_collapsing_line(graph, sb);
     ++		graph_output_collapsing_line(graph, &line);
     + 		return 0;
     + 	}
     + 
      @@
       static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)
       {
       	int i;
      -	int chars_written = 0;
     ++	struct graph_line line = { .buf = sb, .width = 0 };
       
       	if (graph->state != GRAPH_COMMIT) {
       		graph_next_line(graph, sb);
      @@
     + 	for (i = 0; i < graph->num_columns; i++) {
       		struct column *col = &graph->columns[i];
       
     - 		strbuf_write_column(sb, col, '|');
     +-		strbuf_write_column(sb, col, '|');
      -		chars_written++;
     ++		graph_line_write_column(&line, col, '|');
       
       		if (col->commit == graph->commit && graph->num_parents > 2) {
       			int len = (graph->num_parents - 2) * 2;
     - 			strbuf_addchars(sb, ' ', len);
     +-			strbuf_addchars(sb, ' ', len);
      -			chars_written += len;
     ++			graph_line_addchars(&line, ' ', len);
       		} else {
     - 			strbuf_addch(sb, ' ');
     +-			strbuf_addch(sb, ' ');
      -			chars_written++;
     ++			graph_line_addch(&line, ' ');
       		}
       	}
       
      -	graph_pad_horizontally(graph, sb, chars_written);
     -+	graph_pad_horizontally(graph, sb);
     ++	graph_pad_horizontally(graph, &line);
       
       	/*
       	 * Update graph->prev_state since we have output a padding line
     -
     - diff --git a/strbuf.h b/strbuf.h
     - --- a/strbuf.h
     - +++ b/strbuf.h
     -@@
     - struct strbuf {
     - 	size_t alloc;
     - 	size_t len;
     -+	size_t width;
     - 	char *buf;
     - };
     - 
     - extern char strbuf_slopbuf[];
     --#define STRBUF_INIT  { .alloc = 0, .len = 0, .buf = strbuf_slopbuf }
     -+#define STRBUF_INIT  { .alloc = 0, .len = 0, .width = 0, .buf = strbuf_slopbuf }
     - 
     - /*
     -  * Predeclare this here, since cache.h includes this file before it defines the
     -@@
     - {
     - 	if (len > (sb->alloc ? sb->alloc - 1 : 0))
     - 		die("BUG: strbuf_setlen() beyond buffer");
     -+	if (len > sb->len)
     -+		sb->width += len - sb->len;
     -+	else
     -+		sb->width = len;
     - 	sb->len = len;
     - 	if (sb->buf != strbuf_slopbuf)
     - 		sb->buf[len] = '\0';
     -@@
     - 		strbuf_grow(sb, 1);
     - 	sb->buf[sb->len++] = c;
     - 	sb->buf[sb->len] = '\0';
     -+	sb->width++;
     - }
     - 
     - /**
  -:  ---------- >  2:  d962eb2e02 graph: handle line padding in `graph_next_line()`
  2:  a130574191 !  3:  ecfcfbfd5c graph: reuse `find_new_column_by_commit()`
     @@ -71,7 +71,7 @@
      -	return NULL;
      -}
      -
     - static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf *sb)
     + static void graph_output_post_merge_line(struct git_graph *graph, struct graph_line *line)
       {
       	int seen_this = 0;
      @@
     @@ -88,17 +88,17 @@
      +			par_column = graph_find_new_column_by_commit(graph, parents->item);
      +			assert(par_column >= 0);
       
     --			strbuf_write_column(sb, par_column, '|');
     -+			strbuf_write_column(sb, &graph->new_columns[par_column], '|');
     +-			graph_line_write_column(line, par_column, '|');
     ++			graph_line_write_column(line, &graph->new_columns[par_column], '|');
       			for (j = 0; j < graph->num_parents - 1; j++) {
       				parents = next_interesting_parent(graph, parents);
       				assert(parents);
      -				par_column = find_new_column_by_commit(graph, parents->item);
      -				assert(par_column);
     --				strbuf_write_column(sb, par_column, '\\');
     +-				graph_line_write_column(line, par_column, '\\');
      +				par_column = graph_find_new_column_by_commit(graph, parents->item);
      +				assert(par_column >= 0);
     -+				strbuf_write_column(sb, &graph->new_columns[par_column], '\\');
     - 				strbuf_addch(sb, ' ');
     ++				graph_line_write_column(line, &graph->new_columns[par_column], '\\');
     + 				graph_line_addch(line, ' ');
       			}
       		} else if (seen_this) {
  3:  21a36efd7b =  4:  50ce875ed9 graph: reduce duplication in `graph_insert_into_new_columns()`
  4:  674b992371 =  5:  d2e8958eed graph: remove `mapping_idx` and `graph_update_width()`
  5:  d5d60ca9a2 !  6:  163600585c graph: extract logic for moving to GRAPH_PRE_COMMIT state
     @@ -35,8 +35,8 @@
       	else
       		graph->state = GRAPH_COMMIT;
      @@
     - 	strbuf_addstr(sb, "...");
     - 	graph_pad_horizontally(graph, sb);
     + 	 */
     + 	graph_line_addstr(line, "...");
       
      -	if (graph->num_parents >= 3 &&
      -	    graph->commit_index < (graph->num_columns - 1))
  -:  ---------- >  7:  51495be940 graph: example of graph output that can be simplified
  6:  12c0916cb1 !  8:  2ab0f9775b graph: tidy up display of left-skewed merges
     @@ -4,7 +4,7 @@
      
          Currently, when we display a merge whose first parent is already present
          in a column to the left of the merge commit, we display the first parent
     -    as a veritcal pipe `|` in the GRAPH_POST_MERGE line and then immediately
     +    as a vertical pipe `|` in the GRAPH_POST_MERGE line and then immediately
          enter the GRAPH_COLLAPSING state. The first-parent line tracks to the
          left and all the other parent lines follow it; this creates a "kink" in
          those lines:
     @@ -187,7 +187,7 @@
       void graph_update(struct git_graph *graph, struct commit *commit)
      @@
       static void graph_output_pre_commit_line(struct git_graph *graph,
     - 					 struct strbuf *sb)
     + 					 struct graph_line *line)
       {
      -	int num_expansion_rows;
       	int i, seen_this;
     @@ -246,7 +246,7 @@
       
      +const char merge_chars[] = {'/', '|', '\\'};
      +
     - static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf *sb)
     + static void graph_output_post_merge_line(struct git_graph *graph, struct graph_line *line)
       {
       	int seen_this = 0;
      -	int i, j;
     @@ -272,7 +272,7 @@
      -			par_column = graph_find_new_column_by_commit(graph, parents->item);
      -			assert(par_column >= 0);
      -
     --			strbuf_write_column(sb, &graph->new_columns[par_column], '|');
     +-			graph_line_write_column(line, &graph->new_columns[par_column], '|');
      -			for (j = 0; j < graph->num_parents - 1; j++) {
      -				parents = next_interesting_parent(graph, parents);
      -				assert(parents);
     @@ -280,37 +280,37 @@
      +			for (; parents; parents = next_interesting_parent(graph, parents)) {
       				par_column = graph_find_new_column_by_commit(graph, parents->item);
       				assert(par_column >= 0);
     --				strbuf_write_column(sb, &graph->new_columns[par_column], '\\');
     --				strbuf_addch(sb, ' ');
     +-				graph_line_write_column(line, &graph->new_columns[par_column], '\\');
     +-				graph_line_addch(line, ' ');
      +
      +				c = merge_chars[idx];
     -+				strbuf_write_column(sb, &graph->new_columns[par_column], c);
     ++				graph_line_write_column(line, &graph->new_columns[par_column], c);
      +				if (idx == 2)
     -+					strbuf_addch(sb, ' ');
     ++					graph_line_addch(line, ' ');
      +				else
      +					idx++;
       			}
       		} else if (seen_this) {
     - 			strbuf_write_column(sb, col, '\\');
     - 			strbuf_addch(sb, ' ');
     + 			graph_line_write_column(line, col, '\\');
     + 			graph_line_addch(line, ' ');
       		} else {
     - 			strbuf_write_column(sb, col, '|');
     --			strbuf_addch(sb, ' ');
     + 			graph_line_write_column(line, col, '|');
     +-			graph_line_addch(line, ' ');
      +			if (graph->merge_layout != 0 || i != graph->commit_index - 1)
     -+				strbuf_addch(sb, seen_parent ? '_' : ' ');
     ++				graph_line_addch(line, seen_parent ? '_' : ' ');
       		}
      +
      +		if (col_commit == first_parent->item)
      +			seen_parent = 1;
       	}
       
     - 	graph_pad_horizontally(graph, sb);
     + 	/*
      
       diff --git a/t/t4214-log-graph-octopus.sh b/t/t4214-log-graph-octopus.sh
       --- a/t/t4214-log-graph-octopus.sh
       +++ b/t/t4214-log-graph-octopus.sh
      @@
     - test_expect_success 'set up merge history' '
     + test_expect_success 'log --graph with tricky octopus merge, no color' '
       	cat >expect.uncolored <<-\EOF &&
       	* left
      -	| *---.   octopus-merge
     @@ -322,7 +322,7 @@
       	| | * | 3
       	| | |/
      @@
     - 	EOF
     + 	test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
       	cat >expect.colors <<-\EOF &&
       	* left
      -	<RED>|<RESET> *<BLUE>-<RESET><BLUE>-<RESET><MAGENTA>-<RESET><MAGENTA>.<RESET>   octopus-merge
     @@ -333,51 +333,88 @@
       	<RED>|<RESET> <YELLOW>|<RESET> <BLUE>|<RESET> * 4
       	<RED>|<RESET> <YELLOW>|<RESET> * <MAGENTA>|<RESET> 3
       	<RED>|<RESET> <YELLOW>|<RESET> <MAGENTA>|<RESET><MAGENTA>/<RESET>
     +@@
     + 	cat >expect.uncolored <<-\EOF &&
     + 	* left
     + 	| * after-merge
     +-	| *---.   octopus-merge
     +-	| |\ \ \
     +-	|/ / / /
     ++	| *-.   octopus-merge
     ++	|/|\ \
     + 	| | | * 4
     + 	| | * | 3
     + 	| | |/
     +@@
     + 	cat >expect.colors <<-\EOF &&
     + 	* left
     + 	<RED>|<RESET> * after-merge
     +-	<RED>|<RESET> *<MAGENTA>-<RESET><MAGENTA>-<RESET><CYAN>-<RESET><CYAN>.<RESET>   octopus-merge
     +-	<RED>|<RESET> <RED>|<RESET><BLUE>\<RESET> <MAGENTA>\<RESET> <CYAN>\<RESET>
     +-	<RED>|<RESET><RED>/<RESET> <BLUE>/<RESET> <MAGENTA>/<RESET> <CYAN>/<RESET>
     ++	<RED>|<RESET> *<CYAN>-<RESET><CYAN>.<RESET>   octopus-merge
     ++	<RED>|<RESET><RED>/<RESET><BLUE>|<RESET><MAGENTA>\<RESET> <CYAN>\<RESET>
     + 	<RED>|<RESET> <BLUE>|<RESET> <MAGENTA>|<RESET> * 4
     + 	<RED>|<RESET> <BLUE>|<RESET> * <CYAN>|<RESET> 3
     + 	<RED>|<RESET> <BLUE>|<RESET> <CYAN>|<RESET><CYAN>/<RESET>
      
       diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
     - new file mode 100755
     - --- /dev/null
     + --- a/t/t4215-log-skewed-merges.sh
       +++ b/t/t4215-log-skewed-merges.sh
      @@
     -+#!/bin/sh
     -+
     -+test_description='git log --graph of skewed merges'
     -+
     -+. ./test-lib.sh
     -+
     -+test_expect_success 'setup left-skewed merge' '
     -+	git checkout --orphan _a && test_commit A &&
     -+	git branch _b &&
     -+	git branch _c &&
     -+	git branch _d &&
     -+	git branch _e &&
     -+	git checkout _b && test_commit B &&
     -+	git checkout _c && test_commit C &&
     -+	git checkout _d && test_commit D &&
     -+	git checkout _e && git merge --no-ff _d -m E &&
     -+	git checkout _a && git merge --no-ff _b _c _e -m F
     -+'
     + 	| *   G
     + 	| |\
     + 	| | * F
     +-	| | |
     +-	| |  \
     +-	| *-. \   E
     +-	| |\ \ \
     +-	|/ / / /
     +-	| | | /
     ++	| * \   E
     ++	|/|\ \
     + 	| | |/
     + 	| | * D
     + 	| * | C
     +@@
     + 	test_cmp expect actual
     + '
     + 
     ++test_expect_success 'log --graph with left-skewed merge' '
     ++	cat >expect <<-\EOF &&
     ++	*-----.   0_H
     ++	|\ \ \ \
     ++	| | | | * 0_G
     ++	| |_|_|/|
     ++	|/| | | |
     ++	| | | * \   0_F
     ++	| |_|/|\ \
     ++	|/| | | |/
     ++	| | | | * 0_E
     ++	| |_|_|/
     ++	|/| | |
     ++	| | * | 0_D
     ++	| | |/
     ++	| | * 0_C
     ++	| |/
     ++	|/|
     ++	| * 0_B
     ++	|/
     ++	* 0_A
     ++	EOF
      +
     -+cat > expect <<\EOF
     -+*---.   F
     -+|\ \ \
     -+| | | * E
     -+| |_|/|
     -+|/| | |
     -+| | | * D
     -+| |_|/
     -+|/| |
     -+| | * C
     -+| |/
     -+|/|
     -+| * B
     -+|/
     -+* A
     -+EOF
     ++	git checkout --orphan 0_p && test_commit 0_A &&
     ++	git checkout -b 0_q 0_p && test_commit 0_B &&
     ++	git checkout -b 0_r 0_p &&
     ++	test_commit 0_C &&
     ++	test_commit 0_D &&
     ++	git checkout -b 0_s 0_p && test_commit 0_E &&
     ++	git checkout -b 0_t 0_p && git merge --no-ff 0_r^ 0_s -m 0_F &&
     ++	git checkout 0_p && git merge --no-ff 0_s -m 0_G &&
     ++	git checkout @^ && git merge --no-ff 0_q 0_r 0_t 0_p -m 0_H &&
      +
     -+test_expect_success 'log --graph with left-skewed merge' '
     -+	git log --graph --pretty=tformat:%s | sed "s/ *$//" > actual &&
     ++	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
      +	test_cmp expect actual
      +'
      +
     -+test_done
     + test_done
  7:  6c173663aa !  9:  0e37c88c60 graph: commit and post-merge lines for left-skewed merges
     @@ -183,10 +183,10 @@
      @@
       
       			if (graph->num_parents > 2)
     - 				graph_draw_octopus_merge(graph, sb);
     + 				graph_draw_octopus_merge(graph, line);
      -		} else if (seen_this && (graph->num_parents > 2)) {
      +		} else if (seen_this && (graph->edges_added > 1)) {
     - 			strbuf_write_column(sb, col, '\\');
     + 			graph_line_write_column(line, col, '\\');
      -		} else if (seen_this && (graph->num_parents == 2)) {
      +		} else if (seen_this && (graph->edges_added == 1)) {
       			/*
     @@ -204,33 +204,67 @@
       			if (graph->prev_state == GRAPH_POST_MERGE &&
      +			    graph->prev_edges_added > 0 &&
       			    graph->prev_commit_index < i)
     - 				strbuf_write_column(sb, col, '\\');
     + 				graph_line_write_column(line, col, '\\');
       			else
      @@
       				else
       					idx++;
       			}
      +			if (graph->edges_added == 0)
     -+				strbuf_addch(sb, ' ');
     ++				graph_line_addch(line, ' ');
      +
       		} else if (seen_this) {
     --			strbuf_write_column(sb, col, '\\');
     +-			graph_line_write_column(line, col, '\\');
      +			if (graph->edges_added > 0)
     -+				strbuf_write_column(sb, col, '\\');
     ++				graph_line_write_column(line, col, '\\');
      +			else
     -+				strbuf_write_column(sb, col, '|');
     - 			strbuf_addch(sb, ' ');
     ++				graph_line_write_column(line, col, '|');
     + 			graph_line_addch(line, ' ');
       		} else {
     - 			strbuf_write_column(sb, col, '|');
     + 			graph_line_write_column(line, col, '|');
      
       diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
       --- a/t/t4215-log-skewed-merges.sh
       +++ b/t/t4215-log-skewed-merges.sh
     +@@
     + 	| *   G
     + 	| |\
     + 	| | * F
     +-	| * \   E
     ++	| * |   E
     + 	|/|\ \
     + 	| | |/
     + 	| | * D
     +@@
     + 	| | | | * 0_G
     + 	| |_|_|/|
     + 	|/| | | |
     +-	| | | * \   0_F
     ++	| | | * |   0_F
     + 	| |_|/|\ \
     + 	|/| | | |/
     + 	| | | | * 0_E
      @@
       	test_cmp expect actual
       '
       
     -+test_expect_success 'setup nested left-skewed merge' '
     ++test_expect_success 'log --graph with nested left-skewed merge' '
     ++	cat >expect <<-\EOF &&
     ++	*   1_H
     ++	|\
     ++	| *   1_G
     ++	| |\
     ++	| | * 1_F
     ++	| * | 1_E
     ++	|/| |
     ++	| * | 1_D
     ++	* | | 1_C
     ++	|/ /
     ++	* | 1_B
     ++	|/
     ++	* 1_A
     ++	EOF
     ++
      +	git checkout --orphan 1_p &&
      +	test_commit 1_A &&
      +	test_commit 1_B &&
     @@ -239,31 +273,34 @@
      +	git checkout 1_p && git merge --no-ff 1_q -m 1_E &&
      +	git checkout -b 1_r @~3 && test_commit 1_F &&
      +	git checkout 1_p && git merge --no-ff 1_r -m 1_G &&
     -+	git checkout @^^ && git merge --no-ff 1_p -m 1_H
     -+'
     -+
     -+cat > expect <<\EOF
     -+*   1_H
     -+|\
     -+| *   1_G
     -+| |\
     -+| | * 1_F
     -+| * | 1_E
     -+|/| |
     -+| * | 1_D
     -+* | | 1_C
     -+|/ /
     -+* | 1_B
     -+|/
     -+* 1_A
     -+EOF
     ++	git checkout @^^ && git merge --no-ff 1_p -m 1_H &&
      +
     -+test_expect_success 'log --graph with nested left-skewed merge' '
     -+	git log --graph --pretty=tformat:%s | sed "s/ *$//" > actual &&
     ++	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
      +	test_cmp expect actual
      +'
      +
     -+test_expect_success 'setup nested left-skewed merge following normal merge' '
     ++test_expect_success 'log --graph with nested left-skewed merge following normal merge' '
     ++	cat >expect <<-\EOF &&
     ++	*   2_K
     ++	|\
     ++	| *   2_J
     ++	| |\
     ++	| | *   2_H
     ++	| | |\
     ++	| | * | 2_G
     ++	| |/| |
     ++	| | * | 2_F
     ++	| * | | 2_E
     ++	| |/ /
     ++	| * | 2_D
     ++	* | | 2_C
     ++	| |/
     ++	|/|
     ++	* | 2_B
     ++	|/
     ++	* 2_A
     ++	EOF
     ++
      +	git checkout --orphan 2_p &&
      +	test_commit 2_A &&
      +	test_commit 2_B &&
     @@ -276,36 +313,32 @@
      +	git merge --no-ff 2_r -m 2_G &&
      +	git merge --no-ff 2_p^ -m 2_H &&
      +	git checkout -b 2_s @^^ && git merge --no-ff 2_q -m 2_J &&
     -+	git checkout 2_p && git merge --no-ff 2_s -m 2_K
     -+'
     -+
     -+cat > expect <<\EOF
     -+*   2_K
     -+|\
     -+| *   2_J
     -+| |\
     -+| | *   2_H
     -+| | |\
     -+| | * | 2_G
     -+| |/| |
     -+| | * | 2_F
     -+| * | | 2_E
     -+| |/ /
     -+| * | 2_D
     -+* | | 2_C
     -+| |/
     -+|/|
     -+* | 2_B
     -+|/
     -+* 2_A
     -+EOF
     ++	git checkout 2_p && git merge --no-ff 2_s -m 2_K &&
      +
     -+test_expect_success 'log --graph with nested left-skewed merge following normal merge' '
     -+	git log --graph --pretty=tformat:%s | sed "s/ *$//" > actual &&
     ++	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
      +	test_cmp expect actual
      +'
      +
     -+test_expect_success 'setup nested right-skewed merge following left-skewed merge' '
     ++test_expect_success 'log --graph with nested right-skewed merge following left-skewed merge' '
     ++	cat >expect <<-\EOF &&
     ++	*   3_J
     ++	|\
     ++	| *   3_H
     ++	| |\
     ++	| | * 3_G
     ++	| * | 3_F
     ++	|/| |
     ++	| * |   3_E
     ++	| |\ \
     ++	| | |/
     ++	| | * 3_D
     ++	| * | 3_C
     ++	| |/
     ++	| * 3_B
     ++	|/
     ++	* 3_A
     ++	EOF
     ++
      +	git checkout --orphan 3_p &&
      +	test_commit 3_A &&
      +	git checkout -b 3_q &&
     @@ -317,34 +350,32 @@
      +	git checkout 3_p && git merge --no-ff 3_q -m 3_F &&
      +	git checkout 3_r && test_commit 3_G &&
      +	git checkout 3_p && git merge --no-ff 3_r -m 3_H &&
     -+	git checkout @^^ && git merge --no-ff 3_p -m 3_J
     -+'
     -+
     -+cat > expect <<\EOF
     -+*   3_J
     -+|\
     -+| *   3_H
     -+| |\
     -+| | * 3_G
     -+| * | 3_F
     -+|/| |
     -+| * |   3_E
     -+| |\ \
     -+| | |/
     -+| | * 3_D
     -+| * | 3_C
     -+| |/
     -+| * 3_B
     -+|/
     -+* 3_A
     -+EOF
     ++	git checkout @^^ && git merge --no-ff 3_p -m 3_J &&
      +
     -+test_expect_success 'log --graph with nested right-skewed merge following left-skewed merge' '
     -+	git log --graph --pretty=tformat:%s | sed "s/ *$//" > actual &&
     ++	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
      +	test_cmp expect actual
      +'
      +
     -+test_expect_success 'setup right-skewed merge following a left-skewed one' '
     ++test_expect_success 'log --graph with right-skewed merge following a left-skewed one' '
     ++	cat >expect <<-\EOF &&
     ++	*   4_H
     ++	|\
     ++	| *   4_G
     ++	| |\
     ++	| * | 4_F
     ++	|/| |
     ++	| * |   4_E
     ++	| |\ \
     ++	| | * | 4_D
     ++	| |/ /
     ++	|/| |
     ++	| | * 4_C
     ++	| |/
     ++	| * 4_B
     ++	|/
     ++	* 4_A
     ++	EOF
     ++
      +	git checkout --orphan 4_p &&
      +	test_commit 4_A &&
      +	test_commit 4_B &&
     @@ -354,30 +385,9 @@
      +	git checkout -b 4_s 4_p^^ &&
      +	git merge --no-ff 4_r -m 4_F &&
      +	git merge --no-ff 4_p -m 4_G &&
     -+	git checkout @^^ && git merge --no-ff 4_s -m 4_H
     -+'
     ++	git checkout @^^ && git merge --no-ff 4_s -m 4_H &&
      +
     -+cat > expect <<\EOF
     -+*   4_H
     -+|\
     -+| *   4_G
     -+| |\
     -+| * | 4_F
     -+|/| |
     -+| * |   4_E
     -+| |\ \
     -+| | * | 4_D
     -+| |/ /
     -+|/| |
     -+| | * 4_C
     -+| |/
     -+| * 4_B
     -+|/
     -+* 4_A
     -+EOF
     -+
     -+test_expect_success 'log --graph with right-skewed merge following a left-skewed one' '
     -+	git log --graph --date-order --pretty=tformat:%s | sed "s/ *$//" > actual &&
     ++	git log --graph --date-order --pretty=tformat:%s | sed "s/ *$//" >actual &&
      +	test_cmp expect actual
      +'
      +
  8:  23f13bbefa ! 10:  9bbf738e6d graph: rename `new_mapping` to `old_mapping`
     @@ -146,7 +146,7 @@
      -		int target = graph->new_mapping[i];
      +		int target = graph->mapping[i];
       		if (target < 0)
     - 			strbuf_addch(sb, ' ');
     + 			graph_line_addch(line, ' ');
       		else if (target * 2 == i)
      @@
       				 * won't continue into the next line.
     @@ -155,17 +155,15 @@
      -					graph->new_mapping[i] = -1;
      +					graph->mapping[i] = -1;
       				used_horizontal = 1;
     - 			strbuf_write_column(sb, &graph->new_columns[target], '_');
     + 			graph_line_write_column(line, &graph->new_columns[target], '_');
       		} else {
       			if (used_horizontal && i < horizontal_edge)
      -				graph->new_mapping[i] = -1;
      +				graph->mapping[i] = -1;
     - 			strbuf_write_column(sb, &graph->new_columns[target], '/');
     + 			graph_line_write_column(line, &graph->new_columns[target], '/');
       
       		}
     -@@
     - 
     - 	graph_pad_horizontally(graph, sb);
     + 	}
       
      -	/*
      -	 * Swap mapping and new_mapping

9: f9ced9090d ! 11: 67051ec31a graph: smooth appearance of collapsing edges on commit lines
@@ -77,15 +77,15 @@
int *old_mapping;
/*
@@
- strbuf_write_column(sb, col, '\\');
+ graph_line_write_column(line, col, '\\');
else
- strbuf_write_column(sb, col, '|');
+ graph_line_write_column(line, col, '|');
+ } else if (graph->prev_state == GRAPH_COLLAPSING &&
+ graph->old_mapping[2 * i + 1] == i &&
+ graph->mapping[2 * i] < i) {
-+ strbuf_write_column(sb, col, '/');
++ graph_line_write_column(line, col, '/');
} else {
- strbuf_write_column(sb, col, '|');
+ graph_line_write_column(line, col, '|');
}
@@
}
@@ -177,19 +177,76 @@
<BLUE>|<RESET><BLUE>/<RESET>
* initial
EOF
+@@
+ | | | * 4
+ | | * | 3
+ | | |/
+- | * | 2
++ | * / 2
+ | |/
+- * | 1
++ * / 1
+ |/
+ * initial
+ EOF
+@@
+ <GREEN>|<RESET> <YELLOW>|<RESET> <BLUE>|<RESET> * 4
+ <GREEN>|<RESET> <YELLOW>|<RESET> * <MAGENTA>|<RESET> 3
+ <GREEN>|<RESET> <YELLOW>|<RESET> <MAGENTA>|<RESET><MAGENTA>/<RESET>
+- <GREEN>|<RESET> * <MAGENTA>|<RESET> 2
++ <GREEN>|<RESET> * <MAGENTA>/<RESET> 2
+ <GREEN>|<RESET> <MAGENTA>|<RESET><MAGENTA>/<RESET>
+- * <MAGENTA>|<RESET> 1
++ * <MAGENTA>/<RESET> 1
+ <MAGENTA>|<RESET><MAGENTA>/<RESET>
+ * initial
+ EOF
+@@
+ | | | * 4
+ | | * | 3
+ | | |/
+- | * | 2
++ | * / 2
+ | |/
+- * | 1
++ * / 1
+ |/
+ * initial
+ EOF
+@@
+ <RED>|<RESET> <BLUE>|<RESET> <MAGENTA>|<RESET> * 4
+ <RED>|<RESET> <BLUE>|<RESET> * <CYAN>|<RESET> 3
+ <RED>|<RESET> <BLUE>|<RESET> <CYAN>|<RESET><CYAN>/<RESET>
+- <RED>|<RESET> * <CYAN>|<RESET> 2
++ <RED>|<RESET> * <CYAN>/<RESET> 2
+ <RED>|<RESET> <CYAN>|<RESET><CYAN>/<RESET>
+- * <CYAN>|<RESET> 1
++ * <CYAN>/<RESET> 1
+ <CYAN>|<RESET><CYAN>/<RESET>
+ * initial
+ EOF

diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh --- a/t/t4215-log-skewed-merges.sh
+++ b/t/t4215-log-skewed-merges.sh
@@
- | * | 1_D
- * | | 1_C
- |/ /
--* | 1_B
-+* / 1_B
- |/
- * 1_A
- EOF
+ | | * D
+ | * | C
+ | |/
+- * | B
++ * / B
+ |/
+ * A
+ EOF
+@@
+ | * | 1_D
+ * | | 1_C
+ |/ /
+- * | 1_B
++ * / 1_B
+ |/
+ * 1_A
+ EOF

diff --git a/t/t6016-rev-list-graph-simplify-history.sh b/t/t6016-rev-list-graph-simplify-history.sh --- a/t/t6016-rev-list-graph-simplify-history.sh
10: 50756edcf7 ! 12: 503c846d2b graph: flatten edges that join to their right neighbor

     @@ -1,6 +1,6 @@
      Author: James Coglan <[email protected]>
      
     -    graph: flatten edges that join to their right neighbor
     +    graph: flatten edges that fuse with their right neighbor
      
          When a merge commit is printed and its final parent is the same commit
          that occupies the column to the right of the merge, this results in a
     @@ -109,7 +109,7 @@
       
       	/*
      @@
     - static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf *sb)
     + static void graph_output_post_merge_line(struct git_graph *graph, struct graph_line *line)
       {
       	int seen_this = 0;
      -	int i;
     @@ -127,41 +127,86 @@
       				assert(par_column >= 0);
       
       				c = merge_chars[idx];
     - 				strbuf_write_column(sb, &graph->new_columns[par_column], c);
     + 				graph_line_write_column(line, &graph->new_columns[par_column], c);
      -				if (idx == 2)
     --					strbuf_addch(sb, ' ');
     +-					graph_line_addch(line, ' ');
      -				else
      +				if (idx == 2) {
      +					if (graph->edges_added > 0 || j < graph->num_parents - 1)
     -+						strbuf_addch(sb, ' ');
     ++						graph_line_addch(line, ' ');
      +				} else {
       					idx++;
      +				}
      +				parents = next_interesting_parent(graph, parents);
       			}
       			if (graph->edges_added == 0)
     - 				strbuf_addch(sb, ' ');
     + 				graph_line_addch(line, ' ');
      
       diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
       --- a/t/t4215-log-skewed-merges.sh
       +++ b/t/t4215-log-skewed-merges.sh
      @@
     - | | * 3_G
     - | * | 3_F
     - |/| |
     --| * |   3_E
     --| |\ \
     --| | |/
     -+| * | 3_E
     -+| |\|
     - | | * 3_D
     - | * | 3_C
     - | |/
     + 	| *   G
     + 	| |\
     + 	| | * F
     +-	| * |   E
     +-	|/|\ \
     +-	| | |/
     ++	| * | E
     ++	|/|\|
     + 	| | * D
     + 	| * | C
     + 	| |/
     +@@
     + 	| | | | * 0_G
     + 	| |_|_|/|
     + 	|/| | | |
     +-	| | | * |   0_F
     +-	| |_|/|\ \
     +-	|/| | | |/
     ++	| | | * | 0_F
     ++	| |_|/|\|
     ++	|/| | | |
     + 	| | | | * 0_E
     + 	| |_|_|/
     + 	|/| | |
     +@@
     + 	| | * 3_G
     + 	| * | 3_F
     + 	|/| |
     +-	| * |   3_E
     +-	| |\ \
     +-	| | |/
     ++	| * | 3_E
     ++	| |\|
     + 	| | * 3_D
     + 	| * | 3_C
     + 	| |/
      @@
       	test_cmp expect actual
       '
       
     -+test_expect_success 'setup octopus merge with column joining its penultimate parent' '
     ++test_expect_success 'log --graph with octopus merge with column joining its penultimate parent' '
     ++	cat >expect <<-\EOF &&
     ++	*   5_H
     ++	|\
     ++	| *-.   5_G
     ++	| |\ \
     ++	| | | * 5_F
     ++	| | * |   5_E
     ++	| |/|\ \
     ++	| |_|/ /
     ++	|/| | /
     ++	| | |/
     ++	* | | 5_D
     ++	| | * 5_C
     ++	| |/
     ++	|/|
     ++	| * 5_B
     ++	|/
     ++	* 5_A
     ++	EOF
     ++
      +	git checkout --orphan 5_p &&
      +	test_commit 5_A &&
      +	git branch 5_q &&
     @@ -175,64 +220,9 @@
      +	git checkout -b 5_s 5_p^ &&
      +	git merge --no-ff 5_p 5_q -m 5_G &&
      +	git checkout 5_r &&
     -+	git merge --no-ff 5_s -m 5_H
     -+'
     -+
     -+cat > expect <<\EOF
     -+*   5_H
     -+|\
     -+| *-.   5_G
     -+| |\ \
     -+| | | * 5_F
     -+| | * |   5_E
     -+| |/|\ \
     -+| |_|/ /
     -+|/| | /
     -+| | |/
     -+* | | 5_D
     -+| | * 5_C
     -+| |/
     -+|/|
     -+| * 5_B
     -+|/
     -+* 5_A
     -+EOF
     -+
     -+test_expect_success 'log --graph with octopus merge with column joining its penultimate parent' '
     -+	git log --graph --pretty=tformat:%s | sed "s/ *$//" > actual &&
     -+	test_cmp expect actual
     -+'
     -+
     -+test_expect_success 'setup merge fusing with its left and right neighbors' '
     -+	git checkout --orphan 6_p &&
     -+	test_commit 6_A &&
     -+	test_commit 6_B &&
     -+	git checkout -b 6_q @^ && test_commit 6_C &&
     -+	git checkout -b 6_r @^ && test_commit 6_D &&
     -+	git checkout 6_p && git merge --no-ff 6_q 6_r -m 6_E &&
     -+	git checkout 6_r && test_commit 6_F &&
     -+	git checkout 6_p && git merge --no-ff 6_r -m 6_G &&
     -+	git checkout @^^ && git merge --no-ff 6_p -m 6_H
     -+'
     -+
     -+cat > expect <<\EOF
     -+*   6_H
     -+|\
     -+| *   6_G
     -+| |\
     -+| | * 6_F
     -+| * | 6_E
     -+|/|\|
     -+| | * 6_D
     -+| * | 6_C
     -+| |/
     -+* / 6_B
     -+|/
     -+* 6_A
     -+EOF
     ++	git merge --no-ff 5_s -m 5_H &&
      +
     -+test_expect_success 'log --graph with merge fusing with its left and right neighbors' '
     -+	git log --graph --pretty=tformat:%s | sed "s/ *$//" > actual &&
     ++	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
      +	test_cmp expect actual
      +'
      +
 11:  ea0df1d94a ! 13:  07ddd509c5 graph: fix coloring of octopus dashes
     @@ -81,7 +81,7 @@
       
       static int graph_needs_pre_commit_line(struct git_graph *graph)
      @@
     - static void graph_draw_octopus_merge(struct git_graph *graph, struct strbuf *sb)
     + static void graph_draw_octopus_merge(struct git_graph *graph, struct graph_line *line)
       {
       	/*
      -	 * Here dashless_parents represents the number of parents which don't
     @@ -142,103 +142,69 @@
       
      -	int i;
      -	for (i = 0; i < dashful_parents; i++) {
     --		strbuf_write_column(sb, &graph->new_columns[i+first_col], '-');
     --		strbuf_write_column(sb, &graph->new_columns[i+first_col],
     --				    i == dashful_parents-1 ? '.' : '-');
     +-		graph_line_write_column(line, &graph->new_columns[i+first_col], '-');
     +-		graph_line_write_column(line, &graph->new_columns[i+first_col],
     +-					  i == dashful_parents-1 ? '.' : '-');
      +	int dashed_parents = graph_num_dashed_parents(graph);
      +
      +	for (i = 0; i < dashed_parents; i++) {
      +		j = graph->mapping[(graph->commit_index + i + 2) * 2];
      +		col = &graph->new_columns[j];
      +
     -+		strbuf_write_column(sb, col, '-');
     -+		strbuf_write_column(sb, col, (i == dashed_parents - 1) ? '.' : '-');
     ++		graph_line_write_column(line, col, '-');
     ++		graph_line_write_column(line, col, (i == dashed_parents - 1) ? '.' : '-');
       	}
      +
      +	return;
       }
       
     - static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
     + static void graph_output_commit_line(struct git_graph *graph, struct graph_line *line)
      
       diff --git a/t/t4214-log-graph-octopus.sh b/t/t4214-log-graph-octopus.sh
       --- a/t/t4214-log-graph-octopus.sh
       +++ b/t/t4214-log-graph-octopus.sh
      @@
     - 	test_tick &&
     - 	git merge -m octopus-merge 1 2 3 4 &&
     - 	git checkout 1 -b L &&
     --	test_commit left
     -+	test_commit left &&
     -+	git checkout 4 -b R &&
     -+	test_commit right
     + 	test_cmp expect.uncolored actual
     + '
     + 
     +-test_expect_failure 'log --graph with normal octopus and child merge with colors' '
     ++test_expect_success 'log --graph with normal octopus and child merge with colors' '
     + 	cat >expect.colors <<-\EOF &&
     + 	* after-merge
     + 	*<BLUE>-<RESET><BLUE>-<RESET><MAGENTA>-<RESET><MAGENTA>.<RESET>   octopus-merge
     +@@
     + 	test_cmp expect.uncolored actual
       '
       
     - test_expect_success 'log --graph with tricky octopus merge with colors' '
     +-test_expect_failure 'log --graph with tricky octopus merge and its child with colors' '
     ++test_expect_success 'log --graph with tricky octopus merge and its child with colors' '
       	test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
     --	git log --color=always --graph --date-order --pretty=tformat:%s --all >actual.colors.raw &&
     -+	git log --color=always --graph --date-order --pretty=tformat:%s L merge >actual.colors.raw &&
     - 	test_decode_color <actual.colors.raw | sed "s/ *\$//" >actual.colors &&
     - 	test_cmp expect.colors actual.colors
     + 	cat >expect.colors <<-\EOF &&
     + 	* left
     +@@
     + 	test_cmp expect.uncolored actual
       '
       
     - test_expect_success 'log --graph with tricky octopus merge, no color' '
     --	git log --color=never --graph --date-order --pretty=tformat:%s --all >actual.raw &&
     -+	git log --color=never --graph --date-order --pretty=tformat:%s L merge >actual.raw &&
     - 	sed "s/ *\$//" actual.raw >actual &&
     +-test_expect_failure 'log --graph with crossover in octopus merge with colors' '
     ++test_expect_success 'log --graph with crossover in octopus merge with colors' '
     + 	test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
     + 	cat >expect.colors <<-\EOF &&
     + 	* after-4
     +@@
       	test_cmp expect.uncolored actual
       '
       
     --# Repeat the previous two tests with "normal" octopus merge (i.e.,
     -+# Repeat the previous two tests with an octopus merge whose final parent skews left
     -+
     -+test_expect_success 'log --graph with left-skewed final parent, no color' '
     -+	cat >expect.uncolored <<-\EOF &&
     -+	* right
     -+	| *---.   octopus-merge
     -+	| |\ \ \
     -+	| |_|_|/
     -+	|/| | |
     -+	* | | | 4
     -+	| | | * 3
     -+	| |_|/
     -+	|/| |
     -+	| | * 2
     -+	| |/
     -+	|/|
     -+	| * 1
     -+	|/
     -+	* initial
     -+	EOF
     -+	git log --color=never --graph --date-order --pretty=tformat:%s R merge >actual.raw &&
     -+	sed "s/ *\$//" actual.raw >actual &&
     -+	test_cmp expect.uncolored actual
     -+'
     -+
     -+test_expect_success 'log --graph with left-skewed final parent with colors' '
     -+	cat >expect.colors <<-\EOF &&
     -+	* right
     -+	<RED>|<RESET> *<BLUE>-<RESET><BLUE>-<RESET><RED>-<RESET><RED>.<RESET>   octopus-merge
     -+	<RED>|<RESET> <GREEN>|<RESET><YELLOW>\<RESET> <BLUE>\<RESET> <RED>\<RESET>
     -+	<RED>|<RESET> <GREEN>|<RESET><RED>_<RESET><YELLOW>|<RESET><RED>_<RESET><BLUE>|<RESET><RED>/<RESET>
     -+	<RED>|<RESET><RED>/<RESET><GREEN>|<RESET> <YELLOW>|<RESET> <BLUE>|<RESET>
     -+	* <GREEN>|<RESET> <YELLOW>|<RESET> <BLUE>|<RESET> 4
     -+	<MAGENTA>|<RESET> <GREEN>|<RESET> <YELLOW>|<RESET> * 3
     -+	<MAGENTA>|<RESET> <GREEN>|<RESET><MAGENTA>_<RESET><YELLOW>|<RESET><MAGENTA>/<RESET>
     -+	<MAGENTA>|<RESET><MAGENTA>/<RESET><GREEN>|<RESET> <YELLOW>|<RESET>
     -+	<MAGENTA>|<RESET> <GREEN>|<RESET> * 2
     -+	<MAGENTA>|<RESET> <GREEN>|<RESET><MAGENTA>/<RESET>
     -+	<MAGENTA>|<RESET><MAGENTA>/<RESET><GREEN>|<RESET>
     -+	<MAGENTA>|<RESET> * 1
     -+	<MAGENTA>|<RESET><MAGENTA>/<RESET>
     -+	* initial
     -+	EOF
     -+	test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
     -+	git log --color=always --graph --date-order --pretty=tformat:%s R merge >actual.colors.raw &&
     -+	test_decode_color <actual.colors.raw | sed "s/ *\$//" >actual.colors &&
     -+	test_cmp expect.colors actual.colors
     -+'
     -+
     -+# Repeat the first two tests with "normal" octopus merge (i.e.,
     - # without the first parent skewing to the "left" branch column).
     +-test_expect_failure 'log --graph with crossover in octopus merge and its child with colors' '
     ++test_expect_success 'log --graph with crossover in octopus merge and its child with colors' '
     + 	test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
     + 	cat >expect.colors <<-\EOF &&
     + 	* after-4
     +@@
     + 	test_cmp expect.uncolored actual
     + '
       
     - test_expect_success 'log --graph with normal octopus merge, no color' '
     +-test_expect_failure 'log --graph with unrelated commit and octopus child with colors' '
     ++test_expect_success 'log --graph with unrelated commit and octopus child with colors' '
     + 	test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
     + 	cat >expect.colors <<-\EOF &&
     + 	* after-initial
Signature · 1 line
gitgitgadget
#43 lore raw

PATCH v2 1/13 graph: automatically track display width of graph lines

From: James Coglan <[email protected]>

All the output functions called by graph_next_line() currently keep track of how many printable chars they've written to the buffer, before calling graph_pad_horizontally() to pad the line with spaces. Some functions do this by incrementing a counter whenever they write to the buffer, and others do it by encoding an assumption about how many chars are written, as in:

    graph_pad_horizontally(graph, sb, graph->num_columns * 2);

This adds a fair amount of noise to the functions' logic and is easily broken if one forgets to increment the right counter or update the calculations used for padding.

To make this easier to use, I'm introducing a new struct called graph_line that wraps a strbuf and keeps count of its display width implicitly. graph_next_line() wraps this around the struct strbuf * it's given and passes a struct graph_line * to the output functions, which use its interface.

The graph_line interface wraps the strbuf_addch(), strbuf_addchars() and strbuf_addstr() functions, and adds the graph_line_write_column() function for adding a single character with color formatting. The graph_pad_horizontally() function can then use the width field from the struct rather than taking a character count as a parameter.

---
 graph.c | 194 +++++++++++++++++++++++++++++---------------------------
 1 file changed, 99 insertions(+), 95 deletions(-)
graph.c +99 -95
index f53135485f..2f81a5d23d 100644
@@ -112,14 +112,42 @@ static const char *column_get_color_code(unsigned short color)
 	return column_colors[color];
 }
 
-static void strbuf_write_column(struct strbuf *sb, const struct column *c,
-				char col_char)
+struct graph_line {
+	struct strbuf *buf;
+	size_t width;
+};
+
+static inline void graph_line_addch(struct graph_line *line, int c)
+{
+	strbuf_addch(line->buf, c);
+	line->width++;
+}
+
+static inline void graph_line_addchars(struct graph_line *line, int c, size_t n)
+{
+	strbuf_addchars(line->buf, c, n);
+	line->width += n;
+}
+
+static inline void graph_line_addstr(struct graph_line *line, const char *s)
+{
+	strbuf_addstr(line->buf, s);
+	line->width += strlen(s);
+}
+
+static inline void graph_line_addcolor(struct graph_line *line, unsigned short color)
+{
+	strbuf_addstr(line->buf, column_get_color_code(color));
+}
+
+static void graph_line_write_column(struct graph_line *line, const struct column *c,
+				    char col_char)
 {
 	if (c->color < column_colors_max)
-		strbuf_addstr(sb, column_get_color_code(c->color));
-	strbuf_addch(sb, col_char);
+		graph_line_addcolor(line, c->color);
+	graph_line_addch(line, col_char);
 	if (c->color < column_colors_max)
-		strbuf_addstr(sb, column_get_color_code(column_colors_max));
+		graph_line_addcolor(line, column_colors_max);
 }
 
 struct git_graph {
@@ -686,8 +714,7 @@ static int graph_is_mapping_correct(struct git_graph *graph)
 	return 1;
 }
 
-static void graph_pad_horizontally(struct git_graph *graph, struct strbuf *sb,
-				   int chars_written)
+static void graph_pad_horizontally(struct git_graph *graph, struct graph_line *line)
 {
 	/*
 	 * Add additional spaces to the end of the strbuf, so that all
@@ -696,12 +723,12 @@ static void graph_pad_horizontally(struct git_graph *graph, struct strbuf *sb,
 	 * This way, fields printed to the right of the graph will remain
 	 * aligned for the entire commit.
 	 */
-	if (chars_written < graph->width)
-		strbuf_addchars(sb, ' ', graph->width - chars_written);
+	if (line->width < graph->width)
+		graph_line_addchars(line, ' ', graph->width - line->width);
 }
 
 static void graph_output_padding_line(struct git_graph *graph,
-				      struct strbuf *sb)
+				      struct graph_line *line)
 {
 	int i;
 
@@ -719,11 +746,11 @@ 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++) {
-		strbuf_write_column(sb, &graph->new_columns[i], '|');
-		strbuf_addch(sb, ' ');
+		graph_line_write_column(line, &graph->new_columns[i], '|');
+		graph_line_addch(line, ' ');
 	}
 
-	graph_pad_horizontally(graph, sb, graph->num_new_columns * 2);
+	graph_pad_horizontally(graph, line);
 }
 
 
@@ -733,14 +760,14 @@ int graph_width(struct git_graph *graph)
 }
 
 
-static void graph_output_skip_line(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_skip_line(struct git_graph *graph, struct graph_line *line)
 {
 	/*
 	 * Output an ellipsis to indicate that a portion
 	 * of the graph is missing.
 	 */
-	strbuf_addstr(sb, "...");
-	graph_pad_horizontally(graph, sb, 3);
+	graph_line_addstr(line, "...");
+	graph_pad_horizontally(graph, line);
 
 	if (graph->num_parents >= 3 &&
 	    graph->commit_index < (graph->num_columns - 1))
@@ -750,11 +777,10 @@ static void graph_output_skip_line(struct git_graph *graph, struct strbuf *sb)
 }
 
 static void graph_output_pre_commit_line(struct git_graph *graph,
-					 struct strbuf *sb)
+					 struct graph_line *line)
 {
 	int num_expansion_rows;
 	int i, seen_this;
-	int chars_written;
 
 	/*
 	 * This function formats a row that increases the space around a commit
@@ -777,14 +803,12 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 	 * Output the row
 	 */
 	seen_this = 0;
-	chars_written = 0;
 	for (i = 0; i < graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		if (col->commit == graph->commit) {
 			seen_this = 1;
-			strbuf_write_column(sb, col, '|');
-			strbuf_addchars(sb, ' ', graph->expansion_row);
-			chars_written += 1 + graph->expansion_row;
+			graph_line_write_column(line, col, '|');
+			graph_line_addchars(line, ' ', graph->expansion_row);
 		} else if (seen_this && (graph->expansion_row == 0)) {
 			/*
 			 * This is the first line of the pre-commit output.
@@ -797,22 +821,18 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 			 */
 			if (graph->prev_state == GRAPH_POST_MERGE &&
 			    graph->prev_commit_index < i)
-				strbuf_write_column(sb, col, '\\');
+				graph_line_write_column(line, col, '\\');
 			else
-				strbuf_write_column(sb, col, '|');
-			chars_written++;
+				graph_line_write_column(line, col, '|');
 		} else if (seen_this && (graph->expansion_row > 0)) {
-			strbuf_write_column(sb, col, '\\');
-			chars_written++;
+			graph_line_write_column(line, col, '\\');
 		} else {
-			strbuf_write_column(sb, col, '|');
-			chars_written++;
+			graph_line_write_column(line, col, '|');
 		}
-		strbuf_addch(sb, ' ');
-		chars_written++;
+		graph_line_addch(line, ' ');
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, line);
 
 	/*
 	 * Increment graph->expansion_row,
@@ -823,7 +843,7 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 		graph_update_state(graph, GRAPH_COMMIT);
 }
 
-static void graph_output_commit_char(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_commit_char(struct git_graph *graph, struct graph_line *line)
 {
 	/*
 	 * For boundary commits, print 'o'
@@ -831,22 +851,20 @@ static void graph_output_commit_char(struct git_graph *graph, struct strbuf *sb)
 	 */
 	if (graph->commit->object.flags & BOUNDARY) {
 		assert(graph->revs->boundary);
-		strbuf_addch(sb, 'o');
+		graph_line_addch(line, 'o');
 		return;
 	}
 
 	/*
 	 * get_revision_mark() handles all other cases without assert()
 	 */
-	strbuf_addstr(sb, get_revision_mark(graph->revs, graph->commit));
+	graph_line_addstr(line, get_revision_mark(graph->revs, graph->commit));
 }
 
 /*
- * Draw the horizontal dashes of an octopus merge and return the number of
- * characters written.
+ * Draw the horizontal dashes of an octopus merge.
  */
-static int graph_draw_octopus_merge(struct git_graph *graph,
-				    struct strbuf *sb)
+static void graph_draw_octopus_merge(struct git_graph *graph, struct graph_line *line)
 {
 	/*
 	 * Here dashless_parents represents the number of parents which don't
@@ -886,17 +904,16 @@ static int graph_draw_octopus_merge(struct git_graph *graph,
 
 	int i;
 	for (i = 0; i < dashful_parents; i++) {
-		strbuf_write_column(sb, &graph->new_columns[i+first_col], '-');
-		strbuf_write_column(sb, &graph->new_columns[i+first_col],
-				    i == dashful_parents-1 ? '.' : '-');
+		graph_line_write_column(line, &graph->new_columns[i+first_col], '-');
+		graph_line_write_column(line, &graph->new_columns[i+first_col],
+					  i == dashful_parents-1 ? '.' : '-');
 	}
-	return 2 * dashful_parents;
 }
 
-static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_commit_line(struct git_graph *graph, struct graph_line *line)
 {
 	int seen_this = 0;
-	int i, chars_written;
+	int i;
 
 	/*
 	 * Output the row containing this commit
@@ -906,7 +923,6 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 	 * children that we have already processed.)
 	 */
 	seen_this = 0;
-	chars_written = 0;
 	for (i = 0; i <= graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		struct commit *col_commit;
@@ -920,15 +936,12 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 
 		if (col_commit == graph->commit) {
 			seen_this = 1;
-			graph_output_commit_char(graph, sb);
-			chars_written++;
+			graph_output_commit_char(graph, line);
 
 			if (graph->num_parents > 2)
-				chars_written += graph_draw_octopus_merge(graph,
-									  sb);
+				graph_draw_octopus_merge(graph, line);
 		} else if (seen_this && (graph->num_parents > 2)) {
-			strbuf_write_column(sb, col, '\\');
-			chars_written++;
+			graph_line_write_column(line, col, '\\');
 		} else if (seen_this && (graph->num_parents == 2)) {
 			/*
 			 * This is a 2-way merge commit.
@@ -945,19 +958,16 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 			 */
 			if (graph->prev_state == GRAPH_POST_MERGE &&
 			    graph->prev_commit_index < i)
-				strbuf_write_column(sb, col, '\\');
+				graph_line_write_column(line, col, '\\');
 			else
-				strbuf_write_column(sb, col, '|');
-			chars_written++;
+				graph_line_write_column(line, col, '|');
 		} else {
-			strbuf_write_column(sb, col, '|');
-			chars_written++;
+			graph_line_write_column(line, col, '|');
 		}
-		strbuf_addch(sb, ' ');
-		chars_written++;
+		graph_line_addch(line, ' ');
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, line);
 
 	/*
 	 * Update graph->state
@@ -981,15 +991,14 @@ static struct column *find_new_column_by_commit(struct git_graph *graph,
 	return NULL;
 }
 
-static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_post_merge_line(struct git_graph *graph, struct graph_line *line)
 {
 	int seen_this = 0;
-	int i, j, chars_written;
+	int i, j;
 
 	/*
 	 * Output the post-merge row
 	 */
-	chars_written = 0;
 	for (i = 0; i <= graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		struct commit *col_commit;
@@ -1016,29 +1025,25 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf
 			par_column = find_new_column_by_commit(graph, parents->item);
 			assert(par_column);
 
-			strbuf_write_column(sb, par_column, '|');
-			chars_written++;
+			graph_line_write_column(line, par_column, '|');
 			for (j = 0; j < graph->num_parents - 1; j++) {
 				parents = next_interesting_parent(graph, parents);
 				assert(parents);
 				par_column = find_new_column_by_commit(graph, parents->item);
 				assert(par_column);
-				strbuf_write_column(sb, par_column, '\\');
-				strbuf_addch(sb, ' ');
+				graph_line_write_column(line, par_column, '\\');
+				graph_line_addch(line, ' ');
 			}
-			chars_written += j * 2;
 		} else if (seen_this) {
-			strbuf_write_column(sb, col, '\\');
-			strbuf_addch(sb, ' ');
-			chars_written += 2;
+			graph_line_write_column(line, col, '\\');
+			graph_line_addch(line, ' ');
 		} else {
-			strbuf_write_column(sb, col, '|');
-			strbuf_addch(sb, ' ');
-			chars_written += 2;
+			graph_line_write_column(line, col, '|');
+			graph_line_addch(line, ' ');
 		}
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, line);
 
 	/*
 	 * Update graph->state
@@ -1049,7 +1054,7 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf
 		graph_update_state(graph, GRAPH_COLLAPSING);
 }
 
-static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_collapsing_line(struct git_graph *graph, struct graph_line *line)
 {
 	int i;
 	short used_horizontal = 0;
@@ -1159,9 +1164,9 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 	for (i = 0; i < graph->mapping_size; i++) {
 		int target = graph->new_mapping[i];
 		if (target < 0)
-			strbuf_addch(sb, ' ');
+			graph_line_addch(line, ' ');
 		else if (target * 2 == i)
-			strbuf_write_column(sb, &graph->new_columns[target], '|');
+			graph_line_write_column(line, &graph->new_columns[target], '|');
 		else if (target == horizontal_edge_target &&
 			 i != horizontal_edge - 1) {
 				/*
@@ -1172,16 +1177,16 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 				if (i != (target * 2)+3)
 					graph->new_mapping[i] = -1;
 				used_horizontal = 1;
-			strbuf_write_column(sb, &graph->new_columns[target], '_');
+			graph_line_write_column(line, &graph->new_columns[target], '_');
 		} else {
 			if (used_horizontal && i < horizontal_edge)
 				graph->new_mapping[i] = -1;
-			strbuf_write_column(sb, &graph->new_columns[target], '/');
+			graph_line_write_column(line, &graph->new_columns[target], '/');
 
 		}
 	}
 
-	graph_pad_horizontally(graph, sb, graph->mapping_size);
+	graph_pad_horizontally(graph, line);
 
 	/*
 	 * Swap mapping and new_mapping
@@ -1199,24 +1204,26 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 
 int graph_next_line(struct git_graph *graph, struct strbuf *sb)
 {
+	struct graph_line line = { .buf = sb, .width = 0 };
+
 	switch (graph->state) {
 	case GRAPH_PADDING:
-		graph_output_padding_line(graph, sb);
+		graph_output_padding_line(graph, &line);
 		return 0;
 	case GRAPH_SKIP:
-		graph_output_skip_line(graph, sb);
+		graph_output_skip_line(graph, &line);
 		return 0;
 	case GRAPH_PRE_COMMIT:
-		graph_output_pre_commit_line(graph, sb);
+		graph_output_pre_commit_line(graph, &line);
 		return 0;
 	case GRAPH_COMMIT:
-		graph_output_commit_line(graph, sb);
+		graph_output_commit_line(graph, &line);
 		return 1;
 	case GRAPH_POST_MERGE:
-		graph_output_post_merge_line(graph, sb);
+		graph_output_post_merge_line(graph, &line);
 		return 0;
 	case GRAPH_COLLAPSING:
-		graph_output_collapsing_line(graph, sb);
+		graph_output_collapsing_line(graph, &line);
 		return 0;
 	}
 
@@ -1227,7 +1234,7 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb)
 static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)
 {
 	int i;
-	int chars_written = 0;
+	struct graph_line line = { .buf = sb, .width = 0 };
 
 	if (graph->state != GRAPH_COMMIT) {
 		graph_next_line(graph, sb);
@@ -1244,20 +1251,17 @@ 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];
 
-		strbuf_write_column(sb, col, '|');
-		chars_written++;
+		graph_line_write_column(&line, col, '|');
 
 		if (col->commit == graph->commit && graph->num_parents > 2) {
 			int len = (graph->num_parents - 2) * 2;
-			strbuf_addchars(sb, ' ', len);
-			chars_written += len;
+			graph_line_addchars(&line, ' ', len);
 		} else {
-			strbuf_addch(sb, ' ');
-			chars_written++;
+			graph_line_addch(&line, ' ');
 		}
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, &line);
 
 	/*
 	 * Update graph->prev_state since we have output a padding line
Signature · 1 line
gitgitgadget
#44 lore raw

PATCH v2 2/13 graph: handle line padding in `graph_next_line()`

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

Now that the display width of graph lines is implicitly tracked via the graph_line interface, the calls to graph_pad_horizontally() no longer need to be located inside the individual output functions, where the character counting was previously being done.

All the functions called by graph_next_line() generate a line of output, then call graph_pad_horizontally(), and finally change the graph state if necessary. As padding is the final change to the output done by all these functions, it can be removed from all of them and done in graph_next_line() instead.

I've also moved the guard in graph_output_padding_line() that checks the graph has a commit; this function is only called by graph_next_line() and we must not pad the graph_line if no commit is set.

---
 graph.c | 49 ++++++++++++++++++++-----------------------------
 1 file changed, 20 insertions(+), 29 deletions(-)
graph.c +20 -29
index 2f81a5d23d..4c68557b17 100644
@@ -732,16 +732,6 @@ static void graph_output_padding_line(struct git_graph *graph,
 {
 	int i;
 
-	/*
-	 * We could conceivable be called with a NULL commit
-	 * if our caller has a bug, and invokes graph_next_line()
-	 * immediately after graph_init(), without first calling
-	 * graph_update().  Return without outputting anything in this
-	 * case.
-	 */
-	if (!graph->commit)
-		return;
-
 	/*
 	 * Output a padding row, that leaves all branch lines unchanged
 	 */
@@ -749,8 +739,6 @@ static void graph_output_padding_line(struct git_graph *graph,
 		graph_line_write_column(line, &graph->new_columns[i], '|');
 		graph_line_addch(line, ' ');
 	}
-
-	graph_pad_horizontally(graph, line);
 }
 
 
@@ -767,7 +755,6 @@ static void graph_output_skip_line(struct git_graph *graph, struct graph_line *l
 	 * of the graph is missing.
 	 */
 	graph_line_addstr(line, "...");
-	graph_pad_horizontally(graph, line);
 
 	if (graph->num_parents >= 3 &&
 	    graph->commit_index < (graph->num_columns - 1))
@@ -832,8 +819,6 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 		graph_line_addch(line, ' ');
 	}
 
-	graph_pad_horizontally(graph, line);
-
 	/*
 	 * Increment graph->expansion_row,
 	 * and move to state GRAPH_COMMIT if necessary
@@ -967,8 +952,6 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line
 		graph_line_addch(line, ' ');
 	}
 
-	graph_pad_horizontally(graph, line);
-
 	/*
 	 * Update graph->state
 	 */
@@ -1043,8 +1026,6 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l
 		}
 	}
 
-	graph_pad_horizontally(graph, line);
-
 	/*
 	 * Update graph->state
 	 */
@@ -1186,8 +1167,6 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
 		}
 	}
 
-	graph_pad_horizontally(graph, line);
-
 	/*
 	 * Swap mapping and new_mapping
 	 */
@@ -1204,31 +1183,43 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
 
 int graph_next_line(struct git_graph *graph, struct strbuf *sb)
 {
+	int shown_commit_line = 0;
 	struct graph_line line = { .buf = sb, .width = 0 };
 
+	/*
+	 * We could conceivable be called with a NULL commit
+	 * if our caller has a bug, and invokes graph_next_line()
+	 * immediately after graph_init(), without first calling
+	 * graph_update().  Return without outputting anything in this
+	 * case.
+	 */
+	if (!graph->commit)
+		return -1;
+
 	switch (graph->state) {
 	case GRAPH_PADDING:
 		graph_output_padding_line(graph, &line);
-		return 0;
+		break;
 	case GRAPH_SKIP:
 		graph_output_skip_line(graph, &line);
-		return 0;
+		break;
 	case GRAPH_PRE_COMMIT:
 		graph_output_pre_commit_line(graph, &line);
-		return 0;
+		break;
 	case GRAPH_COMMIT:
 		graph_output_commit_line(graph, &line);
-		return 1;
+		shown_commit_line = 1;
+		break;
 	case GRAPH_POST_MERGE:
 		graph_output_post_merge_line(graph, &line);
-		return 0;
+		break;
 	case GRAPH_COLLAPSING:
 		graph_output_collapsing_line(graph, &line);
-		return 0;
+		break;
 	}
 
-	assert(0);
-	return 0;
+	graph_pad_horizontally(graph, &line);
+	return shown_commit_line;
 }
 
 static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)
Signature · 1 line
gitgitgadget
#45 lore raw

PATCH v2 3/13 graph: reuse `find_new_column_by_commit()`

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

I will shortly be making some changes to graph_insert_into_new_columns() and so am trying to simplify it. One possible simplification is that we can extract the loop for finding the element in new_columns containing the given commit.

find_new_column_by_commit() contains a very similar loop but it returns a struct column * rather than an int offset into the array. Here I'm introducing a version that returns int and using that in graph_insert_into_new_columns() and graph_output_post_merge_line().

---
 graph.c | 48 +++++++++++++++++++++++-------------------------
 1 file changed, 23 insertions(+), 25 deletions(-)
graph.c +23 -25
index 4c68557b17..c9646d9e00 100644
@@ -460,22 +460,31 @@ static unsigned short graph_find_commit_color(const struct git_graph *graph,
 	return graph_get_current_column_color(graph);
 }
 
+static int graph_find_new_column_by_commit(struct git_graph *graph,
+					   struct commit *commit)
+{
+	int i;
+	for (i = 0; i < graph->num_new_columns; i++) {
+		if (graph->new_columns[i].commit == commit)
+			return i;
+	}
+	return -1;
+}
+
 static void graph_insert_into_new_columns(struct git_graph *graph,
 					  struct commit *commit,
 					  int *mapping_index)
 {
-	int i;
+	int i = graph_find_new_column_by_commit(graph, commit);
 
 	/*
 	 * If the commit is already in the new_columns list, we don't need to
 	 * add it.  Just update the mapping correctly.
 	 */
-	for (i = 0; i < graph->num_new_columns; i++) {
-		if (graph->new_columns[i].commit == commit) {
-			graph->mapping[*mapping_index] = i;
-			*mapping_index += 2;
-			return;
-		}
+	if (i >= 0) {
+		graph->mapping[*mapping_index] = i;
+		*mapping_index += 2;
+		return;
 	}
 
 	/*
@@ -963,17 +972,6 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line
 		graph_update_state(graph, GRAPH_COLLAPSING);
 }
 
-static struct column *find_new_column_by_commit(struct git_graph *graph,
-						struct commit *commit)
-{
-	int i;
-	for (i = 0; i < graph->num_new_columns; i++) {
-		if (graph->new_columns[i].commit == commit)
-			return &graph->new_columns[i];
-	}
-	return NULL;
-}
-
 static void graph_output_post_merge_line(struct git_graph *graph, struct graph_line *line)
 {
 	int seen_this = 0;
@@ -1001,20 +999,20 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l
 			 * edges.
 			 */
 			struct commit_list *parents = NULL;
-			struct column *par_column;
+			int par_column;
 			seen_this = 1;
 			parents = first_interesting_parent(graph);
 			assert(parents);
-			par_column = find_new_column_by_commit(graph, parents->item);
-			assert(par_column);
+			par_column = graph_find_new_column_by_commit(graph, parents->item);
+			assert(par_column >= 0);
 
-			graph_line_write_column(line, par_column, '|');
+			graph_line_write_column(line, &graph->new_columns[par_column], '|');
 			for (j = 0; j < graph->num_parents - 1; j++) {
 				parents = next_interesting_parent(graph, parents);
 				assert(parents);
-				par_column = find_new_column_by_commit(graph, parents->item);
-				assert(par_column);
-				graph_line_write_column(line, par_column, '\\');
+				par_column = graph_find_new_column_by_commit(graph, parents->item);
+				assert(par_column >= 0);
+				graph_line_write_column(line, &graph->new_columns[par_column], '\\');
 				graph_line_addch(line, ' ');
 			}
 		} else if (seen_this) {
Signature · 1 line
gitgitgadget
#46 lore raw

PATCH v2 4/13 graph: reduce duplication in `graph_insert_into_new_columns()`

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

I will shortly be making some changes to this function and so am trying to simplify it. It currently contains some duplicated logic; both branches the function can take assign the commit's column index into the mapping array and increment mapping_index.

Here I change the function so that the only conditional behaviour is that it appends the commit to new_columns if it's not present. All manipulation of mapping now happens on a single code path.

---
 graph.c | 20 +++++++-------------
 1 file changed, 7 insertions(+), 13 deletions(-)
graph.c +7 -13
index c9646d9e00..512ae16535 100644
@@ -478,23 +478,17 @@ static void graph_insert_into_new_columns(struct git_graph *graph,
 	int i = graph_find_new_column_by_commit(graph, commit);
 
 	/*
-	 * If the commit is already in the new_columns list, we don't need to
-	 * add it.  Just update the mapping correctly.
+	 * If the commit is not already in the new_columns array, then add it
+	 * and record it as being in the final column.
 	 */
-	if (i >= 0) {
-		graph->mapping[*mapping_index] = i;
-		*mapping_index += 2;
-		return;
+	if (i < 0) {
+		i = graph->num_new_columns++;
+		graph->new_columns[i].commit = commit;
+		graph->new_columns[i].color = graph_find_commit_color(graph, commit);
 	}
 
-	/*
-	 * This commit isn't already in new_columns.  Add it.
-	 */
-	graph->new_columns[graph->num_new_columns].commit = commit;
-	graph->new_columns[graph->num_new_columns].color = graph_find_commit_color(graph, commit);
-	graph->mapping[*mapping_index] = graph->num_new_columns;
+	graph->mapping[*mapping_index] = i;
 	*mapping_index += 2;
-	graph->num_new_columns++;
 }
 
 static void graph_update_width(struct git_graph *graph,
Signature · 1 line
gitgitgadget
#47 lore raw

PATCH v2 5/13 graph: remove `mapping_idx` and `graph_update_width()`

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

There's a duplication of logic between graph_insert_into_new_columns() and graph_update_width(). graph_insert_into_new_columns() is called repeatedly by graph_update_columns() with an int * that tracks the offset into the mapping array where we should write the next value. Each call to graph_insert_into_new_columns() effectively pushes one column index and one "null" value (-1) onto the mapping array and therefore increments mapping_idx by 2.

graph_update_width() duplicates this process: the width of the graph is essentially the initial width of the mapping array before edges begin collapsing. The graph_update_width() function's logic effectively works out how many times graph_insert_into_new_columns() was called based on the relationship of the current commit to the rest of the graph.

I'm about to make some changes that make the assignment of values into the mapping array more complicated. Rather than make graph_update_width() more complicated at the same time, we can simply remove this function and use graph->width to track the offset into the mapping array as we're building it. This removes the duplication and makes sure that graph->width is the same as the visual width of the mapping array once graph_update_columns() is complete.

---
 graph.c | 65 +++++++++------------------------------------------------
 1 file changed, 10 insertions(+), 55 deletions(-)
graph.c +10 -55
index 512ae16535..d724ef25c3 100644
@@ -472,8 +472,7 @@ static int graph_find_new_column_by_commit(struct git_graph *graph,
 }
 
 static void graph_insert_into_new_columns(struct git_graph *graph,
-					  struct commit *commit,
-					  int *mapping_index)
+					  struct commit *commit)
 {
 	int i = graph_find_new_column_by_commit(graph, commit);
 
@@ -487,50 +486,14 @@ static void graph_insert_into_new_columns(struct git_graph *graph,
 		graph->new_columns[i].color = graph_find_commit_color(graph, commit);
 	}
 
-	graph->mapping[*mapping_index] = i;
-	*mapping_index += 2;
-}
-
-static void graph_update_width(struct git_graph *graph,
-			       int is_commit_in_existing_columns)
-{
-	/*
-	 * Compute the width needed to display the graph for this commit.
-	 * This is the maximum width needed for any row.  All other rows
-	 * will be padded to this width.
-	 *
-	 * Compute the number of columns in the widest row:
-	 * Count each existing column (graph->num_columns), and each new
-	 * column added by this commit.
-	 */
-	int max_cols = graph->num_columns + graph->num_parents;
-
-	/*
-	 * Even if the current commit has no parents to be printed, it
-	 * still takes up a column for itself.
-	 */
-	if (graph->num_parents < 1)
-		max_cols++;
-
-	/*
-	 * We added a column for the current commit as part of
-	 * graph->num_parents.  If the current commit was already in
-	 * graph->columns, then we have double counted it.
-	 */
-	if (is_commit_in_existing_columns)
-		max_cols--;
-
-	/*
-	 * Each column takes up 2 spaces
-	 */
-	graph->width = max_cols * 2;
+	graph->mapping[graph->width] = i;
+	graph->width += 2;
 }
 
 static void graph_update_columns(struct git_graph *graph)
 {
 	struct commit_list *parent;
 	int max_new_columns;
-	int mapping_idx;
 	int i, seen_this, is_commit_in_columns;
 
 	/*
@@ -563,6 +526,8 @@ static void graph_update_columns(struct git_graph *graph)
 	for (i = 0; i < graph->mapping_size; i++)
 		graph->mapping[i] = -1;
 
+	graph->width = 0;
+
 	/*
 	 * Populate graph->new_columns and graph->mapping
 	 *
@@ -573,7 +538,6 @@ static void graph_update_columns(struct git_graph *graph)
 	 * supposed to end up after the collapsing is performed.
 	 */
 	seen_this = 0;
-	mapping_idx = 0;
 	is_commit_in_columns = 1;
 	for (i = 0; i <= graph->num_columns; i++) {
 		struct commit *col_commit;
@@ -587,7 +551,6 @@ static void graph_update_columns(struct git_graph *graph)
 		}
 
 		if (col_commit == graph->commit) {
-			int old_mapping_idx = mapping_idx;
 			seen_this = 1;
 			graph->commit_index = i;
 			for (parent = first_interesting_parent(graph);
@@ -602,21 +565,18 @@ static void graph_update_columns(struct git_graph *graph)
 				    !is_commit_in_columns) {
 					graph_increment_column_color(graph);
 				}
-				graph_insert_into_new_columns(graph,
-							      parent->item,
-							      &mapping_idx);
+				graph_insert_into_new_columns(graph, parent->item);
 			}
 			/*
-			 * We always need to increment mapping_idx by at
+			 * We always need to increment graph->width by at
 			 * least 2, even if it has no interesting parents.
 			 * The current commit always takes up at least 2
 			 * spaces.
 			 */
-			if (mapping_idx == old_mapping_idx)
-				mapping_idx += 2;
+			if (graph->num_parents == 0)
+				graph->width += 2;
 		} else {
-			graph_insert_into_new_columns(graph, col_commit,
-						      &mapping_idx);
+			graph_insert_into_new_columns(graph, col_commit);
 		}
 	}
 
@@ -626,11 +586,6 @@ static void graph_update_columns(struct git_graph *graph)
 	while (graph->mapping_size > 1 &&
 	       graph->mapping[graph->mapping_size - 1] < 0)
 		graph->mapping_size--;
-
-	/*
-	 * Compute graph->width for this commit
-	 */
-	graph_update_width(graph, is_commit_in_columns);
 }
 
 void graph_update(struct git_graph *graph, struct commit *commit)
Signature · 1 line
gitgitgadget
#48 lore raw

PATCH v2 6/13 graph: extract logic for moving to GRAPH_PRE_COMMIT state

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

This computation is repeated in a couple of places and I need to add another condition to it to implement a further improvement to the graph rendering, so I'm extracting this into a function.

---
 graph.c | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)
graph.c +8 -4
index d724ef25c3..bd7403065e 100644
@@ -588,6 +588,12 @@ static void graph_update_columns(struct git_graph *graph)
 		graph->mapping_size--;
 }
 
+static int graph_needs_pre_commit_line(struct git_graph *graph)
+{
+	return graph->num_parents >= 3 &&
+	       graph->commit_index < (graph->num_columns - 1);
+}
+
 void graph_update(struct git_graph *graph, struct commit *commit)
 {
 	struct commit_list *parent;
@@ -643,8 +649,7 @@ void graph_update(struct git_graph *graph, struct commit *commit)
 	 */
 	if (graph->state != GRAPH_PADDING)
 		graph->state = GRAPH_SKIP;
-	else if (graph->num_parents >= 3 &&
-		 graph->commit_index < (graph->num_columns - 1))
+	else if (graph_needs_pre_commit_line(graph))
 		graph->state = GRAPH_PRE_COMMIT;
 	else
 		graph->state = GRAPH_COMMIT;
@@ -714,8 +719,7 @@ static void graph_output_skip_line(struct git_graph *graph, struct graph_line *l
 	 */
 	graph_line_addstr(line, "...");
 
-	if (graph->num_parents >= 3 &&
-	    graph->commit_index < (graph->num_columns - 1))
+	if (graph_needs_pre_commit_line(graph))
 		graph_update_state(graph, GRAPH_PRE_COMMIT);
 	else
 		graph_update_state(graph, GRAPH_COMMIT);
Signature · 1 line
gitgitgadget
#49 lore raw

PATCH v2 7/13 graph: example of graph output that can be simplified

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

The commits following this one introduce a series of improvements to the layout of graphs, tidying up a few edge cases, namely:

- merge whose first parent fuses with an existing column to the left
- merge whose last parent fuses with its immediate neighbor on the right
- edges that collapse to the left above and below a commit line

This test case exemplifies these cases and provides a motivating example of the kind of history I'm aiming to clear up.

The first parent of merge E is the same as the parent of H, so those edges fuse together.

        * H
        |
        | *-.   E
        | |\ \
        |/ / /
        |
        * B

We can "skew" the display of this merge so that it doesn't introduce additional columns that immediately collapse:

        * H
        |
        | *   E
        |/|\
        |
        * B

The last parent of E is D, the same as the parent of F which is the edge to the right of the merge.

            * F
            |
             \
          *-. \   E
          |\ \ \
         / / / /
            | /
            |/
            * D

The two edges leading to D could be fused sooner: rather than expanding the F edge around the merge and then letting the edges collapse, the F edge could fuse with the E edge in the post-merge line:

            * F
            |
             \
          *-. | E
          |\ \|
         / / /
            |
            * D

If this is combined with the "skew" effect above, we get a much cleaner graph display for these edges:

* F
|
* | E
/|\|
|
* D

Finally, the edge leading from C to A appears jagged as it passes through the commit line for B:

        | * | C
        | |/
        * | B
        |/

* A

This can be smoothed out so that such edges are easier to read:

        | * | C
        | |/
        * / B
        |/
        * A
---
 t/t4215-log-skewed-merges.sh | 43 ++++++++++++++++++++++++++++++++++++
 1 file changed, 43 insertions(+)
 create mode 100755 t/t4215-log-skewed-merges.sh
t/t4215-log-skewed-merges.sh new file +43 -0
new file mode 100755
index 0000000000..4582ba066a
@@ -0,0 +1,43 @@
+#!/bin/sh
+
+test_description='git log --graph of skewed merges'
+
+. ./test-lib.sh
+
+test_expect_success 'log --graph with merge fusing with its left and right neighbors' '
+	cat >expect <<-\EOF &&
+	*   H
+	|\
+	| *   G
+	| |\
+	| | * F
+	| | |
+	| |  \
+	| *-. \   E
+	| |\ \ \
+	|/ / / /
+	| | | /
+	| | |/
+	| | * D
+	| * | C
+	| |/
+	* | B
+	|/
+	* A
+	EOF
+
+	git checkout --orphan _p &&
+	test_commit A &&
+	test_commit B &&
+	git checkout -b _q @^ && test_commit C &&
+	git checkout -b _r @^ && test_commit D &&
+	git checkout _p && git merge --no-ff _q _r -m E &&
+	git checkout _r && test_commit F &&
+	git checkout _p && git merge --no-ff _r -m G &&
+	git checkout @^^ && git merge --no-ff _p -m H &&
+
+	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
+	test_cmp expect actual
+'
+
+test_done
Signature · 1 line
gitgitgadget
#50 lore raw

PATCH v2 8/13 graph: tidy up display of left-skewed merges

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

Currently, when we display a merge whose first parent is already present in a column to the left of the merge commit, we display the first parent as a vertical pipe | in the GRAPH_POST_MERGE line and then immediately enter the GRAPH_COLLAPSING state. The first-parent line tracks to the left and all the other parent lines follow it; this creates a "kink" in those lines:

        | *---.
        | |\ \ \
        |/ / / /
        | | | *

This change tidies the display of such commits such that if the first parent appears to the left of the merge, we render it as a / and the second parent as a |. This reduces the horizontal and vertical space needed to render the merge, and makes the resulting lines easier to read.

        | *-.
        |/|\ \
        | | | *

If the first parent is separated from the merge by several columns, a horizontal line is drawn in a similar manner to how the GRAPH_COLLAPSING state displays the line.

        | | | *-.
        | |_|/|\ \
        |/| | | | *

This effect is applied to both "normal" two-parent merges, and to octopus merges. It also reduces the vertical space needed for pre-commit lines, as the merge occupies one less column than usual.

        Before:         After:

        | *             | *
        | |\            | |\
        | | \           | * \
        | |  \          |/|\ \
        | *-. \
        | |\ \ \
---
 graph.c                      | 125 +++++++++++++++++++++++++++--------
 t/t4214-log-graph-octopus.sh |  20 +++---
 t/t4215-log-skewed-merges.sh |  45 +++++++++++--
 3 files changed, 144 insertions(+), 46 deletions(-)
graph.c +97 -28
index bd7403065e..e37127f5ab 100644
@@ -202,6 +202,20 @@ struct git_graph {
 	 * previous commit.
 	 */
 	int prev_commit_index;
+	/*
+	 * Which layout variant to use to display merge commits. If the
+	 * commit's first parent is known to be in a column to the left of the
+	 * merge, then this value is 0 and we use the layout on the left.
+	 * Otherwise, the value is 1 and the layout on the right is used. This
+	 * field tells us how many columns the first parent occupies.
+	 *
+	 * 		0)			1)
+	 *
+	 * 		| | | *-.		| | *---.
+	 * 		| |_|/|\ \		| | |\ \ \
+	 * 		|/| | | | |		| | | | | *
+	 */
+	int merge_layout;
 	/*
 	 * The maximum number of columns that can be stored in the columns
 	 * and new_columns arrays.  This is also half the number of entries
@@ -313,6 +327,7 @@ struct git_graph *graph_init(struct rev_info *opt)
 	graph->prev_state = GRAPH_PADDING;
 	graph->commit_index = 0;
 	graph->prev_commit_index = 0;
+	graph->merge_layout = 0;
 	graph->num_columns = 0;
 	graph->num_new_columns = 0;
 	graph->mapping_size = 0;
@@ -472,9 +487,11 @@ static int graph_find_new_column_by_commit(struct git_graph *graph,
 }
 
 static void graph_insert_into_new_columns(struct git_graph *graph,
-					  struct commit *commit)
+					  struct commit *commit,
+					  int idx)
 {
 	int i = graph_find_new_column_by_commit(graph, commit);
+	int mapping_idx;
 
 	/*
 	 * If the commit is not already in the new_columns array, then add it
@@ -486,8 +503,26 @@ static void graph_insert_into_new_columns(struct git_graph *graph,
 		graph->new_columns[i].color = graph_find_commit_color(graph, commit);
 	}
 
-	graph->mapping[graph->width] = i;
-	graph->width += 2;
+	if (graph->num_parents > 1 && idx > -1 && graph->merge_layout == -1) {
+		/*
+		 * If this is the first parent of a merge, choose a layout for
+		 * the merge line based on whether the parent appears in a
+		 * column to the left of the merge
+		 */
+		int dist, shift;
+
+		dist = idx - i;
+		shift = (dist > 1) ? 2 * dist - 3 : 1;
+
+		graph->merge_layout = (dist > 0) ? 0 : 1;
+		mapping_idx = graph->width + (graph->merge_layout - 1) * shift;
+		graph->width += 2 * graph->merge_layout;
+	} else {
+		mapping_idx = graph->width;
+		graph->width += 2;
+	}
+
+	graph->mapping[mapping_idx] = i;
 }
 
 static void graph_update_columns(struct git_graph *graph)
@@ -553,6 +588,7 @@ static void graph_update_columns(struct git_graph *graph)
 		if (col_commit == graph->commit) {
 			seen_this = 1;
 			graph->commit_index = i;
+			graph->merge_layout = -1;
 			for (parent = first_interesting_parent(graph);
 			     parent;
 			     parent = next_interesting_parent(graph, parent)) {
@@ -565,7 +601,7 @@ static void graph_update_columns(struct git_graph *graph)
 				    !is_commit_in_columns) {
 					graph_increment_column_color(graph);
 				}
-				graph_insert_into_new_columns(graph, parent->item);
+				graph_insert_into_new_columns(graph, parent->item, i);
 			}
 			/*
 			 * We always need to increment graph->width by at
@@ -576,7 +612,7 @@ static void graph_update_columns(struct git_graph *graph)
 			if (graph->num_parents == 0)
 				graph->width += 2;
 		} else {
-			graph_insert_into_new_columns(graph, col_commit);
+			graph_insert_into_new_columns(graph, col_commit, -1);
 		}
 	}
 
@@ -588,10 +624,36 @@ static void graph_update_columns(struct git_graph *graph)
 		graph->mapping_size--;
 }
 
+static int graph_num_expansion_rows(struct git_graph *graph)
+{
+	/*
+	 * Normally, we need two expansion rows for each dashed parent line from
+	 * an octopus merge:
+	 *
+	 * 		| *
+	 * 		| |\
+	 * 		| | \
+	 * 		| |  \
+	 * 		| *-. \
+	 * 		| |\ \ \
+	 *
+	 * If the merge is skewed to the left, then its parents occupy one less
+	 * column, and we don't need as many expansion rows to route around it;
+	 * in some cases that means we don't need any expansion rows at all:
+	 *
+	 * 		| *
+	 * 		| |\
+	 * 		| * \
+	 * 		|/|\ \
+	 */
+	return (graph->num_parents + graph->merge_layout - 3) * 2;
+}
+
 static int graph_needs_pre_commit_line(struct git_graph *graph)
 {
 	return graph->num_parents >= 3 &&
-	       graph->commit_index < (graph->num_columns - 1);
+	       graph->commit_index < (graph->num_columns - 1) &&
+	       graph->expansion_row < graph_num_expansion_rows(graph);
 }
 
 void graph_update(struct git_graph *graph, struct commit *commit)
@@ -728,7 +790,6 @@ static void graph_output_skip_line(struct git_graph *graph, struct graph_line *l
 static void graph_output_pre_commit_line(struct git_graph *graph,
 					 struct graph_line *line)
 {
-	int num_expansion_rows;
 	int i, seen_this;
 
 	/*
@@ -739,14 +800,13 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 	 * We need 2 extra rows for every parent over 2.
 	 */
 	assert(graph->num_parents >= 3);
-	num_expansion_rows = (graph->num_parents - 2) * 2;
 
 	/*
 	 * graph->expansion_row tracks the current expansion row we are on.
 	 * It should be in the range [0, num_expansion_rows - 1]
 	 */
 	assert(0 <= graph->expansion_row &&
-	       graph->expansion_row < num_expansion_rows);
+	       graph->expansion_row < graph_num_expansion_rows(graph));
 
 	/*
 	 * Output the row
@@ -786,7 +846,7 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 	 * and move to state GRAPH_COMMIT if necessary
 	 */
 	graph->expansion_row++;
-	if (graph->expansion_row >= num_expansion_rows)
+	if (!graph_needs_pre_commit_line(graph))
 		graph_update_state(graph, GRAPH_COMMIT);
 }
 
@@ -824,7 +884,7 @@ static void graph_draw_octopus_merge(struct git_graph *graph, struct graph_line
 	 * x 0 1 2 3
 	 *
 	 */
-	const int dashless_parents = 2;
+	const int dashless_parents = 3 - graph->merge_layout;
 	int dashful_parents = graph->num_parents - dashless_parents;
 
 	/*
@@ -832,9 +892,9 @@ static void graph_draw_octopus_merge(struct git_graph *graph, struct graph_line
 	 * above) but sometimes the first parent goes into an existing column,
 	 * like this:
 	 *
-	 * | *---.
-	 * | |\ \ \
-	 * |/ / / /
+	 * | *-.
+	 * |/|\ \
+	 * | | | |
 	 * x 0 1 2
 	 *
 	 * In which case the number of parents will be one greater than the
@@ -925,10 +985,15 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line
 		graph_update_state(graph, GRAPH_COLLAPSING);
 }
 
+const char merge_chars[] = {'/', '|', '\\'};
+
 static void graph_output_post_merge_line(struct git_graph *graph, struct graph_line *line)
 {
 	int seen_this = 0;
-	int i, j;
+	int i;
+
+	struct commit_list *first_parent = first_interesting_parent(graph);
+	int seen_parent = 0;
 
 	/*
 	 * Output the post-merge row
@@ -951,30 +1016,34 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l
 			 * new_columns and use those to format the
 			 * edges.
 			 */
-			struct commit_list *parents = NULL;
+			struct commit_list *parents = first_parent;
 			int par_column;
+			int idx = graph->merge_layout;
+			char c;
 			seen_this = 1;
-			parents = first_interesting_parent(graph);
-			assert(parents);
-			par_column = graph_find_new_column_by_commit(graph, parents->item);
-			assert(par_column >= 0);
-
-			graph_line_write_column(line, &graph->new_columns[par_column], '|');
-			for (j = 0; j < graph->num_parents - 1; j++) {
-				parents = next_interesting_parent(graph, parents);
-				assert(parents);
+
+			for (; parents; parents = next_interesting_parent(graph, parents)) {
 				par_column = graph_find_new_column_by_commit(graph, parents->item);
 				assert(par_column >= 0);
-				graph_line_write_column(line, &graph->new_columns[par_column], '\\');
-				graph_line_addch(line, ' ');
+
+				c = merge_chars[idx];
+				graph_line_write_column(line, &graph->new_columns[par_column], c);
+				if (idx == 2)
+					graph_line_addch(line, ' ');
+				else
+					idx++;
 			}
 		} else if (seen_this) {
 			graph_line_write_column(line, col, '\\');
 			graph_line_addch(line, ' ');
 		} else {
 			graph_line_write_column(line, col, '|');
-			graph_line_addch(line, ' ');
+			if (graph->merge_layout != 0 || i != graph->commit_index - 1)
+				graph_line_addch(line, seen_parent ? '_' : ' ');
 		}
+
+		if (col_commit == first_parent->item)
+			seen_parent = 1;
 	}
 
 	/*
t/t4214-log-graph-octopus.sh +8 -12
index 3ae8e51e50..1b96276894 100755
@@ -26,9 +26,8 @@ test_expect_success 'set up merge history' '
 test_expect_success 'log --graph with tricky octopus merge, no color' '
 	cat >expect.uncolored <<-\EOF &&
 	* left
-	| *---.   octopus-merge
-	| |\ \ \
-	|/ / / /
+	| *-.   octopus-merge
+	|/|\ \
 	| | | * 4
 	| | * | 3
 	| | |/
@@ -47,9 +46,8 @@ test_expect_success 'log --graph with tricky octopus merge with colors' '
 	test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
 	cat >expect.colors <<-\EOF &&
 	* left
-	<RED>|<RESET> *<BLUE>-<RESET><BLUE>-<RESET><MAGENTA>-<RESET><MAGENTA>.<RESET>   octopus-merge
-	<RED>|<RESET> <RED>|<RESET><YELLOW>\<RESET> <BLUE>\<RESET> <MAGENTA>\<RESET>
-	<RED>|<RESET><RED>/<RESET> <YELLOW>/<RESET> <BLUE>/<RESET> <MAGENTA>/<RESET>
+	<RED>|<RESET> *<MAGENTA>-<RESET><MAGENTA>.<RESET>   octopus-merge
+	<RED>|<RESET><RED>/<RESET><YELLOW>|<RESET><BLUE>\<RESET> <MAGENTA>\<RESET>
 	<RED>|<RESET> <YELLOW>|<RESET> <BLUE>|<RESET> * 4
 	<RED>|<RESET> <YELLOW>|<RESET> * <MAGENTA>|<RESET> 3
 	<RED>|<RESET> <YELLOW>|<RESET> <MAGENTA>|<RESET><MAGENTA>/<RESET>
@@ -147,9 +145,8 @@ test_expect_success 'log --graph with tricky octopus merge and its child, no col
 	cat >expect.uncolored <<-\EOF &&
 	* left
 	| * after-merge
-	| *---.   octopus-merge
-	| |\ \ \
-	|/ / / /
+	| *-.   octopus-merge
+	|/|\ \
 	| | | * 4
 	| | * | 3
 	| | |/
@@ -169,9 +166,8 @@ test_expect_failure 'log --graph with tricky octopus merge and its child with co
 	cat >expect.colors <<-\EOF &&
 	* left
 	<RED>|<RESET> * after-merge
-	<RED>|<RESET> *<MAGENTA>-<RESET><MAGENTA>-<RESET><CYAN>-<RESET><CYAN>.<RESET>   octopus-merge
-	<RED>|<RESET> <RED>|<RESET><BLUE>\<RESET> <MAGENTA>\<RESET> <CYAN>\<RESET>
-	<RED>|<RESET><RED>/<RESET> <BLUE>/<RESET> <MAGENTA>/<RESET> <CYAN>/<RESET>
+	<RED>|<RESET> *<CYAN>-<RESET><CYAN>.<RESET>   octopus-merge
+	<RED>|<RESET><RED>/<RESET><BLUE>|<RESET><MAGENTA>\<RESET> <CYAN>\<RESET>
 	<RED>|<RESET> <BLUE>|<RESET> <MAGENTA>|<RESET> * 4
 	<RED>|<RESET> <BLUE>|<RESET> * <CYAN>|<RESET> 3
 	<RED>|<RESET> <BLUE>|<RESET> <CYAN>|<RESET><CYAN>/<RESET>
t/t4215-log-skewed-merges.sh +39 -6

55 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#51 lore raw

PATCH v2 9/13 graph: commit and post-merge lines for left-skewed merges

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

Following the introduction of "left-skewed" merges, which are merges whose first parent fuses with another edge to its left, we have some more edge cases to deal with in the display of commit and post-merge lines.

The current graph code handles the following cases for edges appearing to the right of the commit (*) on commit lines. A 2-way merge is usually followed by vertical lines:

        | | |
        | * |
        | |\ \

An octopus merge (more than two parents) is always followed by edges sloping to the right:

        | |  \          | |    \
        | *-. \         | *---. \
        | |\ \ \        | |\ \ \ \

A 2-way merge is followed by a right-sloping edge if the commit line immediately follows a post-merge line for a commit that appears in the same column as the current commit, or any column to the left of that:

        | *             | * |
        | |\            | |\ \
        | * \           | | * \
        | |\ \          | | |\ \

This commit introduces the following new cases for commit lines. If a 2-way merge skews to the left, then the edges to its right are always vertical lines, even if the commit follows a post-merge line:

        | | |           | |\
        | * |           | * |
        |/| |           |/| |

A commit with 3 parents that skews left is followed by vertical edges:

        | | |
        | * |
        |/|\ \

If a 3-way left-skewed merge commit appears immediately after a post-merge line, then it may be followed the right-sloping edges, just like a 2-way merge that is not skewed.

        | |\
        | * \
        |/|\ \

Octopus merges with 4 or more parents that skew to the left will always be followed by right-sloping edges, because the existing columns need to expand around the merge.

        | |  \
        | *-. \
        |/|\ \ \

On post-merge lines, usually all edges following the current commit slope to the right:

        | * | |
        | |\ \ \

However, if the commit is a left-skewed 2-way merge, the edges to its right remain vertical. We also need to display a space after the vertical line descending from the commit marker, whereas this line would normally be followed by a backslash.

        | * | |
        |/| | |

If a left-skewed merge has more than 2 parents, then the edges to its right are still sloped as they bend around the edges introduced by the merge.

        | * | |
        |/|\ \ \

To handle these new cases, we need to know not just how many parents each commit has, but how many new columns it adds to the display; this quantity is recorded in the edges_added field for the current commit, and prev_edges_added field for the previous commit.

Here, "column" refers to visual columns, not the logical columns of the columns array. This is because even if all the commit's parents end up fusing with existing edges, they initially introduce distinct edges in the commit and post-merge lines before those edges collapse. For example, a 3-way merge whose 2nd and 3rd parents fuse with existing edges still introduces 2 visual columns that affect the display of edges to their right.

        | | |  \
        | | *-. \
        | | |\ \ \
        | |_|/ / /
        |/| | / /
        | | |/ /
        | |/| |
        | | | |

This merge does not introduce any *logical* columns; there are 4 edges before and after this commit once all edges have collapsed. But it does initially introduce 2 new edges that need to be accommodated by the edges to their right.

---
 graph.c                      |  63 +++++++++++++--
 t/t4215-log-skewed-merges.sh | 147 ++++++++++++++++++++++++++++++++++-
 2 files changed, 203 insertions(+), 7 deletions(-)
graph.c +58 -5
index e37127f5ab..21edad8085 100644
@@ -216,6 +216,46 @@ struct git_graph {
 	 * 		|/| | | | |		| | | | | *
 	 */
 	int merge_layout;
+	/*
+	 * The number of columns added to the graph by the current commit. For
+	 * 2-way and octopus merges, this is is usually one less than the
+	 * number of parents:
+	 *
+	 * 		| | |			| |    \
+	 *		| * |			| *---. \
+	 *		| |\ \			| |\ \ \ \
+	 *		| | | |         	| | | | | |
+	 *
+	 *		num_parents: 2		num_parents: 4
+	 *		edges_added: 1		edges_added: 3
+	 *
+	 * For left-skewed merges, the first parent fuses with its neighbor and
+	 * so one less column is added:
+	 *
+	 *		| | |			| |  \
+	 *		| * |			| *-. \
+	 *		|/| |			|/|\ \ \
+	 *		| | |			| | | | |
+	 *
+	 *		num_parents: 2		num_parents: 4
+	 *		edges_added: 0		edges_added: 2
+	 *
+	 * This number determines how edges to the right of the merge are
+	 * displayed in commit and post-merge lines; if no columns have been
+	 * added then a vertical line should be used where a right-tracking
+	 * line would otherwise be used.
+	 *
+	 *		| * \			| * |
+	 *		| |\ \			|/| |
+	 *		| | * \			| * |
+	 */
+	int edges_added;
+	/*
+	 * The number of columns added by the previous commit, which is used to
+	 * smooth edges appearing to the right of a commit in a commit line
+	 * following a post-merge line.
+	 */
+	int prev_edges_added;
 	/*
 	 * The maximum number of columns that can be stored in the columns
 	 * and new_columns arrays.  This is also half the number of entries
@@ -328,6 +368,8 @@ struct git_graph *graph_init(struct rev_info *opt)
 	graph->commit_index = 0;
 	graph->prev_commit_index = 0;
 	graph->merge_layout = 0;
+	graph->edges_added = 0;
+	graph->prev_edges_added = 0;
 	graph->num_columns = 0;
 	graph->num_new_columns = 0;
 	graph->mapping_size = 0;
@@ -689,6 +731,9 @@ void graph_update(struct git_graph *graph, struct commit *commit)
 	 */
 	graph_update_columns(graph);
 
+	graph->prev_edges_added = graph->edges_added;
+	graph->edges_added = graph->num_parents + graph->merge_layout - 2;
+
 	graph->expansion_row = 0;
 
 	/*
@@ -947,12 +992,13 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line
 
 			if (graph->num_parents > 2)
 				graph_draw_octopus_merge(graph, line);
-		} else if (seen_this && (graph->num_parents > 2)) {
+		} else if (seen_this && (graph->edges_added > 1)) {
 			graph_line_write_column(line, col, '\\');
-		} else if (seen_this && (graph->num_parents == 2)) {
+		} else if (seen_this && (graph->edges_added == 1)) {
 			/*
-			 * This is a 2-way merge commit.
-			 * There is no GRAPH_PRE_COMMIT stage for 2-way
+			 * This is either a right-skewed 2-way merge
+			 * commit, or a left-skewed 3-way merge.
+			 * There is no GRAPH_PRE_COMMIT stage for such
 			 * merges, so this is the first line of output
 			 * for this commit.  Check to see what the previous
 			 * line of output was.
@@ -964,6 +1010,7 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line
 			 * makes the output look nicer.
 			 */
 			if (graph->prev_state == GRAPH_POST_MERGE &&
+			    graph->prev_edges_added > 0 &&
 			    graph->prev_commit_index < i)
 				graph_line_write_column(line, col, '\\');
 			else
@@ -1033,8 +1080,14 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l
 				else
 					idx++;
 			}
+			if (graph->edges_added == 0)
+				graph_line_addch(line, ' ');
+
 		} else if (seen_this) {
-			graph_line_write_column(line, col, '\\');
+			if (graph->edges_added > 0)
+				graph_line_write_column(line, col, '\\');
+			else
+				graph_line_write_column(line, col, '|');
 			graph_line_addch(line, ' ');
 		} else {
 			graph_line_write_column(line, col, '|');
t/t4215-log-skewed-merges.sh +145 -2

163 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#52 lore raw

PATCH v2 10/13 graph: rename `new_mapping` to `old_mapping`

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

The change I'm about to make requires being able to inspect the mapping array that was used to render the last GRAPH_COLLAPSING line while rendering a GRAPH_COMMIT line. The new_mapping array currently exists as a pre-allocated space for computing the next mapping array during graph_output_collapsing_line(), but we can repurpose it to let us see the previous mapping state.

To support this use it will make more sense if this array is named old_mapping, as it will contain the mapping data for the previous line we rendered, at the point we're rendering a commit line.

---
 graph.c | 54 +++++++++++++++++++++++++++---------------------------
 1 file changed, 27 insertions(+), 27 deletions(-)
graph.c +27 -27
index 21edad8085..2315f3604d 100644
@@ -259,7 +259,7 @@ struct git_graph {
 	/*
 	 * The maximum number of columns that can be stored in the columns
 	 * and new_columns arrays.  This is also half the number of entries
-	 * that can be stored in the mapping and new_mapping arrays.
+	 * that can be stored in the mapping and old_mapping arrays.
 	 */
 	int column_capacity;
 	/*
@@ -302,7 +302,7 @@ struct git_graph {
 	 * of the git_graph simply so we don't have to allocate a new
 	 * temporary array each time we have to output a collapsing line.
 	 */
-	int *new_mapping;
+	int *old_mapping;
 	/*
 	 * The current default column color being used.  This is
 	 * stored as an index into the array column_colors.
@@ -388,7 +388,7 @@ struct git_graph *graph_init(struct rev_info *opt)
 	ALLOC_ARRAY(graph->columns, graph->column_capacity);
 	ALLOC_ARRAY(graph->new_columns, graph->column_capacity);
 	ALLOC_ARRAY(graph->mapping, 2 * graph->column_capacity);
-	ALLOC_ARRAY(graph->new_mapping, 2 * graph->column_capacity);
+	ALLOC_ARRAY(graph->old_mapping, 2 * graph->column_capacity);
 
 	/*
 	 * The diff output prefix callback, with this we can make
@@ -418,7 +418,7 @@ static void graph_ensure_capacity(struct git_graph *graph, int num_columns)
 	REALLOC_ARRAY(graph->columns, graph->column_capacity);
 	REALLOC_ARRAY(graph->new_columns, graph->column_capacity);
 	REALLOC_ARRAY(graph->mapping, graph->column_capacity * 2);
-	REALLOC_ARRAY(graph->new_mapping, graph->column_capacity * 2);
+	REALLOC_ARRAY(graph->old_mapping, graph->column_capacity * 2);
 }
 
 /*
@@ -1116,13 +1116,18 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
 	int horizontal_edge_target = -1;
 
 	/*
-	 * Clear out the new_mapping array
+	 * Swap the mapping and old_mapping arrays
+	 */
+	SWAP(graph->mapping, graph->old_mapping);
+
+	/*
+	 * Clear out the mapping array
 	 */
 	for (i = 0; i < graph->mapping_size; i++)
-		graph->new_mapping[i] = -1;
+		graph->mapping[i] = -1;
 
 	for (i = 0; i < graph->mapping_size; i++) {
-		int target = graph->mapping[i];
+		int target = graph->old_mapping[i];
 		if (target < 0)
 			continue;
 
@@ -1143,14 +1148,14 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
 			 * This column is already in the
 			 * correct place
 			 */
-			assert(graph->new_mapping[i] == -1);
-			graph->new_mapping[i] = target;
-		} else if (graph->new_mapping[i - 1] < 0) {
+			assert(graph->mapping[i] == -1);
+			graph->mapping[i] = target;
+		} else if (graph->mapping[i - 1] < 0) {
 			/*
 			 * Nothing is to the left.
 			 * Move to the left by one
 			 */
-			graph->new_mapping[i - 1] = target;
+			graph->mapping[i - 1] = target;
 			/*
 			 * If there isn't already an edge moving horizontally
 			 * select this one.
@@ -1166,9 +1171,9 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
 				 * line.
 				 */
 				for (j = (target * 2)+3; j < (i - 2); j += 2)
-					graph->new_mapping[j] = target;
+					graph->mapping[j] = target;
 			}
-		} else if (graph->new_mapping[i - 1] == target) {
+		} else if (graph->mapping[i - 1] == target) {
 			/*
 			 * There is a branch line to our left
 			 * already, and it is our target.  We
@@ -1176,7 +1181,7 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
 			 * the same parent commit.
 			 *
 			 * We don't have to add anything to the
-			 * output or new_mapping, since the
+			 * output or mapping, since the
 			 * existing branch line has already taken
 			 * care of it.
 			 */
@@ -1192,10 +1197,10 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
 			 * The branch to the left of that space
 			 * should be our eventual target.
 			 */
-			assert(graph->new_mapping[i - 1] > target);
-			assert(graph->new_mapping[i - 2] < 0);
-			assert(graph->new_mapping[i - 3] == target);
-			graph->new_mapping[i - 2] = target;
+			assert(graph->mapping[i - 1] > target);
+			assert(graph->mapping[i - 2] < 0);
+			assert(graph->mapping[i - 3] == target);
+			graph->mapping[i - 2] = target;
 			/*
 			 * Mark this branch as the horizontal edge to
 			 * prevent any other edges from moving
@@ -1209,14 +1214,14 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
 	/*
 	 * The new mapping may be 1 smaller than the old mapping
 	 */
-	if (graph->new_mapping[graph->mapping_size - 1] < 0)
+	if (graph->mapping[graph->mapping_size - 1] < 0)
 		graph->mapping_size--;
 
 	/*
 	 * Output out a line based on the new mapping info
 	 */
 	for (i = 0; i < graph->mapping_size; i++) {
-		int target = graph->new_mapping[i];
+		int target = graph->mapping[i];
 		if (target < 0)
 			graph_line_addch(line, ' ');
 		else if (target * 2 == i)
@@ -1229,22 +1234,17 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
 				 * won't continue into the next line.
 				 */
 				if (i != (target * 2)+3)
-					graph->new_mapping[i] = -1;
+					graph->mapping[i] = -1;
 				used_horizontal = 1;
 			graph_line_write_column(line, &graph->new_columns[target], '_');
 		} else {
 			if (used_horizontal && i < horizontal_edge)
-				graph->new_mapping[i] = -1;
+				graph->mapping[i] = -1;
 			graph_line_write_column(line, &graph->new_columns[target], '/');
 
 		}
 	}
 
-	/*
-	 * Swap mapping and new_mapping
-	 */
-	SWAP(graph->mapping, graph->new_mapping);
-
 	/*
 	 * If graph->mapping indicates that all of the branch lines
 	 * are already in the correct positions, we are done.
Signature · 1 line
gitgitgadget
#53 lore raw

PATCH v2 11/13 graph: smooth appearance of collapsing edges on commit lines

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

When a graph contains edges that are in the process of collapsing to the left, but those edges cross a commit line, the effect is that the edges have a jagged appearance:

        *
        |\
        | *
        |  \
        *-. \
        |\ \ \
        | | * |
        | * | |
        | |/ /
        * | |
        |/ /
        * |
        |/
        *

We already takes steps to smooth edges like this when they're expanding; when an edge appears to the right of a merge commit marker on a GRAPH_COMMIT line immediately following a GRAPH_POST_MERGE line, we render it as a \:

        * \
        |\ \
        | * \
        | |\ \

We can make a similar improvement to collapsing edges, making them easier to follow and giving the overall graph a feeling of increased symmetry:

        *
        |\
        | *
        |  \
        *-. \
        |\ \ \
        | | * |
        | * | |
        | |/ /
        * / /
        |/ /
        * /
        |/
        *

To do this, we introduce a new special case for edges on GRAPH_COMMIT lines that immediately follow a GRAPH_COLLAPSING line. By retaining a copy of the mapping array used to render the GRAPH_COLLAPSING line in the old_mapping array, we can determine that an edge is collapsing through the GRAPH_COMMIT line and should be smoothed.

---
 graph.c                                    | 17 +++++++++---
 t/t3430-rebase-merges.sh                   |  2 +-
 t/t4202-log.sh                             |  2 +-
 t/t4214-log-graph-octopus.sh               | 32 +++++++++++-----------
 t/t4215-log-skewed-merges.sh               |  4 +--
 t/t6016-rev-list-graph-simplify-history.sh |  4 +--
 6 files changed, 35 insertions(+), 26 deletions(-)
graph.c +13 -4
index 2315f3604d..63f8d18baa 100644
@@ -297,10 +297,10 @@ struct git_graph {
 	 */
 	int *mapping;
 	/*
-	 * A temporary array for computing the next mapping state
-	 * while we are outputting a mapping line.  This is stored as part
-	 * of the git_graph simply so we don't have to allocate a new
-	 * temporary array each time we have to output a collapsing line.
+	 * A copy of the contents of the mapping array from the last commit,
+	 * which we use to improve the display of columns that are tracking
+	 * from right to left through a commit line.  We also use this to
+	 * avoid allocating a fresh array when we compute the next mapping.
 	 */
 	int *old_mapping;
 	/*
@@ -1015,6 +1015,10 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line
 				graph_line_write_column(line, col, '\\');
 			else
 				graph_line_write_column(line, col, '|');
+		} else if (graph->prev_state == GRAPH_COLLAPSING &&
+			   graph->old_mapping[2 * i + 1] == i &&
+			   graph->mapping[2 * i] < i) {
+			graph_line_write_column(line, col, '/');
 		} else {
 			graph_line_write_column(line, col, '|');
 		}
@@ -1211,6 +1215,11 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
 		}
 	}
 
+	/*
+	 * Copy the current mapping array into old_mapping
+	 */
+	COPY_ARRAY(graph->old_mapping, graph->mapping, graph->mapping_size);
+
 	/*
 	 * The new mapping may be 1 smaller than the old mapping
 	 */
t/t3430-rebase-merges.sh +1 -1
index 9efcf4808a..a30d27e9f3 100755
@@ -408,7 +408,7 @@ test_expect_success 'octopus merges' '
 	| | * three
 	| * | two
 	| |/
-	* | one
+	* / one
 	|/
 	o before-octopus
 	EOF
t/t4202-log.sh +1 -1
index e803ba402e..ab0d021365 100755
@@ -667,7 +667,7 @@ cat > expect <<\EOF
 * | | fifth
 * | | fourth
 |/ /
-* | third
+* / third
 |/
 * second
 * initial
t/t4214-log-graph-octopus.sh +16 -16
index 1b96276894..21bc600a82 100755
@@ -31,9 +31,9 @@ test_expect_success 'log --graph with tricky octopus merge, no color' '
 	| | | * 4
 	| | * | 3
 	| | |/
-	| * | 2
+	| * / 2
 	| |/
-	* | 1
+	* / 1
 	|/
 	* initial
 	EOF
@@ -51,9 +51,9 @@ test_expect_success 'log --graph with tricky octopus merge with colors' '
 	<RED>|<RESET> <YELLOW>|<RESET> <BLUE>|<RESET> * 4
 	<RED>|<RESET> <YELLOW>|<RESET> * <MAGENTA>|<RESET> 3
 	<RED>|<RESET> <YELLOW>|<RESET> <MAGENTA>|<RESET><MAGENTA>/<RESET>
-	<RED>|<RESET> * <MAGENTA>|<RESET> 2
+	<RED>|<RESET> * <MAGENTA>/<RESET> 2
 	<RED>|<RESET> <MAGENTA>|<RESET><MAGENTA>/<RESET>
-	* <MAGENTA>|<RESET> 1
+	* <MAGENTA>/<RESET> 1
 	<MAGENTA>|<RESET><MAGENTA>/<RESET>
 	* initial
 	EOF
@@ -72,9 +72,9 @@ test_expect_success 'log --graph with normal octopus merge, no color' '
 	| | | * 4
 	| | * | 3
 	| | |/
-	| * | 2
+	| * / 2
 	| |/
-	* | 1
+	* / 1
 	|/
 	* initial
 	EOF
@@ -90,9 +90,9 @@ test_expect_success 'log --graph with normal octopus merge with colors' '
 	<RED>|<RESET> <GREEN>|<RESET> <YELLOW>|<RESET> * 4
 	<RED>|<RESET> <GREEN>|<RESET> * <BLUE>|<RESET> 3
 	<RED>|<RESET> <GREEN>|<RESET> <BLUE>|<RESET><BLUE>/<RESET>
-	<RED>|<RESET> * <BLUE>|<RESET> 2
+	<RED>|<RESET> * <BLUE>/<RESET> 2
 	<RED>|<RESET> <BLUE>|<RESET><BLUE>/<RESET>
-	* <BLUE>|<RESET> 1
+	* <BLUE>/<RESET> 1
 	<BLUE>|<RESET><BLUE>/<RESET>
 	* initial
 	EOF
@@ -110,9 +110,9 @@ test_expect_success 'log --graph with normal octopus merge and child, no color'
 	| | | * 4
 	| | * | 3
 	| | |/
-	| * | 2
+	| * / 2
 	| |/
-	* | 1
+	* / 1
 	|/
 	* initial
 	EOF
@@ -129,9 +129,9 @@ test_expect_failure 'log --graph with normal octopus and child merge with colors
 	<GREEN>|<RESET> <YELLOW>|<RESET> <BLUE>|<RESET> * 4
 	<GREEN>|<RESET> <YELLOW>|<RESET> * <MAGENTA>|<RESET> 3
 	<GREEN>|<RESET> <YELLOW>|<RESET> <MAGENTA>|<RESET><MAGENTA>/<RESET>
-	<GREEN>|<RESET> * <MAGENTA>|<RESET> 2
+	<GREEN>|<RESET> * <MAGENTA>/<RESET> 2
 	<GREEN>|<RESET> <MAGENTA>|<RESET><MAGENTA>/<RESET>
-	* <MAGENTA>|<RESET> 1
+	* <MAGENTA>/<RESET> 1
 	<MAGENTA>|<RESET><MAGENTA>/<RESET>
 	* initial
 	EOF
@@ -150,9 +150,9 @@ test_expect_success 'log --graph with tricky octopus merge and its child, no col
 	| | | * 4
 	| | * | 3
 	| | |/
-	| * | 2
+	| * / 2
 	| |/
-	* | 1
+	* / 1
 	|/
 	* initial
 	EOF
@@ -171,9 +171,9 @@ test_expect_failure 'log --graph with tricky octopus merge and its child with co
 	<RED>|<RESET> <BLUE>|<RESET> <MAGENTA>|<RESET> * 4
 	<RED>|<RESET> <BLUE>|<RESET> * <CYAN>|<RESET> 3
 	<RED>|<RESET> <BLUE>|<RESET> <CYAN>|<RESET><CYAN>/<RESET>
-	<RED>|<RESET> * <CYAN>|<RESET> 2
+	<RED>|<RESET> * <CYAN>/<RESET> 2
 	<RED>|<RESET> <CYAN>|<RESET><CYAN>/<RESET>
-	* <CYAN>|<RESET> 1
+	* <CYAN>/<RESET> 1
 	<CYAN>|<RESET><CYAN>/<RESET>
 	* initial
 	EOF
t/t4215-log-skewed-merges.sh +2 -2
index e673cdb6f7..1745b3b64c 100755
@@ -17,7 +17,7 @@ test_expect_success 'log --graph with merge fusing with its left and right neigh
 	| | * D
 	| * | C
 	| |/
-	* | B
+	* / B
 	|/
 	* A
 	EOF
@@ -85,7 +85,7 @@ test_expect_success 'log --graph with nested left-skewed merge' '
 	| * | 1_D
 	* | | 1_C
 	|/ /
-	* | 1_B
+	* / 1_B
 	|/
 	* 1_A
 	EOF
t/t6016-rev-list-graph-simplify-history.sh +2 -2
index f7181d1d6a..ca1682f29b 100755
@@ -154,7 +154,7 @@ test_expect_success '--graph --full-history -- bar.txt' '
 	echo "* |   $A4" >> expected &&
 	echo "|\\ \\  " >> expected &&
 	echo "| |/  " >> expected &&
-	echo "* | $A3" >> expected &&
+	echo "* / $A3" >> expected &&
 	echo "|/  " >> expected &&
 	echo "* $A2" >> expected &&
 	git rev-list --graph --full-history --all -- bar.txt > actual &&
@@ -255,7 +255,7 @@ test_expect_success '--graph --boundary ^C3' '
 	echo "* | | | $A3" >> expected &&
 	echo "o | | | $A2" >> expected &&
 	echo "|/ / /  " >> expected &&
-	echo "o | | $A1" >> expected &&
+	echo "o / / $A1" >> expected &&
 	echo " / /  " >> expected &&
 	echo "| o $C3" >> expected &&
 	echo "|/  " >> expected &&
Signature · 1 line
gitgitgadget
#54 lore raw

PATCH v2 12/13 graph: flatten edges that fuse with their right neighbor

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

When a merge commit is printed and its final parent is the same commit that occupies the column to the right of the merge, this results in a kink in the displayed edges:

        * |
        |\ \
        | |/
        | *

Graphs containing these shapes can be hard to read, as the expansion to the right followed immediately by collapsing back to the left creates a lot of zig-zagging edges, especially when many columns are present.

We can improve this by eliminating the zig-zag and having the merge's final parent edge fuse immediately with its neighbor:

        * |
        |\|
        | *

This reduces the horizontal width for the current commit by 2, and requires one less row, making the graph display more compact. Taken in combination with other graph-smoothing enhancements, it greatly compresses the space needed to display certain histories:

*
|\
| * *
| |\ |\
| | * | *
| | | | |\
| | \ | | *
| *-. \ | * |
| |\ \ \ => |/|\|
|/ / / / | | *
| | | / | * |
| | |/ | |/
| | * * /
| * | |/
| |/ *
* |
|/
*

One of the test cases here cannot be correctly rendered in Git v2.23.0; it produces this output following commit E:

        | | *-. \   5_E
        | | |\ \ \
        | |/ / / /
        | | | / _
        | |_|/
        |/| |

The new implementation makes sure that the rightmost edge in this history is not left dangling as above.

---
 graph.c                                    | 34 +++++++++----
 t/t4215-log-skewed-merges.sh               | 56 ++++++++++++++++++----
 t/t6016-rev-list-graph-simplify-history.sh | 30 +++++-------
 3 files changed, 86 insertions(+), 34 deletions(-)
graph.c +26 -8
index 63f8d18baa..80db74aee6 100644
@@ -557,8 +557,24 @@ static void graph_insert_into_new_columns(struct git_graph *graph,
 		shift = (dist > 1) ? 2 * dist - 3 : 1;
 
 		graph->merge_layout = (dist > 0) ? 0 : 1;
+		graph->edges_added = graph->num_parents + graph->merge_layout  - 2;
+
 		mapping_idx = graph->width + (graph->merge_layout - 1) * shift;
 		graph->width += 2 * graph->merge_layout;
+
+	} else if (graph->edges_added > 0 && i == graph->mapping[graph->width - 2]) {
+		/*
+		 * If some columns have been added by a merge, but this commit
+		 * was found in the last existing column, then adjust the
+		 * numbers so that the two edges immediately join, i.e.:
+		 *
+		 *		* |		* |
+		 *		|\ \	=>	|\|
+		 *		| |/		| *
+		 *		| *
+		 */
+		mapping_idx = graph->width - 2;
+		graph->edges_added = -1;
 	} else {
 		mapping_idx = graph->width;
 		graph->width += 2;
@@ -604,6 +620,8 @@ static void graph_update_columns(struct git_graph *graph)
 		graph->mapping[i] = -1;
 
 	graph->width = 0;
+	graph->prev_edges_added = graph->edges_added;
+	graph->edges_added = 0;
 
 	/*
 	 * Populate graph->new_columns and graph->mapping
@@ -731,9 +749,6 @@ void graph_update(struct git_graph *graph, struct commit *commit)
 	 */
 	graph_update_columns(graph);
 
-	graph->prev_edges_added = graph->edges_added;
-	graph->edges_added = graph->num_parents + graph->merge_layout - 2;
-
 	graph->expansion_row = 0;
 
 	/*
@@ -1041,7 +1056,7 @@ const char merge_chars[] = {'/', '|', '\\'};
 static void graph_output_post_merge_line(struct git_graph *graph, struct graph_line *line)
 {
 	int seen_this = 0;
-	int i;
+	int i, j;
 
 	struct commit_list *first_parent = first_interesting_parent(graph);
 	int seen_parent = 0;
@@ -1073,16 +1088,19 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l
 			char c;
 			seen_this = 1;
 
-			for (; parents; parents = next_interesting_parent(graph, parents)) {
+			for (j = 0; j < graph->num_parents; j++) {
 				par_column = graph_find_new_column_by_commit(graph, parents->item);
 				assert(par_column >= 0);
 
 				c = merge_chars[idx];
 				graph_line_write_column(line, &graph->new_columns[par_column], c);
-				if (idx == 2)
-					graph_line_addch(line, ' ');
-				else
+				if (idx == 2) {
+					if (graph->edges_added > 0 || j < graph->num_parents - 1)
+						graph_line_addch(line, ' ');
+				} else {
 					idx++;
+				}
+				parents = next_interesting_parent(graph, parents);
 			}
 			if (graph->edges_added == 0)
 				graph_line_addch(line, ' ');
t/t4215-log-skewed-merges.sh +47 -9

78 lines not on this page · raw message

t/t6016-rev-list-graph-simplify-history.sh +13 -17
index ca1682f29b..f5e6e92f5b 100755
@@ -67,11 +67,10 @@ test_expect_success '--graph --all' '
 	echo "| * $C4" >> expected &&
 	echo "| * $C3" >> expected &&
 	echo "* | $A5" >> expected &&
-	echo "| |     " >> expected &&
-	echo "|  \\    " >> expected &&
-	echo "*-. \\   $A4" >> expected &&
-	echo "|\\ \\ \\  " >> expected &&
-	echo "| | |/  " >> expected &&
+	echo "| |   " >> expected &&
+	echo "|  \\  " >> expected &&
+	echo "*-. | $A4" >> expected &&
+	echo "|\\ \\| " >> expected &&
 	echo "| | * $C2" >> expected &&
 	echo "| | * $C1" >> expected &&
 	echo "| * | $B2" >> expected &&
@@ -97,11 +96,10 @@ test_expect_success '--graph --simplify-by-decoration' '
 	echo "| * $C4" >> expected &&
 	echo "| * $C3" >> expected &&
 	echo "* | $A5" >> expected &&
-	echo "| |     " >> expected &&
-	echo "|  \\    " >> expected &&
-	echo "*-. \\   $A4" >> expected &&
-	echo "|\\ \\ \\  " >> expected &&
-	echo "| | |/  " >> expected &&
+	echo "| |   " >> expected &&
+	echo "|  \\  " >> expected &&
+	echo "*-. | $A4" >> expected &&
+	echo "|\\ \\| " >> expected &&
 	echo "| | * $C2" >> expected &&
 	echo "| | * $C1" >> expected &&
 	echo "| * | $B2" >> expected &&
@@ -131,9 +129,8 @@ test_expect_success '--graph --simplify-by-decoration prune branch B' '
 	echo "| * $C4" >> expected &&
 	echo "| * $C3" >> expected &&
 	echo "* | $A5" >> expected &&
-	echo "* |   $A4" >> expected &&
-	echo "|\\ \\  " >> expected &&
-	echo "| |/  " >> expected &&
+	echo "* | $A4" >> expected &&
+	echo "|\\| " >> expected &&
 	echo "| * $C2" >> expected &&
 	echo "| * $C1" >> expected &&
 	echo "* | $A3" >> expected &&
@@ -151,10 +148,9 @@ test_expect_success '--graph --full-history -- bar.txt' '
 	echo "|\\  " >> expected &&
 	echo "| * $C4" >> expected &&
 	echo "* | $A5" >> expected &&
-	echo "* |   $A4" >> expected &&
-	echo "|\\ \\  " >> expected &&
-	echo "| |/  " >> expected &&
-	echo "* / $A3" >> expected &&
+	echo "* | $A4" >> expected &&
+	echo "|\\| " >> expected &&
+	echo "* | $A3" >> expected &&
 	echo "|/  " >> expected &&
 	echo "* $A2" >> expected &&
 	git rev-list --graph --full-history --all -- bar.txt > actual &&
Signature · 1 line
gitgitgadget
#55 lore raw

PATCH v2 13/13 graph: fix coloring of octopus dashes

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

In 04005834ed ("log: fix coloring of certain octopus merge shapes", 2018-09-01) there is a fix for the coloring of dashes following an octopus merge. It makes a distinction between the case where all parents introduce a new column, versus the case where the first parent collapses into an existing column:

        | *-.           | *-.
        | |\ \          | |\ \
        | | | |         |/ / /

The latter case means that the columns for the merge parents begin one place to the left in the new_columns array compared to the former case.

However, the implementation only works if the commit's parents are kept in order as they map onto the visual columns, as we get the colors by iterating over new_columns as we print the dashes. In general, the commit's parents can arbitrarily merge with existing columns, and change their ordering in the process.

For example, in the following diagram, the number of each column indicates which commit parent appears in each column.

        | | *---.
        | | |\ \ \
        | | |/ / /
        | |/| | /
        | |_|_|/
        |/| | |
        3 1 0 2

If the columns are colored (red, green, yellow, blue), then the dashes will currently be colored yellow and blue, whereas they should be blue and red.

To fix this, we need to look up each column in the mapping array, which before the GRAPH_COLLAPSING state indicates which logical column is displayed in each visual column. This implementation is simpler as it doesn't have any edge cases, and it also handles how left-skewed first parents are now displayed:

        | *-.
        |/|\ \
        | | | |
        0 1 2 3

The color of the first dashes is always the color found in mapping two columns to the right of the commit symbol. Because commits are displayed after all edges have been collapsed together and the visual columns match the logical ones, we can find the visual offset of the commit symbol using commit_index.

---
 graph.c                      | 71 +++++++++++++++++++-----------------
 t/t4214-log-graph-octopus.sh | 10 ++---
 2 files changed, 42 insertions(+), 39 deletions(-)
graph.c +37 -34
index 80db74aee6..e3fd0ea5f8 100644
@@ -684,6 +684,11 @@ static void graph_update_columns(struct git_graph *graph)
 		graph->mapping_size--;
 }
 
+static int graph_num_dashed_parents(struct git_graph *graph)
+{
+	return graph->num_parents + graph->merge_layout - 3;
+}
+
 static int graph_num_expansion_rows(struct git_graph *graph)
 {
 	/*
@@ -706,7 +711,7 @@ static int graph_num_expansion_rows(struct git_graph *graph)
 	 * 		| * \
 	 * 		|/|\ \
 	 */
-	return (graph->num_parents + graph->merge_layout - 3) * 2;
+	return graph_num_dashed_parents(graph) * 2;
 }
 
 static int graph_needs_pre_commit_line(struct git_graph *graph)
@@ -934,47 +939,45 @@ static void graph_output_commit_char(struct git_graph *graph, struct graph_line
 static void graph_draw_octopus_merge(struct git_graph *graph, struct graph_line *line)
 {
 	/*
-	 * Here dashless_parents represents the number of parents which don't
-	 * need to have dashes (the edges labeled "0" and "1").  And
-	 * dashful_parents are the remaining ones.
+	 * The parents of a merge commit can be arbitrarily reordered as they
+	 * are mapped onto display columns, for example this is a valid merge:
 	 *
-	 * | *---.
-	 * | |\ \ \
-	 * | | | | |
-	 * x 0 1 2 3
+	 *	| | *---.
+	 *	| | |\ \ \
+	 *	| | |/ / /
+	 *	| |/| | /
+	 *	| |_|_|/
+	 *	|/| | |
+	 *	3 1 0 2
 	 *
-	 */
-	const int dashless_parents = 3 - graph->merge_layout;
-	int dashful_parents = graph->num_parents - dashless_parents;
-
-	/*
-	 * Usually, we add one new column for each parent (like the diagram
-	 * above) but sometimes the first parent goes into an existing column,
-	 * like this:
+	 * The numbers denote which parent of the merge each visual column
+	 * corresponds to; we can't assume that the parents will initially
+	 * display in the order given by new_columns.
 	 *
-	 * | *-.
-	 * |/|\ \
-	 * | | | |
-	 * x 0 1 2
+	 * To find the right color for each dash, we need to consult the
+	 * mapping array, starting from the column 2 places to the right of the
+	 * merge commit, and use that to find out which logical column each
+	 * edge will collapse to.
 	 *
-	 * In which case the number of parents will be one greater than the
-	 * number of added columns.
+	 * Commits are rendered once all edges have collapsed to their correct
+	 * logcial column, so commit_index gives us the right visual offset for
+	 * the merge commit.
 	 */
-	int added_cols = (graph->num_new_columns - graph->num_columns);
-	int parent_in_old_cols = graph->num_parents - added_cols;
 
-	/*
-	 * In both cases, commit_index corresponds to the edge labeled "0".
-	 */
-	int first_col = graph->commit_index + dashless_parents
-	    - parent_in_old_cols;
+	int i, j;
+	struct column *col;
 
-	int i;
-	for (i = 0; i < dashful_parents; i++) {
-		graph_line_write_column(line, &graph->new_columns[i+first_col], '-');
-		graph_line_write_column(line, &graph->new_columns[i+first_col],
-					  i == dashful_parents-1 ? '.' : '-');
+	int dashed_parents = graph_num_dashed_parents(graph);
+
+	for (i = 0; i < dashed_parents; i++) {
+		j = graph->mapping[(graph->commit_index + i + 2) * 2];
+		col = &graph->new_columns[j];
+
+		graph_line_write_column(line, col, '-');
+		graph_line_write_column(line, col, (i == dashed_parents - 1) ? '.' : '-');
 	}
+
+	return;
 }
 
 static void graph_output_commit_line(struct git_graph *graph, struct graph_line *line)
t/t4214-log-graph-octopus.sh +5 -5
index 21bc600a82..40d27db674 100755
@@ -121,7 +121,7 @@ test_expect_success 'log --graph with normal octopus merge and child, no color'
 	test_cmp expect.uncolored actual
 '
 
-test_expect_failure 'log --graph with normal octopus and child merge with colors' '
+test_expect_success 'log --graph with normal octopus and child merge with colors' '
 	cat >expect.colors <<-\EOF &&
 	* after-merge
 	*<BLUE>-<RESET><BLUE>-<RESET><MAGENTA>-<RESET><MAGENTA>.<RESET>   octopus-merge
@@ -161,7 +161,7 @@ test_expect_success 'log --graph with tricky octopus merge and its child, no col
 	test_cmp expect.uncolored actual
 '
 
-test_expect_failure 'log --graph with tricky octopus merge and its child with colors' '
+test_expect_success 'log --graph with tricky octopus merge and its child with colors' '
 	test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
 	cat >expect.colors <<-\EOF &&
 	* left
@@ -205,7 +205,7 @@ test_expect_success 'log --graph with crossover in octopus merge, no color' '
 	test_cmp expect.uncolored actual
 '
 
-test_expect_failure 'log --graph with crossover in octopus merge with colors' '
+test_expect_success 'log --graph with crossover in octopus merge with colors' '
 	test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
 	cat >expect.colors <<-\EOF &&
 	* after-4
@@ -253,7 +253,7 @@ test_expect_success 'log --graph with crossover in octopus merge and its child,
 	test_cmp expect.uncolored actual
 '
 
-test_expect_failure 'log --graph with crossover in octopus merge and its child with colors' '
+test_expect_success 'log --graph with crossover in octopus merge and its child with colors' '
 	test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
 	cat >expect.colors <<-\EOF &&
 	* after-4
@@ -349,7 +349,7 @@ test_expect_success 'log --graph with unrelated commit and octopus child, no col
 	test_cmp expect.uncolored actual
 '
 
-test_expect_failure 'log --graph with unrelated commit and octopus child with colors' '
+test_expect_success 'log --graph with unrelated commit and octopus child with colors' '
 	test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
 	cat >expect.colors <<-\EOF &&
 	* after-initial
Signature · 1 line
gitgitgadget
#56 lore raw

v3 cover Improve the readability of log --graph output

In reply to James Coglan via GitGitGadget

This series of patches are designed to improve the output of the log --graph command; their effect can be summed up in the following diagram:

    Before                    After
    ------                    -----

    *
    |\
    | *                       *
    | |\                      |\
    | | *                     | *
    | | |                     | |\
    | |  \                    | | *
    | *-. \                   | * |
    | |\ \ \                  |/|\|
    |/ / / /                  | | *
    | | | /                   | * |
    | | |/                    | |/
    | | *                     * /
    | * |                     |/
    | |/                      *
    * |
    |/
    *

These changes aim to make the edges in graph diagrams easier to read, by straightening lines and making certain kinds of topologies display more compactly. Three distinct changes are included.

First, if the first parent of a merge fuses with an edge to the left of the commit, then we display that by making the edges fuse immediately rather than by drawing a line straight down and then having it track to the left. That is, where we currently display these graphs:

    | *             | | | *
    | |\            | | | |\
    |/ /            | |_|/ /
    | |             |/| | |

We will now display these merges as follows:

    | *             | | | *
    |/|             | |_|/|
    | |             |/| | |

This transformation is applied to merges with any number of parents, for example we currently display 3-parent merges like this:

    | *-.           | | | *-.
    | |\ \          | | | |\ \
    |/ / /          | |_|/ / /
    | | |           |/| | | |

And we will now display them like this:

    | *             | | | *
    |/|\            | |_|/|\
    | | |           |/| | | |

If the edge the first parent fuses with is separated from the commit by multiple columns, a horizontal edge is drawn just as we currently do in the 'collapsing' state. This change also affects the display of commit and post-merge lines in subtle ways that are more thoroughly described in the relevant commits.

The second change is that if the final parent of a merge fuses with the edge to the right of the commit, then we can remove the zig-zag effect that currently results. We currently display these merges like this:

    * |
    |\ \
    | |/
    | *

After these changes, this merge will now be displayed like so:

    * |
    |\|
    | *

If the final parent fuses with an edge that's further to the right, its display is unchanged and it will still display like this:

    * | | |
    |\ \ \ \
    | | |_|/
    | |/| |
    | * | |

The final structural change smooths out lines that are collapsing through commit lines. For example, consider the following history:

    *-. \
    |\ \ \
    | | * |
    | * | |
    | |/ /
    * | |
    |/ /
    * |
    |/
    *

This is now rendered so that commit lines display an edge using / instead of |, if that edge is tracking to the left both above and below the commit line. That results in this improved display:

    *-. \
    |\ \ \
    | | * |
    | * | |
    | |/ /
    * / /
    |/ /
    * /
    |/
    *

Taken together, these changes produce the change shown in the first diagram above, with the current rendering on the left and the new rendering on the right.

A final addition to that set of changes fixes the coloring of dashes that are drawn next to octopus merges, in a manner compatible with all these changes. The early commits in this set are refactorings that make the functional changes easier to introduce.

James Coglan (13):
graph: automatically track display width of graph lines
graph: handle line padding in graph_next_line()
graph: reuse find_new_column_by_commit()
graph: reduce duplication in graph_insert_into_new_columns()
graph: remove mapping_idx and graph_update_width()
graph: extract logic for moving to GRAPH_PRE_COMMIT state
graph: example of graph output that can be simplified
graph: tidy up display of left-skewed merges
graph: commit and post-merge lines for left-skewed merges
graph: rename new_mapping to old_mapping
graph: smooth appearance of collapsing edges on commit lines
graph: flatten edges that fuse with their right neighbor
graph: fix coloring of octopus dashes

 graph.c                                    | 646 ++++++++++++---------
 t/t3430-rebase-merges.sh                   |   2 +-
 t/t4202-log.sh                             |   2 +-
 t/t4214-log-graph-octopus.sh               |  62 +-
 t/t4215-log-skewed-merges.sh               | 257 ++++++++
 t/t6016-rev-list-graph-simplify-history.sh |  30 +-
 6 files changed, 673 insertions(+), 326 deletions(-)
 create mode 100755 t/t4215-log-skewed-merges.sh

base-commit: 108b97dc372828f0e72e56bbb40cae8e1e83ece6
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-383%2Fjcoglan%2Fjc%2Fsimplify-graph-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-383/jcoglan/jc/simplify-graph-v3
Pull-Request: https://github.com/gitgitgadget/git/pull/383

Range-diff vs v2:

  1:  c30f5bb43b =  1:  722ab8973a graph: automatically track display width of graph lines
  2:  d962eb2e02 !  2:  056c95d4ed graph: handle line padding in `graph_next_line()`
     @@ -18,6 +18,8 @@
          `graph_next_line()` and we must not pad the `graph_line` if no commit is
          set.
      
     +    Signed-off-by: James Coglan <[email protected]>
     +
       diff --git a/graph.c b/graph.c
       --- a/graph.c
       +++ b/graph.c
  3:  ecfcfbfd5c =  3:  376236ab0b graph: reuse `find_new_column_by_commit()`
  4:  50ce875ed9 =  4:  6a4717bf05 graph: reduce duplication in `graph_insert_into_new_columns()`
  5:  d2e8958eed =  5:  678b53b671 graph: remove `mapping_idx` and `graph_update_width()`
  6:  163600585c =  6:  8ed32b1c74 graph: extract logic for moving to GRAPH_PRE_COMMIT state
  7:  51495be940 !  7:  631ee3cecb graph: example of graph output that can be simplified
     @@ -86,6 +86,8 @@
                  |/
                  * A
      
     +    Signed-off-by: James Coglan <[email protected]>
     +
       diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
       new file mode 100755
       --- /dev/null
  8:  2ab0f9775b =  8:  c34a5eb160 graph: tidy up display of left-skewed merges
  9:  0e37c88c60 =  9:  2f75c697be graph: commit and post-merge lines for left-skewed merges
 10:  9bbf738e6d = 10:  f5c2791436 graph: rename `new_mapping` to `old_mapping`
 11:  67051ec31a = 11:  9b24893de6 graph: smooth appearance of collapsing edges on commit lines
 12:  503c846d2b = 12:  04ba5169bc graph: flatten edges that fuse with their right neighbor
 13:  07ddd509c5 = 13:  cd761b3a32 graph: fix coloring of octopus dashes
Signature · 1 line
gitgitgadget
#57 lore raw

PATCH v3 1/13 graph: automatically track display width of graph lines

From: James Coglan <[email protected]>

All the output functions called by graph_next_line() currently keep track of how many printable chars they've written to the buffer, before calling graph_pad_horizontally() to pad the line with spaces. Some functions do this by incrementing a counter whenever they write to the buffer, and others do it by encoding an assumption about how many chars are written, as in:

    graph_pad_horizontally(graph, sb, graph->num_columns * 2);

This adds a fair amount of noise to the functions' logic and is easily broken if one forgets to increment the right counter or update the calculations used for padding.

To make this easier to use, I'm introducing a new struct called graph_line that wraps a strbuf and keeps count of its display width implicitly. graph_next_line() wraps this around the struct strbuf * it's given and passes a struct graph_line * to the output functions, which use its interface.

The graph_line interface wraps the strbuf_addch(), strbuf_addchars() and strbuf_addstr() functions, and adds the graph_line_write_column() function for adding a single character with color formatting. The graph_pad_horizontally() function can then use the width field from the struct rather than taking a character count as a parameter.

---
 graph.c | 194 +++++++++++++++++++++++++++++---------------------------
 1 file changed, 99 insertions(+), 95 deletions(-)
graph.c +99 -95
index f53135485f..2f81a5d23d 100644
@@ -112,14 +112,42 @@ static const char *column_get_color_code(unsigned short color)
 	return column_colors[color];
 }
 
-static void strbuf_write_column(struct strbuf *sb, const struct column *c,
-				char col_char)
+struct graph_line {
+	struct strbuf *buf;
+	size_t width;
+};
+
+static inline void graph_line_addch(struct graph_line *line, int c)
+{
+	strbuf_addch(line->buf, c);
+	line->width++;
+}
+
+static inline void graph_line_addchars(struct graph_line *line, int c, size_t n)
+{
+	strbuf_addchars(line->buf, c, n);
+	line->width += n;
+}
+
+static inline void graph_line_addstr(struct graph_line *line, const char *s)
+{
+	strbuf_addstr(line->buf, s);
+	line->width += strlen(s);
+}
+
+static inline void graph_line_addcolor(struct graph_line *line, unsigned short color)
+{
+	strbuf_addstr(line->buf, column_get_color_code(color));
+}
+
+static void graph_line_write_column(struct graph_line *line, const struct column *c,
+				    char col_char)
 {
 	if (c->color < column_colors_max)
-		strbuf_addstr(sb, column_get_color_code(c->color));
-	strbuf_addch(sb, col_char);
+		graph_line_addcolor(line, c->color);
+	graph_line_addch(line, col_char);
 	if (c->color < column_colors_max)
-		strbuf_addstr(sb, column_get_color_code(column_colors_max));
+		graph_line_addcolor(line, column_colors_max);
 }
 
 struct git_graph {
@@ -686,8 +714,7 @@ static int graph_is_mapping_correct(struct git_graph *graph)
 	return 1;
 }
 
-static void graph_pad_horizontally(struct git_graph *graph, struct strbuf *sb,
-				   int chars_written)
+static void graph_pad_horizontally(struct git_graph *graph, struct graph_line *line)
 {
 	/*
 	 * Add additional spaces to the end of the strbuf, so that all
@@ -696,12 +723,12 @@ static void graph_pad_horizontally(struct git_graph *graph, struct strbuf *sb,
 	 * This way, fields printed to the right of the graph will remain
 	 * aligned for the entire commit.
 	 */
-	if (chars_written < graph->width)
-		strbuf_addchars(sb, ' ', graph->width - chars_written);
+	if (line->width < graph->width)
+		graph_line_addchars(line, ' ', graph->width - line->width);
 }
 
 static void graph_output_padding_line(struct git_graph *graph,
-				      struct strbuf *sb)
+				      struct graph_line *line)
 {
 	int i;
 
@@ -719,11 +746,11 @@ 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++) {
-		strbuf_write_column(sb, &graph->new_columns[i], '|');
-		strbuf_addch(sb, ' ');
+		graph_line_write_column(line, &graph->new_columns[i], '|');
+		graph_line_addch(line, ' ');
 	}
 
-	graph_pad_horizontally(graph, sb, graph->num_new_columns * 2);
+	graph_pad_horizontally(graph, line);
 }
 
 
@@ -733,14 +760,14 @@ int graph_width(struct git_graph *graph)
 }
 
 
-static void graph_output_skip_line(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_skip_line(struct git_graph *graph, struct graph_line *line)
 {
 	/*
 	 * Output an ellipsis to indicate that a portion
 	 * of the graph is missing.
 	 */
-	strbuf_addstr(sb, "...");
-	graph_pad_horizontally(graph, sb, 3);
+	graph_line_addstr(line, "...");
+	graph_pad_horizontally(graph, line);
 
 	if (graph->num_parents >= 3 &&
 	    graph->commit_index < (graph->num_columns - 1))
@@ -750,11 +777,10 @@ static void graph_output_skip_line(struct git_graph *graph, struct strbuf *sb)
 }
 
 static void graph_output_pre_commit_line(struct git_graph *graph,
-					 struct strbuf *sb)
+					 struct graph_line *line)
 {
 	int num_expansion_rows;
 	int i, seen_this;
-	int chars_written;
 
 	/*
 	 * This function formats a row that increases the space around a commit
@@ -777,14 +803,12 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 	 * Output the row
 	 */
 	seen_this = 0;
-	chars_written = 0;
 	for (i = 0; i < graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		if (col->commit == graph->commit) {
 			seen_this = 1;
-			strbuf_write_column(sb, col, '|');
-			strbuf_addchars(sb, ' ', graph->expansion_row);
-			chars_written += 1 + graph->expansion_row;
+			graph_line_write_column(line, col, '|');
+			graph_line_addchars(line, ' ', graph->expansion_row);
 		} else if (seen_this && (graph->expansion_row == 0)) {
 			/*
 			 * This is the first line of the pre-commit output.
@@ -797,22 +821,18 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 			 */
 			if (graph->prev_state == GRAPH_POST_MERGE &&
 			    graph->prev_commit_index < i)
-				strbuf_write_column(sb, col, '\\');
+				graph_line_write_column(line, col, '\\');
 			else
-				strbuf_write_column(sb, col, '|');
-			chars_written++;
+				graph_line_write_column(line, col, '|');
 		} else if (seen_this && (graph->expansion_row > 0)) {
-			strbuf_write_column(sb, col, '\\');
-			chars_written++;
+			graph_line_write_column(line, col, '\\');
 		} else {
-			strbuf_write_column(sb, col, '|');
-			chars_written++;
+			graph_line_write_column(line, col, '|');
 		}
-		strbuf_addch(sb, ' ');
-		chars_written++;
+		graph_line_addch(line, ' ');
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, line);
 
 	/*
 	 * Increment graph->expansion_row,
@@ -823,7 +843,7 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 		graph_update_state(graph, GRAPH_COMMIT);
 }
 
-static void graph_output_commit_char(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_commit_char(struct git_graph *graph, struct graph_line *line)
 {
 	/*
 	 * For boundary commits, print 'o'
@@ -831,22 +851,20 @@ static void graph_output_commit_char(struct git_graph *graph, struct strbuf *sb)
 	 */
 	if (graph->commit->object.flags & BOUNDARY) {
 		assert(graph->revs->boundary);
-		strbuf_addch(sb, 'o');
+		graph_line_addch(line, 'o');
 		return;
 	}
 
 	/*
 	 * get_revision_mark() handles all other cases without assert()
 	 */
-	strbuf_addstr(sb, get_revision_mark(graph->revs, graph->commit));
+	graph_line_addstr(line, get_revision_mark(graph->revs, graph->commit));
 }
 
 /*
- * Draw the horizontal dashes of an octopus merge and return the number of
- * characters written.
+ * Draw the horizontal dashes of an octopus merge.
  */
-static int graph_draw_octopus_merge(struct git_graph *graph,
-				    struct strbuf *sb)
+static void graph_draw_octopus_merge(struct git_graph *graph, struct graph_line *line)
 {
 	/*
 	 * Here dashless_parents represents the number of parents which don't
@@ -886,17 +904,16 @@ static int graph_draw_octopus_merge(struct git_graph *graph,
 
 	int i;
 	for (i = 0; i < dashful_parents; i++) {
-		strbuf_write_column(sb, &graph->new_columns[i+first_col], '-');
-		strbuf_write_column(sb, &graph->new_columns[i+first_col],
-				    i == dashful_parents-1 ? '.' : '-');
+		graph_line_write_column(line, &graph->new_columns[i+first_col], '-');
+		graph_line_write_column(line, &graph->new_columns[i+first_col],
+					  i == dashful_parents-1 ? '.' : '-');
 	}
-	return 2 * dashful_parents;
 }
 
-static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_commit_line(struct git_graph *graph, struct graph_line *line)
 {
 	int seen_this = 0;
-	int i, chars_written;
+	int i;
 
 	/*
 	 * Output the row containing this commit
@@ -906,7 +923,6 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 	 * children that we have already processed.)
 	 */
 	seen_this = 0;
-	chars_written = 0;
 	for (i = 0; i <= graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		struct commit *col_commit;
@@ -920,15 +936,12 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 
 		if (col_commit == graph->commit) {
 			seen_this = 1;
-			graph_output_commit_char(graph, sb);
-			chars_written++;
+			graph_output_commit_char(graph, line);
 
 			if (graph->num_parents > 2)
-				chars_written += graph_draw_octopus_merge(graph,
-									  sb);
+				graph_draw_octopus_merge(graph, line);
 		} else if (seen_this && (graph->num_parents > 2)) {
-			strbuf_write_column(sb, col, '\\');
-			chars_written++;
+			graph_line_write_column(line, col, '\\');
 		} else if (seen_this && (graph->num_parents == 2)) {
 			/*
 			 * This is a 2-way merge commit.
@@ -945,19 +958,16 @@ static void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
 			 */
 			if (graph->prev_state == GRAPH_POST_MERGE &&
 			    graph->prev_commit_index < i)
-				strbuf_write_column(sb, col, '\\');
+				graph_line_write_column(line, col, '\\');
 			else
-				strbuf_write_column(sb, col, '|');
-			chars_written++;
+				graph_line_write_column(line, col, '|');
 		} else {
-			strbuf_write_column(sb, col, '|');
-			chars_written++;
+			graph_line_write_column(line, col, '|');
 		}
-		strbuf_addch(sb, ' ');
-		chars_written++;
+		graph_line_addch(line, ' ');
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, line);
 
 	/*
 	 * Update graph->state
@@ -981,15 +991,14 @@ static struct column *find_new_column_by_commit(struct git_graph *graph,
 	return NULL;
 }
 
-static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_post_merge_line(struct git_graph *graph, struct graph_line *line)
 {
 	int seen_this = 0;
-	int i, j, chars_written;
+	int i, j;
 
 	/*
 	 * Output the post-merge row
 	 */
-	chars_written = 0;
 	for (i = 0; i <= graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		struct commit *col_commit;
@@ -1016,29 +1025,25 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf
 			par_column = find_new_column_by_commit(graph, parents->item);
 			assert(par_column);
 
-			strbuf_write_column(sb, par_column, '|');
-			chars_written++;
+			graph_line_write_column(line, par_column, '|');
 			for (j = 0; j < graph->num_parents - 1; j++) {
 				parents = next_interesting_parent(graph, parents);
 				assert(parents);
 				par_column = find_new_column_by_commit(graph, parents->item);
 				assert(par_column);
-				strbuf_write_column(sb, par_column, '\\');
-				strbuf_addch(sb, ' ');
+				graph_line_write_column(line, par_column, '\\');
+				graph_line_addch(line, ' ');
 			}
-			chars_written += j * 2;
 		} else if (seen_this) {
-			strbuf_write_column(sb, col, '\\');
-			strbuf_addch(sb, ' ');
-			chars_written += 2;
+			graph_line_write_column(line, col, '\\');
+			graph_line_addch(line, ' ');
 		} else {
-			strbuf_write_column(sb, col, '|');
-			strbuf_addch(sb, ' ');
-			chars_written += 2;
+			graph_line_write_column(line, col, '|');
+			graph_line_addch(line, ' ');
 		}
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, line);
 
 	/*
 	 * Update graph->state
@@ -1049,7 +1054,7 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct strbuf
 		graph_update_state(graph, GRAPH_COLLAPSING);
 }
 
-static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf *sb)
+static void graph_output_collapsing_line(struct git_graph *graph, struct graph_line *line)
 {
 	int i;
 	short used_horizontal = 0;
@@ -1159,9 +1164,9 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 	for (i = 0; i < graph->mapping_size; i++) {
 		int target = graph->new_mapping[i];
 		if (target < 0)
-			strbuf_addch(sb, ' ');
+			graph_line_addch(line, ' ');
 		else if (target * 2 == i)
-			strbuf_write_column(sb, &graph->new_columns[target], '|');
+			graph_line_write_column(line, &graph->new_columns[target], '|');
 		else if (target == horizontal_edge_target &&
 			 i != horizontal_edge - 1) {
 				/*
@@ -1172,16 +1177,16 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 				if (i != (target * 2)+3)
 					graph->new_mapping[i] = -1;
 				used_horizontal = 1;
-			strbuf_write_column(sb, &graph->new_columns[target], '_');
+			graph_line_write_column(line, &graph->new_columns[target], '_');
 		} else {
 			if (used_horizontal && i < horizontal_edge)
 				graph->new_mapping[i] = -1;
-			strbuf_write_column(sb, &graph->new_columns[target], '/');
+			graph_line_write_column(line, &graph->new_columns[target], '/');
 
 		}
 	}
 
-	graph_pad_horizontally(graph, sb, graph->mapping_size);
+	graph_pad_horizontally(graph, line);
 
 	/*
 	 * Swap mapping and new_mapping
@@ -1199,24 +1204,26 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct strbuf
 
 int graph_next_line(struct git_graph *graph, struct strbuf *sb)
 {
+	struct graph_line line = { .buf = sb, .width = 0 };
+
 	switch (graph->state) {
 	case GRAPH_PADDING:
-		graph_output_padding_line(graph, sb);
+		graph_output_padding_line(graph, &line);
 		return 0;
 	case GRAPH_SKIP:
-		graph_output_skip_line(graph, sb);
+		graph_output_skip_line(graph, &line);
 		return 0;
 	case GRAPH_PRE_COMMIT:
-		graph_output_pre_commit_line(graph, sb);
+		graph_output_pre_commit_line(graph, &line);
 		return 0;
 	case GRAPH_COMMIT:
-		graph_output_commit_line(graph, sb);
+		graph_output_commit_line(graph, &line);
 		return 1;
 	case GRAPH_POST_MERGE:
-		graph_output_post_merge_line(graph, sb);
+		graph_output_post_merge_line(graph, &line);
 		return 0;
 	case GRAPH_COLLAPSING:
-		graph_output_collapsing_line(graph, sb);
+		graph_output_collapsing_line(graph, &line);
 		return 0;
 	}
 
@@ -1227,7 +1234,7 @@ int graph_next_line(struct git_graph *graph, struct strbuf *sb)
 static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)
 {
 	int i;
-	int chars_written = 0;
+	struct graph_line line = { .buf = sb, .width = 0 };
 
 	if (graph->state != GRAPH_COMMIT) {
 		graph_next_line(graph, sb);
@@ -1244,20 +1251,17 @@ 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];
 
-		strbuf_write_column(sb, col, '|');
-		chars_written++;
+		graph_line_write_column(&line, col, '|');
 
 		if (col->commit == graph->commit && graph->num_parents > 2) {
 			int len = (graph->num_parents - 2) * 2;
-			strbuf_addchars(sb, ' ', len);
-			chars_written += len;
+			graph_line_addchars(&line, ' ', len);
 		} else {
-			strbuf_addch(sb, ' ');
-			chars_written++;
+			graph_line_addch(&line, ' ');
 		}
 	}
 
-	graph_pad_horizontally(graph, sb, chars_written);
+	graph_pad_horizontally(graph, &line);
 
 	/*
 	 * Update graph->prev_state since we have output a padding line
Signature · 1 line
gitgitgadget
#58 lore raw

Re: [PATCH v3 01/13] graph: automatically track display width of graph lines

Quoted 17 lines · James Coglan via GitGitGadget
graph.c +17 -0
+struct graph_line {
+	struct strbuf *buf;
+	size_t width;
+};
+
+static inline void graph_line_addch(struct graph_line *line, int c)
+{
+	strbuf_addch(line->buf, c);
+	line->width++;
+}
+
+static inline void graph_line_addchars(struct graph_line *line, int c, size_t n)
+{
+	strbuf_addchars(line->buf, c, n);
+	line->width += n;
+}
+

graph.c static const char *column_get_color_code(unsigned short color) · line 141

Quoted 10 lines
graph.c +10 -0
+static inline void graph_line_addstr(struct graph_line *line, const char *s)
+{
+	strbuf_addstr(line->buf, s);
+	line->width += strlen(s);
+}
+
+static inline void graph_line_addcolor(struct graph_line *line, unsigned short color)
+{
+	strbuf_addstr(line->buf, column_get_color_code(color));
+}

All makes sense, and as long as nobody uses strbuf_add*() on line->buf directly, it shouldn't be too hard to extend these to support graph drawn characters outside ASCII in the future after this series settles.

graph.c static void graph_output_pre_commit_line(struct git_graph *graph, · line 811

Quoted 11 lines

static void graph_output_pre_commit_line(struct git_graph *graph,
...

graph.c +2 -3
 	for (i = 0; i < graph->num_columns; i++) {
 		struct column *col = &graph->columns[i];
 		if (col->commit == graph->commit) {
 			seen_this = 1;
-			strbuf_write_column(sb, col, '|');
-			strbuf_addchars(sb, ' ', graph->expansion_row);
-			chars_written += 1 + graph->expansion_row;
+			graph_line_write_column(line, col, '|');
+			graph_line_addchars(line, ' ', graph->expansion_row);

Nice reduction of noise, as the proposed log message promises.

Quoted 1 line

int graph_next_line(struct git_graph *graph, struct strbuf *sb)

I just noticed it but does this have to be extern? Nobody outside graph.[ch] seems to have any reference to it. That might mean that it may be easier than we thought earlier to change the second parameter of this to "struct graph_line *", instead of us wrapping an incoming strbuf (which external callers are aware of, as opposed to graph_line which we prefer not to expose to outsiders). Whether we change the second parameter or not, the first clean-up may be to turn this function into a file-scope static, perhaps?

All the changes are very pleasing to see. Thanks.

#59 lore raw

Re: [PATCH v3 01/13] graph: automatically track display width of graph lines

Quoted 4 lines · Junio C Hamano
Quoted 1 line

int graph_next_line(struct git_graph *graph, struct strbuf *sb)

I just noticed it but does this have to be extern? Nobody outside graph.[ch] seems to have any reference to it.

I was stupid; strike this part out.

Thanks.

#60 lore raw

PATCH v3 2/13 graph: handle line padding in `graph_next_line()`

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

Now that the display width of graph lines is implicitly tracked via the graph_line interface, the calls to graph_pad_horizontally() no longer need to be located inside the individual output functions, where the character counting was previously being done.

All the functions called by graph_next_line() generate a line of output, then call graph_pad_horizontally(), and finally change the graph state if necessary. As padding is the final change to the output done by all these functions, it can be removed from all of them and done in graph_next_line() instead.

I've also moved the guard in graph_output_padding_line() that checks the graph has a commit; this function is only called by graph_next_line() and we must not pad the graph_line if no commit is set.

---
 graph.c | 49 ++++++++++++++++++++-----------------------------
 1 file changed, 20 insertions(+), 29 deletions(-)
graph.c +20 -29
index 2f81a5d23d..4c68557b17 100644
@@ -732,16 +732,6 @@ static void graph_output_padding_line(struct git_graph *graph,
 {
 	int i;
 
-	/*
-	 * We could conceivable be called with a NULL commit
-	 * if our caller has a bug, and invokes graph_next_line()
-	 * immediately after graph_init(), without first calling
-	 * graph_update().  Return without outputting anything in this
-	 * case.
-	 */
-	if (!graph->commit)
-		return;
-
 	/*
 	 * Output a padding row, that leaves all branch lines unchanged
 	 */
@@ -749,8 +739,6 @@ static void graph_output_padding_line(struct git_graph *graph,
 		graph_line_write_column(line, &graph->new_columns[i], '|');
 		graph_line_addch(line, ' ');
 	}
-
-	graph_pad_horizontally(graph, line);
 }
 
 
@@ -767,7 +755,6 @@ static void graph_output_skip_line(struct git_graph *graph, struct graph_line *l
 	 * of the graph is missing.
 	 */
 	graph_line_addstr(line, "...");
-	graph_pad_horizontally(graph, line);
 
 	if (graph->num_parents >= 3 &&
 	    graph->commit_index < (graph->num_columns - 1))
@@ -832,8 +819,6 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 		graph_line_addch(line, ' ');
 	}
 
-	graph_pad_horizontally(graph, line);
-
 	/*
 	 * Increment graph->expansion_row,
 	 * and move to state GRAPH_COMMIT if necessary
@@ -967,8 +952,6 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line
 		graph_line_addch(line, ' ');
 	}
 
-	graph_pad_horizontally(graph, line);
-
 	/*
 	 * Update graph->state
 	 */
@@ -1043,8 +1026,6 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l
 		}
 	}
 
-	graph_pad_horizontally(graph, line);
-
 	/*
 	 * Update graph->state
 	 */
@@ -1186,8 +1167,6 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
 		}
 	}
 
-	graph_pad_horizontally(graph, line);
-
 	/*
 	 * Swap mapping and new_mapping
 	 */
@@ -1204,31 +1183,43 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
 
 int graph_next_line(struct git_graph *graph, struct strbuf *sb)
 {
+	int shown_commit_line = 0;
 	struct graph_line line = { .buf = sb, .width = 0 };
 
+	/*
+	 * We could conceivable be called with a NULL commit
+	 * if our caller has a bug, and invokes graph_next_line()
+	 * immediately after graph_init(), without first calling
+	 * graph_update().  Return without outputting anything in this
+	 * case.
+	 */
+	if (!graph->commit)
+		return -1;
+
 	switch (graph->state) {
 	case GRAPH_PADDING:
 		graph_output_padding_line(graph, &line);
-		return 0;
+		break;
 	case GRAPH_SKIP:
 		graph_output_skip_line(graph, &line);
-		return 0;
+		break;
 	case GRAPH_PRE_COMMIT:
 		graph_output_pre_commit_line(graph, &line);
-		return 0;
+		break;
 	case GRAPH_COMMIT:
 		graph_output_commit_line(graph, &line);
-		return 1;
+		shown_commit_line = 1;
+		break;
 	case GRAPH_POST_MERGE:
 		graph_output_post_merge_line(graph, &line);
-		return 0;
+		break;
 	case GRAPH_COLLAPSING:
 		graph_output_collapsing_line(graph, &line);
-		return 0;
+		break;
 	}
 
-	assert(0);
-	return 0;
+	graph_pad_horizontally(graph, &line);
+	return shown_commit_line;
 }
 
 static void graph_padding_line(struct git_graph *graph, struct strbuf *sb)
Signature · 1 line
gitgitgadget
#61 lore raw

Re: [PATCH v3 02/13] graph: handle line padding in `graph_next_line()`

Quoted 7 lines · James Coglan via GitGitGadget

From: James Coglan <[email protected]>

Now that the display width of graph lines is implicitly tracked via the graph_line interface, the calls to graph_pad_horizontally() no longer need to be located inside the individual output functions, where the character counting was previously being done.

Quoted 5 lines

All the functions called by graph_next_line() generate a line of output, then call graph_pad_horizontally(), and finally change the graph state if necessary. As padding is the final change to the output done by all these functions, it can be removed from all of them and done in graph_next_line() instead.

Very well explained.

#62 lore raw

PATCH v3 3/13 graph: reuse `find_new_column_by_commit()`

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

I will shortly be making some changes to graph_insert_into_new_columns() and so am trying to simplify it. One possible simplification is that we can extract the loop for finding the element in new_columns containing the given commit.

find_new_column_by_commit() contains a very similar loop but it returns a struct column * rather than an int offset into the array. Here I'm introducing a version that returns int and using that in graph_insert_into_new_columns() and graph_output_post_merge_line().

---
 graph.c | 48 +++++++++++++++++++++++-------------------------
 1 file changed, 23 insertions(+), 25 deletions(-)
graph.c +23 -25
index 4c68557b17..c9646d9e00 100644
@@ -460,22 +460,31 @@ static unsigned short graph_find_commit_color(const struct git_graph *graph,
 	return graph_get_current_column_color(graph);
 }
 
+static int graph_find_new_column_by_commit(struct git_graph *graph,
+					   struct commit *commit)
+{
+	int i;
+	for (i = 0; i < graph->num_new_columns; i++) {
+		if (graph->new_columns[i].commit == commit)
+			return i;
+	}
+	return -1;
+}
+
 static void graph_insert_into_new_columns(struct git_graph *graph,
 					  struct commit *commit,
 					  int *mapping_index)
 {
-	int i;
+	int i = graph_find_new_column_by_commit(graph, commit);
 
 	/*
 	 * If the commit is already in the new_columns list, we don't need to
 	 * add it.  Just update the mapping correctly.
 	 */
-	for (i = 0; i < graph->num_new_columns; i++) {
-		if (graph->new_columns[i].commit == commit) {
-			graph->mapping[*mapping_index] = i;
-			*mapping_index += 2;
-			return;
-		}
+	if (i >= 0) {
+		graph->mapping[*mapping_index] = i;
+		*mapping_index += 2;
+		return;
 	}
 
 	/*
@@ -963,17 +972,6 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line
 		graph_update_state(graph, GRAPH_COLLAPSING);
 }
 
-static struct column *find_new_column_by_commit(struct git_graph *graph,
-						struct commit *commit)
-{
-	int i;
-	for (i = 0; i < graph->num_new_columns; i++) {
-		if (graph->new_columns[i].commit == commit)
-			return &graph->new_columns[i];
-	}
-	return NULL;
-}
-
 static void graph_output_post_merge_line(struct git_graph *graph, struct graph_line *line)
 {
 	int seen_this = 0;
@@ -1001,20 +999,20 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l
 			 * edges.
 			 */
 			struct commit_list *parents = NULL;
-			struct column *par_column;
+			int par_column;
 			seen_this = 1;
 			parents = first_interesting_parent(graph);
 			assert(parents);
-			par_column = find_new_column_by_commit(graph, parents->item);
-			assert(par_column);
+			par_column = graph_find_new_column_by_commit(graph, parents->item);
+			assert(par_column >= 0);
 
-			graph_line_write_column(line, par_column, '|');
+			graph_line_write_column(line, &graph->new_columns[par_column], '|');
 			for (j = 0; j < graph->num_parents - 1; j++) {
 				parents = next_interesting_parent(graph, parents);
 				assert(parents);
-				par_column = find_new_column_by_commit(graph, parents->item);
-				assert(par_column);
-				graph_line_write_column(line, par_column, '\\');
+				par_column = graph_find_new_column_by_commit(graph, parents->item);
+				assert(par_column >= 0);
+				graph_line_write_column(line, &graph->new_columns[par_column], '\\');
 				graph_line_addch(line, ' ');
 			}
 		} else if (seen_this) {
Signature · 1 line
gitgitgadget
#63 lore raw

PATCH v3 4/13 graph: reduce duplication in `graph_insert_into_new_columns()`

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

I will shortly be making some changes to this function and so am trying to simplify it. It currently contains some duplicated logic; both branches the function can take assign the commit's column index into the mapping array and increment mapping_index.

Here I change the function so that the only conditional behaviour is that it appends the commit to new_columns if it's not present. All manipulation of mapping now happens on a single code path.

---
 graph.c | 20 +++++++-------------
 1 file changed, 7 insertions(+), 13 deletions(-)
graph.c +7 -13
index c9646d9e00..512ae16535 100644
@@ -478,23 +478,17 @@ static void graph_insert_into_new_columns(struct git_graph *graph,
 	int i = graph_find_new_column_by_commit(graph, commit);
 
 	/*
-	 * If the commit is already in the new_columns list, we don't need to
-	 * add it.  Just update the mapping correctly.
+	 * If the commit is not already in the new_columns array, then add it
+	 * and record it as being in the final column.
 	 */
-	if (i >= 0) {
-		graph->mapping[*mapping_index] = i;
-		*mapping_index += 2;
-		return;
+	if (i < 0) {
+		i = graph->num_new_columns++;
+		graph->new_columns[i].commit = commit;
+		graph->new_columns[i].color = graph_find_commit_color(graph, commit);
 	}
 
-	/*
-	 * This commit isn't already in new_columns.  Add it.
-	 */
-	graph->new_columns[graph->num_new_columns].commit = commit;
-	graph->new_columns[graph->num_new_columns].color = graph_find_commit_color(graph, commit);
-	graph->mapping[*mapping_index] = graph->num_new_columns;
+	graph->mapping[*mapping_index] = i;
 	*mapping_index += 2;
-	graph->num_new_columns++;
 }
 
 static void graph_update_width(struct git_graph *graph,
Signature · 1 line
gitgitgadget
#64 lore raw

PATCH v3 5/13 graph: remove `mapping_idx` and `graph_update_width()`

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

There's a duplication of logic between graph_insert_into_new_columns() and graph_update_width(). graph_insert_into_new_columns() is called repeatedly by graph_update_columns() with an int * that tracks the offset into the mapping array where we should write the next value. Each call to graph_insert_into_new_columns() effectively pushes one column index and one "null" value (-1) onto the mapping array and therefore increments mapping_idx by 2.

graph_update_width() duplicates this process: the width of the graph is essentially the initial width of the mapping array before edges begin collapsing. The graph_update_width() function's logic effectively works out how many times graph_insert_into_new_columns() was called based on the relationship of the current commit to the rest of the graph.

I'm about to make some changes that make the assignment of values into the mapping array more complicated. Rather than make graph_update_width() more complicated at the same time, we can simply remove this function and use graph->width to track the offset into the mapping array as we're building it. This removes the duplication and makes sure that graph->width is the same as the visual width of the mapping array once graph_update_columns() is complete.

---
 graph.c | 65 +++++++++------------------------------------------------
 1 file changed, 10 insertions(+), 55 deletions(-)
graph.c +10 -55
index 512ae16535..d724ef25c3 100644
@@ -472,8 +472,7 @@ static int graph_find_new_column_by_commit(struct git_graph *graph,
 }
 
 static void graph_insert_into_new_columns(struct git_graph *graph,
-					  struct commit *commit,
-					  int *mapping_index)
+					  struct commit *commit)
 {
 	int i = graph_find_new_column_by_commit(graph, commit);
 
@@ -487,50 +486,14 @@ static void graph_insert_into_new_columns(struct git_graph *graph,
 		graph->new_columns[i].color = graph_find_commit_color(graph, commit);
 	}
 
-	graph->mapping[*mapping_index] = i;
-	*mapping_index += 2;
-}
-
-static void graph_update_width(struct git_graph *graph,
-			       int is_commit_in_existing_columns)
-{
-	/*
-	 * Compute the width needed to display the graph for this commit.
-	 * This is the maximum width needed for any row.  All other rows
-	 * will be padded to this width.
-	 *
-	 * Compute the number of columns in the widest row:
-	 * Count each existing column (graph->num_columns), and each new
-	 * column added by this commit.
-	 */
-	int max_cols = graph->num_columns + graph->num_parents;
-
-	/*
-	 * Even if the current commit has no parents to be printed, it
-	 * still takes up a column for itself.
-	 */
-	if (graph->num_parents < 1)
-		max_cols++;
-
-	/*
-	 * We added a column for the current commit as part of
-	 * graph->num_parents.  If the current commit was already in
-	 * graph->columns, then we have double counted it.
-	 */
-	if (is_commit_in_existing_columns)
-		max_cols--;
-
-	/*
-	 * Each column takes up 2 spaces
-	 */
-	graph->width = max_cols * 2;
+	graph->mapping[graph->width] = i;
+	graph->width += 2;
 }
 
 static void graph_update_columns(struct git_graph *graph)
 {
 	struct commit_list *parent;
 	int max_new_columns;
-	int mapping_idx;
 	int i, seen_this, is_commit_in_columns;
 
 	/*
@@ -563,6 +526,8 @@ static void graph_update_columns(struct git_graph *graph)
 	for (i = 0; i < graph->mapping_size; i++)
 		graph->mapping[i] = -1;
 
+	graph->width = 0;
+
 	/*
 	 * Populate graph->new_columns and graph->mapping
 	 *
@@ -573,7 +538,6 @@ static void graph_update_columns(struct git_graph *graph)
 	 * supposed to end up after the collapsing is performed.
 	 */
 	seen_this = 0;
-	mapping_idx = 0;
 	is_commit_in_columns = 1;
 	for (i = 0; i <= graph->num_columns; i++) {
 		struct commit *col_commit;
@@ -587,7 +551,6 @@ static void graph_update_columns(struct git_graph *graph)
 		}
 
 		if (col_commit == graph->commit) {
-			int old_mapping_idx = mapping_idx;
 			seen_this = 1;
 			graph->commit_index = i;
 			for (parent = first_interesting_parent(graph);
@@ -602,21 +565,18 @@ static void graph_update_columns(struct git_graph *graph)
 				    !is_commit_in_columns) {
 					graph_increment_column_color(graph);
 				}
-				graph_insert_into_new_columns(graph,
-							      parent->item,
-							      &mapping_idx);
+				graph_insert_into_new_columns(graph, parent->item);
 			}
 			/*
-			 * We always need to increment mapping_idx by at
+			 * We always need to increment graph->width by at
 			 * least 2, even if it has no interesting parents.
 			 * The current commit always takes up at least 2
 			 * spaces.
 			 */
-			if (mapping_idx == old_mapping_idx)
-				mapping_idx += 2;
+			if (graph->num_parents == 0)
+				graph->width += 2;
 		} else {
-			graph_insert_into_new_columns(graph, col_commit,
-						      &mapping_idx);
+			graph_insert_into_new_columns(graph, col_commit);
 		}
 	}
 
@@ -626,11 +586,6 @@ static void graph_update_columns(struct git_graph *graph)
 	while (graph->mapping_size > 1 &&
 	       graph->mapping[graph->mapping_size - 1] < 0)
 		graph->mapping_size--;
-
-	/*
-	 * Compute graph->width for this commit
-	 */
-	graph_update_width(graph, is_commit_in_columns);
 }
 
 void graph_update(struct git_graph *graph, struct commit *commit)
Signature · 1 line
gitgitgadget
#65 lore raw

PATCH v3 6/13 graph: extract logic for moving to GRAPH_PRE_COMMIT state

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

This computation is repeated in a couple of places and I need to add another condition to it to implement a further improvement to the graph rendering, so I'm extracting this into a function.

---
 graph.c | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)
graph.c +8 -4
index d724ef25c3..bd7403065e 100644
@@ -588,6 +588,12 @@ static void graph_update_columns(struct git_graph *graph)
 		graph->mapping_size--;
 }
 
+static int graph_needs_pre_commit_line(struct git_graph *graph)
+{
+	return graph->num_parents >= 3 &&
+	       graph->commit_index < (graph->num_columns - 1);
+}
+
 void graph_update(struct git_graph *graph, struct commit *commit)
 {
 	struct commit_list *parent;
@@ -643,8 +649,7 @@ void graph_update(struct git_graph *graph, struct commit *commit)
 	 */
 	if (graph->state != GRAPH_PADDING)
 		graph->state = GRAPH_SKIP;
-	else if (graph->num_parents >= 3 &&
-		 graph->commit_index < (graph->num_columns - 1))
+	else if (graph_needs_pre_commit_line(graph))
 		graph->state = GRAPH_PRE_COMMIT;
 	else
 		graph->state = GRAPH_COMMIT;
@@ -714,8 +719,7 @@ static void graph_output_skip_line(struct git_graph *graph, struct graph_line *l
 	 */
 	graph_line_addstr(line, "...");
 
-	if (graph->num_parents >= 3 &&
-	    graph->commit_index < (graph->num_columns - 1))
+	if (graph_needs_pre_commit_line(graph))
 		graph_update_state(graph, GRAPH_PRE_COMMIT);
 	else
 		graph_update_state(graph, GRAPH_COMMIT);
Signature · 1 line
gitgitgadget
#66 lore raw

PATCH v3 7/13 graph: example of graph output that can be simplified

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

The commits following this one introduce a series of improvements to the layout of graphs, tidying up a few edge cases, namely:

- merge whose first parent fuses with an existing column to the left
- merge whose last parent fuses with its immediate neighbor on the right
- edges that collapse to the left above and below a commit line

This test case exemplifies these cases and provides a motivating example of the kind of history I'm aiming to clear up.

The first parent of merge E is the same as the parent of H, so those edges fuse together.

        * H
        |
        | *-.   E
        | |\ \
        |/ / /
        |
        * B

We can "skew" the display of this merge so that it doesn't introduce additional columns that immediately collapse:

        * H
        |
        | *   E
        |/|\
        |
        * B

The last parent of E is D, the same as the parent of F which is the edge to the right of the merge.

            * F
            |
             \
          *-. \   E
          |\ \ \
         / / / /
            | /
            |/
            * D

The two edges leading to D could be fused sooner: rather than expanding the F edge around the merge and then letting the edges collapse, the F edge could fuse with the E edge in the post-merge line:

            * F
            |
             \
          *-. | E
          |\ \|
         / / /
            |
            * D

If this is combined with the "skew" effect above, we get a much cleaner graph display for these edges:

* F
|
* | E
/|\|
|
* D

Finally, the edge leading from C to A appears jagged as it passes through the commit line for B:

        | * | C
        | |/
        * | B
        |/

* A

This can be smoothed out so that such edges are easier to read:

        | * | C
        | |/

* / B
|/
* A

---
 t/t4215-log-skewed-merges.sh | 43 ++++++++++++++++++++++++++++++++++++
 1 file changed, 43 insertions(+)
 create mode 100755 t/t4215-log-skewed-merges.sh
t/t4215-log-skewed-merges.sh new file +43 -0
new file mode 100755
index 0000000000..4582ba066a
@@ -0,0 +1,43 @@
+#!/bin/sh
+
+test_description='git log --graph of skewed merges'
+
+. ./test-lib.sh
+
+test_expect_success 'log --graph with merge fusing with its left and right neighbors' '
+	cat >expect <<-\EOF &&
+	*   H
+	|\
+	| *   G
+	| |\
+	| | * F
+	| | |
+	| |  \
+	| *-. \   E
+	| |\ \ \
+	|/ / / /
+	| | | /
+	| | |/
+	| | * D
+	| * | C
+	| |/
+	* | B
+	|/
+	* A
+	EOF
+
+	git checkout --orphan _p &&
+	test_commit A &&
+	test_commit B &&
+	git checkout -b _q @^ && test_commit C &&
+	git checkout -b _r @^ && test_commit D &&
+	git checkout _p && git merge --no-ff _q _r -m E &&
+	git checkout _r && test_commit F &&
+	git checkout _p && git merge --no-ff _r -m G &&
+	git checkout @^^ && git merge --no-ff _p -m H &&
+
+	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
+	test_cmp expect actual
+'
+
+test_done
Signature · 1 line
gitgitgadget
#67 lore raw

Re: [PATCH v3 07/13] graph: example of graph output that can be simplified

Quoted 23 lines · James Coglan via GitGitGadget
t/t4215-log-skewed-merges.sh new file +17 -0
new file mode 100755
index 0000000000..4582ba066a
@@ -0,0 +1,43 @@
+#!/bin/sh
+
+test_description='git log --graph of skewed merges'
+
+. ./test-lib.sh
+
+test_expect_success 'log --graph with merge fusing with its left and right neighbors' '
+	cat >expect <<-\EOF &&
+	*   H
+	|\
+	| *   G
+	| |\
+	| | * F
+	| | |
+	| |  \
+	| *-. \   E
+	| |\ \ \

t/t4215-log-skewed-merges.sh line 27

Quoted 10 lines
t/t4215-log-skewed-merges.sh new file +10 -0
+	|/ / / /
+	| | | /
+	| | |/
+	| | * D
+	| * | C
+	| |/
+	* | B
+	|/
+	* A
+	EOF

Thanks for adding this test! I really think it helps show some of your improvements later as this test is mutated.

-Stolee

Quoted 17 lines
t/t4215-log-skewed-merges.sh new file +16 -0
+
+	git checkout --orphan _p &&
+	test_commit A &&
+	test_commit B &&
+	git checkout -b _q @^ && test_commit C &&
+	git checkout -b _r @^ && test_commit D &&
+	git checkout _p && git merge --no-ff _q _r -m E &&
+	git checkout _r && test_commit F &&
+	git checkout _p && git merge --no-ff _r -m G &&
+	git checkout @^^ && git merge --no-ff _p -m H &&
+
+	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
+	test_cmp expect actual
+'
+
+test_done
#68 lore raw

Re: [PATCH v3 07/13] graph: example of graph output that can be simplified

In reply to James Coglan via GitGitGadget
Quoted 35 lines · James Coglan via GitGitGadget
t/t4215-log-skewed-merges.sh new file +29 -0
new file mode 100755
index 0000000000..4582ba066a
@@ -0,0 +1,43 @@
+#!/bin/sh
+
+test_description='git log --graph of skewed merges'
+
+. ./test-lib.sh
+
+test_expect_success 'log --graph with merge fusing with its left and right neighbors' '
+	cat >expect <<-\EOF &&
+	*   H
+	|\
+	| *   G
+	| |\
+	| | * F
+	| | |
+	| |  \
+	| *-. \   E
+	| |\ \ \
+	|/ / / /
+	| | | /
+	| | |/
+	| | * D
+	| * | C
+	| |/
+	* | B
+	|/
+	* A
+	EOF
+
+	git checkout --orphan _p &&

t/t4215-log-skewed-merges.sh line 39

Quoted 10 lines
t/t4215-log-skewed-merges.sh new file +10 -0
+	test_commit A &&
+	test_commit B &&
+	git checkout -b _q @^ && test_commit C &&
+	git checkout -b _r @^ && test_commit D &&
+	git checkout _p && git merge --no-ff _q _r -m E &&
+	git checkout _r && test_commit F &&
+	git checkout _p && git merge --no-ff _r -m G &&
+	git checkout @^^ && git merge --no-ff _p -m H &&
+
+	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&

Please don't pipe 'git log --graph's output, but use an intermediate file instead:

  git log --graph ... >out &&
  sed s/// out >actual &&
  test_cmp expect actual

The exit code of a pipeline is the exit code of the last process in the pipeline, and the exit codes of processes upstream of a pipe are ignored. Consequently, if 'git log --graph' produced the expected output but were to fail during housekeeping before exiting (segfault, double free(), whatever), then that failure would go unnoticed.

This applies to several (all?) new tests added in this patch series as well.

I'd like to join the praises from others: this is one excellent first-time submission, thanks.

#69 lore raw

PATCH 1/1 t4215: don't put git commands upstream of pipe

When git commands are placed in the upstream of a pipe, their return codes are lost. In this particular case, it is especially bad since we are testing the intricacies of git log --graph behavior and if we hit an unexpected failure or segfault, we want to know this.

Redirect the output of git commands upstream of pipe into a file and have sed read from that file so that git failures are detected.

---
Thanks for the suggestion, Gábor. (Is that how I should refer to you? I recently learned that some poeple write their names in ALL CAPS as a convention.)

A little late to the party but since this cleanup hasn't been done yet, let's do it now. We can apply this patch to the tip of 'jc/log-graph-simplify'.

 t/t4215-log-skewed-merges.sh | 21 ++++++++++++++-------
 1 file changed, 14 insertions(+), 7 deletions(-)
t/t4215-log-skewed-merges.sh +14 -7
index d33c6438d8..0a2555fb28 100755
@@ -31,7 +31,8 @@ test_expect_success 'log --graph with merge fusing with its left and right neigh
 	git checkout _p && git merge --no-ff _r -m G &&
 	git checkout @^^ && git merge --no-ff _p -m H &&
 
-	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
+	git log --graph --pretty=tformat:%s >actual.raw &&
+	sed "s/ *$//" actual.raw >actual &&
 	test_cmp expect actual
 '
 
@@ -68,7 +69,8 @@ test_expect_success 'log --graph with left-skewed merge' '
 	git checkout 0_p && git merge --no-ff 0_s -m 0_G &&
 	git checkout @^ && git merge --no-ff 0_q 0_r 0_t 0_p -m 0_H &&
 
-	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
+	git log --graph --pretty=tformat:%s >actual.raw &&
+	sed "s/ *$//" actual.raw >actual &&
 	test_cmp expect actual
 '
 
@@ -99,7 +101,8 @@ test_expect_success 'log --graph with nested left-skewed merge' '
 	git checkout 1_p && git merge --no-ff 1_r -m 1_G &&
 	git checkout @^^ && git merge --no-ff 1_p -m 1_H &&
 
-	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
+	git log --graph --pretty=tformat:%s >actual.raw &&
+	sed "s/ *$//" actual.raw >actual &&
 	test_cmp expect actual
 '
 
@@ -139,7 +142,8 @@ test_expect_success 'log --graph with nested left-skewed merge following normal
 	git checkout -b 2_s @^^ && git merge --no-ff 2_q -m 2_J &&
 	git checkout 2_p && git merge --no-ff 2_s -m 2_K &&
 
-	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
+	git log --graph --pretty=tformat:%s >actual.raw &&
+	sed "s/ *$//" actual.raw >actual &&
 	test_cmp expect actual
 '
 
@@ -175,7 +179,8 @@ test_expect_success 'log --graph with nested right-skewed merge following left-s
 	git checkout 3_p && git merge --no-ff 3_r -m 3_H &&
 	git checkout @^^ && git merge --no-ff 3_p -m 3_J &&
 
-	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
+	git log --graph --pretty=tformat:%s >actual.raw &&
+	sed "s/ *$//" actual.raw >actual &&
 	test_cmp expect actual
 '
 
@@ -210,7 +215,8 @@ test_expect_success 'log --graph with right-skewed merge following a left-skewed
 	git merge --no-ff 4_p -m 4_G &&
 	git checkout @^^ && git merge --no-ff 4_s -m 4_H &&
 
-	git log --graph --date-order --pretty=tformat:%s | sed "s/ *$//" >actual &&
+	git log --graph --date-order --pretty=tformat:%s >actual.raw &&
+	sed "s/ *$//" actual.raw >actual &&
 	test_cmp expect actual
 '
 
@@ -250,7 +256,8 @@ test_expect_success 'log --graph with octopus merge with column joining its penu
 	git checkout 5_r &&
 	git merge --no-ff 5_s -m 5_H &&
 
-	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
+	git log --graph --pretty=tformat:%s >actual.raw &&
+	sed "s/ *$//" actual.raw >actual &&
 	test_cmp expect actual
 '
 
Signature · 1 line
2.24.0.300.g722ba42680
#70 lore raw

Re: [PATCH] t4215: don't put git commands upstream of pipe

t/t4215-log-skewed-merges.sh test_expect_success 'log --graph with merge fusing with its left and right neigh · line 35

Quoted 7 lines · Denton Liu

A little late to the party but since this cleanup hasn't been done yet, let's do it now. We can apply this patch to the tip of 'jc/log-graph-simplify'.
...

t/t4215-log-skewed-merges.sh +2 -1
-	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
+	git log --graph --pretty=tformat:%s >actual.raw &&
+	sed "s/ *$//" actual.raw >actual &&

Obviously good but I wonder if

	show_graph () {
		git log --graph --pretty=tformat:%s >actual.raw &&
		sed "s/ *$//" actual.raw &&
		rm actual.raw
	}

would help to make it even more readable without too much repetition.

Will queue; thanks.

Quoted 62 lines
t/t4215-log-skewed-merges.sh +12 -6
 	test_cmp expect actual
 '
@@ -68,7 +69,8 @@ test_expect_success 'log --graph with left-skewed merge' '
 	git checkout 0_p && git merge --no-ff 0_s -m 0_G &&
 	git checkout @^ && git merge --no-ff 0_q 0_r 0_t 0_p -m 0_H &&
 
-	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
+	git log --graph --pretty=tformat:%s >actual.raw &&
+	sed "s/ *$//" actual.raw >actual &&
 	test_cmp expect actual
 '
 
@@ -99,7 +101,8 @@ test_expect_success 'log --graph with nested left-skewed merge' '
 	git checkout 1_p && git merge --no-ff 1_r -m 1_G &&
 	git checkout @^^ && git merge --no-ff 1_p -m 1_H &&
 
-	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
+	git log --graph --pretty=tformat:%s >actual.raw &&
+	sed "s/ *$//" actual.raw >actual &&
 	test_cmp expect actual
 '
 
@@ -139,7 +142,8 @@ test_expect_success 'log --graph with nested left-skewed merge following normal
 	git checkout -b 2_s @^^ && git merge --no-ff 2_q -m 2_J &&
 	git checkout 2_p && git merge --no-ff 2_s -m 2_K &&
 
-	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
+	git log --graph --pretty=tformat:%s >actual.raw &&
+	sed "s/ *$//" actual.raw >actual &&
 	test_cmp expect actual
 '
 
@@ -175,7 +179,8 @@ test_expect_success 'log --graph with nested right-skewed merge following left-s
 	git checkout 3_p && git merge --no-ff 3_r -m 3_H &&
 	git checkout @^^ && git merge --no-ff 3_p -m 3_J &&
 
-	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
+	git log --graph --pretty=tformat:%s >actual.raw &&
+	sed "s/ *$//" actual.raw >actual &&
 	test_cmp expect actual
 '
 
@@ -210,7 +215,8 @@ test_expect_success 'log --graph with right-skewed merge following a left-skewed
 	git merge --no-ff 4_p -m 4_G &&
 	git checkout @^^ && git merge --no-ff 4_s -m 4_H &&
 
-	git log --graph --date-order --pretty=tformat:%s | sed "s/ *$//" >actual &&
+	git log --graph --date-order --pretty=tformat:%s >actual.raw &&
+	sed "s/ *$//" actual.raw >actual &&
 	test_cmp expect actual
 '
 
@@ -250,7 +256,8 @@ test_expect_success 'log --graph with octopus merge with column joining its penu
 	git checkout 5_r &&
 	git merge --no-ff 5_s -m 5_H &&
 
-	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
+	git log --graph --pretty=tformat:%s >actual.raw &&
+	sed "s/ *$//" actual.raw >actual &&
 	test_cmp expect actual
 '
#71 lore raw

Re: [PATCH] t4215: don't put git commands upstream of pipe

Quoted 12 lines · Junio C Hamano

t/t4215-log-skewed-merges.sh test_expect_success 'log --graph with merge fusing with its left and right neigh · line 35

Quoted 7 lines · Denton Liu

A little late to the party but since this cleanup hasn't been done yet, let's do it now. We can apply this patch to the tip of 'jc/log-graph-simplify'.
...

t/t4215-log-skewed-merges.sh +2 -1
-	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
+	git log --graph --pretty=tformat:%s >actual.raw &&
+	sed "s/ *$//" actual.raw >actual &&

Obviously good but I wonder if

Quoted 7 lines
	show_graph () {
		git log --graph --pretty=tformat:%s >actual.raw &&
		sed "s/ *$//" actual.raw &&
		rm actual.raw
	}

would help to make it even more readable without too much repetition.

I think it would indeed, but then let's go one step further, and add that 'test_cmp expect actual' to the function, too, and call it 'check_graph'.

Quoted 64 lines

Will queue; thanks.

Quoted 62 lines
t/t4215-log-skewed-merges.sh +12 -6
 	test_cmp expect actual
 '
@@ -68,7 +69,8 @@ test_expect_success 'log --graph with left-skewed merge' '
 	git checkout 0_p && git merge --no-ff 0_s -m 0_G &&
 	git checkout @^ && git merge --no-ff 0_q 0_r 0_t 0_p -m 0_H &&
 
-	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
+	git log --graph --pretty=tformat:%s >actual.raw &&
+	sed "s/ *$//" actual.raw >actual &&
 	test_cmp expect actual
 '
 
@@ -99,7 +101,8 @@ test_expect_success 'log --graph with nested left-skewed merge' '
 	git checkout 1_p && git merge --no-ff 1_r -m 1_G &&
 	git checkout @^^ && git merge --no-ff 1_p -m 1_H &&
 
-	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
+	git log --graph --pretty=tformat:%s >actual.raw &&
+	sed "s/ *$//" actual.raw >actual &&
 	test_cmp expect actual
 '
 
@@ -139,7 +142,8 @@ test_expect_success 'log --graph with nested left-skewed merge following normal
 	git checkout -b 2_s @^^ && git merge --no-ff 2_q -m 2_J &&
 	git checkout 2_p && git merge --no-ff 2_s -m 2_K &&
 
-	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
+	git log --graph --pretty=tformat:%s >actual.raw &&
+	sed "s/ *$//" actual.raw >actual &&
 	test_cmp expect actual
 '
 
@@ -175,7 +179,8 @@ test_expect_success 'log --graph with nested right-skewed merge following left-s
 	git checkout 3_p && git merge --no-ff 3_r -m 3_H &&
 	git checkout @^^ && git merge --no-ff 3_p -m 3_J &&
 
-	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
+	git log --graph --pretty=tformat:%s >actual.raw &&
+	sed "s/ *$//" actual.raw >actual &&
 	test_cmp expect actual
 '
 
@@ -210,7 +215,8 @@ test_expect_success 'log --graph with right-skewed merge following a left-skewed
 	git merge --no-ff 4_p -m 4_G &&
 	git checkout @^^ && git merge --no-ff 4_s -m 4_H &&
 
-	git log --graph --date-order --pretty=tformat:%s | sed "s/ *$//" >actual &&
+	git log --graph --date-order --pretty=tformat:%s >actual.raw &&
+	sed "s/ *$//" actual.raw >actual &&
 	test_cmp expect actual
 '
 
@@ -250,7 +256,8 @@ test_expect_success 'log --graph with octopus merge with column joining its penu
 	git checkout 5_r &&
 	git merge --no-ff 5_s -m 5_H &&
 
-	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
+	git log --graph --pretty=tformat:%s >actual.raw &&
+	sed "s/ *$//" actual.raw >actual &&
 	test_cmp expect actual
 '
#72 lore raw

PATCH v3 1/1 t4215: use helper function to check output

In reply to Denton Liu

When git commands are placed in the upstream of a pipe, their return codes are lost. In this particular case, it is especially bad since we are testing the intricacies of git log --graph behavior and if we hit an unexpected failure or segfault, we want to know this.

Extract the common output checking logic into check_graph() where we redirect the output of git commands upstream of pipe into a file and have sed read from that file so that git failures are detected.

This patch is best viewed with --color-moved.

---
Thanks for the feedback, everyone. I decided to take Gábor's suggestion and write the check_graph() helper function.

Unfortunately, even though in practice I was moving the here-doc lines down, the diff shows up as me moving the commit and checkout lines up and this might affect how the blame ends up looking. Is there any way to force git to make the diff show up the other way in both this diff and in the blame or is this the best that we can do?

Thanks!

 t/t4215-log-skewed-merges.sh | 208 ++++++++++++++++-------------------
 1 file changed, 97 insertions(+), 111 deletions(-)
t/t4215-log-skewed-merges.sh +97 -111
index d33c6438d8..18709a723e 100755
@@ -4,8 +4,25 @@ test_description='git log --graph of skewed merges'
 
 . ./test-lib.sh
 
+check_graph () {
+	cat >expect &&
+	git log --graph --pretty=tformat:%s "$@" >actual.raw &&
+	sed "s/ *$//" actual.raw >actual &&
+	test_cmp expect actual
+}
+
 test_expect_success 'log --graph with merge fusing with its left and right neighbors' '
-	cat >expect <<-\EOF &&
+	git checkout --orphan _p &&
+	test_commit A &&
+	test_commit B &&
+	git checkout -b _q @^ && test_commit C &&
+	git checkout -b _r @^ && test_commit D &&
+	git checkout _p && git merge --no-ff _q _r -m E &&
+	git checkout _r && test_commit F &&
+	git checkout _p && git merge --no-ff _r -m G &&
+	git checkout @^^ && git merge --no-ff _p -m H &&
+
+	check_graph <<-\EOF
 	*   H
 	|\
 	| *   G
@@ -20,23 +37,20 @@ test_expect_success 'log --graph with merge fusing with its left and right neigh
 	|/
 	* A
 	EOF
-
-	git checkout --orphan _p &&
-	test_commit A &&
-	test_commit B &&
-	git checkout -b _q @^ && test_commit C &&
-	git checkout -b _r @^ && test_commit D &&
-	git checkout _p && git merge --no-ff _q _r -m E &&
-	git checkout _r && test_commit F &&
-	git checkout _p && git merge --no-ff _r -m G &&
-	git checkout @^^ && git merge --no-ff _p -m H &&
-
-	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
-	test_cmp expect actual
 '
 
 test_expect_success 'log --graph with left-skewed merge' '
-	cat >expect <<-\EOF &&
+	git checkout --orphan 0_p && test_commit 0_A &&
+	git checkout -b 0_q 0_p && test_commit 0_B &&
+	git checkout -b 0_r 0_p &&
+	test_commit 0_C &&
+	test_commit 0_D &&
+	git checkout -b 0_s 0_p && test_commit 0_E &&
+	git checkout -b 0_t 0_p && git merge --no-ff 0_r^ 0_s -m 0_F &&
+	git checkout 0_p && git merge --no-ff 0_s -m 0_G &&
+	git checkout @^ && git merge --no-ff 0_q 0_r 0_t 0_p -m 0_H &&
+
+	check_graph <<-\EOF
 	*-----.   0_H
 	|\ \ \ \
 	| | | | * 0_G
@@ -57,23 +71,20 @@ test_expect_success 'log --graph with left-skewed merge' '
 	|/
 	* 0_A
 	EOF
-
-	git checkout --orphan 0_p && test_commit 0_A &&
-	git checkout -b 0_q 0_p && test_commit 0_B &&
-	git checkout -b 0_r 0_p &&
-	test_commit 0_C &&
-	test_commit 0_D &&
-	git checkout -b 0_s 0_p && test_commit 0_E &&
-	git checkout -b 0_t 0_p && git merge --no-ff 0_r^ 0_s -m 0_F &&
-	git checkout 0_p && git merge --no-ff 0_s -m 0_G &&
-	git checkout @^ && git merge --no-ff 0_q 0_r 0_t 0_p -m 0_H &&
-
-	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
-	test_cmp expect actual
 '
 
 test_expect_success 'log --graph with nested left-skewed merge' '
-	cat >expect <<-\EOF &&
+	git checkout --orphan 1_p &&
+	test_commit 1_A &&
+	test_commit 1_B &&
+	test_commit 1_C &&
+	git checkout -b 1_q @^ && test_commit 1_D &&
+	git checkout 1_p && git merge --no-ff 1_q -m 1_E &&
+	git checkout -b 1_r @~3 && test_commit 1_F &&
+	git checkout 1_p && git merge --no-ff 1_r -m 1_G &&
+	git checkout @^^ && git merge --no-ff 1_p -m 1_H &&
+
+	check_graph <<-\EOF
 	*   1_H
 	|\
 	| *   1_G
@@ -88,23 +99,24 @@ test_expect_success 'log --graph with nested left-skewed merge' '
 	|/
 	* 1_A
 	EOF
-
-	git checkout --orphan 1_p &&
-	test_commit 1_A &&
-	test_commit 1_B &&
-	test_commit 1_C &&
-	git checkout -b 1_q @^ && test_commit 1_D &&
-	git checkout 1_p && git merge --no-ff 1_q -m 1_E &&
-	git checkout -b 1_r @~3 && test_commit 1_F &&
-	git checkout 1_p && git merge --no-ff 1_r -m 1_G &&
-	git checkout @^^ && git merge --no-ff 1_p -m 1_H &&
-
-	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
-	test_cmp expect actual
 '
 
 test_expect_success 'log --graph with nested left-skewed merge following normal merge' '
-	cat >expect <<-\EOF &&
+	git checkout --orphan 2_p &&
+	test_commit 2_A &&
+	test_commit 2_B &&
+	test_commit 2_C &&
+	git checkout -b 2_q @^^ &&
+	test_commit 2_D &&
+	test_commit 2_E &&
+	git checkout -b 2_r @^ && test_commit 2_F &&
+	git checkout 2_q &&
+	git merge --no-ff 2_r -m 2_G &&
+	git merge --no-ff 2_p^ -m 2_H &&
+	git checkout -b 2_s @^^ && git merge --no-ff 2_q -m 2_J &&
+	git checkout 2_p && git merge --no-ff 2_s -m 2_K &&
+
+	check_graph <<-\EOF
 	*   2_K
 	|\
 	| *   2_J
@@ -124,27 +136,23 @@ test_expect_success 'log --graph with nested left-skewed merge following normal
 	|/
 	* 2_A
 	EOF
-
-	git checkout --orphan 2_p &&
-	test_commit 2_A &&
-	test_commit 2_B &&
-	test_commit 2_C &&
-	git checkout -b 2_q @^^ &&
-	test_commit 2_D &&
-	test_commit 2_E &&
-	git checkout -b 2_r @^ && test_commit 2_F &&
-	git checkout 2_q &&
-	git merge --no-ff 2_r -m 2_G &&
-	git merge --no-ff 2_p^ -m 2_H &&
-	git checkout -b 2_s @^^ && git merge --no-ff 2_q -m 2_J &&
-	git checkout 2_p && git merge --no-ff 2_s -m 2_K &&
-
-	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
-	test_cmp expect actual
 '
 
 test_expect_success 'log --graph with nested right-skewed merge following left-skewed merge' '
-	cat >expect <<-\EOF &&
+	git checkout --orphan 3_p &&
+	test_commit 3_A &&
+	git checkout -b 3_q &&
+	test_commit 3_B &&
+	test_commit 3_C &&
+	git checkout -b 3_r @^ &&
+	test_commit 3_D &&
+	git checkout 3_q && git merge --no-ff 3_r -m 3_E &&
+	git checkout 3_p && git merge --no-ff 3_q -m 3_F &&
+	git checkout 3_r && test_commit 3_G &&
+	git checkout 3_p && git merge --no-ff 3_r -m 3_H &&
+	git checkout @^^ && git merge --no-ff 3_p -m 3_J &&
+
+	check_graph <<-\EOF
 	*   3_J
 	|\
 	| *   3_H
@@ -161,26 +169,21 @@ test_expect_success 'log --graph with nested right-skewed merge following left-s
 	|/
 	* 3_A
 	EOF
-
-	git checkout --orphan 3_p &&
-	test_commit 3_A &&
-	git checkout -b 3_q &&
-	test_commit 3_B &&
-	test_commit 3_C &&
-	git checkout -b 3_r @^ &&
-	test_commit 3_D &&
-	git checkout 3_q && git merge --no-ff 3_r -m 3_E &&
-	git checkout 3_p && git merge --no-ff 3_q -m 3_F &&
-	git checkout 3_r && test_commit 3_G &&
-	git checkout 3_p && git merge --no-ff 3_r -m 3_H &&
-	git checkout @^^ && git merge --no-ff 3_p -m 3_J &&
-
-	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
-	test_cmp expect actual
 '
 
 test_expect_success 'log --graph with right-skewed merge following a left-skewed one' '
-	cat >expect <<-\EOF &&
+	git checkout --orphan 4_p &&
+	test_commit 4_A &&
+	test_commit 4_B &&
+	test_commit 4_C &&
+	git checkout -b 4_q @^^ && test_commit 4_D &&
+	git checkout -b 4_r 4_p^ && git merge --no-ff 4_q -m 4_E &&
+	git checkout -b 4_s 4_p^^ &&
+	git merge --no-ff 4_r -m 4_F &&
+	git merge --no-ff 4_p -m 4_G &&
+	git checkout @^^ && git merge --no-ff 4_s -m 4_H &&
+
+	check_graph --date-order <<-\EOF
 	*   4_H
 	|\
 	| *   4_G
@@ -198,24 +201,25 @@ test_expect_success 'log --graph with right-skewed merge following a left-skewed
 	|/
 	* 4_A
 	EOF
-
-	git checkout --orphan 4_p &&
-	test_commit 4_A &&
-	test_commit 4_B &&
-	test_commit 4_C &&
-	git checkout -b 4_q @^^ && test_commit 4_D &&
-	git checkout -b 4_r 4_p^ && git merge --no-ff 4_q -m 4_E &&
-	git checkout -b 4_s 4_p^^ &&
-	git merge --no-ff 4_r -m 4_F &&
-	git merge --no-ff 4_p -m 4_G &&
-	git checkout @^^ && git merge --no-ff 4_s -m 4_H &&
-
-	git log --graph --date-order --pretty=tformat:%s | sed "s/ *$//" >actual &&
-	test_cmp expect actual
 '
 
 test_expect_success 'log --graph with octopus merge with column joining its penultimate parent' '
-	cat >expect <<-\EOF &&
+	git checkout --orphan 5_p &&
+	test_commit 5_A &&
+	git branch 5_q &&
+	git branch 5_r &&
+	test_commit 5_B &&
+	git checkout 5_q && test_commit 5_C &&
+	git checkout 5_r && test_commit 5_D &&
+	git checkout 5_p &&
+	git merge --no-ff 5_q 5_r -m 5_E &&
+	git checkout 5_q && test_commit 5_F &&
+	git checkout -b 5_s 5_p^ &&
+	git merge --no-ff 5_p 5_q -m 5_G &&
+	git checkout 5_r &&
+	git merge --no-ff 5_s -m 5_H &&
+
+	check_graph <<-\EOF
 	*   5_H
 	|\
 	| *-.   5_G
@@ -234,24 +238,6 @@ test_expect_success 'log --graph with octopus merge with column joining its penu
 	|/
 	* 5_A
 	EOF
-
-	git checkout --orphan 5_p &&
-	test_commit 5_A &&
-	git branch 5_q &&
-	git branch 5_r &&
-	test_commit 5_B &&
-	git checkout 5_q && test_commit 5_C &&
-	git checkout 5_r && test_commit 5_D &&
-	git checkout 5_p &&
-	git merge --no-ff 5_q 5_r -m 5_E &&
-	git checkout 5_q && test_commit 5_F &&
-	git checkout -b 5_s 5_p^ &&
-	git merge --no-ff 5_p 5_q -m 5_G &&
-	git checkout 5_r &&
-	git merge --no-ff 5_s -m 5_H &&
-
-	git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
-	test_cmp expect actual
 '
 
 test_done
Signature · 1 line
2.24.0.300.g722ba42680
#73 lore raw

Re: [PATCH v3] t4215: use helper function to check output

Quoted 3 lines · Denton Liu

.... Is there any way to
force git to make the diff show up the other way in both this diff and in the blame or is this the best that we can do?

"git blame -M" is your friend, I think.

	$ git blame -b -M HEAD^ t/t4215-log-skewed-merges.sh

would show what this patch did rather nicely.

#74 lore raw

PATCH v3 8/13 graph: tidy up display of left-skewed merges

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

Currently, when we display a merge whose first parent is already present in a column to the left of the merge commit, we display the first parent as a vertical pipe | in the GRAPH_POST_MERGE line and then immediately enter the GRAPH_COLLAPSING state. The first-parent line tracks to the left and all the other parent lines follow it; this creates a "kink" in those lines:

        | *---.
        | |\ \ \
        |/ / / /
        | | | *

This change tidies the display of such commits such that if the first parent appears to the left of the merge, we render it as a / and the second parent as a |. This reduces the horizontal and vertical space needed to render the merge, and makes the resulting lines easier to read.

        | *-.
        |/|\ \
        | | | *

If the first parent is separated from the merge by several columns, a horizontal line is drawn in a similar manner to how the GRAPH_COLLAPSING state displays the line.

        | | | *-.
        | |_|/|\ \
        |/| | | | *

This effect is applied to both "normal" two-parent merges, and to octopus merges. It also reduces the vertical space needed for pre-commit lines, as the merge occupies one less column than usual.

        Before:         After:

        | *             | *
        | |\            | |\
        | | \           | * \
        | |  \          |/|\ \
        | *-. \
        | |\ \ \
---
 graph.c                      | 125 +++++++++++++++++++++++++++--------
 t/t4214-log-graph-octopus.sh |  20 +++---
 t/t4215-log-skewed-merges.sh |  45 +++++++++++--
 3 files changed, 144 insertions(+), 46 deletions(-)
graph.c +97 -28
index bd7403065e..e37127f5ab 100644
@@ -202,6 +202,20 @@ struct git_graph {
 	 * previous commit.
 	 */
 	int prev_commit_index;
+	/*
+	 * Which layout variant to use to display merge commits. If the
+	 * commit's first parent is known to be in a column to the left of the
+	 * merge, then this value is 0 and we use the layout on the left.
+	 * Otherwise, the value is 1 and the layout on the right is used. This
+	 * field tells us how many columns the first parent occupies.
+	 *
+	 * 		0)			1)
+	 *
+	 * 		| | | *-.		| | *---.
+	 * 		| |_|/|\ \		| | |\ \ \
+	 * 		|/| | | | |		| | | | | *
+	 */
+	int merge_layout;
 	/*
 	 * The maximum number of columns that can be stored in the columns
 	 * and new_columns arrays.  This is also half the number of entries
@@ -313,6 +327,7 @@ struct git_graph *graph_init(struct rev_info *opt)
 	graph->prev_state = GRAPH_PADDING;
 	graph->commit_index = 0;
 	graph->prev_commit_index = 0;
+	graph->merge_layout = 0;
 	graph->num_columns = 0;
 	graph->num_new_columns = 0;
 	graph->mapping_size = 0;
@@ -472,9 +487,11 @@ static int graph_find_new_column_by_commit(struct git_graph *graph,
 }
 
 static void graph_insert_into_new_columns(struct git_graph *graph,
-					  struct commit *commit)
+					  struct commit *commit,
+					  int idx)
 {
 	int i = graph_find_new_column_by_commit(graph, commit);
+	int mapping_idx;
 
 	/*
 	 * If the commit is not already in the new_columns array, then add it
@@ -486,8 +503,26 @@ static void graph_insert_into_new_columns(struct git_graph *graph,
 		graph->new_columns[i].color = graph_find_commit_color(graph, commit);
 	}
 
-	graph->mapping[graph->width] = i;
-	graph->width += 2;
+	if (graph->num_parents > 1 && idx > -1 && graph->merge_layout == -1) {
+		/*
+		 * If this is the first parent of a merge, choose a layout for
+		 * the merge line based on whether the parent appears in a
+		 * column to the left of the merge
+		 */
+		int dist, shift;
+
+		dist = idx - i;
+		shift = (dist > 1) ? 2 * dist - 3 : 1;
+
+		graph->merge_layout = (dist > 0) ? 0 : 1;
+		mapping_idx = graph->width + (graph->merge_layout - 1) * shift;
+		graph->width += 2 * graph->merge_layout;
+	} else {
+		mapping_idx = graph->width;
+		graph->width += 2;
+	}
+
+	graph->mapping[mapping_idx] = i;
 }
 
 static void graph_update_columns(struct git_graph *graph)
@@ -553,6 +588,7 @@ static void graph_update_columns(struct git_graph *graph)
 		if (col_commit == graph->commit) {
 			seen_this = 1;
 			graph->commit_index = i;
+			graph->merge_layout = -1;
 			for (parent = first_interesting_parent(graph);
 			     parent;
 			     parent = next_interesting_parent(graph, parent)) {
@@ -565,7 +601,7 @@ static void graph_update_columns(struct git_graph *graph)
 				    !is_commit_in_columns) {
 					graph_increment_column_color(graph);
 				}
-				graph_insert_into_new_columns(graph, parent->item);
+				graph_insert_into_new_columns(graph, parent->item, i);
 			}
 			/*
 			 * We always need to increment graph->width by at
@@ -576,7 +612,7 @@ static void graph_update_columns(struct git_graph *graph)
 			if (graph->num_parents == 0)
 				graph->width += 2;
 		} else {
-			graph_insert_into_new_columns(graph, col_commit);
+			graph_insert_into_new_columns(graph, col_commit, -1);
 		}
 	}
 
@@ -588,10 +624,36 @@ static void graph_update_columns(struct git_graph *graph)
 		graph->mapping_size--;
 }
 
+static int graph_num_expansion_rows(struct git_graph *graph)
+{
+	/*
+	 * Normally, we need two expansion rows for each dashed parent line from
+	 * an octopus merge:
+	 *
+	 * 		| *
+	 * 		| |\
+	 * 		| | \
+	 * 		| |  \
+	 * 		| *-. \
+	 * 		| |\ \ \
+	 *
+	 * If the merge is skewed to the left, then its parents occupy one less
+	 * column, and we don't need as many expansion rows to route around it;
+	 * in some cases that means we don't need any expansion rows at all:
+	 *
+	 * 		| *
+	 * 		| |\
+	 * 		| * \
+	 * 		|/|\ \
+	 */
+	return (graph->num_parents + graph->merge_layout - 3) * 2;
+}
+
 static int graph_needs_pre_commit_line(struct git_graph *graph)
 {
 	return graph->num_parents >= 3 &&
-	       graph->commit_index < (graph->num_columns - 1);
+	       graph->commit_index < (graph->num_columns - 1) &&
+	       graph->expansion_row < graph_num_expansion_rows(graph);
 }
 
 void graph_update(struct git_graph *graph, struct commit *commit)
@@ -728,7 +790,6 @@ static void graph_output_skip_line(struct git_graph *graph, struct graph_line *l
 static void graph_output_pre_commit_line(struct git_graph *graph,
 					 struct graph_line *line)
 {
-	int num_expansion_rows;
 	int i, seen_this;
 
 	/*
@@ -739,14 +800,13 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 	 * We need 2 extra rows for every parent over 2.
 	 */
 	assert(graph->num_parents >= 3);
-	num_expansion_rows = (graph->num_parents - 2) * 2;
 
 	/*
 	 * graph->expansion_row tracks the current expansion row we are on.
 	 * It should be in the range [0, num_expansion_rows - 1]
 	 */
 	assert(0 <= graph->expansion_row &&
-	       graph->expansion_row < num_expansion_rows);
+	       graph->expansion_row < graph_num_expansion_rows(graph));
 
 	/*
 	 * Output the row
@@ -786,7 +846,7 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
 	 * and move to state GRAPH_COMMIT if necessary
 	 */
 	graph->expansion_row++;
-	if (graph->expansion_row >= num_expansion_rows)
+	if (!graph_needs_pre_commit_line(graph))
 		graph_update_state(graph, GRAPH_COMMIT);
 }
 
@@ -824,7 +884,7 @@ static void graph_draw_octopus_merge(struct git_graph *graph, struct graph_line
 	 * x 0 1 2 3
 	 *
 	 */
-	const int dashless_parents = 2;
+	const int dashless_parents = 3 - graph->merge_layout;
 	int dashful_parents = graph->num_parents - dashless_parents;
 
 	/*
@@ -832,9 +892,9 @@ static void graph_draw_octopus_merge(struct git_graph *graph, struct graph_line
 	 * above) but sometimes the first parent goes into an existing column,
 	 * like this:
 	 *
-	 * | *---.
-	 * | |\ \ \
-	 * |/ / / /
+	 * | *-.
+	 * |/|\ \
+	 * | | | |
 	 * x 0 1 2
 	 *
 	 * In which case the number of parents will be one greater than the
@@ -925,10 +985,15 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line
 		graph_update_state(graph, GRAPH_COLLAPSING);
 }
 
+const char merge_chars[] = {'/', '|', '\\'};
+
 static void graph_output_post_merge_line(struct git_graph *graph, struct graph_line *line)
 {
 	int seen_this = 0;
-	int i, j;
+	int i;
+
+	struct commit_list *first_parent = first_interesting_parent(graph);
+	int seen_parent = 0;
 
 	/*
 	 * Output the post-merge row
@@ -951,30 +1016,34 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l
 			 * new_columns and use those to format the
 			 * edges.
 			 */
-			struct commit_list *parents = NULL;
+			struct commit_list *parents = first_parent;
 			int par_column;
+			int idx = graph->merge_layout;
+			char c;
 			seen_this = 1;
-			parents = first_interesting_parent(graph);
-			assert(parents);
-			par_column = graph_find_new_column_by_commit(graph, parents->item);
-			assert(par_column >= 0);
-
-			graph_line_write_column(line, &graph->new_columns[par_column], '|');
-			for (j = 0; j < graph->num_parents - 1; j++) {
-				parents = next_interesting_parent(graph, parents);
-				assert(parents);
+
+			for (; parents; parents = next_interesting_parent(graph, parents)) {
 				par_column = graph_find_new_column_by_commit(graph, parents->item);
 				assert(par_column >= 0);
-				graph_line_write_column(line, &graph->new_columns[par_column], '\\');
-				graph_line_addch(line, ' ');
+
+				c = merge_chars[idx];
+				graph_line_write_column(line, &graph->new_columns[par_column], c);
+				if (idx == 2)
+					graph_line_addch(line, ' ');
+				else
+					idx++;
 			}
 		} else if (seen_this) {
 			graph_line_write_column(line, col, '\\');
 			graph_line_addch(line, ' ');
 		} else {
 			graph_line_write_column(line, col, '|');
-			graph_line_addch(line, ' ');
+			if (graph->merge_layout != 0 || i != graph->commit_index - 1)
+				graph_line_addch(line, seen_parent ? '_' : ' ');
 		}
+
+		if (col_commit == first_parent->item)
+			seen_parent = 1;
 	}
 
 	/*
t/t4214-log-graph-octopus.sh +8 -12
index 3ae8e51e50..1b96276894 100755
@@ -26,9 +26,8 @@ test_expect_success 'set up merge history' '
 test_expect_success 'log --graph with tricky octopus merge, no color' '
 	cat >expect.uncolored <<-\EOF &&
 	* left
-	| *---.   octopus-merge
-	| |\ \ \
-	|/ / / /
+	| *-.   octopus-merge
+	|/|\ \
 	| | | * 4
 	| | * | 3
 	| | |/
@@ -47,9 +46,8 @@ test_expect_success 'log --graph with tricky octopus merge with colors' '
 	test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
 	cat >expect.colors <<-\EOF &&
 	* left
-	<RED>|<RESET> *<BLUE>-<RESET><BLUE>-<RESET><MAGENTA>-<RESET><MAGENTA>.<RESET>   octopus-merge
-	<RED>|<RESET> <RED>|<RESET><YELLOW>\<RESET> <BLUE>\<RESET> <MAGENTA>\<RESET>
-	<RED>|<RESET><RED>/<RESET> <YELLOW>/<RESET> <BLUE>/<RESET> <MAGENTA>/<RESET>
+	<RED>|<RESET> *<MAGENTA>-<RESET><MAGENTA>.<RESET>   octopus-merge
+	<RED>|<RESET><RED>/<RESET><YELLOW>|<RESET><BLUE>\<RESET> <MAGENTA>\<RESET>
 	<RED>|<RESET> <YELLOW>|<RESET> <BLUE>|<RESET> * 4
 	<RED>|<RESET> <YELLOW>|<RESET> * <MAGENTA>|<RESET> 3
 	<RED>|<RESET> <YELLOW>|<RESET> <MAGENTA>|<RESET><MAGENTA>/<RESET>
@@ -147,9 +145,8 @@ test_expect_success 'log --graph with tricky octopus merge and its child, no col
 	cat >expect.uncolored <<-\EOF &&
 	* left
 	| * after-merge
-	| *---.   octopus-merge
-	| |\ \ \
-	|/ / / /
+	| *-.   octopus-merge
+	|/|\ \
 	| | | * 4
 	| | * | 3
 	| | |/
@@ -169,9 +166,8 @@ test_expect_failure 'log --graph with tricky octopus merge and its child with co
 	cat >expect.colors <<-\EOF &&
 	* left
 	<RED>|<RESET> * after-merge
-	<RED>|<RESET> *<MAGENTA>-<RESET><MAGENTA>-<RESET><CYAN>-<RESET><CYAN>.<RESET>   octopus-merge
-	<RED>|<RESET> <RED>|<RESET><BLUE>\<RESET> <MAGENTA>\<RESET> <CYAN>\<RESET>
-	<RED>|<RESET><RED>/<RESET> <BLUE>/<RESET> <MAGENTA>/<RESET> <CYAN>/<RESET>
+	<RED>|<RESET> *<CYAN>-<RESET><CYAN>.<RESET>   octopus-merge
+	<RED>|<RESET><RED>/<RESET><BLUE>|<RESET><MAGENTA>\<RESET> <CYAN>\<RESET>
 	<RED>|<RESET> <BLUE>|<RESET> <MAGENTA>|<RESET> * 4
 	<RED>|<RESET> <BLUE>|<RESET> * <CYAN>|<RESET> 3
 	<RED>|<RESET> <BLUE>|<RESET> <CYAN>|<RESET><CYAN>/<RESET>
t/t4215-log-skewed-merges.sh +39 -6

55 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#75 lore raw

Re: [PATCH v3 08/13] graph: tidy up display of left-skewed merges

Quoted 4 lines · James Coglan via GitGitGadget

This effect is applied to both "normal" two-parent merges, and to octopus merges. It also reduces the vertical space needed for pre-commit lines, as the merge occupies one less column than usual.

Quoted 8 lines
        Before:         After:

        | *             | *
        | |\            | |\
        | | \           | * \
        | |  \          |/|\ \
        | *-. \
        | |\ \ \

Looking at these drawings reminded me of a tangent that is brought up from time to time. We do not do great job when showing multiple roots.

If you have a history like this:

      A---D---E
         /
    B---C

drawing the graph _with_ the merge gives a reasonable representation of the entire topology.

* 46f67dd E
* 6f89516 D
|\
| * e6277a9 C
| * 13ae9b2 B
* afee005 A

But if you start drawing from parents of D (excluding D), you'd get this:

* e6277a9 C
* 13ae9b2 B
* afee005 A

and the fact that B and A do not share parent-child relationships is lost. An easy way to show that would be to draw the bottom three lines of the full history output we saw earlier:

    | * e6277a9 C
    | * 13ae9b2 B
    * afee005 A

either with or without the vertical bar to imply that A may have a child.

This is not something that has to be done as part of this series, but I am hoping that the internal simplification and code restructuring that is done by this series would make it easier to enhance the system to allow such an output.

Thanks.

#76 lore raw

Re: [PATCH v3 08/13] graph: tidy up display of left-skewed merges

Quoted 35 lines · Junio C Hamano
Quoted 4 lines · James Coglan via GitGitGadget

This effect is applied to both "normal" two-parent merges, and to octopus merges. It also reduces the vertical space needed for pre-commit lines, as the merge occupies one less column than usual.

Quoted 8 lines
        Before:         After:

        | *             | *
        | |\            | |\
        | | \           | * \
        | |  \          |/|\ \
        | *-. \
        | |\ \ \

Looking at these drawings reminded me of a tangent that is brought up from time to time. We do not do great job when showing multiple roots.

If you have a history like this:

      A---D---E
         /
    B---C

drawing the graph _with_ the merge gives a reasonable representation of the entire topology.

* 46f67dd E
* 6f89516 D
|\
| * e6277a9 C
| * 13ae9b2 B
* afee005 A

Quoted 6 lines

But if you start drawing from parents of D (excluding D), you'd get this:

* e6277a9 C
* 13ae9b2 B
* afee005 A

I hit this very situation recently when I was experimenting with 'git fast-import' and accidentally created many parallel, independent histories. Running "git log --graph --all --simplify-by-decoration" made it look like all the refs were in a line, but they were not. (The one way I knew something was up: the base commits also appeared without a decoration. That was the only clue that the histories did not continue in a line.)

Quoted 11 lines

and the fact that B and A do not share parent-child relationships is lost. An easy way to show that would be to draw the bottom three lines of the full history output we saw earlier:

    | * e6277a9 C
    | * 13ae9b2 B
    * afee005 A

either with or without the vertical bar to imply that A may have a child.

The natural extension of this would be multiple columns:

 | | | | | *
 | | | | *
 | | | *
 | | *
 | *

*

This does not appear too cumbersome, and such a situation should be rare.

Quoted 4 lines

This is not something that has to be done as part of this series, but I am hoping that the internal simplification and code restructuring that is done by this series would make it easier to enhance the system to allow such an output.

I agree. An excellent idea for a follow-up.

Thanks,
-Stolee

#77 lore raw

Re: [PATCH v3 08/13] graph: tidy up display of left-skewed merges

Quoted 19 lines · Derrick Stolee

I hit this very situation recently when I was experimenting with 'git fast-import' and accidentally created many parallel, independent histories. Running "git log --graph --all --simplify-by-decoration" made it look like all the refs were in a line, but they were not. (The one way I knew something was up: the base commits also appeared without a decoration. That was the only clue that the histories did not continue in a line.)

Quoted 11 lines

and the fact that B and A do not share parent-child relationships is lost. An easy way to show that would be to draw the bottom three lines of the full history output we saw earlier:

    | * e6277a9 C
    | * 13ae9b2 B
    * afee005 A

either with or without the vertical bar to imply that A may have a child.

Quoted 8 lines

The natural extension of this would be multiple columns:

 | | | | | *
 | | | | *
 | | | *
 | | *
 | *

*

After sleeping over it, I now think we shouldn't draw lines that imply a child for each of these commits, as we haven't seen, but I agree that this can be extended to 3 or more roots.

#78 lore raw

PATCH v3 9/13 graph: commit and post-merge lines for left-skewed merges

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

Following the introduction of "left-skewed" merges, which are merges whose first parent fuses with another edge to its left, we have some more edge cases to deal with in the display of commit and post-merge lines.

The current graph code handles the following cases for edges appearing to the right of the commit (*) on commit lines. A 2-way merge is usually followed by vertical lines:

        | | |
        | * |
        | |\ \

An octopus merge (more than two parents) is always followed by edges sloping to the right:

        | |  \          | |    \
        | *-. \         | *---. \
        | |\ \ \        | |\ \ \ \

A 2-way merge is followed by a right-sloping edge if the commit line immediately follows a post-merge line for a commit that appears in the same column as the current commit, or any column to the left of that:

        | *             | * |
        | |\            | |\ \
        | * \           | | * \
        | |\ \          | | |\ \

This commit introduces the following new cases for commit lines. If a 2-way merge skews to the left, then the edges to its right are always vertical lines, even if the commit follows a post-merge line:

        | | |           | |\
        | * |           | * |
        |/| |           |/| |

A commit with 3 parents that skews left is followed by vertical edges:

        | | |
        | * |
        |/|\ \

If a 3-way left-skewed merge commit appears immediately after a post-merge line, then it may be followed the right-sloping edges, just like a 2-way merge that is not skewed.

        | |\
        | * \
        |/|\ \

Octopus merges with 4 or more parents that skew to the left will always be followed by right-sloping edges, because the existing columns need to expand around the merge.

        | |  \
        | *-. \
        |/|\ \ \

On post-merge lines, usually all edges following the current commit slope to the right:

        | * | |
        | |\ \ \

However, if the commit is a left-skewed 2-way merge, the edges to its right remain vertical. We also need to display a space after the vertical line descending from the commit marker, whereas this line would normally be followed by a backslash.

        | * | |
        |/| | |

If a left-skewed merge has more than 2 parents, then the edges to its right are still sloped as they bend around the edges introduced by the merge.

        | * | |
        |/|\ \ \

To handle these new cases, we need to know not just how many parents each commit has, but how many new columns it adds to the display; this quantity is recorded in the edges_added field for the current commit, and prev_edges_added field for the previous commit.

Here, "column" refers to visual columns, not the logical columns of the columns array. This is because even if all the commit's parents end up fusing with existing edges, they initially introduce distinct edges in the commit and post-merge lines before those edges collapse. For example, a 3-way merge whose 2nd and 3rd parents fuse with existing edges still introduces 2 visual columns that affect the display of edges to their right.

        | | |  \
        | | *-. \
        | | |\ \ \
        | |_|/ / /
        |/| | / /
        | | |/ /
        | |/| |
        | | | |

This merge does not introduce any *logical* columns; there are 4 edges before and after this commit once all edges have collapsed. But it does initially introduce 2 new edges that need to be accommodated by the edges to their right.

---
 graph.c                      |  63 +++++++++++++--
 t/t4215-log-skewed-merges.sh | 147 ++++++++++++++++++++++++++++++++++-
 2 files changed, 203 insertions(+), 7 deletions(-)
graph.c +58 -5
index e37127f5ab..21edad8085 100644
@@ -216,6 +216,46 @@ struct git_graph {
 	 * 		|/| | | | |		| | | | | *
 	 */
 	int merge_layout;
+	/*
+	 * The number of columns added to the graph by the current commit. For
+	 * 2-way and octopus merges, this is is usually one less than the
+	 * number of parents:
+	 *
+	 * 		| | |			| |    \
+	 *		| * |			| *---. \
+	 *		| |\ \			| |\ \ \ \
+	 *		| | | |         	| | | | | |
+	 *
+	 *		num_parents: 2		num_parents: 4
+	 *		edges_added: 1		edges_added: 3
+	 *
+	 * For left-skewed merges, the first parent fuses with its neighbor and
+	 * so one less column is added:
+	 *
+	 *		| | |			| |  \
+	 *		| * |			| *-. \
+	 *		|/| |			|/|\ \ \
+	 *		| | |			| | | | |
+	 *
+	 *		num_parents: 2		num_parents: 4
+	 *		edges_added: 0		edges_added: 2
+	 *
+	 * This number determines how edges to the right of the merge are
+	 * displayed in commit and post-merge lines; if no columns have been
+	 * added then a vertical line should be used where a right-tracking
+	 * line would otherwise be used.
+	 *
+	 *		| * \			| * |
+	 *		| |\ \			|/| |
+	 *		| | * \			| * |
+	 */
+	int edges_added;
+	/*
+	 * The number of columns added by the previous commit, which is used to
+	 * smooth edges appearing to the right of a commit in a commit line
+	 * following a post-merge line.
+	 */
+	int prev_edges_added;
 	/*
 	 * The maximum number of columns that can be stored in the columns
 	 * and new_columns arrays.  This is also half the number of entries
@@ -328,6 +368,8 @@ struct git_graph *graph_init(struct rev_info *opt)
 	graph->commit_index = 0;
 	graph->prev_commit_index = 0;
 	graph->merge_layout = 0;
+	graph->edges_added = 0;
+	graph->prev_edges_added = 0;
 	graph->num_columns = 0;
 	graph->num_new_columns = 0;
 	graph->mapping_size = 0;
@@ -689,6 +731,9 @@ void graph_update(struct git_graph *graph, struct commit *commit)
 	 */
 	graph_update_columns(graph);
 
+	graph->prev_edges_added = graph->edges_added;
+	graph->edges_added = graph->num_parents + graph->merge_layout - 2;
+
 	graph->expansion_row = 0;
 
 	/*
@@ -947,12 +992,13 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line
 
 			if (graph->num_parents > 2)
 				graph_draw_octopus_merge(graph, line);
-		} else if (seen_this && (graph->num_parents > 2)) {
+		} else if (seen_this && (graph->edges_added > 1)) {
 			graph_line_write_column(line, col, '\\');
-		} else if (seen_this && (graph->num_parents == 2)) {
+		} else if (seen_this && (graph->edges_added == 1)) {
 			/*
-			 * This is a 2-way merge commit.
-			 * There is no GRAPH_PRE_COMMIT stage for 2-way
+			 * This is either a right-skewed 2-way merge
+			 * commit, or a left-skewed 3-way merge.
+			 * There is no GRAPH_PRE_COMMIT stage for such
 			 * merges, so this is the first line of output
 			 * for this commit.  Check to see what the previous
 			 * line of output was.
@@ -964,6 +1010,7 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line
 			 * makes the output look nicer.
 			 */
 			if (graph->prev_state == GRAPH_POST_MERGE &&
+			    graph->prev_edges_added > 0 &&
 			    graph->prev_commit_index < i)
 				graph_line_write_column(line, col, '\\');
 			else
@@ -1033,8 +1080,14 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l
 				else
 					idx++;
 			}
+			if (graph->edges_added == 0)
+				graph_line_addch(line, ' ');
+
 		} else if (seen_this) {
-			graph_line_write_column(line, col, '\\');
+			if (graph->edges_added > 0)
+				graph_line_write_column(line, col, '\\');
+			else
+				graph_line_write_column(line, col, '|');
 			graph_line_addch(line, ' ');
 		} else {
 			graph_line_write_column(line, col, '|');
t/t4215-log-skewed-merges.sh +145 -2

163 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#79 lore raw

PATCH v3 10/13 graph: rename `new_mapping` to `old_mapping`

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

The change I'm about to make requires being able to inspect the mapping array that was used to render the last GRAPH_COLLAPSING line while rendering a GRAPH_COMMIT line. The new_mapping array currently exists as a pre-allocated space for computing the next mapping array during graph_output_collapsing_line(), but we can repurpose it to let us see the previous mapping state.

To support this use it will make more sense if this array is named old_mapping, as it will contain the mapping data for the previous line we rendered, at the point we're rendering a commit line.

---
 graph.c | 54 +++++++++++++++++++++++++++---------------------------
 1 file changed, 27 insertions(+), 27 deletions(-)
graph.c +27 -27
index 21edad8085..2315f3604d 100644
@@ -259,7 +259,7 @@ struct git_graph {
 	/*
 	 * The maximum number of columns that can be stored in the columns
 	 * and new_columns arrays.  This is also half the number of entries
-	 * that can be stored in the mapping and new_mapping arrays.
+	 * that can be stored in the mapping and old_mapping arrays.
 	 */
 	int column_capacity;
 	/*
@@ -302,7 +302,7 @@ struct git_graph {
 	 * of the git_graph simply so we don't have to allocate a new
 	 * temporary array each time we have to output a collapsing line.
 	 */
-	int *new_mapping;
+	int *old_mapping;
 	/*
 	 * The current default column color being used.  This is
 	 * stored as an index into the array column_colors.
@@ -388,7 +388,7 @@ struct git_graph *graph_init(struct rev_info *opt)
 	ALLOC_ARRAY(graph->columns, graph->column_capacity);
 	ALLOC_ARRAY(graph->new_columns, graph->column_capacity);
 	ALLOC_ARRAY(graph->mapping, 2 * graph->column_capacity);
-	ALLOC_ARRAY(graph->new_mapping, 2 * graph->column_capacity);
+	ALLOC_ARRAY(graph->old_mapping, 2 * graph->column_capacity);
 
 	/*
 	 * The diff output prefix callback, with this we can make
@@ -418,7 +418,7 @@ static void graph_ensure_capacity(struct git_graph *graph, int num_columns)
 	REALLOC_ARRAY(graph->columns, graph->column_capacity);
 	REALLOC_ARRAY(graph->new_columns, graph->column_capacity);
 	REALLOC_ARRAY(graph->mapping, graph->column_capacity * 2);
-	REALLOC_ARRAY(graph->new_mapping, graph->column_capacity * 2);
+	REALLOC_ARRAY(graph->old_mapping, graph->column_capacity * 2);
 }
 
 /*
@@ -1116,13 +1116,18 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
 	int horizontal_edge_target = -1;
 
 	/*
-	 * Clear out the new_mapping array
+	 * Swap the mapping and old_mapping arrays
+	 */
+	SWAP(graph->mapping, graph->old_mapping);
+
+	/*
+	 * Clear out the mapping array
 	 */
 	for (i = 0; i < graph->mapping_size; i++)
-		graph->new_mapping[i] = -1;
+		graph->mapping[i] = -1;
 
 	for (i = 0; i < graph->mapping_size; i++) {
-		int target = graph->mapping[i];
+		int target = graph->old_mapping[i];
 		if (target < 0)
 			continue;
 
@@ -1143,14 +1148,14 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
 			 * This column is already in the
 			 * correct place
 			 */
-			assert(graph->new_mapping[i] == -1);
-			graph->new_mapping[i] = target;
-		} else if (graph->new_mapping[i - 1] < 0) {
+			assert(graph->mapping[i] == -1);
+			graph->mapping[i] = target;
+		} else if (graph->mapping[i - 1] < 0) {
 			/*
 			 * Nothing is to the left.
 			 * Move to the left by one
 			 */
-			graph->new_mapping[i - 1] = target;
+			graph->mapping[i - 1] = target;
 			/*
 			 * If there isn't already an edge moving horizontally
 			 * select this one.
@@ -1166,9 +1171,9 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
 				 * line.
 				 */
 				for (j = (target * 2)+3; j < (i - 2); j += 2)
-					graph->new_mapping[j] = target;
+					graph->mapping[j] = target;
 			}
-		} else if (graph->new_mapping[i - 1] == target) {
+		} else if (graph->mapping[i - 1] == target) {
 			/*
 			 * There is a branch line to our left
 			 * already, and it is our target.  We
@@ -1176,7 +1181,7 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
 			 * the same parent commit.
 			 *
 			 * We don't have to add anything to the
-			 * output or new_mapping, since the
+			 * output or mapping, since the
 			 * existing branch line has already taken
 			 * care of it.
 			 */
@@ -1192,10 +1197,10 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
 			 * The branch to the left of that space
 			 * should be our eventual target.
 			 */
-			assert(graph->new_mapping[i - 1] > target);
-			assert(graph->new_mapping[i - 2] < 0);
-			assert(graph->new_mapping[i - 3] == target);
-			graph->new_mapping[i - 2] = target;
+			assert(graph->mapping[i - 1] > target);
+			assert(graph->mapping[i - 2] < 0);
+			assert(graph->mapping[i - 3] == target);
+			graph->mapping[i - 2] = target;
 			/*
 			 * Mark this branch as the horizontal edge to
 			 * prevent any other edges from moving
@@ -1209,14 +1214,14 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
 	/*
 	 * The new mapping may be 1 smaller than the old mapping
 	 */
-	if (graph->new_mapping[graph->mapping_size - 1] < 0)
+	if (graph->mapping[graph->mapping_size - 1] < 0)
 		graph->mapping_size--;
 
 	/*
 	 * Output out a line based on the new mapping info
 	 */
 	for (i = 0; i < graph->mapping_size; i++) {
-		int target = graph->new_mapping[i];
+		int target = graph->mapping[i];
 		if (target < 0)
 			graph_line_addch(line, ' ');
 		else if (target * 2 == i)
@@ -1229,22 +1234,17 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
 				 * won't continue into the next line.
 				 */
 				if (i != (target * 2)+3)
-					graph->new_mapping[i] = -1;
+					graph->mapping[i] = -1;
 				used_horizontal = 1;
 			graph_line_write_column(line, &graph->new_columns[target], '_');
 		} else {
 			if (used_horizontal && i < horizontal_edge)
-				graph->new_mapping[i] = -1;
+				graph->mapping[i] = -1;
 			graph_line_write_column(line, &graph->new_columns[target], '/');
 
 		}
 	}
 
-	/*
-	 * Swap mapping and new_mapping
-	 */
-	SWAP(graph->mapping, graph->new_mapping);
-
 	/*
 	 * If graph->mapping indicates that all of the branch lines
 	 * are already in the correct positions, we are done.
Signature · 1 line
gitgitgadget
#80 lore raw

PATCH v3 11/13 graph: smooth appearance of collapsing edges on commit lines

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

When a graph contains edges that are in the process of collapsing to the left, but those edges cross a commit line, the effect is that the edges have a jagged appearance:

        *
        |\
        | *
        |  \
        *-. \
        |\ \ \
        | | * |
        | * | |
        | |/ /
        * | |
        |/ /
        * |
        |/
        *

We already takes steps to smooth edges like this when they're expanding; when an edge appears to the right of a merge commit marker on a GRAPH_COMMIT line immediately following a GRAPH_POST_MERGE line, we render it as a \:

        * \
        |\ \
        | * \
        | |\ \

We can make a similar improvement to collapsing edges, making them easier to follow and giving the overall graph a feeling of increased symmetry:

        *
        |\
        | *
        |  \
        *-. \
        |\ \ \
        | | * |
        | * | |
        | |/ /
        * / /
        |/ /
        * /
        |/
        *

To do this, we introduce a new special case for edges on GRAPH_COMMIT lines that immediately follow a GRAPH_COLLAPSING line. By retaining a copy of the mapping array used to render the GRAPH_COLLAPSING line in the old_mapping array, we can determine that an edge is collapsing through the GRAPH_COMMIT line and should be smoothed.

---
 graph.c                                    | 17 +++++++++---
 t/t3430-rebase-merges.sh                   |  2 +-
 t/t4202-log.sh                             |  2 +-
 t/t4214-log-graph-octopus.sh               | 32 +++++++++++-----------
 t/t4215-log-skewed-merges.sh               |  4 +--
 t/t6016-rev-list-graph-simplify-history.sh |  4 +--
 6 files changed, 35 insertions(+), 26 deletions(-)
graph.c +13 -4
index 2315f3604d..63f8d18baa 100644
@@ -297,10 +297,10 @@ struct git_graph {
 	 */
 	int *mapping;
 	/*
-	 * A temporary array for computing the next mapping state
-	 * while we are outputting a mapping line.  This is stored as part
-	 * of the git_graph simply so we don't have to allocate a new
-	 * temporary array each time we have to output a collapsing line.
+	 * A copy of the contents of the mapping array from the last commit,
+	 * which we use to improve the display of columns that are tracking
+	 * from right to left through a commit line.  We also use this to
+	 * avoid allocating a fresh array when we compute the next mapping.
 	 */
 	int *old_mapping;
 	/*
@@ -1015,6 +1015,10 @@ static void graph_output_commit_line(struct git_graph *graph, struct graph_line
 				graph_line_write_column(line, col, '\\');
 			else
 				graph_line_write_column(line, col, '|');
+		} else if (graph->prev_state == GRAPH_COLLAPSING &&
+			   graph->old_mapping[2 * i + 1] == i &&
+			   graph->mapping[2 * i] < i) {
+			graph_line_write_column(line, col, '/');
 		} else {
 			graph_line_write_column(line, col, '|');
 		}
@@ -1211,6 +1215,11 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
 		}
 	}
 
+	/*
+	 * Copy the current mapping array into old_mapping
+	 */
+	COPY_ARRAY(graph->old_mapping, graph->mapping, graph->mapping_size);
+
 	/*
 	 * The new mapping may be 1 smaller than the old mapping
 	 */
t/t3430-rebase-merges.sh +1 -1
index 9efcf4808a..a30d27e9f3 100755
@@ -408,7 +408,7 @@ test_expect_success 'octopus merges' '
 	| | * three
 	| * | two
 	| |/
-	* | one
+	* / one
 	|/
 	o before-octopus
 	EOF
t/t4202-log.sh +1 -1
index e803ba402e..ab0d021365 100755
@@ -667,7 +667,7 @@ cat > expect <<\EOF
 * | | fifth
 * | | fourth
 |/ /
-* | third
+* / third
 |/
 * second
 * initial
t/t4214-log-graph-octopus.sh +16 -16
index 1b96276894..21bc600a82 100755
@@ -31,9 +31,9 @@ test_expect_success 'log --graph with tricky octopus merge, no color' '
 	| | | * 4
 	| | * | 3
 	| | |/
-	| * | 2
+	| * / 2
 	| |/
-	* | 1
+	* / 1
 	|/
 	* initial
 	EOF
@@ -51,9 +51,9 @@ test_expect_success 'log --graph with tricky octopus merge with colors' '
 	<RED>|<RESET> <YELLOW>|<RESET> <BLUE>|<RESET> * 4
 	<RED>|<RESET> <YELLOW>|<RESET> * <MAGENTA>|<RESET> 3
 	<RED>|<RESET> <YELLOW>|<RESET> <MAGENTA>|<RESET><MAGENTA>/<RESET>
-	<RED>|<RESET> * <MAGENTA>|<RESET> 2
+	<RED>|<RESET> * <MAGENTA>/<RESET> 2
 	<RED>|<RESET> <MAGENTA>|<RESET><MAGENTA>/<RESET>
-	* <MAGENTA>|<RESET> 1
+	* <MAGENTA>/<RESET> 1
 	<MAGENTA>|<RESET><MAGENTA>/<RESET>
 	* initial
 	EOF
@@ -72,9 +72,9 @@ test_expect_success 'log --graph with normal octopus merge, no color' '
 	| | | * 4
 	| | * | 3
 	| | |/
-	| * | 2
+	| * / 2
 	| |/
-	* | 1
+	* / 1
 	|/
 	* initial
 	EOF
@@ -90,9 +90,9 @@ test_expect_success 'log --graph with normal octopus merge with colors' '
 	<RED>|<RESET> <GREEN>|<RESET> <YELLOW>|<RESET> * 4
 	<RED>|<RESET> <GREEN>|<RESET> * <BLUE>|<RESET> 3
 	<RED>|<RESET> <GREEN>|<RESET> <BLUE>|<RESET><BLUE>/<RESET>
-	<RED>|<RESET> * <BLUE>|<RESET> 2
+	<RED>|<RESET> * <BLUE>/<RESET> 2
 	<RED>|<RESET> <BLUE>|<RESET><BLUE>/<RESET>
-	* <BLUE>|<RESET> 1
+	* <BLUE>/<RESET> 1
 	<BLUE>|<RESET><BLUE>/<RESET>
 	* initial
 	EOF
@@ -110,9 +110,9 @@ test_expect_success 'log --graph with normal octopus merge and child, no color'
 	| | | * 4
 	| | * | 3
 	| | |/
-	| * | 2
+	| * / 2
 	| |/
-	* | 1
+	* / 1
 	|/
 	* initial
 	EOF
@@ -129,9 +129,9 @@ test_expect_failure 'log --graph with normal octopus and child merge with colors
 	<GREEN>|<RESET> <YELLOW>|<RESET> <BLUE>|<RESET> * 4
 	<GREEN>|<RESET> <YELLOW>|<RESET> * <MAGENTA>|<RESET> 3
 	<GREEN>|<RESET> <YELLOW>|<RESET> <MAGENTA>|<RESET><MAGENTA>/<RESET>
-	<GREEN>|<RESET> * <MAGENTA>|<RESET> 2
+	<GREEN>|<RESET> * <MAGENTA>/<RESET> 2
 	<GREEN>|<RESET> <MAGENTA>|<RESET><MAGENTA>/<RESET>
-	* <MAGENTA>|<RESET> 1
+	* <MAGENTA>/<RESET> 1
 	<MAGENTA>|<RESET><MAGENTA>/<RESET>
 	* initial
 	EOF
@@ -150,9 +150,9 @@ test_expect_success 'log --graph with tricky octopus merge and its child, no col
 	| | | * 4
 	| | * | 3
 	| | |/
-	| * | 2
+	| * / 2
 	| |/
-	* | 1
+	* / 1
 	|/
 	* initial
 	EOF
@@ -171,9 +171,9 @@ test_expect_failure 'log --graph with tricky octopus merge and its child with co
 	<RED>|<RESET> <BLUE>|<RESET> <MAGENTA>|<RESET> * 4
 	<RED>|<RESET> <BLUE>|<RESET> * <CYAN>|<RESET> 3
 	<RED>|<RESET> <BLUE>|<RESET> <CYAN>|<RESET><CYAN>/<RESET>
-	<RED>|<RESET> * <CYAN>|<RESET> 2
+	<RED>|<RESET> * <CYAN>/<RESET> 2
 	<RED>|<RESET> <CYAN>|<RESET><CYAN>/<RESET>
-	* <CYAN>|<RESET> 1
+	* <CYAN>/<RESET> 1
 	<CYAN>|<RESET><CYAN>/<RESET>
 	* initial
 	EOF
t/t4215-log-skewed-merges.sh +2 -2
index e673cdb6f7..1745b3b64c 100755
@@ -17,7 +17,7 @@ test_expect_success 'log --graph with merge fusing with its left and right neigh
 	| | * D
 	| * | C
 	| |/
-	* | B
+	* / B
 	|/
 	* A
 	EOF
@@ -85,7 +85,7 @@ test_expect_success 'log --graph with nested left-skewed merge' '
 	| * | 1_D
 	* | | 1_C
 	|/ /
-	* | 1_B
+	* / 1_B
 	|/
 	* 1_A
 	EOF
t/t6016-rev-list-graph-simplify-history.sh +2 -2
index f7181d1d6a..ca1682f29b 100755
@@ -154,7 +154,7 @@ test_expect_success '--graph --full-history -- bar.txt' '
 	echo "* |   $A4" >> expected &&
 	echo "|\\ \\  " >> expected &&
 	echo "| |/  " >> expected &&
-	echo "* | $A3" >> expected &&
+	echo "* / $A3" >> expected &&
 	echo "|/  " >> expected &&
 	echo "* $A2" >> expected &&
 	git rev-list --graph --full-history --all -- bar.txt > actual &&
@@ -255,7 +255,7 @@ test_expect_success '--graph --boundary ^C3' '
 	echo "* | | | $A3" >> expected &&
 	echo "o | | | $A2" >> expected &&
 	echo "|/ / /  " >> expected &&
-	echo "o | | $A1" >> expected &&
+	echo "o / / $A1" >> expected &&
 	echo " / /  " >> expected &&
 	echo "| o $C3" >> expected &&
 	echo "|/  " >> expected &&
Signature · 1 line
gitgitgadget
#81 lore raw

PATCH v3 12/13 graph: flatten edges that fuse with their right neighbor

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

When a merge commit is printed and its final parent is the same commit that occupies the column to the right of the merge, this results in a kink in the displayed edges:

        * |
        |\ \
        | |/
        | *

Graphs containing these shapes can be hard to read, as the expansion to the right followed immediately by collapsing back to the left creates a lot of zig-zagging edges, especially when many columns are present.

We can improve this by eliminating the zig-zag and having the merge's final parent edge fuse immediately with its neighbor:

        * |
        |\|
        | *

This reduces the horizontal width for the current commit by 2, and requires one less row, making the graph display more compact. Taken in combination with other graph-smoothing enhancements, it greatly compresses the space needed to display certain histories:

*
|\
| * *
| |\ |\
| | * | *
| | | | |\
| | \ | | *
| *-. \ | * |
| |\ \ \ => |/|\|
|/ / / / | | *
| | | / | * |
| | |/ | |/
| | * * /
| * | |/
| |/ *
* |
|/
*

One of the test cases here cannot be correctly rendered in Git v2.23.0; it produces this output following commit E:

        | | *-. \   5_E
        | | |\ \ \
        | |/ / / /
        | | | / _
        | |_|/
        |/| |

The new implementation makes sure that the rightmost edge in this history is not left dangling as above.

---
 graph.c                                    | 34 +++++++++----
 t/t4215-log-skewed-merges.sh               | 56 ++++++++++++++++++----
 t/t6016-rev-list-graph-simplify-history.sh | 30 +++++-------
 3 files changed, 86 insertions(+), 34 deletions(-)
graph.c +26 -8
index 63f8d18baa..80db74aee6 100644
@@ -557,8 +557,24 @@ static void graph_insert_into_new_columns(struct git_graph *graph,
 		shift = (dist > 1) ? 2 * dist - 3 : 1;
 
 		graph->merge_layout = (dist > 0) ? 0 : 1;
+		graph->edges_added = graph->num_parents + graph->merge_layout  - 2;
+
 		mapping_idx = graph->width + (graph->merge_layout - 1) * shift;
 		graph->width += 2 * graph->merge_layout;
+
+	} else if (graph->edges_added > 0 && i == graph->mapping[graph->width - 2]) {
+		/*
+		 * If some columns have been added by a merge, but this commit
+		 * was found in the last existing column, then adjust the
+		 * numbers so that the two edges immediately join, i.e.:
+		 *
+		 *		* |		* |
+		 *		|\ \	=>	|\|
+		 *		| |/		| *
+		 *		| *
+		 */
+		mapping_idx = graph->width - 2;
+		graph->edges_added = -1;
 	} else {
 		mapping_idx = graph->width;
 		graph->width += 2;
@@ -604,6 +620,8 @@ static void graph_update_columns(struct git_graph *graph)
 		graph->mapping[i] = -1;
 
 	graph->width = 0;
+	graph->prev_edges_added = graph->edges_added;
+	graph->edges_added = 0;
 
 	/*
 	 * Populate graph->new_columns and graph->mapping
@@ -731,9 +749,6 @@ void graph_update(struct git_graph *graph, struct commit *commit)
 	 */
 	graph_update_columns(graph);
 
-	graph->prev_edges_added = graph->edges_added;
-	graph->edges_added = graph->num_parents + graph->merge_layout - 2;
-
 	graph->expansion_row = 0;
 
 	/*
@@ -1041,7 +1056,7 @@ const char merge_chars[] = {'/', '|', '\\'};
 static void graph_output_post_merge_line(struct git_graph *graph, struct graph_line *line)
 {
 	int seen_this = 0;
-	int i;
+	int i, j;
 
 	struct commit_list *first_parent = first_interesting_parent(graph);
 	int seen_parent = 0;
@@ -1073,16 +1088,19 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l
 			char c;
 			seen_this = 1;
 
-			for (; parents; parents = next_interesting_parent(graph, parents)) {
+			for (j = 0; j < graph->num_parents; j++) {
 				par_column = graph_find_new_column_by_commit(graph, parents->item);
 				assert(par_column >= 0);
 
 				c = merge_chars[idx];
 				graph_line_write_column(line, &graph->new_columns[par_column], c);
-				if (idx == 2)
-					graph_line_addch(line, ' ');
-				else
+				if (idx == 2) {
+					if (graph->edges_added > 0 || j < graph->num_parents - 1)
+						graph_line_addch(line, ' ');
+				} else {
 					idx++;
+				}
+				parents = next_interesting_parent(graph, parents);
 			}
 			if (graph->edges_added == 0)
 				graph_line_addch(line, ' ');
t/t4215-log-skewed-merges.sh +47 -9

78 lines not on this page · raw message

t/t6016-rev-list-graph-simplify-history.sh +13 -17
index ca1682f29b..f5e6e92f5b 100755
@@ -67,11 +67,10 @@ test_expect_success '--graph --all' '
 	echo "| * $C4" >> expected &&
 	echo "| * $C3" >> expected &&
 	echo "* | $A5" >> expected &&
-	echo "| |     " >> expected &&
-	echo "|  \\    " >> expected &&
-	echo "*-. \\   $A4" >> expected &&
-	echo "|\\ \\ \\  " >> expected &&
-	echo "| | |/  " >> expected &&
+	echo "| |   " >> expected &&
+	echo "|  \\  " >> expected &&
+	echo "*-. | $A4" >> expected &&
+	echo "|\\ \\| " >> expected &&
 	echo "| | * $C2" >> expected &&
 	echo "| | * $C1" >> expected &&
 	echo "| * | $B2" >> expected &&
@@ -97,11 +96,10 @@ test_expect_success '--graph --simplify-by-decoration' '
 	echo "| * $C4" >> expected &&
 	echo "| * $C3" >> expected &&
 	echo "* | $A5" >> expected &&
-	echo "| |     " >> expected &&
-	echo "|  \\    " >> expected &&
-	echo "*-. \\   $A4" >> expected &&
-	echo "|\\ \\ \\  " >> expected &&
-	echo "| | |/  " >> expected &&
+	echo "| |   " >> expected &&
+	echo "|  \\  " >> expected &&
+	echo "*-. | $A4" >> expected &&
+	echo "|\\ \\| " >> expected &&
 	echo "| | * $C2" >> expected &&
 	echo "| | * $C1" >> expected &&
 	echo "| * | $B2" >> expected &&
@@ -131,9 +129,8 @@ test_expect_success '--graph --simplify-by-decoration prune branch B' '
 	echo "| * $C4" >> expected &&
 	echo "| * $C3" >> expected &&
 	echo "* | $A5" >> expected &&
-	echo "* |   $A4" >> expected &&
-	echo "|\\ \\  " >> expected &&
-	echo "| |/  " >> expected &&
+	echo "* | $A4" >> expected &&
+	echo "|\\| " >> expected &&
 	echo "| * $C2" >> expected &&
 	echo "| * $C1" >> expected &&
 	echo "* | $A3" >> expected &&
@@ -151,10 +148,9 @@ test_expect_success '--graph --full-history -- bar.txt' '
 	echo "|\\  " >> expected &&
 	echo "| * $C4" >> expected &&
 	echo "* | $A5" >> expected &&
-	echo "* |   $A4" >> expected &&
-	echo "|\\ \\  " >> expected &&
-	echo "| |/  " >> expected &&
-	echo "* / $A3" >> expected &&
+	echo "* | $A4" >> expected &&
+	echo "|\\| " >> expected &&
+	echo "* | $A3" >> expected &&
 	echo "|/  " >> expected &&
 	echo "* $A2" >> expected &&
 	git rev-list --graph --full-history --all -- bar.txt > actual &&
Signature · 1 line
gitgitgadget
#82 lore raw

PATCH v3 13/13 graph: fix coloring of octopus dashes

In reply to James Coglan via GitGitGadget

From: James Coglan <[email protected]>

In 04005834ed ("log: fix coloring of certain octopus merge shapes", 2018-09-01) there is a fix for the coloring of dashes following an octopus merge. It makes a distinction between the case where all parents introduce a new column, versus the case where the first parent collapses into an existing column:

        | *-.           | *-.
        | |\ \          | |\ \
        | | | |         |/ / /

The latter case means that the columns for the merge parents begin one place to the left in the new_columns array compared to the former case.

However, the implementation only works if the commit's parents are kept in order as they map onto the visual columns, as we get the colors by iterating over new_columns as we print the dashes. In general, the commit's parents can arbitrarily merge with existing columns, and change their ordering in the process.

For example, in the following diagram, the number of each column indicates which commit parent appears in each column.

        | | *---.
        | | |\ \ \
        | | |/ / /
        | |/| | /
        | |_|_|/
        |/| | |
        3 1 0 2

If the columns are colored (red, green, yellow, blue), then the dashes will currently be colored yellow and blue, whereas they should be blue and red.

To fix this, we need to look up each column in the mapping array, which before the GRAPH_COLLAPSING state indicates which logical column is displayed in each visual column. This implementation is simpler as it doesn't have any edge cases, and it also handles how left-skewed first parents are now displayed:

        | *-.
        |/|\ \
        | | | |
        0 1 2 3

The color of the first dashes is always the color found in mapping two columns to the right of the commit symbol. Because commits are displayed after all edges have been collapsed together and the visual columns match the logical ones, we can find the visual offset of the commit symbol using commit_index.

---
 graph.c                      | 71 +++++++++++++++++++-----------------
 t/t4214-log-graph-octopus.sh | 10 ++---
 2 files changed, 42 insertions(+), 39 deletions(-)
graph.c +37 -34
index 80db74aee6..e3fd0ea5f8 100644
@@ -684,6 +684,11 @@ static void graph_update_columns(struct git_graph *graph)
 		graph->mapping_size--;
 }
 
+static int graph_num_dashed_parents(struct git_graph *graph)
+{
+	return graph->num_parents + graph->merge_layout - 3;
+}
+
 static int graph_num_expansion_rows(struct git_graph *graph)
 {
 	/*
@@ -706,7 +711,7 @@ static int graph_num_expansion_rows(struct git_graph *graph)
 	 * 		| * \
 	 * 		|/|\ \
 	 */
-	return (graph->num_parents + graph->merge_layout - 3) * 2;
+	return graph_num_dashed_parents(graph) * 2;
 }
 
 static int graph_needs_pre_commit_line(struct git_graph *graph)
@@ -934,47 +939,45 @@ static void graph_output_commit_char(struct git_graph *graph, struct graph_line
 static void graph_draw_octopus_merge(struct git_graph *graph, struct graph_line *line)
 {
 	/*
-	 * Here dashless_parents represents the number of parents which don't
-	 * need to have dashes (the edges labeled "0" and "1").  And
-	 * dashful_parents are the remaining ones.
+	 * The parents of a merge commit can be arbitrarily reordered as they
+	 * are mapped onto display columns, for example this is a valid merge:
 	 *
-	 * | *---.
-	 * | |\ \ \
-	 * | | | | |
-	 * x 0 1 2 3
+	 *	| | *---.
+	 *	| | |\ \ \
+	 *	| | |/ / /
+	 *	| |/| | /
+	 *	| |_|_|/
+	 *	|/| | |
+	 *	3 1 0 2
 	 *
-	 */
-	const int dashless_parents = 3 - graph->merge_layout;
-	int dashful_parents = graph->num_parents - dashless_parents;
-
-	/*
-	 * Usually, we add one new column for each parent (like the diagram
-	 * above) but sometimes the first parent goes into an existing column,
-	 * like this:
+	 * The numbers denote which parent of the merge each visual column
+	 * corresponds to; we can't assume that the parents will initially
+	 * display in the order given by new_columns.
 	 *
-	 * | *-.
-	 * |/|\ \
-	 * | | | |
-	 * x 0 1 2
+	 * To find the right color for each dash, we need to consult the
+	 * mapping array, starting from the column 2 places to the right of the
+	 * merge commit, and use that to find out which logical column each
+	 * edge will collapse to.
 	 *
-	 * In which case the number of parents will be one greater than the
-	 * number of added columns.
+	 * Commits are rendered once all edges have collapsed to their correct
+	 * logcial column, so commit_index gives us the right visual offset for
+	 * the merge commit.
 	 */
-	int added_cols = (graph->num_new_columns - graph->num_columns);
-	int parent_in_old_cols = graph->num_parents - added_cols;
 
-	/*
-	 * In both cases, commit_index corresponds to the edge labeled "0".
-	 */
-	int first_col = graph->commit_index + dashless_parents
-	    - parent_in_old_cols;
+	int i, j;
+	struct column *col;
 
-	int i;
-	for (i = 0; i < dashful_parents; i++) {
-		graph_line_write_column(line, &graph->new_columns[i+first_col], '-');
-		graph_line_write_column(line, &graph->new_columns[i+first_col],
-					  i == dashful_parents-1 ? '.' : '-');
+	int dashed_parents = graph_num_dashed_parents(graph);
+
+	for (i = 0; i < dashed_parents; i++) {
+		j = graph->mapping[(graph->commit_index + i + 2) * 2];
+		col = &graph->new_columns[j];
+
+		graph_line_write_column(line, col, '-');
+		graph_line_write_column(line, col, (i == dashed_parents - 1) ? '.' : '-');
 	}
+
+	return;
 }
 
 static void graph_output_commit_line(struct git_graph *graph, struct graph_line *line)
t/t4214-log-graph-octopus.sh +5 -5
index 21bc600a82..40d27db674 100755
@@ -121,7 +121,7 @@ test_expect_success 'log --graph with normal octopus merge and child, no color'
 	test_cmp expect.uncolored actual
 '
 
-test_expect_failure 'log --graph with normal octopus and child merge with colors' '
+test_expect_success 'log --graph with normal octopus and child merge with colors' '
 	cat >expect.colors <<-\EOF &&
 	* after-merge
 	*<BLUE>-<RESET><BLUE>-<RESET><MAGENTA>-<RESET><MAGENTA>.<RESET>   octopus-merge
@@ -161,7 +161,7 @@ test_expect_success 'log --graph with tricky octopus merge and its child, no col
 	test_cmp expect.uncolored actual
 '
 
-test_expect_failure 'log --graph with tricky octopus merge and its child with colors' '
+test_expect_success 'log --graph with tricky octopus merge and its child with colors' '
 	test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
 	cat >expect.colors <<-\EOF &&
 	* left
@@ -205,7 +205,7 @@ test_expect_success 'log --graph with crossover in octopus merge, no color' '
 	test_cmp expect.uncolored actual
 '
 
-test_expect_failure 'log --graph with crossover in octopus merge with colors' '
+test_expect_success 'log --graph with crossover in octopus merge with colors' '
 	test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
 	cat >expect.colors <<-\EOF &&
 	* after-4
@@ -253,7 +253,7 @@ test_expect_success 'log --graph with crossover in octopus merge and its child,
 	test_cmp expect.uncolored actual
 '
 
-test_expect_failure 'log --graph with crossover in octopus merge and its child with colors' '
+test_expect_success 'log --graph with crossover in octopus merge and its child with colors' '
 	test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
 	cat >expect.colors <<-\EOF &&
 	* after-4
@@ -349,7 +349,7 @@ test_expect_success 'log --graph with unrelated commit and octopus child, no col
 	test_cmp expect.uncolored actual
 '
 
-test_expect_failure 'log --graph with unrelated commit and octopus child with colors' '
+test_expect_success 'log --graph with unrelated commit and octopus child with colors' '
 	test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
 	cat >expect.colors <<-\EOF &&
 	* after-initial
Signature · 1 line
gitgitgadget