4.0 KB134 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 {CommitPreview, Dag, WithPreviewType} from './previews';
9import type {CommitInfo, Hash} from './types';
10
11export type CommitTree = {
12 info: CommitInfo;
13 children: Array<CommitTree>;
14};
15
16export type CommitTreeWithPreviews = {
17 info: CommitInfo;
18 children: Array<CommitTreeWithPreviews>;
19 previewType?: CommitPreview;
20};
21
22const byTimeDecreasing = (a: CommitInfo & WithPreviewType, b: CommitInfo & WithPreviewType) => {
23 // Consider seqNumber (insertion order during preview calculation).
24 if (a.seqNumber != null && b.seqNumber != null) {
25 const seqDelta = a.seqNumber - b.seqNumber;
26 if (seqDelta !== 0) {
27 return seqDelta;
28 }
29 }
30 // Sort by date.
31 const timeDelta = b.date.getTime() - a.date.getTime();
32 if (timeDelta !== 0) {
33 return timeDelta;
34 }
35 // Always break ties even if timestamp is the same.
36 return a.hash < b.hash ? 1 : -1;
37};
38
39/**
40 * Given a list of commits from disk, produce a tree capturing the
41 * parent/child structure of the commits.
42 * - Public commits are always top level (on the main line)
43 * - Public commits are sorted by date
44 * - Draft commits are always offshoots of public commits (never on main line)
45 * - Caveat: if there are no public commits found, use the parent of everything
46 * as if it were a public commit
47 * - If a public commit has no draft children, it is hidden
48 * - ...unless it has a bookmark
49 * - If a commit has multiple children, they are sorted by date
50 */
51export function getCommitTree(
52 commits: Array<CommitInfo & WithPreviewType>,
53): Array<CommitTreeWithPreviews> {
54 const childNodesByParent = new Map<string, Set<CommitInfo>>();
55 commits.forEach(commit => {
56 const [parent] = commit.parents;
57 if (!parent) {
58 return;
59 }
60 let set = childNodesByParent.get(parent);
61 if (!set) {
62 set = new Set();
63 childNodesByParent.set(parent, set);
64 }
65 set.add(commit);
66 });
67
68 const makeTree = (revision: CommitInfo & WithPreviewType): CommitTreeWithPreviews => {
69 const {hash, previewType} = revision;
70 const childrenSet = childNodesByParent.get(hash) ?? [];
71
72 const childrenInfos = [...childrenSet].sort(byTimeDecreasing);
73
74 const children: Array<CommitTree> =
75 childrenInfos == null
76 ? []
77 : // only make branches off the main line for non-public revisions
78 childrenInfos.filter(child => child.phase !== 'public').map(makeTree);
79
80 return {
81 info: revision,
82 children,
83 previewType,
84 };
85 };
86
87 const initialCommits = commits.filter(
88 commit => commit.phase === 'public' || commit.parents.length === 0,
89 );
90
91 // build tree starting from public revisions
92 return initialCommits.sort(byTimeDecreasing).map(makeTree);
93}
94
95export function* walkTreePostorder(
96 commitTree: Array<CommitTreeWithPreviews>,
97): IterableIterator<CommitTreeWithPreviews> {
98 for (const node of commitTree) {
99 if (node.children.length > 0) {
100 yield* walkTreePostorder(node.children);
101 }
102 yield node;
103 }
104}
105
106export function isDescendant(hash: string, commitTree: CommitTree): boolean {
107 for (const commit of walkTreePostorder([commitTree])) {
108 if (commit.info.hash === hash) {
109 return true;
110 }
111 }
112 return false;
113}
114
115/** Test if a tree is linear - no merge or folds. */
116export function isTreeLinear(tree: CommitTreeWithPreviews): boolean {
117 if (tree.children.length > 1 || tree.info.parents.length > 1) {
118 return false;
119 }
120 return tree.children.every(t => isTreeLinear(t));
121}
122
123/** Finds the public ancestor by walking up parents, either from a starting hash or from `.` if none provided. */
124export function findPublicBaseAncestor(dag?: Dag, from?: Hash): CommitInfo | undefined {
125 let commit = dag?.resolve(from ?? '.');
126 while (commit) {
127 if (commit.phase === 'public') {
128 return commit;
129 }
130 commit = dag?.get(commit.parents.at(0));
131 }
132 return undefined;
133}
134