All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH 00/11] Improve the readability of log --graph output
@ 2019-10-10 16:13 James Coglan via GitGitGadget
  2019-10-10 16:13 ` [PATCH 01/11] graph: automatically track visible width of `strbuf` James Coglan via GitGitGadget
                   ` (13 more replies)
  0 siblings, 14 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-10 16:13 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano

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
-- 
gitgitgadget

^ permalink raw reply	[flat|nested] 83+ messages in thread

* [PATCH 01/11] graph: automatically track visible width of `strbuf`
  2019-10-10 16:13 [PATCH 00/11] Improve the readability of log --graph output James Coglan via GitGitGadget
@ 2019-10-10 16:13 ` James Coglan via GitGitGadget
  2019-10-10 21:07   ` Johannes Schindelin
  2019-10-10 16:13 ` [PATCH 02/11] graph: reuse `find_new_column_by_commit()` James Coglan via GitGitGadget
                   ` (12 subsequent siblings)
  13 siblings, 1 reply; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-10 16:13 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c  | 68 ++++++++++++++++++++++----------------------------------
 strbuf.h |  8 ++++++-
 2 files changed, 33 insertions(+), 43 deletions(-)

diff --git a/graph.c b/graph.c
index f53135485f..c56fdec1fc 100644
--- a/graph.c
+++ b/graph.c
@@ -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
diff --git a/strbuf.h b/strbuf.h
index f62278a0be..3a98147321 100644
--- a/strbuf.h
+++ b/strbuf.h
@@ -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


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH 02/11] graph: reuse `find_new_column_by_commit()`
  2019-10-10 16:13 [PATCH 00/11] Improve the readability of log --graph output James Coglan via GitGitGadget
  2019-10-10 16:13 ` [PATCH 01/11] graph: automatically track visible width of `strbuf` James Coglan via GitGitGadget
@ 2019-10-10 16:13 ` James Coglan via GitGitGadget
  2019-10-10 16:13 ` [PATCH 03/11] graph: reduce duplication in `graph_insert_into_new_columns()` James Coglan via GitGitGadget
                   ` (11 subsequent siblings)
  13 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-10 16:13 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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()`.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c | 48 +++++++++++++++++++++++-------------------------
 1 file changed, 23 insertions(+), 25 deletions(-)

diff --git a/graph.c b/graph.c
index c56fdec1fc..a890c9d8bb 100644
--- a/graph.c
+++ b/graph.c
@@ -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) {
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH 03/11] graph: reduce duplication in `graph_insert_into_new_columns()`
  2019-10-10 16:13 [PATCH 00/11] Improve the readability of log --graph output James Coglan via GitGitGadget
  2019-10-10 16:13 ` [PATCH 01/11] graph: automatically track visible width of `strbuf` James Coglan via GitGitGadget
  2019-10-10 16:13 ` [PATCH 02/11] graph: reuse `find_new_column_by_commit()` James Coglan via GitGitGadget
@ 2019-10-10 16:13 ` James Coglan via GitGitGadget
  2019-10-10 16:13 ` [PATCH 04/11] graph: remove `mapping_idx` and `graph_update_width()` James Coglan via GitGitGadget
                   ` (10 subsequent siblings)
  13 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-10 16:13 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c | 20 +++++++-------------
 1 file changed, 7 insertions(+), 13 deletions(-)

diff --git a/graph.c b/graph.c
index a890c9d8bb..1cd30f80e6 100644
--- a/graph.c
+++ b/graph.c
@@ -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,
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH 04/11] graph: remove `mapping_idx` and `graph_update_width()`
  2019-10-10 16:13 [PATCH 00/11] Improve the readability of log --graph output James Coglan via GitGitGadget
                   ` (2 preceding siblings ...)
  2019-10-10 16:13 ` [PATCH 03/11] graph: reduce duplication in `graph_insert_into_new_columns()` James Coglan via GitGitGadget
@ 2019-10-10 16:13 ` James Coglan via GitGitGadget
  2019-10-10 16:13 ` [PATCH 05/11] graph: extract logic for moving to GRAPH_PRE_COMMIT state James Coglan via GitGitGadget
                   ` (9 subsequent siblings)
  13 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-10 16:13 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c | 65 +++++++++------------------------------------------------
 1 file changed, 10 insertions(+), 55 deletions(-)

diff --git a/graph.c b/graph.c
index 1cd30f80e6..a71ae5cd61 100644
--- a/graph.c
+++ b/graph.c
@@ -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)
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH 05/11] graph: extract logic for moving to GRAPH_PRE_COMMIT state
  2019-10-10 16:13 [PATCH 00/11] Improve the readability of log --graph output James Coglan via GitGitGadget
                   ` (3 preceding siblings ...)
  2019-10-10 16:13 ` [PATCH 04/11] graph: remove `mapping_idx` and `graph_update_width()` James Coglan via GitGitGadget
@ 2019-10-10 16:13 ` James Coglan via GitGitGadget
  2019-10-10 16:13 ` [PATCH 06/11] graph: tidy up display of left-skewed merges James Coglan via GitGitGadget
                   ` (8 subsequent siblings)
  13 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-10 16:13 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/graph.c b/graph.c
index a71ae5cd61..98e8777db4 100644
--- a/graph.c
+++ b/graph.c
@@ -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);
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH 06/11] graph: tidy up display of left-skewed merges
  2019-10-10 16:13 [PATCH 00/11] Improve the readability of log --graph output James Coglan via GitGitGadget
                   ` (4 preceding siblings ...)
  2019-10-10 16:13 ` [PATCH 05/11] graph: extract logic for moving to GRAPH_PRE_COMMIT state James Coglan via GitGitGadget
@ 2019-10-10 16:13 ` James Coglan via GitGitGadget
  2019-10-10 17:19   ` Derrick Stolee
  2019-10-10 16:13 ` [PATCH 07/11] graph: commit and post-merge lines for " James Coglan via GitGitGadget
                   ` (7 subsequent siblings)
  13 siblings, 1 reply; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-10 16:13 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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:

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

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 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

diff --git a/graph.c b/graph.c
index 98e8777db4..9136173e03 100644
--- a/graph.c
+++ b/graph.c
@@ -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);
diff --git a/t/t4214-log-graph-octopus.sh b/t/t4214-log-graph-octopus.sh
index dab96c89aa..40f420877b 100755
--- a/t/t4214-log-graph-octopus.sh
+++ b/t/t4214-log-graph-octopus.sh
@@ -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>
diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
new file mode 100755
index 0000000000..cfaba40f97
--- /dev/null
+++ b/t/t4215-log-skewed-merges.sh
@@ -0,0 +1,42 @@
+#!/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
+'
+
+cat > expect <<\EOF
+*---.   F
+|\ \ \
+| | | * E
+| |_|/|
+|/| | |
+| | | * D
+| |_|/
+|/| |
+| | * C
+| |/
+|/|
+| * B
+|/
+* A
+EOF
+
+test_expect_success 'log --graph with left-skewed merge' '
+	git log --graph --pretty=tformat:%s | sed "s/ *$//" > actual &&
+	test_cmp expect actual
+'
+
+test_done
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH 07/11] graph: commit and post-merge lines for left-skewed merges
  2019-10-10 16:13 [PATCH 00/11] Improve the readability of log --graph output James Coglan via GitGitGadget
                   ` (5 preceding siblings ...)
  2019-10-10 16:13 ` [PATCH 06/11] graph: tidy up display of left-skewed merges James Coglan via GitGitGadget
@ 2019-10-10 16:13 ` James Coglan via GitGitGadget
  2019-10-10 17:49   ` Derrick Stolee
  2019-10-10 16:13 ` [PATCH 08/11] graph: rename `new_mapping` to `old_mapping` James Coglan via GitGitGadget
                   ` (6 subsequent siblings)
  13 siblings, 1 reply; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-10 16:13 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c                      |  63 +++++++++++++--
 t/t4215-log-skewed-merges.sh | 151 +++++++++++++++++++++++++++++++++++
 2 files changed, 209 insertions(+), 5 deletions(-)

diff --git a/graph.c b/graph.c
index 9136173e03..fb2e42850f 100644
--- a/graph.c
+++ b/graph.c
@@ -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, '|');
diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
index cfaba40f97..e479d6e676 100755
--- a/t/t4215-log-skewed-merges.sh
+++ b/t/t4215-log-skewed-merges.sh
@@ -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' '
+	git log --graph --pretty=tformat:%s | sed "s/ *$//" > actual &&
+	test_cmp expect actual
+'
+
+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
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH 08/11] graph: rename `new_mapping` to `old_mapping`
  2019-10-10 16:13 [PATCH 00/11] Improve the readability of log --graph output James Coglan via GitGitGadget
                   ` (6 preceding siblings ...)
  2019-10-10 16:13 ` [PATCH 07/11] graph: commit and post-merge lines for " James Coglan via GitGitGadget
@ 2019-10-10 16:13 ` James Coglan via GitGitGadget
  2019-10-10 16:13 ` [PATCH 09/11] graph: smooth appearance of collapsing edges on commit lines James Coglan via GitGitGadget
                   ` (5 subsequent siblings)
  13 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-10 16:13 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c | 54 +++++++++++++++++++++++++++---------------------------
 1 file changed, 27 insertions(+), 27 deletions(-)

diff --git a/graph.c b/graph.c
index fb2e42850f..b0e31e23ca 100644
--- a/graph.c
+++ b/graph.c
@@ -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.
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH 09/11] graph: smooth appearance of collapsing edges on commit lines
  2019-10-10 16:13 [PATCH 00/11] Improve the readability of log --graph output James Coglan via GitGitGadget
                   ` (7 preceding siblings ...)
  2019-10-10 16:13 ` [PATCH 08/11] graph: rename `new_mapping` to `old_mapping` James Coglan via GitGitGadget
@ 2019-10-10 16:13 ` James Coglan via GitGitGadget
  2019-10-10 16:13 ` [PATCH 10/11] graph: flatten edges that join to their right neighbor James Coglan via GitGitGadget
                   ` (4 subsequent siblings)
  13 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-10 16:13 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 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(-)

diff --git a/graph.c b/graph.c
index b0e31e23ca..6391e393ec 100644
--- a/graph.c
+++ b/graph.c
@@ -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
 	 */
diff --git a/t/t3430-rebase-merges.sh b/t/t3430-rebase-merges.sh
index 7b6c4847ad..051351ecdf 100755
--- a/t/t3430-rebase-merges.sh
+++ b/t/t3430-rebase-merges.sh
@@ -402,7 +402,7 @@ test_expect_success 'octopus merges' '
 	| | * three
 	| * | two
 	| |/
-	* | one
+	* / one
 	|/
 	o before-octopus
 	EOF
diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index c20209324c..c7aa0532c1 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -667,7 +667,7 @@ cat > expect <<\EOF
 * | | fifth
 * | | fourth
 |/ /
-* | third
+* / third
 |/
 * second
 * initial
diff --git a/t/t4214-log-graph-octopus.sh b/t/t4214-log-graph-octopus.sh
index 40f420877b..9ada687628 100755
--- a/t/t4214-log-graph-octopus.sh
+++ b/t/t4214-log-graph-octopus.sh
@@ -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
diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
index e479d6e676..b739268e5e 100755
--- a/t/t4215-log-skewed-merges.sh
+++ b/t/t4215-log-skewed-merges.sh
@@ -62,7 +62,7 @@ cat > expect <<\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
index f7181d1d6a..ca1682f29b 100755
--- a/t/t6016-rev-list-graph-simplify-history.sh
+++ b/t/t6016-rev-list-graph-simplify-history.sh
@@ -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 &&
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH 10/11] graph: flatten edges that join to their right neighbor
  2019-10-10 16:13 [PATCH 00/11] Improve the readability of log --graph output James Coglan via GitGitGadget
                   ` (8 preceding siblings ...)
  2019-10-10 16:13 ` [PATCH 09/11] graph: smooth appearance of collapsing edges on commit lines James Coglan via GitGitGadget
