1+ import {
2+ collectSquashNodes ,
3+ extractNestedMergeParents ,
4+ findSquashEndIndex ,
5+ findSquashStartNodeIndex ,
6+ getParentCommits ,
7+ } from "./csm.util" ;
18import { convertPRCommitsToCommitNodes , convertPRDetailToCommitRaw } from "./pullRequest" ;
29import 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+ */
415const 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+ */
8987const 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 */
110111export 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" ) ;
0 commit comments