@spaceflow/review 0.76.0 → 0.78.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 +47 -0
- package/dist/index.js +3830 -2469
- package/package.json +2 -2
- package/src/deletion-impact.service.ts +17 -130
- package/src/index.ts +34 -2
- package/src/issue-verify.service.ts +18 -82
- package/src/locales/en/review.json +2 -1
- package/src/locales/zh-cn/review.json +2 -1
- package/src/mcp/index.ts +4 -1
- package/src/prompt/code-review.ts +95 -0
- package/src/prompt/deletion-impact.ts +105 -0
- package/src/prompt/index.ts +37 -0
- package/src/prompt/issue-verify.ts +86 -0
- package/src/prompt/pr-description.ts +149 -0
- package/src/prompt/schemas.ts +106 -0
- package/src/prompt/types.ts +53 -0
- package/src/pull-request-model.ts +236 -0
- package/src/review-context.ts +433 -0
- package/src/review-includes-filter.spec.ts +284 -0
- package/src/review-includes-filter.ts +196 -0
- package/src/review-issue-filter.ts +523 -0
- package/src/review-llm.ts +543 -0
- package/src/review-result-model.spec.ts +657 -0
- package/src/review-result-model.ts +1046 -0
- package/src/review-spec/review-spec.service.ts +26 -5
- package/src/review-spec/types.ts +2 -0
- package/src/review.config.ts +40 -5
- package/src/review.service.spec.ts +102 -1625
- package/src/review.service.ts +608 -2742
- package/src/system-rules/index.ts +48 -0
- package/src/system-rules/max-lines-per-file.ts +57 -0
- package/src/types/review-llm.ts +21 -0
- package/src/utils/review-llm.spec.ts +277 -0
- package/src/utils/review-llm.ts +177 -0
- package/src/utils/review-pr-comment.spec.ts +340 -0
- package/src/utils/review-pr-comment.ts +186 -0
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
parseIncludePattern,
|
|
4
|
+
filterFilesByIncludes,
|
|
5
|
+
extractGlobsFromIncludes,
|
|
6
|
+
extractCodeBlockTypes,
|
|
7
|
+
} from "./review-includes-filter";
|
|
8
|
+
|
|
9
|
+
describe("review-includes-filter", () => {
|
|
10
|
+
describe("parseIncludePattern", () => {
|
|
11
|
+
it("无分隔符时返回原始 glob,status 为 undefined", () => {
|
|
12
|
+
expect(parseIncludePattern("*/**/*.ts")).toEqual({ status: undefined, glob: "*/**/*.ts" });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("! 开头的排除模式直接返回,不解析前缀", () => {
|
|
16
|
+
expect(parseIncludePattern("!*/**/*.spec.ts")).toEqual({
|
|
17
|
+
status: undefined,
|
|
18
|
+
glob: "!*/**/*.spec.ts",
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("added| 前缀解析为 status=added", () => {
|
|
23
|
+
expect(parseIncludePattern("added|*/**/*.ts")).toEqual({
|
|
24
|
+
status: "added",
|
|
25
|
+
glob: "*/**/*.ts",
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("modified| 前缀解析为 status=modified", () => {
|
|
30
|
+
expect(parseIncludePattern("modified|*/**/*.ts")).toEqual({
|
|
31
|
+
status: "modified",
|
|
32
|
+
glob: "*/**/*.ts",
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("deleted| 前缀解析为 status=deleted", () => {
|
|
37
|
+
expect(parseIncludePattern("deleted|*/**/*.ts")).toEqual({
|
|
38
|
+
status: "deleted",
|
|
39
|
+
glob: "*/**/*.ts",
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("glob 部分可以带 ! 前缀(status 内排除语法)", () => {
|
|
44
|
+
expect(parseIncludePattern("added|!*/**/*.spec.ts")).toEqual({
|
|
45
|
+
status: "added",
|
|
46
|
+
glob: "!*/**/*.spec.ts",
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("无法识别的前缀当作普通 glob 处理(容错)", () => {
|
|
51
|
+
expect(parseIncludePattern("unknown|*/**/*.ts")).toEqual({
|
|
52
|
+
status: undefined,
|
|
53
|
+
glob: "unknown|*/**/*.ts",
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("extglob 中含 | 不被误识别为前缀分隔符", () => {
|
|
58
|
+
expect(parseIncludePattern("+(*.ts|*.js)")).toEqual({
|
|
59
|
+
status: undefined,
|
|
60
|
+
glob: "+(*.ts|*.js)",
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("前缀大小写不敏感", () => {
|
|
65
|
+
expect(parseIncludePattern("Added|*/**/*.ts")).toEqual({
|
|
66
|
+
status: "added",
|
|
67
|
+
glob: "*/**/*.ts",
|
|
68
|
+
});
|
|
69
|
+
expect(parseIncludePattern("MODIFIED|*/**/*.ts")).toEqual({
|
|
70
|
+
status: "modified",
|
|
71
|
+
glob: "*/**/*.ts",
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("平台别名:created 映射为 added", () => {
|
|
76
|
+
expect(parseIncludePattern("created|*/**/*.ts")).toEqual({
|
|
77
|
+
status: "added",
|
|
78
|
+
glob: "*/**/*.ts",
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("平台别名:removed 映射为 deleted", () => {
|
|
83
|
+
expect(parseIncludePattern("removed|*/**/*.ts")).toEqual({
|
|
84
|
+
status: "deleted",
|
|
85
|
+
glob: "*/**/*.ts",
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("平台别名:renamed 映射为 modified", () => {
|
|
90
|
+
expect(parseIncludePattern("renamed|*/**/*.ts")).toEqual({
|
|
91
|
+
status: "modified",
|
|
92
|
+
glob: "*/**/*.ts",
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("filterFilesByIncludes", () => {
|
|
98
|
+
const files = [
|
|
99
|
+
{ filename: "src/foo.ts", status: "added" },
|
|
100
|
+
{ filename: "src/foo.spec.ts", status: "added" },
|
|
101
|
+
{ filename: "src/bar.ts", status: "modified" },
|
|
102
|
+
{ filename: "src/bar.spec.ts", status: "modified" },
|
|
103
|
+
{ filename: "src/old.ts", status: "removed" },
|
|
104
|
+
{ filename: "src/old.spec.ts", status: "removed" },
|
|
105
|
+
];
|
|
106
|
+
const glob = "**/*.ts";
|
|
107
|
+
const specGlob = "**/*.spec.ts";
|
|
108
|
+
|
|
109
|
+
it("includes 为空时返回全部文件", () => {
|
|
110
|
+
expect(filterFilesByIncludes(files, [])).toEqual(files);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("无前缀 glob 不限 status,匹配所有 .ts 文件", () => {
|
|
114
|
+
const result = filterFilesByIncludes(files, [glob]);
|
|
115
|
+
expect(result.map((f) => f.filename)).toEqual([
|
|
116
|
+
"src/foo.ts",
|
|
117
|
+
"src/foo.spec.ts",
|
|
118
|
+
"src/bar.ts",
|
|
119
|
+
"src/bar.spec.ts",
|
|
120
|
+
"src/old.ts",
|
|
121
|
+
"src/old.spec.ts",
|
|
122
|
+
]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("排除模式过滤掉 spec 文件", () => {
|
|
126
|
+
const result = filterFilesByIncludes(files, [glob, `!${specGlob}`]);
|
|
127
|
+
expect(result.map((f) => f.filename)).toEqual(["src/foo.ts", "src/bar.ts", "src/old.ts"]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("排除模式优先于所有正向匹配", () => {
|
|
131
|
+
const result = filterFilesByIncludes(files, [`added|${glob}`, `!${specGlob}`]);
|
|
132
|
+
expect(result.map((f) => f.filename)).toEqual(["src/foo.ts"]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("added| 前缀只匹配 added 状态文件", () => {
|
|
136
|
+
const result = filterFilesByIncludes(files, [`added|${glob}`]);
|
|
137
|
+
expect(result.map((f) => f.filename)).toEqual(["src/foo.ts", "src/foo.spec.ts"]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("modified| 前缀只匹配 modified 状态文件", () => {
|
|
141
|
+
const result = filterFilesByIncludes(files, [`modified|${glob}`]);
|
|
142
|
+
expect(result.map((f) => f.filename)).toEqual(["src/bar.ts", "src/bar.spec.ts"]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("deleted| 前缀匹配 removed 状态文件(平台别名)", () => {
|
|
146
|
+
const result = filterFilesByIncludes(files, [`deleted|${glob}`]);
|
|
147
|
+
expect(result.map((f) => f.filename)).toEqual(["src/old.ts", "src/old.spec.ts"]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("多个 status 前缀之间是 OR 关系", () => {
|
|
151
|
+
const result = filterFilesByIncludes(files, [`added|${glob}`, `modified|${glob}`]);
|
|
152
|
+
expect(result.map((f) => f.filename)).toEqual([
|
|
153
|
+
"src/foo.ts",
|
|
154
|
+
"src/foo.spec.ts",
|
|
155
|
+
"src/bar.ts",
|
|
156
|
+
"src/bar.spec.ts",
|
|
157
|
+
]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("status 前缀内排除语法 added|!**/*.spec.ts", () => {
|
|
161
|
+
const result = filterFilesByIncludes(files, [`added|${glob}`, `added|!${specGlob}`]);
|
|
162
|
+
expect(result.map((f) => f.filename)).toEqual(["src/foo.ts"]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("status 内排除只影响对应 status,不影响其他 status 的匹配", () => {
|
|
166
|
+
const result = filterFilesByIncludes(files, [
|
|
167
|
+
`added|${glob}`,
|
|
168
|
+
`added|!${specGlob}`,
|
|
169
|
+
`modified|${glob}`,
|
|
170
|
+
]);
|
|
171
|
+
expect(result.map((f) => f.filename)).toEqual([
|
|
172
|
+
"src/foo.ts",
|
|
173
|
+
"src/bar.ts",
|
|
174
|
+
"src/bar.spec.ts",
|
|
175
|
+
]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("无前缀 glob 与 status 前缀混用时任一命中即保留", () => {
|
|
179
|
+
const result = filterFilesByIncludes(files, [glob, "added|**/*.vue"]);
|
|
180
|
+
expect(result.map((f) => f.filename)).toEqual([
|
|
181
|
+
"src/foo.ts",
|
|
182
|
+
"src/foo.spec.ts",
|
|
183
|
+
"src/bar.ts",
|
|
184
|
+
"src/bar.spec.ts",
|
|
185
|
+
"src/old.ts",
|
|
186
|
+
"src/old.spec.ts",
|
|
187
|
+
]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("status 未知的文件 fallback 为 modified", () => {
|
|
191
|
+
const unknownFiles = [{ filename: "src/foo.ts", status: undefined }];
|
|
192
|
+
const result = filterFilesByIncludes(unknownFiles, [`modified|${glob}`]);
|
|
193
|
+
expect(result.map((f) => f.filename)).toEqual(["src/foo.ts"]);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("status 未知的文件不被 added| 匹配", () => {
|
|
197
|
+
const unknownFiles = [{ filename: "src/foo.ts", status: undefined }];
|
|
198
|
+
const result = filterFilesByIncludes(unknownFiles, [`added|${glob}`]);
|
|
199
|
+
expect(result).toEqual([]);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("filename 为空的文件被过滤掉", () => {
|
|
203
|
+
const withEmpty = [
|
|
204
|
+
{ filename: "", status: "added" },
|
|
205
|
+
{ filename: "src/foo.ts", status: "added" },
|
|
206
|
+
];
|
|
207
|
+
const result = filterFilesByIncludes(withEmpty, [glob]);
|
|
208
|
+
expect(result.map((f) => f.filename)).toEqual(["src/foo.ts"]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("GitHub 平台 status=removed 被 deleted| 前缀匹配", () => {
|
|
212
|
+
const ghFiles = [{ filename: "src/old.ts", status: "removed" }];
|
|
213
|
+
expect(filterFilesByIncludes(ghFiles, [`deleted|${glob}`])).toHaveLength(1);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("GitLab 平台 status=deleted 被 deleted| 前缀匹配", () => {
|
|
217
|
+
const glFiles = [{ filename: "src/old.ts", status: "deleted" }];
|
|
218
|
+
expect(filterFilesByIncludes(glFiles, [`deleted|${glob}`])).toHaveLength(1);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("全量 diff 语义:文件在当前分支首次引入后被多次修改,status 仍为 added,added| 始终匹配", () => {
|
|
222
|
+
// 场景:a.ts 在 commit1 创建(status=added),在 commit2 修改
|
|
223
|
+
// 全量 diff(当前分支 vs base)时,compare API 返回的 status 仍为 added
|
|
224
|
+
// 因此 added|*.ts 在后续每次 review 中都能匹配到该文件
|
|
225
|
+
const diffFiles = [{ filename: "src/a.ts", status: "added" }];
|
|
226
|
+
const result = filterFilesByIncludes(diffFiles, [`added|${glob}`]);
|
|
227
|
+
expect(result.map((f) => f.filename)).toEqual(["src/a.ts"]);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe("extractGlobsFromIncludes", () => {
|
|
232
|
+
it("无前缀的 glob 原样返回", () => {
|
|
233
|
+
expect(extractGlobsFromIncludes(["**/*.ts", "!**/*.spec.ts"])).toEqual([
|
|
234
|
+
"**/*.ts",
|
|
235
|
+
"!**/*.spec.ts",
|
|
236
|
+
]);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("有 status 前缀的 pattern 去掉前缀只返回 glob 部分", () => {
|
|
240
|
+
expect(extractGlobsFromIncludes(["added|**/*.ts", "modified|**/*.vue"])).toEqual([
|
|
241
|
+
"**/*.ts",
|
|
242
|
+
"**/*.vue",
|
|
243
|
+
]);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("混合模式只提取 glob 部分", () => {
|
|
247
|
+
expect(extractGlobsFromIncludes(["**/*.ts", "added|**/*.vue", "!**/*.spec.ts"])).toEqual([
|
|
248
|
+
"**/*.ts",
|
|
249
|
+
"**/*.vue",
|
|
250
|
+
"!**/*.spec.ts",
|
|
251
|
+
]);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("空数组返回空数组", () => {
|
|
255
|
+
expect(extractGlobsFromIncludes([])).toEqual([]);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe("extractCodeBlockTypes", () => {
|
|
260
|
+
it("提取纯类型名", () => {
|
|
261
|
+
const result = extractCodeBlockTypes(["function", "class"]);
|
|
262
|
+
expect(result).toEqual(["function", "class"]);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("status|code-* 语法不被识别,返回空", () => {
|
|
266
|
+
const result = extractCodeBlockTypes(["added|code-function", "added|code-class"]);
|
|
267
|
+
expect(result).toEqual([]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("混合:纯类型名保留,status 前缀语法忽略", () => {
|
|
271
|
+
const result = extractCodeBlockTypes(["added|code-function", "class"]);
|
|
272
|
+
expect(result).toEqual(["class"]);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("去重:同一类型出现多次只返回一次", () => {
|
|
276
|
+
const result = extractCodeBlockTypes(["function", "function"]);
|
|
277
|
+
expect(result).toEqual(["function"]);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("空数组返回空数组", () => {
|
|
281
|
+
expect(extractCodeBlockTypes([])).toEqual([]);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
});
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import micromatch from "micromatch";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* includes 模式中的变更类型前缀
|
|
5
|
+
*
|
|
6
|
+
* 语法:`<status>|<glob>`,例如:
|
|
7
|
+
* - `added|*\/**\/*.ts` → 仅匹配新增文件
|
|
8
|
+
* - `modified|*\/**\/*.ts` → 仅匹配修改文件
|
|
9
|
+
* - `deleted|*\/**\/*.ts` → 仅匹配删除文件
|
|
10
|
+
* - `*\/**\/*.ts` → 不限变更类型(原有行为)
|
|
11
|
+
*
|
|
12
|
+
* ## 全量 diff 语义说明
|
|
13
|
+
*
|
|
14
|
+
* 每次 review 是对当前分支与 base 分支(develop/master)的**全量 diff**,
|
|
15
|
+
* 文件的 status 由平台 compare API 给出,表示该文件**相对 base 分支**的变更类型。
|
|
16
|
+
*
|
|
17
|
+
* 因此,若 `a.ts` 是在当前分支上首次引入的(相对 base 不存在),
|
|
18
|
+
* 无论后续经过多少次 commit 修改,其 status **始终为 `added`**。
|
|
19
|
+
* 这意味着 `added|*.ts` 在后续每次 review 中都会继续匹配该文件,
|
|
20
|
+
* 这是符合预期的行为——只要文件相对 base 是"新建"的,`added|` 就应该始终生效。
|
|
21
|
+
*/
|
|
22
|
+
export type IncludeStatusPrefix = "added" | "modified" | "deleted";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 代码结构类型,用于 whenModifiedCode 配置
|
|
26
|
+
*/
|
|
27
|
+
export type CodeBlockType = "function" | "class" | "interface" | "type" | "method";
|
|
28
|
+
|
|
29
|
+
export const CODE_BLOCK_TYPES: CodeBlockType[] = [
|
|
30
|
+
"function",
|
|
31
|
+
"class",
|
|
32
|
+
"interface",
|
|
33
|
+
"type",
|
|
34
|
+
"method",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/** status 值到前缀的映射(兼容 GitHub/GitLab/Gitea 各平台) */
|
|
38
|
+
const STATUS_ALIAS: Record<string, IncludeStatusPrefix> = {
|
|
39
|
+
added: "added",
|
|
40
|
+
created: "added",
|
|
41
|
+
renamed: "modified",
|
|
42
|
+
modified: "modified",
|
|
43
|
+
changed: "modified",
|
|
44
|
+
removed: "deleted",
|
|
45
|
+
deleted: "deleted",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export interface ParsedIncludePattern {
|
|
49
|
+
/** 变更类型前缀,undefined 表示不限类型 */
|
|
50
|
+
status: IncludeStatusPrefix | undefined;
|
|
51
|
+
/** 去掉前缀后的 glob 模式 */
|
|
52
|
+
glob: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 解析单条 include 模式,拆分 status 前缀和 glob。
|
|
57
|
+
*
|
|
58
|
+
* 只有当 `|` 前面的部分是已知 status 关键字时才视为前缀,否则当作普通 glob 处理(容错),
|
|
59
|
+
* 这样可以避免误解析 extglob 语法中含 `|` 的模式(如 `+(*.ts|*.js)`)。
|
|
60
|
+
* 排除模式(以 `!` 开头)始终作为普通 glob 处理。
|
|
61
|
+
*/
|
|
62
|
+
export function parseIncludePattern(pattern: string): ParsedIncludePattern {
|
|
63
|
+
if (pattern.startsWith("!")) {
|
|
64
|
+
return { status: undefined, glob: pattern };
|
|
65
|
+
}
|
|
66
|
+
const separatorIndex = pattern.indexOf("|");
|
|
67
|
+
if (separatorIndex === -1) {
|
|
68
|
+
return { status: undefined, glob: pattern };
|
|
69
|
+
}
|
|
70
|
+
const prefix = pattern.slice(0, separatorIndex).trim().toLowerCase();
|
|
71
|
+
const glob = pattern.slice(separatorIndex + 1).trim();
|
|
72
|
+
const status = STATUS_ALIAS[prefix] as IncludeStatusPrefix | undefined;
|
|
73
|
+
if (!status) {
|
|
74
|
+
// 前缀无法识别(如 extglob 中的 `|`),当作普通 glob 处理
|
|
75
|
+
return { status: undefined, glob: pattern };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { status, glob };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface FileWithStatus {
|
|
82
|
+
filename?: string;
|
|
83
|
+
status?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 根据 includes 模式列表过滤文件,支持 `status|glob` 前缀语法。
|
|
88
|
+
*
|
|
89
|
+
* 算法:
|
|
90
|
+
* 1. 将 includes 拆分为:排除模式(`!`)、无前缀正向 glob、有 status 前缀 glob
|
|
91
|
+
* 2. 每个文件先检查是否命中任意正向条件(无前缀 glob 或匹配 status 的前缀 glob)
|
|
92
|
+
* 3. 最后用排除模式做全局过滤(排除模式始终优先)
|
|
93
|
+
*
|
|
94
|
+
* @param files 待过滤的文件列表
|
|
95
|
+
* @param includes include 模式列表,支持 `added|*.ts`、`modified|*.ts`、`deleted|*.ts` 前缀
|
|
96
|
+
* @returns 匹配的文件列表
|
|
97
|
+
*/
|
|
98
|
+
export function filterFilesByIncludes<T extends FileWithStatus>(
|
|
99
|
+
files: T[],
|
|
100
|
+
includes: string[],
|
|
101
|
+
): T[] {
|
|
102
|
+
if (!includes || includes.length === 0) return files;
|
|
103
|
+
|
|
104
|
+
const parsed = includes.map(parseIncludePattern);
|
|
105
|
+
console.log(`[filterFilesByIncludes] parsed patterns=${JSON.stringify(parsed)}`);
|
|
106
|
+
|
|
107
|
+
// 排除模式(以 ! 开头),用于最终全局过滤
|
|
108
|
+
const negativeGlobs = parsed
|
|
109
|
+
.filter((p) => p.status === undefined && p.glob.startsWith("!"))
|
|
110
|
+
.map((p) => p.glob.slice(1)); // 去掉 ! 前缀,用 micromatch.not 处理
|
|
111
|
+
// 无前缀的正向 globs
|
|
112
|
+
const plainGlobs = parsed
|
|
113
|
+
.filter((p) => p.status === undefined && !p.glob.startsWith("!"))
|
|
114
|
+
.map((p) => p.glob);
|
|
115
|
+
// 有 status 前缀的 patterns
|
|
116
|
+
const statusPatterns = parsed.filter((p) => p.status !== undefined);
|
|
117
|
+
|
|
118
|
+
console.log(
|
|
119
|
+
`[filterFilesByIncludes] negativeGlobs=${JSON.stringify(negativeGlobs)}, plainGlobs=${JSON.stringify(plainGlobs)}, statusPatterns=${JSON.stringify(statusPatterns)}`,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
return files.filter((file) => {
|
|
123
|
+
const filename = file.filename ?? "";
|
|
124
|
+
const fileStatus = STATUS_ALIAS[file.status?.toLowerCase() ?? ""] ?? "modified";
|
|
125
|
+
if (!filename) return false;
|
|
126
|
+
|
|
127
|
+
// 最终排除:命中排除模式的文件直接过滤掉
|
|
128
|
+
if (
|
|
129
|
+
negativeGlobs.length > 0 &&
|
|
130
|
+
micromatch.isMatch(filename, negativeGlobs, { matchBase: true })
|
|
131
|
+
) {
|
|
132
|
+
console.log(`[filterFilesByIncludes] ${filename} excluded by negativeGlobs`);
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 正向匹配:无前缀 glob
|
|
137
|
+
if (plainGlobs.length > 0 && micromatch.isMatch(filename, plainGlobs, { matchBase: true })) {
|
|
138
|
+
console.log(`[filterFilesByIncludes] ${filename} matched plainGlobs`);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 正向匹配:有 status 前缀的 glob,按文件实际 status 过滤
|
|
143
|
+
// glob 可以带 ! 前缀表示在该 status 范围内排除,如 added|!**/*.spec.ts
|
|
144
|
+
if (statusPatterns.length > 0) {
|
|
145
|
+
// 按 status 分组,每组内正向 glob + 排除 glob 合并后批量匹配
|
|
146
|
+
const matchingStatusGlobs = statusPatterns
|
|
147
|
+
.filter(({ status }) => status === fileStatus)
|
|
148
|
+
.map(({ glob }) => glob);
|
|
149
|
+
if (matchingStatusGlobs.length > 0) {
|
|
150
|
+
// 有正向 glob 才有意义,纯排除 glob 组合 micromatch 会视为全匹配再排除
|
|
151
|
+
const positiveGlobs = matchingStatusGlobs.filter((g) => !g.startsWith("!"));
|
|
152
|
+
const negativeStatusGlobs = matchingStatusGlobs
|
|
153
|
+
.filter((g) => g.startsWith("!"))
|
|
154
|
+
.map((g) => g.slice(1));
|
|
155
|
+
if (positiveGlobs.length > 0) {
|
|
156
|
+
const matchesPositive = micromatch.isMatch(filename, positiveGlobs, { matchBase: true });
|
|
157
|
+
const matchesNegative =
|
|
158
|
+
negativeStatusGlobs.length > 0 &&
|
|
159
|
+
micromatch.isMatch(filename, negativeStatusGlobs, { matchBase: true });
|
|
160
|
+
if (matchesPositive && !matchesNegative) {
|
|
161
|
+
console.log(
|
|
162
|
+
`[filterFilesByIncludes] ${filename} (status=${fileStatus}) matched statusPatterns`,
|
|
163
|
+
);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log(`[filterFilesByIncludes] ${filename} (status=${fileStatus}) NOT matched`);
|
|
171
|
+
return false;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 从 includes 模式列表中提取纯 glob(用于 commit 过滤,commit 没有 status 概念)。
|
|
177
|
+
* 带 status 前缀的模式会去掉前缀,仅保留 glob 部分。
|
|
178
|
+
*/
|
|
179
|
+
export function extractGlobsFromIncludes(includes: string[]): string[] {
|
|
180
|
+
return includes.map((p) => parseIncludePattern(p).glob).filter((g) => g.length > 0);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 从 whenModifiedCode 配置中解析代码结构过滤类型。
|
|
185
|
+
* 只接受简单的类型名称,如 "function"、"class"、"interface"、"type"、"method"
|
|
186
|
+
*/
|
|
187
|
+
export function extractCodeBlockTypes(whenModifiedCode: string[]): CodeBlockType[] {
|
|
188
|
+
const types = new Set<CodeBlockType>();
|
|
189
|
+
for (const entry of whenModifiedCode) {
|
|
190
|
+
const trimmed = entry.trim();
|
|
191
|
+
if ((CODE_BLOCK_TYPES as string[]).includes(trimmed)) {
|
|
192
|
+
types.add(trimmed as CodeBlockType);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return [...types];
|
|
196
|
+
}
|