Skip to content

Commit 434b655

Browse files
committed
engine: Centralized leafnode-related code in one place
1 parent b263149 commit 434b655

4 files changed

Lines changed: 367 additions & 16 deletions

File tree

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { buildCommitDict, getLeafNodes, isLeafNode } from "./commit.util";
2+
import type { CommitDict,CommitRaw } from "./types";
3+
4+
describe("getLeafNodes", () => {
5+
const createMockCommitRaw = (
6+
id: string,
7+
branches: string[] = [],
8+
overrides: Partial<CommitRaw> = {}
9+
): CommitRaw => ({
10+
sequence: 1,
11+
id,
12+
parents: [],
13+
branches,
14+
tags: [],
15+
author: { name: "Test Author", email: "test@example.com" },
16+
authorDate: new Date("2023-01-01"),
17+
committer: { name: "Test Committer", email: "test@example.com" },
18+
committerDate: new Date("2023-01-01"),
19+
message: "Test commit message",
20+
differenceStatistic: {
21+
totalInsertionCount: 0,
22+
totalDeletionCount: 0,
23+
fileDictionary: {},
24+
},
25+
commitMessageType: "",
26+
...overrides,
27+
});
28+
29+
describe("normal cases", () => {
30+
it("should return nodes with branches as leaf nodes", () => {
31+
const commits: CommitRaw[] = [
32+
createMockCommitRaw("commit1", ["main"]),
33+
createMockCommitRaw("commit2", ["develop"]),
34+
createMockCommitRaw("commit3", ["feature/test"]),
35+
];
36+
const commitDict: CommitDict = buildCommitDict(commits);
37+
38+
const leafNodes = getLeafNodes(commitDict);
39+
40+
expect(leafNodes).toHaveLength(3);
41+
expect(leafNodes.map(node => node.commit.id)).toEqual(
42+
expect.arrayContaining(["commit1", "commit2", "commit3"])
43+
);
44+
});
45+
46+
it("should return nodes with multiple branches as leaf nodes", () => {
47+
const commits: CommitRaw[] = [
48+
createMockCommitRaw("commit1", ["main", "develop"]),
49+
];
50+
const commitDict: CommitDict = buildCommitDict(commits);
51+
52+
const leafNodes = getLeafNodes(commitDict);
53+
54+
expect(leafNodes).toHaveLength(1);
55+
expect(leafNodes[0].commit.id).toBe("commit1");
56+
expect(leafNodes[0].commit.branches).toEqual(["main", "develop"]);
57+
});
58+
});
59+
60+
describe("edge cases", () => {
61+
it("should return empty array for empty CommitDict", () => {
62+
const commitDict: CommitDict = new Map();
63+
64+
const leafNodes = getLeafNodes(commitDict);
65+
66+
expect(leafNodes).toEqual([]);
67+
});
68+
69+
it("should not return nodes without branches as leaf nodes", () => {
70+
const commits: CommitRaw[] = [
71+
createMockCommitRaw("commit1", []),
72+
createMockCommitRaw("commit2", []),
73+
];
74+
const commitDict: CommitDict = buildCommitDict(commits);
75+
76+
const leafNodes = getLeafNodes(commitDict);
77+
78+
expect(leafNodes).toEqual([]);
79+
});
80+
81+
it("should return only nodes with branches when mixed", () => {
82+
const commits: CommitRaw[] = [
83+
createMockCommitRaw("commit1", ["main"]),
84+
createMockCommitRaw("commit2", []),
85+
createMockCommitRaw("commit3", ["develop"]),
86+
createMockCommitRaw("commit4", []),
87+
];
88+
const commitDict: CommitDict = buildCommitDict(commits);
89+
90+
const leafNodes = getLeafNodes(commitDict);
91+
92+
expect(leafNodes).toHaveLength(2);
93+
expect(leafNodes.map(node => node.commit.id)).toEqual(
94+
expect.arrayContaining(["commit1", "commit3"])
95+
);
96+
});
97+
});
98+
99+
describe("type validation", () => {
100+
it("should return array elements with CommitNode type", () => {
101+
const commits: CommitRaw[] = [
102+
createMockCommitRaw("commit1", ["main"]),
103+
];
104+
const commitDict: CommitDict = buildCommitDict(commits);
105+
106+
const leafNodes = getLeafNodes(commitDict);
107+
108+
expect(leafNodes[0]).toHaveProperty("commit");
109+
expect(leafNodes[0].commit).toHaveProperty("id");
110+
expect(leafNodes[0].commit).toHaveProperty("branches");
111+
expect(Array.isArray(leafNodes[0].commit.branches)).toBe(true);
112+
});
113+
});
114+
115+
describe("real-world scenarios", () => {
116+
it("should correctly find leaf nodes in git-like history", () => {
117+
const commits: CommitRaw[] = [
118+
createMockCommitRaw("abc123", ["main"], {
119+
message: "feat: add new feature",
120+
author: { name: "Developer 1", email: "dev1@example.com" },
121+
}),
122+
createMockCommitRaw("def456", [], {
123+
message: "fix: bug fix",
124+
author: { name: "Developer 2", email: "dev2@example.com" },
125+
}),
126+
createMockCommitRaw("ghi789", ["develop", "feature/branch"], {
127+
message: "refactor: code cleanup",
128+
author: { name: "Developer 3", email: "dev3@example.com" },
129+
}),
130+
];
131+
const commitDict: CommitDict = buildCommitDict(commits);
132+
133+
const leafNodes = getLeafNodes(commitDict);
134+
135+
expect(leafNodes).toHaveLength(2);
136+
expect(leafNodes.map(node => node.commit.id)).toEqual(
137+
expect.arrayContaining(["abc123", "ghi789"])
138+
);
139+
140+
const mainBranchNode = leafNodes.find(node => node.commit.id === "abc123");
141+
expect(mainBranchNode?.commit.message).toBe("feat: add new feature");
142+
expect(mainBranchNode?.commit.branches).toEqual(["main"]);
143+
});
144+
});
145+
});
146+
147+
describe("isLeafNode", () => {
148+
const createMockCommitRaw = (
149+
id: string,
150+
branches: string[] = [],
151+
overrides: Partial<CommitRaw> = {}
152+
): CommitRaw => ({
153+
sequence: 1,
154+
id,
155+
parents: [],
156+
branches,
157+
tags: [],
158+
author: { name: "Test Author", email: "test@example.com" },
159+
authorDate: new Date("2023-01-01"),
160+
committer: { name: "Test Committer", email: "test@example.com" },
161+
committerDate: new Date("2023-01-01"),
162+
message: "Test commit message",
163+
differenceStatistic: {
164+
totalInsertionCount: 0,
165+
totalDeletionCount: 0,
166+
fileDictionary: {},
167+
},
168+
commitMessageType: "",
169+
...overrides,
170+
});
171+
172+
it("should return true for nodes with branches", () => {
173+
const node = { commit: createMockCommitRaw("test1", ["main"]) };
174+
175+
expect(isLeafNode(node)).toBe(true);
176+
});
177+
178+
it("should return true for nodes with multiple branches", () => {
179+
const node = { commit: createMockCommitRaw("test1", ["main", "develop"]) };
180+
181+
expect(isLeafNode(node)).toBe(true);
182+
});
183+
184+
it("should return false for nodes without branches", () => {
185+
const node = { commit: createMockCommitRaw("test1", []) };
186+
187+
expect(isLeafNode(node)).toBe(false);
188+
});
189+
190+
it("should be determined by branches regardless of stemId", () => {
191+
const nodeWithoutBranches = {
192+
stemId: "stem1",
193+
commit: createMockCommitRaw("test1", [])
194+
};
195+
196+
const nodeWithBranches = {
197+
stemId: "stem1",
198+
commit: createMockCommitRaw("test2", ["main"])
199+
};
200+
201+
expect(isLeafNode(nodeWithoutBranches)).toBe(false);
202+
expect(isLeafNode(nodeWithBranches)).toBe(true);
203+
});
204+
});

