Compare commits

..
Author SHA1 Message Date
Jeffery 1279cae575 test(gitea): 補 .reviewignore 解析與載入測試
CI / 1. BUILD (pull_request) Successful in 2s
CI / 2. TEST (pull_request) Failing after 26m13s
CI / 3. RESULT (pull_request) Has been skipped
2026-07-03 13:34:49 +08:00
Jeffery 47aa199e5e feat(diff 過濾): 改讀 .reviewignore 外部化 diff 排除清單 2026-07-03 13:34:49 +08:00
3 changed files with 85 additions and 12 deletions
+13
View File
@@ -0,0 +1,13 @@
# AI Code Review 忽略清單
# 符合下列前綴/路徑的檔案不會納入送給 LLM 的 git diff。
# 規則:每行一個路徑前綴(相對 repo 根),# 開頭為註解,空行略過。
# 註:任何深度的 node_modules/ 一律排除(程式內建保險),此處列出僅為明示。
.gitea/
.github/
README.md
TODO.md
package-lock.json
src/package-lock.json
dist/
node_modules/
+43 -11
View File
@@ -41,23 +41,55 @@ export function getBotReviewOutcome(message) {
return match?.[1]?.toLowerCase() || 'unknown'; return match?.[1]?.toLowerCase() || 'unknown';
} }
// 找不到 .reviewignore 時(例如其他 repo 未提供)採用的內建預設排除清單。
// 任何深度的 node_modules/ 另由 filterDiff 內建強制排除,不倚賴此清單。
export const DEFAULT_REVIEW_IGNORE = [
'.gitea/',
'.github/',
'README.md',
'TODO.md',
'package-lock.json',
'src/package-lock.json',
'dist/',
];
/** /**
* 取得目前 PR 的完整 Git diff,並排除 CI/文件等不需審查的路徑(.gitea/、.github/、README.md、TODO.md)。 * 解析 .reviewignore 文字為排除前綴陣列(gitignore 風格)。
* 規則:每行一個路徑前綴,trim 後略過空行與 `#` 開頭的註解行。
* @param {string} text - .reviewignore 檔案內容。
* @returns {string[]} 排除前綴清單。
*/
export function parseReviewIgnore(text) {
return String(text || '')
.split('\n')
.map(l => l.trim())
.filter(l => l && !l.startsWith('#'));
}
/**
* 從被審 PR 的 head ref 取得 `.reviewignore` 並解析為排除清單。
* 檔案不存在或為空時退回 {@link DEFAULT_REVIEW_IGNORE}。
* @returns {Promise<string[]>} 套用於 diff 過濾的排除前綴清單。
*/
export async function getReviewIgnore() {
const patterns = parseReviewIgnore(await getFileContentAtRef('.reviewignore'));
if (patterns.length > 0) {
line(`已套用 .reviewignore${patterns.length} 條排除規則`);
return patterns;
}
return DEFAULT_REVIEW_IGNORE;
}
/**
* 取得目前 PR 的完整 Git diff,並依 `.reviewignore`(讀不到時用內建預設)排除不需審查的路徑。
* 透過 Gitea `GET /repos/{repo}/pulls/{index}.diff`(純文字 diff),授權使用 GITEA_TOKEN。 * 透過 Gitea `GET /repos/{repo}/pulls/{index}.diff`(純文字 diff),授權使用 GITEA_TOKEN。
* @returns {Promise<string>} 過濾後的 diff 文字。 * @returns {Promise<string>} 過濾後的 diff 文字。
* @throws {Error} 當 Gitea API 請求失敗(網路錯誤、逾時或非 2xx 狀態)時拋出 axios 例外。 * @throws {Error} 當 Gitea 取 diff 的 API 請求失敗(網路錯誤、逾時或非 2xx 狀態)時拋出 axios 例外。
*/ */
export async function getPRDiff() { export async function getPRDiff() {
const patterns = await getReviewIgnore();
const resp = await axios.get(api(`/repos/${GITEA_REPOSITORY}/pulls/${PR_NUMBER}.diff`), { headers: headers(), timeout: 60000, httpsAgent }); const resp = await axios.get(api(`/repos/${GITEA_REPOSITORY}/pulls/${PR_NUMBER}.diff`), { headers: headers(), timeout: 60000, httpsAgent });
return filterDiff(resp.data, [ return filterDiff(resp.data, patterns);
'.gitea/',
'.github/',
'README.md',
'TODO.md',
'package-lock.json',
'src/package-lock.json',
'dist/',
]);
} }
/** /**
+29 -1
View File
@@ -1,7 +1,7 @@
import { describe, it, afterEach, mock } from 'node:test'; import { describe, it, afterEach, mock } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import axios from 'axios'; import axios from 'axios';
import { getPRDiff, filterDiff, postComment, postPullReviewComment, postPullReview, getCommitMessageBySha, getBranchHeadCommitMessage, shouldSkipBotCommit, getBotReviewOutcome, listPullReviews, getPullReviewComments, listAllReviewComments, resolvePullReviewComment, getFileContentAtRef } from '../gitea.js'; import { getPRDiff, filterDiff, parseReviewIgnore, getReviewIgnore, DEFAULT_REVIEW_IGNORE, postComment, postPullReviewComment, postPullReview, getCommitMessageBySha, getBranchHeadCommitMessage, shouldSkipBotCommit, getBotReviewOutcome, listPullReviews, getPullReviewComments, listAllReviewComments, resolvePullReviewComment, getFileContentAtRef } from '../gitea.js';
afterEach(() => mock.restoreAll()); afterEach(() => mock.restoreAll());
@@ -259,3 +259,31 @@ describe('filterDiff', () => {
assert.ok(result.includes('src/main.js')); assert.ok(result.includes('src/main.js'));
}); });
}); });
describe('parseReviewIgnore', () => {
it('parses prefixes, skipping blanks and comments', () => {
const text = '# comment\n\n.gitea/\n dist/ \n# another\nREADME.md\n';
assert.deepEqual(parseReviewIgnore(text), ['.gitea/', 'dist/', 'README.md']);
});
it('returns an empty array for empty/nullish input', () => {
assert.deepEqual(parseReviewIgnore(''), []);
assert.deepEqual(parseReviewIgnore(null), []);
});
});
describe('getReviewIgnore', () => {
it('uses patterns fetched from .reviewignore when present', async () => {
mock.method(axios, 'get', async () => ({
data: { content: Buffer.from('a/\nb/\n# c\n').toString('base64'), encoding: 'base64' },
}));
const patterns = await getReviewIgnore();
assert.deepEqual(patterns, ['a/', 'b/']);
});
it('falls back to the default list when .reviewignore is missing/empty', async () => {
mock.method(axios, 'get', async () => ({ data: {} }));
const patterns = await getReviewIgnore();
assert.deepEqual(patterns, DEFAULT_REVIEW_IGNORE);
});
});