Add history graph API
This new API allows the commit history to be displayed as a text-based graphical representation. Signed-off-by: Adam Simpkins <adam@adamsimpkins.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>
Adam Simpkins committed
May 4, 2008 at 03:36 UTC
c12172d2eab91b79b8181b04ab5a5332a96e34a8
4 files changed
+1206
Documentation/technical/api-history-graph.txt
new
+176
@@ -0,0 +1,176 @@
1
+history graph API
2
+=================
3
+
4
+The graph API is used to draw a text-based representation of the commit
5
+history. The API generates the graph in a line-by-line fashion.
6
+
7
+Functions
8
+---------
9
+
10
+Core functions:
11
+
12
+* `graph_init()` creates a new `struct git_graph`
13
+
14
+* `graph_release()` destroys a `struct git_graph`, and frees the memory
15
+ associated with it.
16
+
17
+* `graph_update()` moves the graph to a new commit.
18
+
19
+* `graph_next_line()` outputs the next line of the graph into a strbuf. It
20
+ does not add a terminating newline.
21
+
22
+* `graph_padding_line()` outputs a line of vertical padding in the graph. It
23
+ is similar to `graph_next_line()`, but is guaranteed to never print the line
24
+ containing the current commit. Where `graph_next_line()` would print the
25
+ commit line next, `graph_padding_line()` prints a line that simply extends
26
+ all branch lines downwards one row, leaving their positions unchanged.
27
+
28
+* `graph_is_commit_finished()` determines if the graph has output all lines
29
+ necessary for the current commit. If `graph_update()` is called before all
30
+ lines for the current commit have been printed, the next call to
31
+ `graph_next_line()` will output an ellipsis, to indicate that a portion of
32
+ the graph was omitted.
33
+
34
+The following utility functions are wrappers around `graph_next_line()` and
35
+`graph_is_commit_finished()`. They always print the output to stdout.
36
+They can all be called with a NULL graph argument, in which case no graph
37
+output will be printed.
38
+
39
+* `graph_show_commit()` calls `graph_next_line()` until it returns non-zero.
40
+ This prints all graph lines up to, and including, the line containing this
41
+ commit. Output is printed to stdout. The last line printed does not contain
42
+ a terminating newline. This should not be called if the commit line has
43
+ already been printed, or it will loop forever.
44
+
45
+* `graph_show_oneline()` calls `graph_next_line()` and prints the result to
46
+ stdout. The line printed does not contain a terminating newline.
47
+
48
+* `graph_show_padding()` calls `graph_padding_line()` and prints the result to
49
+ stdout. The line printed does not contain a terminating newline.
50
+
51
+* `graph_show_remainder()` calls `graph_next_line()` until
52
+ `graph_is_commit_finished()` returns non-zero. Output is printed to stdout.
53
+ The last line printed does not contain a terminating newline. Returns 1 if
54
+ output was printed, and 0 if no output was necessary.
55
+
56
+* `graph_show_strbuf()` prints the specified strbuf to stdout, prefixing all
57
+ lines but the first with a graph line. The caller is responsible for
58
+ ensuring graph output for the first line has already been printed to stdout.
59
+ (This can be done with `graph_show_commit()` or `graph_show_oneline()`.) If
60
+ a NULL graph is supplied, the strbuf is printed as-is.
61
+
62
+* `graph_show_commit_msg()` is similar to `graph_show_strbuf()`, but it also
63
+ prints the remainder of the graph, if more lines are needed after the strbuf
64
+ ends. It is better than directly calling `graph_show_strbuf()` followed by
65
+ `graph_show_remainder()` since it properly handles buffers that do not end in
66
+ a terminating newline. The output printed by `graph_show_commit_msg()` will
67
+ end in a newline if and only if the strbuf ends in a newline.
68
+
69
+Data structure
70
+--------------
71
+`struct git_graph` is an opaque data type used to store the current graph
72
+state.
73
+
74
+Calling sequence
75
+----------------
76
+
77
+* Create a `struct git_graph` by calling `graph_init()`.
78
+
79
+* Use the revision walking API to walk through a group of contiguous commits.
80
+
81
+* For each commit traversed, call `graph_update()` to move the graph to the
82
+ next commit. Once `graph_update()` has been called, call `graph_next_line()`
83
+ repeatedly, until `graph_is_commit_finished()` returns non-zero. Each call
84
+ to `graph_next_line()` will output a single line of the graph. The resulting
85
+ lines will not contain any newlines. `graph_next_line()` returns 1 if the
86
+ resulting line contains the current commit, or 0 if this is merely a line
87
+ needed to adjust the graph before or after the current commit. This return
88
+ value can be used to determine where to print the commit summary information
89
+ alongside the graph output.
90
+
91
+Limitations
92
+-----------
93
+
94
+* `graph_update()` must be called with commits in topological order. It should
95
+ not be called on a commit if it has already been invoked with an ancestor of
96
+ that commit, or the graph output will be incorrect.
97
+
98
+* `graph_update()` must be called on a contiguous group of commits. If
99
+ `graph_update()` is called on a particular commit, it should later be called
100
+ on all parents of that commit. Parents must not be skipped, or the graph
101
+ output will appear incorrect.
102
++
103
+`graph_update()` may be used on a pruned set of commits only if the parent list
104
+has been rewritten so as to include only ancestors from the pruned set.
105
+
106
+* The graph API does not currently support reverse commit ordering. In
107
+ order to implement reverse ordering, the graphing API needs an
108
+ (efficient) mechanism to find the children of a commit.
109
+
110
+Sample usage
111
+------------
112
+
113
+------------
114
+struct commit *commit;
115
+struct git_graph *graph = graph_init();
116
+
117
+while ((commit = get_revision(opts)) != NULL) {
118
+ graph_update(graph, commit);
119
+ while (!graph_is_commit_finished(graph))
120
+ {
121
+ struct strbuf sb;
122
+ int is_commit_line;
123
+
124
+ strbuf_init(&sb, 0);
125
+ is_commit_line = graph_next_line(graph, &sb);
126
+ fputs(sb.buf, stdout);
127
+
128
+ if (is_commit_line)
129
+ log_tree_commit(opts, commit);
130
+ else
131
+ putchar(opts->diffopt.line_termination);
132
+ }
133
+}
134
+
135
+graph_release(graph);
136
+------------
137
+
138
+Sample output
139
+-------------
140
+
141
+The following is an example of the output from the graph API. This output does
142
+not include any commit summary information--callers are responsible for
143
+outputting that information, if desired.
144
+
145
+------------
146
+*
147
+*
148
+M
149
+|\
150
+* |
151
+| | *
152
+| \ \
153
+| \ \
154
+M-. \ \
155
+|\ \ \ \
156
+| | * | |
157
+| | | | | *
158
+| | | | | *
159
+| | | | | M
160
+| | | | | |\
161
+| | | | | | *
162
+| * | | | | |
163
+| | | | | M \
164
+| | | | | |\ |
165
+| | | | * | | |
166
+| | | | * | | |
167
+* | | | | | | |
168
+| |/ / / / / /
169
+|/| / / / / /
170
+* | | | | | |
171
+|/ / / / / /
172
+* | | | | |
173
+| | | | | *
174
+| | | | |/
175
+| | | | *
176
+------------
Makefile
+2
@@ -346,6 +346,7 @@ LIB_H += diff.h
346
LIB_H += dir.h
347
LIB_H += fsck.h
348
LIB_H += git-compat-util.h
349
+LIB_H += graph.h
350
LIB_H += grep.h
351
LIB_H += hash.h
352
LIB_H += list-objects.h
@@ -411,6 +412,7 @@ LIB_OBJS += entry.o
412
LIB_OBJS += environment.o
413
LIB_OBJS += exec_cmd.o
414
LIB_OBJS += fsck.o
415
+LIB_OBJS += graph.o
416
LIB_OBJS += grep.o
417
LIB_OBJS += hash.o
418
LIB_OBJS += help.o
graph.c
new
+907
@@ -0,0 +1,907 @@
1
+#include "cache.h"
2
+#include "commit.h"
3
+#include "graph.h"
4
+#include "diff.h"
5
+#include "revision.h"
6
+
7
+/*
8
+ * TODO:
9
+ * - Add colors to the graph.
10
+ * Pick a color for each column, and print all characters
11
+ * in that column with the specified color.
12
+ *
13
+ * - Limit the number of columns, similar to the way gitk does.
14
+ * If we reach more than a specified number of columns, omit
15
+ * sections of some columns.
16
+ *
17
+ * - The output during the GRAPH_PRE_COMMIT and GRAPH_COLLAPSING states
18
+ * could be made more compact by printing horizontal lines, instead of
19
+ * long diagonal lines. For example, during collapsing, something like
20
+ * this: instead of this:
21
+ * | | | | | | | | | |
22
+ * | |_|_|/ | | | |/
23
+ * |/| | | | | |/|
24
+ * | | | | | |/| |
25
+ * |/| | |
26
+ * | | | |
27
+ *
28
+ * If there are several parallel diagonal lines, they will need to be
29
+ * replaced with horizontal lines on subsequent rows.
30
+ */
31
+
32
+struct column {
33
+ /*
34
+ * The parent commit of this column.
35
+ */
36
+ struct commit *commit;
37
+ /*
38
+ * XXX: Once we add support for colors, struct column could also
39
+ * contain the color of its branch line.
40
+ */
41
+};
42
+
43
+enum graph_state {
44
+ GRAPH_PADDING,
45
+ GRAPH_SKIP,
46
+ GRAPH_PRE_COMMIT,
47
+ GRAPH_COMMIT,
48
+ GRAPH_POST_MERGE,
49
+ GRAPH_COLLAPSING
50
+};
51
+
52
+struct git_graph {
53
+ /*
54
+ * The commit currently being processed
55
+ */
56
+ struct commit *commit;
57
+ /*
58
+ * The number of parents this commit has.
59
+ * (Stored so we don't have to walk over them each time we need
60
+ * this number)
61
+ */
62
+ int num_parents;
63
+ /*
64
+ * The next expansion row to print
65
+ * when state is GRAPH_PRE_COMMIT
66
+ */
67
+ int expansion_row;
68
+ /*
69
+ * The current output state.
70
+ * This tells us what kind of line graph_next_line() should output.
71
+ */
72
+ enum graph_state state;
73
+ /*
74
+ * The maximum number of columns that can be stored in the columns
75
+ * and new_columns arrays. This is also half the number of entries
76
+ * that can be stored in the mapping and new_mapping arrays.
77
+ */
78
+ int column_capacity;
79
+ /*
80
+ * The number of columns (also called "branch lines" in some places)
81
+ */
82
+ int num_columns;
83
+ /*
84
+ * The number of columns in the new_columns array
85
+ */
86
+ int num_new_columns;
87
+ /*
88
+ * The number of entries in the mapping array
89
+ */
90
+ int mapping_size;
91
+ /*
92
+ * The column state before we output the current commit.
93
+ */
94
+ struct column *columns;
95
+ /*
96
+ * The new column state after we output the current commit.
97
+ * Only valid when state is GRAPH_COLLAPSING.
98
+ */
99
+ struct column *new_columns;
100
+ /*
101
+ * An array that tracks the current state of each
102
+ * character in the output line during state GRAPH_COLLAPSING.
103
+ * Each entry is -1 if this character is empty, or a non-negative
104
+ * integer if the character contains a branch line. The value of
105
+ * the integer indicates the target position for this branch line.
106
+ * (I.e., this array maps the current column positions to their
107
+ * desired positions.)
108
+ *
109
+ * The maximum capacity of this array is always
110
+ * sizeof(int) * 2 * column_capacity.
111
+ */
112
+ int *mapping;
113
+ /*
114
+ * A temporary array for computing the next mapping state
115
+ * while we are outputting a mapping line. This is stored as part
116
+ * of the git_graph simply so we don't have to allocate a new
117
+ * temporary array each time we have to output a collapsing line.
118
+ */
119
+ int *new_mapping;
120
+};
121
+
122
+struct git_graph *graph_init(void)
123
+{
124
+ struct git_graph *graph = xmalloc(sizeof(struct git_graph));
125
+ graph->commit = NULL;
126
+ graph->num_parents = 0;
127
+ graph->expansion_row = 0;
128
+ graph->state = GRAPH_PADDING;
129
+ graph->num_columns = 0;
130
+ graph->num_new_columns = 0;
131
+ graph->mapping_size = 0;
132
+
133
+ /*
134
+ * Allocate a reasonably large default number of columns
135
+ * We'll automatically grow columns later if we need more room.
136
+ */
137
+ graph->column_capacity = 30;
138
+ graph->columns = xmalloc(sizeof(struct column) *
139
+ graph->column_capacity);
140
+ graph->new_columns = xmalloc(sizeof(struct column) *
141
+ graph->column_capacity);
142
+ graph->mapping = xmalloc(sizeof(int) * 2 * graph->column_capacity);
143
+ graph->new_mapping = xmalloc(sizeof(int) * 2 * graph->column_capacity);
144
+
145
+ return graph;
146
+}
147
+
148
+void graph_release(struct git_graph *graph)
149
+{
150
+ free(graph->columns);
151
+ free(graph->new_columns);
152
+ free(graph->mapping);
153
+ free(graph);
154
+}
155
+
156
+static void graph_ensure_capacity(struct git_graph *graph, int num_columns)
157
+{
158
+ if (graph->column_capacity >= num_columns)
159
+ return;
160
+
161
+ do {
162
+ graph->column_capacity *= 2;
163
+ } while (graph->column_capacity < num_columns);
164
+
165
+ graph->columns = xrealloc(graph->columns,
166
+ sizeof(struct column) *
167
+ graph->column_capacity);
168
+ graph->new_columns = xrealloc(graph->new_columns,
169
+ sizeof(struct column) *
170
+ graph->column_capacity);
171
+ graph->mapping = xrealloc(graph->mapping,
172
+ sizeof(int) * 2 * graph->column_capacity);
173
+ graph->new_mapping = xrealloc(graph->new_mapping,
174
+ sizeof(int) * 2 * graph->column_capacity);
175
+}
176
+
177
+static void graph_insert_into_new_columns(struct git_graph *graph,
178
+ struct commit *commit,
179
+ int *mapping_index)
180
+{
181
+ int i;
182
+
183
+ /*
184
+ * Ignore uinteresting and pruned commits
185
+ */
186
+ if (commit->object.flags & (UNINTERESTING | TREESAME))
187
+ return;
188
+
189
+ /*
190
+ * If the commit is already in the new_columns list, we don't need to
191
+ * add it. Just update the mapping correctly.
192
+ */
193
+ for (i = 0; i < graph->num_new_columns; i++) {
194
+ if (graph->new_columns[i].commit == commit) {
195
+ graph->mapping[*mapping_index] = i;
196
+ *mapping_index += 2;
197
+ return;
198
+ }
199
+ }
200
+
201
+ /*
202
+ * This commit isn't already in new_columns. Add it.
203
+ */
204
+ graph->new_columns[graph->num_new_columns].commit = commit;
205
+ graph->mapping[*mapping_index] = graph->num_new_columns;
206
+ *mapping_index += 2;
207
+ graph->num_new_columns++;
208
+}
209
+
210
+static void graph_update_columns(struct git_graph *graph)
211
+{
212
+ struct commit_list *parent;
213
+ struct column *tmp_columns;
214
+ int max_new_columns;
215
+ int mapping_idx;
216
+ int i, seen_this;
217
+
218
+ /*
219
+ * Swap graph->columns with graph->new_columns
220
+ * graph->columns contains the state for the previous commit,
221
+ * and new_columns now contains the state for our commit.
222
+ *
223
+ * We'll re-use the old columns array as storage to compute the new
224
+ * columns list for the commit after this one.
225
+ */
226
+ tmp_columns = graph->columns;
227
+ graph->columns = graph->new_columns;
228
+ graph->num_columns = graph->num_new_columns;
229
+
230
+ graph->new_columns = tmp_columns;
231
+ graph->num_new_columns = 0;
232
+
233
+ /*
234
+ * Now update new_columns and mapping with the information for the
235
+ * commit after this one.
236
+ *
237
+ * First, make sure we have enough room. At most, there will
238
+ * be graph->num_columns + graph->num_parents columns for the next
239
+ * commit.
240
+ */
241
+ max_new_columns = graph->num_columns + graph->num_parents;
242
+ graph_ensure_capacity(graph, max_new_columns);
243
+
244
+ /*
245
+ * Clear out graph->mapping
246
+ */
247
+ graph->mapping_size = 2 * max_new_columns;
248
+ for (i = 0; i < graph->mapping_size; i++)
249
+ graph->mapping[i] = -1;
250
+
251
+ /*
252
+ * Populate graph->new_columns and graph->mapping
253
+ *
254
+ * Some of the parents of this commit may already be in
255
+ * graph->columns. If so, graph->new_columns should only contain a
256
+ * single entry for each such commit. graph->mapping should
257
+ * contain information about where each current branch line is
258
+ * supposed to end up after the collapsing is performed.
259
+ */
260
+ seen_this = 0;
261
+ mapping_idx = 0;
262
+ for (i = 0; i <= graph->num_columns; i++) {
263
+ struct commit *col_commit;
264
+ if (i == graph->num_columns) {
265
+ if (seen_this)
266
+ break;
267
+ col_commit = graph->commit;
268
+ } else {
269
+ col_commit = graph->columns[i].commit;
270
+ }
271
+
272
+ if (col_commit == graph->commit) {
273
+ seen_this = 1;
274
+ for (parent = graph->commit->parents;
275
+ parent;
276
+ parent = parent->next) {
277
+ graph_insert_into_new_columns(graph,
278
+ parent->item,
279
+ &mapping_idx);
280
+ }
281
+ } else {
282
+ graph_insert_into_new_columns(graph, col_commit,
283
+ &mapping_idx);
284
+ }
285
+ }
286
+
287
+ /*
288
+ * Shrink mapping_size to be the minimum necessary
289
+ */
290
+ while (graph->mapping_size > 1 &&
291
+ graph->mapping[graph->mapping_size - 1] < 0)
292
+ graph->mapping_size--;
293
+}
294
+
295
+void graph_update(struct git_graph *graph, struct commit *commit)
296
+{
297
+ struct commit_list *parent;
298
+
299
+ /*
300
+ * Set the new commit
301
+ */
302
+ graph->commit = commit;
303
+
304
+ /*
305
+ * Count how many parents this commit has
306
+ */
307
+ graph->num_parents = 0;
308
+ for (parent = commit->parents; parent; parent = parent->next)
309
+ graph->num_parents++;
310
+
311
+ /*
312
+ * Call graph_update_columns() to update
313
+ * columns, new_columns, and mapping.
314
+ */
315
+ graph_update_columns(graph);
316
+
317
+ graph->expansion_row = 0;
318
+
319
+ /*
320
+ * Update graph->state.
321
+ *
322
+ * If the previous commit didn't get to the GRAPH_PADDING state,
323
+ * it never finished its output. Goto GRAPH_SKIP, to print out
324
+ * a line to indicate that portion of the graph is missing.
325
+ *
326
+ * Otherwise, if there are 3 or more parents, we need to print
327
+ * extra rows before the commit, to expand the branch lines around
328
+ * it and make room for it.
329
+ *
330
+ * If there are less than 3 parents, we can immediately print the
331
+ * commit line.
332
+ */
333
+ if (graph->state != GRAPH_PADDING)
334
+ graph->state = GRAPH_SKIP;
335
+ else if (graph->num_parents >= 3)
336
+ graph->state = GRAPH_PRE_COMMIT;
337
+ else
338
+ graph->state = GRAPH_COMMIT;
339
+}
340
+
341
+static int graph_is_mapping_correct(struct git_graph *graph)
342
+{
343
+ int i;
344
+
345
+ /*
346
+ * The mapping is up to date if each entry is at its target,
347
+ * or is 1 greater than its target.
348
+ * (If it is 1 greater than the target, '/' will be printed, so it
349
+ * will look correct on the next row.)
350
+ */
351
+ for (i = 0; i < graph->mapping_size; i++) {
352
+ int target = graph->mapping[i];
353
+ if (target < 0)
354
+ continue;
355
+ if (target == (i / 2))
356
+ continue;
357
+ return 0;
358
+ }
359
+
360
+ return 1;
361
+}
362
+
363
+static void graph_pad_horizontally(struct git_graph *graph, struct strbuf *sb)
364
+{
365
+ /*
366
+ * Add additional spaces to the end of the strbuf, so that all
367
+ * lines for a particular commit have the same width.
368
+ *
369
+ * This way, fields printed to the right of the graph will remain
370
+ * aligned for the entire commit.
371
+ *
372
+ * This computation results in 3 extra space to the right in most
373
+ * cases, but only 1 extra space if the commit doesn't have any
374
+ * children that have already been displayed in the graph (i.e.,
375
+ * if the current commit isn't in graph->columns).
376
+ */
377
+ size_t extra;
378
+ size_t final_width = graph->num_columns + graph->num_parents;
379
+ if (graph->num_parents < 1)
380
+ final_width++;
381
+ final_width *= 2;
382
+
383
+ if (sb->len >= final_width)
384
+ return;
385
+
386
+ extra = final_width - sb->len;
387
+ strbuf_addf(sb, "%*s", (int) extra, "");
388
+}
389
+
390
+static void graph_output_padding_line(struct git_graph *graph,
391
+ struct strbuf *sb)
392
+{
393
+ int i;
394
+
395
+ /*
396
+ * We could conceivable be called with a NULL commit
397
+ * if our caller has a bug, and invokes graph_next_line()
398
+ * immediately after graph_init(), without first calling
399
+ * graph_update(). Return without outputting anything in this
400
+ * case.
401
+ */
402
+ if (!graph->commit)
403
+ return;
404
+
405
+ /*
406
+ * Output a padding row, that leaves all branch lines unchanged
407
+ */
408
+ for (i = 0; i < graph->num_new_columns; i++) {
409
+ strbuf_addstr(sb, "| ");
410
+ }
411
+
412
+ graph_pad_horizontally(graph, sb);
413
+}
414
+
415
+static void graph_output_skip_line(struct git_graph *graph, struct strbuf *sb)
416
+{
417
+ /*
418
+ * Output an ellipsis to indicate that a portion
419
+ * of the graph is missing.
420
+ */
421
+ strbuf_addstr(sb, "...");
422
+ graph_pad_horizontally(graph, sb);
423
+
424
+ if (graph->num_parents >= 3)
425
+ graph->state = GRAPH_PRE_COMMIT;
426
+ else
427
+ graph->state = GRAPH_COMMIT;
428
+}
429
+
430
+static void graph_output_pre_commit_line(struct git_graph *graph,
431
+ struct strbuf *sb)
432
+{
433
+ int num_expansion_rows;
434
+ int i, seen_this;
435
+
436
+ /*
437
+ * This function formats a row that increases the space around a commit
438
+ * with multiple parents, to make room for it. It should only be
439
+ * called when there are 3 or more parents.
440
+ *
441
+ * We need 2 extra rows for every parent over 2.
442
+ */
443
+ assert(graph->num_parents >= 3);
444
+ num_expansion_rows = (graph->num_parents - 2) * 2;
445
+
446
+ /*
447
+ * graph->expansion_row tracks the current expansion row we are on.
448
+ * It should be in the range [0, num_expansion_rows - 1]
449
+ */
450
+ assert(0 <= graph->expansion_row &&
451
+ graph->expansion_row < num_expansion_rows);
452
+
453
+ /*
454
+ * Output the row
455
+ */
456
+ seen_this = 0;
457
+ for (i = 0; i < graph->num_columns; i++) {
458
+ struct column *col = &graph->columns[i];
459
+ if (col->commit == graph->commit) {
460
+ seen_this = 1;
461
+ strbuf_addf(sb, "| %*s", graph->expansion_row, "");
462
+ } else if (seen_this) {
463
+ strbuf_addstr(sb, "\\ ");
464
+ } else {
465
+ strbuf_addstr(sb, "| ");
466
+ }
467
+ }
468
+
469
+ graph_pad_horizontally(graph, sb);
470
+
471
+ /*
472
+ * Increment graph->expansion_row,
473
+ * and move to state GRAPH_COMMIT if necessary
474
+ */
475
+ graph->expansion_row++;
476
+ if (graph->expansion_row >= num_expansion_rows)
477
+ graph->state = GRAPH_COMMIT;
478
+}
479
+
480
+void graph_output_commit_line(struct git_graph *graph, struct strbuf *sb)
481
+{
482
+ int seen_this = 0;
483
+ int i, j;
484
+
485
+ /*
486
+ * Output the row containing this commit
487
+ * Iterate up to and including graph->num_columns,
488
+ * since the current commit may not be in any of the existing
489
+ * columns. (This happens when the current commit doesn't have any
490
+ * children that we have already processed.)
491
+ */
492
+ seen_this = 0;
493
+ for (i = 0; i <= graph->num_columns; i++) {
494
+ struct commit *col_commit;
495
+ if (i == graph->num_columns) {
496
+ if (seen_this)
497
+ break;
498
+ col_commit = graph->commit;
499
+ } else {
500
+ col_commit = graph->columns[i].commit;
501
+ }
502
+
503
+ if (col_commit == graph->commit) {
504
+ seen_this = 1;
505
+ if (graph->num_parents > 1)
506
+ strbuf_addch(sb, 'M');
507
+ else
508
+ strbuf_addch(sb, '*');
509
+
510
+ if (graph->num_parents < 2)
511
+ strbuf_addch(sb, ' ');
512
+ else if (graph->num_parents == 2)
513
+ strbuf_addstr(sb, " ");
514
+ else {
515
+ int num_dashes =
516
+ ((graph->num_parents - 2) * 2) - 1;
517
+ for (j = 0; j < num_dashes; j++)
518
+ strbuf_addch(sb, '-');
519
+ strbuf_addstr(sb, ". ");
520
+ }
521
+ } else if (seen_this && (graph->num_parents > 1)) {
522
+ strbuf_addstr(sb, "\\ ");
523
+ } else {
524
+ strbuf_addstr(sb, "| ");
525
+ }
526
+ }
527
+
528
+ graph_pad_horizontally(graph, sb);
529
+
530
+ /*
531
+ * Update graph->state
532
+ */
533
+ if (graph->num_parents > 1)
534
+ graph->state = GRAPH_POST_MERGE;
535
+ else if (graph_is_mapping_correct(graph))
536
+ graph->state = GRAPH_PADDING;
537
+ else
538
+ graph->state = GRAPH_COLLAPSING;
539
+}
540
+
541
+void graph_output_post_merge_line(struct git_graph *graph, struct strbuf *sb)
542
+{
543
+ int seen_this = 0;
544
+ int i, j;
545
+
546
+ /*
547
+ * Output the post-merge row
548
+ */
549
+ for (i = 0; i <= graph->num_columns; i++) {
550
+ struct commit *col_commit;
551
+ if (i == graph->num_columns) {
552
+ if (seen_this)
553
+ break;
554
+ col_commit = graph->commit;
555
+ } else {
556
+ col_commit = graph->columns[i].commit;
557
+ }
558
+
559
+ if (col_commit == graph->commit) {
560
+ seen_this = 1;
561
+ strbuf_addch(sb, '|');
562
+ for (j = 0; j < graph->num_parents - 1; j++)
563
+ strbuf_addstr(sb, "\\ ");
564
+ if (graph->num_parents == 2)
565
+ strbuf_addch(sb, ' ');
566
+ } else if (seen_this && (graph->num_parents > 2)) {
567
+ strbuf_addstr(sb, "\\ ");
568
+ } else {
569
+ strbuf_addstr(sb, "| ");
570
+ }
571
+ }
572
+
573
+ graph_pad_horizontally(graph, sb);
574
+
575
+ /*
576
+ * Update graph->state
577
+ */
578
+ if (graph_is_mapping_correct(graph))
579
+ graph->state = GRAPH_PADDING;
580
+ else
581
+ graph->state = GRAPH_COLLAPSING;
582
+}
583
+
584
+void graph_output_collapsing_line(struct git_graph *graph, struct strbuf *sb)
585
+{
586
+ int i;
587
+ int *tmp_mapping;
588
+
589
+ /*
590
+ * Clear out the new_mapping array
591
+ */
592
+ for (i = 0; i < graph->mapping_size; i++)
593
+ graph->new_mapping[i] = -1;
594
+
595
+ for (i = 0; i < graph->mapping_size; i++) {
596
+ int target = graph->mapping[i];
597
+ if (target < 0)
598
+ continue;
599
+
600
+ /*
601
+ * Since update_columns() always inserts the leftmost
602
+ * column first, each branch's target location should
603
+ * always be either its current location or to the left of
604
+ * its current location.
605
+ *
606
+ * We never have to move branches to the right. This makes
607
+ * the graph much more legible, since whenever branches
608
+ * cross, only one is moving directions.
609
+ */
610
+ assert(target * 2 <= i);
611
+
612
+ if (target * 2 == i) {
613
+ /*
614
+ * This column is already in the
615
+ * correct place
616
+ */
617
+ assert(graph->new_mapping[i] == -1);
618
+ graph->new_mapping[i] = target;
619
+ } else if (graph->new_mapping[i - 1] < 0) {
620
+ /*
621
+ * Nothing is to the left.
622
+ * Move to the left by one
623
+ */
624
+ graph->new_mapping[i - 1] = target;
625
+ } else if (graph->new_mapping[i - 1] == target) {
626
+ /*
627
+ * There is a branch line to our left
628
+ * already, and it is our target. We
629
+ * combine with this line, since we share
630
+ * the same parent commit.
631
+ *
632
+ * We don't have to add anything to the
633
+ * output or new_mapping, since the
634
+ * existing branch line has already taken
635
+ * care of it.
636
+ */
637
+ } else {
638
+ /*
639
+ * There is a branch line to our left,
640
+ * but it isn't our target. We need to
641
+ * cross over it.
642
+ *
643
+ * The space just to the left of this
644
+ * branch should always be empty.
645
+ */
646
+ assert(graph->new_mapping[i - 1] > target);
647
+ assert(graph->new_mapping[i - 2] < 0);
648
+ graph->new_mapping[i - 2] = target;
649
+ }
650
+ }
651
+
652
+ /*
653
+ * The new mapping may be 1 smaller than the old mapping
654
+ */
655
+ if (graph->new_mapping[graph->mapping_size - 1] < 0)
656
+ graph->mapping_size--;
657
+
658
+ /*
659
+ * Output out a line based on the new mapping info
660
+ */
661
+ for (i = 0; i < graph->mapping_size; i++) {
662
+ int target = graph->new_mapping[i];
663
+ if (target < 0)
664
+ strbuf_addch(sb, ' ');
665
+ else if (target * 2 == i)
666
+ strbuf_addch(sb, '|');
667
+ else
668
+ strbuf_addch(sb, '/');
669
+ }
670
+
671
+ graph_pad_horizontally(graph, sb);
672
+
673
+ /*
674
+ * Swap mapping and new_mapping
675
+ */
676
+ tmp_mapping = graph->mapping;
677
+ graph->mapping = graph->new_mapping;
678
+ graph->new_mapping = tmp_mapping;
679
+
680
+ /*
681
+ * If graph->mapping indicates that all of the branch lines
682
+ * are already in the correct positions, we are done.
683
+ * Otherwise, we need to collapse some branch lines together.
684
+ */
685
+ if (graph_is_mapping_correct(graph))
686
+ graph->state = GRAPH_PADDING;
687
+}
688
+
689
+int graph_next_line(struct git_graph *graph, struct strbuf *sb)
690
+{
691
+ switch (graph->state) {
692
+ case GRAPH_PADDING:
693
+ graph_output_padding_line(graph, sb);
694
+ return 0;
695
+ case GRAPH_SKIP:
696
+ graph_output_skip_line(graph, sb);
697
+ return 0;
698
+ case GRAPH_PRE_COMMIT:
699
+ graph_output_pre_commit_line(graph, sb);
700
+ return 0;
701
+ case GRAPH_COMMIT:
702
+ graph_output_commit_line(graph, sb);
703
+ return 1;
704
+ case GRAPH_POST_MERGE:
705
+ graph_output_post_merge_line(graph, sb);
706
+ return 0;
707
+ case GRAPH_COLLAPSING:
708
+ graph_output_collapsing_line(graph, sb);
709
+ return 0;
710
+ }
711
+
712
+ assert(0);
713
+ return 0;
714
+}
715
+
716
+void graph_padding_line(struct git_graph *graph, struct strbuf *sb)
717
+{
718
+ int i, j;
719
+
720
+ if (graph->state != GRAPH_COMMIT) {
721
+ graph_next_line(graph, sb);
722
+ return;
723
+ }
724
+
725
+ /*
726
+ * Output the row containing this commit
727
+ * Iterate up to and including graph->num_columns,
728
+ * since the current commit may not be in any of the existing
729
+ * columns. (This happens when the current commit doesn't have any
730
+ * children that we have already processed.)
731
+ */
732
+ for (i = 0; i < graph->num_columns; i++) {
733
+ struct commit *col_commit = graph->columns[i].commit;
734
+ if (col_commit == graph->commit) {
735
+ strbuf_addch(sb, '|');
736
+
737
+ if (graph->num_parents < 3)
738
+ strbuf_addch(sb, ' ');
739
+ else {
740
+ int num_spaces = ((graph->num_parents - 2) * 2);
741
+ for (j = 0; j < num_spaces; j++)
742
+ strbuf_addch(sb, ' ');
743
+ }
744
+ } else {
745
+ strbuf_addstr(sb, "| ");
746
+ }
747
+ }
748
+
749
+ graph_pad_horizontally(graph, sb);
750
+}
751
+
752
+int graph_is_commit_finished(struct git_graph const *graph)
753
+{
754
+ return (graph->state == GRAPH_PADDING);
755
+}
756
+
757
+void graph_show_commit(struct git_graph *graph)
758
+{
759
+ struct strbuf msgbuf;
760
+ int shown_commit_line = 0;
761
+
762
+ if (!graph)
763
+ return;
764
+
765
+ strbuf_init(&msgbuf, 0);
766
+
767
+ while (!shown_commit_line) {
768
+ shown_commit_line = graph_next_line(graph, &msgbuf);
769
+ fwrite(msgbuf.buf, sizeof(char), msgbuf.len, stdout);
770
+ if (!shown_commit_line)
771
+ putchar('\n');
772
+ strbuf_setlen(&msgbuf, 0);
773
+ }
774
+
775
+ strbuf_release(&msgbuf);
776
+}
777
+
778
+void graph_show_oneline(struct git_graph *graph)
779
+{
780
+ struct strbuf msgbuf;
781
+
782
+ if (!graph)
783
+ return;
784
+
785
+ strbuf_init(&msgbuf, 0);
786
+ graph_next_line(graph, &msgbuf);
787
+ fwrite(msgbuf.buf, sizeof(char), msgbuf.len, stdout);
788
+ strbuf_release(&msgbuf);
789
+}
790
+
791
+void graph_show_padding(struct git_graph *graph)
792
+{
793
+ struct strbuf msgbuf;
794
+
795
+ if (!graph)
796
+ return;
797
+
798
+ strbuf_init(&msgbuf, 0);
799
+ graph_padding_line(graph, &msgbuf);
800
+ fwrite(msgbuf.buf, sizeof(char), msgbuf.len, stdout);
801
+ strbuf_release(&msgbuf);
802
+}
803
+
804
+int graph_show_remainder(struct git_graph *graph)
805
+{
806
+ struct strbuf msgbuf;
807
+ int shown = 0;
808
+
809
+ if (!graph)
810
+ return 0;
811
+
812
+ if (graph_is_commit_finished(graph))
813
+ return 0;
814
+
815
+ strbuf_init(&msgbuf, 0);
816
+ for (;;) {
817
+ graph_next_line(graph, &msgbuf);
818
+ fwrite(msgbuf.buf, sizeof(char), msgbuf.len, stdout);
819
+ strbuf_setlen(&msgbuf, 0);
820
+ shown = 1;
821
+
822
+ if (!graph_is_commit_finished(graph))
823
+ putchar('\n');
824
+ else
825
+ break;
826
+ }
827
+ strbuf_release(&msgbuf);
828
+
829
+ return shown;
830
+}
831
+
832
+
833
+void graph_show_strbuf(struct git_graph *graph, struct strbuf const *sb)
834
+{
835
+ char *p;
836
+
837
+ if (!graph) {
838
+ fwrite(sb->buf, sizeof(char), sb->len, stdout);
839
+ return;
840
+ }
841
+
842
+ /*
843
+ * Print the strbuf line by line,
844
+ * and display the graph info before each line but the first.
845
+ */
846
+ p = sb->buf;
847
+ while (p) {
848
+ size_t len;
849
+ char *next_p = strchr(p, '\n');
850
+ if (next_p) {
851
+ next_p++;
852
+ len = next_p - p;
853
+ } else {
854
+ len = (sb->buf + sb->len) - p;
855
+ }
856
+ fwrite(p, sizeof(char), len, stdout);
857
+ if (next_p && *next_p != '\0')
858
+ graph_show_oneline(graph);
859
+ p = next_p;
860
+ }
861
+}
862
+
863
+void graph_show_commit_msg(struct git_graph *graph,
864
+ struct strbuf const *sb)
865
+{
866
+ int newline_terminated;
867
+
868
+ if (!graph) {
869
+ /*
870
+ * If there's no graph, just print the message buffer.
871
+ *
872
+ * The message buffer for CMIT_FMT_ONELINE and
873
+ * CMIT_FMT_USERFORMAT are already missing a terminating
874
+ * newline. All of the other formats should have it.
875
+ */
876
+ fwrite(sb->buf, sizeof(char), sb->len, stdout);
877
+ return;
878
+ }
879
+
880
+ newline_terminated = (sb->len && sb->buf[sb->len - 1] == '\n');
881
+
882
+ /*
883
+ * Show the commit message
884
+ */
885
+ graph_show_strbuf(graph, sb);
886
+
887
+ /*
888
+ * If there is more output needed for this commit, show it now
889
+ */
890
+ if (!graph_is_commit_finished(graph)) {
891
+ /*
892
+ * If sb doesn't have a terminating newline, print one now,
893
+ * so we can start the remainder of the graph output on a
894
+ * new line.
895
+ */
896
+ if (!newline_terminated)
897
+ putchar('\n');
898
+
899
+ graph_show_remainder(graph);
900
+
901
+ /*
902
+ * If sb ends with a newline, our output should too.
903
+ */
904
+ if (newline_terminated)
905
+ putchar('\n');
906
+ }
907
+}
graph.h
new
+121
@@ -0,0 +1,121 @@
1
+#ifndef GRAPH_H
2
+#define GRAPH_H
3
+
4
+/* A graph is a pointer to this opaque structure */
5
+struct git_graph;
6
+
7
+/*
8
+ * Create a new struct git_graph.
9
+ * The graph should be freed with graph_release() when no longer needed.
10
+ */
11
+struct git_graph *graph_init();
12
+
13
+/*
14
+ * Destroy a struct git_graph and free associated memory.
15
+ */
16
+void graph_release(struct git_graph *graph);
17
+
18
+/*
19
+ * Update a git_graph with a new commit.
20
+ * This will cause the graph to begin outputting lines for the new commit
21
+ * the next time graph_next_line() is called.
22
+ *
23
+ * If graph_update() is called before graph_is_commit_finished() returns 1,
24
+ * the next call to graph_next_line() will output an ellipsis ("...")
25
+ * to indicate that a portion of the graph is missing.
26
+ */
27
+void graph_update(struct git_graph *graph, struct commit *commit);
28
+
29
+/*
30
+ * Output the next line for a graph.
31
+ * This formats the next graph line into the specified strbuf. It is not
32
+ * terminated with a newline.
33
+ *
34
+ * Returns 1 if the line includes the current commit, and 0 otherwise.
35
+ * graph_next_line() will return 1 exactly once for each time
36
+ * graph_update() is called.
37
+ */
38
+int graph_next_line(struct git_graph *graph, struct strbuf *sb);
39
+
40
+/*
41
+ * Output a padding line in the graph.
42
+ * This is similar to graph_next_line(). However, it is guaranteed to
43
+ * never print the current commit line. Instead, if the commit line is
44
+ * next, it will simply output a line of vertical padding, extending the
45
+ * branch lines downwards, but leaving them otherwise unchanged.
46
+ */
47
+void graph_padding_line(struct git_graph *graph, struct strbuf *sb);
48
+
49
+/*
50
+ * Determine if a graph has finished outputting lines for the current
51
+ * commit.
52
+ *
53
+ * Returns 1 if graph_next_line() needs to be called again before
54
+ * graph_update() should be called. Returns 0 if no more lines are needed
55
+ * for this commit. If 0 is returned, graph_next_line() may still be
56
+ * called without calling graph_update(), and it will merely output
57
+ * appropriate "vertical padding" in the graph.
58
+ */
59
+int graph_is_commit_finished(struct git_graph const *graph);
60
+
61
+
62
+/*
63
+ * graph_show_*: helper functions for printing to stdout
64
+ */
65
+
66
+
67
+/*
68
+ * If the graph is non-NULL, print the history graph to stdout,
69
+ * up to and including the line containing this commit.
70
+ * Does not print a terminating newline on the last line.
71
+ */
72
+void graph_show_commit(struct git_graph *graph);
73
+
74
+/*
75
+ * If the graph is non-NULL, print one line of the history graph to stdout.
76
+ * Does not print a terminating newline on the last line.
77
+ */
78
+void graph_show_oneline(struct git_graph *graph);
79
+
80
+/*
81
+ * If the graph is non-NULL, print one line of vertical graph padding to
82
+ * stdout. Does not print a terminating newline on the last line.
83
+ */
84
+void graph_show_padding(struct git_graph *graph);
85
+
86
+/*
87
+ * If the graph is non-NULL, print the rest of the history graph for this
88
+ * commit to stdout. Does not print a terminating newline on the last line.
89
+ */
90
+int graph_show_remainder(struct git_graph *graph);
91
+
92
+/*
93
+ * Print a strbuf to stdout. If the graph is non-NULL, all lines but the
94
+ * first will be prefixed with the graph output.
95
+ *
96
+ * If the strbuf ends with a newline, the output will end after this
97
+ * newline. A new graph line will not be printed after the final newline.
98
+ * If the strbuf is empty, no output will be printed.
99
+ *
100
+ * Since the first line will not include the graph ouput, the caller is
101
+ * responsible for printing this line's graph (perhaps via
102
+ * graph_show_commit() or graph_show_oneline()) before calling
103
+ * graph_show_strbuf().
104
+ */
105
+void graph_show_strbuf(struct git_graph *graph, struct strbuf const *sb);
106
+
107
+/*
108
+ * Print a commit message strbuf and the remainder of the graph to stdout.
109
+ *
110
+ * This is similar to graph_show_strbuf(), but it always prints the
111
+ * remainder of the graph.
112
+ *
113
+ * If the strbuf ends with a newline, the output printed by
114
+ * graph_show_commit_msg() will end with a newline. If the strbuf is
115
+ * missing a terminating newline (including if it is empty), the output
116
+ * printed by graph_show_commit_msg() will also be missing a terminating
117
+ * newline.
118
+ */
119
+void graph_show_commit_msg(struct git_graph *graph, struct strbuf const *sb);
120
+
121
+#endif /* GRAPH_H */