@ 2019-10-10 16:13 ` James Coglan via GitGitGadget
  2019-10-10 16:13 ` [PATCH 11/11] graph: fix coloring of octopus dashes James Coglan via GitGitGadget
                   ` (3 subsequent siblings)
  13 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-10 16:13 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 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(-)

diff --git a/graph.c b/graph.c
index 6391e393ec..7dd2fab625 100644
--- a/graph.c
+++ b/graph.c
@@ -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, ' ');
diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
index b739268e5e..1481e6fd80 100755
--- a/t/t4215-log-skewed-merges.sh
+++ b/t/t4215-log-skewed-merges.sh
@@ -137,9 +137,8 @@ cat > expect <<\EOF
 | | * 3_G
 | * | 3_F
 |/| |
-| * |   3_E
-| |\ \
-| | |/
+| * | 3_E
+| |\|
 | | * 3_D
 | * | 3_C
 | |/
@@ -190,4 +189,79 @@ test_expect_success 'log --graph with right-skewed merge following a left-skewed
 	test_cmp expect actual
 '
 
+test_expect_success 'setup octopus merge with column joining its penultimate parent' '
+	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
+'
+
+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
+
+test_expect_success 'log --graph with merge fusing with its left and right neighbors' '
+	git log --graph --pretty=tformat:%s | sed "s/ *$//" > actual &&
+	test_cmp expect actual
+'
+
 test_done
diff --git a/t/t6016-rev-list-graph-simplify-history.sh b/t/t6016-rev-list-graph-simplify-history.sh
index ca1682f29b..f5e6e92f5b 100755
--- a/t/t6016-rev-list-graph-simplify-history.sh
+++ b/t/t6016-rev-list-graph-simplify-history.sh
@@ -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 &&
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH 11/11] graph: fix coloring of octopus dashes
  2019-10-10 16:13 [PATCH 00/11] Improve the readability of log --graph output James Coglan via GitGitGadget
                   ` (9 preceding siblings ...)
  2019-10-10 16:13 ` [PATCH 10/11] graph: flatten edges that join to their right neighbor James Coglan via GitGitGadget
@ 2019-10-10 16:13 ` James Coglan via GitGitGadget
  2019-10-10 18:16   ` Denton Liu
  2019-10-10 17:54 ` [PATCH 00/11] Improve the readability of log --graph output Derrick Stolee
                   ` (2 subsequent siblings)
  13 siblings, 1 reply; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-10 16:13 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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`.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c                      | 71 +++++++++++++++++++-----------------
 t/t4214-log-graph-octopus.sh | 59 ++++++++++++++++++++++++++++--
 2 files changed, 92 insertions(+), 38 deletions(-)

diff --git a/graph.c b/graph.c
index 7dd2fab625..908f257018 100644
--- a/graph.c
+++ b/graph.c
@@ -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)
diff --git a/t/t4214-log-graph-octopus.sh b/t/t4214-log-graph-octopus.sh
index 9ada687628..fbd485d83a 100755
--- a/t/t4214-log-graph-octopus.sh
+++ b/t/t4214-log-graph-octopus.sh
@@ -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
+	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' '
-- 
gitgitgadget

^ permalink raw reply related	[flat|nested] 83+ messages in thread

* Re: [PATCH 06/11] graph: tidy up display of left-skewed merges
  2019-10-10 16:13 ` [PATCH 06/11] graph: tidy up display of left-skewed merges James Coglan via GitGitGadget
@ 2019-10-10 17:19   ` Derrick Stolee
  2019-10-11 16:50     ` James Coglan
  0 siblings, 1 reply; 83+ messages in thread
From: Derrick Stolee @ 2019-10-10 17:19 UTC (permalink / raw)
  To: James Coglan via GitGitGadget, git; +Cc: Junio C Hamano, James Coglan

On 10/10/2019 12:13 PM, James Coglan via GitGitGadget wrote:
> From: James Coglan <jcoglan@gmail.com>
> 
> 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:
> 
>         | *             | *
>         | |\            | |\
>         | | \           | * \
>         | |  \          |/|\ \
>         | *-. \
>         | |\ \ \
> 

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]

> +++ b/t/t4215-log-skewed-merges.sh
> @@ -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




^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH 07/11] graph: commit and post-merge lines for left-skewed merges
  2019-10-10 16:13 ` [PATCH 07/11] graph: commit and post-merge lines for " James Coglan via GitGitGadget
@ 2019-10-10 17:49   ` Derrick Stolee
  2019-10-11 17:04     ` James Coglan
  0 siblings, 1 reply; 83+ messages in thread
From: Derrick Stolee @ 2019-10-10 17:49 UTC (permalink / raw)
  To: James Coglan via GitGitGadget, git; +Cc: Junio C Hamano, James Coglan

On 10/10/2019 12:13 PM, James Coglan via GitGitGadget wrote:
> From: James Coglan <jcoglan@gmail.com>
> 
> 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.
> 
> Signed-off-by: James Coglan <jcoglan@gmail.com>
> ---
>  graph.c                      |  63 +++++++++++++--
>  t/t4215-log-skewed-merges.sh | 151 +++++++++++++++++++++++++++++++++++
>  2 files changed, 209 insertions(+), 5 deletions(-)
> 
> diff --git a/graph.c b/graph.c
> index 9136173e03..fb2e42850f 100644
> --- a/graph.c
> +++ b/graph.c
> @@ -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, '|');
> diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
> index cfaba40f97..e479d6e676 100755
> --- a/t/t4215-log-skewed-merges.sh
> +++ b/t/t4215-log-skewed-merges.sh
> @@ -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' '
> +	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.

> +
> +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
> 


^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH 00/11] Improve the readability of log --graph output
  2019-10-10 16:13 [PATCH 00/11] Improve the readability of log --graph output James Coglan via GitGitGadget
                   ` (10 preceding siblings ...)
  2019-10-10 16:13 ` [PATCH 11/11] graph: fix coloring of octopus dashes James Coglan via GitGitGadget
@ 2019-10-10 17:54 ` Derrick Stolee
  2019-10-13  7:15 ` Jeff King
  2019-10-15 23:40 ` [PATCH v2 00/13] " James Coglan via GitGitGadget
  13 siblings, 0 replies; 83+ messages in thread
From: Derrick Stolee @ 2019-10-10 17:54 UTC (permalink / raw)
  To: James Coglan via GitGitGadget, git; +Cc: Junio C Hamano

On 10/10/2019 12:13 PM, James Coglan via GitGitGadget wrote:
> 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
>     ------                    -----
> 
>     *
>     |\
>     | *                       *
>     | |\                      |\
>     | | *                     | *
>     | | |                     | |\
>     | |  \                    | | *
>     | *-. \                   | * |
>     | |\ \ \                  |/|\|
>     |/ / / /                  | | *
>     | | | /                   | * |
>     | | |/                    | |/
>     | | *                     * /
>     | * |                     |/
>     | |/                      *
>     * |
>     |/
>     *

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

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH 11/11] graph: fix coloring of octopus dashes
  2019-10-10 16:13 ` [PATCH 11/11] graph: fix coloring of octopus dashes James Coglan via GitGitGadget
@ 2019-10-10 18:16   ` Denton Liu
  2019-10-10 18:28     ` Denton Liu
  2019-10-13  7:22     ` Jeff King
  0 siblings, 2 replies; 83+ messages in thread
From: Denton Liu @ 2019-10-10 18:16 UTC (permalink / raw)
  To: James Coglan via GitGitGadget; +Cc: git, Junio C Hamano, James Coglan

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:

[...]

> diff --git a/t/t4214-log-graph-octopus.sh b/t/t4214-log-graph-octopus.sh
> index 9ada687628..fbd485d83a 100755
> --- a/t/t4214-log-graph-octopus.sh
> +++ b/t/t4214-log-graph-octopus.sh
> @@ -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
> +	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:

	Tested-by: Denton Liu <liu.denton@gmail.com>

> -- 
> gitgitgadget

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH 11/11] graph: fix coloring of octopus dashes
  2019-10-10 18:16   ` Denton Liu
@ 2019-10-10 18:28     ` Denton Liu
  2019-10-13  7:22     ` Jeff King
  1 sibling, 0 replies; 83+ messages in thread
From: Denton Liu @ 2019-10-10 18:28 UTC (permalink / raw)
  To: James Coglan via GitGitGadget; +Cc: git, Junio C Hamano, James Coglan

On Thu, Oct 10, 2019 at 11:16:24AM -0700, Denton Liu wrote:
> 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:

diff --git a/t/t4214-log-graph-octopus.sh b/t/t4214-log-graph-octopus.sh
index 3ae8e51e50..40d27db674 100755
--- a/t/t4214-log-graph-octopus.sh
+++ b/t/t4214-log-graph-octopus.sh
@@ -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

^ permalink raw reply related	[flat|nested] 83+ messages in thread

* Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`
  2019-10-10 16:13 ` [PATCH 01/11] graph: automatically track visible width of `strbuf` James Coglan via GitGitGadget
@ 2019-10-10 21:07   ` Johannes Schindelin
  2019-10-10 23:05     ` Denton Liu
                       ` (2 more replies)
  0 siblings, 3 replies; 83+ messages in thread
From: Johannes Schindelin @ 2019-10-10 21:07 UTC (permalink / raw)
  To: James Coglan via GitGitGadget; +Cc: git, Junio C Hamano, James Coglan

Hi James,

On Thu, 10 Oct 2019, James Coglan via GitGitGadget wrote:

> From: James Coglan <jcoglan@gmail.com>
>
> 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.

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

> 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.
>
> Signed-off-by: James Coglan <jcoglan@gmail.com>
> ---
>  graph.c  | 68 ++++++++++++++++++++++----------------------------------
>  strbuf.h |  8 ++++++-
>  2 files changed, 33 insertions(+), 43 deletions(-)
>
> diff --git a/graph.c b/graph.c
> index f53135485f..c56fdec1fc 100644
> --- a/graph.c
> +++ b/graph.c
> @@ -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
> diff --git a/strbuf.h b/strbuf.h
> index f62278a0be..3a98147321 100644
> --- a/strbuf.h
> +++ b/strbuf.h
> @@ -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
>
>

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`
  2019-10-10 21:07   ` Johannes Schindelin
@ 2019-10-10 23:05     ` Denton Liu
  2019-10-11  0:49       ` Derrick Stolee
  2019-10-11  1:42       ` Junio C Hamano
  2019-10-11  1:40     ` Junio C Hamano
  2019-10-11 17:08     ` James Coglan
  2 siblings, 2 replies; 83+ messages in thread
From: Denton Liu @ 2019-10-10 23:05 UTC (permalink / raw)
  To: Johannes Schindelin
  Cc: James Coglan via GitGitGadget, git, Junio C Hamano, James Coglan

Hi Dscho,

On Thu, Oct 10, 2019 at 11:07:35PM +0200, Johannes Schindelin wrote:
> Hi James,
> 
> On Thu, 10 Oct 2019, James Coglan via GitGitGadget wrote:
> 
> > From: James Coglan <jcoglan@gmail.com>
> >
> > 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.
> 
> 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.

> 
> 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

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`
  2019-10-10 23:05     ` Denton Liu
@ 2019-10-11  0:49       ` Derrick Stolee
  2019-10-11  1:42       ` Junio C Hamano
  1 sibling, 0 replies; 83+ messages in thread
From: Derrick Stolee @ 2019-10-11  0:49 UTC (permalink / raw)
  To: Denton Liu, Johannes Schindelin
  Cc: James Coglan via GitGitGadget, git, Junio C Hamano, James Coglan



On 10/10/2019 7:05 PM, Denton Liu wrote:
> Hi Dscho,
> 
> On Thu, Oct 10, 2019 at 11:07:35PM +0200, Johannes Schindelin wrote:
>> Hi James,
>>
>> On Thu, 10 Oct 2019, James Coglan via GitGitGadget wrote:
>>
>>> From: James Coglan <jcoglan@gmail.com>
>>>
>>> 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.
>>
>> 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.

>> 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.

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

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`
  2019-10-10 21:07   ` Johannes Schindelin
  2019-10-10 23:05     ` Denton Liu
@ 2019-10-11  1:40     ` Junio C Hamano
  2019-10-11 17:08     ` James Coglan
  2 siblings, 0 replies; 83+ messages in thread
From: Junio C Hamano @ 2019-10-11  1:40 UTC (permalink / raw)
  To: Johannes Schindelin; +Cc: James Coglan via GitGitGadget, git, James Coglan

Johannes Schindelin <Johannes.Schindelin@gmx.de> writes:

> 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.








^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`
  2019-10-10 23:05     ` Denton Liu
  2019-10-11  0:49       ` Derrick Stolee
