diff --git a/src/github/api/queries/github.ts b/src/github/api/queries/github.ts index 1702061f..a6e8a185 100644 --- a/src/github/api/queries/github.ts +++ b/src/github/api/queries/github.ts @@ -84,6 +84,7 @@ export const PR_QUERY = ` body path line + diffHunk author { login } diff --git a/src/github/data/formatter.ts b/src/github/data/formatter.ts index 82dbe568..ad428ebe 100644 --- a/src/github/data/formatter.ts +++ b/src/github/data/formatter.ts @@ -118,7 +118,16 @@ export function formatReviewComments( body = sanitizeContent(body); - return ` [Comment on ${comment.path}:${comment.line || "?"}]: ${body}`; + let formatted = ` [Comment on ${comment.path}:${comment.line || "?"}]: ${body}`; + + // The diff hunk is the code the comment was left on. Without it the + // comment arrives without the context it was written against. + if (comment.diffHunk) { + const diffHunk = sanitizeContent(comment.diffHunk); + formatted += `\n Diff context:\n\`\`\`diff\n${diffHunk}\n\`\`\``; + } + + return formatted; }) .join("\n"); if (comments) { diff --git a/src/github/types.ts b/src/github/types.ts index d5053f42..c5140c12 100644 --- a/src/github/types.ts +++ b/src/github/types.ts @@ -22,6 +22,7 @@ export type GitHubComment = { export type GitHubReviewComment = GitHubComment & { path: string; line: number | null; + diffHunk?: string | null; }; export type GitHubCommit = { diff --git a/test/data-formatter.test.ts b/test/data-formatter.test.ts index 423bbcda..8735f23e 100644 --- a/test/data-formatter.test.ts +++ b/test/data-formatter.test.ts @@ -508,6 +508,112 @@ describe("formatReviewComments", () => { ); }); + test("includes the diff hunk as context when present", () => { + const reviewData = { + nodes: [ + { + id: "review1", + databaseId: "300001", + author: { login: "reviewer1" }, + body: "", + state: "COMMENTED", + submittedAt: "2023-01-01T00:00:00Z", + comments: { + nodes: [ + { + id: "comment1", + databaseId: "200001", + body: "This can overflow", + author: { login: "reviewer1" }, + createdAt: "2023-01-01T00:00:00Z", + path: "src/index.ts", + line: 42, + diffHunk: "@@ -40,3 +40,3 @@\n-const a = 1;\n+const a = 2;", + }, + ], + }, + }, + ], + }; + + const result = formatReviewComments(reviewData); + + expect(result).toContain("[Comment on src/index.ts:42]: This can overflow"); + expect(result).toContain("Diff context:"); + expect(result).toContain("@@ -40,3 +40,3 @@"); + expect(result).toContain("+const a = 2;"); + }); + + test("omits the diff context when the comment has no diff hunk", () => { + const reviewData = { + nodes: [ + { + id: "review1", + databaseId: "300001", + author: { login: "reviewer1" }, + body: "", + state: "COMMENTED", + submittedAt: "2023-01-01T00:00:00Z", + comments: { + nodes: [ + { + id: "comment1", + databaseId: "200001", + body: "No hunk here", + author: { login: "reviewer1" }, + createdAt: "2023-01-01T00:00:00Z", + path: "src/index.ts", + line: 42, + }, + ], + }, + }, + ], + }; + + const result = formatReviewComments(reviewData); + + expect(result).toContain("[Comment on src/index.ts:42]: No hunk here"); + expect(result).not.toContain("Diff context:"); + }); + + // GitHub returns line: null and diffHunk: "" for outdated comments whose + // line no longer exists in the diff (observed on anthropics/claude-code-action#1025). + test("omits the diff context for an outdated comment with an empty diff hunk", () => { + const reviewData = { + nodes: [ + { + id: "review1", + databaseId: "300001", + author: { login: "reviewer1" }, + body: "", + state: "COMMENTED", + submittedAt: "2023-01-01T00:00:00Z", + comments: { + nodes: [ + { + id: "comment1", + databaseId: "200001", + body: "Outdated comment", + author: { login: "reviewer1" }, + createdAt: "2023-01-01T00:00:00Z", + path: "src/index.ts", + line: null, + diffHunk: "", + }, + ], + }, + }, + ], + }; + + const result = formatReviewComments(reviewData); + + expect(result).toContain("[Comment on src/index.ts:?]: Outdated comment"); + expect(result).not.toContain("Diff context:"); + expect(result).not.toContain("```diff"); + }); + test("formats review with only body (no comments) correctly", () => { const reviewData = { nodes: [