graph: tidy up display of left-skewed merges

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> Signed-off-by: Junio C Hamano <gitster@pobox.com>

James Coglan committed Oct 15, 2019 at 23:47 UTC 0f0f389f12029b1c3745f8ed7aacfe6b2fc7a6cc
3 files changed +144 -46
graph.c
+97 -28
@@ -202,6 +202,20 @@ struct git_graph {
202 * previous commit.
203 */
204 int prev_commit_index;
205 + /*
206 + * Which layout variant to use to display merge commits. If the
207 + * commit's first parent is known to be in a column to the left of the
208 + * merge, then this value is 0 and we use the layout on the left.
209 + * Otherwise, the value is 1 and the layout on the right is used. This
210 + * field tells us how many columns the first parent occupies.
211 + *
212 + * 0) 1)
213 + *
214 + * | | | *-. | | *---.
215 + * | |_|/|\ \ | | |\ \ \
216 + * |/| | | | | | | | | | *
217 + */
218 + int merge_layout;
219 /*
220 * The maximum number of columns that can be stored in the columns
221 * 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)
327 graph->prev_state = GRAPH_PADDING;
328 graph->commit_index = 0;
329 graph->prev_commit_index = 0;
330 + graph->merge_layout = 0;
331 graph->num_columns = 0;
332 graph->num_new_columns = 0;
333 graph->mapping_size = 0;
@@ -472,9 +487,11 @@ static int graph_find_new_column_by_commit(struct git_graph *graph,
487 }
488
489 static void graph_insert_into_new_columns(struct git_graph *graph,
475 - struct commit *commit)
490 + struct commit *commit,
491 + int idx)
492 {
493 int i = graph_find_new_column_by_commit(graph, commit);
494 + int mapping_idx;
495
496 /*
497 * 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,
503 graph->new_columns[i].color = graph_find_commit_color(graph, commit);
504 }
505
489 - graph->mapping[graph->width] = i;
490 - graph->width += 2;
506 + if (graph->num_parents > 1 && idx > -1 && graph->merge_layout == -1) {
507 + /*
508 + * If this is the first parent of a merge, choose a layout for
509 + * the merge line based on whether the parent appears in a
510 + * column to the left of the merge
511 + */
512 + int dist, shift;
513 +
514 + dist = idx - i;
515 + shift = (dist > 1) ? 2 * dist - 3 : 1;
516 +
517 + graph->merge_layout = (dist > 0) ? 0 : 1;
518 + mapping_idx = graph->width + (graph->merge_layout - 1) * shift;
519 + graph->width += 2 * graph->merge_layout;
520 + } else {
521 + mapping_idx = graph->width;
522 + graph->width += 2;
523 + }
524 +
525 + graph->mapping[mapping_idx] = i;
526 }
527
528 static void graph_update_columns(struct git_graph *graph)
@@ -553,6 +588,7 @@ static void graph_update_columns(struct git_graph *graph)
588 if (col_commit == graph->commit) {
589 seen_this = 1;
590 graph->commit_index = i;
591 + graph->merge_layout = -1;
592 for (parent = first_interesting_parent(graph);
593 parent;
594 parent = next_interesting_parent(graph, parent)) {
@@ -565,7 +601,7 @@ static void graph_update_columns(struct git_graph *graph)
601 !is_commit_in_columns) {
602 graph_increment_column_color(graph);
603 }
568 - graph_insert_into_new_columns(graph, parent->item);
604 + graph_insert_into_new_columns(graph, parent->item, i);
605 }
606 /*
607 * We always need to increment graph->width by at
@@ -576,7 +612,7 @@ static void graph_update_columns(struct git_graph *graph)
612 if (graph->num_parents == 0)
613 graph->width += 2;
614 } else {
579 - graph_insert_into_new_columns(graph, col_commit);
615 + graph_insert_into_new_columns(graph, col_commit, -1);
616 }
617 }
618
@@ -588,10 +624,36 @@ static void graph_update_columns(struct git_graph *graph)
624 graph->mapping_size--;
625 }
626
627 +static int graph_num_expansion_rows(struct git_graph *graph)
628 +{
629 + /*
630 + * Normally, we need two expansion rows for each dashed parent line from
631 + * an octopus merge:
632 + *
633 + * | *
634 + * | |\
635 + * | | \
636 + * | | \
637 + * | *-. \
638 + * | |\ \ \
639 + *
640 + * If the merge is skewed to the left, then its parents occupy one less
641 + * column, and we don't need as many expansion rows to route around it;
642 + * in some cases that means we don't need any expansion rows at all:
643 + *
644 + * | *
645 + * | |\
646 + * | * \
647 + * |/|\ \
648 + */
649 + return (graph->num_parents + graph->merge_layout - 3) * 2;
650 +}
651 +
652 static int graph_needs_pre_commit_line(struct git_graph *graph)
653 {
654 return graph->num_parents >= 3 &&
594 - graph->commit_index < (graph->num_columns - 1);
655 + graph->commit_index < (graph->num_columns - 1) &&
656 + graph->expansion_row < graph_num_expansion_rows(graph);
657 }
658
659 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
790 static void graph_output_pre_commit_line(struct git_graph *graph,
791 struct graph_line *line)
792 {
731 - int num_expansion_rows;
793 int i, seen_this;
794
795 /*
@@ -739,14 +800,13 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
800 * We need 2 extra rows for every parent over 2.
801 */
802 assert(graph->num_parents >= 3);
742 - num_expansion_rows = (graph->num_parents - 2) * 2;
803
804 /*
805 * graph->expansion_row tracks the current expansion row we are on.
806 * It should be in the range [0, num_expansion_rows - 1]
807 */
808 assert(0 <= graph->expansion_row &&
749 - graph->expansion_row < num_expansion_rows);
809 + graph->expansion_row < graph_num_expansion_rows(graph));
810
811 /*
812 * Output the row
@@ -786,7 +846,7 @@ static void graph_output_pre_commit_line(struct git_graph *graph,
846 * and move to state GRAPH_COMMIT if necessary
847 */
848 graph->expansion_row++;
789 - if (graph->expansion_row >= num_expansion_rows)
849 + if (!graph_needs_pre_commit_line(graph))
850 graph_update_state(graph, GRAPH_COMMIT);
851 }
852
@@ -824,7 +884,7 @@ static void graph_draw_octopus_merge(struct git_graph *graph, struct graph_line
884 * x 0 1 2 3
885 *
886 */
827 - const int dashless_parents = 2;
887 + const int dashless_parents = 3 - graph->merge_layout;
888 int dashful_parents = graph->num_parents - dashless_parents;
889
890 /*
@@ -832,9 +892,9 @@ static void graph_draw_octopus_merge(struct git_graph *graph, struct graph_line
892 * above) but sometimes the first parent goes into an existing column,
893 * like this:
894 *
835 - * | *---.
836 - * | |\ \ \
837 - * |/ / / /
895 + * | *-.
896 + * |/|\ \
897 + * | | | |
898 * x 0 1 2
899 *
900 * 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
985 graph_update_state(graph, GRAPH_COLLAPSING);
986 }
987
988 +const char merge_chars[] = {'/', '|', '\\'};
989 +
990 static void graph_output_post_merge_line(struct git_graph *graph, struct graph_line *line)
991 {
992 int seen_this = 0;
931 - int i, j;
993 + int i;
994 +
995 + struct commit_list *first_parent = first_interesting_parent(graph);
996 + int seen_parent = 0;
997
998 /*
999 * Output the post-merge row
@@ -951,30 +1016,34 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l
1016 * new_columns and use those to format the
1017 * edges.
1018 */
954 - struct commit_list *parents = NULL;
1019 + struct commit_list *parents = first_parent;
1020 int par_column;
1021 + int idx = graph->merge_layout;
1022 + char c;
1023 seen_this = 1;
957 - parents = first_interesting_parent(graph);
958 - assert(parents);
959 - par_column = graph_find_new_column_by_commit(graph, parents->item);
960 - assert(par_column >= 0);
961 -
962 - graph_line_write_column(line, &graph->new_columns[par_column], '|');
963 - for (j = 0; j < graph->num_parents - 1; j++) {
964 - parents = next_interesting_parent(graph, parents);
965 - assert(parents);
1024 +
1025 + for (; parents; parents = next_interesting_parent(graph, parents)) {
1026 par_column = graph_find_new_column_by_commit(graph, parents->item);
1027 assert(par_column >= 0);
968 - graph_line_write_column(line, &graph->new_columns[par_column], '\\');
969 - graph_line_addch(line, ' ');
1028 +
1029 + c = merge_chars[idx];
1030 + graph_line_write_column(line, &graph->new_columns[par_column], c);
1031 + if (idx == 2)
1032 + graph_line_addch(line, ' ');
1033 + else
1034 + idx++;
1035 }
1036 } else if (seen_this) {
1037 graph_line_write_column(line, col, '\\');
1038 graph_line_addch(line, ' ');
1039 } else {
1040 graph_line_write_column(line, col, '|');
976 - graph_line_addch(line, ' ');
1041 + if (graph->merge_layout != 0 || i != graph->commit_index - 1)
1042 + graph_line_addch(line, seen_parent ? '_' : ' ');
1043 }
1044 +
1045 + if (col_commit == first_parent->item)
1046 + seen_parent = 1;
1047 }
1048
1049 /*
t/t4214-log-graph-octopus.sh
+8 -12
@@ -26,9 +26,8 @@ test_expect_success 'set up merge history' '
26 test_expect_success 'log --graph with tricky octopus merge, no color' '
27 cat >expect.uncolored <<-\EOF &&
28 * left
29 - | *---. octopus-merge
30 - | |\ \ \
31 - |/ / / /
29 + | *-. octopus-merge
30 + |/|\ \
31 | | | * 4
32 | | * | 3
33 | | |/
@@ -47,9 +46,8 @@ test_expect_success 'log --graph with tricky octopus merge with colors' '
46 test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
47 cat >expect.colors <<-\EOF &&
48 * left
50 - <RED>|<RESET> *<BLUE>-<RESET><BLUE>-<RESET><MAGENTA>-<RESET><MAGENTA>.<RESET> octopus-merge
51 - <RED>|<RESET> <RED>|<RESET><YELLOW>\<RESET> <BLUE>\<RESET> <MAGENTA>\<RESET>
52 - <RED>|<RESET><RED>/<RESET> <YELLOW>/<RESET> <BLUE>/<RESET> <MAGENTA>/<RESET>
49 + <RED>|<RESET> *<MAGENTA>-<RESET><MAGENTA>.<RESET> octopus-merge
50 + <RED>|<RESET><RED>/<RESET><YELLOW>|<RESET><BLUE>\<RESET> <MAGENTA>\<RESET>
51 <RED>|<RESET> <YELLOW>|<RESET> <BLUE>|<RESET> * 4
52 <RED>|<RESET> <YELLOW>|<RESET> * <MAGENTA>|<RESET> 3
53 <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
145 cat >expect.uncolored <<-\EOF &&
146 * left
147 | * after-merge
150 - | *---. octopus-merge
151 - | |\ \ \
152 - |/ / / /
148 + | *-. octopus-merge
149 + |/|\ \
150 | | | * 4
151 | | * | 3
152 | | |/
@@ -169,9 +166,8 @@ test_expect_failure 'log --graph with tricky octopus merge and its child with co
166 cat >expect.colors <<-\EOF &&
167 * left
168 <RED>|<RESET> * after-merge
172 - <RED>|<RESET> *<MAGENTA>-<RESET><MAGENTA>-<RESET><CYAN>-<RESET><CYAN>.<RESET> octopus-merge
173 - <RED>|<RESET> <RED>|<RESET><BLUE>\<RESET> <MAGENTA>\<RESET> <CYAN>\<RESET>
174 - <RED>|<RESET><RED>/<RESET> <BLUE>/<RESET> <MAGENTA>/<RESET> <CYAN>/<RESET>
169 + <RED>|<RESET> *<CYAN>-<RESET><CYAN>.<RESET> octopus-merge
170 + <RED>|<RESET><RED>/<RESET><BLUE>|<RESET><MAGENTA>\<RESET> <CYAN>\<RESET>
171 <RED>|<RESET> <BLUE>|<RESET> <MAGENTA>|<RESET> * 4
172 <RED>|<RESET> <BLUE>|<RESET> * <CYAN>|<RESET> 3
173 <RED>|<RESET> <BLUE>|<RESET> <CYAN>|<RESET><CYAN>/<RESET>
t/t4215-log-skewed-merges.sh
+39 -6
@@ -11,12 +11,8 @@ test_expect_success 'log --graph with merge fusing with its left and right neigh
11 | * G
12 | |\
13 | | * F
14 - | | |
15 - | | \
16 - | *-. \ E
17 - | |\ \ \
18 - |/ / / /
19 - | | | /
14 + | * \ E
15 + |/|\ \
16 | | |/
17 | | * D
18 | * | C
@@ -40,4 +36,41 @@ test_expect_success 'log --graph with merge fusing with its left and right neigh
36 test_cmp expect actual
37 '
38
39 +test_expect_success 'log --graph with left-skewed merge' '
40 + cat >expect <<-\EOF &&
41 + *-----. 0_H
42 + |\ \ \ \
43 + | | | | * 0_G
44 + | |_|_|/|
45 + |/| | | |
46 + | | | * \ 0_F
47 + | |_|/|\ \
48 + |/| | | |/
49 + | | | | * 0_E
50 + | |_|_|/
51 + |/| | |
52 + | | * | 0_D
53 + | | |/
54 + | | * 0_C
55 + | |/
56 + |/|
57 + | * 0_B
58 + |/
59 + * 0_A
60 + EOF
61 +
62 + git checkout --orphan 0_p && test_commit 0_A &&
63 + git checkout -b 0_q 0_p && test_commit 0_B &&
64 + git checkout -b 0_r 0_p &&
65 + test_commit 0_C &&
66 + test_commit 0_D &&
67 + git checkout -b 0_s 0_p && test_commit 0_E &&
68 + git checkout -b 0_t 0_p && git merge --no-ff 0_r^ 0_s -m 0_F &&
69 + git checkout 0_p && git merge --no-ff 0_s -m 0_G &&
70 + git checkout @^ && git merge --no-ff 0_q 0_r 0_t 0_p -m 0_H &&
71 +
72 + git log --graph --pretty=tformat:%s | sed "s/ *$//" >actual &&
73 + test_cmp expect actual
74 +'
75 +
76 test_done