diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..ad93c14a --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,5 @@ +{ + "image": "mcr.microsoft.com/devcontainers/universal:2", + "features": { + } +} diff --git a/README.md b/README.md index d9d64925..5e1c2759 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,26 @@ -# OpenAI ChatGPT-based PR reviewer and summarizer +# AI-based PR reviewer and summarizer -![AI](./docs/images/ai.png) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +
- \`\`\`
- ---
- -:
-
- \`\`\`
-
- \`\`\`
- ---
- ...
+## IMPORTANT Instructions
+
+Input: New hunks annotated with line numbers and old hunks (replaced code). Hunks represent incomplete code fragments.
+Additional Context: PR title, description, summaries and comment chains.
+Task: Review new hunks for substantive issues using provided context and respond with comments if necessary.
+Output: Review comments in markdown with exact line number ranges in new hunks. Start and end line numbers must be within the same hunk. For single-line comments, start=end line number. Must use example response format below.
+Use fenced code blocks using the relevant language identifier where applicable.
+Don't annotate code snippets with line numbers. Format and indent code correctly.
+Do not use \`suggestion\` code blocks.
+For fixes, use \`diff\` code blocks, marking changes with \`+\` or \`-\`. The line number range for comments with fix snippets must exactly match the range to replace in the new hunk.
+
+- Do NOT provide general feedback, summaries, explanations of changes, or praises
+ for making good additions.
+- Focus solely on offering specific, objective insights based on the
+ given context and refrain from making broad comments about potential impacts on
+ the system or question intentions behind the changes.
+
+If there are no issues found on a line range, you MUST respond with the
+text \`LGTM!\` for that line range in the review section.
## Example
### Example changes
- ---new_hunk---
- 1: def add(x, y):
- 2: z = x+y
- 3: retrn z
- 4:
- 5: def multiply(x, y):
- 6: return x * y
+---new_hunk---
+\`\`\`
+ z = x / y
+ return z
+
+20: def add(x, y):
+21: z = x + y
+22: retrn z
+23:
+24: def multiply(x, y):
+25: return x * y
+
+def subtract(x, y):
+ z = x - y
+\`\`\`
- ---old_hunk---
- def add(x, y):
- return x + y
+---old_hunk---
+\`\`\`
+ z = x / y
+ return z
+
+def add(x, y):
+ return x + y
+
+def subtract(x, y):
+ z = x - y
+\`\`\`
+
+---comment_chains---
+\`\`\`
+Please review this change.
+\`\`\`
+
+---end_change_section---
### Example response
- 1-3:
- There's a typo in the return statement.
- \`\`\`suggestion
- def add(x, y):
- z = x + y
- return z
- \`\`\`
- ---
- 5-6:
- LGTM!
- ---
+22-22:
+There's a syntax error in the add function.
+\`\`\`diff
+- retrn z
++ return z
+\`\`\`
+---
+24-25:
+LGTM!
+---
## Changes made to \`$filename\` for your review
@@ -6890,7 +6980,7 @@ $comment
// eslint-disable-next-line camelcase
const context = _actions_github__WEBPACK_IMPORTED_MODULE_1__.context;
const repo = context.repo;
-const ASK_BOT = '@openai';
+const ASK_BOT = '@coderabbitai';
const handleReviewComment = async (heavyBot, options, prompts) => {
const commenter = new _commenter__WEBPACK_IMPORTED_MODULE_2__/* .Commenter */ .Es();
const inputs = new _inputs__WEBPACK_IMPORTED_MODULE_5__/* .Inputs */ .k();
@@ -7186,10 +7276,11 @@ var tokenizer = __nccwpck_require__(652);
// eslint-disable-next-line camelcase
const context = github.context;
const repo = context.repo;
-const ignoreKeyword = '@openai: ignore';
+const ignoreKeyword = '@coderabbitai: ignore';
const codeReview = async (lightBot, heavyBot, options, prompts) => {
const commenter = new lib_commenter/* Commenter */.Es();
const openaiConcurrencyLimit = pLimit(options.openaiConcurrencyLimit);
+ const githubConcurrencyLimit = pLimit(options.githubConcurrencyLimit);
if (context.eventName !== 'pull_request' &&
context.eventName !== 'pull_request_target') {
(0,core.warning)(`Skipped: current event is ${context.eventName}, only support pull_request event`);
@@ -7214,10 +7305,12 @@ const codeReview = async (lightBot, heavyBot, options, prompts) => {
// get SUMMARIZE_TAG message
const existingSummarizeCmt = await commenter.findCommentWithTag(lib_commenter/* SUMMARIZE_TAG */.Rp, context.payload.pull_request.number);
let existingCommitIdsBlock = '';
+ let existingSummarizeCmtBody = '';
if (existingSummarizeCmt != null) {
- inputs.rawSummary = commenter.getRawSummary(existingSummarizeCmt.body);
- inputs.shortSummary = commenter.getShortSummary(existingSummarizeCmt.body);
- existingCommitIdsBlock = commenter.getReviewedCommitIdsBlock(existingSummarizeCmt.body);
+ existingSummarizeCmtBody = existingSummarizeCmt.body;
+ inputs.rawSummary = commenter.getRawSummary(existingSummarizeCmtBody);
+ inputs.shortSummary = commenter.getShortSummary(existingSummarizeCmtBody);
+ existingCommitIdsBlock = commenter.getReviewedCommitIdsBlock(existingSummarizeCmtBody);
}
const allCommitIds = await commenter.getAllCommitIds();
// find highest reviewed commit id
@@ -7259,11 +7352,6 @@ const codeReview = async (lightBot, heavyBot, options, prompts) => {
(0,core.warning)('Skipped: files is null');
return;
}
- const commits = incrementalDiff.data.commits;
- if (commits.length === 0) {
- (0,core.warning)('Skipped: ommits is null');
- return;
- }
// skip files if they are filtered out
const filterSelectedFiles = [];
const filterIgnoredFiles = [];
@@ -7276,8 +7364,17 @@ const codeReview = async (lightBot, heavyBot, options, prompts) => {
filterSelectedFiles.push(file);
}
}
+ if (filterSelectedFiles.length === 0) {
+ (0,core.warning)('Skipped: filterSelectedFiles is null');
+ return;
+ }
+ const commits = incrementalDiff.data.commits;
+ if (commits.length === 0) {
+ (0,core.warning)('Skipped: commits is null');
+ return;
+ }
// find hunks to review
- const filteredFiles = await Promise.all(filterSelectedFiles.map(async (file) => {
+ const filteredFiles = await Promise.all(filterSelectedFiles.map(file => githubConcurrencyLimit(async () => {
// retrieve file contents
let fileContent = '';
if (context.payload.pull_request == null) {
@@ -7340,13 +7437,43 @@ ${hunks.oldHunk}
else {
return null;
}
- }));
+ })));
// Filter out any null results
const filesAndChanges = filteredFiles.filter(file => file !== null);
if (filesAndChanges.length === 0) {
(0,core.error)('Skipped: no files to review');
return;
}
+ let statusMsg = `
+Commits
+Files that changed from the base of the PR and between ${highestReviewedCommitId} and ${context.payload.pull_request.head.sha} commits.
+
+${filesAndChanges.length > 0
+ ? `
+
+Files selected (${filesAndChanges.length})
+
+* ${filesAndChanges
+ .map(([filename, , , patches]) => `${filename} (${patches.length})`)
+ .join('\n* ')}
+
+`
+ : ''}
+${filterIgnoredFiles.length > 0
+ ? `
+
+Files ignored due to filter (${filterIgnoredFiles.length})
+
+* ${filterIgnoredFiles.map(file => file.filename).join('\n* ')}
+
+
+`
+ : ''}
+`;
+ // update the existing comment with in progress status
+ const inProgressSummarizeCmt = commenter.addInProgressStatus(existingSummarizeCmtBody, statusMsg);
+ // add in progress status to the summarize comment
+ await commenter.comment(`${inProgressSummarizeCmt}`, lib_commenter/* SUMMARIZE_TAG */.Rp, 'replace');
const summariesFailed = [];
const doSummary = async (filename, fileContent, fileDiff) => {
(0,core.info)(`summarize: ${filename}`);
@@ -7357,19 +7484,18 @@ ${hunks.oldHunk}
return null;
}
ins.filename = filename;
+ ins.fileDiff = fileDiff;
// render prompt based on inputs so far
- let tokens = (0,tokenizer/* getTokenCount */.V)(prompts.renderSummarizeFileDiff(ins, options.reviewSimpleChanges));
- const diffTokens = (0,tokenizer/* getTokenCount */.V)(fileDiff);
- if (tokens + diffTokens > options.lightTokenLimits.requestTokens) {
+ const summarizePrompt = prompts.renderSummarizeFileDiff(ins, options.reviewSimpleChanges);
+ const tokens = (0,tokenizer/* getTokenCount */.V)(summarizePrompt);
+ if (tokens > options.lightTokenLimits.requestTokens) {
(0,core.info)(`summarize: diff tokens exceeds limit, skip ${filename}`);
summariesFailed.push(`${filename} (diff tokens exceeds limit)`);
return null;
}
- ins.fileDiff = fileDiff;
- tokens += fileDiff.length;
// summarize content
try {
- const [summarizeResp] = await lightBot.chat(prompts.renderSummarizeFileDiff(ins, options.reviewSimpleChanges), {});
+ const [summarizeResp] = await lightBot.chat(summarizePrompt, {});
if (summarizeResp === '') {
(0,core.info)('summarize: nothing obtained from openai');
summariesFailed.push(`${filename} (nothing obtained from openai)`);
@@ -7444,7 +7570,7 @@ ${filename}: ${summary}
(0,core.info)('release notes: nothing obtained from openai');
}
else {
- let message = '### Summary by OpenAI\n\n';
+ let message = '### Summary by CodeRabbit\n\n';
message += releaseNotesResponse;
try {
await commenter.updateDescription(context.payload.pull_request.number, message);
@@ -7464,54 +7590,34 @@ ${lib_commenter/* RAW_SUMMARY_END_TAG */.rV}
${lib_commenter/* SHORT_SUMMARY_START_TAG */.O$}
${inputs.shortSummary}
${lib_commenter/* SHORT_SUMMARY_END_TAG */.Zb}
----
-
-### Chat with 🤖 OpenAI Bot (\`@openai\`)
-- Reply on review comments left by this bot to ask follow-up questions. A review comment is a comment on a diff or a file.
-- Invite the bot into a review comment chain by tagging \`@openai\` in a reply.
-
-### Code suggestions
-- The bot may make code suggestions, but please review them carefully before committing since the line number ranges may be misaligned.
-- You can edit the comment made by the bot and manually tweak the suggestion if it is slightly off.
-
-### Ignoring further reviews
-- Type \`@openai: ignore\` anywhere in the PR description to ignore further reviews from the bot.
---
-${filterIgnoredFiles.length > 0
- ? `
-Files ignored due to filter (${filterIgnoredFiles.length})
+Uplevel your code reviews with CodeRabbit Pro
-### Ignored files
+### CodeRabbit Pro
-* ${filterIgnoredFiles.map(file => file.filename).join('\n* ')}
+If you like this project, please support us by purchasing the [Pro version](https://coderabbit.ai). The Pro version has advanced context, superior noise reduction and several proprietary improvements compared to the open source version. Moreover, CodeRabbit Pro is free for open source projects.
-`
- : ''}
-
+`;
+ statusMsg += `
${skippedFiles.length > 0
? `
Files not processed due to max files limit (${skippedFiles.length})
-### Not processed
-
* ${skippedFiles.join('\n* ')}
`
: ''}
-
${summariesFailed.length > 0
? `
Files not summarized due to errors (${summariesFailed.length})
-### Failed to summarize
-
* ${summariesFailed.join('\n* ')}
@@ -7528,6 +7634,8 @@ ${summariesFailed.length > 0
.map(([filename]) => filename);
// failed reviews array
const reviewsFailed = [];
+ let lgtmCount = 0;
+ let reviewCount = 0;
const doReview = async (filename, fileContent, patches) => {
(0,core.info)(`reviewing ${filename}`);
// make a copy of inputs
@@ -7596,38 +7704,45 @@ ${commentChain}
---end_change_section---
`;
}
- // perform review
- try {
- const [response] = await heavyBot.chat(prompts.renderReviewFileDiff(ins), {});
- if (response === '') {
- (0,core.info)('review: nothing obtained from openai');
- reviewsFailed.push(`${filename} (no response)`);
- return;
- }
- // parse review
- const reviews = parseReview(response, patches, options.debug);
- for (const review of reviews) {
- // check for LGTM
- if (!options.reviewCommentLGTM &&
- (review.comment.includes('LGTM') ||
- review.comment.includes('looks good to me'))) {
- continue;
- }
- if (context.payload.pull_request == null) {
- (0,core.warning)('No pull request found, skipping.');
- continue;
- }
- try {
- await commenter.bufferReviewComment(filename, review.startLine, review.endLine, `${review.comment}`);
+ if (patchesPacked > 0) {
+ // perform review
+ try {
+ const [response] = await heavyBot.chat(prompts.renderReviewFileDiff(ins), {});
+ if (response === '') {
+ (0,core.info)('review: nothing obtained from openai');
+ reviewsFailed.push(`${filename} (no response)`);
+ return;
}
- catch (e) {
- reviewsFailed.push(`${filename} comment failed (${e})`);
+ // parse review
+ const reviews = parseReview(response, patches, options.debug);
+ for (const review of reviews) {
+ // check for LGTM
+ if (!options.reviewCommentLGTM &&
+ (review.comment.includes('LGTM') ||
+ review.comment.includes('looks good to me'))) {
+ lgtmCount += 1;
+ continue;
+ }
+ if (context.payload.pull_request == null) {
+ (0,core.warning)('No pull request found, skipping.');
+ continue;
+ }
+ try {
+ reviewCount += 1;
+ await commenter.bufferReviewComment(filename, review.startLine, review.endLine, `${review.comment}`);
+ }
+ catch (e) {
+ reviewsFailed.push(`${filename} comment failed (${e})`);
+ }
}
}
+ catch (e) {
+ (0,core.warning)(`Failed to review: ${e}, skipping. backtrace: ${e.stack}`);
+ reviewsFailed.push(`${filename} (${e})`);
+ }
}
- catch (e) {
- (0,core.warning)(`Failed to review: ${e}, skipping. backtrace: ${e.stack}`);
- reviewsFailed.push(`${filename} (${e})`);
+ else {
+ reviewsSkipped.push(`${filename} (diff too large)`);
}
};
const reviewPromises = [];
@@ -7642,41 +7757,58 @@ ${commentChain}
}
}
await Promise.all(reviewPromises);
- summarizeComment += `
----
-In the recent run, only the files that changed from the \`base\` of the PR and between \`${highestReviewedCommitId}\` and \`${context.payload.pull_request.head.sha}\` commits were reviewed.
-
+ statusMsg += `
${reviewsFailed.length > 0
? `
-Files not reviewed due to errors in the recent run (${reviewsFailed.length})
-
-### Failed to review in the last run
+Files not reviewed due to errors (${reviewsFailed.length})
* ${reviewsFailed.join('\n* ')}
`
: ''}
-
${reviewsSkipped.length > 0
? `
-Files not reviewed due to simple changes (${reviewsSkipped.length})
-
-### Skipped review in the recent run
+Files skipped from review due to trivial changes (${reviewsSkipped.length})
* ${reviewsSkipped.join('\n* ')}
`
: ''}
+
+Review comments generated (${reviewCount + lgtmCount})
+
+* Review: ${reviewCount}
+* LGTM: ${lgtmCount}
+
+
+
+---
+
+
+Tips
+
+### Chat with CodeRabbit Bot (\`@coderabbitai\`)
+- Reply on review comments left by this bot to ask follow-up questions. A review comment is a comment on a diff or a file.
+- Invite the bot into a review comment chain by tagging \`@coderabbitai\` in a reply.
+
+### Code suggestions
+- The bot may make code suggestions, but please review them carefully before committing since the line number ranges may be misaligned.
+- You can edit the comment made by the bot and manually tweak the suggestion if it is slightly off.
+
+### Pausing incremental reviews
+- Add \`@coderabbitai: ignore\` anywhere in the PR description to pause further reviews from the bot.
+
+
`;
// add existing_comment_ids_block with latest head sha
summarizeComment += `\n${commenter.addReviewedCommitId(existingCommitIdsBlock, context.payload.pull_request.head.sha)}`;
+ // post the review
+ await commenter.submitReview(context.payload.pull_request.number, commits[commits.length - 1].sha, statusMsg);
}
// post the final summary comment
await commenter.comment(`${summarizeComment}`, lib_commenter/* SUMMARIZE_TAG */.Rp, 'replace');
- // post the review
- await commenter.submitReview(context.payload.pull_request.number, commits[commits.length - 1].sha);
};
const splitPatch = (patch) => {
if (patch == null) {
@@ -7730,26 +7862,36 @@ const parsePatch = (patch) => {
}
const oldHunkLines = [];
const newHunkLines = [];
- // let old_line = hunkInfo.old_hunk.start_line
let newLine = hunkInfo.newHunk.startLine;
const lines = patch.split('\n').slice(1); // Skip the @@ line
// Remove the last line if it's empty
if (lines[lines.length - 1] === '') {
lines.pop();
}
+ // Skip annotations for the first 3 and last 3 lines
+ const skipStart = 3;
+ const skipEnd = 3;
+ let currentLine = 0;
+ const removalOnly = !lines.some(line => line.startsWith('+'));
for (const line of lines) {
+ currentLine++;
if (line.startsWith('-')) {
oldHunkLines.push(`${line.substring(1)}`);
- // old_line++
}
else if (line.startsWith('+')) {
newHunkLines.push(`${newLine}: ${line.substring(1)}`);
newLine++;
}
else {
+ // context line
oldHunkLines.push(`${line}`);
- newHunkLines.push(`${newLine}: ${line}`);
- // old_line++
+ if (removalOnly ||
+ (currentLine > skipStart && currentLine <= lines.length - skipEnd)) {
+ newHunkLines.push(`${newLine}: ${line}`);
+ }
+ else {
+ newHunkLines.push(`${line}`);
+ }
newLine++;
}
}
@@ -7760,6 +7902,7 @@ const parsePatch = (patch) => {
};
function parseReview(response, patches, debug = false) {
const reviews = [];
+ response = sanitizeResponse(response.trim());
const lines = response.split('\n');
const lineNumberRangeRegex = /(?:^|\s)(\d+)-(\d+):\s*$/;
const commentSeparator = '---';
@@ -7768,11 +7911,10 @@ function parseReview(response, patches, debug = false) {
let currentComment = '';
function storeReview() {
if (currentStartLine !== null && currentEndLine !== null) {
- const sanitizedComment = sanitizeComment(currentComment.trim());
const review = {
startLine: currentStartLine,
endLine: currentEndLine,
- comment: sanitizedComment.trim()
+ comment: currentComment
};
let withinPatch = false;
let bestPatchStartLine = -1;
@@ -7812,28 +7954,33 @@ ${review.comment}`;
(0,core.info)(`Stored comment for line range ${currentStartLine}-${currentEndLine}: ${currentComment.trim()}`);
}
}
- function sanitizeComment(comment) {
- const suggestionStart = '```suggestion';
- const suggestionEnd = '```';
+ function sanitizeCodeBlock(comment, codeBlockLabel) {
+ const codeBlockStart = `\`\`\`${codeBlockLabel}`;
+ const codeBlockEnd = '```';
const lineNumberRegex = /^ *(\d+): /gm;
- let suggestionStartIndex = comment.indexOf(suggestionStart);
- while (suggestionStartIndex !== -1) {
- const suggestionEndIndex = comment.indexOf(suggestionEnd, suggestionStartIndex + suggestionStart.length);
- if (suggestionEndIndex === -1)
+ let codeBlockStartIndex = comment.indexOf(codeBlockStart);
+ while (codeBlockStartIndex !== -1) {
+ const codeBlockEndIndex = comment.indexOf(codeBlockEnd, codeBlockStartIndex + codeBlockStart.length);
+ if (codeBlockEndIndex === -1)
break;
- const suggestionBlock = comment.substring(suggestionStartIndex + suggestionStart.length, suggestionEndIndex);
- const sanitizedBlock = suggestionBlock.replace(lineNumberRegex, '');
+ const codeBlock = comment.substring(codeBlockStartIndex + codeBlockStart.length, codeBlockEndIndex);
+ const sanitizedBlock = codeBlock.replace(lineNumberRegex, '');
comment =
- comment.slice(0, suggestionStartIndex + suggestionStart.length) +
+ comment.slice(0, codeBlockStartIndex + codeBlockStart.length) +
sanitizedBlock +
- comment.slice(suggestionEndIndex);
- suggestionStartIndex = comment.indexOf(suggestionStart, suggestionStartIndex +
- suggestionStart.length +
+ comment.slice(codeBlockEndIndex);
+ codeBlockStartIndex = comment.indexOf(codeBlockStart, codeBlockStartIndex +
+ codeBlockStart.length +
sanitizedBlock.length +
- suggestionEnd.length);
+ codeBlockEnd.length);
}
return comment;
}
+ function sanitizeResponse(comment) {
+ comment = sanitizeCodeBlock(comment, 'suggestion');
+ comment = sanitizeCodeBlock(comment, 'diff');
+ return comment;
+ }
for (const line of lines) {
const lineNumberRangeMatch = line.match(lineNumberRangeRegex);
if (lineNumberRangeMatch != null) {
diff --git a/docs/images/PRSummary.png b/docs/images/PRSummary.png
new file mode 100644
index 00000000..bf90664b
Binary files /dev/null and b/docs/images/PRSummary.png differ
diff --git a/docs/images/ReleaseNotes.png b/docs/images/ReleaseNotes.png
new file mode 100644
index 00000000..64da5dce
Binary files /dev/null and b/docs/images/ReleaseNotes.png differ
diff --git a/docs/images/section-1.png b/docs/images/section-1.png
new file mode 100644
index 00000000..b8639bd8
Binary files /dev/null and b/docs/images/section-1.png differ
diff --git a/docs/images/section-3.png b/docs/images/section-3.png
new file mode 100644
index 00000000..04715f00
Binary files /dev/null and b/docs/images/section-3.png differ
diff --git a/src/bot.ts b/src/bot.ts
index 054138a0..4a5a1e4b 100644
--- a/src/bot.ts
+++ b/src/bot.ts
@@ -28,7 +28,10 @@ export class Bot {
const currentDate = new Date().toISOString().split('T')[0]
const systemMessage = `${options.systemMessage}
Knowledge cutoff: ${openaiOptions.tokenLimits.knowledgeCutOff}
-Current date: ${currentDate}`
+Current date: ${currentDate}
+
+IMPORTANT: Entire response must be in the language with ISO code: ${options.language}
+`
this.api = new ChatGPTAPI({
apiBaseUrl: options.apiBaseUrl,
diff --git a/src/commenter.ts b/src/commenter.ts
index 5c3e132b..463e92cf 100644
--- a/src/commenter.ts
+++ b/src/commenter.ts
@@ -7,34 +7,40 @@ import {octokit} from './octokit'
const context = github_context
const repo = context.repo
-export const COMMENT_GREETING = ':robot: OpenAI'
+export const COMMENT_GREETING = ` CodeRabbit`
export const COMMENT_TAG =
- ''
+ ''
export const COMMENT_REPLY_TAG =
- ''
+ ''
export const SUMMARIZE_TAG =
- ''
+ ''
-export const DESCRIPTION_START_TAG = `
-`
+export const IN_PROGRESS_START_TAG =
+ ''
+
+export const IN_PROGRESS_END_TAG =
+ ''
+
+export const DESCRIPTION_START_TAG =
+ ''
export const DESCRIPTION_END_TAG =
- ''
+ ''
-export const RAW_SUMMARY_START_TAG = `
+export const RAW_SUMMARY_START_TAG = `
-`
+`
-export const SHORT_SUMMARY_START_TAG = `
+export const SHORT_SUMMARY_START_TAG = `
-`
+`
export const COMMIT_ID_START_TAG = ''
export const COMMIT_ID_END_TAG = ''
@@ -87,7 +93,7 @@ ${tag}`
removeContentWithinTags(content: string, startTag: string, endTag: string) {
const start = content.indexOf(startTag)
- const end = content.indexOf(endTag)
+ const end = content.lastIndexOf(endTag)
if (start >= 0 && end >= 0) {
return content.slice(0, start) + content.slice(end + endTag.length)
}
@@ -149,7 +155,7 @@ ${tag}`
DESCRIPTION_START_TAG,
DESCRIPTION_END_TAG
)
- const newDescription = `${description}${DESCRIPTION_START_TAG}\n${messageClean}\n${DESCRIPTION_END_TAG}`
+ const newDescription = `${description}\n${DESCRIPTION_START_TAG}\n${messageClean}\n${DESCRIPTION_END_TAG}`
await octokit.pulls.update({
owner: repo.owner,
repo: repo.repo,
@@ -190,14 +196,67 @@ ${COMMENT_TAG}`
})
}
- async submitReview(pullNumber: number, commitId: string) {
- info(
- `Submitting review for PR #${pullNumber}, total comments: ${this.reviewCommentsBuffer.length}`
- )
- let commentCounter = 0
+ async deletePendingReview(pullNumber: number) {
+ try {
+ const reviews = await octokit.pulls.listReviews({
+ owner: repo.owner,
+ repo: repo.repo,
+ // eslint-disable-next-line camelcase
+ pull_number: pullNumber
+ })
+
+ const pendingReview = reviews.data.find(
+ review => review.state === 'PENDING'
+ )
+
+ if (pendingReview) {
+ info(
+ `Deleting pending review for PR #${pullNumber} id: ${pendingReview.id}`
+ )
+ try {
+ await octokit.pulls.deletePendingReview({
+ owner: repo.owner,
+ repo: repo.repo,
+ // eslint-disable-next-line camelcase
+ pull_number: pullNumber,
+ // eslint-disable-next-line camelcase
+ review_id: pendingReview.id
+ })
+ } catch (e) {
+ warning(`Failed to delete pending review: ${e}`)
+ }
+ }
+ } catch (e) {
+ warning(`Failed to list reviews: ${e}`)
+ }
+ }
+
+ async submitReview(pullNumber: number, commitId: string, statusMsg: string) {
+ const body = `${COMMENT_GREETING}
+
+${statusMsg}
+`
+
+ if (this.reviewCommentsBuffer.length === 0) {
+ // Submit empty review with statusMsg
+ info(`Submitting empty review for PR #${pullNumber}`)
+ try {
+ await octokit.pulls.createReview({
+ owner: repo.owner,
+ repo: repo.repo,
+ // eslint-disable-next-line camelcase
+ pull_number: pullNumber,
+ // eslint-disable-next-line camelcase
+ commit_id: commitId,
+ event: 'COMMENT',
+ body
+ })
+ } catch (e) {
+ warning(`Failed to submit empty review: ${e}`)
+ }
+ return
+ }
for (const comment of this.reviewCommentsBuffer) {
- info(`Posting comment: ${comment.message}`)
- let found = false
const comments = await this.getCommentsAtRange(
pullNumber,
comment.path,
@@ -207,25 +266,75 @@ ${COMMENT_TAG}`
for (const c of comments) {
if (c.body.includes(COMMENT_TAG)) {
info(
- `Updating review comment for ${comment.path}:${comment.startLine}-${comment.endLine}: ${comment.message}`
+ `Deleting review comment for ${comment.path}:${comment.startLine}-${comment.endLine}: ${comment.message}`
)
try {
- await octokit.pulls.updateReviewComment({
+ await octokit.pulls.deleteReviewComment({
owner: repo.owner,
repo: repo.repo,
// eslint-disable-next-line camelcase
- comment_id: c.id,
- body: comment.message
+ comment_id: c.id
})
} catch (e) {
- warning(`Failed to update review comment: ${e}`)
+ warning(`Failed to delete review comment: ${e}`)
}
- found = true
- break
}
}
+ }
+
+ await this.deletePendingReview(pullNumber)
+
+ const generateCommentData = (comment: any) => {
+ const commentData: any = {
+ path: comment.path,
+ body: comment.message,
+ line: comment.endLine
+ }
+
+ if (comment.startLine !== comment.endLine) {
+ // eslint-disable-next-line camelcase
+ commentData.start_line = comment.startLine
+ // eslint-disable-next-line camelcase
+ commentData.start_side = 'RIGHT'
+ }
- if (!found) {
+ return commentData
+ }
+
+ try {
+ const review = await octokit.pulls.createReview({
+ owner: repo.owner,
+ repo: repo.repo,
+ // eslint-disable-next-line camelcase
+ pull_number: pullNumber,
+ // eslint-disable-next-line camelcase
+ commit_id: commitId,
+ comments: this.reviewCommentsBuffer.map(comment =>
+ generateCommentData(comment)
+ )
+ })
+
+ info(
+ `Submitting review for PR #${pullNumber}, total comments: ${this.reviewCommentsBuffer.length}, review id: ${review.data.id}`
+ )
+
+ await octokit.pulls.submitReview({
+ owner: repo.owner,
+ repo: repo.repo,
+ // eslint-disable-next-line camelcase
+ pull_number: pullNumber,
+ // eslint-disable-next-line camelcase
+ review_id: review.data.id,
+ event: 'COMMENT',
+ body
+ })
+ } catch (e) {
+ warning(
+ `Failed to create review: ${e}. Falling back to individual comments.`
+ )
+ await this.deletePendingReview(pullNumber)
+ let commentCounter = 0
+ for (const comment of this.reviewCommentsBuffer) {
info(
`Creating new review comment for ${comment.path}:${comment.startLine}-${comment.endLine}: ${comment.message}`
)
@@ -236,28 +345,20 @@ ${COMMENT_TAG}`
pull_number: pullNumber,
// eslint-disable-next-line camelcase
commit_id: commitId,
- body: comment.message,
- path: comment.path,
- line: comment.endLine
+ ...generateCommentData(comment)
}
- if (comment.startLine !== comment.endLine) {
- // eslint-disable-next-line camelcase
- commentData.start_side = 'RIGHT'
- // eslint-disable-next-line camelcase
- commentData.start_line = comment.startLine
- }
try {
await octokit.pulls.createReviewComment(commentData)
- } catch (e) {
- warning(`Failed to create review comment: ${e}`)
+ } catch (ee) {
+ warning(`Failed to create review comment: ${ee}`)
}
- }
- commentCounter++
- info(
- `Comment ${commentCounter}/${this.reviewCommentsBuffer.length} posted`
- )
+ commentCounter++
+ info(
+ `Comment ${commentCounter}/${this.reviewCommentsBuffer.length} posted`
+ )
+ }
}
}
@@ -483,14 +584,20 @@ ${chain}
async create(body: string, target: number) {
try {
- // get commend ID from the response
- await octokit.issues.createComment({
+ // get comment ID from the response
+ const response = await octokit.issues.createComment({
owner: repo.owner,
repo: repo.repo,
// eslint-disable-next-line camelcase
issue_number: target,
body
})
+ // add comment to issueCommentsCache
+ if (this.issueCommentsCache[target]) {
+ this.issueCommentsCache[target].push(response.data)
+ } else {
+ this.issueCommentsCache[target] = [response.data]
+ }
} catch (e) {
warning(`Failed to create comment: ${e}`)
}
@@ -645,4 +752,41 @@ ${chain}
return allCommits
}
+
+ // add in-progress status to the comment body
+ addInProgressStatus(commentBody: string, statusMsg: string): string {
+ const start = commentBody.indexOf(IN_PROGRESS_START_TAG)
+ const end = commentBody.indexOf(IN_PROGRESS_END_TAG)
+ // add to the beginning of the comment body if the marker doesn't exist
+ // otherwise do nothing
+ if (start === -1 || end === -1) {
+ return `${IN_PROGRESS_START_TAG}
+
+Currently reviewing new changes in this PR...
+
+${statusMsg}
+
+${IN_PROGRESS_END_TAG}
+
+---
+
+${commentBody}`
+ }
+ return commentBody
+ }
+
+ // remove in-progress status from the comment body
+ removeInProgressStatus(commentBody: string): string {
+ const start = commentBody.indexOf(IN_PROGRESS_START_TAG)
+ const end = commentBody.indexOf(IN_PROGRESS_END_TAG)
+ // remove the in-progress status if the marker exists
+ // otherwise do nothing
+ if (start !== -1 && end !== -1) {
+ return (
+ commentBody.substring(0, start) +
+ commentBody.substring(end + IN_PROGRESS_END_TAG.length)
+ )
+ }
+ return commentBody
+ }
}
diff --git a/src/limits.ts b/src/limits.ts
index 7c2afc52..aca807f6 100644
--- a/src/limits.ts
+++ b/src/limits.ts
@@ -9,6 +9,9 @@ export class TokenLimits {
if (model === 'gpt-4-32k') {
this.maxTokens = 32600
this.responseTokens = 4000
+ } else if (model === 'gpt-3.5-turbo-16k') {
+ this.maxTokens = 16300
+ this.responseTokens = 3000
} else if (model === 'gpt-4') {
this.maxTokens = 8000
this.responseTokens = 2000
diff --git a/src/main.ts b/src/main.ts
index e0b69db0..0b716c48 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -27,7 +27,9 @@ async function run(): Promise {
getInput('openai_retries'),
getInput('openai_timeout_ms'),
getInput('openai_concurrency_limit'),
- getInput('openai_base_url')
+ getInput('github_concurrency_limit'),
+ getInput('openai_base_url'),
+ getInput('language')
)
// print options
diff --git a/src/octokit.ts b/src/octokit.ts
index d08e4f02..a743d3eb 100644
--- a/src/octokit.ts
+++ b/src/octokit.ts
@@ -22,17 +22,23 @@ Retry after: ${retryAfter} seconds
Retry count: ${retryCount}
`
)
- return true
+ if (retryCount <= 3) {
+ warning(`Retrying after ${retryAfter} seconds!`)
+ return true
+ }
},
- onSecondaryRateLimit: (_retryAfter: number, options: any) => {
+ onSecondaryRateLimit: (retryAfter: number, options: any) => {
warning(
- `SecondaryRateLimit detected for request ${options.method} ${options.url}`
+ `SecondaryRateLimit detected for request ${options.method} ${options.url} ; retry after ${retryAfter} seconds`
)
+ // if we are doing a POST method on /repos/{owner}/{repo}/pulls/{pull_number}/reviews then we shouldn't retry
+ if (
+ options.method === 'POST' &&
+ options.url.match(/\/repos\/.*\/.*\/pulls\/.*\/reviews/)
+ ) {
+ return false
+ }
return true
}
- },
- retry: {
- doNotRetry: ['429'],
- maxRetries: 10
}
})
diff --git a/src/options.ts b/src/options.ts
index cc0bbe96..02541975 100644
--- a/src/options.ts
+++ b/src/options.ts
@@ -17,9 +17,11 @@ export class Options {
openaiRetries: number
openaiTimeoutMS: number
openaiConcurrencyLimit: number
+ githubConcurrencyLimit: number
lightTokenLimits: TokenLimits
heavyTokenLimits: TokenLimits
apiBaseUrl: string
+ language: string
constructor(
debug: boolean,
@@ -35,8 +37,10 @@ export class Options {
openaiModelTemperature = '0.0',
openaiRetries = '3',
openaiTimeoutMS = '120000',
- openaiConcurrencyLimit = '4',
- apiBaseUrl = 'https://api.openai.com/v1'
+ openaiConcurrencyLimit = '6',
+ githubConcurrencyLimit = '6',
+ apiBaseUrl = 'https://api.openai.com/v1',
+ language = 'en-US'
) {
this.debug = debug
this.disableReview = disableReview
@@ -52,9 +56,11 @@ export class Options {
this.openaiRetries = parseInt(openaiRetries)
this.openaiTimeoutMS = parseInt(openaiTimeoutMS)
this.openaiConcurrencyLimit = parseInt(openaiConcurrencyLimit)
+ this.githubConcurrencyLimit = parseInt(githubConcurrencyLimit)
this.lightTokenLimits = new TokenLimits(openaiLightModel)
this.heavyTokenLimits = new TokenLimits(openaiHeavyModel)
this.apiBaseUrl = apiBaseUrl
+ this.language = language
}
// print all options using core.info
@@ -73,9 +79,11 @@ export class Options {
info(`openai_retries: ${this.openaiRetries}`)
info(`openai_timeout_ms: ${this.openaiTimeoutMS}`)
info(`openai_concurrency_limit: ${this.openaiConcurrencyLimit}`)
+ info(`github_concurrency_limit: ${this.githubConcurrencyLimit}`)
info(`summary_token_limits: ${this.lightTokenLimits.string()}`)
info(`review_token_limits: ${this.heavyTokenLimits.string()}`)
info(`api_base_url: ${this.apiBaseUrl}`)
+ info(`language: ${this.language}`)
}
checkPath(path: string): boolean {
diff --git a/src/prompts.ts b/src/prompts.ts
index 265d2aaa..32167d85 100644
--- a/src/prompts.ts
+++ b/src/prompts.ts
@@ -22,7 +22,11 @@ $file_diff
## Instructions
-I would like you to summarize the diff within 50 words.
+I would like you to succinctly summarize the diff within 100 words.
+If applicable, your summary should include a note about alterations
+to the signatures of exported functions, global data structures and
+variables, and any changes that might affect the external interface or
+behavior of the code.
`
triageFileDiff = `Below the summary, I would also like you to triage the diff as \`NEEDS_REVIEW\` or
\`APPROVED\` based on the following criteria:
@@ -66,9 +70,9 @@ $raw_summary
`
- summarizeShort = `Your task is to provide a concise summary of the changes
-and the goal of this PR. This summary will be used as a prompt while reviewing each
-file and must be very clear for the AI bot to understand.
+ summarizeShort = `Your task is to provide a concise summary of the changes. This
+summary will be used as a prompt while reviewing each file and must be very clear for
+the AI bot to understand.
Instructions:
@@ -76,7 +80,7 @@ Instructions:
- Do not provide any instructions to the bot on how to perform the review.
- Do not mention that files need a through review or caution about potential issues.
- Do not mention that these changes affect the logic or functionality of the code.
-- The summary should not exceed 250 words.
+- The summary should not exceed 500 words.
`
reviewFileDiff = `## GitHub PR Title
@@ -89,136 +93,83 @@ Instructions:
$description
\`\`\`
-## Summary generated by the AI bot
+## Summary of changes
\`\`\`
$short_summary
\`\`\`
-## How to parse the changes
-
-The format for changes provided below consists of multiple change
-sections, each containing a new hunk (annotated with line numbers),
-an old hunk, and optionally, existing comment chains. Note that the
-old hunk code has been replaced by the new hunk to fulfill the
-goal of this PR. The line number annotation on each line in the new
-hunk is of the format \`\`.
-
-### Format for changes
-
- ---new_hunk---
- \`\`\`
-
- \`\`\`
-
- ---old_hunk---
- \`\`\`
-
- \`\`\`
-
- ---comment_chains---
- \`\`\`
-
- \`\`\`
-
- ---end_change_section---
- ...
-
-## Response Instructions
-
-- Your task is to review ONLY the new hunks line by line, ONLY pointing out
- substantive issues within line number ranges. Provide the exact line
- number range (inclusive) for each issue. Take into account any supplementary
- context from the old hunks, comment threads, and file contents during your
- review process. Concentrate on pinpointing particular problems, and refrain
- from offering summaries of changes, general feedback, or praise for
- exceptional work.
-- Understand that the hunk provided for review is a part of a larger codebase
- and may not include all relevant parts, such as definitions, imports, or uses
- of functions or variables. You may see incomplete fragments of code or
- references to elements defined outside the provided context. Refrain from
- flagging issues about missing definitions, imports, or uses unless there is
- strong evidence within the provided context to suggest there might be a problem.
-- IMPORTANT: Respond only in the below response format (consisting of review
- sections). Each review section must have a line number range and a review
- comment for that range. Do not include general feedback or summaries. You
- may optionally include a single replacement suggestion snippet and/or
- multiple new code snippets in the review comment. Separate review sections
- using separators.
-- IMPORTANT: Line number ranges for each review section must be within the
- range of a specific new hunk. must belong to the same
- hunk as the . The line number range is sufficient to map
- your comment to the code changes in the GitHub pull request.
-- Use Markdown format for review comment text and fenced code blocks for
- code snippets. Do not annotate code snippets with line numbers.
-- If needed, provide replacement code suggestions to fix the issue by using
- fenced code blocks with the \`suggestion\` as the language identifier. The
- line number range must map exactly to the range (inclusive) that needs to
- be replaced within a new hunk. For instance, if 2 lines of code in a hunk
- need to be replaced with 15 lines of code, the line number range must be
- those exact 2 lines. If an entire hunk need to be replaced with new code,
- then the line number range must be the entire hunk and the new code must
- exactly replace all the lines in the hunk.
-- Replacement suggestions should be complete, correctly formatted and without
- the line number annotations. Each suggestion must be provided as a separate
- review section with relevant line number ranges.
-- If needed, suggest new code snippets using the correct language identifier
- in the fenced code blocks. These snippets may be added to a different file
- (e.g. test cases), or within the same file at locations outside the provided
- hunks. Multiple new code snippets are allowed within a single review section.
-- IMPORTANT: If there are no issues found on a line range, you must only
- respond with the text \`LGTM!\` for that line range in the review section.
-- Reflect on your comments and line number ranges before sending the final
- response to ensure accuracy of line number ranges and replacement snippets.
-
-### Response format expected
-
- -:
-
- ---
- -:
-
- \`\`\`suggestion
-
- \`\`\`
- ---
- -:
-
- \`\`\`
-
- \`\`\`
- ---
- ...
+## IMPORTANT Instructions
+
+Input: New hunks annotated with line numbers and old hunks (replaced code). Hunks represent incomplete code fragments.
+Additional Context: PR title, description, summaries and comment chains.
+Task: Review new hunks for substantive issues using provided context and respond with comments if necessary.
+Output: Review comments in markdown with exact line number ranges in new hunks. Start and end line numbers must be within the same hunk. For single-line comments, start=end line number. Must use example response format below.
+Use fenced code blocks using the relevant language identifier where applicable.
+Don't annotate code snippets with line numbers. Format and indent code correctly.
+Do not use \`suggestion\` code blocks.
+For fixes, use \`diff\` code blocks, marking changes with \`+\` or \`-\`. The line number range for comments with fix snippets must exactly match the range to replace in the new hunk.
+
+- Do NOT provide general feedback, summaries, explanations of changes, or praises
+ for making good additions.
+- Focus solely on offering specific, objective insights based on the
+ given context and refrain from making broad comments about potential impacts on
+ the system or question intentions behind the changes.
+
+If there are no issues found on a line range, you MUST respond with the
+text \`LGTM!\` for that line range in the review section.
## Example
### Example changes
- ---new_hunk---
- 1: def add(x, y):
- 2: z = x+y
- 3: retrn z
- 4:
- 5: def multiply(x, y):
- 6: return x * y
+---new_hunk---
+\`\`\`
+ z = x / y
+ return z
+
+20: def add(x, y):
+21: z = x + y
+22: retrn z
+23:
+24: def multiply(x, y):
+25: return x * y
+
+def subtract(x, y):
+ z = x - y
+\`\`\`
- ---old_hunk---
- def add(x, y):
- return x + y
+---old_hunk---
+\`\`\`
+ z = x / y
+ return z
+
+def add(x, y):
+ return x + y
+
+def subtract(x, y):
+ z = x - y
+\`\`\`
+
+---comment_chains---
+\`\`\`
+Please review this change.
+\`\`\`
+
+---end_change_section---
### Example response
- 1-3:
- There's a typo in the return statement.
- \`\`\`suggestion
- def add(x, y):
- z = x + y
- return z
- \`\`\`
- ---
- 5-6:
- LGTM!
- ---
+22-22:
+There's a syntax error in the add function.
+\`\`\`diff
+- retrn z
++ return z
+\`\`\`
+---
+24-25:
+LGTM!
+---
## Changes made to \`$filename\` for your review
diff --git a/src/review-comment.ts b/src/review-comment.ts
index 1a5b0b17..f42c53db 100644
--- a/src/review-comment.ts
+++ b/src/review-comment.ts
@@ -17,7 +17,7 @@ import {getTokenCount} from './tokenizer'
// eslint-disable-next-line camelcase
const context = github_context
const repo = context.repo
-const ASK_BOT = '@openai'
+const ASK_BOT = '@coderabbitai'
export const handleReviewComment = async (
heavyBot: Bot,
diff --git a/src/review.ts b/src/review.ts
index 58bcb1ee..5e7dd9db 100644
--- a/src/review.ts
+++ b/src/review.ts
@@ -22,7 +22,7 @@ import {getTokenCount} from './tokenizer'
const context = github_context
const repo = context.repo
-const ignoreKeyword = '@openai: ignore'
+const ignoreKeyword = '@coderabbitai: ignore'
export const codeReview = async (
lightBot: Bot,
@@ -33,6 +33,7 @@ export const codeReview = async (
const commenter: Commenter = new Commenter()
const openaiConcurrencyLimit = pLimit(options.openaiConcurrencyLimit)
+ const githubConcurrencyLimit = pLimit(options.githubConcurrencyLimit)
if (
context.eventName !== 'pull_request' &&
@@ -71,11 +72,13 @@ export const codeReview = async (
context.payload.pull_request.number
)
let existingCommitIdsBlock = ''
+ let existingSummarizeCmtBody = ''
if (existingSummarizeCmt != null) {
- inputs.rawSummary = commenter.getRawSummary(existingSummarizeCmt.body)
- inputs.shortSummary = commenter.getShortSummary(existingSummarizeCmt.body)
+ existingSummarizeCmtBody = existingSummarizeCmt.body
+ inputs.rawSummary = commenter.getRawSummary(existingSummarizeCmtBody)
+ inputs.shortSummary = commenter.getShortSummary(existingSummarizeCmtBody)
existingCommitIdsBlock = commenter.getReviewedCommitIdsBlock(
- existingSummarizeCmt.body
+ existingSummarizeCmtBody
)
}
@@ -139,13 +142,6 @@ export const codeReview = async (
return
}
- const commits = incrementalDiff.data.commits
-
- if (commits.length === 0) {
- warning('Skipped: ommits is null')
- return
- }
-
// skip files if they are filtered out
const filterSelectedFiles = []
const filterIgnoredFiles = []
@@ -158,61 +154,74 @@ export const codeReview = async (
}
}
+ if (filterSelectedFiles.length === 0) {
+ warning('Skipped: filterSelectedFiles is null')
+ return
+ }
+
+ const commits = incrementalDiff.data.commits
+
+ if (commits.length === 0) {
+ warning('Skipped: commits is null')
+ return
+ }
+
// find hunks to review
const filteredFiles: Array<
[string, string, string, Array<[number, number, string]>] | null
> = await Promise.all(
- filterSelectedFiles.map(async file => {
- // retrieve file contents
- let fileContent = ''
- if (context.payload.pull_request == null) {
- warning('Skipped: context.payload.pull_request is null')
- return null
- }
- try {
- const contents = await octokit.repos.getContent({
- owner: repo.owner,
- repo: repo.repo,
- path: file.filename,
- ref: context.payload.pull_request.base.sha
- })
- if (contents.data != null) {
- if (!Array.isArray(contents.data)) {
- if (
- contents.data.type === 'file' &&
- contents.data.content != null
- ) {
- fileContent = Buffer.from(
- contents.data.content,
- 'base64'
- ).toString()
+ filterSelectedFiles.map(file =>
+ githubConcurrencyLimit(async () => {
+ // retrieve file contents
+ let fileContent = ''
+ if (context.payload.pull_request == null) {
+ warning('Skipped: context.payload.pull_request is null')
+ return null
+ }
+ try {
+ const contents = await octokit.repos.getContent({
+ owner: repo.owner,
+ repo: repo.repo,
+ path: file.filename,
+ ref: context.payload.pull_request.base.sha
+ })
+ if (contents.data != null) {
+ if (!Array.isArray(contents.data)) {
+ if (
+ contents.data.type === 'file' &&
+ contents.data.content != null
+ ) {
+ fileContent = Buffer.from(
+ contents.data.content,
+ 'base64'
+ ).toString()
+ }
}
}
+ } catch (e: any) {
+ warning(
+ `Failed to get file contents: ${
+ e as string
+ }. This is OK if it's a new file.`
+ )
}
- } catch (e: any) {
- warning(
- `Failed to get file contents: ${
- e as string
- }. This is OK if it's a new file.`
- )
- }
- let fileDiff = ''
- if (file.patch != null) {
- fileDiff = file.patch
- }
-
- const patches: Array<[number, number, string]> = []
- for (const patch of splitPatch(file.patch)) {
- const patchLines = patchStartEndLine(patch)
- if (patchLines == null) {
- continue
- }
- const hunks = parsePatch(patch)
- if (hunks == null) {
- continue
+ let fileDiff = ''
+ if (file.patch != null) {
+ fileDiff = file.patch
}
- const hunksStr = `
+
+ const patches: Array<[number, number, string]> = []
+ for (const patch of splitPatch(file.patch)) {
+ const patchLines = patchStartEndLine(patch)
+ if (patchLines == null) {
+ continue
+ }
+ const hunks = parsePatch(patch)
+ if (hunks == null) {
+ continue
+ }
+ const hunksStr = `
---new_hunk---
\`\`\`
${hunks.newHunk}
@@ -223,18 +232,24 @@ ${hunks.newHunk}
${hunks.oldHunk}
\`\`\`
`
- patches.push([
- patchLines.newHunk.startLine,
- patchLines.newHunk.endLine,
- hunksStr
- ])
- }
- if (patches.length > 0) {
- return [file.filename, fileContent, fileDiff, patches]
- } else {
- return null
- }
- })
+ patches.push([
+ patchLines.newHunk.startLine,
+ patchLines.newHunk.endLine,
+ hunksStr
+ ])
+ }
+ if (patches.length > 0) {
+ return [file.filename, fileContent, fileDiff, patches] as [
+ string,
+ string,
+ string,
+ Array<[number, number, string]>
+ ]
+ } else {
+ return null
+ }
+ })
+ )
)
// Filter out any null results
@@ -247,6 +262,48 @@ ${hunks.oldHunk}
return
}
+ let statusMsg = `
+Commits
+Files that changed from the base of the PR and between ${highestReviewedCommitId} and ${
+ context.payload.pull_request.head.sha
+ } commits.
+
+${
+ filesAndChanges.length > 0
+ ? `
+
+Files selected (${filesAndChanges.length})
+
+* ${filesAndChanges
+ .map(([filename, , , patches]) => `${filename} (${patches.length})`)
+ .join('\n* ')}
+
+`
+ : ''
+}
+${
+ filterIgnoredFiles.length > 0
+ ? `
+
+Files ignored due to filter (${filterIgnoredFiles.length})
+
+* ${filterIgnoredFiles.map(file => file.filename).join('\n* ')}
+
+
+`
+ : ''
+}
+`
+
+ // update the existing comment with in progress status
+ const inProgressSummarizeCmt = commenter.addInProgressStatus(
+ existingSummarizeCmtBody,
+ statusMsg
+ )
+
+ // add in progress status to the summarize comment
+ await commenter.comment(`${inProgressSummarizeCmt}`, SUMMARIZE_TAG, 'replace')
+
const summariesFailed: string[] = []
const doSummary = async (
@@ -263,28 +320,24 @@ ${hunks.oldHunk}
}
ins.filename = filename
+ ins.fileDiff = fileDiff
// render prompt based on inputs so far
- let tokens = getTokenCount(
- prompts.renderSummarizeFileDiff(ins, options.reviewSimpleChanges)
+ const summarizePrompt = prompts.renderSummarizeFileDiff(
+ ins,
+ options.reviewSimpleChanges
)
+ const tokens = getTokenCount(summarizePrompt)
- const diffTokens = getTokenCount(fileDiff)
- if (tokens + diffTokens > options.lightTokenLimits.requestTokens) {
+ if (tokens > options.lightTokenLimits.requestTokens) {
info(`summarize: diff tokens exceeds limit, skip ${filename}`)
summariesFailed.push(`${filename} (diff tokens exceeds limit)`)
return null
}
- ins.fileDiff = fileDiff
- tokens += fileDiff.length
-
// summarize content
try {
- const [summarizeResp] = await lightBot.chat(
- prompts.renderSummarizeFileDiff(ins, options.reviewSimpleChanges),
- {}
- )
+ const [summarizeResp] = await lightBot.chat(summarizePrompt, {})
if (summarizeResp === '') {
info('summarize: nothing obtained from openai')
@@ -377,7 +430,7 @@ ${filename}: ${summary}
if (releaseNotesResponse === '') {
info('release notes: nothing obtained from openai')
} else {
- let message = '### Summary by OpenAI\n\n'
+ let message = '### Summary by CodeRabbit\n\n'
message += releaseNotesResponse
try {
await commenter.updateDescription(
@@ -404,36 +457,20 @@ ${RAW_SUMMARY_END_TAG}
${SHORT_SUMMARY_START_TAG}
${inputs.shortSummary}
${SHORT_SUMMARY_END_TAG}
----
-
-### Chat with 🤖 OpenAI Bot (\`@openai\`)
-- Reply on review comments left by this bot to ask follow-up questions. A review comment is a comment on a diff or a file.
-- Invite the bot into a review comment chain by tagging \`@openai\` in a reply.
-
-### Code suggestions
-- The bot may make code suggestions, but please review them carefully before committing since the line number ranges may be misaligned.
-- You can edit the comment made by the bot and manually tweak the suggestion if it is slightly off.
-
-### Ignoring further reviews
-- Type \`@openai: ignore\` anywhere in the PR description to ignore further reviews from the bot.
---
-${
- filterIgnoredFiles.length > 0
- ? `
-Files ignored due to filter (${filterIgnoredFiles.length})
+Uplevel your code reviews with CodeRabbit Pro
-### Ignored files
+### CodeRabbit Pro
-* ${filterIgnoredFiles.map(file => file.filename).join('\n* ')}
+If you like this project, please support us by purchasing the [Pro version](https://coderabbit.ai). The Pro version has advanced context, superior noise reduction and several proprietary improvements compared to the open source version. Moreover, CodeRabbit Pro is free for open source projects.
`
- : ''
-}
+ statusMsg += `
${
skippedFiles.length > 0
? `
@@ -442,15 +479,12 @@ ${
skippedFiles.length
})
-### Not processed
-
* ${skippedFiles.join('\n* ')}
`
: ''
}
-
${
summariesFailed.length > 0
? `
@@ -459,8 +493,6 @@ ${
summariesFailed.length
})
-### Failed to summarize
-
* ${summariesFailed.join('\n* ')}
@@ -468,6 +500,7 @@ ${
: ''
}
`
+
if (!options.disableReview) {
const filesAndChangesReview = filesAndChanges.filter(([filename]) => {
const needsReview =
@@ -488,6 +521,8 @@ ${
// failed reviews array
const reviewsFailed: string[] = []
+ let lgtmCount = 0
+ let reviewCount = 0
const doReview = async (
filename: string,
fileContent: string,
@@ -581,51 +616,57 @@ ${commentChain}
`
}
- // perform review
- try {
- const [response] = await heavyBot.chat(
- prompts.renderReviewFileDiff(ins),
- {}
- )
- if (response === '') {
- info('review: nothing obtained from openai')
- reviewsFailed.push(`${filename} (no response)`)
- return
- }
- // parse review
- const reviews = parseReview(response, patches, options.debug)
- for (const review of reviews) {
- // check for LGTM
- if (
- !options.reviewCommentLGTM &&
- (review.comment.includes('LGTM') ||
- review.comment.includes('looks good to me'))
- ) {
- continue
- }
- if (context.payload.pull_request == null) {
- warning('No pull request found, skipping.')
- continue
+ if (patchesPacked > 0) {
+ // perform review
+ try {
+ const [response] = await heavyBot.chat(
+ prompts.renderReviewFileDiff(ins),
+ {}
+ )
+ if (response === '') {
+ info('review: nothing obtained from openai')
+ reviewsFailed.push(`${filename} (no response)`)
+ return
}
+ // parse review
+ const reviews = parseReview(response, patches, options.debug)
+ for (const review of reviews) {
+ // check for LGTM
+ if (
+ !options.reviewCommentLGTM &&
+ (review.comment.includes('LGTM') ||
+ review.comment.includes('looks good to me'))
+ ) {
+ lgtmCount += 1
+ continue
+ }
+ if (context.payload.pull_request == null) {
+ warning('No pull request found, skipping.')
+ continue
+ }
- try {
- await commenter.bufferReviewComment(
- filename,
- review.startLine,
- review.endLine,
- `${review.comment}`
- )
- } catch (e: any) {
- reviewsFailed.push(`${filename} comment failed (${e as string})`)
+ try {
+ reviewCount += 1
+ await commenter.bufferReviewComment(
+ filename,
+ review.startLine,
+ review.endLine,
+ `${review.comment}`
+ )
+ } catch (e: any) {
+ reviewsFailed.push(`${filename} comment failed (${e as string})`)
+ }
}
+ } catch (e: any) {
+ warning(
+ `Failed to review: ${e as string}, skipping. backtrace: ${
+ e.stack as string
+ }`
+ )
+ reviewsFailed.push(`${filename} (${e as string})`)
}
- } catch (e: any) {
- warning(
- `Failed to review: ${e as string}, skipping. backtrace: ${
- e.stack as string
- }`
- )
- reviewsFailed.push(`${filename} (${e as string})`)
+ } else {
+ reviewsSkipped.push(`${filename} (diff too large)`)
}
}
@@ -644,20 +685,11 @@ ${commentChain}
await Promise.all(reviewPromises)
- summarizeComment += `
----
-In the recent run, only the files that changed from the \`base\` of the PR and between \`${highestReviewedCommitId}\` and \`${
- context.payload.pull_request.head.sha
- }\` commits were reviewed.
-
+ statusMsg += `
${
reviewsFailed.length > 0
? `
-Files not reviewed due to errors in the recent run (${
- reviewsFailed.length
- })
-
-### Failed to review in the last run
+Files not reviewed due to errors (${reviewsFailed.length})
* ${reviewsFailed.join('\n* ')}
@@ -665,38 +697,61 @@ ${
`
: ''
}
-
${
reviewsSkipped.length > 0
? `
-Files not reviewed due to simple changes (${
+Files skipped from review due to trivial changes (${
reviewsSkipped.length
})
-### Skipped review in the recent run
-
* ${reviewsSkipped.join('\n* ')}
`
: ''
}
+
+Review comments generated (${reviewCount + lgtmCount})
+
+* Review: ${reviewCount}
+* LGTM: ${lgtmCount}
+
+
+
+---
+
+
+Tips
+
+### Chat with CodeRabbit Bot (\`@coderabbitai\`)
+- Reply on review comments left by this bot to ask follow-up questions. A review comment is a comment on a diff or a file.
+- Invite the bot into a review comment chain by tagging \`@coderabbitai\` in a reply.
+
+### Code suggestions
+- The bot may make code suggestions, but please review them carefully before committing since the line number ranges may be misaligned.
+- You can edit the comment made by the bot and manually tweak the suggestion if it is slightly off.
+
+### Pausing incremental reviews
+- Add \`@coderabbitai: ignore\` anywhere in the PR description to pause further reviews from the bot.
+
+
`
// add existing_comment_ids_block with latest head sha
summarizeComment += `\n${commenter.addReviewedCommitId(
existingCommitIdsBlock,
context.payload.pull_request.head.sha
)}`
+
+ // post the review
+ await commenter.submitReview(
+ context.payload.pull_request.number,
+ commits[commits.length - 1].sha,
+ statusMsg
+ )
}
// post the final summary comment
await commenter.comment(`${summarizeComment}`, SUMMARIZE_TAG, 'replace')
-
- // post the review
- await commenter.submitReview(
- context.payload.pull_request.number,
- commits[commits.length - 1].sha
- )
}
const splitPatch = (patch: string | null | undefined): string[] => {
@@ -762,7 +817,6 @@ const parsePatch = (
const oldHunkLines: string[] = []
const newHunkLines: string[] = []
- // let old_line = hunkInfo.old_hunk.start_line
let newLine = hunkInfo.newHunk.startLine
const lines = patch.split('\n').slice(1) // Skip the @@ line
@@ -772,17 +826,32 @@ const parsePatch = (
lines.pop()
}
+ // Skip annotations for the first 3 and last 3 lines
+ const skipStart = 3
+ const skipEnd = 3
+
+ let currentLine = 0
+
+ const removalOnly = !lines.some(line => line.startsWith('+'))
+
for (const line of lines) {
+ currentLine++
if (line.startsWith('-')) {
oldHunkLines.push(`${line.substring(1)}`)
- // old_line++
} else if (line.startsWith('+')) {
newHunkLines.push(`${newLine}: ${line.substring(1)}`)
newLine++
} else {
+ // context line
oldHunkLines.push(`${line}`)
- newHunkLines.push(`${newLine}: ${line}`)
- // old_line++
+ if (
+ removalOnly ||
+ (currentLine > skipStart && currentLine <= lines.length - skipEnd)
+ ) {
+ newHunkLines.push(`${newLine}: ${line}`)
+ } else {
+ newHunkLines.push(`${line}`)
+ }
newLine++
}
}
@@ -806,6 +875,8 @@ function parseReview(
): Review[] {
const reviews: Review[] = []
+ response = sanitizeResponse(response.trim())
+
const lines = response.split('\n')
const lineNumberRangeRegex = /(?:^|\s)(\d+)-(\d+):\s*$/
const commentSeparator = '---'
@@ -815,11 +886,10 @@ function parseReview(
let currentComment = ''
function storeReview(): void {
if (currentStartLine !== null && currentEndLine !== null) {
- const sanitizedComment = sanitizeComment(currentComment.trim())
const review: Review = {
startLine: currentStartLine,
endLine: currentEndLine,
- comment: sanitizedComment.trim()
+ comment: currentComment
}
let withinPatch = false
@@ -870,44 +940,50 @@ ${review.comment}`
}
}
- function sanitizeComment(comment: string): string {
- const suggestionStart = '```suggestion'
- const suggestionEnd = '```'
+ function sanitizeCodeBlock(comment: string, codeBlockLabel: string): string {
+ const codeBlockStart = `\`\`\`${codeBlockLabel}`
+ const codeBlockEnd = '```'
const lineNumberRegex = /^ *(\d+): /gm
- let suggestionStartIndex = comment.indexOf(suggestionStart)
+ let codeBlockStartIndex = comment.indexOf(codeBlockStart)
- while (suggestionStartIndex !== -1) {
- const suggestionEndIndex = comment.indexOf(
- suggestionEnd,
- suggestionStartIndex + suggestionStart.length
+ while (codeBlockStartIndex !== -1) {
+ const codeBlockEndIndex = comment.indexOf(
+ codeBlockEnd,
+ codeBlockStartIndex + codeBlockStart.length
)
- if (suggestionEndIndex === -1) break
+ if (codeBlockEndIndex === -1) break
- const suggestionBlock = comment.substring(
- suggestionStartIndex + suggestionStart.length,
- suggestionEndIndex
+ const codeBlock = comment.substring(
+ codeBlockStartIndex + codeBlockStart.length,
+ codeBlockEndIndex
)
- const sanitizedBlock = suggestionBlock.replace(lineNumberRegex, '')
+ const sanitizedBlock = codeBlock.replace(lineNumberRegex, '')
comment =
- comment.slice(0, suggestionStartIndex + suggestionStart.length) +
+ comment.slice(0, codeBlockStartIndex + codeBlockStart.length) +
sanitizedBlock +
- comment.slice(suggestionEndIndex)
+ comment.slice(codeBlockEndIndex)
- suggestionStartIndex = comment.indexOf(
- suggestionStart,
- suggestionStartIndex +
- suggestionStart.length +
+ codeBlockStartIndex = comment.indexOf(
+ codeBlockStart,
+ codeBlockStartIndex +
+ codeBlockStart.length +
sanitizedBlock.length +
- suggestionEnd.length
+ codeBlockEnd.length
)
}
return comment
}
+ function sanitizeResponse(comment: string): string {
+ comment = sanitizeCodeBlock(comment, 'suggestion')
+ comment = sanitizeCodeBlock(comment, 'diff')
+ return comment
+ }
+
for (const line of lines) {
const lineNumberRangeMatch = line.match(lineNumberRangeRegex)