153 lines
4.9 KiB
TypeScript
153 lines
4.9 KiB
TypeScript
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
|