@ 2019-10-11  1:42       ` Junio C Hamano
  2019-10-11  5:01         ` Denton Liu
  1 sibling, 1 reply; 83+ messages in thread
From: Junio C Hamano @ 2019-10-11  1:42 UTC (permalink / raw)
  To: Denton Liu
  Cc: Johannes Schindelin, James Coglan via GitGitGadget, git, James Coglan

Denton Liu <liu.denton@gmail.com> writes:

> 	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;
> 		}
> 	}

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.


^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`
  2019-10-11  1:42       ` Junio C Hamano
@ 2019-10-11  5:01         ` Denton Liu
  2019-10-11 16:02           ` Johannes Schindelin
  0 siblings, 1 reply; 83+ messages in thread
From: Denton Liu @ 2019-10-11  5:01 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Johannes Schindelin, James Coglan via GitGitGadget, git, James Coglan

On Fri, Oct 11, 2019 at 10:42:20AM +0900, Junio C Hamano wrote:
> Denton Liu <liu.denton@gmail.com> writes:
> 
> > 	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;
> > 		}
> > 	}
> 
> 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);

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`
  2019-10-11  5:01         ` Denton Liu
@ 2019-10-11 16:02           ` Johannes Schindelin
  2019-10-11 17:20             ` James Coglan
  0 siblings, 1 reply; 83+ messages in thread
From: Johannes Schindelin @ 2019-10-11 16:02 UTC (permalink / raw)
  To: Denton Liu
  Cc: Junio C Hamano, James Coglan via GitGitGadget, git, James Coglan

Hi,

On Thu, 10 Oct 2019, Denton Liu wrote:

> On Fri, Oct 11, 2019 at 10:42:20AM +0900, Junio C Hamano wrote:
> > Denton Liu <liu.denton@gmail.com> writes:
> >
> > > 	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;
> > > 		}
> > > 	}
> >
> > 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);

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

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH 06/11] graph: tidy up display of left-skewed merges
  2019-10-10 17:19   ` Derrick Stolee
@ 2019-10-11 16:50     ` James Coglan
  2019-10-12  1:36       ` Derrick Stolee
  0 siblings, 1 reply; 83+ messages in thread
From: James Coglan @ 2019-10-11 16:50 UTC (permalink / raw)
  To: Derrick Stolee, James Coglan via GitGitGadget, git; +Cc: Junio C Hamano

On 10/10/2019 18:19, Derrick Stolee wrote:
> On 10/10/2019 12:13 PM, James Coglan via GitGitGadget wrote:
>> +++ b/t/t4215-log-skewed-merges.sh
>> @@ -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

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.

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH 07/11] graph: commit and post-merge lines for left-skewed merges
  2019-10-10 17:49   ` Derrick Stolee
@ 2019-10-11 17:04     ` James Coglan
  2019-10-13  6:56       ` Jeff King
  0 siblings, 1 reply; 83+ messages in thread
From: James Coglan @ 2019-10-11 17:04 UTC (permalink / raw)
  To: Derrick Stolee, James Coglan via GitGitGadget, git; +Cc: Junio C Hamano

On 10/10/2019 18:49, Derrick Stolee wrote:
> On 10/10/2019 12:13 PM, James Coglan via GitGitGadget wrote:
>> From: James Coglan <jcoglan@gmail.com>
>>
>> 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.
>>
>> Signed-off-by: James Coglan <jcoglan@gmail.com>
>> ---
>>  graph.c                      |  63 +++++++++++++--
>>  t/t4215-log-skewed-merges.sh | 151 +++++++++++++++++++++++++++++++++++
>>  2 files changed, 209 insertions(+), 5 deletions(-)
>>
>> diff --git a/graph.c b/graph.c
>> index 9136173e03..fb2e42850f 100644
>> --- a/graph.c
>> +++ b/graph.c
>> @@ -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, '|');
>> diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
>> index cfaba40f97..e479d6e676 100755
>> --- a/t/t4215-log-skewed-merges.sh
>> +++ b/t/t4215-log-skewed-merges.sh
>> @@ -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' '
>> +	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.

>> +
>> +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
>>
> 

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`
  2019-10-10 21:07   ` Johannes Schindelin
  2019-10-10 23:05     ` Denton Liu
  2019-10-11  1:40     ` Junio C Hamano
@ 2019-10-11 17:08     ` James Coglan
  2 siblings, 0 replies; 83+ messages in thread
From: James Coglan @ 2019-10-11 17:08 UTC (permalink / raw)
  To: Johannes Schindelin, James Coglan via GitGitGadget; +Cc: git, Junio C Hamano

Hi Johannes,

On 10/10/2019 22:07, Johannes Schindelin wrote:
> Hi James,
> 
> On Thu, 10 Oct 2019, James Coglan via GitGitGadget wrote:
> 
>> From: James Coglan <jcoglan@gmail.com>
>>
>> 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.
> 
> 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`.
 
> 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.

> 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.
> 
>> 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.
>>
>> Signed-off-by: James Coglan <jcoglan@gmail.com>
>> ---
>>  graph.c  | 68 ++++++++++++++++++++++----------------------------------
>>  strbuf.h |  8 ++++++-
>>  2 files changed, 33 insertions(+), 43 deletions(-)
>>
>> diff --git a/graph.c b/graph.c
>> index f53135485f..c56fdec1fc 100644
>> --- a/graph.c
>> +++ b/graph.c
>> @@ -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
>> diff --git a/strbuf.h b/strbuf.h
>> index f62278a0be..3a98147321 100644
>> --- a/strbuf.h
>> +++ b/strbuf.h
>> @@ -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
>>
>>

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`
  2019-10-11 16:02           ` Johannes Schindelin
@ 2019-10-11 17:20             ` James Coglan
  2019-10-12  0:27               ` Junio C Hamano
  0 siblings, 1 reply; 83+ messages in thread
From: James Coglan @ 2019-10-11 17:20 UTC (permalink / raw)
  To: Johannes Schindelin, Denton Liu
  Cc: Junio C Hamano, James Coglan via GitGitGadget, git

On 11/10/2019 17:02, Johannes Schindelin wrote:
> Hi,
> 
> On Thu, 10 Oct 2019, Denton Liu wrote:
> 
>> On Fri, Oct 11, 2019 at 10:42:20AM +0900, Junio C Hamano wrote:
>>> Denton Liu <liu.denton@gmail.com> writes:
>>>
>>>> 	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;
>>>> 		}
>>>> 	}
>>>
>>> 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);
> 
> 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.

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`:

diff --git a/graph.c b/graph.c
index f53135485f..24bf1f4fe1 100644
--- a/graph.c
+++ b/graph.c
@@ -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



^ permalink raw reply related	[flat|nested] 83+ messages in thread

* Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`
  2019-10-11 17:20             ` James Coglan
@ 2019-10-12  0:27               ` Junio C Hamano
  2019-10-12 16:22                 ` Johannes Schindelin
  2019-10-14 12:55                 ` James Coglan
  0 siblings, 2 replies; 83+ messages in thread
From: Junio C Hamano @ 2019-10-12  0:27 UTC (permalink / raw)
  To: James Coglan
  Cc: Johannes Schindelin, Denton Liu, James Coglan via GitGitGadget, git

James Coglan <jcoglan@gmail.com> writes:

> - 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").

> - 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.

> - 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.

> To this end I've prepared a different implementation that
> introduces the indirection described above, and does not modify
> `strbuf`:
>
> +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?

> +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);
> +}

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.

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH 06/11] graph: tidy up display of left-skewed merges
  2019-10-11 16:50     ` James Coglan
@ 2019-10-12  1:36       ` Derrick Stolee
  2019-10-14 13:11         ` James Coglan
  0 siblings, 1 reply; 83+ messages in thread
From: Derrick Stolee @ 2019-10-12  1:36 UTC (permalink / raw)
  To: James Coglan, James Coglan via GitGitGadget, git; +Cc: Junio C Hamano

On 10/11/2019 12:50 PM, James Coglan wrote:
> On 10/10/2019 18:19, Derrick Stolee wrote:
>> On 10/10/2019 12:13 PM, James Coglan via GitGitGadget wrote:
>>> +++ b/t/t4215-log-skewed-merges.sh
>>> @@ -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
> 
> 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.

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

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`
  2019-10-12  0:27               ` Junio C Hamano
@ 2019-10-12 16:22                 ` Johannes Schindelin
  2019-10-14 12:55                 ` James Coglan
  1 sibling, 0 replies; 83+ messages in thread
From: Johannes Schindelin @ 2019-10-12 16:22 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: James Coglan, Denton Liu, James Coglan via GitGitGadget, git

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...

On Sat, 12 Oct 2019, Junio C Hamano wrote:

> James Coglan <jcoglan@gmail.com> writes:
>
> > - 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

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH 07/11] graph: commit and post-merge lines for left-skewed merges
  2019-10-11 17:04     ` James Coglan
@ 2019-10-13  6:56       ` Jeff King
  2019-10-14 15:38         ` James Coglan
  0 siblings, 1 reply; 83+ messages in thread
From: Jeff King @ 2019-10-13  6:56 UTC (permalink / raw)
  To: James Coglan
  Cc: Derrick Stolee, James Coglan via GitGitGadget, git, Junio C Hamano

On Fri, Oct 11, 2019 at 06:04:21PM +0100, James Coglan wrote:

> > 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:

> >> +cat > expect <<\EOF

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

-Peff

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH 00/11] Improve the readability of log --graph output
  2019-10-10 16:13 [PATCH 00/11] Improve the readability of log --graph output James Coglan via GitGitGadget
                   ` (11 preceding siblings ...)
  2019-10-10 17:54 ` [PATCH 00/11] Improve the readability of log --graph output Derrick Stolee
@ 2019-10-13  7:15 ` Jeff King
  2019-10-14 15:49   ` James Coglan
  2019-10-15 23:40 ` [PATCH v2 00/13] " James Coglan via GitGitGadget
  13 siblings, 1 reply; 83+ messages in thread
From: Jeff King @ 2019-10-13  7:15 UTC (permalink / raw)
  To: James Coglan via GitGitGadget; +Cc: James Coglan, git, Junio C Hamano

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

> 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.

> 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
>     ------                    -----
> 
>     *
>     |\
>     | *                       *
>     | |\                      |\
>     | | *                     | *
>     | | |                     | |\
>     | |  \                    | | *
>     | *-. \                   | * |
>     | |\ \ \                  |/|\|
>     |/ / / /                  | | *
>     | | | /                   | * |
>     | | |/                    | |/
>     | | *                     * /
>     | * |                     |/
>     | |/                      *
>     * |
>     |/
>     *

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

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH 11/11] graph: fix coloring of octopus dashes
  2019-10-10 18:16   ` Denton Liu
  2019-10-10 18:28     ` Denton Liu
@ 2019-10-13  7:22     ` Jeff King
  1 sibling, 0 replies; 83+ messages in thread
From: Jeff King @ 2019-10-13  7:22 UTC (permalink / raw)
  To: Denton Liu
  Cc: James Coglan via GitGitGadget, git, Junio C Hamano, James Coglan

On Thu, Oct 10, 2019 at 11:16:24AM -0700, Denton Liu wrote:

> >  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/20190926202326.GA16664@sigill.intra.peff.net/

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

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`
  2019-10-12  0:27               ` Junio C Hamano
  2019-10-12 16:22                 ` Johannes Schindelin
@ 2019-10-14 12:55                 ` James Coglan
  2019-10-14 13:01                   ` James Coglan
  2019-10-16  2:15                   ` Junio C Hamano
  1 sibling, 2 replies; 83+ messages in thread
From: James Coglan @ 2019-10-14 12:55 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Johannes Schindelin, Denton Liu, James Coglan via GitGitGadget, git

On 12/10/2019 01:27, Junio C Hamano wrote:
> James Coglan <jcoglan@gmail.com> writes:
> 
>> - 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").

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.

>> - 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.
> 
>> - 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 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);

>> To this end I've prepared a different implementation that
>> introduces the indirection described above, and does not modify
>> `strbuf`:
>>
>> +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?

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.

>> +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);
>> +}
> 
> 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.

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.

> 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 <jcoglan@gmail.com>
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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c | 194 +++++++++++++++++++++++++++++---------------------------
 1 file changed, 99 insertions(+), 95 deletions(-)

