Skip to content

Commit

Permalink
Merge pull request #3 from presubmit/dev
Browse files Browse the repository at this point in the history
Add PR Summary Generation with File Diffs and Messages
  • Loading branch information
bstanga authored Nov 9, 2024
2 parents 5f128de + 8b42ad8 commit 83757c6
Show file tree
Hide file tree
Showing 5 changed files with 321 additions and 21 deletions.
194 changes: 194 additions & 0 deletions src/diff.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
type File = {
filename: string;
status:
| "added"
| "removed"
| "modified"
| "renamed"
| "copied"
| "changed"
| "unchanged";
previous_filename?: string;
patch?: string;
};

export type Hunk = {
startLine: number;
endLine: number;
diff: string;
};

export type FileDiff = File & {
hunks: Hunk[];
};

export function parseFileDiff(file: File): FileDiff {
if (!file.patch) {
return {
...file,
hunks: [],
};
}

const hunks: Hunk[] = [];

let currentHunk: Hunk | null = null;
for (const line of file.patch.split("\n")) {
const hunkHeader = line.match(/@@ -(\d+),?\d* \+(\d+),?\d* @@/);
if (hunkHeader) {
if (currentHunk) {
hunks.push(currentHunk);
}
currentHunk = {
startLine: parseInt(hunkHeader[2]),
endLine: parseInt(hunkHeader[2]),
diff: line + "\n",
};
} else if (currentHunk) {
currentHunk.diff += line + "\n";
if (line[0] !== "-") {
currentHunk.endLine++;
}
}
}
if (currentHunk) {
hunks.push(currentHunk);
}

return {
...file,
hunks,
};
}

function removeDeletedLines(hunk: Hunk): Hunk {
return {
...hunk,
diff: hunk.diff
.split("\n")
.filter((line) => !line.startsWith("-"))
.join("\n"),
};
}

function removeAddedLines(hunk: Hunk): Hunk {
return {
...hunk,
diff: hunk.diff
.split("\n")
.filter((line) => !line.startsWith("+"))
.join("\n"),
};
}

function prependLineNumbers(hunk: Hunk): Hunk {
const lines = hunk.diff.split("\n");
let currentLine = hunk.startLine;
const numberedLines = lines.map((line) => {
// Skip empty lines at the end of the diff
if (!line) return line;

// Handle different line prefixes
if (line.startsWith("@@")) {
return line; // Keep hunk headers as is
} else if (line.startsWith("-")) {
return line; // Don't number removed lines
} else if (line.startsWith("+")) {
return `${currentLine++} ${line}`;
} else {
return `${currentLine++} ${line}`;
}
});

return {
startLine: hunk.startLine,
endLine: hunk.endLine,
diff: numberedLines.join("\n"),
};
}

function formatDiffHunk(hunk: Hunk): string {
const oldHunk = removeAddedLines(hunk);
const newHunk = prependLineNumbers(removeDeletedLines(hunk));

// Extract the @@ header from the first line
const lines = hunk.diff.split("\n");
const headerLine = lines.find((line) => line.startsWith("@@"));

// Check if there's content in each hunk after removing lines (excluding @@ header)
const hasOldContent = oldHunk.diff
.trim()
.split("\n")
.some((line) => line && !line.startsWith("@@"));
const hasNewContent = newHunk.diff
.trim()
.split("\n")
.some((line) => line && !line.startsWith("@@"));

let output = "";

// Add header first if we have any content
if ((hasOldContent || hasNewContent) && headerLine) {
output += `${headerLine}\n`;
}

if (hasNewContent) {
// Remove @@ header from new hunk content
const newContent = newHunk.diff
.split("\n")
.filter((line) => !line.startsWith("@@"))
.join("\n")
.trimEnd();
output += `__new hunk__\n${newContent}\n`;
}

if (hasOldContent) {
if (hasNewContent) output += "\n";
// Remove @@ header from old hunk content
const oldContent = oldHunk.diff
.split("\n")
.filter((line) => !line.startsWith("@@"))
.join("\n")
.trimEnd();
output += `__old hunk__\n${oldContent}\n`;
}

return output || "No changes in this hunk";
}

export function formatFileDiff(file: File): string {
const diff = file.patch || "";

let header = `## File ${file.status}: `;
if (file.previous_filename) {
header += `'${file.previous_filename}' → `;
}
header += `'${file.filename}'`;

if (diff.length) {
header += `\n\n${diff}`;
}

return header;
}

export function generateFileCodeDiff(
fileDiff: FileDiff,
simplified: boolean = false
): string {
const hunksText = simplified
? fileDiff.patch || ""
: fileDiff.hunks.map((hunk) => formatDiffHunk(hunk)).join("\n\n");

let header = `## File ${fileDiff.status}: `;
if (fileDiff.previous_filename) {
header += `'${fileDiff.previous_filename}' → `;
}
header += `'${fileDiff.filename}'`;

if (hunksText.length) {
header += `\n\n${hunksText}`;
}

return header;
}
60 changes: 60 additions & 0 deletions src/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { FileDiff } from "./diff";
import { PullRequestSummary } from "./prompts";

