Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
73 changes: 34 additions & 39 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,38 @@ import {
updateComment,
} from "./services/codecommit.js";

const BLOB_FETCH_CONCURRENCY = 5;

async function fetchBlobTexts(
client: CodeCommitClient,
repositoryName: string,
differences: Difference[],
): Promise<Map<string, { before: string; after: string }>> {
const texts = new Map<string, { before: string; after: string }>();
let index = 0;

async function worker() {
while (index < differences.length) {
const i = index++;
const diff = differences[i]!;
const beforeBlobId = diff.beforeBlob?.blobId;
const afterBlobId = diff.afterBlob?.blobId;
const key = `${beforeBlobId ?? ""}:${afterBlobId ?? ""}`;

const [before, after] = await Promise.all([
beforeBlobId ? getBlobContent(client, repositoryName, beforeBlobId) : Promise.resolve(""),
afterBlobId ? getBlobContent(client, repositoryName, afterBlobId) : Promise.resolve(""),
]);

texts.set(key, { before, after });
}
}

const workerCount = Math.min(BLOB_FETCH_CONCURRENCY, differences.length);
await Promise.all(Array.from({ length: workerCount }, () => worker()));
return texts;
}

type Screen = "repos" | "prs" | "detail";

interface PaginationState {
Expand Down Expand Up @@ -231,27 +263,7 @@ export function App({ client, initialRepo }: AppProps) {

const reactionsPromise = reloadReactions(detail.commentThreads);

const blobPromise = (async () => {
const blobFetches = detail.differences.map(async (diff) => {
const beforeBlobId = diff.beforeBlob?.blobId;
const afterBlobId = diff.afterBlob?.blobId;
const key = `${beforeBlobId ?? ""}:${afterBlobId ?? ""}`;

const [before, after] = await Promise.all([
beforeBlobId ? getBlobContent(client, selectedRepo, beforeBlobId) : Promise.resolve(""),
afterBlobId ? getBlobContent(client, selectedRepo, afterBlobId) : Promise.resolve(""),
]);

return { key, before, after };
});

const blobResults = await Promise.all(blobFetches);
const texts = new Map<string, { before: string; after: string }>();
for (const result of blobResults) {
texts.set(result.key, { before: result.before, after: result.after });
}
return texts;
})();
const blobPromise = fetchBlobTexts(client, selectedRepo, detail.differences);

const commitsPromise =
sourceCommit && mergeBase
Expand Down Expand Up @@ -511,24 +523,7 @@ export function App({ client, initialRepo }: AppProps) {
const diffs = await getCommitDifferences(client, selectedRepo, parentId, commit.commitId);
setCommitDifferences(diffs);

const blobFetches = diffs.map(async (diff) => {
const beforeBlobId = diff.beforeBlob?.blobId;
const afterBlobId = diff.afterBlob?.blobId;
const key = `${beforeBlobId ?? ""}:${afterBlobId ?? ""}`;

const [before, after] = await Promise.all([
beforeBlobId ? getBlobContent(client, selectedRepo, beforeBlobId) : Promise.resolve(""),
afterBlobId ? getBlobContent(client, selectedRepo, afterBlobId) : Promise.resolve(""),
]);

return { key, before, after };
});

const blobResults = await Promise.all(blobFetches);
const texts = new Map<string, { before: string; after: string }>();
for (const result of blobResults) {
texts.set(result.key, { before: result.before, after: result.after });
}
const texts = await fetchBlobTexts(client, selectedRepo, diffs);
setCommitDiffTexts(texts);
} finally {
setIsLoadingCommitDiff(false);
Expand Down
133 changes: 133 additions & 0 deletions src/services/codecommit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,50 @@ describe("getPullRequestDetail", () => {
}),
);
});

