基于 OpenCodeReview 的 webhook 服务:监听 Gitea 的 push 与 Pull Request 事件, 调用 OCR 审查 diff,把结果发布回 Gitea,并按阻断阈值决定是否自动合并。 主要能力: - Push / PR 事件触发,支持分支 glob 过滤与 PR-only / push-only 范围 - PR 内联评论(按 diff 行号定位)、汇总评论、Issue 生命周期、提交状态 - 可配置阻断阈值(严重级别 / 类别 / 任意意见) - 无阻断问题时自动合并,审查覆盖不完整时拒绝合并 - 内置 Web 后台:仓库配置、任务日志、失败重跑、连通性自检 - SQLite 持久化,worker 重启回收卡死任务,失败自动重试 实现为独立服务而非 Gitea Action:本机 act_runner 指向的实例不可达, 且后台配置与任务历史需要独立进程承载。
133 lines
4.1 KiB
JavaScript
133 lines
4.1 KiB
JavaScript
/** Parse unified git diffs into per-file hunks with old/new line maps. */
|
|
|
|
const FILE_HEADER = /^diff --git "?a\/(.+?)"? "?b\/(.+?)"?$/;
|
|
const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
|
|
function stripPrefixQuotes(value) {
|
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
return value.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
}
|
|
return value;
|
|
}
|
|
|
|
/**
|
|
* Parse a unified diff string.
|
|
* Returns a Map of newPath -> { oldPath, status, hunks: [{ oldStart, oldLines,
|
|
* newStart, newLines, newLineNumbers: number[], lines: string[] }] }.
|
|
* `newLineNumbers[i]` is the new-file line number for hunk line i, or 0 for
|
|
* removed lines / "\ No newline" markers.
|
|
*/
|
|
export function parseUnifiedDiff(diffText) {
|
|
const files = new Map();
|
|
if (!diffText) return files;
|
|
|
|
let current = null;
|
|
let hunk = null;
|
|
let newLine = 0;
|
|
let oldLine = 0;
|
|
|
|
for (const raw of diffText.split("\n")) {
|
|
const header = FILE_HEADER.exec(raw);
|
|
if (header) {
|
|
const newPath = stripPrefixQuotes(header[2]);
|
|
current = {
|
|
oldPath: stripPrefixQuotes(header[1]),
|
|
newPath,
|
|
status: "modified",
|
|
hunks: [],
|
|
};
|
|
files.set(newPath, current);
|
|
hunk = null;
|
|
continue;
|
|
}
|
|
if (!current) continue;
|
|
|
|
if (raw.startsWith("new file mode")) { current.status = "added"; continue; }
|
|
if (raw.startsWith("deleted file mode")) { current.status = "deleted"; continue; }
|
|
if (raw.startsWith("rename to ")) { current.newPath = stripPrefixQuotes(raw.slice(10).trim()); continue; }
|
|
if (raw.startsWith("--- ") || raw.startsWith("+++ ") || raw.startsWith("index ")
|
|
|| raw.startsWith("similarity index") || raw.startsWith("rename from")
|
|
|| raw.startsWith("old mode") || raw.startsWith("new mode")) {
|
|
continue;
|
|
}
|
|
|
|
const h = HUNK_HEADER.exec(raw);
|
|
if (h) {
|
|
oldLine = Number(h[1]);
|
|
newLine = Number(h[3]);
|
|
hunk = {
|
|
oldStart: oldLine,
|
|
oldLines: h[2] === undefined ? 1 : Number(h[2]),
|
|
newStart: newLine,
|
|
newLines: h[4] === undefined ? 1 : Number(h[4]),
|
|
newLineNumbers: [],
|
|
lines: [],
|
|
};
|
|
current.hunks.push(hunk);
|
|
continue;
|
|
}
|
|
if (!hunk) continue;
|
|
|
|
hunk.lines.push(raw);
|
|
if (raw.startsWith("+")) {
|
|
hunk.newLineNumbers.push(newLine);
|
|
newLine += 1;
|
|
} else if (raw.startsWith("-")) {
|
|
hunk.newLineNumbers.push(0);
|
|
oldLine += 1;
|
|
} else if (raw.startsWith("\\")) {
|
|
hunk.newLineNumbers.push(0);
|
|
} else {
|
|
hunk.newLineNumbers.push(newLine);
|
|
newLine += 1;
|
|
oldLine += 1;
|
|
}
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
/** All new-file line numbers present in the diff for a given path. */
|
|
export function addedLineNumbers(fileEntry) {
|
|
const added = new Set();
|
|
for (const hunk of fileEntry.hunks) {
|
|
for (let i = 0; i < hunk.lines.length; i += 1) {
|
|
if (hunk.lines[i].startsWith("+")) {
|
|
const line = hunk.newLineNumbers[i];
|
|
if (line > 0) added.add(line);
|
|
}
|
|
}
|
|
}
|
|
return added;
|
|
}
|
|
|
|
/** All new-file line numbers in any hunk (added + context) for a path. */
|
|
export function diffLineNumbers(fileEntry) {
|
|
const lines = new Set();
|
|
for (const hunk of fileEntry.hunks) {
|
|
for (let i = 0; i < hunk.lines.length; i += 1) {
|
|
const line = hunk.newLineNumbers[i];
|
|
if (line > 0) lines.add(line);
|
|
}
|
|
}
|
|
return lines;
|
|
}
|
|
|
|
/**
|
|
* Choose the best inline anchor for a finding.
|
|
* Prefers a line inside the diff (Gitea renders those inline); falls back to
|
|
* the start line so the finding is still recorded on the pull request.
|
|
*/
|
|
export function pickAnchorLine(fileEntry, startLine, endLine) {
|
|
const inDiff = diffLineNumbers(fileEntry);
|
|
const added = addedLineNumbers(fileEntry);
|
|
const lo = Math.max(1, Math.min(startLine || endLine || 1, endLine || startLine || 1));
|
|
const hi = Math.max(startLine || endLine || 1, endLine || startLine || 1);
|
|
for (let line = lo; line <= hi; line += 1) {
|
|
if (added.has(line)) return { line, inDiff: true };
|
|
}
|
|
for (let line = lo; line <= hi; line += 1) {
|
|
if (inDiff.has(line)) return { line, inDiff: true };
|
|
}
|
|
return { line: lo, inDiff: false };
|
|
} |