packages/analysis-engine/src/commit.util.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,23 @@ import {
66
type CommitRaw,
77
} from "./types";
88

9+
export const isLeafNode = (node: CommitNode): boolean => node.commit.branches.length > 0;
10+
11+
export const latestFirstComparator = (a: CommitNode, b: CommitNode): number => {
12+
// branches 값 존재하는 노드 => leaf / main / HEAD 노드.
13+
// 이 노드는 큐에 들어올 때 순서가 정해져 있기 때문에 순서를 바꾸지 않음.
14+
if (isLeafNode(a) || isLeafNode(b)) {
15+
return 0;
16+
}
17+
return new Date(b.commit.committerDate).getTime() - new Date(a.commit.committerDate).getTime();
18+
};
19+
920
export function buildCommitDict(commits: CommitRaw[]): CommitDict {
1021
return new Map(commits.map((commit) => [commit.id, { commit } as CommitNode]));
1122
}
1223

1324
export function getLeafNodes(commitDict: CommitDict): CommitNode[] {
14-
const leafNodes: CommitNode[] = [];
15-
commitDict.forEach((node) => node.commit.branches.length && leafNodes.push(node));
16-
return leafNodes;
25+
return [...commitDict.values()].filter(isLeafNode);
1726
}
1827

1928
export function getCommitMessageType(message: string): CommitMessageType {

packages/analysis-engine/src/stem.spec.ts

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { buildCommitDict, getLeafNodes } from "./commit.util";
1+
import Queue from "../src/queue";
2+
import { buildCommitDict, getLeafNodes, latestFirstComparator } from "./commit.util";
23
import { buildStemDict } from "./stem";
34
import type { CommitNode, CommitRaw } from "./types";
45

@@ -173,3 +174,150 @@ describe("stem", () => {
173174
expect(stemDict.get("implicit-1")?.nodes.map((node) => node.commit.id)).toEqual(expectedStemDict["implicit-1"]);
174175
});
175176
});
177+
178+
describe("stem module", () => {
179+
const createMockCommitRaw = (
180+
id: string,
181+
branches: string[] = [],
182+
parents: string[] = [],
183+
committerDate: string = "2023-01-01",
184+
overrides: Partial<CommitRaw> = {}
185+
): CommitRaw => ({
186+
sequence: 1,
187+
id,
188+
parents,
189+
branches,
190+
tags: [],
191+
author: { name: "Test Author", email: "test@example.com" },
192+
authorDate: new Date(committerDate),
193+
committer: { name: "Test Committer", email: "test@example.com" },
194+
committerDate: new Date(committerDate),
195+
message: "Test commit message",
196+
differenceStatistic: {
197+
totalInsertionCount: 0,
198+
totalDeletionCount: 0,
199+
fileDictionary: {},
200+
},
201+
commitMessageType: "",
202+
...overrides,
203+
});
204+
205+
const createCommitNode = (commit: CommitRaw, stemId?: string): CommitNode => ({
206+
commit,
207+
stemId,
208+
});
209+
210+
describe("latestFirstComparator", () => {
211+
it("should maintain order for leaf nodes", () => {
212+
const leafNode1 = createCommitNode(
213+
createMockCommitRaw("leaf1", ["main"], [], "2023-01-01")
214+
);
215+
const leafNode2 = createCommitNode(
216+
createMockCommitRaw("leaf2", ["develop"], [], "2023-01-02")
217+
);
218+
219+
const queue = new Queue<CommitNode>(latestFirstComparator);
220+
221+
queue.push(leafNode1);
222+
queue.push(leafNode2);
223+
224+
const first = queue.pop();
225+
const second = queue.pop();
226+
227+
expect(first?.commit.id).toBe("leaf1");
228+
expect(second?.commit.id).toBe("leaf2");
229+
});
230+
231+
it("should sort non-leaf nodes by commit time with latest first", () => {
232+
const commit1 = createCommitNode(
233+
createMockCommitRaw("commit-2023-01-01", [], [], "2023-01-01T10:00:00Z")
234+
);
235+
const commit2 = createCommitNode(
236+
createMockCommitRaw("commit-2023-01-02", [], [], "2023-01-02T10:00:00Z")
237+
);
238+
const commit3 = createCommitNode(
239+
createMockCommitRaw("commit-2023-01-03", [], [], "2023-01-03T10:00:00Z")
240+
);
241+
242+
const queue = new Queue<CommitNode>(latestFirstComparator);
243+
244+
queue.push(commit1);
245+
queue.push(commit2);
246+
queue.push(commit3);
247+
248+
const first = queue.pop();
249+
const second = queue.pop();
250+
const third = queue.pop();
251+
252+
expect(first?.commit.id).toBe("commit-2023-01-03");
253+
expect(second?.commit.id).toBe("commit-2023-01-02");
254+
expect(third?.commit.id).toBe("commit-2023-01-01");
255+
});
256+
257+
it("should prioritize latest commits regardless of insertion order", () => {
258+
const olderCommit = createCommitNode(
259+
createMockCommitRaw("older-commit", [], [], "2023-01-01T10:00:00Z")
260+
);
261+
const newerCommit = createCommitNode(
262+
createMockCommitRaw("newer-commit", [], [], "2023-01-10T10:00:00Z")
263+
);
264+
const latestCommit = createCommitNode(
265+
createMockCommitRaw("latest-commit", [], [], "2023-01-15T10:00:00Z")
266+
);
267+
268+
const queue = new Queue<CommitNode>(latestFirstComparator);
269+
270+
queue.push(latestCommit);
271+
queue.push(olderCommit);
272+
queue.push(newerCommit);
273+
274+
const first = queue.pop();
275+
const second = queue.pop();
276+
const third = queue.pop();
277+
278+
expect(first?.commit.id).toBe("latest-commit");
279+
expect(second?.commit.id).toBe("newer-commit");
280+
expect(third?.commit.id).toBe("older-commit");
281+
});
282+
283+
it("should handle mixed leaf and non-leaf nodes", () => {
284+
const leafNode = createCommitNode(
285+
createMockCommitRaw("leaf", ["main"], [], "2023-01-01")
286+
);
287+
const nonLeafNode = createCommitNode(
288+
createMockCommitRaw("nonLeaf", [], [], "2023-01-02")
289+
);
290+
291+
const queue = new Queue<CommitNode>(latestFirstComparator);
292+
293+
queue.push(nonLeafNode);
294+
queue.push(leafNode);
295+
296+
const first = queue.pop();
297+
const second = queue.pop();
298+
299+
expect(first?.commit.id).toBe("nonLeaf");
300+
expect(second?.commit.id).toBe("leaf");
301+
});
302+
303+
it("should handle mixed leaf and non-leaf nodes with different insertion order", () => {
304+
const leafNode = createCommitNode(
305+
createMockCommitRaw("leaf", ["main"], [], "2023-01-01")
306+
);
307+
const nonLeafNode = createCommitNode(
308+
createMockCommitRaw("nonLeaf", [], [], "2023-01-02")
309+
);
310+
311+
const queue = new Queue<CommitNode>(latestFirstComparator);
312+
313+
queue.push(leafNode);
314+
queue.push(nonLeafNode);
315+
316+
const first = queue.pop();
317+
const second = queue.pop();
318+
319+
expect(first?.commit.id).toBe("leaf");
320+
expect(second?.commit.id).toBe("nonLeaf");
321+
});
322+
});
323+
});

0 commit comments

Comments
 (0)