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 => {
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+ */
5987const 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 */
80133export 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} ;
0 commit comments