diff --git a/graph.c b/graph.c
index f53135485f..2f81a5d23d 100644
--- a/graph.c
+++ b/graph.c
@@ -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
-- 
2.23.0


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`
  2019-10-14 12:55                 ` James Coglan
@ 2019-10-14 13:01                   ` James Coglan
  2019-10-16  2:15                   ` Junio C Hamano
  1 sibling, 0 replies; 83+ messages in thread
From: James Coglan @ 2019-10-14 13:01 UTC (permalink / raw)
  To: Junio C Hamano
  Cc: Johannes Schindelin, Denton Liu, James Coglan via GitGitGadget, git

On 14/10/2019 13:55, James Coglan wrote:
> 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 <jcoglan@gmail.com>
> 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.
> 
> Signed-off-by: James Coglan <jcoglan@gmail.com>
> ---
>  graph.c | 194 +++++++++++++++++++++++++++++---------------------------
>  1 file changed, 99 insertions(+), 95 deletions(-)
> 
> diff --git a/graph.c b/graph.c
> index f53135485f..2f81a5d23d 100644
> --- a/graph.c
> +++ b/graph.c
> @@ -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

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 <jcoglan@gmail.com>
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(-)

diff --git a/graph.c b/graph.c
index 2f81a5d23d..4c68557b17 100644
--- a/graph.c
+++ b/graph.c
@@ -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)
-- 
2.23.0



^ permalink raw reply related	[flat|nested] 83+ messages in thread

* Re: [PATCH 06/11] graph: tidy up display of left-skewed merges
  2019-10-12  1:36       ` Derrick Stolee
@ 2019-10-14 13:11         ` James Coglan
  0 siblings, 0 replies; 83+ messages in thread
From: James Coglan @ 2019-10-14 13:11 UTC (permalink / raw)
  To: Derrick Stolee, James Coglan via GitGitGadget, git; +Cc: Junio C Hamano

On 12/10/2019 02:36, Derrick Stolee wrote:
> On 10/11/2019 12:50 PM, James Coglan wrote:
>> On 10/10/2019 18:19, Derrick Stolee wrote:
>>> On 10/10/2019 12:13 PM, James Coglan via GitGitGadget wrote:
>>>> +++ b/t/t4215-log-skewed-merges.sh
>>>> @@ -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
>>
>> 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.
> 
> 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

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH 07/11] graph: commit and post-merge lines for left-skewed merges
  2019-10-13  6:56       ` Jeff King
@ 2019-10-14 15:38         ` James Coglan
  2019-10-14 17:41           ` Derrick Stolee
  2019-10-14 20:42           ` Johannes Schindelin
  0 siblings, 2 replies; 83+ messages in thread
From: James Coglan @ 2019-10-14 15:38 UTC (permalink / raw)
  To: Jeff King
  Cc: Derrick Stolee, James Coglan via GitGitGadget, git, Junio C Hamano

On 13/10/2019 07:56, Jeff King wrote:
> On Fri, Oct 11, 2019 at 06:04:21PM +0100, James Coglan wrote:
> 
>>> 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:
> 
>>>> +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?

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH 00/11] Improve the readability of log --graph output
  2019-10-13  7:15 ` Jeff King
@ 2019-10-14 15:49   ` James Coglan
  0 siblings, 0 replies; 83+ messages in thread
From: James Coglan @ 2019-10-14 15:49 UTC (permalink / raw)
  To: Jeff King, James Coglan via GitGitGadget; +Cc: git, Junio C Hamano

On 13/10/2019 08:15, Jeff King wrote:
> On Thu, Oct 10, 2019 at 09:13:42AM -0700, James Coglan via GitGitGadget wrote:
> 
>> 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.
> 
>> 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
>>     ------                    -----
>>
>>     *
>>     |\
>>     | *                       *
>>     | |\                      |\
>>     | | *                     | *
>>     | | |                     | |\
>>     | |  \                    | | *
>>     | *-. \                   | * |
>>     | |\ \ \                  |/|\|
>>     |/ / / /                  | | *
>>     | | | /                   | * |
>>     | | |/                    | |/
>>     | | *                     * /
>>     | * |                     |/
>>     | |/                      *
>>     * |
>>     |/
>>     *
> 
> 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.

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH 07/11] graph: commit and post-merge lines for left-skewed merges
  2019-10-14 15:38         ` James Coglan
@ 2019-10-14 17:41           ` Derrick Stolee
  2019-10-14 20:42           ` Johannes Schindelin
  1 sibling, 0 replies; 83+ messages in thread
From: Derrick Stolee @ 2019-10-14 17:41 UTC (permalink / raw)
  To: James Coglan, Jeff King
  Cc: James Coglan via GitGitGadget, git, Junio C Hamano

On 10/14/2019 11:38 AM, James Coglan wrote:
> On 13/10/2019 07:56, Jeff King wrote:
>> On Fri, Oct 11, 2019 at 06:04:21PM +0100, James Coglan wrote:
>>
>>>> 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:
>>
>>>>> +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

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH 07/11] graph: commit and post-merge lines for left-skewed merges
  2019-10-14 15:38         ` James Coglan
  2019-10-14 17:41           ` Derrick Stolee
@ 2019-10-14 20:42           ` Johannes Schindelin
  1 sibling, 0 replies; 83+ messages in thread
From: Johannes Schindelin @ 2019-10-14 20:42 UTC (permalink / raw)
  To: James Coglan
  Cc: Jeff King, Derrick Stolee, James Coglan via GitGitGadget, git,
	Junio C Hamano

Hi James,

On Mon, 14 Oct 2019, James Coglan wrote:

> [...] 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

^ permalink raw reply	[flat|nested] 83+ messages in thread

* [PATCH v2 00/13] Improve the readability of log --graph output
  2019-10-10 16:13 [PATCH 00/11] Improve the readability of log --graph output James Coglan via GitGitGadget
                   ` (12 preceding siblings ...)
  2019-10-13  7:15 ` Jeff King
@ 2019-10-15 23:40 ` James Coglan via GitGitGadget
  2019-10-15 23:40   ` [PATCH v2 01/13] graph: automatically track display width of graph lines James Coglan via GitGitGadget
                     ` (13 more replies)
  13 siblings, 14 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:40 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano

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 <jcoglan@gmail.com>
      
     -    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 <jcoglan@gmail.com>
      
     @@ -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 <jcoglan@gmail.com>
      
     -    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

-- 
gitgitgadget

^ permalink raw reply	[flat|nested] 83+ messages in thread

* [PATCH v2 01/13] graph: automatically track display width of graph lines
  2019-10-15 23:40 ` [PATCH v2 00/13] " James Coglan via GitGitGadget
@ 2019-10-15 23:40   ` James Coglan via GitGitGadget
  2019-10-15 23:40   ` [PATCH v2 02/13] graph: handle line padding in `graph_next_line()` James Coglan via GitGitGadget
                     ` (12 subsequent siblings)
  13 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:40 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c | 194 +++++++++++++++++++++++++++++---------------------------
 1 file changed, 99 insertions(+), 95 deletions(-)

diff --git a/graph.c b/graph.c
index f53135485f..2f81a5d23d 100644
--- a/graph.c
+++ b/graph.c
@@ -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
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v2 02/13] graph: handle line padding in `graph_next_line()`
  2019-10-15 23:40 ` [PATCH v2 00/13] " James Coglan via GitGitGadget
  2019-10-15 23:40   ` [PATCH v2 01/13] graph: automatically track display width of graph lines James Coglan via GitGitGadget
@ 2019-10-15 23:40   ` James Coglan via GitGitGadget
  2019-10-15 23:40   ` [PATCH v2 03/13] graph: reuse `find_new_column_by_commit()` James Coglan via GitGitGadget
                     ` (11 subsequent siblings)
  13 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:40 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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(-)

diff --git a/graph.c b/graph.c
index 2f81a5d23d..4c68557b17 100644
--- a/graph.c
+++ b/graph.c
@@ -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)
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v2 03/13] graph: reuse `find_new_column_by_commit()`
  2019-10-15 23:40 ` [PATCH v2 00/13] " James Coglan via GitGitGadget
  2019-10-15 23:40   ` [PATCH v2 01/13] graph: automatically track display width of graph lines James Coglan via GitGitGadget
  2019-10-15 23:40   ` [PATCH v2 02/13] graph: handle line padding in `graph_next_line()` James Coglan via GitGitGadget
@ 2019-10-15 23:40   ` James Coglan via GitGitGadget
  2019-10-15 23:40   ` [PATCH v2 04/13] graph: reduce duplication in `graph_insert_into_new_columns()` James Coglan via GitGitGadget
                     ` (10 subsequent siblings)
  13 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:40 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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()`.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c | 48 +++++++++++++++++++++++-------------------------
 1 file changed, 23 insertions(+), 25 deletions(-)

diff --git a/graph.c b/graph.c
index 4c68557b17..c9646d9e00 100644
--- a/graph.c
+++ b/graph.c
@@ -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) {
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v2 04/13] graph: reduce duplication in `graph_insert_into_new_columns()`
  2019-10-15 23:40 ` [PATCH v2 00/13] " James Coglan via GitGitGadget
                     ` (2 preceding siblings ...)
  2019-10-15 23:40   ` [PATCH v2 03/13] graph: reuse `find_new_column_by_commit()` James Coglan via GitGitGadget
@ 2019-10-15 23:40   ` James Coglan via GitGitGadget
  2019-10-15 23:40   ` [PATCH v2 05/13] graph: remove `mapping_idx` and `graph_update_width()` James Coglan via GitGitGadget
                     ` (9 subsequent siblings)
  13 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:40 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c | 20 +++++++-------------
 1 file changed, 7 insertions(+), 13 deletions(-)

diff --git a/graph.c b/graph.c
index c9646d9e00..512ae16535 100644
--- a/graph.c
+++ b/graph.c
@@ -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,
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v2 05/13] graph: remove `mapping_idx` and `graph_update_width()`
  2019-10-15 23:40 ` [PATCH v2 00/13] " James Coglan via GitGitGadget
                     ` (3 preceding siblings ...)
  2019-10-15 23:40   ` [PATCH v2 04/13] graph: reduce duplication in `graph_insert_into_new_columns()` James Coglan via GitGitGadget
@ 2019-10-15 23:40   ` James Coglan via GitGitGadget
  2019-10-15 23:40   ` [PATCH v2 06/13] graph: extract logic for moving to GRAPH_PRE_COMMIT state James Coglan via GitGitGadget
                     ` (8 subsequent siblings)
  13 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:40 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c | 65 +++++++++------------------------------------------------
 1 file changed, 10 insertions(+), 55 deletions(-)

diff --git a/graph.c b/graph.c
index 512ae16535..d724ef25c3 100644
--- a/graph.c
+++ b/graph.c
@@ -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)
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v2 06/13] graph: extract logic for moving to GRAPH_PRE_COMMIT state
  2019-10-15 23:40 ` [PATCH v2 00/13] " James Coglan via GitGitGadget
                     ` (4 preceding siblings ...)
  2019-10-15 23:40   ` [PATCH v2 05/13] graph: remove `mapping_idx` and `graph_update_width()` James Coglan via GitGitGadget
@ 2019-10-15 23:40   ` James Coglan via GitGitGadget
  2019-10-15 23:40   ` [PATCH v2 07/13] graph: example of graph output that can be simplified James Coglan via GitGitGadget
                     ` (7 subsequent siblings)
  13 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:40 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/graph.c b/graph.c
index d724ef25c3..bd7403065e 100644
--- a/graph.c
+++ b/graph.c
@@ -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);
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v2 07/13] graph: example of graph output that can be simplified
  2019-10-15 23:40 ` [PATCH v2 00/13] " James Coglan via GitGitGadget
                     ` (5 preceding siblings ...)
  2019-10-15 23:40   ` [PATCH v2 06/13] graph: extract logic for moving to GRAPH_PRE_COMMIT state James Coglan via GitGitGadget
@ 2019-10-15 23:40   ` James Coglan via GitGitGadget
  2019-10-15 23:40   ` [PATCH v2 08/13] graph: tidy up display of left-skewed merges James Coglan via GitGitGadget
                     ` (6 subsequent siblings)
  13 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:40 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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

diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
new file mode 100755
index 0000000000..4582ba066a
--- /dev/null
+++ b/t/t4215-log-skewed-merges.sh
@@ -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
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v2 08/13] graph: tidy up display of left-skewed merges
  2019-10-15 23:40 ` [PATCH v2 00/13] " James Coglan via GitGitGadget
                     ` (6 preceding siblings ...)
  2019-10-15 23:40   ` [PATCH v2 07/13] graph: example of graph output that can be simplified James Coglan via GitGitGadget
