@spaceflow/review 0.29.1
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 +533 -0
- package/README.md +124 -0
- package/dist/551.js +9 -0
- package/dist/index.js +5704 -0
- package/package.json +50 -0
- package/src/README.md +364 -0
- package/src/__mocks__/@anthropic-ai/claude-agent-sdk.js +3 -0
- package/src/__mocks__/json-stringify-pretty-compact.ts +4 -0
- package/src/deletion-impact.service.spec.ts +974 -0
- package/src/deletion-impact.service.ts +879 -0
- package/src/dto/mcp.dto.ts +42 -0
- package/src/index.ts +32 -0
- package/src/issue-verify.service.spec.ts +460 -0
- package/src/issue-verify.service.ts +309 -0
- package/src/locales/en/review.json +31 -0
- package/src/locales/index.ts +11 -0
- package/src/locales/zh-cn/review.json +31 -0
- package/src/parse-title-options.spec.ts +251 -0
- package/src/parse-title-options.ts +185 -0
- package/src/review-report/formatters/deletion-impact.formatter.ts +144 -0
- package/src/review-report/formatters/index.ts +4 -0
- package/src/review-report/formatters/json.formatter.ts +8 -0
- package/src/review-report/formatters/markdown.formatter.ts +291 -0
- package/src/review-report/formatters/terminal.formatter.ts +130 -0
- package/src/review-report/index.ts +4 -0
- package/src/review-report/review-report.module.ts +8 -0
- package/src/review-report/review-report.service.ts +58 -0
- package/src/review-report/types.ts +26 -0
- package/src/review-spec/index.ts +3 -0
- package/src/review-spec/review-spec.module.ts +10 -0
- package/src/review-spec/review-spec.service.spec.ts +1543 -0
- package/src/review-spec/review-spec.service.ts +902 -0
- package/src/review-spec/types.ts +143 -0
- package/src/review.command.ts +244 -0
- package/src/review.config.ts +58 -0
- package/src/review.mcp.ts +184 -0
- package/src/review.module.ts +52 -0
- package/src/review.service.spec.ts +3007 -0
- package/src/review.service.ts +2603 -0
- package/tsconfig.json +8 -0
- package/vitest.config.ts +34 -0
|
@@ -0,0 +1,879 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Injectable,
|
|
3
|
+
LlmProxyService,
|
|
4
|
+
logStreamEvent,
|
|
5
|
+
createStreamLoggerState,
|
|
6
|
+
type LLMMode,
|
|
7
|
+
type VerboseLevel,
|
|
8
|
+
shouldLog,
|
|
9
|
+
type LlmJsonPutSchema,
|
|
10
|
+
LlmJsonPut,
|
|
11
|
+
GitProviderService,
|
|
12
|
+
ChangedFile,
|
|
13
|
+
} from "@spaceflow/core";
|
|
14
|
+
import micromatch from "micromatch";
|
|
15
|
+
import type { DeletionImpactResult } from "./review-spec";
|
|
16
|
+
import { spawn } from "child_process";
|
|
17
|
+
|
|
18
|
+
export interface DeletedCodeBlock {
|
|
19
|
+
file: string;
|
|
20
|
+
startLine: number;
|
|
21
|
+
endLine: number;
|
|
22
|
+
content: string;
|
|
23
|
+
commit?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DELETION_IMPACT_SCHEMA: LlmJsonPutSchema = {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
impacts: {
|
|
30
|
+
type: "array",
|
|
31
|
+
items: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
file: { type: "string", description: "被删除代码所在的文件路径" },
|
|
35
|
+
deletedCode: { type: "string", description: "被删除的代码片段摘要(前50字符)" },
|
|
36
|
+
riskLevel: {
|
|
37
|
+
type: "string",
|
|
38
|
+
enum: ["high", "medium", "low", "none"],
|
|
39
|
+
description:
|
|
40
|
+
"风险等级:high=可能导致功能异常,medium=可能影响部分功能,low=影响较小,none=无影响",
|
|
41
|
+
},
|
|
42
|
+
affectedFiles: {
|
|
43
|
+
type: "array",
|
|
44
|
+
items: { type: "string" },
|
|
45
|
+
description: "可能受影响的文件列表",
|
|
46
|
+
},
|
|
47
|
+
reason: { type: "string", description: "影响分析的详细说明" },
|
|
48
|
+
suggestion: { type: "string", description: "建议的处理方式" },
|
|
49
|
+
},
|
|
50
|
+
required: ["file", "deletedCode", "riskLevel", "affectedFiles", "reason"],
|
|
51
|
+
additionalProperties: false,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
summary: { type: "string", description: "删除代码影响的整体总结" },
|
|
55
|
+
},
|
|
56
|
+
required: ["impacts", "summary"],
|
|
57
|
+
additionalProperties: false,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type DeletionDiffSource = "provider-api" | "git-diff";
|
|
61
|
+
|
|
62
|
+
export interface DeletionAnalysisContext {
|
|
63
|
+
owner?: string;
|
|
64
|
+
repo?: string;
|
|
65
|
+
prNumber?: number;
|
|
66
|
+
baseRef?: string;
|
|
67
|
+
headRef?: string;
|
|
68
|
+
/** diff 来源:provider-api 使用 Git Provider API,git-diff 使用本地 git 命令(两点语法) */
|
|
69
|
+
diffSource?: DeletionDiffSource;
|
|
70
|
+
/** 分析模式:openai 使用标准模式,claude-agent 使用 Agent 模式(可使用工具) */
|
|
71
|
+
analysisMode?: LLMMode;
|
|
72
|
+
/** 文件过滤 glob 模式,与 review.includes 一致 */
|
|
73
|
+
includes?: string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@Injectable()
|
|
77
|
+
export class DeletionImpactService {
|
|
78
|
+
constructor(
|
|
79
|
+
protected readonly llmProxyService: LlmProxyService,
|
|
80
|
+
protected readonly gitProvider: GitProviderService,
|
|
81
|
+
) {}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 分析删除代码的影响
|
|
85
|
+
*/
|
|
86
|
+
async analyzeDeletionImpact(
|
|
87
|
+
context: DeletionAnalysisContext,
|
|
88
|
+
llmMode: LLMMode,
|
|
89
|
+
verbose?: VerboseLevel,
|
|
90
|
+
): Promise<DeletionImpactResult> {
|
|
91
|
+
if (shouldLog(verbose, 1)) {
|
|
92
|
+
console.log(`\n🔍 开始分析删除代码的影响...`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 1. 获取删除的代码块
|
|
96
|
+
let deletedBlocks: DeletedCodeBlock[];
|
|
97
|
+
const diffSource = context.diffSource ?? (context.prNumber ? "provider-api" : "git-diff");
|
|
98
|
+
|
|
99
|
+
if (diffSource === "provider-api" && context.prNumber && context.owner && context.repo) {
|
|
100
|
+
// Git Provider API 模式:使用 API 获取 diff
|
|
101
|
+
if (shouldLog(verbose, 1)) {
|
|
102
|
+
console.log(` 📡 使用 Git Provider API 获取 PR #${context.prNumber} 的 diff`);
|
|
103
|
+
}
|
|
104
|
+
const changedFiles = await this.gitProvider.getPullRequestFiles(
|
|
105
|
+
context.owner,
|
|
106
|
+
context.repo,
|
|
107
|
+
context.prNumber,
|
|
108
|
+
);
|
|
109
|
+
// 检查 changedFiles 是否包含 patch 字段
|
|
110
|
+
const filesWithPatch = changedFiles.filter((f) => f.patch);
|
|
111
|
+
const filesWithDeletions = changedFiles.filter((f) => f.deletions && f.deletions > 0);
|
|
112
|
+
if (shouldLog(verbose, 1)) {
|
|
113
|
+
console.log(
|
|
114
|
+
` 📊 共 ${changedFiles.length} 个文件, ${filesWithPatch.length} 个有 patch, ${filesWithDeletions.length} 个有删除`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (filesWithPatch.length > 0) {
|
|
119
|
+
// 有 patch 字段,直接解析
|
|
120
|
+
deletedBlocks = this.extractDeletedBlocksFromChangedFiles(changedFiles);
|
|
121
|
+
} else if (filesWithDeletions.length > 0) {
|
|
122
|
+
// 没有 patch 字段但有删除,使用 PR diff API
|
|
123
|
+
if (shouldLog(verbose, 1)) {
|
|
124
|
+
console.log(` ⚠️ API 未返回 patch 字段,使用 PR diff API`);
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const diffText = await this.gitProvider.getPullRequestDiff(
|
|
128
|
+
context.owner,
|
|
129
|
+
context.repo,
|
|
130
|
+
context.prNumber,
|
|
131
|
+
);
|
|
132
|
+
deletedBlocks = this.extractDeletedBlocksFromDiffText(diffText);
|
|
133
|
+
} catch (e) {
|
|
134
|
+
if (shouldLog(verbose, 1)) {
|
|
135
|
+
console.log(` ❌ PR diff API 失败: ${e instanceof Error ? e.message : String(e)}`);
|
|
136
|
+
}
|
|
137
|
+
deletedBlocks = [];
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
// 没有删除的文件
|
|
141
|
+
deletedBlocks = [];
|
|
142
|
+
}
|
|
143
|
+
} else if (context.baseRef && context.headRef) {
|
|
144
|
+
// Git Diff 模式:使用本地 git 命令(两点语法)
|
|
145
|
+
if (shouldLog(verbose, 1)) {
|
|
146
|
+
console.log(` 💻 使用 Git Diff 获取 ${context.baseRef}..${context.headRef} 的差异`);
|
|
147
|
+
}
|
|
148
|
+
deletedBlocks = await this.getDeletedCodeBlocks(context.baseRef, context.headRef, verbose);
|
|
149
|
+
} else {
|
|
150
|
+
if (shouldLog(verbose, 1)) {
|
|
151
|
+
console.log(` ⚠️ 缺少必要参数,跳过删除分析`);
|
|
152
|
+
}
|
|
153
|
+
return { impacts: [], summary: "缺少必要参数" };
|
|
154
|
+
}
|
|
155
|
+
if (deletedBlocks.length === 0) {
|
|
156
|
+
if (shouldLog(verbose, 1)) {
|
|
157
|
+
console.log(` ✅ 没有发现删除的代码`);
|
|
158
|
+
}
|
|
159
|
+
return { impacts: [], summary: "没有发现删除的代码" };
|
|
160
|
+
}
|
|
161
|
+
if (shouldLog(verbose, 1)) {
|
|
162
|
+
console.log(` 📦 发现 ${deletedBlocks.length} 个删除的代码块`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 1.5 使用 includes 过滤文件
|
|
166
|
+
if (context.includes && context.includes.length > 0) {
|
|
167
|
+
const beforeCount = deletedBlocks.length;
|
|
168
|
+
const filenames = deletedBlocks.map((b) => b.file);
|
|
169
|
+
const matchedFilenames = micromatch(filenames, context.includes);
|
|
170
|
+
deletedBlocks = deletedBlocks.filter((b) => matchedFilenames.includes(b.file));
|
|
171
|
+
if (shouldLog(verbose, 1)) {
|
|
172
|
+
console.log(` 🔍 Includes 过滤: ${beforeCount} -> ${deletedBlocks.length} 个删除块`);
|
|
173
|
+
}
|
|
174
|
+
if (deletedBlocks.length === 0) {
|
|
175
|
+
return { impacts: [], summary: "过滤后没有需要分析的删除代码" };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 2. 获取删除代码的引用关系
|
|
180
|
+
const references = await this.findCodeReferences(deletedBlocks);
|
|
181
|
+
if (shouldLog(verbose, 1)) {
|
|
182
|
+
console.log(` 🔗 找到 ${references.size} 个文件可能引用了被删除的代码`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 3. 根据分析模式选择不同的分析方法
|
|
186
|
+
const analysisMode = context.analysisMode ?? "openai";
|
|
187
|
+
let result: DeletionImpactResult;
|
|
188
|
+
|
|
189
|
+
if (["claude-code", "open-code"].includes(analysisMode)) {
|
|
190
|
+
// Claude Agent 模式:使用工具主动探索代码库
|
|
191
|
+
result = await this.analyzeWithAgent(analysisMode, deletedBlocks, references, verbose);
|
|
192
|
+
} else {
|
|
193
|
+
// OpenAI 模式:标准 chat completion
|
|
194
|
+
result = await this.analyzeWithLLM(deletedBlocks, references, llmMode, verbose);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 防御性检查:确保 impacts 是数组
|
|
198
|
+
if (!result.impacts || !Array.isArray(result.impacts)) {
|
|
199
|
+
result.impacts = [];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const highRiskCount = result.impacts.filter((i) => i.riskLevel === "high").length;
|
|
203
|
+
const mediumRiskCount = result.impacts.filter((i) => i.riskLevel === "medium").length;
|
|
204
|
+
if (shouldLog(verbose, 1)) {
|
|
205
|
+
console.log(`\n📊 分析完成: ${highRiskCount} 个高风险, ${mediumRiskCount} 个中风险`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 从 Git Provider API 返回的 ChangedFile 中提取被删除的代码块
|
|
213
|
+
*/
|
|
214
|
+
protected extractDeletedBlocksFromChangedFiles(changedFiles: ChangedFile[]): DeletedCodeBlock[] {
|
|
215
|
+
const deletedBlocks: DeletedCodeBlock[] = [];
|
|
216
|
+
|
|
217
|
+
for (const file of changedFiles) {
|
|
218
|
+
if (!file.filename || !file.patch) continue;
|
|
219
|
+
|
|
220
|
+
const blocks = this.parseDeletedBlocksFromPatch(file.filename, file.patch);
|
|
221
|
+
deletedBlocks.push(...blocks);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 过滤掉空白行和注释行为主的删除块
|
|
225
|
+
return this.filterMeaningfulBlocks(deletedBlocks);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 从单个文件的 patch 中解析删除的代码块
|
|
230
|
+
*/
|
|
231
|
+
protected parseDeletedBlocksFromPatch(filename: string, patch: string): DeletedCodeBlock[] {
|
|
232
|
+
const deletedBlocks: DeletedCodeBlock[] = [];
|
|
233
|
+
const lines = patch.split("\n");
|
|
234
|
+
let currentDeleteBlock: { startLine: number; lines: string[] } | null = null;
|
|
235
|
+
|
|
236
|
+
for (const line of lines) {
|
|
237
|
+
// 解析 hunk header: @@ -oldStart,oldCount +newStart,newCount @@
|
|
238
|
+
const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+\d+(?:,\d+)? @@/);
|
|
239
|
+
if (hunkMatch) {
|
|
240
|
+
// 保存之前的删除块
|
|
241
|
+
if (currentDeleteBlock && currentDeleteBlock.lines.length > 0) {
|
|
242
|
+
deletedBlocks.push({
|
|
243
|
+
file: filename,
|
|
244
|
+
startLine: currentDeleteBlock.startLine,
|
|
245
|
+
endLine: currentDeleteBlock.startLine + currentDeleteBlock.lines.length - 1,
|
|
246
|
+
content: currentDeleteBlock.lines.join("\n"),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
currentDeleteBlock = {
|
|
250
|
+
startLine: parseInt(hunkMatch[1], 10),
|
|
251
|
+
lines: [],
|
|
252
|
+
};
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 删除的行(以 - 开头,但不是 ---)
|
|
257
|
+
if (line.startsWith("-") && !line.startsWith("---") && currentDeleteBlock) {
|
|
258
|
+
currentDeleteBlock.lines.push(line.slice(1)); // 去掉 - 前缀
|
|
259
|
+
} else if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
260
|
+
// 新增行,保存当前删除块
|
|
261
|
+
if (currentDeleteBlock && currentDeleteBlock.lines.length > 0) {
|
|
262
|
+
deletedBlocks.push({
|
|
263
|
+
file: filename,
|
|
264
|
+
startLine: currentDeleteBlock.startLine,
|
|
265
|
+
endLine: currentDeleteBlock.startLine + currentDeleteBlock.lines.length - 1,
|
|
266
|
+
content: currentDeleteBlock.lines.join("\n"),
|
|
267
|
+
});
|
|
268
|
+
currentDeleteBlock = { startLine: currentDeleteBlock.startLine, lines: [] };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 保存最后一个删除块
|
|
274
|
+
if (currentDeleteBlock && currentDeleteBlock.lines.length > 0) {
|
|
275
|
+
deletedBlocks.push({
|
|
276
|
+
file: filename,
|
|
277
|
+
startLine: currentDeleteBlock.startLine,
|
|
278
|
+
endLine: currentDeleteBlock.startLine + currentDeleteBlock.lines.length - 1,
|
|
279
|
+
content: currentDeleteBlock.lines.join("\n"),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return deletedBlocks;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 过滤掉空白行和注释行为主的删除块
|
|
288
|
+
*/
|
|
289
|
+
protected filterMeaningfulBlocks(blocks: DeletedCodeBlock[]): DeletedCodeBlock[] {
|
|
290
|
+
return blocks.filter((block) => {
|
|
291
|
+
const meaningfulLines = block.content.split("\n").filter((line) => {
|
|
292
|
+
const trimmed = line.trim();
|
|
293
|
+
return (
|
|
294
|
+
trimmed &&
|
|
295
|
+
!trimmed.startsWith("//") &&
|
|
296
|
+
!trimmed.startsWith("*") &&
|
|
297
|
+
!trimmed.startsWith("/*")
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
return meaningfulLines.length > 0;
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* 从 diff 文本中提取被删除的代码块
|
|
306
|
+
*/
|
|
307
|
+
protected extractDeletedBlocksFromDiffText(diffText: string): DeletedCodeBlock[] {
|
|
308
|
+
const deletedBlocks: DeletedCodeBlock[] = [];
|
|
309
|
+
const fileDiffs = diffText.split(/^diff --git /m).filter(Boolean);
|
|
310
|
+
|
|
311
|
+
for (const fileDiff of fileDiffs) {
|
|
312
|
+
// 解析文件名
|
|
313
|
+
const headerMatch = fileDiff.match(/^a\/(.+?) b\/(.+?)[\r\n]/);
|
|
314
|
+
if (!headerMatch) continue;
|
|
315
|
+
|
|
316
|
+
const filename = headerMatch[1]; // 使用原文件名(a/...)
|
|
317
|
+
const lines = fileDiff.split("\n");
|
|
318
|
+
let currentDeleteBlock: { startLine: number; lines: string[] } | null = null;
|
|
319
|
+
|
|
320
|
+
for (const line of lines) {
|
|
321
|
+
// 解析 hunk header: @@ -oldStart,oldCount +newStart,newCount @@
|
|
322
|
+
const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+\d+(?:,\d+)? @@/);
|
|
323
|
+
if (hunkMatch) {
|
|
324
|
+
// 保存之前的删除块
|
|
325
|
+
if (currentDeleteBlock && currentDeleteBlock.lines.length > 0) {
|
|
326
|
+
deletedBlocks.push({
|
|
327
|
+
file: filename,
|
|
328
|
+
startLine: currentDeleteBlock.startLine,
|
|
329
|
+
endLine: currentDeleteBlock.startLine + currentDeleteBlock.lines.length - 1,
|
|
330
|
+
content: currentDeleteBlock.lines.join("\n"),
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
currentDeleteBlock = {
|
|
334
|
+
startLine: parseInt(hunkMatch[1], 10),
|
|
335
|
+
lines: [],
|
|
336
|
+
};
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 删除的行(以 - 开头,但不是 ---)
|
|
341
|
+
if (line.startsWith("-") && !line.startsWith("---") && currentDeleteBlock) {
|
|
342
|
+
currentDeleteBlock.lines.push(line.slice(1)); // 去掉 - 前缀
|
|
343
|
+
} else if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
344
|
+
// 新增行,保存当前删除块
|
|
345
|
+
if (currentDeleteBlock && currentDeleteBlock.lines.length > 0) {
|
|
346
|
+
deletedBlocks.push({
|
|
347
|
+
file: filename,
|
|
348
|
+
startLine: currentDeleteBlock.startLine,
|
|
349
|
+
endLine: currentDeleteBlock.startLine + currentDeleteBlock.lines.length - 1,
|
|
350
|
+
content: currentDeleteBlock.lines.join("\n"),
|
|
351
|
+
});
|
|
352
|
+
currentDeleteBlock = { startLine: currentDeleteBlock.startLine, lines: [] };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// 保存最后一个删除块
|
|
358
|
+
if (currentDeleteBlock && currentDeleteBlock.lines.length > 0) {
|
|
359
|
+
deletedBlocks.push({
|
|
360
|
+
file: filename,
|
|
361
|
+
startLine: currentDeleteBlock.startLine,
|
|
362
|
+
endLine: currentDeleteBlock.startLine + currentDeleteBlock.lines.length - 1,
|
|
363
|
+
content: currentDeleteBlock.lines.join("\n"),
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// 过滤掉空白行和注释行为主的删除块
|
|
369
|
+
return this.filterMeaningfulBlocks(deletedBlocks);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 从 git diff 中提取被删除的代码块
|
|
374
|
+
*/
|
|
375
|
+
protected async getDeletedCodeBlocks(
|
|
376
|
+
baseRef: string,
|
|
377
|
+
headRef: string,
|
|
378
|
+
verbose?: VerboseLevel,
|
|
379
|
+
): Promise<DeletedCodeBlock[]> {
|
|
380
|
+
if (shouldLog(verbose, 1)) {
|
|
381
|
+
console.log(` 🔎 分析 ${baseRef}...${headRef} 的删除代码`);
|
|
382
|
+
}
|
|
383
|
+
// 尝试解析 ref,支持本地分支、远程分支、commit SHA
|
|
384
|
+
const resolvedBaseRef = await this.resolveRef(baseRef, verbose);
|
|
385
|
+
const resolvedHeadRef = await this.resolveRef(headRef, verbose);
|
|
386
|
+
|
|
387
|
+
// 使用两点语法 (..) 而非三点语法 (...),避免浅克隆时找不到 merge base
|
|
388
|
+
// 两点语法直接比较两个 ref 的差异,不需要计算共同祖先
|
|
389
|
+
const diffOutput = await this.runGitCommand([
|
|
390
|
+
"diff",
|
|
391
|
+
"-U0", // 不显示上下文,只显示变更
|
|
392
|
+
`${resolvedBaseRef}..${resolvedHeadRef}`,
|
|
393
|
+
]);
|
|
394
|
+
|
|
395
|
+
const deletedBlocks: DeletedCodeBlock[] = [];
|
|
396
|
+
const fileDiffs = diffOutput.split(/^diff --git /m).filter(Boolean);
|
|
397
|
+
|
|
398
|
+
for (const fileDiff of fileDiffs) {
|
|
399
|
+
// 解析文件名
|
|
400
|
+
const headerMatch = fileDiff.match(/^a\/(.+?) b\/(.+?)[\r\n]/);
|
|
401
|
+
if (!headerMatch) continue;
|
|
402
|
+
|
|
403
|
+
const filename = headerMatch[1]; // 使用原文件名(a/...)
|
|
404
|
+
const lines = fileDiff.split("\n");
|
|
405
|
+
let currentDeleteBlock: { startLine: number; lines: string[] } | null = null;
|
|
406
|
+
|
|
407
|
+
for (const line of lines) {
|
|
408
|
+
// 解析 hunk header: @@ -oldStart,oldCount +newStart,newCount @@
|
|
409
|
+
const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+\d+(?:,\d+)? @@/);
|
|
410
|
+
if (hunkMatch) {
|
|
411
|
+
// 保存之前的删除块
|
|
412
|
+
if (currentDeleteBlock && currentDeleteBlock.lines.length > 0) {
|
|
413
|
+
deletedBlocks.push({
|
|
414
|
+
file: filename,
|
|
415
|
+
startLine: currentDeleteBlock.startLine,
|
|
416
|
+
endLine: currentDeleteBlock.startLine + currentDeleteBlock.lines.length - 1,
|
|
417
|
+
content: currentDeleteBlock.lines.join("\n"),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
currentDeleteBlock = {
|
|
421
|
+
startLine: parseInt(hunkMatch[1], 10),
|
|
422
|
+
lines: [],
|
|
423
|
+
};
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// 删除的行(以 - 开头,但不是 ---)
|
|
428
|
+
if (line.startsWith("-") && !line.startsWith("---") && currentDeleteBlock) {
|
|
429
|
+
currentDeleteBlock.lines.push(line.slice(1)); // 去掉 - 前缀
|
|
430
|
+
} else if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
431
|
+
// 新增行,保存当前删除块
|
|
432
|
+
if (currentDeleteBlock && currentDeleteBlock.lines.length > 0) {
|
|
433
|
+
deletedBlocks.push({
|
|
434
|
+
file: filename,
|
|
435
|
+
startLine: currentDeleteBlock.startLine,
|
|
436
|
+
endLine: currentDeleteBlock.startLine + currentDeleteBlock.lines.length - 1,
|
|
437
|
+
content: currentDeleteBlock.lines.join("\n"),
|
|
438
|
+
});
|
|
439
|
+
currentDeleteBlock = { startLine: currentDeleteBlock.startLine, lines: [] };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// 保存最后一个删除块
|
|
445
|
+
if (currentDeleteBlock && currentDeleteBlock.lines.length > 0) {
|
|
446
|
+
deletedBlocks.push({
|
|
447
|
+
file: filename,
|
|
448
|
+
startLine: currentDeleteBlock.startLine,
|
|
449
|
+
endLine: currentDeleteBlock.startLine + currentDeleteBlock.lines.length - 1,
|
|
450
|
+
content: currentDeleteBlock.lines.join("\n"),
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// 过滤掉空白行和注释行为主的删除块
|
|
456
|
+
return this.filterMeaningfulBlocks(deletedBlocks);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* 查找可能引用被删除代码的文件
|
|
461
|
+
*/
|
|
462
|
+
protected async findCodeReferences(
|
|
463
|
+
deletedBlocks: DeletedCodeBlock[],
|
|
464
|
+
): Promise<Map<string, string[]>> {
|
|
465
|
+
const references = new Map<string, string[]>();
|
|
466
|
+
|
|
467
|
+
for (const block of deletedBlocks) {
|
|
468
|
+
// 从删除的代码中提取可能的标识符(函数名、类名、变量名等)
|
|
469
|
+
const identifiers = this.extractIdentifiers(block.content);
|
|
470
|
+
const fileRefs: string[] = [];
|
|
471
|
+
|
|
472
|
+
for (const identifier of identifiers) {
|
|
473
|
+
if (identifier.length < 3) continue; // 跳过太短的标识符
|
|
474
|
+
|
|
475
|
+
try {
|
|
476
|
+
// 使用 git grep 查找引用
|
|
477
|
+
const grepOutput = await this.runGitCommand([
|
|
478
|
+
"grep",
|
|
479
|
+
"-l", // 只输出文件名
|
|
480
|
+
"-w", // 全词匹配
|
|
481
|
+
identifier,
|
|
482
|
+
"--",
|
|
483
|
+
"*.ts",
|
|
484
|
+
"*.js",
|
|
485
|
+
"*.tsx",
|
|
486
|
+
"*.jsx",
|
|
487
|
+
]);
|
|
488
|
+
|
|
489
|
+
const files = grepOutput
|
|
490
|
+
.trim()
|
|
491
|
+
.split("\n")
|
|
492
|
+
.filter((f) => f && f !== block.file);
|
|
493
|
+
fileRefs.push(...files);
|
|
494
|
+
} catch {
|
|
495
|
+
// grep 没找到匹配,忽略
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (fileRefs.length > 0) {
|
|
500
|
+
const uniqueRefs = [...new Set(fileRefs)];
|
|
501
|
+
references.set(`${block.file}:${block.startLine}-${block.endLine}`, uniqueRefs);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return references;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* 从代码中提取标识符
|
|
510
|
+
*/
|
|
511
|
+
protected extractIdentifiers(code: string): string[] {
|
|
512
|
+
const identifiers: string[] = [];
|
|
513
|
+
|
|
514
|
+
// 匹配函数定义
|
|
515
|
+
const funcMatches = code.matchAll(/(?:function|async\s+function)\s+(\w+)/g);
|
|
516
|
+
for (const match of funcMatches) {
|
|
517
|
+
identifiers.push(match[1]);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// 匹配方法定义
|
|
521
|
+
const methodMatches = code.matchAll(/(?:async\s+)?(\w+)\s*\([^)]*\)\s*[:{]/g);
|
|
522
|
+
for (const match of methodMatches) {
|
|
523
|
+
if (!["if", "for", "while", "switch", "catch", "function"].includes(match[1])) {
|
|
524
|
+
identifiers.push(match[1]);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// 匹配类定义
|
|
529
|
+
const classMatches = code.matchAll(/class\s+(\w+)/g);
|
|
530
|
+
for (const match of classMatches) {
|
|
531
|
+
identifiers.push(match[1]);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// 匹配接口定义
|
|
535
|
+
const interfaceMatches = code.matchAll(/interface\s+(\w+)/g);
|
|
536
|
+
for (const match of interfaceMatches) {
|
|
537
|
+
identifiers.push(match[1]);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// 匹配类型定义
|
|
541
|
+
const typeMatches = code.matchAll(/type\s+(\w+)/g);
|
|
542
|
+
for (const match of typeMatches) {
|
|
543
|
+
identifiers.push(match[1]);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// 匹配 export 的变量/常量
|
|
547
|
+
const exportMatches = code.matchAll(/export\s+(?:const|let|var)\s+(\w+)/g);
|
|
548
|
+
for (const match of exportMatches) {
|
|
549
|
+
identifiers.push(match[1]);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return [...new Set(identifiers)];
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* 使用 LLM 分析删除代码的影响
|
|
557
|
+
*/
|
|
558
|
+
protected async analyzeWithLLM(
|
|
559
|
+
deletedBlocks: DeletedCodeBlock[],
|
|
560
|
+
references: Map<string, string[]>,
|
|
561
|
+
llmMode: LLMMode,
|
|
562
|
+
verbose?: VerboseLevel,
|
|
563
|
+
): Promise<DeletionImpactResult> {
|
|
564
|
+
const llmJsonPut = new LlmJsonPut<DeletionImpactResult>(DELETION_IMPACT_SCHEMA);
|
|
565
|
+
|
|
566
|
+
const systemPrompt = `你是一个代码审查专家,专门分析删除代码可能带来的影响。
|
|
567
|
+
|
|
568
|
+
## 任务
|
|
569
|
+
分析以下被删除的代码块,判断删除这些代码是否会影响到其他功能。
|
|
570
|
+
|
|
571
|
+
## 分析要点
|
|
572
|
+
1. **功能依赖**: 被删除的代码是否被其他模块调用或依赖
|
|
573
|
+
2. **接口变更**: 删除是否会导致 API 或接口不兼容
|
|
574
|
+
3. **副作用**: 删除是否会影响系统的其他行为
|
|
575
|
+
4. **数据流**: 删除是否会中断数据处理流程
|
|
576
|
+
|
|
577
|
+
## 风险等级判断标准
|
|
578
|
+
- **high**: 删除的代码被其他文件直接调用,删除后会导致编译错误或运行时异常
|
|
579
|
+
- **medium**: 删除的代码可能影响某些功能的行为,但不会导致直接错误
|
|
580
|
+
- **low**: 删除的代码影响较小,可能只是清理无用代码
|
|
581
|
+
- **none**: 删除的代码确实是无用代码,不会产生任何影响
|
|
582
|
+
|
|
583
|
+
## 输出要求
|
|
584
|
+
- 对每个有风险的删除块给出详细分析
|
|
585
|
+
- 如果删除是安全的,也要说明原因
|
|
586
|
+
- 提供具体的建议`;
|
|
587
|
+
|
|
588
|
+
const deletedCodeSection = deletedBlocks
|
|
589
|
+
.map((block, index) => {
|
|
590
|
+
const refs = references.get(`${block.file}:${block.startLine}-${block.endLine}`) || [];
|
|
591
|
+
return `### 删除块 ${index + 1}: ${block.file}:${block.startLine}-${block.endLine}
|
|
592
|
+
|
|
593
|
+
\`\`\`
|
|
594
|
+
${block.content}
|
|
595
|
+
\`\`\`
|
|
596
|
+
|
|
597
|
+
可能引用此代码的文件: ${refs.length > 0 ? refs.join(", ") : "未发现直接引用"}
|
|
598
|
+
`;
|
|
599
|
+
})
|
|
600
|
+
.join("\n");
|
|
601
|
+
|
|
602
|
+
const userPrompt = `## 被删除的代码块
|
|
603
|
+
|
|
604
|
+
${deletedCodeSection}
|
|
605
|
+
|
|
606
|
+
请分析这些删除操作可能带来的影响。`;
|
|
607
|
+
|
|
608
|
+
if (shouldLog(verbose, 2)) {
|
|
609
|
+
console.log(`\nsystemPrompt:\n----------------\n${systemPrompt}\n----------------`);
|
|
610
|
+
console.log(`\nuserPrompt:\n----------------\n${userPrompt}\n----------------`);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
try {
|
|
614
|
+
const stream = this.llmProxyService.chatStream(
|
|
615
|
+
[
|
|
616
|
+
{ role: "system", content: systemPrompt },
|
|
617
|
+
{ role: "user", content: userPrompt },
|
|
618
|
+
],
|
|
619
|
+
{
|
|
620
|
+
adapter: llmMode,
|
|
621
|
+
jsonSchema: llmJsonPut,
|
|
622
|
+
verbose,
|
|
623
|
+
},
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
let result: DeletionImpactResult | undefined;
|
|
627
|
+
for await (const event of stream) {
|
|
628
|
+
if (event.type === "result") {
|
|
629
|
+
result = event.response.structuredOutput as DeletionImpactResult | undefined;
|
|
630
|
+
} else if (event.type === "error") {
|
|
631
|
+
console.error(` ❌ 分析失败: ${event.message}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// 防御性检查:确保返回的是有效对象
|
|
636
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
637
|
+
return { impacts: [], summary: "分析返回格式无效" };
|
|
638
|
+
}
|
|
639
|
+
return result;
|
|
640
|
+
} catch (error) {
|
|
641
|
+
if (error instanceof Error) {
|
|
642
|
+
console.error(` ❌ LLM 调用失败: ${error.message}`);
|
|
643
|
+
if (error.stack) {
|
|
644
|
+
console.error(` 堆栈信息:\n${error.stack}`);
|
|
645
|
+
}
|
|
646
|
+
} else {
|
|
647
|
+
console.error(` ❌ LLM 调用失败: ${String(error)}`);
|
|
648
|
+
}
|
|
649
|
+
return { impacts: [], summary: "LLM 调用失败" };
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* 使用 Claude Agent 模式分析删除代码的影响
|
|
655
|
+
* Claude Agent 可以使用工具主动探索代码库,分析更深入
|
|
656
|
+
*/
|
|
657
|
+
protected async analyzeWithAgent(
|
|
658
|
+
analysisMode: LLMMode,
|
|
659
|
+
deletedBlocks: DeletedCodeBlock[],
|
|
660
|
+
references: Map<string, string[]>,
|
|
661
|
+
verbose?: VerboseLevel,
|
|
662
|
+
): Promise<DeletionImpactResult> {
|
|
663
|
+
const llmJsonPut = new LlmJsonPut<DeletionImpactResult>(DELETION_IMPACT_SCHEMA);
|
|
664
|
+
|
|
665
|
+
const systemPrompt = `你是一个资深代码架构师,擅长分析代码变更的影响范围和潜在风险。
|
|
666
|
+
|
|
667
|
+
## 任务
|
|
668
|
+
深入分析以下被删除的代码块,评估删除操作对代码库的影响。
|
|
669
|
+
|
|
670
|
+
## 你的能力
|
|
671
|
+
你可以使用以下工具来深入分析代码:
|
|
672
|
+
- **Read**: 读取文件内容,查看被删除代码的完整上下文
|
|
673
|
+
- **Grep**: 搜索代码库,查找对被删除代码的引用
|
|
674
|
+
- **Glob**: 查找匹配模式的文件
|
|
675
|
+
|
|
676
|
+
## 分析流程
|
|
677
|
+
1. 首先阅读被删除代码的上下文,理解其功能
|
|
678
|
+
2. 使用 Grep 搜索代码库中对这些代码的引用
|
|
679
|
+
3. 分析引用处的代码,判断删除后的影响
|
|
680
|
+
4. 给出风险评估和建议
|
|
681
|
+
|
|
682
|
+
## 风险等级判断标准
|
|
683
|
+
- **high**: 删除的代码被其他文件直接调用,删除后会导致编译错误或运行时异常
|
|
684
|
+
- **medium**: 删除的代码可能影响某些功能的行为,但不会导致直接错误
|
|
685
|
+
- **low**: 删除的代码影响较小,可能只是清理无用代码
|
|
686
|
+
- **none**: 删除的代码确实是无用代码,不会产生任何影响
|
|
687
|
+
|
|
688
|
+
## 输出要求
|
|
689
|
+
- 对每个有风险的删除块给出详细分析
|
|
690
|
+
- 如果删除是安全的,也要说明原因
|
|
691
|
+
- 提供具体的建议`;
|
|
692
|
+
|
|
693
|
+
const deletedCodeSection = deletedBlocks
|
|
694
|
+
.map((block, index) => {
|
|
695
|
+
const refs = references.get(`${block.file}:${block.startLine}-${block.endLine}`) || [];
|
|
696
|
+
return `### 删除块 ${index + 1}: ${block.file}:${block.startLine}-${block.endLine}
|
|
697
|
+
|
|
698
|
+
\`\`\`
|
|
699
|
+
${block.content}
|
|
700
|
+
\`\`\`
|
|
701
|
+
|
|
702
|
+
可能引用此代码的文件: ${refs.length > 0 ? refs.join(", ") : "未发现直接引用"}
|
|
703
|
+
`;
|
|
704
|
+
})
|
|
705
|
+
.join("\n");
|
|
706
|
+
|
|
707
|
+
const userPrompt = `## 被删除的代码块
|
|
708
|
+
|
|
709
|
+
${deletedCodeSection}
|
|
710
|
+
|
|
711
|
+
## 补充说明
|
|
712
|
+
|
|
713
|
+
请使用你的工具能力深入分析这些删除操作可能带来的影响。
|
|
714
|
+
- 如果需要查看更多上下文,请读取相关文件
|
|
715
|
+
- 如果需要确认引用关系,请搜索代码库
|
|
716
|
+
- 分析完成后,给出结构化的影响评估`;
|
|
717
|
+
|
|
718
|
+
if (shouldLog(verbose, 2)) {
|
|
719
|
+
console.log(
|
|
720
|
+
`\n[Agent Mode] systemPrompt:\n----------------\n${systemPrompt}\n----------------`,
|
|
721
|
+
);
|
|
722
|
+
console.log(`\n[Agent Mode] userPrompt:\n----------------\n${userPrompt}\n----------------`);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (shouldLog(verbose, 1)) {
|
|
726
|
+
console.log(` 🤖 使用 Agent 模式分析(${analysisMode},可使用工具)...`);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
try {
|
|
730
|
+
const stream = this.llmProxyService.chatStream(
|
|
731
|
+
[
|
|
732
|
+
{ role: "system", content: systemPrompt },
|
|
733
|
+
{ role: "user", content: userPrompt },
|
|
734
|
+
],
|
|
735
|
+
{
|
|
736
|
+
adapter: analysisMode,
|
|
737
|
+
jsonSchema: llmJsonPut,
|
|
738
|
+
allowedTools: ["Read", "Grep", "Glob"],
|
|
739
|
+
verbose,
|
|
740
|
+
},
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
let result: DeletionImpactResult | undefined;
|
|
744
|
+
const streamLoggerState = createStreamLoggerState();
|
|
745
|
+
for await (const event of stream) {
|
|
746
|
+
if (shouldLog(verbose, 1)) {
|
|
747
|
+
logStreamEvent(event, streamLoggerState);
|
|
748
|
+
}
|
|
749
|
+
if (event.type === "result") {
|
|
750
|
+
result = event.response.structuredOutput as DeletionImpactResult | undefined;
|
|
751
|
+
} else if (event.type === "error") {
|
|
752
|
+
console.error(` ❌ 分析失败: ${event.message}`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// 防御性检查:确保返回的是有效对象
|
|
757
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
758
|
+
return { impacts: [], summary: "分析返回格式无效" };
|
|
759
|
+
}
|
|
760
|
+
return result;
|
|
761
|
+
} catch (error) {
|
|
762
|
+
if (error instanceof Error) {
|
|
763
|
+
console.error(` ❌ Agent 调用失败: ${error.message}`);
|
|
764
|
+
if (error.stack) {
|
|
765
|
+
console.error(` 堆栈信息:\n${error.stack}`);
|
|
766
|
+
}
|
|
767
|
+
} else {
|
|
768
|
+
console.error(` ❌ Agent 调用失败: ${String(error)}`);
|
|
769
|
+
}
|
|
770
|
+
return { impacts: [], summary: "Agent 调用失败" };
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* 解析 ref,支持本地分支、远程分支、commit SHA
|
|
776
|
+
* 优先级:本地分支 > origin/分支 > fetch后重试 > 原始值
|
|
777
|
+
*/
|
|
778
|
+
protected async resolveRef(ref: string, verbose?: VerboseLevel): Promise<string> {
|
|
779
|
+
if (!ref) {
|
|
780
|
+
throw new Error(`resolveRef: ref 参数不能为空。调用栈: ${new Error().stack}`);
|
|
781
|
+
}
|
|
782
|
+
// 如果已经是 commit SHA 格式(7-40位十六进制),直接返回
|
|
783
|
+
if (/^[0-9a-f]{7,40}$/i.test(ref)) {
|
|
784
|
+
if (shouldLog(verbose, 1)) {
|
|
785
|
+
console.log(` 📌 ${ref} 是 commit SHA,直接使用`);
|
|
786
|
+
}
|
|
787
|
+
return ref;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// 如果已经是 origin/ 格式,直接返回
|
|
791
|
+
if (ref.startsWith("origin/")) {
|
|
792
|
+
if (shouldLog(verbose, 1)) {
|
|
793
|
+
console.log(` 📌 ${ref} 已是远程分支格式,直接使用`);
|
|
794
|
+
}
|
|
795
|
+
return ref;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// 尝试解析本地分支
|
|
799
|
+
try {
|
|
800
|
+
await this.runGitCommand(["rev-parse", "--verify", ref]);
|
|
801
|
+
if (shouldLog(verbose, 1)) {
|
|
802
|
+
console.log(` 📌 ${ref} 解析为本地分支`);
|
|
803
|
+
}
|
|
804
|
+
return ref;
|
|
805
|
+
} catch {
|
|
806
|
+
// 本地分支不存在,尝试 origin/分支
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// 尝试 origin/分支
|
|
810
|
+
try {
|
|
811
|
+
await this.runGitCommand(["rev-parse", "--verify", `origin/${ref}`]);
|
|
812
|
+
if (shouldLog(verbose, 1)) {
|
|
813
|
+
console.log(` 📌 ${ref} 解析为 origin/${ref}`);
|
|
814
|
+
}
|
|
815
|
+
return `origin/${ref}`;
|
|
816
|
+
} catch {
|
|
817
|
+
// origin/分支也不存在,尝试 fetch
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// 尝试 fetch 该分支
|
|
821
|
+
if (shouldLog(verbose, 1)) {
|
|
822
|
+
console.log(` ⏳ 尝试 fetch ${ref}...`);
|
|
823
|
+
}
|
|
824
|
+
try {
|
|
825
|
+
await this.runGitCommand([
|
|
826
|
+
"fetch",
|
|
827
|
+
"origin",
|
|
828
|
+
`${ref}:refs/remotes/origin/${ref}`,
|
|
829
|
+
"--depth=1",
|
|
830
|
+
]);
|
|
831
|
+
if (shouldLog(verbose, 1)) {
|
|
832
|
+
console.log(` 📌 ${ref} fetch 成功,使用 origin/${ref}`);
|
|
833
|
+
}
|
|
834
|
+
return `origin/${ref}`;
|
|
835
|
+
} catch (e) {
|
|
836
|
+
if (shouldLog(verbose, 1)) {
|
|
837
|
+
console.log(` ⚠️ fetch ${ref} 失败: ${e instanceof Error ? e.message : String(e)}`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (shouldLog(verbose, 1)) {
|
|
842
|
+
console.log(` ⚠️ 无法解析 ${ref},使用原始值`);
|
|
843
|
+
}
|
|
844
|
+
return ref;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
protected runGitCommand(args: string[]): Promise<string> {
|
|
848
|
+
return new Promise((resolve, reject) => {
|
|
849
|
+
const child = spawn("git", args, {
|
|
850
|
+
cwd: process.cwd(),
|
|
851
|
+
env: process.env,
|
|
852
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
let stdout = "";
|
|
856
|
+
let stderr = "";
|
|
857
|
+
|
|
858
|
+
child.stdout.on("data", (data) => {
|
|
859
|
+
stdout += data.toString();
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
child.stderr.on("data", (data) => {
|
|
863
|
+
stderr += data.toString();
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
child.on("close", (code) => {
|
|
867
|
+
if (code === 0) {
|
|
868
|
+
resolve(stdout);
|
|
869
|
+
} else {
|
|
870
|
+
reject(new Error(`Git 命令失败 (${code}): ${stderr}`));
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
child.on("error", (err) => {
|
|
875
|
+
reject(err);
|
|
876
|
+
});
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
}
|