22.4 KB753 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import type {Hash} from '../types';
9
10import {assert} from '../utils';
11
12/* eslint no-bitwise: 0 */
13/* Translated from fbcode/eden/scm/lib/renderdag/src/render.rs */
14
15enum ColumnType {
16 Empty = 0,
17 Blocked = 1,
18 Reserved = 2,
19 Ancestor = 3,
20 Parent = 4,
21}
22
23type ColumnProps =
24 | {
25 type: ColumnType.Empty | ColumnType.Blocked;
26 hash: undefined;
27 }
28 | {
29 type: ColumnType.Reserved | ColumnType.Ancestor | ColumnType.Parent;
30 hash: Hash;
31 };
32
33export class Column {
34 constructor(private inner: ColumnProps = {type: ColumnType.Empty, hash: undefined}) {}
35
36 static empty(): Column {
37 return new Column();
38 }
39
40 get type(): ColumnType {
41 return this.inner.type;
42 }
43
44 get hash(): undefined | Hash {
45 return this.inner.hash;
46 }
47
48 matches(n: Hash): boolean {
49 return this.hash === n;
50 }
51
52 isEmpty(): boolean {
53 return this.type === ColumnType.Empty;
54 }
55
56 variant(): number {
57 return this.type;
58 }
59
60 mergeColumn(other: Column): Column {
61 return other.variant() > this.variant() ? other : this;
62 }
63
64 reset(): Column {
65 return this.type === ColumnType.Blocked ? Column.empty() : this;
66 }
67
68 toNodeLine(): NodeLine {
69 switch (this.type) {
70 case ColumnType.Ancestor:
71 return NodeLine.Ancestor;
72 case ColumnType.Parent:
73 return NodeLine.Parent;
74 default:
75 return NodeLine.Blank;
76 }
77 }
78
79 toLinkLine(): LinkLine {
80 switch (this.type) {
81 case ColumnType.Ancestor:
82 return LinkLine.from(LinkLine.VERT_ANCESTOR);
83 case ColumnType.Parent:
84 return LinkLine.from(LinkLine.VERT_PARENT);
85 default:
86 return LinkLine.empty();
87 }
88 }
89
90 toPadLine(): PadLine {
91 switch (this.type) {
92 case ColumnType.Ancestor:
93 return PadLine.Ancestor;
94 case ColumnType.Parent:
95 return PadLine.Parent;
96 default:
97 return PadLine.Blank;
98 }
99 }
100}
101
102class Columns {
103 public inner: Array<Column>;
104
105 constructor(columns?: Array<Column>) {
106 this.inner = columns ?? [];
107 }
108
109 find(node: Hash): number | undefined {
110 const index = this.inner.findIndex(column => column.matches(node));
111 return index >= 0 ? index : undefined;
112 }
113
114 findEmpty(index?: number): number | undefined {
115 if (index != null && this.inner.at(index)?.isEmpty()) {
116 return index;
117 }
118 return this.firstEmpty();
119 }
120
121 firstEmpty(): number | undefined {
122 const index = this.inner.findIndex(column => column.isEmpty());
123 return index >= 0 ? index : undefined;
124 }
125
126 newEmpty(): number {
127 const columns = this.inner;
128 columns.push(Column.empty());
129 return columns.length - 1;
130 }
131
132 convertAncestorToParent() {
133 const columns = this.inner;
134 for (let i = 0; i < columns.length; i++) {
135 const {type, hash} = columns[i];
136 if (type === ColumnType.Ancestor && hash != null) {
137 columns[i] = new Column({type: ColumnType.Parent, hash});
138 }
139 }
140 }
141
142 reset(): void {
143 let columns = this.inner;
144 columns = columns.map(column => column.reset());
145 while (columns.at(-1)?.isEmpty()) {
146 columns.pop();
147 }
148 this.inner = columns;
149 }
150
151 merge(index: number, column: Column) {
152 const columns = this.inner;
153 columns[index] = columns[index].mergeColumn(column);
154 }
155
156 swap(index1: number, index2: number) {
157 if (index1 !== index2) {
158 const column1 = this.inner[index1];
159 const column2 = this.inner[index2];
160 this.inner[index1] = column2;
161 this.inner[index2] = column1;
162 }
163 }
164}
165
166export enum AncestorType {
167 Ancestor = 'Ancestor',
168 Parent = 'Parent',
169 Anonymous = 'Anonymous',
170}
171
172type AncestorProps =
173 | {
174 type: AncestorType.Ancestor | AncestorType.Parent;
175 hash: Hash;
176 }
177 | {
178 type: AncestorType.Anonymous;
179 hash: undefined;
180 };
181
182export class Ancestor {
183 constructor(private inner: AncestorProps = {type: AncestorType.Anonymous, hash: undefined}) {}
184
185 toColumn(): Column {
186 switch (this.inner.type) {
187 case AncestorType.Ancestor:
188 return new Column({type: ColumnType.Ancestor, hash: this.inner.hash});
189 case AncestorType.Parent:
190 return new Column({type: ColumnType.Parent, hash: this.inner.hash});
191 case AncestorType.Anonymous:
192 return new Column({type: ColumnType.Blocked, hash: undefined});
193 }
194 }
195
196 id(): Hash | undefined {
197 return this.inner.hash;
198 }
199
200 isDirect(): boolean {
201 return this.inner.type !== AncestorType.Ancestor;
202 }
203
204 toLinkLine(direct: LinkLine, indirect: LinkLine): LinkLine {
205 return this.isDirect() ? direct : indirect;
206 }
207}
208
209type AncestorColumnBoundsProps = {
210 target: number;
211 minAncestor: number;
212 minParent: number;
213 maxParent: number;
214 maxAncestor: number;
215};
216
217export class AncestorColumnBounds {
218 constructor(private inner: AncestorColumnBoundsProps) {}
219
220 static new(columns: Array<[number, Ancestor]>, target: number): AncestorColumnBounds | undefined {
221 if (columns.length === 0) {
222 return undefined;
223 }
224 const ancestorNumbers = [target, ...columns.map(([index]) => index)];
225 const parentNumbers = [target, ...columns.filter(([, a]) => a.isDirect()).map(([i]) => i)];
226 const minAncestor = Math.min(...ancestorNumbers);
227 const maxAncestor = Math.max(...ancestorNumbers);
228 const minParent = Math.min(...parentNumbers);
229 const maxParent = Math.max(...parentNumbers);
230 return new AncestorColumnBounds({
231 target,
232 minAncestor,
233 minParent,
234 maxParent,
235 maxAncestor,
236 });
237 }
238
239 get minAncestor(): number {
240 return this.inner.minAncestor;
241 }
242
243 get minParent(): number {
244 return this.inner.minParent;
245 }
246
247 get maxParent(): number {
248 return this.inner.maxParent;
249 }
250
251 get maxAncestor(): number {
252 return this.inner.maxAncestor;
253 }
254
255 get target(): number {
256 return this.inner.target;
257 }
258
259 *range(): Iterable<number> {
260 for (let i = this.minAncestor + 1; i < this.maxAncestor; ++i) {
261 yield i;
262 }
263 }
264
265 horizontalLine(index: number): LinkLine {
266 if (index === this.target) {
267 return LinkLine.empty();
268 } else if (index > this.minParent && index < this.maxParent) {
269 return LinkLine.from(LinkLine.HORIZ_PARENT);
270 } else if (index > this.minAncestor && index < this.maxAncestor) {
271 return LinkLine.from(LinkLine.HORIZ_ANCESTOR);
272 } else {
273 return LinkLine.empty();
274 }
275 }
276}
277
278export class LinkLine {
279 constructor(public value = 0) {}
280
281 /** This cell contains a horizontal line that connects to a parent. */
282 static HORIZ_PARENT = 1 << 0;
283 /** This cell contains a horizontal line that connects to an ancestor. */
284 static HORIZ_ANCESTOR = 1 << 1;
285 /** The descendent of this cell is connected to the parent. */
286 static VERT_PARENT = 1 << 2;
287 /** The descendent of this cell is connected to an ancestor. */
288 static VERT_ANCESTOR = 1 << 3;
289 /** The parent of this cell is linked in this link row and the child is to the left. */
290 static LEFT_FORK_PARENT = 1 << 4;
291 /** The ancestor of this cell is linked in this link row and the child is to the left. */
292 static LEFT_FORK_ANCESTOR = 1 << 5;
293 /** The parent of this cell is linked in this link row and the child is to the right. */
294 static RIGHT_FORK_PARENT = 1 << 6;
295 /** The ancestor of this cell is linked in this link row and the child is to the right. */
296 static RIGHT_FORK_ANCESTOR = 1 << 7;
297 /** The child of this cell is linked to parents on the left. */
298 static LEFT_MERGE_PARENT = 1 << 8;
299 /** The child of this cell is linked to ancestors on the left. */
300 static LEFT_MERGE_ANCESTOR = 1 << 9;
301 /** The child of this cell is linked to parents on the right. */
302 static RIGHT_MERGE_PARENT = 1 << 10;
303 /** The child of this cell is linked to ancestors on the right. */
304 static RIGHT_MERGE_ANCESTOR = 1 << 11;
305 /**
306 * The target node of this link line is the child of this column.
307 * This disambiguates between the node that is connected in this link line,
308 * and other nodes that are also connected vertically.
309 */
310 static CHILD = 1 << 12;
311
312 static HORIZONTAL = LinkLine.HORIZ_PARENT | LinkLine.HORIZ_ANCESTOR;
313 static VERTICAL = LinkLine.VERT_PARENT | LinkLine.VERT_ANCESTOR;
314 static LEFT_FORK = LinkLine.LEFT_FORK_PARENT | LinkLine.LEFT_FORK_ANCESTOR;
315 static RIGHT_FORK = LinkLine.RIGHT_FORK_PARENT | LinkLine.RIGHT_FORK_ANCESTOR;
316 static LEFT_MERGE = LinkLine.LEFT_MERGE_PARENT | LinkLine.LEFT_MERGE_ANCESTOR;
317 static RIGHT_MERGE = LinkLine.RIGHT_MERGE_PARENT | LinkLine.RIGHT_MERGE_ANCESTOR;
318 static ANY_MERGE = LinkLine.LEFT_MERGE | LinkLine.RIGHT_MERGE;
319 static ANY_FORK = LinkLine.LEFT_FORK | LinkLine.RIGHT_FORK;
320 static ANY_FORK_OR_MERGE = LinkLine.ANY_MERGE | LinkLine.ANY_FORK;
321
322 static from(value: number): LinkLine {
323 return new LinkLine(value);
324 }
325
326 static empty(): LinkLine {
327 return new LinkLine(0);
328 }
329
330 valueOf(): number {
331 return this.value;
332 }
333
334 contains(value: number): boolean {
335 return (this.value & value) === value;
336 }
337
338 intersects(value: number): boolean {
339 return (this.value & value) !== 0;
340 }
341
342 or(value: number): LinkLine {
343 return LinkLine.from(this.value | value);
344 }
345}
346
347export enum NodeLine {
348 Blank = 0,
349 Ancestor = 1,
350 Parent = 2,
351 Node = 3,
352}
353
354export enum PadLine {
355 Blank = 0,
356 Ancestor = 1,
357 Parent = 2,
358}
359
360type GraphRow = {
361 hash: Hash;
362 merge: boolean;
363 /** The node ("o") columns for this row. */
364 nodeLine: Array<NodeLine>;
365 /** The link columns for this row if necessary. Cannot be repeated. */
366 linkLine?: Array<LinkLine>;
367 /**
368 * The location of any terminators, if necessary.
369 * Between postNode and ancestryLines.
370 */
371 termLine?: Array<boolean>;
372 /**
373 * Lines to represent "ancestry" relationship.
374 * "|" for direct parent, ":" for indirect ancestor.
375 * Can be repeated. Can be skipped if there are no indirect ancestors.
376 * Practically, CLI repeats this line. ISL "repeats" preNode and postNode lines.
377 */
378 ancestryLine: Array<PadLine>;
379
380 /** True if the node is a head (no children, uses a new column) */
381 isHead: boolean;
382 /** True if the node is a root (no parents) */
383 isRoot: boolean;
384
385 /**
386 * Column that contains the "node" above the link line.
387 * nodeLine[nodeColumn] should be NodeLine.Node.
388 */
389 nodeColumn: number;
390
391 /**
392 * Parent columns reachable from "node" below the link line.
393 */
394 parentColumns: number[];
395
396 /**
397 * A subset of LinkLine that comes from "node". For example:
398 *
399 * │ o // node line
400 * ├─╯ // link line
401 *
402 * The `fromNodeValue` LinkLine looks like:
403 *
404 * ╭─╯
405 *
406 * Note "├" is changed to "╭".
407 */
408 linkLineFromNode?: Array<LinkLine>;
409};
410
411/**
412 * Output row for a "commit".
413 *
414 * Example line types:
415 *
416 * ```plain
417 * │ // preNodeLine (repeatable)
418 * │ // preNodeLine
419 * o F // nodeLine
420 * │ very long message 0 // postNodeLine (repeatable)
421 * │ very long message 0 // postNodeLine
422 * ├─┬─╮ very long message 1 // linkLine
423 * │ │ ~ very long message 2 // termLine
424 * : │ very long message 3 // ancestryLine
425 * │ │ very long message 4 // postAncestryLine (repeatable)
426 * │ │ very long message 5 // postAncestryLine
427 * ```
428 *
429 * This is `GraphRow` with derived fields.
430 */
431export type ExtendedGraphRow = GraphRow & {
432 /** If there are indirect ancestors, aka. the ancestryLine is interesting to render. */
433 hasIndirectAncestor: boolean;
434 /** The columns before the node columns. Repeatable. */
435 preNodeLine: Array<PadLine>;
436 /** The columns after node, before the term, link columns. Repeatable. */
437 postNodeLine: Array<PadLine>;
438 /** The columns after ancestryLine. Repeatable. */
439 postAncestryLine: Array<PadLine>;
440 /** Str to test equality. */
441 valueOf(): string;
442};
443
444function nodeToPadLine(node: NodeLine, useBlank: boolean): PadLine {
445 switch (node) {
446 case NodeLine.Blank:
447 return PadLine.Blank;
448 case NodeLine.Ancestor:
449 return PadLine.Ancestor;
450 case NodeLine.Parent:
451 return PadLine.Parent;
452 case NodeLine.Node:
453 return useBlank ? PadLine.Blank : PadLine.Parent;
454 }
455}
456
457function extendGraphRow(row: GraphRow): ExtendedGraphRow {
458 // Single string that includes all states of the row.
459 // Useful to test equality.
460 const rowStr = [
461 row.hash,
462 row.nodeLine.join(''),
463 row.linkLine?.map(l => l.value.toString(16)).join('') ?? '',
464 row.termLine?.map(l => (l ? '1' : '0')).join('') ?? '',
465 row.ancestryLine?.join('') ?? '',
466 row.parentColumns.join(','),
467 row.isHead ? 'h' : '',
468 row.isRoot ? 'r' : '',
469 ].join(';');
470
471 return {
472 ...row,
473 get hasIndirectAncestor() {
474 return row.ancestryLine.some(line => line === PadLine.Ancestor);
475 },
476 get preNodeLine() {
477 return row.nodeLine.map(l => nodeToPadLine(l, row.isHead));
478 },
479 get postNodeLine() {
480 return row.nodeLine.map(l => nodeToPadLine(l, row.isRoot));
481 },
482 get postAncestryLine() {
483 return row.ancestryLine.map(l => (l === PadLine.Ancestor ? PadLine.Parent : l));
484 },
485 valueOf(): string {
486 return rowStr;
487 },
488 };
489}
490
491type NextRowOptions = {
492 /**
493 * Ensure this node uses the last (right-most) column.
494 * Only works for heads, i.e. nodes without children.
495 */
496 forceLastColumn?: boolean;
497};
498
499export class Renderer {
500 private columns: Columns = new Columns();
501
502 /**
503 * Reserve a column for the given hash.
504 * This is usually used to indent draft commits by reserving
505 * columns for public commits.
506 */
507 reserve(hash: Hash) {
508 if (this.columns.find(hash) == null) {
509 const index = this.columns.firstEmpty();
510 const column = new Column({type: ColumnType.Reserved, hash});
511 if (index == null) {
512 this.columns.inner.push(column);
513 } else {
514 this.columns.inner[index] = column;
515 }
516 }
517 }
518
519 /**
520 * Render the next row.
521 * Main logic of the renderer.
522 */
523 nextRow(hash: Hash, parents: Array<Ancestor>, opts?: NextRowOptions): ExtendedGraphRow {
524 const {forceLastColumn = false} = opts ?? {};
525
526 // Find a column for this node.
527 const existingColumn = this.columns.find(hash);
528 let column: number;
529 if (forceLastColumn) {
530 assert(
531 existingColumn == null,
532 'requireLastColumn should only apply to heads (ex. "You are here")',
533 );
534 column = this.columns.newEmpty();
535 } else {
536 column = existingColumn ?? this.columns.firstEmpty() ?? this.columns.newEmpty();
537 }
538 const isHead =
539 existingColumn == null || this.columns.inner.at(existingColumn)?.type === ColumnType.Reserved;
540 const isRoot = parents.length === 0;
541
542 this.columns.inner[column] = Column.empty();
543
544 // This row is for a merge if there are multiple parents.
545 const merge = parents.length > 1;
546
547 // Build the initial node line.
548 const nodeLine: NodeLine[] = this.columns.inner.map(c => c.toNodeLine());
549 nodeLine[column] = NodeLine.Node;
550
551 // Build the initial link line.
552 const linkLine: LinkLine[] = this.columns.inner.map(c => c.toLinkLine());
553 const linkLineFromNode: LinkLine[] = this.columns.inner.map(_c => LinkLine.empty());
554 linkLineFromNode[column] = linkLine[column];
555 let needLinkLine = false;
556
557 // Update linkLine[i] and linkLineFromNode[i] to include `bits`.
558 const linkBoth = (i: number, bits: number) => {
559 if (bits < 0) {
560 linkLine[i] = LinkLine.from(bits);
561 linkLineFromNode[i] = LinkLine.from(bits);
562 } else {
563 linkLine[i] = linkLine[i].or(bits);
564 linkLineFromNode[i] = linkLineFromNode[i].or(bits);
565 }
566 };
567
568 // Build the initial term line.
569 const termLine: boolean[] = this.columns.inner.map(_c => false);
570 let needTermLine = false;
571
572 // Build the initial ancestry line.
573 const ancestryLine: PadLine[] = this.columns.inner.map(c => c.toPadLine());
574
575 // Assign each parent to a column.
576 const parentColumns = new Map<number, Ancestor>();
577 for (const p of parents) {
578 // Check if the parent already has a column.
579 const parentId = p.id();
580 if (parentId != null) {
581 const index = this.columns.find(parentId);
582 if (index != null) {
583 this.columns.merge(index, p.toColumn());
584 parentColumns.set(index, p);
585 continue;
586 }
587 }
588
589 // Assign the parent to an empty column, preferring the column
590 // the current node is going in, to maintain linearity.
591 const index = this.columns.findEmpty(column);
592 if (index != null) {
593 this.columns.merge(index, p.toColumn());
594 parentColumns.set(index, p);
595 continue;
596 }
597
598 // There are no empty columns left. Make a new column.
599 parentColumns.set(this.columns.inner.length, p);
600 nodeLine.push(NodeLine.Blank);
601 ancestryLine.push(PadLine.Blank);
602 linkLine.push(LinkLine.empty());
603 linkLineFromNode.push(LinkLine.empty());
604 termLine.push(false);
605 this.columns.inner.push(p.toColumn());
606 }
607
608 // Mark parent columns with anonymous parents as terminating.
609 for (const [i, p] of parentColumns.entries()) {
610 if (p.id() == null) {
611 termLine[i] = true;
612 needTermLine = true;
613 }
614 }
615
616 // Check if we can move the parent to the current column.
617 //
618 // Before After
619 // ├─╮ ├─╮
620 // │ o C │ o C
621 // o ╷ B o ╷ B
622 // ╰─╮ ├─╯
623 // o A o A
624 //
625 // o J o J
626 // ├─┬─╮ ├─┬─╮
627 // │ │ o I │ │ o I
628 // │ o │ H │ o │ H
629 // ╭─┼─┬─┬─╮ ╭─┼─┬─┬─╮
630 // │ │ │ │ o G │ │ │ │ o G
631 // │ │ │ o │ E │ │ │ o │ E
632 // │ │ │ ╰─┤ │ │ │ ├─╯
633 // │ │ o │ D │ │ o │ D
634 // │ │ ├───╮ │ │ ├─╮
635 // │ o │ │ C │ o │ │ C
636 // │ ╰─────┤ │ ├───╯
637 // o │ │ F o │ │ F
638 // ╰───────┤ ├─╯ │
639 // │ o B o │ B
640 // ├───╯ ├───╯
641 // o A o A
642 if (parents.length === 1) {
643 const [[parentColumn, parentAncestor]] = parentColumns.entries();
644 if (parentColumn != null && parentColumn > column) {
645 // This node has a single parent which was already
646 // assigned to a column to the right of this one.
647 // Move the parent to this column.
648 this.columns.swap(column, parentColumn);
649 parentColumns.delete(parentColumn);
650 parentColumns.set(column, parentAncestor);
651 // Generate a line from this column to the old
652 // parent column. We need to continue with the style
653 // that was being used for the parent column.
654 //
655 // old parent
656 // o v
657 // ╭────╯
658 // ^
659 // new parent (moved here, nodeColumn)
660 const wasDirect = linkLine.at(parentColumn)?.contains(LinkLine.VERT_PARENT);
661 linkLine[column] = linkLine[column].or(
662 wasDirect ? LinkLine.RIGHT_FORK_PARENT : LinkLine.RIGHT_FORK_ANCESTOR,
663 );
664 for (let i = column + 1; i < parentColumn; ++i) {
665 linkLine[i] = linkLine[i].or(wasDirect ? LinkLine.HORIZ_PARENT : LinkLine.HORIZ_ANCESTOR);
666 }
667 linkLine[parentColumn] = LinkLine.from(
668 wasDirect ? LinkLine.LEFT_MERGE_PARENT : LinkLine.LEFT_MERGE_ANCESTOR,
669 );
670 needLinkLine = true;
671 // The ancestry line for the old parent column is now blank.
672 ancestryLine[parentColumn] = PadLine.Blank;
673 }
674 }
675
676 // Connect the node column to all the parent columns.
677 const bounds = AncestorColumnBounds.new([...parentColumns.entries()], column);
678 if (bounds != null) {
679 // If the parents extend beyond the columns adjacent to the node, draw a horizontal
680 // ancestor line between the two outermost ancestors.
681 for (const i of bounds.range()) {
682 linkBoth(i, bounds.horizontalLine(i).value);
683 needLinkLine = true;
684 }
685 // If there is a parent or ancestor to the right of the node
686 // column, the node merges from the right.
687 if (bounds.maxParent > column) {
688 linkBoth(column, LinkLine.RIGHT_MERGE_PARENT);
689 needLinkLine = true;
690 } else if (bounds.maxAncestor > column) {
691 linkBoth(column, LinkLine.RIGHT_MERGE_ANCESTOR);
692 needLinkLine = true;
693 }
694 // If there is a parent or ancestor to the left of the node column, the node merges from the left.
695 if (bounds.minParent < column) {
696 linkBoth(column, LinkLine.LEFT_MERGE_PARENT);
697 needLinkLine = true;
698 } else if (bounds.minAncestor < column) {
699 linkBoth(column, LinkLine.LEFT_MERGE_ANCESTOR);
700 needLinkLine = true;
701 }
702 // Each parent or ancestor forks towards the node column.
703 for (const [i, p] of parentColumns.entries()) {
704 ancestryLine[i] = this.columns.inner[i].toPadLine();
705 let orValue = 0;
706 if (i < column) {
707 orValue = p.toLinkLine(
708 LinkLine.from(LinkLine.RIGHT_FORK_PARENT),
709 LinkLine.from(LinkLine.RIGHT_FORK_ANCESTOR),
710 ).value;
711 } else if (i === column) {
712 orValue =
713 LinkLine.CHILD |
714 p.toLinkLine(LinkLine.from(LinkLine.VERT_PARENT), LinkLine.from(LinkLine.VERT_ANCESTOR))
715 .value;
716 } else {
717 orValue = p.toLinkLine(
718 LinkLine.from(LinkLine.LEFT_FORK_PARENT),
719 LinkLine.from(LinkLine.LEFT_FORK_ANCESTOR),
720 ).value;
721 }
722 linkBoth(i, orValue);
723 }
724 }
725
726 // Only show ":" once per branch.
727 this.columns.convertAncestorToParent();
728
729 // Now that we have assigned all the columns, reset their state.
730 this.columns.reset();
731
732 // Filter out the link line or term line if they are not needed.
733 const optionalLinkLine = needLinkLine ? linkLine : undefined;
734 const optionalTermLine = needTermLine ? termLine : undefined;
735
736 const row: GraphRow = {
737 hash,
738 merge,
739 nodeLine,
740 linkLine: optionalLinkLine,
741 termLine: optionalTermLine,
742 ancestryLine,
743 isHead,
744 isRoot,
745 nodeColumn: column,
746 parentColumns: [...parentColumns.keys()].sort((a, b) => a - b),
747 linkLineFromNode: needLinkLine ? linkLineFromNode : undefined,
748 };
749
750 return extendGraphRow(row);
751 }
752}
753