@ 2019-10-15 23:40   ` James Coglan via GitGitGadget
  2019-10-15 23:41   ` [PATCH v2 09/13] graph: commit and post-merge lines for " James Coglan via GitGitGadget
                     ` (5 subsequent siblings)
  13 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:40 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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:

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

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c                      | 125 +++++++++++++++++++++++++++--------
 t/t4214-log-graph-octopus.sh |  20 +++---
 t/t4215-log-skewed-merges.sh |  45 +++++++++++--
 3 files changed, 144 insertions(+), 46 deletions(-)

diff --git a/graph.c b/graph.c
index bd7403065e..e37127f5ab 100644
--- a/graph.c
+++ b/graph.c
@@ -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;
 	}
 
 	/*
diff --git a/t/t4214-log-graph-octopus.sh b/t/t4214-log-graph-octopus.sh
index 3ae8e51e50..1b96276894 100755
--- a/t/t4214-log-graph-octopus.sh
+++ b/t/t4214-log-graph-octopus.sh
@@ -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>
diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
index 4582ba066a..dc187b5caf 100755
--- a/t/t4215-log-skewed-merges.sh
+++ b/t/t4215-log-skewed-merges.sh
@@ -11,12 +11,8 @@ test_expect_success 'log --graph with merge fusing with its left and right neigh
 	| *   G
 	| |\
 	| | * F
-	| | |
-	| |  \
-	| *-. \   E
-	| |\ \ \
-	|/ / / /
-	| | | /
+	| * \   E
+	|/|\ \
 	| | |/
 	| | * D
 	| * | C
@@ -40,4 +36,41 @@ test_expect_success 'log --graph with merge fusing with its left and right neigh
 	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
+
+	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_done
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v2 09/13] graph: commit and post-merge lines for left-skewed merges
  2019-10-15 23:40 ` [PATCH v2 00/13] " James Coglan via GitGitGadget
                     ` (7 preceding siblings ...)
  2019-10-15 23:40   ` [PATCH v2 08/13] graph: tidy up display of left-skewed merges James Coglan via GitGitGadget
@ 2019-10-15 23:41   ` James Coglan via GitGitGadget
  2019-10-15 23:41   ` [PATCH v2 10/13] graph: rename `new_mapping` to `old_mapping` James Coglan via GitGitGadget
                     ` (4 subsequent siblings)
  13 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c                      |  63 +++++++++++++--
 t/t4215-log-skewed-merges.sh | 147 ++++++++++++++++++++++++++++++++++-
 2 files changed, 203 insertions(+), 7 deletions(-)

diff --git a/graph.c b/graph.c
index e37127f5ab..21edad8085 100644
--- a/graph.c
+++ b/graph.c
@@ -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, '|');
diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
index dc187b5caf..e673cdb6f7 100755
--- a/t/t4215-log-skewed-merges.sh
+++ b/t/t4215-log-skewed-merges.sh
@@ -11,7 +11,7 @@ test_expect_success 'log --graph with merge fusing with its left and right neigh
 	| *   G
 	| |\
 	| | * F
-	| * \   E
+	| * |   E
 	|/|\ \
 	| | |/
 	| | * D
@@ -43,7 +43,7 @@ test_expect_success 'log --graph with left-skewed merge' '
 	| | | | * 0_G
 	| |_|_|/|
 	|/| | | |
-	| | | * \   0_F
+	| | | * |   0_F
 	| |_|/|\ \
 	|/| | | |/
 	| | | | * 0_E
@@ -73,4 +73,147 @@ test_expect_success 'log --graph with left-skewed merge' '
 	test_cmp expect actual
 '
 
+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 &&
+	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 &&
+	*   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 &&
+	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 &&
+	*   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 &&
+	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 &&
+	*   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 &&
+	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_done
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v2 10/13] graph: rename `new_mapping` to `old_mapping`
  2019-10-15 23:40 ` [PATCH v2 00/13] " James Coglan via GitGitGadget
                     ` (8 preceding siblings ...)
  2019-10-15 23:41   ` [PATCH v2 09/13] graph: commit and post-merge lines for " James Coglan via GitGitGadget
@ 2019-10-15 23:41   ` James Coglan via GitGitGadget
  2019-10-15 23:41   ` [PATCH v2 11/13] graph: smooth appearance of collapsing edges on commit lines James Coglan via GitGitGadget
                     ` (3 subsequent siblings)
  13 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c | 54 +++++++++++++++++++++++++++---------------------------
 1 file changed, 27 insertions(+), 27 deletions(-)

diff --git a/graph.c b/graph.c
index 21edad8085..2315f3604d 100644
--- a/graph.c
+++ b/graph.c
@@ -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.
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v2 11/13] graph: smooth appearance of collapsing edges on commit lines
  2019-10-15 23:40 ` [PATCH v2 00/13] " James Coglan via GitGitGadget
                     ` (9 preceding siblings ...)
  2019-10-15 23:41   ` [PATCH v2 10/13] graph: rename `new_mapping` to `old_mapping` James Coglan via GitGitGadget
@ 2019-10-15 23:41   ` James Coglan via GitGitGadget
  2019-10-15 23:41   ` [PATCH v2 12/13] graph: flatten edges that fuse with their right neighbor James Coglan via GitGitGadget
                     ` (2 subsequent siblings)
  13 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 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(-)

diff --git a/graph.c b/graph.c
index 2315f3604d..63f8d18baa 100644
--- a/graph.c
+++ b/graph.c
@@ -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
 	 */
diff --git a/t/t3430-rebase-merges.sh b/t/t3430-rebase-merges.sh
index 9efcf4808a..a30d27e9f3 100755
--- a/t/t3430-rebase-merges.sh
+++ b/t/t3430-rebase-merges.sh
@@ -408,7 +408,7 @@ test_expect_success 'octopus merges' '
 	| | * three
 	| * | two
 	| |/
-	* | one
+	* / one
 	|/
 	o before-octopus
 	EOF
diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index e803ba402e..ab0d021365 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -667,7 +667,7 @@ cat > expect <<\EOF
 * | | fifth
 * | | fourth
 |/ /
-* | third
+* / third
 |/
 * second
 * initial
diff --git a/t/t4214-log-graph-octopus.sh b/t/t4214-log-graph-octopus.sh
index 1b96276894..21bc600a82 100755
--- a/t/t4214-log-graph-octopus.sh
+++ b/t/t4214-log-graph-octopus.sh
@@ -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
diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
index e673cdb6f7..1745b3b64c 100755
--- a/t/t4215-log-skewed-merges.sh
+++ b/t/t4215-log-skewed-merges.sh
@@ -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
diff --git a/t/t6016-rev-list-graph-simplify-history.sh b/t/t6016-rev-list-graph-simplify-history.sh
index f7181d1d6a..ca1682f29b 100755
--- a/t/t6016-rev-list-graph-simplify-history.sh
+++ b/t/t6016-rev-list-graph-simplify-history.sh
@@ -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 &&
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v2 12/13] graph: flatten edges that fuse with their right neighbor
  2019-10-15 23:40 ` [PATCH v2 00/13] " James Coglan via GitGitGadget
                     ` (10 preceding siblings ...)
  2019-10-15 23:41   ` [PATCH v2 11/13] graph: smooth appearance of collapsing edges on commit lines James Coglan via GitGitGadget
@ 2019-10-15 23:41   ` James Coglan via GitGitGadget
  2019-10-15 23:41   ` [PATCH v2 13/13] graph: fix coloring of octopus dashes James Coglan via GitGitGadget
  2019-10-15 23:47   ` [PATCH v3 00/13] Improve the readability of log --graph output James Coglan via GitGitGadget
  13 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 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(-)

diff --git a/graph.c b/graph.c
index 63f8d18baa..80db74aee6 100644
--- a/graph.c
+++ b/graph.c
@@ -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, ' ');
diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
index 1745b3b64c..d33c6438d8 100755
--- a/t/t4215-log-skewed-merges.sh
+++ b/t/t4215-log-skewed-merges.sh
@@ -11,9 +11,8 @@ test_expect_success 'log --graph with merge fusing with its left and right neigh
 	| *   G
 	| |\
 	| | * F
-	| * |   E
-	|/|\ \
-	| | |/
+	| * | E
+	|/|\|
 	| | * D
 	| * | C
 	| |/
@@ -43,9 +42,9 @@ test_expect_success 'log --graph with left-skewed merge' '
 	| | | | * 0_G
 	| |_|_|/|
 	|/| | | |
-	| | | * |   0_F
-	| |_|/|\ \
-	|/| | | |/
+	| | | * | 0_F
+	| |_|/|\|
+	|/| | | |
 	| | | | * 0_E
 	| |_|_|/
 	|/| | |
@@ -153,9 +152,8 @@ test_expect_success 'log --graph with nested right-skewed merge following left-s
 	| | * 3_G
 	| * | 3_F
 	|/| |
-	| * |   3_E
-	| |\ \
-	| | |/
+	| * | 3_E
+	| |\|
 	| | * 3_D
 	| * | 3_C
 	| |/
@@ -216,4 +214,44 @@ test_expect_success 'log --graph with right-skewed merge following a left-skewed
 	test_cmp expect actual
 '
 
+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 &&
+	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
diff --git a/t/t6016-rev-list-graph-simplify-history.sh b/t/t6016-rev-list-graph-simplify-history.sh
index ca1682f29b..f5e6e92f5b 100755
--- a/t/t6016-rev-list-graph-simplify-history.sh
+++ b/t/t6016-rev-list-graph-simplify-history.sh
@@ -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 &&
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v2 13/13] graph: fix coloring of octopus dashes
  2019-10-15 23:40 ` [PATCH v2 00/13] " James Coglan via GitGitGadget
                     ` (11 preceding siblings ...)
  2019-10-15 23:41   ` [PATCH v2 12/13] graph: flatten edges that fuse with their right neighbor James Coglan via GitGitGadget
@ 2019-10-15 23:41   ` James Coglan via GitGitGadget
  2019-10-15 23:47   ` [PATCH v3 00/13] Improve the readability of log --graph output James Coglan via GitGitGadget
  13 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:41 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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`.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c                      | 71 +++++++++++++++++++-----------------
 t/t4214-log-graph-octopus.sh | 10 ++---
 2 files changed, 42 insertions(+), 39 deletions(-)

diff --git a/graph.c b/graph.c
index 80db74aee6..e3fd0ea5f8 100644
--- a/graph.c
+++ b/graph.c
@@ -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)
diff --git a/t/t4214-log-graph-octopus.sh b/t/t4214-log-graph-octopus.sh
index 21bc600a82..40d27db674 100755
--- a/t/t4214-log-graph-octopus.sh
+++ b/t/t4214-log-graph-octopus.sh
@@ -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
-- 
gitgitgadget

^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v3 00/13] Improve the readability of log --graph output
  2019-10-15 23:40 ` [PATCH v2 00/13] " James Coglan via GitGitGadget
                     ` (12 preceding siblings ...)
  2019-10-15 23:41   ` [PATCH v2 13/13] graph: fix coloring of octopus dashes James Coglan via GitGitGadget
@ 2019-10-15 23:47   ` James Coglan via GitGitGadget
  2019-10-15 23:47     ` [PATCH v3 01/13] graph: automatically track display width of graph lines James Coglan via GitGitGadget
                       ` (12 more replies)
  13 siblings, 13 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:47 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano

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 <jcoglan@gmail.com>
     +
       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 <jcoglan@gmail.com>
     +
       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

-- 
gitgitgadget

^ permalink raw reply	[flat|nested] 83+ messages in thread

* [PATCH v3 01/13] graph: automatically track display width of graph lines
  2019-10-15 23:47   ` [PATCH v3 00/13] Improve the readability of log --graph output James Coglan via GitGitGadget
