@spaceflow/review 0.78.0 → 0.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/dist/index.js +39 -23
- package/package.json +1 -1
- package/src/review-context.ts +24 -10
- package/src/review.service.spec.ts +64 -2
- package/src/review.service.ts +16 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.78.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.77.0...@spaceflow/review@0.78.0) (2026-04-07)
|
|
4
|
+
|
|
5
|
+
### 新特性
|
|
6
|
+
|
|
7
|
+
* **review:** 新增 code-* 代码结构过滤语法,支持按函数/类/接口等类型审查新增代码 ([f3b17d3](https://github.com/Lydanne/spaceflow/commit/f3b17d36c6360269a2fd4075311ae764e85cf428))
|
|
8
|
+
|
|
9
|
+
### 代码重构
|
|
10
|
+
|
|
11
|
+
* **ci:** 将 PR 审查工作流日志级别从 -vv 提升至 -vvv ([43bd563](https://github.com/Lydanne/spaceflow/commit/43bd5632dbd7151e11977520ca0634fdbc393279))
|
|
12
|
+
* **review:** 优化 syncReactionsToIssues 中的 issue 匹配逻辑,优先使用 issue-key 精确匹配并回退到 path+position 匹配 ([eb8ea47](https://github.com/Lydanne/spaceflow/commit/eb8ea47c56466aa25d58358a09f1e0c9094aa28d))
|
|
13
|
+
* **review:** 修改 buildLinesWithNumbers 忽略占位符格式,从 `...... ..| ignore X-Y code` 改为 `....... ignore X-Y line .......` ([a45dc1a](https://github.com/Lydanne/spaceflow/commit/a45dc1a5b92d50f75682782cec24b0100732e434))
|
|
14
|
+
* **review:** 将 code-* 语法从 includes 迁移至独立 filterCodeBlocks 配置项 ([ed4b921](https://github.com/Lydanne/spaceflow/commit/ed4b9217a563d992b76d1e55af21f9b364329737))
|
|
15
|
+
* **review:** 将 filterCodeBlocks 重命名为 whenModifiedCode,简化代码结构过滤语法 ([951c570](https://github.com/Lydanne/spaceflow/commit/951c570ceb7c04d25265d043c3948687bbb230f1))
|
|
16
|
+
* **review:** 将 prompt 构建逻辑提取到独立的 prompt 模块,新增类型验证和 schema 定义 ([5f21356](https://github.com/Lydanne/spaceflow/commit/5f21356964ad4f08004e229ac7bd538c0301b37d))
|
|
17
|
+
* **review:** 将 review-pr-comment-utils 重构为 utils/review-pr-comment,新增完整测试覆盖 ([29041b6](https://github.com/Lydanne/spaceflow/commit/29041b635e3a6ecbd2b2ae49ad1b657aa1a66e9c))
|
|
18
|
+
* **review:** 将 skipDuplicateWorkflow 重构为 duplicateWorkflowResolved,支持 skip/delete 两种处理模式 ([37238f9](https://github.com/Lydanne/spaceflow/commit/37238f9ae8ed38c67f46acb963e3ebb795c0ef71))
|
|
19
|
+
* **review:** 将无效 commit hash 的 issue 标记为 invalid 并清除 commit 字段,移除 code-* 前缀语法支持 ([0a89136](https://github.com/Lydanne/spaceflow/commit/0a89136b32cf49a70dbdfa26683e97ee13dc207e))
|
|
20
|
+
* **review:** 提前检测无效 commit hash,在 fillIssueAuthors 中对已有 author 的 issue 也进行校验 ([c166496](https://github.com/Lydanne/spaceflow/commit/c1664966066bca032bd936b6c1d2c8f320949f9c))
|
|
21
|
+
* **review:** 新增 systemRules 静态规则系统,支持 maxLinesPerFile 限制并跳过超限文件的 LLM 审查 ([3721d0f](https://github.com/Lydanne/spaceflow/commit/3721d0f46d40f71230aceb060350682a3d5642d7))
|
|
22
|
+
* **review:** 新增 whenModifiedCode 过滤逻辑,跳过无匹配代码块的文件并记录日志 ([3d0c1e1](https://github.com/Lydanne/spaceflow/commit/3d0c1e1a4b492e62104af92d16208c206934ad10))
|
|
23
|
+
* **review:** 简化 includes glob 模式,移除冗余的 `*/**` 前缀 ([56cf145](https://github.com/Lydanne/spaceflow/commit/56cf145fbf8f1311df9d538fffc61581d4ead400))
|
|
24
|
+
|
|
25
|
+
### 测试用例
|
|
26
|
+
|
|
27
|
+
* **review:** 新增全量 diff 语义测试,验证 added| 对分支首次引入文件的持续匹配行为 ([5b72577](https://github.com/Lydanne/spaceflow/commit/5b725772091d1d4fe5207af1b7d176e705bdb8d3))
|
|
28
|
+
|
|
29
|
+
### 其他修改
|
|
30
|
+
|
|
31
|
+
* **review-summary:** released version 0.46.0 [no ci] ([72fa783](https://github.com/Lydanne/spaceflow/commit/72fa783ef5c104d654a29481300d791b333df0ee))
|
|
32
|
+
* **scripts:** released version 0.32.0 [no ci] ([7871aa9](https://github.com/Lydanne/spaceflow/commit/7871aa94a7f1227d6f09fc43ea69fb7d4e193ec4))
|
|
33
|
+
* **shell:** released version 0.32.0 [no ci] ([88bdc81](https://github.com/Lydanne/spaceflow/commit/88bdc81cb0ec7809fbd76c7b157e0177b2f4db20))
|
|
34
|
+
|
|
3
35
|
## [0.77.0](https://github.com/Lydanne/spaceflow/compare/@spaceflow/review@0.76.0...@spaceflow/review@0.77.0) (2026-04-07)
|
|
4
36
|
|
|
5
37
|
### 新特性
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LlmJsonPut, REVIEW_STATE, addLocaleResources, calculateNewLineNumber, createStreamLoggerState, defineExtension, logStreamEvent, normalizeVerbose, parallel, parseChangedLinesFromPatch, parseDiffText, parseHunksFromPatch, parseRepoUrl, parseVerbose, shouldLog, t, z } from "@spaceflow/core";
|
|
2
2
|
import { access, mkdir, readFile, readdir, writeFile } from "fs/promises";
|
|
3
|
-
import { basename, dirname, extname, isAbsolute, join, relative } from "path";
|
|
3
|
+
import { basename, dirname, extname, isAbsolute, join, normalize, relative } from "path";
|
|
4
4
|
import { homedir } from "os";
|
|
5
5
|
import { execSync, spawn } from "child_process";
|
|
6
6
|
import micromatch_0 from "micromatch";
|
|
@@ -3239,11 +3239,14 @@ class ReviewContextBuilder {
|
|
|
3239
3239
|
if (reviewConf.references?.length) {
|
|
3240
3240
|
specSources.push(...reviewConf.references);
|
|
3241
3241
|
}
|
|
3242
|
+
const normalizedFiles = this.normalizeFilePaths(options.files);
|
|
3242
3243
|
// 解析本地模式:非 CI、非 PR、无 base/head 时默认启用 uncommitted 模式
|
|
3244
|
+
// 当显式指定 files 时,强制走“按文件审查模式”,不进入本地未提交模式
|
|
3243
3245
|
const localMode = this.resolveLocalMode(options, {
|
|
3244
3246
|
ci: options.ci,
|
|
3245
3247
|
hasPrNumber: !!prNumber,
|
|
3246
|
-
hasBaseHead: !!(options.base || options.head)
|
|
3248
|
+
hasBaseHead: !!(options.base || options.head),
|
|
3249
|
+
hasFiles: !!normalizedFiles?.length
|
|
3247
3250
|
});
|
|
3248
3251
|
// 当没有 PR 且没有指定 base/head 且不是本地模式时,自动获取默认值
|
|
3249
3252
|
let baseRef = options.base;
|
|
@@ -3273,7 +3276,7 @@ class ReviewContextBuilder {
|
|
|
3273
3276
|
includes: ctxIncludes,
|
|
3274
3277
|
whenModifiedCode: options.whenModifiedCode ?? reviewConf.whenModifiedCode,
|
|
3275
3278
|
llmMode: options.llmMode ?? titleOptions.llmMode ?? reviewConf.llmMode,
|
|
3276
|
-
files:
|
|
3279
|
+
files: normalizedFiles,
|
|
3277
3280
|
commits: options.commits,
|
|
3278
3281
|
verifyFixes: options.verifyFixes ?? titleOptions.verifyFixes ?? reviewConf.verifyFixes ?? true,
|
|
3279
3282
|
verifyConcurrency: options.verifyConcurrency ?? reviewConf.verifyFixesConcurrency ?? 10,
|
|
@@ -3303,6 +3306,10 @@ class ReviewContextBuilder {
|
|
|
3303
3306
|
* - 显式指定 --no-local 时禁用
|
|
3304
3307
|
* - 非 CI、非 PR、无 base/head 时默认启用 uncommitted 模式
|
|
3305
3308
|
*/ resolveLocalMode(options, env) {
|
|
3309
|
+
// 显式指定了 files,优先进入按文件审查模式
|
|
3310
|
+
if (env.hasFiles) {
|
|
3311
|
+
return false;
|
|
3312
|
+
}
|
|
3306
3313
|
// 显式指定了 --no-local
|
|
3307
3314
|
if (options.local === false) {
|
|
3308
3315
|
return false;
|
|
@@ -3328,13 +3335,17 @@ class ReviewContextBuilder {
|
|
|
3328
3335
|
*/ normalizeFilePaths(files) {
|
|
3329
3336
|
if (!files || files.length === 0) return files;
|
|
3330
3337
|
const cwd = process.cwd();
|
|
3331
|
-
return files.map((file)=>
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
+
return files.map((file)=>this.normalizeSingleFilePath(file, cwd));
|
|
3339
|
+
}
|
|
3340
|
+
/**
|
|
3341
|
+
* 规范化单个文件路径为仓库相对路径:
|
|
3342
|
+
* - 绝对路径转相对路径
|
|
3343
|
+
* - 统一分隔符为 /
|
|
3344
|
+
* - 移除前导 ./
|
|
3345
|
+
*/ normalizeSingleFilePath(file, cwd) {
|
|
3346
|
+
const normalizedInput = normalize(file);
|
|
3347
|
+
const relativePath = isAbsolute(normalizedInput) ? relative(cwd, normalizedInput) : normalizedInput;
|
|
3348
|
+
return relativePath.replaceAll("\\", "/").replace(/^\.\/+/, "");
|
|
3338
3349
|
}
|
|
3339
3350
|
/**
|
|
3340
3351
|
* 根据 AnalyzeDeletionsMode 和当前环境解析是否启用删除代码分析
|
|
@@ -5147,6 +5158,10 @@ class ReviewService {
|
|
|
5147
5158
|
const source = await this.resolveSourceData(context);
|
|
5148
5159
|
if (source.earlyReturn) return source.earlyReturn;
|
|
5149
5160
|
const { prModel, commits, changedFiles, headSha, isDirectFileMode } = source;
|
|
5161
|
+
const effectiveWhenModifiedCode = isDirectFileMode ? undefined : context.whenModifiedCode;
|
|
5162
|
+
if (isDirectFileMode && context.whenModifiedCode?.length && shouldLog(verbose, 1)) {
|
|
5163
|
+
console.log(`ℹ️ 直接文件模式下忽略 whenModifiedCode 过滤`);
|
|
5164
|
+
}
|
|
5150
5165
|
// 2. 规则匹配
|
|
5151
5166
|
const specs = await this.issueFilter.loadSpecs(specSources, verbose);
|
|
5152
5167
|
const applicableSpecs = this.reviewSpecService.filterApplicableSpecs(specs, changedFiles);
|
|
@@ -5174,7 +5189,7 @@ class ReviewService {
|
|
|
5174
5189
|
if (shouldLog(verbose, 1)) {
|
|
5175
5190
|
console.log(`🔄 当前审查轮次: ${(existingResultModel?.round ?? 0) + 1}`);
|
|
5176
5191
|
}
|
|
5177
|
-
const reviewPrompt = await this.buildReviewPrompt(specs, changedFiles, fileContents, commits, existingResultModel?.result ?? null,
|
|
5192
|
+
const reviewPrompt = await this.buildReviewPrompt(specs, changedFiles, fileContents, commits, existingResultModel?.result ?? null, effectiveWhenModifiedCode, verbose, context.systemRules);
|
|
5178
5193
|
const result = await this.runLLMReview(llmMode, reviewPrompt, {
|
|
5179
5194
|
verbose,
|
|
5180
5195
|
concurrency: context.concurrency,
|
|
@@ -5230,7 +5245,7 @@ class ReviewService {
|
|
|
5230
5245
|
* 如果需要提前返回(如同分支、重复 workflow),通过 earlyReturn 字段传递。
|
|
5231
5246
|
*/ async resolveSourceData(context) {
|
|
5232
5247
|
const { owner, repo, prNumber, baseRef, headRef, verbose, ci, includes, files, commits: filterCommits, localMode, duplicateWorkflowResolved } = context;
|
|
5233
|
-
const isDirectFileMode = !!(files && files.length > 0 &&
|
|
5248
|
+
const isDirectFileMode = !!(files && files.length > 0 && !prNumber);
|
|
5234
5249
|
let isLocalMode = !!localMode;
|
|
5235
5250
|
let effectiveBaseRef = baseRef;
|
|
5236
5251
|
let effectiveHeadRef = headRef;
|
|
@@ -5289,8 +5304,17 @@ class ReviewService {
|
|
|
5289
5304
|
}
|
|
5290
5305
|
}
|
|
5291
5306
|
}
|
|
5292
|
-
//
|
|
5293
|
-
if (
|
|
5307
|
+
// 直接文件审查模式(-f):绕过 diff,直接按指定文件构造审查输入
|
|
5308
|
+
if (isDirectFileMode) {
|
|
5309
|
+
if (shouldLog(verbose, 1)) {
|
|
5310
|
+
console.log(`📥 直接审查指定文件模式 (${files.length} 个文件)`);
|
|
5311
|
+
}
|
|
5312
|
+
changedFiles = files.map((f)=>({
|
|
5313
|
+
filename: f,
|
|
5314
|
+
status: "modified"
|
|
5315
|
+
}));
|
|
5316
|
+
isLocalMode = true;
|
|
5317
|
+
} else if (prNumber) {
|
|
5294
5318
|
if (shouldLog(verbose, 1)) {
|
|
5295
5319
|
console.log(`📥 获取 PR #${prNumber} 信息 (owner: ${owner}, repo: ${repo})`);
|
|
5296
5320
|
}
|
|
@@ -5319,15 +5343,7 @@ class ReviewService {
|
|
|
5319
5343
|
}
|
|
5320
5344
|
}
|
|
5321
5345
|
} else if (effectiveBaseRef && effectiveHeadRef) {
|
|
5322
|
-
if (
|
|
5323
|
-
if (shouldLog(verbose, 1)) {
|
|
5324
|
-
console.log(`📥 直接审查指定文件模式 (${files.length} 个文件)`);
|
|
5325
|
-
}
|
|
5326
|
-
changedFiles = files.map((f)=>({
|
|
5327
|
-
filename: f,
|
|
5328
|
-
status: "modified"
|
|
5329
|
-
}));
|
|
5330
|
-
} else if (changedFiles.length === 0) {
|
|
5346
|
+
if (changedFiles.length === 0) {
|
|
5331
5347
|
if (shouldLog(verbose, 1)) {
|
|
5332
5348
|
console.log(`📥 获取 ${effectiveBaseRef}...${effectiveHeadRef} 的差异 (owner: ${owner}, repo: ${repo})`);
|
|
5333
5349
|
}
|
package/package.json
CHANGED
package/src/review-context.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { parseTitleOptions } from "./parse-title-options";
|
|
|
15
15
|
import { type ReviewIssue, type UserInfo } from "./review-spec";
|
|
16
16
|
import { readFile } from "fs/promises";
|
|
17
17
|
import { join } from "path";
|
|
18
|
-
import { isAbsolute, relative } from "path";
|
|
18
|
+
import { isAbsolute, normalize, relative } from "path";
|
|
19
19
|
import { homedir } from "os";
|
|
20
20
|
import type { ReportFormat } from "./review-report";
|
|
21
21
|
|
|
@@ -137,11 +137,15 @@ export class ReviewContextBuilder {
|
|
|
137
137
|
specSources.push(...reviewConf.references);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
const normalizedFiles = this.normalizeFilePaths(options.files);
|
|
141
|
+
|
|
140
142
|
// 解析本地模式:非 CI、非 PR、无 base/head 时默认启用 uncommitted 模式
|
|
143
|
+
// 当显式指定 files 时,强制走“按文件审查模式”,不进入本地未提交模式
|
|
141
144
|
const localMode = this.resolveLocalMode(options, {
|
|
142
145
|
ci: options.ci,
|
|
143
146
|
hasPrNumber: !!prNumber,
|
|
144
147
|
hasBaseHead: !!(options.base || options.head),
|
|
148
|
+
hasFiles: !!normalizedFiles?.length,
|
|
145
149
|
});
|
|
146
150
|
|
|
147
151
|
// 当没有 PR 且没有指定 base/head 且不是本地模式时,自动获取默认值
|
|
@@ -175,7 +179,7 @@ export class ReviewContextBuilder {
|
|
|
175
179
|
includes: ctxIncludes,
|
|
176
180
|
whenModifiedCode: options.whenModifiedCode ?? reviewConf.whenModifiedCode,
|
|
177
181
|
llmMode: options.llmMode ?? titleOptions.llmMode ?? reviewConf.llmMode,
|
|
178
|
-
files:
|
|
182
|
+
files: normalizedFiles,
|
|
179
183
|
commits: options.commits,
|
|
180
184
|
verifyFixes:
|
|
181
185
|
options.verifyFixes ?? titleOptions.verifyFixes ?? reviewConf.verifyFixes ?? true,
|
|
@@ -219,8 +223,12 @@ export class ReviewContextBuilder {
|
|
|
219
223
|
*/
|
|
220
224
|
resolveLocalMode(
|
|
221
225
|
options: ReviewOptions,
|
|
222
|
-
env: { ci: boolean; hasPrNumber: boolean; hasBaseHead: boolean },
|
|
226
|
+
env: { ci: boolean; hasPrNumber: boolean; hasBaseHead: boolean; hasFiles: boolean },
|
|
223
227
|
): "uncommitted" | "staged" | false {
|
|
228
|
+
// 显式指定了 files,优先进入按文件审查模式
|
|
229
|
+
if (env.hasFiles) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
224
232
|
// 显式指定了 --no-local
|
|
225
233
|
if (options.local === false) {
|
|
226
234
|
return false;
|
|
@@ -249,13 +257,19 @@ export class ReviewContextBuilder {
|
|
|
249
257
|
if (!files || files.length === 0) return files;
|
|
250
258
|
|
|
251
259
|
const cwd = process.cwd();
|
|
252
|
-
return files.map((file) =>
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
260
|
+
return files.map((file) => this.normalizeSingleFilePath(file, cwd));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 规范化单个文件路径为仓库相对路径:
|
|
265
|
+
* - 绝对路径转相对路径
|
|
266
|
+
* - 统一分隔符为 /
|
|
267
|
+
* - 移除前导 ./
|
|
268
|
+
*/
|
|
269
|
+
private normalizeSingleFilePath(file: string, cwd: string): string {
|
|
270
|
+
const normalizedInput = normalize(file);
|
|
271
|
+
const relativePath = isAbsolute(normalizedInput) ? relative(cwd, normalizedInput) : normalizedInput;
|
|
272
|
+
return relativePath.replaceAll("\\", "/").replace(/^\.\/+/, "");
|
|
259
273
|
}
|
|
260
274
|
|
|
261
275
|
/**
|
|
@@ -127,6 +127,10 @@ describe("ReviewService", () => {
|
|
|
127
127
|
parseDiffText: vi.fn().mockReturnValue([]),
|
|
128
128
|
getRemoteUrl: vi.fn().mockReturnValue(null),
|
|
129
129
|
parseRepositoryFromRemoteUrl: vi.fn().mockReturnValue(null),
|
|
130
|
+
getUncommittedFiles: vi.fn().mockReturnValue([]),
|
|
131
|
+
getStagedFiles: vi.fn().mockReturnValue([]),
|
|
132
|
+
getUncommittedDiff: vi.fn().mockReturnValue([]),
|
|
133
|
+
getStagedDiff: vi.fn().mockReturnValue([]),
|
|
130
134
|
getChangedFilesBetweenRefs: vi.fn().mockResolvedValue([]),
|
|
131
135
|
getCommitsBetweenRefs: vi.fn().mockResolvedValue([]),
|
|
132
136
|
getDiffBetweenRefs: vi.fn().mockResolvedValue([]),
|
|
@@ -419,6 +423,26 @@ describe("ReviewService", () => {
|
|
|
419
423
|
expect(result.success).toBe(true);
|
|
420
424
|
});
|
|
421
425
|
|
|
426
|
+
it("should ignore whenModifiedCode in direct file mode", async () => {
|
|
427
|
+
const context: ReviewContext = {
|
|
428
|
+
owner: "owner",
|
|
429
|
+
repo: "repo",
|
|
430
|
+
files: ["src/app.ts"],
|
|
431
|
+
specSources: ["/spec/dir"],
|
|
432
|
+
dryRun: true,
|
|
433
|
+
ci: false,
|
|
434
|
+
llmMode: "openai",
|
|
435
|
+
whenModifiedCode: ["function", "class"],
|
|
436
|
+
};
|
|
437
|
+
const buildReviewPromptSpy = vi.spyOn(service as any, "buildReviewPrompt");
|
|
438
|
+
|
|
439
|
+
const result = await service.execute(context);
|
|
440
|
+
|
|
441
|
+
expect(result.success).toBe(true);
|
|
442
|
+
expect(buildReviewPromptSpy).toHaveBeenCalled();
|
|
443
|
+
expect(buildReviewPromptSpy.mock.calls[0][5]).toBeUndefined();
|
|
444
|
+
});
|
|
445
|
+
|
|
422
446
|
it("should filter files by includes pattern", async () => {
|
|
423
447
|
const context: ReviewContext = {
|
|
424
448
|
owner: "owner",
|
|
@@ -1363,14 +1387,28 @@ describe("ReviewService", () => {
|
|
|
1363
1387
|
|
|
1364
1388
|
it("should normalize absolute file paths", async () => {
|
|
1365
1389
|
configService.get.mockReturnValue({ repository: "owner/repo", refName: "main" });
|
|
1390
|
+
const absPath = `${process.cwd()}/src/file.ts`;
|
|
1366
1391
|
const options = {
|
|
1367
1392
|
dryRun: false,
|
|
1368
1393
|
ci: false,
|
|
1369
|
-
files: [
|
|
1394
|
+
files: [absPath, "./relative.ts"],
|
|
1370
1395
|
};
|
|
1371
1396
|
const context = await service.getContextFromEnv(options as any);
|
|
1372
1397
|
expect(context.files).toBeDefined();
|
|
1373
|
-
expect(context.files
|
|
1398
|
+
expect(context.files).toEqual(["src/file.ts", "relative.ts"]);
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
it("should force direct file mode when files are specified", async () => {
|
|
1402
|
+
configService.get.mockReturnValue({ repository: "owner/repo", refName: "main" });
|
|
1403
|
+
const options = {
|
|
1404
|
+
dryRun: false,
|
|
1405
|
+
ci: false,
|
|
1406
|
+
local: "uncommitted" as const,
|
|
1407
|
+
files: ["./miniprogram/utils/asyncSharedUtilsLoader.js"],
|
|
1408
|
+
};
|
|
1409
|
+
const context = await service.getContextFromEnv(options as any);
|
|
1410
|
+
expect(context.localMode).toBe(false);
|
|
1411
|
+
expect(context.files).toEqual(["miniprogram/utils/asyncSharedUtilsLoader.js"]);
|
|
1374
1412
|
});
|
|
1375
1413
|
|
|
1376
1414
|
it("should auto-detect base/head with verbose logging", async () => {
|
|
@@ -1429,6 +1467,30 @@ describe("ReviewService", () => {
|
|
|
1429
1467
|
});
|
|
1430
1468
|
});
|
|
1431
1469
|
|
|
1470
|
+
describe("ReviewService.resolveSourceData - direct file mode", () => {
|
|
1471
|
+
it("should bypass local uncommitted scanning when files are specified", async () => {
|
|
1472
|
+
const context: ReviewContext = {
|
|
1473
|
+
owner: "o",
|
|
1474
|
+
repo: "r",
|
|
1475
|
+
dryRun: true,
|
|
1476
|
+
ci: false,
|
|
1477
|
+
specSources: ["/spec"],
|
|
1478
|
+
files: ["miniprogram/utils/asyncSharedUtilsLoader.js"],
|
|
1479
|
+
localMode: false,
|
|
1480
|
+
};
|
|
1481
|
+
|
|
1482
|
+
const result = await (service as any).resolveSourceData(context);
|
|
1483
|
+
|
|
1484
|
+
expect(result.isDirectFileMode).toBe(true);
|
|
1485
|
+
expect(result.isLocalMode).toBe(true);
|
|
1486
|
+
expect(result.changedFiles).toEqual([
|
|
1487
|
+
{ filename: "miniprogram/utils/asyncSharedUtilsLoader.js", status: "modified" },
|
|
1488
|
+
]);
|
|
1489
|
+
expect(mockGitSdkService.getUncommittedFiles).not.toHaveBeenCalled();
|
|
1490
|
+
expect(mockGitSdkService.getStagedFiles).not.toHaveBeenCalled();
|
|
1491
|
+
});
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1432
1494
|
describe("ReviewService.getFilesForCommit", () => {
|
|
1433
1495
|
it("should return files from git sdk", async () => {
|
|
1434
1496
|
mockGitSdkService.getFilesForCommit.mockResolvedValue([
|
package/src/review.service.ts
CHANGED
|
@@ -97,6 +97,10 @@ export class ReviewService {
|
|
|
97
97
|
if (source.earlyReturn) return source.earlyReturn;
|
|
98
98
|
|
|
99
99
|
const { prModel, commits, changedFiles, headSha, isDirectFileMode } = source;
|
|
100
|
+
const effectiveWhenModifiedCode = isDirectFileMode ? undefined : context.whenModifiedCode;
|
|
101
|
+
if (isDirectFileMode && context.whenModifiedCode?.length && shouldLog(verbose, 1)) {
|
|
102
|
+
console.log(`ℹ️ 直接文件模式下忽略 whenModifiedCode 过滤`);
|
|
103
|
+
}
|
|
100
104
|
|
|
101
105
|
// 2. 规则匹配
|
|
102
106
|
const specs = await this.issueFilter.loadSpecs(specSources, verbose);
|
|
@@ -147,7 +151,7 @@ export class ReviewService {
|
|
|
147
151
|
fileContents,
|
|
148
152
|
commits,
|
|
149
153
|
existingResultModel?.result ?? null,
|
|
150
|
-
|
|
154
|
+
effectiveWhenModifiedCode,
|
|
151
155
|
verbose,
|
|
152
156
|
context.systemRules,
|
|
153
157
|
);
|
|
@@ -234,7 +238,7 @@ export class ReviewService {
|
|
|
234
238
|
duplicateWorkflowResolved,
|
|
235
239
|
} = context;
|
|
236
240
|
|
|
237
|
-
const isDirectFileMode = !!(files && files.length > 0 &&
|
|
241
|
+
const isDirectFileMode = !!(files && files.length > 0 && !prNumber);
|
|
238
242
|
let isLocalMode = !!localMode;
|
|
239
243
|
let effectiveBaseRef = baseRef;
|
|
240
244
|
let effectiveHeadRef = headRef;
|
|
@@ -294,8 +298,16 @@ export class ReviewService {
|
|
|
294
298
|
}
|
|
295
299
|
}
|
|
296
300
|
|
|
301
|
+
// 直接文件审查模式(-f):绕过 diff,直接按指定文件构造审查输入
|
|
302
|
+
if (isDirectFileMode) {
|
|
303
|
+
if (shouldLog(verbose, 1)) {
|
|
304
|
+
console.log(`📥 直接审查指定文件模式 (${files!.length} 个文件)`);
|
|
305
|
+
}
|
|
306
|
+
changedFiles = files!.map((f) => ({ filename: f, status: "modified" as const }));
|
|
307
|
+
isLocalMode = true;
|
|
308
|
+
}
|
|
297
309
|
// PR 模式、分支比较模式、或本地模式回退后的分支比较
|
|
298
|
-
if (prNumber) {
|
|
310
|
+
else if (prNumber) {
|
|
299
311
|
if (shouldLog(verbose, 1)) {
|
|
300
312
|
console.log(`📥 获取 PR #${prNumber} 信息 (owner: ${owner}, repo: ${repo})`);
|
|
301
313
|
}
|
|
@@ -330,12 +342,7 @@ export class ReviewService {
|
|
|
330
342
|
}
|
|
331
343
|
}
|
|
332
344
|
} else if (effectiveBaseRef && effectiveHeadRef) {
|
|
333
|
-
if (
|
|
334
|
-
if (shouldLog(verbose, 1)) {
|
|
335
|
-
console.log(`📥 直接审查指定文件模式 (${files.length} 个文件)`);
|
|
336
|
-
}
|
|
337
|
-
changedFiles = files.map((f) => ({ filename: f, status: "modified" as const }));
|
|
338
|
-
} else if (changedFiles.length === 0) {
|
|
345
|
+
if (changedFiles.length === 0) {
|
|
339
346
|
if (shouldLog(verbose, 1)) {
|
|
340
347
|
console.log(
|
|
341
348
|
`📥 获取 ${effectiveBaseRef}...${effectiveHeadRef} 的差异 (owner: ${owner}, repo: ${repo})`,
|