Skip to content

Commit 585f54a

Browse files
authored
Merge pull request #915 from githru/refactor/csm-improvements
refactor(engine): CSM 코드 구조 및 가독성 개선
2 parents 0057860 + 478ef82 commit 585f54a

2 files changed

Lines changed: 128 additions & 53 deletions

File tree

packages/analysis-engine/src/csm.ts

Lines changed: 54 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
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 => {
516
if (baseCommitNode.commit.parents.length <= 1) {
617
return {
@@ -9,11 +20,8 @@ const buildCSMNode = (baseCommitNode: CommitNode, commitDict: CommitDict, stemDi
920
};
1021
}
1122

12-
const mergeParentCommits = baseCommitNode.commit.parents
13-
.slice(1)
14-
.map((parentId) => commitDict.get(parentId))
15-
.filter((commit): commit is CommitNode => !!commit);
16-
23+
// Return empty source for non-merge commits
24+
const mergeParentCommits = getParentCommits(baseCommitNode, commitDict);
1725
if (mergeParentCommits.length === 0) {
1826
return {
1927
base: baseCommitNode,
@@ -25,59 +33,41 @@ const buildCSMNode = (baseCommitNode: CommitNode, commitDict: CommitDict, stemDi
2533

2634
const squashTaskQueue: CommitNode[] = [...mergeParentCommits];
2735
while (squashTaskQueue.length > 0) {
28-
// get target
29-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
30-
const squashStartNode = squashTaskQueue.shift()!;
36+
const squashStartNode = squashTaskQueue.shift();
37+
if (!squashStartNode?.stemId) {
38+
continue;
39+
}
3140

32-
// get target's stem
33-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
34-
const squashStemId = squashStartNode.stemId!;
41+
const squashStemId = squashStartNode.stemId;
3542
const squashStem = stemDict.get(squashStemId);
3643
if (!squashStem) {
3744
continue;
3845
}
3946

40-
// find squashStartNode index in stem
41-
const squashStartNodeIndex = squashStem.nodes.findIndex(({ commit: { id } }) => id === squashStartNode.commit.id);
47+
// Find the index of the start node in the stem
48+
const squashStartNodeIndex = findSquashStartNodeIndex(squashStem, squashStartNode.commit.id);
4249
if (squashStartNodeIndex === -1) {
4350
continue;
4451
}
4552

46-
// collect nodes from squashStartNode to end of stem
47-
// (or until the next mergedIntoStem node for future merges)
48-
const spliceCommitNodes: CommitNode[] = [];
49-
50-
// First, find the end index (before next mergedIntoStem or end of stem)
51-
let endIndex = squashStem.nodes.length - 1;
52-
for (let i = squashStartNodeIndex + 1; i < squashStem.nodes.length; i++) {
53-
if (squashStem.nodes[i].mergedIntoBaseStem) {
54-
endIndex = i - 1;
55-
break;
56-
}
57-
}
58-
59-
// Collect nodes from start to end
60-
for (let i = squashStartNodeIndex; i <= endIndex; i++) {
61-
spliceCommitNodes.push(squashStem.nodes[i]);
62-
}
63-
64-
// remove collected nodes from stem
65-
squashStem.nodes.splice(squashStartNodeIndex, spliceCommitNodes.length);
66-
squashCommitNodes.push(...spliceCommitNodes);
67-
68-
// check nested-merge
69-
const nestedMergeParentCommitIds = spliceCommitNodes
70-
.filter((node) => node.commit.parents.length > 1)
71-
.map((node) => node.commit.parents)
72-
.reduce((pCommitIds, parents) => [...pCommitIds, ...parents], []);
73-
const nestedMergeParentCommits = nestedMergeParentCommitIds
74-
.map((commitId) => commitDict.get(commitId))
75-
.filter((node): node is CommitNode => node !== undefined)
76-
.filter((node) => node.stemId !== baseCommitNode.stemId && node.stemId !== squashStemId);
77-
78-
squashTaskQueue.push(...nestedMergeParentCommits);
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);
7968
}
8069

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

8373
return {
@@ -86,6 +76,14 @@ const buildCSMNode = (baseCommitNode: CommitNode, commitDict: CommitDict, stemDi
8676
};
8777
};
8878

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+
*/
8987
const buildCSMNodeWithPullRequest = (csmNode: CSMNode, pr: PullRequest): CSMNode => {
9088
const convertedCommit = convertPRDetailToCommitRaw(csmNode.base.commit, pr);
9189

@@ -99,13 +97,16 @@ const buildCSMNodeWithPullRequest = (csmNode: CSMNode, pr: PullRequest): CSMNode
9997
};
10098

10199
/**
102-
* CSM 생성
100+
* Builds a CSM (Commit Summary Model) dictionary.
101+
* Creates CSM nodes for each commit in the base branch,
102+
* and integrates Pull Request information if available.
103103
*
104-
* @param {Map<string, CommitNode>} commitDict
105-
* @param {Map<string, Stem>} stemDict
106-
* @param {string} baseBranchName
107-
* @param {Array<PullRequest>} pullRequests
108-
* @returns {CSMDictionary}
104+
* @param commitDict - Commit dictionary
105+
* @param stemDict - Stem dictionary
106+
* @param baseBranchName - Base branch name (e.g., 'main', 'master')
107+
* @param pullRequests - Pull Request array (optional)
108+
* @returns CSM dictionary (CSM node array per branch)
109+
* @throws {Error} When there are no stems or no base branch stem
109110
*/
110111
export const buildCSMDict = (
111112
commitDict: CommitDict,
@@ -118,7 +119,7 @@ export const buildCSMDict = (
118119
// return {};
119120
}
120121

121-
// v0.1 에서는 master STEM 으로만 CSM 생성함
122+
// In v0.1, CSM is only created from the master STEM
122123
const masterStem = stemDict.get(baseBranchName);
123124
if (!masterStem) {
124125
throw new Error("no master-stem");
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)