@ 2019-10-15 23:47     ` James Coglan via GitGitGadget
  2019-10-16  3:35       ` Junio C Hamano
  2019-10-15 23:47     ` [PATCH v3 02/13] graph: handle line padding in `graph_next_line()` James Coglan via GitGitGadget
                       ` (11 subsequent siblings)
  12 siblings, 1 reply; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:47 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c | 194 +++++++++++++++++++++++++++++---------------------------
 1 file changed, 99 insertions(+), 95 deletions(-)

diff --git a/graph.c b/graph.c
index f53135485f..2f81a5d23d 100644
--- a/graph.c
+++ b/graph.c
@@ -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
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v3 02/13] graph: handle line padding in `graph_next_line()`
  2019-10-15 23:47   ` [PATCH v3 00/13] Improve the readability of log --graph output James Coglan via GitGitGadget
  2019-10-15 23:47     ` [PATCH v3 01/13] graph: automatically track display width of graph lines James Coglan via GitGitGadget
@ 2019-10-15 23:47     ` James Coglan via GitGitGadget
  2019-10-16  3:37       ` Junio C Hamano
  2019-10-15 23:47     ` [PATCH v3 03/13] graph: reuse `find_new_column_by_commit()` James Coglan via GitGitGadget
                       ` (10 subsequent siblings)
  12 siblings, 1 reply; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:47 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c | 49 ++++++++++++++++++++-----------------------------
 1 file changed, 20 insertions(+), 29 deletions(-)

diff --git a/graph.c b/graph.c
index 2f81a5d23d..4c68557b17 100644
--- a/graph.c
+++ b/graph.c
@@ -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)
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v3 03/13] graph: reuse `find_new_column_by_commit()`
  2019-10-15 23:47   ` [PATCH v3 00/13] Improve the readability of log --graph output James Coglan via GitGitGadget
  2019-10-15 23:47     ` [PATCH v3 01/13] graph: automatically track display width of graph lines James Coglan via GitGitGadget
  2019-10-15 23:47     ` [PATCH v3 02/13] graph: handle line padding in `graph_next_line()` James Coglan via GitGitGadget
@ 2019-10-15 23:47     ` James Coglan via GitGitGadget
  2019-10-15 23:47     ` [PATCH v3 04/13] graph: reduce duplication in `graph_insert_into_new_columns()` James Coglan via GitGitGadget
                       ` (9 subsequent siblings)
  12 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:47 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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()`.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c | 48 +++++++++++++++++++++++-------------------------
 1 file changed, 23 insertions(+), 25 deletions(-)

diff --git a/graph.c b/graph.c
index 4c68557b17..c9646d9e00 100644
--- a/graph.c
+++ b/graph.c
@@ -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) {
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v3 04/13] graph: reduce duplication in `graph_insert_into_new_columns()`
  2019-10-15 23:47   ` [PATCH v3 00/13] Improve the readability of log --graph output James Coglan via GitGitGadget
                       ` (2 preceding siblings ...)
  2019-10-15 23:47     ` [PATCH v3 03/13] graph: reuse `find_new_column_by_commit()` James Coglan via GitGitGadget
@ 2019-10-15 23:47     ` James Coglan via GitGitGadget
  2019-10-15 23:47     ` [PATCH v3 05/13] graph: remove `mapping_idx` and `graph_update_width()` James Coglan via GitGitGadget
                       ` (8 subsequent siblings)
  12 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:47 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c | 20 +++++++-------------
 1 file changed, 7 insertions(+), 13 deletions(-)

diff --git a/graph.c b/graph.c
index c9646d9e00..512ae16535 100644
--- a/graph.c
+++ b/graph.c
@@ -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,
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v3 05/13] graph: remove `mapping_idx` and `graph_update_width()`
  2019-10-15 23:47   ` [PATCH v3 00/13] Improve the readability of log --graph output James Coglan via GitGitGadget
                       ` (3 preceding siblings ...)
  2019-10-15 23:47     ` [PATCH v3 04/13] graph: reduce duplication in `graph_insert_into_new_columns()` James Coglan via GitGitGadget
@ 2019-10-15 23:47     ` James Coglan via GitGitGadget
  2019-10-15 23:47     ` [PATCH v3 06/13] graph: extract logic for moving to GRAPH_PRE_COMMIT state James Coglan via GitGitGadget
                       ` (7 subsequent siblings)
  12 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:47 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c | 65 +++++++++------------------------------------------------
 1 file changed, 10 insertions(+), 55 deletions(-)

diff --git a/graph.c b/graph.c
index 512ae16535..d724ef25c3 100644
--- a/graph.c
+++ b/graph.c
@@ -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)
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v3 06/13] graph: extract logic for moving to GRAPH_PRE_COMMIT state
  2019-10-15 23:47   ` [PATCH v3 00/13] Improve the readability of log --graph output James Coglan via GitGitGadget
                       ` (4 preceding siblings ...)
  2019-10-15 23:47     ` [PATCH v3 05/13] graph: remove `mapping_idx` and `graph_update_width()` James Coglan via GitGitGadget
@ 2019-10-15 23:47     ` James Coglan via GitGitGadget
  2019-10-15 23:47     ` [PATCH v3 07/13] graph: example of graph output that can be simplified James Coglan via GitGitGadget
                       ` (6 subsequent siblings)
  12 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:47 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/graph.c b/graph.c
index d724ef25c3..bd7403065e 100644
--- a/graph.c
+++ b/graph.c
@@ -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);
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v3 07/13] graph: example of graph output that can be simplified
  2019-10-15 23:47   ` [PATCH v3 00/13] Improve the readability of log --graph output James Coglan via GitGitGadget
                       ` (5 preceding siblings ...)
  2019-10-15 23:47     ` [PATCH v3 06/13] graph: extract logic for moving to GRAPH_PRE_COMMIT state James Coglan via GitGitGadget
@ 2019-10-15 23:47     ` James Coglan via GitGitGadget
  2019-10-17 12:30       ` Derrick Stolee
  2019-10-18 15:21       ` SZEDER Gábor
  2019-10-15 23:47     ` [PATCH v3 08/13] graph: tidy up display of left-skewed merges James Coglan via GitGitGadget
                       ` (5 subsequent siblings)
  12 siblings, 2 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:47 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 t/t4215-log-skewed-merges.sh | 43 ++++++++++++++++++++++++++++++++++++
 1 file changed, 43 insertions(+)
 create mode 100755 t/t4215-log-skewed-merges.sh

diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
new file mode 100755
index 0000000000..4582ba066a
--- /dev/null
+++ b/t/t4215-log-skewed-merges.sh
@@ -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
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v3 08/13] graph: tidy up display of left-skewed merges
  2019-10-15 23:47   ` [PATCH v3 00/13] Improve the readability of log --graph output James Coglan via GitGitGadget
                       ` (6 preceding siblings ...)
  2019-10-15 23:47     ` [PATCH v3 07/13] graph: example of graph output that can be simplified James Coglan via GitGitGadget
@ 2019-10-15 23:47     ` James Coglan via GitGitGadget
  2019-10-16  4:00       ` Junio C Hamano
  2019-10-15 23:47     ` [PATCH v3 09/13] graph: commit and post-merge lines for " James Coglan via GitGitGadget
                       ` (4 subsequent siblings)
  12 siblings, 1 reply; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:47 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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:

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

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c                      | 125 +++++++++++++++++++++++++++--------
 t/t4214-log-graph-octopus.sh |  20 +++---
 t/t4215-log-skewed-merges.sh |  45 +++++++++++--
 3 files changed, 144 insertions(+), 46 deletions(-)

diff --git a/graph.c b/graph.c
index bd7403065e..e37127f5ab 100644
--- a/graph.c
+++ b/graph.c
@@ -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;
 	}
 
 	/*
diff --git a/t/t4214-log-graph-octopus.sh b/t/t4214-log-graph-octopus.sh
index 3ae8e51e50..1b96276894 100755
--- a/t/t4214-log-graph-octopus.sh
+++ b/t/t4214-log-graph-octopus.sh
@@ -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>
diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
index 4582ba066a..dc187b5caf 100755
--- a/t/t4215-log-skewed-merges.sh
+++ b/t/t4215-log-skewed-merges.sh
@@ -11,12 +11,8 @@ test_expect_success 'log --graph with merge fusing with its left and right neigh
 	| *   G
 	| |\
 	| | * F
-	| | |
-	| |  \
-	| *-. \   E
-	| |\ \ \
-	|/ / / /
-	| | | /
+	| * \   E
+	|/|\ \
 	| | |/
 	| | * D
 	| * | C
@@ -40,4 +36,41 @@ test_expect_success 'log --graph with merge fusing with its left and right neigh
 	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
+
+	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_done
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v3 09/13] graph: commit and post-merge lines for left-skewed merges
  2019-10-15 23:47   ` [PATCH v3 00/13] Improve the readability of log --graph output James Coglan via GitGitGadget
                       ` (7 preceding siblings ...)
  2019-10-15 23:47     ` [PATCH v3 08/13] graph: tidy up display of left-skewed merges James Coglan via GitGitGadget
@ 2019-10-15 23:47     ` James Coglan via GitGitGadget
  2019-10-15 23:47     ` [PATCH v3 10/13] graph: rename `new_mapping` to `old_mapping` James Coglan via GitGitGadget
                       ` (3 subsequent siblings)
  12 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:47 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c                      |  63 +++++++++++++--
 t/t4215-log-skewed-merges.sh | 147 ++++++++++++++++++++++++++++++++++-
 2 files changed, 203 insertions(+), 7 deletions(-)

diff --git a/graph.c b/graph.c
index e37127f5ab..21edad8085 100644
--- a/graph.c
+++ b/graph.c
@@ -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, '|');
diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
index dc187b5caf..e673cdb6f7 100755
--- a/t/t4215-log-skewed-merges.sh
+++ b/t/t4215-log-skewed-merges.sh
@@ -11,7 +11,7 @@ test_expect_success 'log --graph with merge fusing with its left and right neigh
 	| *   G
 	| |\
 	| | * F
-	| * \   E
+	| * |   E
 	|/|\ \
 	| | |/
 	| | * D
@@ -43,7 +43,7 @@ test_expect_success 'log --graph with left-skewed merge' '
 	| | | | * 0_G
 	| |_|_|/|
 	|/| | | |
-	| | | * \   0_F
+	| | | * |   0_F
 	| |_|/|\ \
 	|/| | | |/
 	| | | | * 0_E
@@ -73,4 +73,147 @@ test_expect_success 'log --graph with left-skewed merge' '
 	test_cmp expect actual
 '
 
+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 &&
+	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 &&
+	*   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 &&
+	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 &&
+	*   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 &&
+	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 &&
+	*   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 &&
+	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_done
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v3 10/13] graph: rename `new_mapping` to `old_mapping`
  2019-10-15 23:47   ` [PATCH v3 00/13] Improve the readability of log --graph output James Coglan via GitGitGadget
                       ` (8 preceding siblings ...)
  2019-10-15 23:47     ` [PATCH v3 09/13] graph: commit and post-merge lines for " James Coglan via GitGitGadget
@ 2019-10-15 23:47     ` James Coglan via GitGitGadget
  2019-10-15 23:47     ` [PATCH v3 11/13] graph: smooth appearance of collapsing edges on commit lines James Coglan via GitGitGadget
                       ` (2 subsequent siblings)
  12 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:47 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c | 54 +++++++++++++++++++++++++++---------------------------
 1 file changed, 27 insertions(+), 27 deletions(-)

diff --git a/graph.c b/graph.c
index 21edad8085..2315f3604d 100644
--- a/graph.c
+++ b/graph.c
@@ -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.
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v3 11/13] graph: smooth appearance of collapsing edges on commit lines
  2019-10-15 23:47   ` [PATCH v3 00/13] Improve the readability of log --graph output James Coglan via GitGitGadget
                       ` (9 preceding siblings ...)
  2019-10-15 23:47     ` [PATCH v3 10/13] graph: rename `new_mapping` to `old_mapping` James Coglan via GitGitGadget
@ 2019-10-15 23:47     ` James Coglan via GitGitGadget
  2019-10-15 23:47     ` [PATCH v3 12/13] graph: flatten edges that fuse with their right neighbor James Coglan via GitGitGadget
  2019-10-15 23:47     ` [PATCH v3 13/13] graph: fix coloring of octopus dashes James Coglan via GitGitGadget
  12 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:47 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 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(-)

diff --git a/graph.c b/graph.c
index 2315f3604d..63f8d18baa 100644
--- a/graph.c
+++ b/graph.c
@@ -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
 	 */
