cfix 1.0.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/.env.example +69 -0
- package/README.md +1590 -0
- package/bin/cfix +14 -0
- package/bin/cfix.cmd +6 -0
- package/cli/commands/config.js +58 -0
- package/cli/commands/doctor.js +240 -0
- package/cli/commands/fix.js +211 -0
- package/cli/commands/help.js +62 -0
- package/cli/commands/init.js +226 -0
- package/cli/commands/logs.js +161 -0
- package/cli/commands/monitor.js +151 -0
- package/cli/commands/project.js +331 -0
- package/cli/commands/service.js +133 -0
- package/cli/commands/status.js +115 -0
- package/cli/commands/task.js +412 -0
- package/cli/commands/version.js +19 -0
- package/cli/index.js +269 -0
- package/cli/lib/config-manager.js +612 -0
- package/cli/lib/formatter.js +224 -0
- package/cli/lib/process-manager.js +233 -0
- package/cli/lib/service-client.js +271 -0
- package/cli/scripts/install-completion.js +133 -0
- package/package.json +85 -0
- package/public/monitor.html +1096 -0
- package/scripts/completion.bash +87 -0
- package/scripts/completion.zsh +102 -0
- package/src/assets/README.md +32 -0
- package/src/assets/error.png +0 -0
- package/src/assets/icon.png +0 -0
- package/src/assets/success.png +0 -0
- package/src/claude-cli-service.js +216 -0
- package/src/config/index.js +69 -0
- package/src/database/manager.js +391 -0
- package/src/database/migration.js +252 -0
- package/src/git-service.js +1278 -0
- package/src/index.js +1658 -0
- package/src/logger.js +139 -0
- package/src/metrics/collector.js +184 -0
- package/src/middleware/auth.js +86 -0
- package/src/middleware/rate-limit.js +85 -0
- package/src/queue/integration-example.js +283 -0
- package/src/queue/task-queue.js +333 -0
- package/src/services/notification-limiter.js +48 -0
- package/src/services/notification-service.js +115 -0
- package/src/services/system-notifier.js +130 -0
- package/src/task-manager.js +289 -0
- package/src/utils/exec.js +87 -0
- package/src/utils/project-lock.js +246 -0
- package/src/utils/retry.js +110 -0
- package/src/utils/sanitizer.js +174 -0
- package/src/websocket/notifier.js +363 -0
- package/src/wechat-notifier.js +97 -0
|
@@ -0,0 +1,1278 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const ExecUtil = require('./utils/exec');
|
|
4
|
+
const { logger } = require('./logger');
|
|
5
|
+
const RetryUtil = require('./utils/retry');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Git 操作服务
|
|
9
|
+
*/
|
|
10
|
+
class GitService {
|
|
11
|
+
/**
|
|
12
|
+
* 获取当前分支信息
|
|
13
|
+
* @param {string} projectPath - 项目路径
|
|
14
|
+
* @returns {Object} 分支信息
|
|
15
|
+
*/
|
|
16
|
+
static getCurrentBranchInfo(projectPath) {
|
|
17
|
+
try {
|
|
18
|
+
const currentBranch = ExecUtil.execSync('git rev-parse --abbrev-ref HEAD', {
|
|
19
|
+
cwd: projectPath,
|
|
20
|
+
}).trim();
|
|
21
|
+
|
|
22
|
+
const commitHash = ExecUtil.execSync('git rev-parse HEAD', {
|
|
23
|
+
cwd: projectPath,
|
|
24
|
+
}).trim();
|
|
25
|
+
|
|
26
|
+
const commitMessage = ExecUtil.execSync('git log -1 --pretty=%B', {
|
|
27
|
+
cwd: projectPath,
|
|
28
|
+
}).trim();
|
|
29
|
+
|
|
30
|
+
const remoteUrl = this.getRemoteUrl(projectPath);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
branch: currentBranch,
|
|
34
|
+
commit: commitHash.substring(0, 8),
|
|
35
|
+
commitFull: commitHash,
|
|
36
|
+
commitMessage: commitMessage.substring(0, 100),
|
|
37
|
+
remote: remoteUrl,
|
|
38
|
+
projectPath
|
|
39
|
+
};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
logger.error('获取分支信息失败:', error);
|
|
42
|
+
return {
|
|
43
|
+
branch: 'unknown',
|
|
44
|
+
commit: 'unknown',
|
|
45
|
+
projectPath
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 获取远程仓库地址
|
|
52
|
+
* @param {string} projectPath - 项目路径
|
|
53
|
+
* @returns {string} 远程地址
|
|
54
|
+
*/
|
|
55
|
+
static getRemoteUrl(projectPath) {
|
|
56
|
+
try {
|
|
57
|
+
return ExecUtil.execSync('git remote get-url origin', {
|
|
58
|
+
cwd: projectPath,
|
|
59
|
+
}).trim();
|
|
60
|
+
} catch {
|
|
61
|
+
return 'no-remote';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 记录分支操作上下文
|
|
67
|
+
* @param {string} projectPath - 项目路径
|
|
68
|
+
* @param {string} operation - 操作名称
|
|
69
|
+
*/
|
|
70
|
+
static logBranchContext(projectPath, operation) {
|
|
71
|
+
const branchInfo = this.getCurrentBranchInfo(projectPath);
|
|
72
|
+
logger.info(`🔧 [${operation}] 分支上下文:`, {
|
|
73
|
+
operation,
|
|
74
|
+
currentBranch: branchInfo.branch,
|
|
75
|
+
commit: branchInfo.commit,
|
|
76
|
+
remote: branchInfo.remote,
|
|
77
|
+
projectPath: path.basename(projectPath)
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 检查项目是否存在
|
|
83
|
+
* @param {string} projectPath - 项目路径
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
86
|
+
static projectExists(projectPath) {
|
|
87
|
+
return fs.existsSync(projectPath) && fs.existsSync(path.join(projectPath, '.git'));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 检测项目的默认分支 (main/master)
|
|
92
|
+
* @param {string} projectPath - 项目路径
|
|
93
|
+
* @returns {string} 默认分支名称
|
|
94
|
+
*/
|
|
95
|
+
static detectDefaultBranch(projectPath) {
|
|
96
|
+
try {
|
|
97
|
+
// 1. 尝试从远程获取默认分支
|
|
98
|
+
try {
|
|
99
|
+
const remoteHead = ExecUtil.execSync(
|
|
100
|
+
'git symbolic-ref refs/remotes/origin/HEAD',
|
|
101
|
+
{ cwd: projectPath }
|
|
102
|
+
).trim();
|
|
103
|
+
// 格式: refs/remotes/origin/main
|
|
104
|
+
const defaultBranch = remoteHead.replace('refs/remotes/origin/', '');
|
|
105
|
+
logger.debug(`🔍 从远程检测到默认分支: ${defaultBranch}`);
|
|
106
|
+
return defaultBranch;
|
|
107
|
+
} catch (err) {
|
|
108
|
+
logger.debug('无法从远程获取默认分支,尝试本地检测');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 2. 检查本地分支
|
|
112
|
+
try {
|
|
113
|
+
ExecUtil.execSync('git rev-parse --verify main', { cwd: projectPath, stdio: 'ignore' });
|
|
114
|
+
logger.debug('🔍 检测到本地 main 分支');
|
|
115
|
+
return 'main';
|
|
116
|
+
} catch (err) {
|
|
117
|
+
// main 不存在
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
ExecUtil.execSync('git rev-parse --verify master', { cwd: projectPath, stdio: 'ignore' });
|
|
122
|
+
logger.debug('🔍 检测到本地 master 分支');
|
|
123
|
+
return 'master';
|
|
124
|
+
} catch (err) {
|
|
125
|
+
// master 不存在
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 3. 都不存在,返回默认值
|
|
129
|
+
logger.warn('⚠️ 无法检测默认分支,使用 main 作为默认值');
|
|
130
|
+
return 'main';
|
|
131
|
+
} catch (error) {
|
|
132
|
+
logger.error(`❌ 检测默认分支失败: ${error.message}`);
|
|
133
|
+
return 'main';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 智能准备项目(处理现有项目的各种分支状态)
|
|
139
|
+
* @param {string} projectPath - 项目路径
|
|
140
|
+
* @param {string} baseBranch - 基准分支名称(可选,默认自动检测 main/master)
|
|
141
|
+
* @returns {Promise<Object>} 准备结果 { baseBranch, originalBranch, switched }
|
|
142
|
+
*/
|
|
143
|
+
static async prepareExistingProject(projectPath, baseBranch = null) {
|
|
144
|
+
logger.info('🔧 智能准备现有项目...', { projectPath: path.basename(projectPath) });
|
|
145
|
+
|
|
146
|
+
// 1. 获取当前状态
|
|
147
|
+
const branchInfo = this.getCurrentBranchInfo(projectPath);
|
|
148
|
+
const originalBranch = branchInfo.branch;
|
|
149
|
+
|
|
150
|
+
logger.info('📍 当前分支状态:', {
|
|
151
|
+
branch: originalBranch,
|
|
152
|
+
commit: branchInfo.commit,
|
|
153
|
+
remote: branchInfo.remote,
|
|
154
|
+
projectPath: path.basename(projectPath)
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// 2. 检测或使用基准分支
|
|
158
|
+
if (!baseBranch) {
|
|
159
|
+
baseBranch = this.detectDefaultBranch(projectPath);
|
|
160
|
+
logger.info(`🔍 自动检测到基准分支: ${baseBranch}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 3. 判断当前分支类型
|
|
164
|
+
const isFeatureBranch = originalBranch.startsWith('fix_') ||
|
|
165
|
+
originalBranch.startsWith('feature_') ||
|
|
166
|
+
originalBranch.includes('task-');
|
|
167
|
+
|
|
168
|
+
const isBaseBranch = originalBranch === baseBranch ||
|
|
169
|
+
originalBranch === 'main' ||
|
|
170
|
+
originalBranch === 'master';
|
|
171
|
+
|
|
172
|
+
let switched = false;
|
|
173
|
+
|
|
174
|
+
if (isFeatureBranch) {
|
|
175
|
+
logger.warn('⚠️ 当前处于功能分支,切换到基准分支以获取最新代码', {
|
|
176
|
+
currentBranch: originalBranch,
|
|
177
|
+
baseBranch
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// 4. 保存当前工作区(如果有未提交的更改)
|
|
181
|
+
const hasChanges = this.hasUncommittedChanges(projectPath);
|
|
182
|
+
if (hasChanges) {
|
|
183
|
+
logger.info('💾 检测到未提交的更改,暂存工作区...');
|
|
184
|
+
try {
|
|
185
|
+
ExecUtil.execSync('git stash push -m "Auto stash before switching branch"', {
|
|
186
|
+
cwd: projectPath,
|
|
187
|
+
});
|
|
188
|
+
logger.info('✅ 工作区已暂存');
|
|
189
|
+
} catch (error) {
|
|
190
|
+
logger.warn('⚠️ 暂存工作区失败,尝试继续...', { error: error.message });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 5. 切换到基准分支
|
|
195
|
+
try {
|
|
196
|
+
logger.info(`🔀 切换到基准分支: ${baseBranch}`);
|
|
197
|
+
ExecUtil.execSync(`git checkout ${baseBranch}`, {
|
|
198
|
+
cwd: projectPath,
|
|
199
|
+
});
|
|
200
|
+
switched = true;
|
|
201
|
+
logger.info(`✅ 已切换到 ${baseBranch} 分支`);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
logger.error(`❌ 切换分支失败: ${error.message}`);
|
|
204
|
+
throw new Error(`无法切换到基准分支 ${baseBranch}: ${error.message}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 6. 拉取基准分支最新代码
|
|
208
|
+
logger.info(`🔄 拉取 ${baseBranch} 分支最新代码...`);
|
|
209
|
+
await this.pullLatest(projectPath);
|
|
210
|
+
|
|
211
|
+
} else if (isBaseBranch) {
|
|
212
|
+
logger.info('✅ 当前已在基准分支,直接拉取最新代码', {
|
|
213
|
+
branch: originalBranch
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// 直接拉取最新代码
|
|
217
|
+
await this.pullLatest(projectPath);
|
|
218
|
+
|
|
219
|
+
} else {
|
|
220
|
+
logger.warn('⚠️ 当前分支既不是功能分支也不是基准分支,谨慎处理', {
|
|
221
|
+
currentBranch: originalBranch,
|
|
222
|
+
baseBranch
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// 尝试切换到基准分支
|
|
226
|
+
try {
|
|
227
|
+
logger.info(`🔀 尝试切换到基准分支: ${baseBranch}`);
|
|
228
|
+
ExecUtil.execSync(`git checkout ${baseBranch}`, {
|
|
229
|
+
cwd: projectPath,
|
|
230
|
+
});
|
|
231
|
+
switched = true;
|
|
232
|
+
logger.info(`✅ 已切换到 ${baseBranch} 分支`);
|
|
233
|
+
await this.pullLatest(projectPath);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
logger.error(`❌ 切换分支失败,停留在当前分支: ${error.message}`);
|
|
236
|
+
// 不抛出错误,允许继续
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const finalBranchInfo = this.getCurrentBranchInfo(projectPath);
|
|
241
|
+
logger.info('📊 项目准备完成:', {
|
|
242
|
+
originalBranch,
|
|
243
|
+
currentBranch: finalBranchInfo.branch,
|
|
244
|
+
baseBranch,
|
|
245
|
+
switched,
|
|
246
|
+
commit: finalBranchInfo.commit
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
baseBranch,
|
|
251
|
+
originalBranch,
|
|
252
|
+
currentBranch: finalBranchInfo.branch,
|
|
253
|
+
switched
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 检查是否有未提交的更改
|
|
259
|
+
* @param {string} projectPath - 项目路径
|
|
260
|
+
* @returns {boolean}
|
|
261
|
+
*/
|
|
262
|
+
static hasUncommittedChanges(projectPath) {
|
|
263
|
+
try {
|
|
264
|
+
const status = ExecUtil.execSync('git status --porcelain', {
|
|
265
|
+
cwd: projectPath,
|
|
266
|
+
}).trim();
|
|
267
|
+
return status.length > 0;
|
|
268
|
+
} catch (error) {
|
|
269
|
+
logger.warn('检查 Git 状态失败', { error: error.message });
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* 克隆仓库
|
|
276
|
+
* @param {string} repoUrl - 仓库地址
|
|
277
|
+
* @param {string} targetPath - 目标路径
|
|
278
|
+
*/
|
|
279
|
+
static cloneRepo(repoUrl, targetPath) {
|
|
280
|
+
logger.info(`📦 克隆仓库: ${repoUrl}`);
|
|
281
|
+
ExecUtil.execSync(`git clone "${repoUrl}" "${targetPath}"`);
|
|
282
|
+
logger.info(`✅ 仓库已克隆到: ${targetPath}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* 拉取最新代码(带重试)
|
|
287
|
+
* @param {string} projectPath - 项目路径
|
|
288
|
+
*/
|
|
289
|
+
static async pullLatest(projectPath) {
|
|
290
|
+
// 记录操作前的分支上下文
|
|
291
|
+
this.logBranchContext(projectPath, 'Pull Latest - Before');
|
|
292
|
+
|
|
293
|
+
const branchInfo = this.getCurrentBranchInfo(projectPath);
|
|
294
|
+
logger.info('🔄 拉取最新代码...', {
|
|
295
|
+
branch: branchInfo.branch,
|
|
296
|
+
commit: branchInfo.commit,
|
|
297
|
+
remote: branchInfo.remote,
|
|
298
|
+
projectPath: path.basename(projectPath)
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
await RetryUtil.withRetry(
|
|
303
|
+
() => {
|
|
304
|
+
const output = ExecUtil.execSync('git pull', { cwd: projectPath });
|
|
305
|
+
logger.debug('Git pull 输出:', { output: output.trim() });
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
maxRetries: 3,
|
|
309
|
+
delay: 2000,
|
|
310
|
+
operation: 'Git 拉取',
|
|
311
|
+
shouldRetry: (error) => {
|
|
312
|
+
// 记录拉取失败的详细信息
|
|
313
|
+
logger.warn('Git 拉取失败,分析错误原因:', {
|
|
314
|
+
branch: branchInfo.branch,
|
|
315
|
+
commit: branchInfo.commit,
|
|
316
|
+
error: error.message,
|
|
317
|
+
projectPath: path.basename(projectPath)
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const message = error.message.toLowerCase();
|
|
321
|
+
|
|
322
|
+
// 检查是否是未设置上游分支的错误
|
|
323
|
+
if (message.includes('no tracking information') ||
|
|
324
|
+
message.includes('set-upstream-to')) {
|
|
325
|
+
logger.warn('⚠️ 检测到未设置上游分支,尝试自动修复...', {
|
|
326
|
+
branch: branchInfo.branch,
|
|
327
|
+
suggestion: `git branch --set-upstream-to=origin/${branchInfo.branch} ${branchInfo.branch}`
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// 尝试设置上游分支
|
|
331
|
+
try {
|
|
332
|
+
ExecUtil.execSync(`git branch --set-upstream-to=origin/${branchInfo.branch} ${branchInfo.branch}`, {
|
|
333
|
+
cwd: projectPath,
|
|
334
|
+
});
|
|
335
|
+
logger.info('✅ 已自动设置上游分支,重新拉取...', {
|
|
336
|
+
branch: branchInfo.branch
|
|
337
|
+
});
|
|
338
|
+
// 重新拉取
|
|
339
|
+
ExecUtil.execSync('git pull', { cwd: projectPath });
|
|
340
|
+
return false; // 成功,不需要重试
|
|
341
|
+
} catch (setupError) {
|
|
342
|
+
logger.error('❌ 设置上游分支失败:', {
|
|
343
|
+
branch: branchInfo.branch,
|
|
344
|
+
error: setupError.message
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 网络错误才重试
|
|
350
|
+
return message.includes('network') ||
|
|
351
|
+
message.includes('timeout') ||
|
|
352
|
+
message.includes('connection');
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
// 记录操作后的分支上下文
|
|
358
|
+
this.logBranchContext(projectPath, 'Pull Latest - After');
|
|
359
|
+
|
|
360
|
+
const afterInfo = this.getCurrentBranchInfo(projectPath);
|
|
361
|
+
logger.info('✅ 代码已更新', {
|
|
362
|
+
branch: afterInfo.branch,
|
|
363
|
+
fromCommit: branchInfo.commit,
|
|
364
|
+
toCommit: afterInfo.commit,
|
|
365
|
+
projectPath: path.basename(projectPath)
|
|
366
|
+
});
|
|
367
|
+
} catch (error) {
|
|
368
|
+
logger.warn('⚠️ 拉取失败,继续使用本地代码', {
|
|
369
|
+
branch: branchInfo.branch,
|
|
370
|
+
commit: branchInfo.commit,
|
|
371
|
+
error: error.message,
|
|
372
|
+
projectPath: path.basename(projectPath),
|
|
373
|
+
suggestion: '请检查分支是否有远程跟踪分支或网络连接'
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* 创建新分支(使用新的命名规则)
|
|
380
|
+
* @param {string} projectPath - 项目路径
|
|
381
|
+
* @param {string} username - 用户名
|
|
382
|
+
* @param {string} taskId - 任务ID
|
|
383
|
+
* @param {string|null} baseBranch - 基准分支(可选,默认自动查找 main/master)
|
|
384
|
+
* @returns {string} 创建的分支名称
|
|
385
|
+
*/
|
|
386
|
+
static createBranch(projectPath, username, taskId, baseBranch = null) {
|
|
387
|
+
// 记录操作前的分支上下文
|
|
388
|
+
this.logBranchContext(projectPath, 'Create Branch - Before');
|
|
389
|
+
|
|
390
|
+
// 生成新分支名称: fix_[username]_[taskid]_[timestamp]
|
|
391
|
+
const timestamp = Date.now();
|
|
392
|
+
let branchName = `fix_${username}_${taskId}_${timestamp}`;
|
|
393
|
+
|
|
394
|
+
logger.info('🌿 开始创建新分支', {
|
|
395
|
+
username,
|
|
396
|
+
taskId,
|
|
397
|
+
baseBranch: baseBranch || 'auto-detect',
|
|
398
|
+
projectPath: path.basename(projectPath)
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// 检查分支是否已存在
|
|
402
|
+
let attempt = 0;
|
|
403
|
+
while (this.branchExists(projectPath, branchName)) {
|
|
404
|
+
attempt++;
|
|
405
|
+
|
|
406
|
+
if (attempt > 10) {
|
|
407
|
+
const error = `无法生成唯一分支名,已尝试 ${attempt} 次`;
|
|
408
|
+
logger.error('❌ ' + error, {
|
|
409
|
+
username,
|
|
410
|
+
taskId,
|
|
411
|
+
lastAttempt: branchName,
|
|
412
|
+
projectPath: path.basename(projectPath)
|
|
413
|
+
});
|
|
414
|
+
throw new Error(error);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// 添加随机后缀
|
|
418
|
+
const randomSuffix = Math.random().toString(36).substr(2, 4);
|
|
419
|
+
const oldBranchName = branchName;
|
|
420
|
+
branchName = `fix_${username}_${taskId}_${timestamp}_${randomSuffix}`;
|
|
421
|
+
|
|
422
|
+
logger.warn(`⚠️ 分支名冲突,尝试新名称 (尝试 ${attempt}/10)`, {
|
|
423
|
+
oldName: oldBranchName,
|
|
424
|
+
newName: branchName,
|
|
425
|
+
projectPath: path.basename(projectPath)
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// 验证分支名称合法性
|
|
430
|
+
if (!this.validateBranchName(branchName)) {
|
|
431
|
+
const error = `分支名称不合法: ${branchName}`;
|
|
432
|
+
logger.error('❌ ' + error, {
|
|
433
|
+
branchName,
|
|
434
|
+
projectPath: path.basename(projectPath)
|
|
435
|
+
});
|
|
436
|
+
throw new Error(error);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
logger.info(`📝 分支名称验证通过: ${branchName}`);
|
|
440
|
+
|
|
441
|
+
// 如果指定了基准分支,直接使用
|
|
442
|
+
if (baseBranch) {
|
|
443
|
+
logger.info(`📌 从基准分支创建: ${baseBranch}`, {
|
|
444
|
+
targetBranch: branchName,
|
|
445
|
+
projectPath: path.basename(projectPath)
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
// 切换到基准分支前记录当前状态
|
|
450
|
+
const beforeInfo = this.getCurrentBranchInfo(projectPath);
|
|
451
|
+
|
|
452
|
+
ExecUtil.execSync(`git checkout "${baseBranch}"`, { cwd: projectPath });
|
|
453
|
+
|
|
454
|
+
const afterCheckoutInfo = this.getCurrentBranchInfo(projectPath);
|
|
455
|
+
logger.info(`✅ 已切换到基准分支`, {
|
|
456
|
+
from: beforeInfo.branch,
|
|
457
|
+
to: afterCheckoutInfo.branch,
|
|
458
|
+
commit: afterCheckoutInfo.commit,
|
|
459
|
+
projectPath: path.basename(projectPath)
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// 拉取最新代码
|
|
463
|
+
try {
|
|
464
|
+
ExecUtil.execSync('git pull', { cwd: projectPath });
|
|
465
|
+
const afterPullInfo = this.getCurrentBranchInfo(projectPath);
|
|
466
|
+
logger.info('✅ 基准分支已更新到最新', {
|
|
467
|
+
branch: baseBranch,
|
|
468
|
+
fromCommit: afterCheckoutInfo.commit,
|
|
469
|
+
toCommit: afterPullInfo.commit,
|
|
470
|
+
projectPath: path.basename(projectPath)
|
|
471
|
+
});
|
|
472
|
+
} catch (pullError) {
|
|
473
|
+
logger.warn('⚠️ 拉取基准分支失败,使用本地版本', {
|
|
474
|
+
branch: baseBranch,
|
|
475
|
+
error: pullError.message,
|
|
476
|
+
projectPath: path.basename(projectPath)
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
} catch (error) {
|
|
480
|
+
logger.error(`❌ 切换到基准分支 ${baseBranch} 失败`, {
|
|
481
|
+
baseBranch,
|
|
482
|
+
error: error.message,
|
|
483
|
+
projectPath: path.basename(projectPath),
|
|
484
|
+
suggestion: `请确认分支 ${baseBranch} 存在`
|
|
485
|
+
});
|
|
486
|
+
throw new Error(`基准分支 ${baseBranch} 不存在或无法切换`);
|
|
487
|
+
}
|
|
488
|
+
} else {
|
|
489
|
+
// 自动查找 main 或 master
|
|
490
|
+
logger.info('🔍 自动查找主分支 (main/master)...', {
|
|
491
|
+
projectPath: path.basename(projectPath)
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
try {
|
|
495
|
+
ExecUtil.execSync('git checkout main', { cwd: projectPath });
|
|
496
|
+
logger.info('✅ 找到并切换到 main 分支', {
|
|
497
|
+
projectPath: path.basename(projectPath)
|
|
498
|
+
});
|
|
499
|
+
} catch {
|
|
500
|
+
try {
|
|
501
|
+
ExecUtil.execSync('git checkout master', { cwd: projectPath });
|
|
502
|
+
logger.info('✅ 找到并切换到 master 分支', {
|
|
503
|
+
projectPath: path.basename(projectPath)
|
|
504
|
+
});
|
|
505
|
+
} catch {
|
|
506
|
+
const currentInfo = this.getCurrentBranchInfo(projectPath);
|
|
507
|
+
logger.warn('⚠️ 未找到 main 或 master 分支,使用当前分支', {
|
|
508
|
+
currentBranch: currentInfo.branch,
|
|
509
|
+
projectPath: path.basename(projectPath)
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// 创建并切换到新分支
|
|
516
|
+
const baseInfo = this.getCurrentBranchInfo(projectPath);
|
|
517
|
+
logger.info(`🔨 创建新分支: ${branchName}`, {
|
|
518
|
+
from: baseInfo.branch,
|
|
519
|
+
commit: baseInfo.commit,
|
|
520
|
+
projectPath: path.basename(projectPath)
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
try {
|
|
524
|
+
ExecUtil.execSync(`git checkout -b "${branchName}"`, { cwd: projectPath });
|
|
525
|
+
|
|
526
|
+
// 验证分支创建成功
|
|
527
|
+
const newBranchInfo = this.getCurrentBranchInfo(projectPath);
|
|
528
|
+
logger.info('✅ 分支创建成功', {
|
|
529
|
+
branchName,
|
|
530
|
+
basedOn: baseInfo.branch,
|
|
531
|
+
commit: newBranchInfo.commit,
|
|
532
|
+
projectPath: path.basename(projectPath)
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// 记录操作后的分支上下文
|
|
536
|
+
this.logBranchContext(projectPath, 'Create Branch - After');
|
|
537
|
+
|
|
538
|
+
return branchName;
|
|
539
|
+
} catch (error) {
|
|
540
|
+
logger.error('❌ 创建分支失败', {
|
|
541
|
+
branchName,
|
|
542
|
+
baseBranch: baseInfo.branch,
|
|
543
|
+
error: error.message,
|
|
544
|
+
projectPath: path.basename(projectPath)
|
|
545
|
+
});
|
|
546
|
+
throw error;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* 切换到现有分支(不创建新分支)
|
|
552
|
+
* @param {string} projectPath - 项目路径
|
|
553
|
+
* @param {string|null} baseBranch - 目标分支(可选,默认自动查找 main/master)
|
|
554
|
+
* @returns {string} 当前分支名称
|
|
555
|
+
*/
|
|
556
|
+
static checkoutBranch(projectPath, baseBranch = null) {
|
|
557
|
+
// 记录操作前的分支上下文
|
|
558
|
+
this.logBranchContext(projectPath, 'Checkout Branch - Before');
|
|
559
|
+
|
|
560
|
+
const beforeInfo = this.getCurrentBranchInfo(projectPath);
|
|
561
|
+
let targetBranch = baseBranch;
|
|
562
|
+
|
|
563
|
+
if (!targetBranch) {
|
|
564
|
+
// 自动查找 main 或 master
|
|
565
|
+
logger.info('🔍 自动查找主分支 (main/master)...', {
|
|
566
|
+
currentBranch: beforeInfo.branch,
|
|
567
|
+
projectPath: path.basename(projectPath)
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
try {
|
|
571
|
+
ExecUtil.execSync('git checkout main', { cwd: projectPath });
|
|
572
|
+
targetBranch = 'main';
|
|
573
|
+
logger.info('✅ 找到并切换到 main 分支', {
|
|
574
|
+
from: beforeInfo.branch,
|
|
575
|
+
to: 'main',
|
|
576
|
+
projectPath: path.basename(projectPath)
|
|
577
|
+
});
|
|
578
|
+
} catch {
|
|
579
|
+
try {
|
|
580
|
+
ExecUtil.execSync('git checkout master', { cwd: projectPath });
|
|
581
|
+
targetBranch = 'master';
|
|
582
|
+
logger.info('✅ 找到并切换到 master 分支', {
|
|
583
|
+
from: beforeInfo.branch,
|
|
584
|
+
to: 'master',
|
|
585
|
+
projectPath: path.basename(projectPath)
|
|
586
|
+
});
|
|
587
|
+
} catch {
|
|
588
|
+
// 获取当前分支名称
|
|
589
|
+
const currentBranch = ExecUtil.execSync('git rev-parse --abbrev-ref HEAD', {
|
|
590
|
+
cwd: projectPath,
|
|
591
|
+
}).trim();
|
|
592
|
+
logger.warn('⚠️ 未找到 main 或 master 分支,使用当前分支', {
|
|
593
|
+
currentBranch,
|
|
594
|
+
projectPath: path.basename(projectPath)
|
|
595
|
+
});
|
|
596
|
+
targetBranch = currentBranch;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
} else {
|
|
600
|
+
logger.info(`📌 切换到基准分支: ${targetBranch}`, {
|
|
601
|
+
from: beforeInfo.branch,
|
|
602
|
+
to: targetBranch,
|
|
603
|
+
projectPath: path.basename(projectPath)
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
try {
|
|
607
|
+
ExecUtil.execSync(`git checkout "${targetBranch}"`, { cwd: projectPath });
|
|
608
|
+
|
|
609
|
+
const afterCheckoutInfo = this.getCurrentBranchInfo(projectPath);
|
|
610
|
+
logger.info('✅ 分支切换成功', {
|
|
611
|
+
from: beforeInfo.branch,
|
|
612
|
+
to: afterCheckoutInfo.branch,
|
|
613
|
+
commit: afterCheckoutInfo.commit,
|
|
614
|
+
projectPath: path.basename(projectPath)
|
|
615
|
+
});
|
|
616
|
+
} catch (error) {
|
|
617
|
+
logger.error(`❌ 切换到分支 ${targetBranch} 失败`, {
|
|
618
|
+
targetBranch,
|
|
619
|
+
currentBranch: beforeInfo.branch,
|
|
620
|
+
error: error.message,
|
|
621
|
+
projectPath: path.basename(projectPath),
|
|
622
|
+
suggestion: `请确认分支 ${targetBranch} 存在`
|
|
623
|
+
});
|
|
624
|
+
throw new Error(`分支 ${targetBranch} 不存在或无法切换`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// 拉取最新代码
|
|
629
|
+
const beforePullInfo = this.getCurrentBranchInfo(projectPath);
|
|
630
|
+
try {
|
|
631
|
+
ExecUtil.execSync('git pull', { cwd: projectPath });
|
|
632
|
+
|
|
633
|
+
const afterPullInfo = this.getCurrentBranchInfo(projectPath);
|
|
634
|
+
logger.info(`✅ 已切换到分支 ${targetBranch} 并拉取最新代码`, {
|
|
635
|
+
branch: targetBranch,
|
|
636
|
+
fromCommit: beforePullInfo.commit,
|
|
637
|
+
toCommit: afterPullInfo.commit,
|
|
638
|
+
projectPath: path.basename(projectPath)
|
|
639
|
+
});
|
|
640
|
+
} catch (error) {
|
|
641
|
+
logger.warn('⚠️ 拉取最新代码失败', {
|
|
642
|
+
branch: targetBranch,
|
|
643
|
+
commit: beforePullInfo.commit,
|
|
644
|
+
error: error.message,
|
|
645
|
+
projectPath: path.basename(projectPath),
|
|
646
|
+
suggestion: '请检查网络连接或分支是否有远程跟踪分支'
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// 记录操作后的分支上下文
|
|
651
|
+
this.logBranchContext(projectPath, 'Checkout Branch - After');
|
|
652
|
+
|
|
653
|
+
return targetBranch;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* 提交更改
|
|
658
|
+
* @param {string} projectPath - 项目路径
|
|
659
|
+
* @param {string} commitMessage - 提交信息
|
|
660
|
+
*/
|
|
661
|
+
static commit(projectPath, commitMessage) {
|
|
662
|
+
// 记录操作前的分支上下文
|
|
663
|
+
this.logBranchContext(projectPath, 'Commit - Before');
|
|
664
|
+
|
|
665
|
+
const branchInfo = this.getCurrentBranchInfo(projectPath);
|
|
666
|
+
logger.info('📝 提交更改...', {
|
|
667
|
+
branch: branchInfo.branch,
|
|
668
|
+
currentCommit: branchInfo.commit,
|
|
669
|
+
projectPath: path.basename(projectPath)
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
// 检查是否有更改
|
|
673
|
+
const status = ExecUtil.execSync('git status --porcelain', { cwd: projectPath });
|
|
674
|
+
if (!status.trim()) {
|
|
675
|
+
logger.info('ℹ️ 没有文件更改,跳过提交', {
|
|
676
|
+
branch: branchInfo.branch,
|
|
677
|
+
commit: branchInfo.commit,
|
|
678
|
+
projectPath: path.basename(projectPath)
|
|
679
|
+
});
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// 统计变更文件
|
|
684
|
+
const changedFiles = status.trim().split('\n');
|
|
685
|
+
const fileStats = {
|
|
686
|
+
total: changedFiles.length,
|
|
687
|
+
modified: changedFiles.filter(f => f.startsWith(' M')).length,
|
|
688
|
+
added: changedFiles.filter(f => f.startsWith('??') || f.startsWith('A ')).length,
|
|
689
|
+
deleted: changedFiles.filter(f => f.startsWith(' D')).length,
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
logger.info('📊 检测到文件变更', {
|
|
693
|
+
branch: branchInfo.branch,
|
|
694
|
+
...fileStats,
|
|
695
|
+
files: changedFiles.map(f => f.trim()).slice(0, 10), // 只显示前10个文件
|
|
696
|
+
projectPath: path.basename(projectPath)
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
try {
|
|
700
|
+
// 添加所有更改
|
|
701
|
+
ExecUtil.execSync('git add .', { cwd: projectPath });
|
|
702
|
+
logger.debug('✅ 文件已暂存', {
|
|
703
|
+
branch: branchInfo.branch,
|
|
704
|
+
fileCount: fileStats.total,
|
|
705
|
+
projectPath: path.basename(projectPath)
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
// 提交
|
|
709
|
+
ExecUtil.execSync(`git commit -m "${commitMessage}"`, { cwd: projectPath });
|
|
710
|
+
|
|
711
|
+
// 获取新的提交信息
|
|
712
|
+
const afterInfo = this.getCurrentBranchInfo(projectPath);
|
|
713
|
+
logger.info('✅ 更改已提交', {
|
|
714
|
+
branch: afterInfo.branch,
|
|
715
|
+
fromCommit: branchInfo.commit,
|
|
716
|
+
toCommit: afterInfo.commit,
|
|
717
|
+
commitMessage: commitMessage.substring(0, 100),
|
|
718
|
+
changedFiles: fileStats.total,
|
|
719
|
+
projectPath: path.basename(projectPath)
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
// 记录操作后的分支上下文
|
|
723
|
+
this.logBranchContext(projectPath, 'Commit - After');
|
|
724
|
+
|
|
725
|
+
return true;
|
|
726
|
+
} catch (error) {
|
|
727
|
+
logger.error('❌ 提交失败', {
|
|
728
|
+
branch: branchInfo.branch,
|
|
729
|
+
commit: branchInfo.commit,
|
|
730
|
+
error: error.message,
|
|
731
|
+
projectPath: path.basename(projectPath)
|
|
732
|
+
});
|
|
733
|
+
throw error;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* 推送分支(带重试)
|
|
739
|
+
* @param {string} projectPath - 项目路径
|
|
740
|
+
* @param {string} branchName - 分支名称
|
|
741
|
+
*/
|
|
742
|
+
static async push(projectPath, branchName) {
|
|
743
|
+
// 记录操作前的分支上下文
|
|
744
|
+
this.logBranchContext(projectPath, 'Push - Before');
|
|
745
|
+
|
|
746
|
+
const branchInfo = this.getCurrentBranchInfo(projectPath);
|
|
747
|
+
logger.info('📤 推送到远程仓库...', {
|
|
748
|
+
branch: branchName,
|
|
749
|
+
currentBranch: branchInfo.branch,
|
|
750
|
+
commit: branchInfo.commit,
|
|
751
|
+
remote: branchInfo.remote,
|
|
752
|
+
projectPath: path.basename(projectPath)
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
try {
|
|
756
|
+
await RetryUtil.withRetry(
|
|
757
|
+
() => {
|
|
758
|
+
const output = ExecUtil.execSync(`git push origin "${branchName}"`, {
|
|
759
|
+
cwd: projectPath,
|
|
760
|
+
});
|
|
761
|
+
logger.debug('Git push 输出:', {
|
|
762
|
+
branch: branchName,
|
|
763
|
+
output: output.trim(),
|
|
764
|
+
projectPath: path.basename(projectPath)
|
|
765
|
+
});
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
maxRetries: 3,
|
|
769
|
+
delay: 2000,
|
|
770
|
+
operation: 'Git 推送',
|
|
771
|
+
shouldRetry: (error) => {
|
|
772
|
+
// 记录推送失败的详细信息
|
|
773
|
+
logger.warn('Git 推送失败,分析错误原因:', {
|
|
774
|
+
branch: branchName,
|
|
775
|
+
currentBranch: branchInfo.branch,
|
|
776
|
+
commit: branchInfo.commit,
|
|
777
|
+
remote: branchInfo.remote,
|
|
778
|
+
error: error.message,
|
|
779
|
+
projectPath: path.basename(projectPath)
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
// 网络错误才重试,权限错误不重试
|
|
783
|
+
const message = error.message.toLowerCase();
|
|
784
|
+
const isNetworkError = message.includes('network') ||
|
|
785
|
+
message.includes('timeout') ||
|
|
786
|
+
message.includes('connection') ||
|
|
787
|
+
message.includes('failed to connect');
|
|
788
|
+
const isPermissionError = message.includes('permission') ||
|
|
789
|
+
message.includes('authentication') ||
|
|
790
|
+
message.includes('denied');
|
|
791
|
+
|
|
792
|
+
if (isPermissionError) {
|
|
793
|
+
logger.error('❌ 检测到权限错误,不进行重试', {
|
|
794
|
+
branch: branchName,
|
|
795
|
+
error: error.message,
|
|
796
|
+
suggestion: '请检查 Git 凭证配置或远程仓库访问权限'
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
return isNetworkError && !isPermissionError;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
);
|
|
804
|
+
|
|
805
|
+
// 推送成功后记录
|
|
806
|
+
const afterInfo = this.getCurrentBranchInfo(projectPath);
|
|
807
|
+
logger.info('✅ 推送成功', {
|
|
808
|
+
branch: branchName,
|
|
809
|
+
commit: afterInfo.commit,
|
|
810
|
+
remote: afterInfo.remote,
|
|
811
|
+
projectPath: path.basename(projectPath)
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
// 记录操作后的分支上下文
|
|
815
|
+
this.logBranchContext(projectPath, 'Push - After');
|
|
816
|
+
|
|
817
|
+
return true;
|
|
818
|
+
} catch (error) {
|
|
819
|
+
logger.error('❌ 推送失败', {
|
|
820
|
+
branch: branchName,
|
|
821
|
+
currentBranch: branchInfo.branch,
|
|
822
|
+
commit: branchInfo.commit,
|
|
823
|
+
remote: branchInfo.remote,
|
|
824
|
+
error: error.message,
|
|
825
|
+
projectPath: path.basename(projectPath),
|
|
826
|
+
suggestion: '请检查网络连接、远程仓库访问权限或分支是否已存在'
|
|
827
|
+
});
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* 运行测试(可选)
|
|
834
|
+
* @param {string} projectPath - 项目路径
|
|
835
|
+
* @returns {boolean} 测试是否通过
|
|
836
|
+
*/
|
|
837
|
+
static runTests(projectPath) {
|
|
838
|
+
logger.info('🧪 运行测试...');
|
|
839
|
+
|
|
840
|
+
// 检查是否有测试脚本
|
|
841
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
842
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
843
|
+
logger.info('ℹ️ 未找到 package.json,跳过测试');
|
|
844
|
+
return true;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
848
|
+
if (!packageJson.scripts || !packageJson.scripts.test) {
|
|
849
|
+
logger.info('ℹ️ 未配置测试脚本,跳过测试');
|
|
850
|
+
return true;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
try {
|
|
854
|
+
ExecUtil.execSync('npm test', {
|
|
855
|
+
cwd: projectPath,
|
|
856
|
+
stdio: 'inherit' // 测试需要实时输出
|
|
857
|
+
});
|
|
858
|
+
logger.info('✅ 测试通过');
|
|
859
|
+
return true;
|
|
860
|
+
} catch (error) {
|
|
861
|
+
logger.error('❌ 测试失败');
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* 获取项目名称
|
|
868
|
+
* @param {string} projectPath - 项目路径
|
|
869
|
+
* @returns {string}
|
|
870
|
+
*/
|
|
871
|
+
static getProjectName(projectPath) {
|
|
872
|
+
return path.basename(projectPath);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* 合并分支到目标分支
|
|
877
|
+
* @param {string} projectPath - 项目路径
|
|
878
|
+
* @param {string} sourceBranch - 源分支
|
|
879
|
+
* @param {string} targetBranch - 目标分支
|
|
880
|
+
* @returns {boolean} 是否合并成功
|
|
881
|
+
*/
|
|
882
|
+
static mergeBranch(projectPath, sourceBranch, targetBranch) {
|
|
883
|
+
// 记录操作前的分支上下文
|
|
884
|
+
this.logBranchContext(projectPath, 'Merge - Before');
|
|
885
|
+
|
|
886
|
+
const beforeInfo = this.getCurrentBranchInfo(projectPath);
|
|
887
|
+
logger.info(`🔀 合并分支 ${sourceBranch} 到 ${targetBranch}...`, {
|
|
888
|
+
sourceBranch,
|
|
889
|
+
targetBranch,
|
|
890
|
+
currentBranch: beforeInfo.branch,
|
|
891
|
+
currentCommit: beforeInfo.commit,
|
|
892
|
+
projectPath: path.basename(projectPath)
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
try {
|
|
896
|
+
// 切换到目标分支
|
|
897
|
+
ExecUtil.execSync(`git checkout "${targetBranch}"`, { cwd: projectPath });
|
|
898
|
+
|
|
899
|
+
const afterCheckoutInfo = this.getCurrentBranchInfo(projectPath);
|
|
900
|
+
logger.info(`✅ 已切换到目标分支 ${targetBranch}`, {
|
|
901
|
+
from: beforeInfo.branch,
|
|
902
|
+
to: afterCheckoutInfo.branch,
|
|
903
|
+
commit: afterCheckoutInfo.commit,
|
|
904
|
+
projectPath: path.basename(projectPath)
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
// 拉取最新代码
|
|
908
|
+
const beforePullInfo = this.getCurrentBranchInfo(projectPath);
|
|
909
|
+
try {
|
|
910
|
+
ExecUtil.execSync('git pull', { cwd: projectPath });
|
|
911
|
+
const afterPullInfo = this.getCurrentBranchInfo(projectPath);
|
|
912
|
+
logger.info('✅ 目标分支已更新', {
|
|
913
|
+
branch: targetBranch,
|
|
914
|
+
fromCommit: beforePullInfo.commit,
|
|
915
|
+
toCommit: afterPullInfo.commit,
|
|
916
|
+
projectPath: path.basename(projectPath)
|
|
917
|
+
});
|
|
918
|
+
} catch (pullError) {
|
|
919
|
+
logger.warn('⚠️ 拉取目标分支失败,使用本地版本', {
|
|
920
|
+
branch: targetBranch,
|
|
921
|
+
commit: beforePullInfo.commit,
|
|
922
|
+
error: pullError.message,
|
|
923
|
+
projectPath: path.basename(projectPath)
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// 合并源分支
|
|
928
|
+
const beforeMergeInfo = this.getCurrentBranchInfo(projectPath);
|
|
929
|
+
logger.info(`🔀 开始合并 ${sourceBranch}...`, {
|
|
930
|
+
sourceBranch,
|
|
931
|
+
targetBranch,
|
|
932
|
+
targetCommit: beforeMergeInfo.commit,
|
|
933
|
+
projectPath: path.basename(projectPath)
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
ExecUtil.execSync(`git merge "${sourceBranch}" --no-ff -m "Merge branch '${sourceBranch}' into ${targetBranch}"`, {
|
|
937
|
+
cwd: projectPath,
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
// 获取合并后的信息
|
|
941
|
+
const afterMergeInfo = this.getCurrentBranchInfo(projectPath);
|
|
942
|
+
logger.info(`✅ 成功合并到 ${targetBranch}`, {
|
|
943
|
+
sourceBranch,
|
|
944
|
+
targetBranch,
|
|
945
|
+
beforeCommit: beforeMergeInfo.commit,
|
|
946
|
+
afterCommit: afterMergeInfo.commit,
|
|
947
|
+
commitMessage: afterMergeInfo.commitMessage,
|
|
948
|
+
projectPath: path.basename(projectPath)
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
// 记录操作后的分支上下文
|
|
952
|
+
this.logBranchContext(projectPath, 'Merge - After');
|
|
953
|
+
|
|
954
|
+
return true;
|
|
955
|
+
} catch (error) {
|
|
956
|
+
const currentInfo = this.getCurrentBranchInfo(projectPath);
|
|
957
|
+
logger.error(`❌ 合并到 ${targetBranch} 失败`, {
|
|
958
|
+
sourceBranch,
|
|
959
|
+
targetBranch,
|
|
960
|
+
currentBranch: currentInfo.branch,
|
|
961
|
+
currentCommit: currentInfo.commit,
|
|
962
|
+
error: error.message,
|
|
963
|
+
projectPath: path.basename(projectPath),
|
|
964
|
+
suggestion: '可能存在冲突,请手动解决或检查分支是否存在'
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
// 尝试中止合并
|
|
968
|
+
try {
|
|
969
|
+
ExecUtil.execSync('git merge --abort', { cwd: projectPath });
|
|
970
|
+
logger.info('✅ 已中止合并操作', {
|
|
971
|
+
targetBranch,
|
|
972
|
+
projectPath: path.basename(projectPath)
|
|
973
|
+
});
|
|
974
|
+
} catch (abortError) {
|
|
975
|
+
logger.warn('⚠️ 中止合并失败', {
|
|
976
|
+
targetBranch,
|
|
977
|
+
error: abortError.message,
|
|
978
|
+
projectPath: path.basename(projectPath)
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
return false;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* 批量合并到多个目标分支(异步)
|
|
988
|
+
* @param {string} projectPath - 项目路径
|
|
989
|
+
* @param {string} sourceBranch - 源分支
|
|
990
|
+
* @param {string[]} targetBranches - 目标分支数组
|
|
991
|
+
* @returns {Promise<Object>} 合并结果 { branch: success }
|
|
992
|
+
*/
|
|
993
|
+
static async mergeToMultipleBranches(projectPath, sourceBranch, targetBranches) {
|
|
994
|
+
const results = {};
|
|
995
|
+
|
|
996
|
+
if (!targetBranches || targetBranches.length === 0) {
|
|
997
|
+
logger.info('ℹ️ 未指定合并目标分支,跳过合并', {
|
|
998
|
+
sourceBranch,
|
|
999
|
+
projectPath: path.basename(projectPath)
|
|
1000
|
+
});
|
|
1001
|
+
return results;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const beforeInfo = this.getCurrentBranchInfo(projectPath);
|
|
1005
|
+
logger.info(`🔀 开始批量合并`, {
|
|
1006
|
+
sourceBranch,
|
|
1007
|
+
targetBranches: targetBranches.join(', '),
|
|
1008
|
+
currentBranch: beforeInfo.branch,
|
|
1009
|
+
currentCommit: beforeInfo.commit,
|
|
1010
|
+
totalTargets: targetBranches.length,
|
|
1011
|
+
projectPath: path.basename(projectPath)
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
let successCount = 0;
|
|
1015
|
+
let failedCount = 0;
|
|
1016
|
+
|
|
1017
|
+
for (const targetBranch of targetBranches) {
|
|
1018
|
+
logger.info(`📋 处理目标分支 ${targetBranch} (${Object.keys(results).length + 1}/${targetBranches.length})`, {
|
|
1019
|
+
sourceBranch,
|
|
1020
|
+
targetBranch,
|
|
1021
|
+
projectPath: path.basename(projectPath)
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
const success = this.mergeBranch(projectPath, sourceBranch, targetBranch);
|
|
1025
|
+
results[targetBranch] = success;
|
|
1026
|
+
|
|
1027
|
+
if (success) {
|
|
1028
|
+
successCount++;
|
|
1029
|
+
} else {
|
|
1030
|
+
failedCount++;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// 如果合并成功,推送目标分支
|
|
1034
|
+
if (success) {
|
|
1035
|
+
logger.info(`📤 推送合并后的分支 ${targetBranch}...`, {
|
|
1036
|
+
sourceBranch,
|
|
1037
|
+
targetBranch,
|
|
1038
|
+
projectPath: path.basename(projectPath)
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
try {
|
|
1042
|
+
await RetryUtil.withRetry(
|
|
1043
|
+
() => {
|
|
1044
|
+
ExecUtil.execSync(`git push origin "${targetBranch}"`, {
|
|
1045
|
+
cwd: projectPath,
|
|
1046
|
+
});
|
|
1047
|
+
},
|
|
1048
|
+
{
|
|
1049
|
+
maxRetries: 3,
|
|
1050
|
+
delay: 2000,
|
|
1051
|
+
operation: `推送 ${targetBranch}`,
|
|
1052
|
+
}
|
|
1053
|
+
);
|
|
1054
|
+
|
|
1055
|
+
const pushedInfo = this.getCurrentBranchInfo(projectPath);
|
|
1056
|
+
logger.info(`✅ ${targetBranch} 已推送到远程`, {
|
|
1057
|
+
branch: targetBranch,
|
|
1058
|
+
commit: pushedInfo.commit,
|
|
1059
|
+
remote: pushedInfo.remote,
|
|
1060
|
+
projectPath: path.basename(projectPath)
|
|
1061
|
+
});
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
logger.error(`❌ 推送 ${targetBranch} 失败`, {
|
|
1064
|
+
branch: targetBranch,
|
|
1065
|
+
error: error.message,
|
|
1066
|
+
projectPath: path.basename(projectPath),
|
|
1067
|
+
suggestion: '合并成功但推送失败,可稍后手动推送'
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// 切回源分支
|
|
1074
|
+
try {
|
|
1075
|
+
ExecUtil.execSync(`git checkout "${sourceBranch}"`, { cwd: projectPath });
|
|
1076
|
+
const finalInfo = this.getCurrentBranchInfo(projectPath);
|
|
1077
|
+
logger.info('✅ 已切回源分支', {
|
|
1078
|
+
sourceBranch,
|
|
1079
|
+
branch: finalInfo.branch,
|
|
1080
|
+
commit: finalInfo.commit,
|
|
1081
|
+
projectPath: path.basename(projectPath)
|
|
1082
|
+
});
|
|
1083
|
+
} catch (error) {
|
|
1084
|
+
logger.warn(`⚠️ 无法切回源分支 ${sourceBranch}`, {
|
|
1085
|
+
sourceBranch,
|
|
1086
|
+
error: error.message,
|
|
1087
|
+
projectPath: path.basename(projectPath),
|
|
1088
|
+
suggestion: '请手动切换到源分支'
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// 输出批量合并总结
|
|
1093
|
+
logger.info('📊 批量合并完成', {
|
|
1094
|
+
sourceBranch,
|
|
1095
|
+
totalTargets: targetBranches.length,
|
|
1096
|
+
successCount,
|
|
1097
|
+
failedCount,
|
|
1098
|
+
results,
|
|
1099
|
+
projectPath: path.basename(projectPath)
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
return results;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* 检查分支是否存在
|
|
1107
|
+
* @param {string} projectPath - 项目路径
|
|
1108
|
+
* @param {string} branchName - 分支名称
|
|
1109
|
+
* @returns {boolean} 分支是否存在
|
|
1110
|
+
*/
|
|
1111
|
+
static branchExists(projectPath, branchName) {
|
|
1112
|
+
try {
|
|
1113
|
+
ExecUtil.execSync(`git rev-parse --verify "${branchName}"`, {
|
|
1114
|
+
cwd: projectPath,
|
|
1115
|
+
stdio: 'ignore',
|
|
1116
|
+
});
|
|
1117
|
+
return true;
|
|
1118
|
+
} catch {
|
|
1119
|
+
return false;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* 验证分支名称合法性
|
|
1125
|
+
* @param {string} branchName - 分支名称
|
|
1126
|
+
* @returns {boolean} 是否合法
|
|
1127
|
+
*/
|
|
1128
|
+
static validateBranchName(branchName) {
|
|
1129
|
+
// Git 分支名规则
|
|
1130
|
+
const invalidPatterns = [
|
|
1131
|
+
/\.\./, // 不能包含 ..
|
|
1132
|
+
/^[.-]/, // 不能以 . 或 - 开头
|
|
1133
|
+
/[\/.]$/, // 不能以 / 或 . 结尾
|
|
1134
|
+
/\/\//, // 不能包含连续的 /
|
|
1135
|
+
/[@{\\^~:?*\[\]]/, // 不能包含特殊字符
|
|
1136
|
+
];
|
|
1137
|
+
|
|
1138
|
+
for (const pattern of invalidPatterns) {
|
|
1139
|
+
if (pattern.test(branchName)) {
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// 长度限制
|
|
1145
|
+
if (branchName.length > 255) {
|
|
1146
|
+
return false;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
return true;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* 获取工作区的 Git Diff 详细信息
|
|
1154
|
+
* @param {string} projectPath - 项目路径
|
|
1155
|
+
* @returns {Object} Git diff 结果
|
|
1156
|
+
*/
|
|
1157
|
+
static getWorkspaceDiff(projectPath) {
|
|
1158
|
+
const result = {
|
|
1159
|
+
fullDiff: "",
|
|
1160
|
+
actualChangedFiles: [],
|
|
1161
|
+
untrackedFiles: [],
|
|
1162
|
+
stagedFiles: [],
|
|
1163
|
+
unstagedFiles: [],
|
|
1164
|
+
fileChanges: [],
|
|
1165
|
+
};
|
|
1166
|
+
|
|
1167
|
+
try {
|
|
1168
|
+
// 1. 获取完整的 diff
|
|
1169
|
+
try {
|
|
1170
|
+
result.fullDiff = ExecUtil.execSync("git diff HEAD", {
|
|
1171
|
+
cwd: projectPath,
|
|
1172
|
+
});
|
|
1173
|
+
} catch (err) {
|
|
1174
|
+
logger.warn("获取完整 diff 失败:", err.message);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// 2. 获取已修改文件列表(已跟踪的文件)
|
|
1178
|
+
let modifiedFiles = [];
|
|
1179
|
+
try {
|
|
1180
|
+
const diffOutput = ExecUtil.execSync("git diff HEAD --name-only", {
|
|
1181
|
+
cwd: projectPath,
|
|
1182
|
+
});
|
|
1183
|
+
modifiedFiles = diffOutput.split("\n").filter(Boolean);
|
|
1184
|
+
} catch (err) {
|
|
1185
|
+
logger.warn("获取已修改文件失败:", err.message);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// 3. 获取未跟踪的新文件
|
|
1189
|
+
try {
|
|
1190
|
+
const untrackedOutput = ExecUtil.execSync(
|
|
1191
|
+
"git ls-files --others --exclude-standard",
|
|
1192
|
+
{
|
|
1193
|
+
cwd: projectPath,
|
|
1194
|
+
}
|
|
1195
|
+
);
|
|
1196
|
+
result.untrackedFiles = untrackedOutput.split("\n").filter(Boolean);
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
// 没有未跟踪文件或命令失败
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// 4. 合并文件列表
|
|
1202
|
+
result.actualChangedFiles = [...modifiedFiles, ...result.untrackedFiles];
|
|
1203
|
+
|
|
1204
|
+
// 5. 获取暂存文件列表
|
|
1205
|
+
try {
|
|
1206
|
+
const stagedOutput = ExecUtil.execSync("git diff --cached --name-only", {
|
|
1207
|
+
cwd: projectPath,
|
|
1208
|
+
});
|
|
1209
|
+
result.stagedFiles = stagedOutput.split("\n").filter(Boolean);
|
|
1210
|
+
} catch (err) {
|
|
1211
|
+
// 没有暂存文件
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// 6. 获取未暂存文件列表
|
|
1215
|
+
try {
|
|
1216
|
+
const unstagedOutput = ExecUtil.execSync("git diff --name-only", {
|
|
1217
|
+
cwd: projectPath,
|
|
1218
|
+
});
|
|
1219
|
+
result.unstagedFiles = unstagedOutput.split("\n").filter(Boolean);
|
|
1220
|
+
} catch (err) {
|
|
1221
|
+
// 没有未暂存文件
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// 7. 为每个文件获取详细信息
|
|
1225
|
+
result.fileChanges = result.actualChangedFiles.map((filePath) => {
|
|
1226
|
+
const isUntracked = result.untrackedFiles.includes(filePath);
|
|
1227
|
+
|
|
1228
|
+
if (isUntracked) {
|
|
1229
|
+
// 新文件,读取完整内容
|
|
1230
|
+
const fullPath = path.join(projectPath, filePath);
|
|
1231
|
+
const content = fs.existsSync(fullPath)
|
|
1232
|
+
? fs.readFileSync(fullPath, "utf-8")
|
|
1233
|
+
: null;
|
|
1234
|
+
|
|
1235
|
+
return {
|
|
1236
|
+
file: filePath,
|
|
1237
|
+
diff: null,
|
|
1238
|
+
content,
|
|
1239
|
+
isNewFile: true,
|
|
1240
|
+
isUntracked: true,
|
|
1241
|
+
isStaged: result.stagedFiles.includes(filePath),
|
|
1242
|
+
};
|
|
1243
|
+
} else {
|
|
1244
|
+
// 已跟踪文件,获取 diff
|
|
1245
|
+
try {
|
|
1246
|
+
const fileDiff = ExecUtil.execSync(`git diff HEAD -- "${filePath}"`, {
|
|
1247
|
+
cwd: projectPath,
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
return {
|
|
1251
|
+
file: filePath,
|
|
1252
|
+
diff: fileDiff,
|
|
1253
|
+
content: null,
|
|
1254
|
+
isNewFile: false,
|
|
1255
|
+
isUntracked: false,
|
|
1256
|
+
isStaged: result.stagedFiles.includes(filePath),
|
|
1257
|
+
isUnstaged: result.unstagedFiles.includes(filePath),
|
|
1258
|
+
};
|
|
1259
|
+
} catch (err) {
|
|
1260
|
+
return {
|
|
1261
|
+
file: filePath,
|
|
1262
|
+
error: err.message,
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
logger.debug(`📊 Git Diff 结果: ${result.actualChangedFiles.length} 个文件变更`);
|
|
1269
|
+
return result;
|
|
1270
|
+
} catch (error) {
|
|
1271
|
+
logger.error("❌ 获取 Git diff 失败:", error.message);
|
|
1272
|
+
return result; // 返回空结果
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
|
|
1278
|
+
module.exports = GitService;
|