Skip to content

Commit 477a6f3

Browse files
authored
Merge pull request #917 from githru/feature/#916_build-csm-with-pagination
feat(engine): Pagination 기능을 가진 CSM Dictionary Builder 구현
2 parents 585f54a + b8b81ca commit 477a6f3

3 files changed

Lines changed: 226 additions & 14 deletions

File tree

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

Lines changed: 146 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { buildCSMDict } from "./csm";
2-
import type { CommitNode, CommitRaw, CSMDictionary, Stem } from "./types";
1+
import { buildCSMDict, buildPaginatedCSMDict } from "./csm";
2+
import type { CommitNode, CommitRaw, CSMDictionary, PullRequest, Stem } from "./types";
33

44
describe("csm", () => {
55
// master = [0, 1, 2, 3, 4, 5]
@@ -435,4 +435,148 @@ describe("csm", () => {
435435
expect(mergeNode!.source).toEqual([]);
436436
});
437437
});
438+
439+
describe("buildPaginatedCSMDict", () => {
440+
it("should load first page when lastCommitId is not provided", () => {
441+
const perPage = 2;
442+
const result = buildPaginatedCSMDict(
443+
fakeCommitNodeDict,
444+
fakeStemDict,
445+
"master",
446+
perPage
447+
);
448+
449+
expect(result).toBeDefined();
450+
expect(result.master).toBeDefined();
451+
expect(result.master.length).toBe(2);
452+
// Master nodes in order: [5, 4, 3, 2, 1, 0]
453+
// First page should return [5, 4]
454+
expect(result.master[0].base.commit.id).toBe("5");
455+
expect(result.master[1].base.commit.id).toBe("4");
456+
});
457+
458+
it("should load next page when lastCommitId is provided", () => {
459+
const perPage = 2;
460+
const result = buildPaginatedCSMDict(
461+
fakeCommitNodeDict,
462+
fakeStemDict,
463+
"master",
464+
perPage,
465+
"4" // Last commit of first page
466+
);
467+
468+
expect(result).toBeDefined();
469+
expect(result.master).toBeDefined();
470+
expect(result.master.length).toBe(2);
471+
// Next page should return [3, 2]
472+
expect(result.master[0].base.commit.id).toBe("3");
473+
expect(result.master[1].base.commit.id).toBe("2");
474+
});
475+
476+
it("should return remaining nodes when perPage exceeds remaining nodes", () => {
477+
const perPage = 10;
478+
const result = buildPaginatedCSMDict(
479+
fakeCommitNodeDict,
480+
fakeStemDict,
481+
"master",
482+
perPage,
483+
"2" // Only [1, 0] remaining
484+
);
485+
486+
expect(result).toBeDefined();
487+
expect(result.master).toBeDefined();
488+
expect(result.master.length).toBe(2);
489+
expect(result.master[0].base.commit.id).toBe("1");
490+
expect(result.master[1].base.commit.id).toBe("0");
491+
});
492+
493+
it("should return empty array when no more nodes available", () => {
494+
const perPage = 2;
495+
const result = buildPaginatedCSMDict(
496+
fakeCommitNodeDict,
497+
fakeStemDict,
498+
"master",
499+
perPage,
500+
"0" // Last node
501+
);
502+
503+
expect(result).toBeDefined();
504+
expect(result.master).toBeDefined();
505+
expect(result.master.length).toBe(0);
506+
});
507+
508+
it("should throw error when lastCommitId is invalid", () => {
509+
expect(() => {
510+
buildPaginatedCSMDict(
511+
fakeCommitNodeDict,
512+
fakeStemDict,
513+
"master",
514+
2,
515+
"invalid-commit-id"
516+
);
517+
}).toThrow("Invalid lastCommitId");
518+
});
519+
520+
it("should throw error when perPage is less than or equal to 0", () => {
521+
expect(() => {
522+
buildPaginatedCSMDict(
523+
fakeCommitNodeDict,
524+
fakeStemDict,
525+
"master",
526+
0
527+
);
528+
}).toThrow("perPage must be greater than 0");
529+
530+
expect(() => {
531+
buildPaginatedCSMDict(
532+
fakeCommitNodeDict,
533+
fakeStemDict,
534+
"master",
535+
-1
536+
);
537+
}).toThrow("perPage must be greater than 0");
538+
});
539+
540+
it("should throw error when base branch does not exist", () => {
541+
expect(() => {
542+
buildPaginatedCSMDict(
543+
fakeCommitNodeDict,
544+
fakeStemDict,
545+
"non-existent-branch",
546+
2
547+
);
548+
}).toThrow("no master-stem");
549+
});
550+
551+
it("should integrate pull request information when provided", () => {
552+
const fakePR = {
553+
detail: {
554+
data: {
555+
merge_commit_sha: "5",
556+
},
557+
headers: {},
558+
status: 200,
559+
url: "",
560+
},
561+
commitDetails: {
562+
data: [],
563+
headers: {},
564+
status: 200,
565+
url: "",
566+
},
567+
} as unknown as PullRequest;
568+
569+
const result = buildPaginatedCSMDict(
570+
fakeCommitNodeDict,
571+
fakeStemDict,
572+
"master",
573+
1,
574+
undefined,
575+
[fakePR]
576+
);
577+
578+
expect(result.master[0].base.commit.id).toBe("5");
579+
// PR integration logic should be applied
580+
});
581+
});
438582
});

packages/analysis-engine/src/csm.ts

Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,28 @@ const buildCSMNodeWithPullRequest = (csmNode: CSMNode, pr: PullRequest): CSMNode
9696
};
9797
};
9898

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+
99121
/**
100122
* Builds a CSM (Commit Summary Model) dictionary.
101123
* Creates CSM nodes for each commit in the base branch,
@@ -126,18 +148,62 @@ export const buildCSMDict = (
126148
// return {};
127149
}
128150

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

134-
const csmDict: CSMDictionary = {};
135-
const stemNodes = masterStem.nodes; // start on latest-node
136-
csmDict[baseBranchName] = stemNodes.map((commitNode) => {
137-
const csmNode = buildCSMNode(commitNode, commitDict, stemDict);
138-
const pr = prDictByMergedCommitSha.get(csmNode.base.commit.id);
139-
return pr ? buildCSMNodeWithPullRequest(csmNode, pr) : csmNode;
140-
});
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+
perPage: number,
169+
lastCommitId?: string,
170+
pullRequests: Array<PullRequest> = []
171+
): CSMDictionary => {
172+
// Validate perPage
173+
if (perPage <= 0) {
174+
throw new Error("perPage 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+
}
141187

142-
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 + perPage, 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+
};
143209
};

packages/analysis-engine/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { PluginOctokit } from "./pluginOctokit";
99
import { buildStemDict } from "./stem";
1010
import { getSummary } from "./summary";
1111

12+
export { buildPaginatedCSMDict } from "./csm";
13+
1214
type AnalysisEngineArgs = {
1315
isDebugMode?: boolean;
1416
gitLog: string;

0 commit comments

Comments
 (0)