diff --git a/t/t3430-rebase-merges.sh b/t/t3430-rebase-merges.sh
index 9efcf4808a..a30d27e9f3 100755
--- a/t/t3430-rebase-merges.sh
+++ b/t/t3430-rebase-merges.sh
@@ -408,7 +408,7 @@ test_expect_success 'octopus merges' '
 	| | * three
 	| * | two
 	| |/
-	* | one
+	* / one
 	|/
 	o before-octopus
 	EOF
diff --git a/t/t4202-log.sh b/t/t4202-log.sh
index e803ba402e..ab0d021365 100755
--- a/t/t4202-log.sh
+++ b/t/t4202-log.sh
@@ -667,7 +667,7 @@ cat > expect <<\EOF
 * | | fifth
 * | | fourth
 |/ /
-* | third
+* / third
 |/
 * second
 * initial
diff --git a/t/t4214-log-graph-octopus.sh b/t/t4214-log-graph-octopus.sh
index 1b96276894..21bc600a82 100755
--- a/t/t4214-log-graph-octopus.sh
+++ b/t/t4214-log-graph-octopus.sh
@@ -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
diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
index e673cdb6f7..1745b3b64c 100755
--- a/t/t4215-log-skewed-merges.sh
+++ b/t/t4215-log-skewed-merges.sh
@@ -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
diff --git a/t/t6016-rev-list-graph-simplify-history.sh b/t/t6016-rev-list-graph-simplify-history.sh
index f7181d1d6a..ca1682f29b 100755
--- a/t/t6016-rev-list-graph-simplify-history.sh
+++ b/t/t6016-rev-list-graph-simplify-history.sh
@@ -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 &&
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v3 12/13] graph: flatten edges that fuse with their right neighbor
  2019-10-15 23:47   ` [PATCH v3 00/13] Improve the readability of log --graph output James Coglan via GitGitGadget
                       ` (10 preceding siblings ...)
  2019-10-15 23:47     ` [PATCH v3 11/13] graph: smooth appearance of collapsing edges on commit lines James Coglan via GitGitGadget
@ 2019-10-15 23:47     ` James Coglan via GitGitGadget
  2019-10-15 23:47     ` [PATCH v3 13/13] graph: fix coloring of octopus dashes James Coglan via GitGitGadget
  12 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:47 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 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(-)

diff --git a/graph.c b/graph.c
index 63f8d18baa..80db74aee6 100644
--- a/graph.c
+++ b/graph.c
@@ -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, ' ');
diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
index 1745b3b64c..d33c6438d8 100755
--- a/t/t4215-log-skewed-merges.sh
+++ b/t/t4215-log-skewed-merges.sh
@@ -11,9 +11,8 @@ test_expect_success 'log --graph with merge fusing with its left and right neigh
 	| *   G
 	| |\
 	| | * F
-	| * |   E
-	|/|\ \
-	| | |/
+	| * | E
+	|/|\|
 	| | * D
 	| * | C
 	| |/
@@ -43,9 +42,9 @@ test_expect_success 'log --graph with left-skewed merge' '
 	| | | | * 0_G
 	| |_|_|/|
 	|/| | | |
-	| | | * |   0_F
-	| |_|/|\ \
-	|/| | | |/
+	| | | * | 0_F
+	| |_|/|\|
+	|/| | | |
 	| | | | * 0_E
 	| |_|_|/
 	|/| | |
@@ -153,9 +152,8 @@ test_expect_success 'log --graph with nested right-skewed merge following left-s
 	| | * 3_G
 	| * | 3_F
 	|/| |
-	| * |   3_E
-	| |\ \
-	| | |/
+	| * | 3_E
+	| |\|
 	| | * 3_D
 	| * | 3_C
 	| |/
@@ -216,4 +214,44 @@ test_expect_success 'log --graph with right-skewed merge following a left-skewed
 	test_cmp expect actual
 '
 
+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 &&
+	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
diff --git a/t/t6016-rev-list-graph-simplify-history.sh b/t/t6016-rev-list-graph-simplify-history.sh
index ca1682f29b..f5e6e92f5b 100755
--- a/t/t6016-rev-list-graph-simplify-history.sh
+++ b/t/t6016-rev-list-graph-simplify-history.sh
@@ -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 &&
-- 
gitgitgadget


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* [PATCH v3 13/13] graph: fix coloring of octopus dashes
  2019-10-15 23:47   ` [PATCH v3 00/13] Improve the readability of log --graph output James Coglan via GitGitGadget
                       ` (11 preceding siblings ...)
  2019-10-15 23:47     ` [PATCH v3 12/13] graph: flatten edges that fuse with their right neighbor James Coglan via GitGitGadget
@ 2019-10-15 23:47     ` James Coglan via GitGitGadget
  12 siblings, 0 replies; 83+ messages in thread
From: James Coglan via GitGitGadget @ 2019-10-15 23:47 UTC (permalink / raw)
  To: git; +Cc: Junio C Hamano, James Coglan

From: James Coglan <jcoglan@gmail.com>

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`.

Signed-off-by: James Coglan <jcoglan@gmail.com>
---
 graph.c                      | 71 +++++++++++++++++++-----------------
 t/t4214-log-graph-octopus.sh | 10 ++---
 2 files changed, 42 insertions(+), 39 deletions(-)

diff --git a/graph.c b/graph.c
index 80db74aee6..e3fd0ea5f8 100644
--- a/graph.c
+++ b/graph.c
@@ -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)
diff --git a/t/t4214-log-graph-octopus.sh b/t/t4214-log-graph-octopus.sh
index 21bc600a82..40d27db674 100755
--- a/t/t4214-log-graph-octopus.sh
+++ b/t/t4214-log-graph-octopus.sh
@@ -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
-- 
gitgitgadget

^ permalink raw reply related	[flat|nested] 83+ messages in thread

* Re: [PATCH 01/11] graph: automatically track visible width of `strbuf`
  2019-10-14 12:55                 ` James Coglan
  2019-10-14 13:01                   ` James Coglan
@ 2019-10-16  2:15                   ` Junio C Hamano
  1 sibling, 0 replies; 83+ messages in thread
From: Junio C Hamano @ 2019-10-16  2:15 UTC (permalink / raw)
  To: James Coglan
  Cc: Johannes Schindelin, Denton Liu, James Coglan via GitGitGadget, git

James Coglan <jcoglan@gmail.com> writes:

>> 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 *`:
>
> 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.

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH v3 01/13] graph: automatically track display width of graph lines
  2019-10-15 23:47     ` [PATCH v3 01/13] graph: automatically track display width of graph lines James Coglan via GitGitGadget
@ 2019-10-16  3:35       ` Junio C Hamano
  2019-10-16  5:10         ` Junio C Hamano
  0 siblings, 1 reply; 83+ messages in thread
From: Junio C Hamano @ 2019-10-16  3:35 UTC (permalink / raw)
  To: James Coglan via GitGitGadget; +Cc: git, James Coglan

"James Coglan via GitGitGadget" <gitgitgadget@gmail.com> writes:

> +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));
> +}

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.

>  static void graph_output_pre_commit_line(struct git_graph *graph,
> ...
>  	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.

>  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.

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH v3 02/13] graph: handle line padding in `graph_next_line()`
  2019-10-15 23:47     ` [PATCH v3 02/13] graph: handle line padding in `graph_next_line()` James Coglan via GitGitGadget
@ 2019-10-16  3:37       ` Junio C Hamano
  0 siblings, 0 replies; 83+ messages in thread
From: Junio C Hamano @ 2019-10-16  3:37 UTC (permalink / raw)
  To: James Coglan via GitGitGadget; +Cc: git, James Coglan

"James Coglan via GitGitGadget" <gitgitgadget@gmail.com> writes:

> From: James Coglan <jcoglan@gmail.com>
>
> 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.

Very well explained.

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH v3 08/13] graph: tidy up display of left-skewed merges
  2019-10-15 23:47     ` [PATCH v3 08/13] graph: tidy up display of left-skewed merges James Coglan via GitGitGadget
@ 2019-10-16  4:00       ` Junio C Hamano
  2019-10-17 12:34         ` Derrick Stolee
  0 siblings, 1 reply; 83+ messages in thread
From: Junio C Hamano @ 2019-10-16  4:00 UTC (permalink / raw)
  To: James Coglan via GitGitGadget; +Cc: git, James Coglan

"James Coglan via GitGitGadget" <gitgitgadget@gmail.com> writes:

> 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:
>
>         | *             | *
>         | |\            | |\
>         | | \           | * \
>         | |  \          |/|\ \
>         | *-. \
>         | |\ \ \

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.

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH v3 01/13] graph: automatically track display width of graph lines
  2019-10-16  3:35       ` Junio C Hamano
@ 2019-10-16  5:10         ` Junio C Hamano
  0 siblings, 0 replies; 83+ messages in thread
From: Junio C Hamano @ 2019-10-16  5:10 UTC (permalink / raw)
  To: James Coglan via GitGitGadget; +Cc: git, James Coglan

Junio C Hamano <gitster@pobox.com> writes:

>>  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.


^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH v3 07/13] graph: example of graph output that can be simplified
  2019-10-15 23:47     ` [PATCH v3 07/13] graph: example of graph output that can be simplified James Coglan via GitGitGadget
@ 2019-10-17 12:30       ` Derrick Stolee
  2019-10-18 15:21       ` SZEDER Gábor
  1 sibling, 0 replies; 83+ messages in thread
From: Derrick Stolee @ 2019-10-17 12:30 UTC (permalink / raw)
  To: James Coglan via GitGitGadget, git; +Cc: Junio C Hamano, James Coglan

On 10/15/2019 7:47 PM, James Coglan via GitGitGadget wrote:
> diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
> new file mode 100755
> index 0000000000..4582ba066a
> --- /dev/null
> +++ b/t/t4215-log-skewed-merges.sh
> @@ -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

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

-Stolee

> +
> +	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
> 


^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH v3 08/13] graph: tidy up display of left-skewed merges
  2019-10-16  4:00       ` Junio C Hamano
@ 2019-10-17 12:34         ` Derrick Stolee
  2019-10-18  0:49           ` Junio C Hamano
  0 siblings, 1 reply; 83+ messages in thread
From: Derrick Stolee @ 2019-10-17 12:34 UTC (permalink / raw)
  To: Junio C Hamano, James Coglan via GitGitGadget; +Cc: git, James Coglan

On 10/16/2019 12:00 AM, Junio C Hamano wrote:
> "James Coglan via GitGitGadget" <gitgitgadget@gmail.com> writes:
> 
>> 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:
>>
>>         | *             | *
>>         | |\            | |\
>>         | | \           | * \
>>         | |  \          |/|\ \
>>         | *-. \
>>         | |\ \ \
> 
> 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

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.)

> 
> 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.

> 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


^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH v3 08/13] graph: tidy up display of left-skewed merges
  2019-10-17 12:34         ` Derrick Stolee
@ 2019-10-18  0:49           ` Junio C Hamano
  0 siblings, 0 replies; 83+ messages in thread
From: Junio C Hamano @ 2019-10-18  0:49 UTC (permalink / raw)
  To: Derrick Stolee; +Cc: James Coglan via GitGitGadget, git, James Coglan

Derrick Stolee <stolee@gmail.com> writes:

> 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.)
>
>> 
>> 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:
>
>  | | | | | *
>  | | | | *
>  | | | *
>  | | *
>  | *
>  *

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.


^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH v3 07/13] graph: example of graph output that can be simplified
  2019-10-15 23:47     ` [PATCH v3 07/13] graph: example of graph output that can be simplified James Coglan via GitGitGadget
  2019-10-17 12:30       ` Derrick Stolee
@ 2019-10-18 15:21       ` SZEDER Gábor
  2019-11-12  1:08         ` [PATCH] t4215: don't put git commands upstream of pipe Denton Liu
  1 sibling, 1 reply; 83+ messages in thread
From: SZEDER Gábor @ 2019-10-18 15:21 UTC (permalink / raw)
  To: James Coglan via GitGitGadget; +Cc: git, Junio C Hamano, James Coglan

On Tue, Oct 15, 2019 at 11:47:53PM +0000, James Coglan via GitGitGadget wrote:
> diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
> new file mode 100755
> index 0000000000..4582ba066a
> --- /dev/null
> +++ b/t/t4215-log-skewed-merges.sh
> @@ -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 &&

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.



