Skip to content

Commit e30cc35

Browse files
authored
Merge core/csmDict into main
Merge core/csmDict into main
2 parents 7b66beb + 46577d7 commit e30cc35

File tree

30 files changed

+958
-169
lines changed

30 files changed

+958
-169
lines changed

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

Lines changed: 404 additions & 7 deletions
Large diffs are not rendered by default.
Lines changed: 145 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
1+
import {
2+
collectSquashNodes,
3+
extractNestedMergeParents,
4+
findSquashEndIndex,
5+
findSquashStartNodeIndex,
6+
getParentCommits,
7+
} from "./csm.util";
18
import { convertPRCommitsToCommitNodes, convertPRDetailToCommitRaw } from "./pullRequest";
29
import type { CommitDict, CommitNode, CSMDictionary, CSMNode, PullRequest, PullRequestDict, StemDict } from "./types";
310

11+
/**
12+
* Builds a CSM node.
13+
* For merge commits, collects squashed commits using DFS traversal.
14+
*/
415
const buildCSMNode = (baseCommitNode: CommitNode, commitDict: CommitDict, stemDict: StemDict): CSMNode => {
5-
const mergeParentCommit = commitDict.get(baseCommitNode.commit.parents[1]);
6-
if (!mergeParentCommit) {
16+
if (baseCommitNode.commit.parents.length <= 1) {
17+
return {
18+
base: baseCommitNode,
19+
source: [],
20+
};
21+
}
22+
23+
// Return empty source for non-merge commits
24+
const mergeParentCommits = getParentCommits(baseCommitNode, commitDict);
25+
if (mergeParentCommits.length === 0) {
726
return {
827
base: baseCommitNode,
928
source: [],
@@ -12,42 +31,43 @@ const buildCSMNode = (baseCommitNode: CommitNode, commitDict: CommitDict, stemDi
1231

1332
const squashCommitNodes: CommitNode[] = [];
1433

15-
const squashTaskQueue: CommitNode[] = [mergeParentCommit];
34+
const squashTaskQueue: CommitNode[] = [...mergeParentCommits];
1635
while (squashTaskQueue.length > 0) {
17-
// get target
18-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
19-
const squashStartNode = squashTaskQueue.shift()!;
36+
const squashStartNode = squashTaskQueue.shift();
37+
if (!squashStartNode?.stemId) {
38+
continue;
39+
}
2040

21-
// get target's stem
22-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
23-
const squashStemId = squashStartNode.stemId!;
41+
const squashStemId = squashStartNode.stemId;
2442
const squashStem = stemDict.get(squashStemId);
2543
if (!squashStem) {
2644
continue;
2745
}
2846

29-
// prepare squash
30-
const squashStemLastIndex = squashStem.nodes.length - 1;
31-
const squashStartNodeIndex = squashStem.nodes.findIndex(({ commit: { id } }) => id === squashStartNode.commit.id);
32-
const spliceCount = squashStemLastIndex - squashStartNodeIndex + 1;
33-
34-
// squash
35-
const spliceCommitNodes = squashStem.nodes.splice(squashStartNodeIndex, spliceCount);
36-
squashCommitNodes.push(...spliceCommitNodes);
37-
38-
// check nested-merge
39-
const nestedMergeParentCommitIds = spliceCommitNodes
40-
.filter((node) => node.commit.parents.length > 1)
41-
.map((node) => node.commit.parents)
42-
.reduce((pCommitIds, parents) => [...pCommitIds, ...parents], []);
43-
const nestedMergeParentCommits = nestedMergeParentCommitIds
44-
.map((commitId) => commitDict.get(commitId))
45-
.filter((node): node is CommitNode => node !== undefined)
46-
.filter((node) => node.stemId !== baseCommitNode.stemId && node.stemId !== squashStemId);
47-
48-
squashTaskQueue.push(...nestedMergeParentCommits);
47+
// Find the index of the start node in the stem
48+
const squashStartNodeIndex = findSquashStartNodeIndex(squashStem, squashStartNode.commit.id);
49+
if (squashStartNodeIndex === -1) {
50+
continue;
51+
}
52+
53+
// Find the end index for squash collection
54+
const endIndex = findSquashEndIndex(squashStem, squashStartNodeIndex);
55+
56+
// Collect nodes and remove from stem
57+
const collectedNodes = collectSquashNodes(squashStem, squashStartNodeIndex, endIndex);
58+
squashCommitNodes.push(...collectedNodes);
59+
60+
// Handle nested merges: add branches to queue if collected nodes contain merge commits
61+
const nestedMergeParents = extractNestedMergeParents(
62+
collectedNodes,
63+
commitDict,
64+
baseCommitNode.stemId ?? "",
65+
squashStemId
66+
);
67+
squashTaskQueue.push(...nestedMergeParents);
4968
}
5069

70+
// Sort by sequence order (reverse -> forward)
5171
squashCommitNodes.sort((a, b) => a.commit.sequence - b.commit.sequence);
5272

5373
return {
@@ -56,6 +76,14 @@ const buildCSMNode = (baseCommitNode: CommitNode, commitDict: CommitDict, stemDi
5676
};
5777
};
5878

79+
/**
80+
* Integrates Pull Request information into a CSM node.
81+
* Reflects PR details in commit message and statistics.
82+
*
83+
* @param csmNode - Existing CSM node
84+
* @param pr - Pull Request information
85+
* @returns CSM node with integrated PR information
86+
*/
5987
const buildCSMNodeWithPullRequest = (csmNode: CSMNode, pr: PullRequest): CSMNode => {
6088
const convertedCommit = convertPRDetailToCommitRaw(csmNode.base.commit, pr);
6189

@@ -68,14 +96,39 @@ const buildCSMNodeWithPullRequest = (csmNode: CSMNode, pr: PullRequest): CSMNode
6896
};
6997
};
7098

99+
/** Builds a Pull Request dictionary indexed by merge commit SHA. */
100+
const buildPRDict = (pullRequests: Array<PullRequest>): PullRequestDict => {
101+
return pullRequests.reduce(
102+
(dict, pr) => dict.set(`${pr.detail.data.merge_commit_sha}`, pr),
103+
new Map<string, PullRequest>() as PullRequestDict
104+
);
105+
};
106+
107+
/** Builds CSM nodes from commit nodes with PR integration. */
108+
const buildCSMNodesWithPR = (
109+
commitNodes: CommitNode[],
110+
commitDict: CommitDict,
111+
stemDict: StemDict,
112+
prDict: PullRequestDict
113+
): CSMNode[] => {
114+
return commitNodes.map((commitNode) => {
115+
const csmNode = buildCSMNode(commitNode, commitDict, stemDict);
116+
const pr = prDict.get(csmNode.base.commit.id);
117+
return pr ? buildCSMNodeWithPullRequest(csmNode, pr) : csmNode;
118+
});
119+
};
120+
71121
/**
72-
* CSM 생성
122+
* Builds a CSM (Commit Summary Model) dictionary.
123+
* Creates CSM nodes for each commit in the base branch,
124+
* and integrates Pull Request information if available.
73125
*
74-
* @param {Map<string, CommitNode>} commitDict
75-
* @param {Map<string, Stem>} stemDict
76-
* @param {string} baseBranchName
77-
* @param {Array<PullRequest>} pullRequests
78-
* @returns {CSMDictionary}
126+
* @param commitDict - Commit dictionary
127+
* @param stemDict - Stem dictionary
128+
* @param baseBranchName - Base branch name (e.g., 'main', 'master')
129+
* @param pullRequests - Pull Request array (optional)
130+
* @returns CSM dictionary (CSM node array per branch)
131+
* @throws {Error} When there are no stems or no base branch stem
79132
*/
80133
export const buildCSMDict = (
81134
commitDict: CommitDict,
@@ -88,25 +141,69 @@ export const buildCSMDict = (
88141
// return {};
89142
}
90143

91-
// v0.1 에서는 master STEM 으로만 CSM 생성함
144+
// In v0.1, CSM is only created from the master STEM
92145
const masterStem = stemDict.get(baseBranchName);
93146
if (!masterStem) {
94147
throw new Error("no master-stem");
95148
// return {};
96149
}
97150

98-
const prDictByMergedCommitSha = pullRequests.reduce(
99-
(dict, pr) => dict.set(`${pr.detail.data.merge_commit_sha}`, pr),
100-
new Map<string, PullRequest>() as PullRequestDict
101-
);
151+
const prDict = buildPRDict(pullRequests);
152+
const csmNodes = buildCSMNodesWithPR(masterStem.nodes, commitDict, stemDict, prDict);
102153

103-
const csmDict: CSMDictionary = {};
104-
const stemNodes = masterStem.nodes.reverse(); // start on root-node
105-
csmDict[baseBranchName] = stemNodes.map((commitNode) => {
106-
const csmNode = buildCSMNode(commitNode, commitDict, stemDict);
107-
const pr = prDictByMergedCommitSha.get(csmNode.base.commit.id);
108-
return pr ? buildCSMNodeWithPullRequest(csmNode, pr) : csmNode;
109-
});
154+
return {
155+
[baseBranchName]: csmNodes,
156+
};
157+
};
158+
159+
/**
160+
* Builds a paginated CSM dictionary.
161+
* Creates CSM nodes for a specific range of commits in the base branch,
162+
* enabling efficient lazy loading for large repositories.
163+
*/
164+
export const buildPaginatedCSMDict = (
165+
commitDict: CommitDict,
166+
stemDict: StemDict,
167+
baseBranchName: string,
168+
commitCountPerPage: number,
169+
lastCommitId?: string,
170+
pullRequests: Array<PullRequest> = []
171+
): CSMDictionary => {
172+
// Validate commitCountPerPage
173+
if (commitCountPerPage <= 0) {
174+
throw new Error("commitCountPerPage must be greater than 0");
175+
}
176+
177+
// Validate stemDict
178+
if (stemDict.size === 0) {
179+
throw new Error("no stem");
180+
}
181+
182+
// Get base branch stem
183+
const baseStem = stemDict.get(baseBranchName);
184+
if (!baseStem) {
185+
throw new Error("no master-stem");
186+
}
110187

111-
return csmDict;
188+
// Determine start index based on cursor
189+
let startIndex = 0;
190+
if (lastCommitId) {
191+
const lastCommitIndex = baseStem.nodes.findIndex((node) => node.commit.id === lastCommitId);
192+
if (lastCommitIndex === -1) {
193+
throw new Error("Invalid lastCommitId");
194+
}
195+
startIndex = lastCommitIndex + 1;
196+
}
197+
198+
// Calculate end index and extract page nodes
199+
const endIndex = Math.min(startIndex + commitCountPerPage, baseStem.nodes.length);
200+
const pageNodes = baseStem.nodes.slice(startIndex, endIndex);
201+
202+
// Build CSM nodes with PR integration
203+
const prDict = buildPRDict(pullRequests);
204+
const csmNodes = buildCSMNodesWithPR(pageNodes, commitDict, stemDict, prDict);
205+
206+
return {
207+
[baseBranchName]: csmNodes,
208+
};
112209
};
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import type { CommitDict, CommitNode, Stem } from "./types";
2+
3+
/** Gets the second parent (starting point of merged branch) of a merge commit. */
4+
export const getParentCommits = (baseCommitNode: CommitNode, commitDict: CommitDict): CommitNode[] => {
5+
return baseCommitNode.commit.parents
6+
.slice(1)
7+
.map((parentId) => commitDict.get(parentId))
8+
.filter((commit): commit is CommitNode => !!commit);
9+
};
10+
11+
/** Finds the index of a specific commit node in a stem. */
12+
export const findSquashStartNodeIndex = (stem: Stem, commitId: string): number => {
13+
return stem.nodes.findIndex(({ commit: { id } }) => id === commitId);
14+
};
15+
16+
/**
17+
* Finds the end index for squash collection.
18+
* Collects until the next mergedIntoStem node appears or until the end of the stem.
19+
*/
20+
export const findSquashEndIndex = (stem: Stem, startIndex: number): number => {
21+
let endIndex = stem.nodes.length - 1;
22+
23+
for (let i = startIndex + 1; i < stem.nodes.length; i++) {
24+
if (stem.nodes[i].mergedIntoBaseStem) {
25+
endIndex = i - 1;
26+
break;
27+
}
28+
}
29+
30+
return endIndex;
31+
};
32+
33+
/**
34+
* Collects nodes from start index to end index in a stem and removes them.
35+
*
36+
* @param stem - Target stem
37+
* @param startIndex - Start index
38+
* @param endIndex - End index
39+
* @returns Array of collected commit nodes
40+
*/
41+
export const collectSquashNodes = (stem: Stem, startIndex: number, endIndex: number): CommitNode[] => {
42+
const nodesToCollect: CommitNode[] = [];
43+
44+
for (let i = startIndex; i <= endIndex; i++) {
45+
nodesToCollect.push(stem.nodes[i]);
46+
}
47+
48+
// Remove collected nodes from stem
49+
stem.nodes.splice(startIndex, nodesToCollect.length);
50+
51+
return nodesToCollect;
52+
};
53+
54+
/**
55+
* Extracts parent commits of nested merges from squashed nodes.
56+
* Returns parent commits that do not belong to the base stem or current squash stem.
57+
*/
58+
export const extractNestedMergeParents = (
59+
squashedNodes: CommitNode[],
60+
commitDict: CommitDict,
61+
baseStemId: string,
62+
currentSquashStemId: string
63+
): CommitNode[] => {
64+
// Collect all parent commit IDs from merge commits
65+
const nestedMergeParentCommitIds = squashedNodes
66+
.filter((node) => node.commit.parents.length > 1)
67+
.map((node) => node.commit.parents)
68+
.reduce((pCommitIds, parents) => [...pCommitIds, ...parents], []);
69+
70+
return nestedMergeParentCommitIds
71+
.map((commitId) => commitDict.get(commitId))
72+
.filter((node): node is CommitNode => node !== undefined)
73+
.filter((node) => node.stemId !== baseStemId && node.stemId !== currentSquashStemId);
74+
};

0 commit comments

Comments
 (0)