export function buildInitialMessage(
baseCommit: string,
headCommit: string,
fileDiffs: FileDiff[]
): string {
let message = `⏳ **Analyzing changes in this PR...** ⏳\n\n`;
message += `_This might take a few minutes, please wait_\n\n`;

// Group files by operation

message += `<details>\n<summary>📥 Commits</summary>\n\n`;
message += `Analyzing changes from base commit (\`${baseCommit}\`) to head commit (\`${headCommit}\`)\n\n`;
message += "\n</details>\n\n";

message += `<details>\n<summary>📁 Files being considered (${fileDiffs.length})</summary>\n\n`;
for (const diff of fileDiffs) {
let prefix = "🔄"; // Modified
if (diff.status === "added") prefix = "➕";
if (diff.status === "removed") prefix = "➖";
if (diff.status === "renamed") prefix = "📝";

let fileText = `${prefix} ${diff.filename}`;
if (diff.status === "renamed") {
fileText += ` (from ${diff.previous_filename})`;
}
fileText += ` _(${diff.hunks.length} hunks)_`;
message += `${fileText}\n`;
}
message += "\n</details>\n\n";

message += "--- \n_autogenerated by presubmit.ai_";

return message;
}

export function buildWalkthroughMessage(summary: PullRequestSummary): string {
let message = `# 📖 Walkthrough\n\n`;

// Add description with proper spacing
message += `${summary.description.trim()}\n\n`;

message += `## Changes\n\n`;

// Create table with proper column alignment and escaping
message += `| File | Summary |\n`;
message += `|:----------|:---------------|\n`; // Left-align columns

for (const file of summary.files) {
// Escape pipes and wrap paths in backticks for better formatting
const escapedPath = file.filename.replace(/\|/g, "\\|");
const escapedSummary = file.summary.replace(/\|/g, "\\|");

message += `| \`${escapedPath}\` | ${escapedSummary} |\n`;
}

return message;
}
2 changes: 1 addition & 1 deletion src/octokit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const initOctokit = (token?: string) => {
throw new Error("No github token");
}
return new SmartOctokit({
auth: `token ${token}`,
auth: token,
throttle: {
onRateLimit: (
retryAfter: any,
Expand Down
20 changes: 16 additions & 4 deletions src/prompts.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import { runPrompt } from "./ai";
import { z } from "zod";
import { formatFileDiff } from "./diff";

type PullRequestSummaryPrompt = {
prTitle: string;
prDescription: string;
commitMessages: string[];
files: FileDiff[];
diff: string;
files: {
filename: string;
status:
| "added"
| "removed"
| "modified"
| "renamed"
| "copied"
| "changed"
| "unchanged";
previous_filename?: string;
patch?: string;
}[];
};

export type PullRequestSummary = {
Expand Down Expand Up @@ -46,11 +58,11 @@ ${pr.commitMessages.join("\n")}
</Commit Messages>
<Affected Files>
${pr.files.map((file) => `- ${file.operation}: ${file.fileName}`).join("\n")}
${pr.files.map((file) => `- ${file.status}: ${file.filename}`).join("\n")}
</Affected Files>
<File Diffs>
${pr.diff}
${pr.files.map((file) => formatFileDiff(file)).join("\n\n")}
</File Diffs>
Make sure each affected file is summarized and it's part of the returned JSON.
Expand Down
66 changes: 50 additions & 16 deletions src/pull_request.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { warning } from "@actions/core";
import { info, warning } from "@actions/core";
import { Config } from "./config";
import { initOctokit } from "./octokit";
import { loadContext } from "./context";
import runSummaryPrompt from "./prompts";
import { buildInitialMessage, buildWalkthroughMessage } from "./messages";
import { parseFileDiff } from "./diff";

export async function handlePullRequest(config: Config) {
const context = await loadContext();
Expand All @@ -21,24 +24,55 @@ export async function handlePullRequest(config: Config) {

const octokit = initOctokit(config.githubToken);

// Get PR files
// Get modified files
const { data: files } = await octokit.rest.pulls.listFiles({
...context.repo,
pull_number: pull_request.number,
});
const fileDiffs = files.map(parseFileDiff);
info(`successfully fetched file diffs`);

console.log("files: ", files);
console.log("pull_request: ", pull_request);

return {
title: pull_request.title,
description: pull_request.body || "",
files: files.map((file) => ({
filename: file.filename,
status: file.status,
additions: file.additions,
deletions: file.deletions,
patch: file.patch,
})),
};
// Create initial comment with the summary
const initialComment = await octokit.rest.issues.createComment({
...context.repo,
issue_number: pull_request.number,
body: buildInitialMessage(
pull_request.base.sha,
pull_request.head.sha,
fileDiffs
),
});
info(`posted initial comment`);
// Get commit messages
const { data: commits } = await octokit.rest.pulls.listCommits({
...context.repo,
pull_number: pull_request.number,
});
info(`successfully fetched commit messages`);

// Generate PR summary
const summary = await runSummaryPrompt({
prTitle: pull_request.title,
prDescription: pull_request.body || "",
commitMessages: commits.map((commit) => commit.commit.message),
files: files,
});
info(`generated pull request summary: ${summary.title}`);

// Update PR title and description
await octokit.rest.pulls.update({
...context.repo,
pull_number: pull_request.number,
title: summary.title,
body: summary.description,
});
info(`updated pull request title and description`);

// Update initial comment with the walkthrough
await octokit.rest.issues.updateComment({
...context.repo,
comment_id: initialComment.data.id,
body: buildWalkthroughMessage(summary),
});
info(`posted walkthrough`);
}

0 comments on commit 83757c6

Please sign in to comment.