Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 146 additions & 2 deletions packages/analysis-engine/src/csm.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { buildCSMDict } from "./csm";
import type { CommitNode, CommitRaw, CSMDictionary, Stem } from "./types";
import { buildCSMDict, buildPaginatedCSMDict } from "./csm";
import type { CommitNode, CommitRaw, CSMDictionary, PullRequest, Stem } from "./types";

describe("csm", () => {
// master = [0, 1, 2, 3, 4, 5]
Expand Down Expand Up @@ -179,4 +179,148 @@ describe("csm", () => {
});
});
});

describe("buildPaginatedCSMDict", () => {
it("should load first page when lastCommitId is not provided", () => {
const perPage = 2;
const result = buildPaginatedCSMDict(
fakeCommitNodeDict,
fakeStemDict,
"master",
perPage
);

expect(result).toBeDefined();
expect(result.master).toBeDefined();
expect(result.master.length).toBe(2);
// Master nodes in order: [5, 4, 3, 2, 1, 0]
// First page should return [5, 4]
expect(result.master[0].base.commit.id).toBe("5");
expect(result.master[1].base.commit.id).toBe("4");
});

it("should load next page when lastCommitId is provided", () => {
const perPage = 2;
const result = buildPaginatedCSMDict(
fakeCommitNodeDict,
fakeStemDict,
"master",
perPage,
"4" // Last commit of first page
);

expect(result).toBeDefined();
expect(result.master).toBeDefined();
expect(result.master.length).toBe(2);
// Next page should return [3, 2]
expect(result.master[0].base.commit.id).toBe("3");
expect(result.master[1].base.commit.id).toBe("2");
});

it("should return remaining nodes when perPage exceeds remaining nodes", () => {
const perPage = 10;
const result = buildPaginatedCSMDict(
fakeCommitNodeDict,
fakeStemDict,
"master",
perPage,
"2" // Only [1, 0] remaining
);

expect(result).toBeDefined();
expect(result.master).toBeDefined();
expect(result.master.length).toBe(2);
expect(result.master[0].base.commit.id).toBe("1");
expect(result.master[1].base.commit.id).toBe("0");
});

it("should return empty array when no more nodes available", () => {
const perPage = 2;
const result = buildPaginatedCSMDict(
fakeCommitNodeDict,
fakeStemDict,
"master",
perPage,
"0" // Last node
);

expect(result).toBeDefined();
expect(result.master).toBeDefined();
expect(result.master.length).toBe(0);
});

it("should throw error when lastCommitId is invalid", () => {
expect(() => {
buildPaginatedCSMDict(
fakeCommitNodeDict,
fakeStemDict,
"master",
2,
"invalid-commit-id"
);
}).toThrow("Invalid lastCommitId");
});

it("should throw error when perPage is less than or equal to 0", () => {
expect(() => {
buildPaginatedCSMDict(
fakeCommitNodeDict,
fakeStemDict,
"master",
0
);
}).toThrow("perPage must be greater than 0");

expect(() => {
buildPaginatedCSMDict(
fakeCommitNodeDict,
fakeStemDict,
"master",
-1
);
}).toThrow("perPage must be greater than 0");
});

it("should throw error when base branch does not exist", () => {
expect(() => {
buildPaginatedCSMDict(
fakeCommitNodeDict,
fakeStemDict,
"non-existent-branch",
2
);
}).toThrow("no master-stem");
});

it("should integrate pull request information when provided", () => {
const fakePR = {
detail: {
data: {
merge_commit_sha: "5",
},
headers: {},
status: 200,
url: "",
},
commitDetails: {
data: [],
headers: {},
status: 200,
url: "",
},
} as unknown as PullRequest;

const result = buildPaginatedCSMDict(
fakeCommitNodeDict,
fakeStemDict,
"master",
1,
undefined,
[fakePR]
);

expect(result.master[0].base.commit.id).toBe("5");
// PR integration logic should be applied
});
});
});
90 changes: 78 additions & 12 deletions packages/analysis-engine/src/csm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,28 @@ const buildCSMNodeWithPullRequest = (csmNode: CSMNode, pr: PullRequest): CSMNode
};
};

/** Builds a Pull Request dictionary indexed by merge commit SHA. */
const buildPRDict = (pullRequests: Array<PullRequest>): PullRequestDict => {
return pullRequests.reduce(
(dict, pr) => dict.set(`${pr.detail.data.merge_commit_sha}`, pr),
new Map<string, PullRequest>() as PullRequestDict
);
};

/** Builds CSM nodes from commit nodes with PR integration. */
const buildCSMNodesWithPR = (
commitNodes: CommitNode[],
commitDict: CommitDict,
stemDict: StemDict,
prDict: PullRequestDict
): CSMNode[] => {
return commitNodes.map((commitNode) => {
const csmNode = buildCSMNode(commitNode, commitDict, stemDict);
const pr = prDict.get(csmNode.base.commit.id);
return pr ? buildCSMNodeWithPullRequest(csmNode, pr) : csmNode;
});
};

/**
* Builds a CSM (Commit Summary Model) dictionary.
* Creates CSM nodes for each commit in the base branch,
Expand Down Expand Up @@ -120,18 +142,62 @@ export const buildCSMDict = (
// return {};
}

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

const csmDict: CSMDictionary = {};
const stemNodes = masterStem.nodes; // start on latest-node
csmDict[baseBranchName] = stemNodes.map((commitNode) => {
const csmNode = buildCSMNode(commitNode, commitDict, stemDict);
const pr = prDictByMergedCommitSha.get(csmNode.base.commit.id);
return pr ? buildCSMNodeWithPullRequest(csmNode, pr) : csmNode;
});
return {
[baseBranchName]: csmNodes,
};
};

/**
* Builds a paginated CSM dictionary.
* Creates CSM nodes for a specific range of commits in the base branch,
* enabling efficient lazy loading for large repositories.
*/
export const buildPaginatedCSMDict = (
commitDict: CommitDict,
stemDict: StemDict,
baseBranchName: string,
perPage: number,
lastCommitId?: string,
pullRequests: Array<PullRequest> = []
): CSMDictionary => {
// Validate perPage
if (perPage <= 0) {
throw new Error("perPage must be greater than 0");
}

// Validate stemDict
if (stemDict.size === 0) {
throw new Error("no stem");
}

// Get base branch stem
const baseStem = stemDict.get(baseBranchName);
if (!baseStem) {
throw new Error("no master-stem");
}

return csmDict;
// Determine start index based on cursor
let startIndex = 0;
if (lastCommitId) {
const lastCommitIndex = baseStem.nodes.findIndex((node) => node.commit.id === lastCommitId);
if (lastCommitIndex === -1) {
throw new Error("Invalid lastCommitId");
}
startIndex = lastCommitIndex + 1;
}

// Calculate end index and extract page nodes
const endIndex = Math.min(startIndex + perPage, baseStem.nodes.length);
const pageNodes = baseStem.nodes.slice(startIndex, endIndex);

// Build CSM nodes with PR integration
const prDict = buildPRDict(pullRequests);
const csmNodes = buildCSMNodesWithPR(pageNodes, commitDict, stemDict, prDict);

return {
[baseBranchName]: csmNodes,
};
};
2 changes: 2 additions & 0 deletions packages/analysis-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { PluginOctokit } from "./pluginOctokit";
import { buildStemDict } from "./stem";
import { getSummary } from "./summary";

export { buildPaginatedCSMDict } from "./csm";

type AnalysisEngineArgs = {
isDebugMode?: boolean;
gitLog: string;
Expand Down