it("paginates GetDifferences when NextToken is returned", async () => {
mockSend.mockResolvedValueOnce({
pullRequest: {
pullRequestId: "42",
title: "big PR",
pullRequestTargets: [
{
sourceCommit: "abc123",
destinationCommit: "def456",
},
],
},
});
// Promise.all runs getAllDifferences and getComments concurrently.
// Mock order matches actual call order:
// 1. GetDifferencesCommand (page 1) - from getAllDifferences
// 2. GetCommentsForPullRequestCommand - from getComments (concurrent)
// 3. GetDifferencesCommand (page 2) - from getAllDifferences loop
mockSend.mockResolvedValueOnce({
differences: [
{
beforeBlob: { blobId: "b1", path: "file1.ts" },
afterBlob: { blobId: "b2", path: "file1.ts" },
},
],
NextToken: "page2-token",
});
mockSend.mockResolvedValueOnce({ commentsForPullRequestData: [] });
mockSend.mockResolvedValueOnce({
differences: [
{
beforeBlob: { blobId: "b3", path: "file2.ts" },
afterBlob: { blobId: "b4", path: "file2.ts" },
},
],
NextToken: undefined,
});

const detail = await getPullRequestDetail(mockClient, "42", "my-service");
expect(detail.differences).toHaveLength(2);
expect(detail.differences[0].beforeBlob?.path).toBe("file1.ts");
expect(detail.differences[1].beforeBlob?.path).toBe("file2.ts");
});
});

describe("listPullRequests edge cases", () => {
Expand Down Expand Up @@ -802,6 +846,33 @@ describe("getComments", () => {
expect(threads[0].comments[2].commentId).toBe("reply-3");
expect(threads[0].comments[3].commentId).toBe("reply-2");
});

it("paginates when nextToken is returned", async () => {
mockSend.mockResolvedValueOnce({
commentsForPullRequestData: [
{
comments: [{ commentId: "c1", content: "Page 1 comment" }],
},
],
nextToken: "page2-token",
});
mockSend.mockResolvedValueOnce({
commentsForPullRequestData: [
{
comments: [{ commentId: "c2", content: "Page 2 comment" }],
},
],
nextToken: undefined,
});

const threads = await getComments(mockClient, "42");
expect(threads).toHaveLength(2);
expect(threads[0].comments[0].commentId).toBe("c1");
expect(threads[1].comments[0].commentId).toBe("c2");
expect(mockSend).toHaveBeenCalledTimes(2);
// Second call should include nextToken
expect(mockSend.mock.calls[1][0].input.nextToken).toBe("page2-token");
});
});

describe("postCommentReply", () => {
Expand Down Expand Up @@ -1455,6 +1526,47 @@ describe("getCommitsForPR", () => {
const result = await getCommitsForPR(mockClient, "my-service", "commit-0", "unreachable");
expect(result).toHaveLength(100);
});

it("handles merge commits by visiting all parents via BFS", async () => {
// Graph: base -- A -- M (sourceCommit, merge commit)
// /
// B --
// M.parents = [A, B], A.parents = [base], B.parents = [base]
mockSend.mockResolvedValueOnce({
commit: {
commitId: "M",
parents: ["A", "B"],
message: "Merge commit",
author: { name: "watany", date: "1707868803" },
},
});
// BFS level 2: A and B fetched in parallel
mockSend.mockResolvedValueOnce({
commit: {
commitId: "A",
parents: ["base"],
message: "Commit A",
author: { name: "watany", date: "1707868801" },
},
});
mockSend.mockResolvedValueOnce({
commit: {
commitId: "B",
parents: ["base"],
message: "Commit B",
author: { name: "watany", date: "1707868802" },
},
});

const result = await getCommitsForPR(mockClient, "my-service", "M", "base");
expect(result).toHaveLength(3);
const ids = result.map((c) => c.commitId);
expect(ids).toContain("M");
expect(ids).toContain("A");
expect(ids).toContain("B");
// Only 3 API calls (M, A, B) - base is never fetched
expect(mockSend).toHaveBeenCalledTimes(3);
});
});

describe("getCommitDifferences", () => {
Expand Down Expand Up @@ -1492,6 +1604,27 @@ describe("getCommitDifferences", () => {
afterCommitSpecifier: "commitDEF",
});
});

it("paginates when NextToken is returned", async () => {
mockSend.mockResolvedValueOnce({
differences: [
{ beforeBlob: { blobId: "b1", path: "a.ts" }, afterBlob: { blobId: "b2", path: "a.ts" } },
],
NextToken: "token-2",
});
mockSend.mockResolvedValueOnce({
differences: [
{ beforeBlob: { blobId: "b3", path: "b.ts" }, afterBlob: { blobId: "b4", path: "b.ts" } },
],
NextToken: undefined,
});

const result = await getCommitDifferences(mockClient, "my-service", "parent1", "commit1");
expect(result).toHaveLength(2);
expect(result[0].beforeBlob?.path).toBe("a.ts");
expect(result[1].beforeBlob?.path).toBe("b.ts");
expect(mockSend).toHaveBeenCalledTimes(2);
});
});

describe("updateComment", () => {
Expand Down
Loading
Loading