^ permalink raw reply	[flat|nested] 83+ messages in thread

* [PATCH] t4215: don't put git commands upstream of pipe
  2019-10-18 15:21       ` SZEDER Gábor
@ 2019-11-12  1:08         ` Denton Liu
  2019-11-12  6:57           ` Junio C Hamano
  2019-11-12 18:56           ` [PATCH v3] t4215: use helper function to check output Denton Liu
  0 siblings, 2 replies; 83+ messages in thread
From: Denton Liu @ 2019-11-12  1:08 UTC (permalink / raw)
  To: Git Mailing List; +Cc: James Coglan, Junio C Hamano, SZEDER Gábor

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.

Signed-off-by: Denton Liu <liu.denton@gmail.com>
---
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(-)

diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
index d33c6438d8..0a2555fb28 100755
--- a/t/t4215-log-skewed-merges.sh
+++ b/t/t4215-log-skewed-merges.sh
@@ -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
 '
 
-- 
2.24.0.300.g722ba42680


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* Re: [PATCH] t4215: don't put git commands upstream of pipe
  2019-11-12  1:08         ` [PATCH] t4215: don't put git commands upstream of pipe Denton Liu
@ 2019-11-12  6:57           ` Junio C Hamano
  2019-11-12 10:54             ` SZEDER Gábor
  2019-11-12 18:56           ` [PATCH v3] t4215: use helper function to check output Denton Liu
  1 sibling, 1 reply; 83+ messages in thread
From: Junio C Hamano @ 2019-11-12  6:57 UTC (permalink / raw)
  To: Denton Liu; +Cc: Git Mailing List, James Coglan, SZEDER Gábor

Denton Liu <liu.denton@gmail.com> writes:

> 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'.
> ...
> -	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.

>  	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
>  '

^ permalink raw reply	[flat|nested] 83+ messages in thread

* Re: [PATCH] t4215: don't put git commands upstream of pipe
  2019-11-12  6:57           ` Junio C Hamano
@ 2019-11-12 10:54             ` SZEDER Gábor
  0 siblings, 0 replies; 83+ messages in thread
From: SZEDER Gábor @ 2019-11-12 10:54 UTC (permalink / raw)
  To: Junio C Hamano; +Cc: Denton Liu, Git Mailing List, James Coglan

On Tue, Nov 12, 2019 at 03:57:08PM +0900, Junio C Hamano wrote:
> Denton Liu <liu.denton@gmail.com> writes:
> 
> > 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'.
> > ...
> > -	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.

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'.

> Will queue; thanks.
> 
> >  	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
> >  '

^ permalink raw reply	[flat|nested] 83+ messages in thread

* [PATCH v3] t4215: use helper function to check output
  2019-11-12  1:08         ` [PATCH] t4215: don't put git commands upstream of pipe Denton Liu
  2019-11-12  6:57           ` Junio C Hamano
@ 2019-11-12 18:56           ` Denton Liu
  2019-11-13  2:05             ` Junio C Hamano
  1 sibling, 1 reply; 83+ messages in thread
From: Denton Liu @ 2019-11-12 18:56 UTC (permalink / raw)
  To: Git Mailing List; +Cc: James Coglan, Junio C Hamano, SZEDER Gábor

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`.

Signed-off-by: Denton Liu <liu.denton@gmail.com>
---
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(-)

diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
index d33c6438d8..18709a723e 100755
--- a/t/t4215-log-skewed-merges.sh
+++ b/t/t4215-log-skewed-merges.sh
@@ -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
-- 
2.24.0.300.g722ba42680


^ permalink raw reply related	[flat|nested] 83+ messages in thread

* Re: [PATCH v3] t4215: use helper function to check output
  2019-11-12 18:56           ` [PATCH v3] t4215: use helper function to check output Denton Liu
@ 2019-11-13  2:05             ` Junio C Hamano
  0 siblings, 0 replies; 83+ messages in thread
From: Junio C Hamano @ 2019-11-13  2:05 UTC (permalink / raw)
  To: Denton Liu; +Cc: Git Mailing List, James Coglan, SZEDER Gábor

Denton Liu <liu.denton@gmail.com> writes:

> .... 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.

^ permalink raw reply	[flat|nested] 83+ messages in thread

end of thread, other threads:[~2019-11-13  2:05 UTC | newest]

Thread overview: 83+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2019-10-10 16:13 [PATCH 00/11] Improve the readability of log --graph output James Coglan via GitGitGadget
2019-10-10 16:13 ` [PATCH 01/11] graph: automatically track visible width of `strbuf` James Coglan via GitGitGadget
2019-10-10 21:07   ` Johannes Schindelin
2019-10-10 23:05     ` Denton Liu
2019-10-11  0:49       ` Derrick Stolee
2019-10-11  1:42       ` Junio C Hamano
2019-10-11  5:01         ` Denton Liu
2019-10-11 16:02           ` Johannes Schindelin
2019-10-11 17:20             ` James Coglan
2019-10-12  0:27               ` Junio C Hamano
2019-10-12 16:22                 ` Johannes Schindelin
2019-10-14 12:55                 ` James Coglan
2019-10-14 13:01                   ` James Coglan
2019-10-16  2:15                   ` Junio C Hamano
2019-10-11  1:40     ` Junio C Hamano
2019-10-11 17:08     ` James Coglan
2019-10-10 16:13 ` [PATCH 02/11] graph: reuse `find_new_column_by_commit()` James Coglan via GitGitGadget
2019-10-10 16:13 ` [PATCH 03/11] graph: reduce duplication in `graph_insert_into_new_columns()` James Coglan via GitGitGadget
2019-10-10 16:13 ` [PATCH 04/11] graph: remove `mapping_idx` and `graph_update_width()` James Coglan via GitGitGadget
2019-10-10 16:13 ` [PATCH 05/11] graph: extract logic for moving to GRAPH_PRE_COMMIT state James Coglan via GitGitGadget
2019-10-10 16:13 ` [PATCH 06/11] graph: tidy up display of left-skewed merges James Coglan via GitGitGadget
2019-10-10 17:19   ` Derrick Stolee
2019-10-11 16:50     ` James Coglan
2019-10-12  1:36       ` Derrick Stolee
2019-10-14 13:11         ` James Coglan
2019-10-10 16:13 ` [PATCH 07/11] graph: commit and post-merge lines for " James Coglan via GitGitGadget
2019-10-10 17:49   ` Derrick Stolee
2019-10-11 17:04     ` James Coglan
2019-10-13  6:56       ` Jeff King
2019-10-14 15:38         ` James Coglan
2019-10-14 17:41           ` Derrick Stolee
2019-10-14 20:42           ` Johannes Schindelin
2019-10-10 16:13 ` [PATCH 08/11] graph: rename `new_mapping` to `old_mapping` James Coglan via GitGitGadget
2019-10-10 16:13 ` [PATCH 09/11] graph: smooth appearance of collapsing edges on commit lines James Coglan via GitGitGadget
2019-10-10 16:13 ` [PATCH 10/11] graph: flatten edges that join to their right neighbor James Coglan via GitGitGadget
2019-10-10 16:13 ` [PATCH 11/11] graph: fix coloring of octopus dashes James Coglan via GitGitGadget
2019-10-10 18:16   ` Denton Liu
2019-10-10 18:28     ` Denton Liu
2019-10-13  7:22     ` Jeff King
2019-10-10 17:54 ` [PATCH 00/11] Improve the readability of log --graph output Derrick Stolee
2019-10-13  7:15 ` Jeff King
2019-10-14 15:49   ` James Coglan
2019-10-15 23:40 ` [PATCH v2 00/13] " James Coglan via GitGitGadget
2019-10-15 23:40   ` [PATCH v2 01/13] graph: automatically track display width of graph lines James Coglan via GitGitGadget
2019-10-15 23:40   ` [PATCH v2 02/13] graph: handle line padding in `graph_next_line()` James Coglan via GitGitGadget
2019-10-15 23:40   ` [PATCH v2 03/13] graph: reuse `find_new_column_by_commit()` James Coglan via GitGitGadget
2019-10-15 23:40   ` [PATCH v2 04/13] graph: reduce duplication in `graph_insert_into_new_columns()` James Coglan via GitGitGadget
2019-10-15 23:40   ` [PATCH v2 05/13] graph: remove `mapping_idx` and `graph_update_width()` James Coglan via GitGitGadget
2019-10-15 23:40   ` [PATCH v2 06/13] graph: extract logic for moving to GRAPH_PRE_COMMIT state James Coglan via GitGitGadget
2019-10-15 23:40   ` [PATCH v2 07/13] graph: example of graph output that can be simplified James Coglan via GitGitGadget
2019-10-15 23:40   ` [PATCH v2 08/13] graph: tidy up display of left-skewed merges James Coglan via GitGitGadget
2019-10-15 23:41   ` [PATCH v2 09/13] graph: commit and post-merge lines for " James Coglan via GitGitGadget
2019-10-15 23:41   ` [PATCH v2 10/13] graph: rename `new_mapping` to `old_mapping` James Coglan via GitGitGadget
2019-10-15 23:41   ` [PATCH v2 11/13] graph: smooth appearance of collapsing edges on commit lines James Coglan via GitGitGadget
2019-10-15 23:41   ` [PATCH v2 12/13] graph: flatten edges that fuse with their right neighbor James Coglan via GitGitGadget
2019-10-15 23:41   ` [PATCH v2 13/13] graph: fix coloring of octopus dashes James Coglan via GitGitGadget
2019-10-15 23:47   ` [PATCH v3 00/13] Improve the readability of log --graph output James Coglan via GitGitGadget
2019-10-15 23:47     ` [PATCH v3 01/13] graph: automatically track display width of graph lines James Coglan via GitGitGadget
2019-10-16  3:35       ` Junio C Hamano
2019-10-16  5:10         ` Junio C Hamano
2019-10-15 23:47     ` [PATCH v3 02/13] graph: handle line padding in `graph_next_line()` James Coglan via GitGitGadget
2019-10-16  3:37       ` Junio C Hamano
2019-10-15 23:47     ` [PATCH v3 03/13] graph: reuse `find_new_column_by_commit()` James Coglan via GitGitGadget
2019-10-15 23:47     ` [PATCH v3 04/13] graph: reduce duplication in `graph_insert_into_new_columns()` James Coglan via GitGitGadget
2019-10-15 23:47     ` [PATCH v3 05/13] graph: remove `mapping_idx` and `graph_update_width()` James Coglan via GitGitGadget
2019-10-15 23:47     ` [PATCH v3 06/13] graph: extract logic for moving to GRAPH_PRE_COMMIT state James Coglan via GitGitGadget
2019-10-15 23:47     ` [PATCH v3 07/13] graph: example of graph output that can be simplified James Coglan via GitGitGadget
2019-10-17 12:30       ` Derrick Stolee
2019-10-18 15:21       ` SZEDER Gábor
2019-11-12  1:08         ` [PATCH] t4215: don't put git commands upstream of pipe Denton Liu
2019-11-12  6:57           ` Junio C Hamano
2019-11-12 10:54             ` SZEDER Gábor
2019-11-12 18:56           ` [PATCH v3] t4215: use helper function to check output Denton Liu
2019-11-13  2:05             ` Junio C Hamano
2019-10-15 23:47     ` [PATCH v3 08/13] graph: tidy up display of left-skewed merges James Coglan via GitGitGadget
2019-10-16  4:00       ` Junio C Hamano
2019-10-17 12:34         ` Derrick Stolee
2019-10-18  0:49           ` Junio C Hamano
2019-10-15 23:47     ` [PATCH v3 09/13] graph: commit and post-merge lines for " James Coglan via GitGitGadget
2019-10-15 23:47     ` [PATCH v3 10/13] graph: rename `new_mapping` to `old_mapping` James Coglan via GitGitGadget
2019-10-15 23:47     ` [PATCH v3 11/13] graph: smooth appearance of collapsing edges on commit lines James Coglan via GitGitGadget
2019-10-15 23:47     ` [PATCH v3 12/13] graph: flatten edges that fuse with their right neighbor James Coglan via GitGitGadget
2019-10-15 23:47     ` [PATCH v3 13/13] graph: fix coloring of octopus dashes James Coglan via GitGitGadget

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.