153 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { CommitDiffSchema } from "@gitbeaker/rest"
import pLimit from "p-limit"
import { Logger } from "winston"
import chatTools from "../../utils/chatTools"
import tokenTools from "../../utils/tokenTools"
import Commenter from "./utils/commenter"
import diffTools, { Review } from "./utils/diffTools"
import { Inputs } from "./utils/inputs"
import { Prompts } from "./utils/prompts"
/**
* 审查需要审查的文件列表,并生成评论。
* @param {CommitDiffSchema[]} needReviewFiles - 需要审查的文件列表。
* @param {Inputs} rawInput - 原始输入。
* @param {Commenter} commenter - 评论工具实例。
* @param {Logger} logger - 日志记录器。
* @returns {Promise<void>} 返回一个Promise表示审查过程的完成。
*/
const reviewFiles = async (
needReviewFiles: CommitDiffSchema[],
rawInput: Inputs,
commenter: Commenter,
logger: Logger
) => {
const prompts = new Prompts()
// 创建一个Map来存储文件的差异信息
const diffsFileMap = new Map<string, [number, number, string][]>()
// 解析每个文件的差异并存储在diffsFileMap中
needReviewFiles.forEach((file) => {
const diffs = diffTools.parseFileDiffs(file)
const fileDiff = diffsFileMap.get(file.new_path) || []
diffsFileMap.set(file.new_path, fileDiff.concat(diffs))
})
logger.debug(`diffsFileMap: ${JSON.stringify([...diffsFileMap])}`)
// 如果没有需要审查的文件,记录日志并创建评论
if (diffsFileMap.size === 0) {
logger.info("No files need review")
commenter.createComment("没有需要审查的文件")
return
}
const skippedFiles: string[] = []
const fileCRResultsMap = new Map<string, Review[]>()
/**
* 审查单个文件的差异。
* @param {Array} diff - 文件的差异数组。
* @param {string} filename - 文件名。
* @returns {Promise<void>} 返回一个Promise表示审查过程的完成。
*/
const doFileReview = async (
diff: [number, number, string][],
filename: string
) => {
logger.info(`Reviewing ${filename}`)
const inputs = rawInput.clone()
inputs.filename = filename
let tokens = tokenTools.getTokenCount(prompts.renderReviewFileDiff(inputs))
// 计算并打包文件的差异,确保令牌数不超过限制
const packedFileCount = diff.findIndex((d, idx) => {
const hunk = d[2]
if (tokens + tokenTools.getTokenCount(hunk) > 100 * 1000) {
logger.info(
`only packing ${idx} / ${diff.length} diffs, tokens: ${tokens} / ${100 * 1000}`
)
return true
}
inputs.patches += `
${hunk}
`
tokens += tokenTools.getTokenCount(hunk)
return false
})
// 如果没有打包任何差异,跳过该文件
if (packedFileCount === 0) {
logger.info(`skipping ${filename}`)
skippedFiles.push(filename)
return
}
// 获取GPT-4模型并生成审查提示
const codeChatBot = await chatTools.getGpt4oModel(0)
const reviewPrompt = prompts.renderReviewFileDiff(inputs)
logger.debug(`reviewPrompt for ${filename}: ${reviewPrompt}`)
try {
const { content: review } = await codeChatBot.invoke(reviewPrompt)
if (!review) throw new Error("Empty review")
logger.info(`review for ${filename}: ${review}`)
// 解析审查结果并过滤需要评论的审查
const reviews = diffTools.parseReview(review as string, diff)
logger.debug(`reviews for ${filename}: ${JSON.stringify(reviews)}`)
const needCommentReviews = reviews.filter(
(r) => !r.comment.includes("LGTM")
)
fileCRResultsMap.set(filename, needCommentReviews)
} catch {
logger.error(`Failed to review ${filename}`)
}
}
/**
* 创建评论。
* @param {string} filename - 文件名。
* @param {Review} reviews - 审查结果。
* @returns {Promise<void>} 返回一个Promise表示评论过程的完成。
*/
const doComment = async (filename: string, reviews: Review) => {
const file = needReviewFiles.find((f) => f.new_path === filename)
if (!file) {
logger.error(`Failed to find file ${filename}`)
return
}
await commenter.createReviewComment(
file.new_path,
file.old_path,
reviews.startLine,
reviews.endLine,
reviews.comment
)
}
// 使用pLimit限制并发审查文件的数量
const limit = pLimit(10)
await Promise.allSettled(
[...diffsFileMap].map(([filename, diffs]) =>
limit(() => doFileReview(diffs, filename))
)
)
// 把fileCRResultsMap中的needCommentReviews拍平成一个数组
const needCommentReviews: { filename: string; reviews: Review }[] = []
fileCRResultsMap.forEach((reviews, filename) => {
reviews.forEach((r) => needCommentReviews.push({ filename, reviews: r }))
})
// 创建评论
await Promise.allSettled(
needCommentReviews.map(({ filename, reviews }) =>
limit(() => doComment(filename, reviews))
)
)
}
export default reviewFiles