@icode-js/icode 3.0.4 → 3.2.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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -5
  3. package/bin/icode.js +4 -4
  4. package/package.json +1 -1
  5. package/src/cli.js +60 -23
  6. package/src/commands/ai.js +4 -4
  7. package/src/commands/checkout.js +2 -2
  8. package/src/commands/clean.js +5 -2
  9. package/src/commands/codereview.js +5 -2
  10. package/src/commands/completion.js +93 -0
  11. package/src/commands/config.js +123 -10
  12. package/src/commands/doctor.js +80 -0
  13. package/src/commands/explain.js +6 -3
  14. package/src/commands/help.js +17 -0
  15. package/src/commands/info.js +5 -2
  16. package/src/commands/migrate.js +40 -3
  17. package/src/commands/push.js +72 -24
  18. package/src/commands/ready.js +110 -0
  19. package/src/commands/save.js +98 -0
  20. package/src/commands/sync.js +2 -2
  21. package/src/commands/tag.js +2 -2
  22. package/src/commands/undo.js +73 -10
  23. package/src/core/{ai-client.js → ai/client.js} +3 -3
  24. package/src/core/{ai-config.js → ai/config.js} +2 -2
  25. package/src/core/ai/diff-prompt.js +204 -0
  26. package/src/core/{ai-diff-range.js → ai/diff-range.js} +2 -2
  27. package/src/core/{args.js → cli/args.js} +23 -2
  28. package/src/core/cli/completion.js +720 -0
  29. package/src/core/cli/update-notifier.js +138 -0
  30. package/src/core/{shell.js → commands/shell.js} +13 -1
  31. package/src/core/{config-store.js → config/store.js} +53 -3
  32. package/src/core/{git-context.js → git/context.js} +24 -10
  33. package/src/core/git/preconditions.js +88 -0
  34. package/src/core/{git-service.js → git/service.js} +195 -5
  35. package/src/core/remote-merge/client.js +42 -0
  36. package/src/core/remote-merge/providers/gitlab.js +596 -0
  37. package/src/core/remote-merge/shared.js +49 -0
  38. package/src/core/tools/interactive.js +368 -0
  39. package/src/core/{loading.js → tools/loading.js} +5 -1
  40. package/src/core/{logger.js → tools/logger.js} +5 -0
  41. package/src/workflows/ai-codereview-workflow.js +18 -6
  42. package/src/workflows/ai-commit-workflow.js +20 -12
  43. package/src/workflows/ai-conflict-workflow.js +4 -4
  44. package/src/workflows/ai-explain-workflow.js +4 -4
  45. package/src/workflows/ai-risk-review-workflow.js +1 -1
  46. package/src/workflows/checkout-workflow.js +17 -5
  47. package/src/workflows/clean-workflow.js +92 -75
  48. package/src/workflows/doctor-workflow.js +131 -0
  49. package/src/workflows/info-workflow.js +4 -4
  50. package/src/workflows/migrate-workflow.js +269 -62
  51. package/src/workflows/push-workflow.js +475 -58
  52. package/src/workflows/ready-workflow.js +278 -0
  53. package/src/workflows/rollback-workflow.js +24 -9
  54. package/src/workflows/save-workflow.js +152 -0
  55. package/src/workflows/sync-workflow.js +22 -6
  56. package/src/workflows/tag-workflow.js +4 -4
  57. package/src/workflows/undo-workflow.js +37 -12
  58. package/src/core/prompts.js +0 -152
  59. /package/src/core/{ai-commit-summary.js → ai/commit-summary.js} +0 -0
  60. /package/src/core/{commit-conventions.js → git/commit-conventions.js} +0 -0
  61. /package/src/core/{hook-diagnostics.js → git/hook-diagnostics.js} +0 -0
@@ -1,16 +1,69 @@
1
- import { getRepoPolicy } from '../core/config-store.js'
1
+ import { formatAiCommitSummary } from '../core/ai/commit-summary.js'
2
+ import { getPlatformConfig, getRepoPolicy } from '../core/config/store.js'
2
3
  import { IcodeError } from '../core/errors.js'
3
- import { GitService } from '../core/git-service.js'
4
- import { resolveGitContext } from '../core/git-context.js'
5
- import { formatAiCommitSummary } from '../core/ai-commit-summary.js'
6
- import { logger } from '../core/logger.js'
7
- import { confirm, input } from '../core/prompts.js'
4
+ import { resolveGitContext } from '../core/git/context.js'
5
+ import {
6
+ assertNoConflictedFiles,
7
+ assertNoPendingOperation,
8
+ readGitSafetyState
9
+ } from '../core/git/preconditions.js'
10
+ import { GitService } from '../core/git/service.js'
11
+ import { confirm, input } from '../core/tools/interactive.js'
12
+ import { requestRemoteMerge, resolveRemoteMergeProjectUrl } from '../core/remote-merge/client.js'
13
+ import { logger } from '../core/tools/logger.js'
8
14
  import { runAiCommitWorkflow } from './ai-commit-workflow.js'
9
15
 
10
16
  function uniqueBranches(branches) {
11
17
  return [...new Set(branches.map((item) => item.trim()).filter(Boolean))]
12
18
  }
13
19
 
20
+ // 基于 commit 内容构建远程 MR 标题与正文,确保标题和内容都来自真实提交信息。
21
+ async function buildRemoteMergeCommitContent(git, sourceBranch, targetBranch) {
22
+ const rangeSpec = `origin/${targetBranch}..${sourceBranch}`
23
+ let commits = await git.listCommitMessages(rangeSpec)
24
+
25
+ if (!commits.length) {
26
+ const latestCommit = await git.getCommitMessage(sourceBranch)
27
+ if (latestCommit) {
28
+ commits = [latestCommit]
29
+ }
30
+ }
31
+
32
+ if (!commits.length) {
33
+ throw new IcodeError(`无法读取远程合并提交内容: ${sourceBranch} -> ${targetBranch}`, {
34
+ code: 'PUSH_REMOTE_MERGE_COMMIT_CONTENT_EMPTY',
35
+ exitCode: 2
36
+ })
37
+ }
38
+
39
+ const latestCommit = commits[commits.length - 1]
40
+ const description = commits
41
+ .map((commit) => {
42
+ if (commit.body) {
43
+ return `${commit.subject}\n\n${commit.body}`
44
+ }
45
+ return commit.subject
46
+ })
47
+ .join('\n\n')
48
+
49
+ return {
50
+ title: latestCommit.subject,
51
+ description
52
+ }
53
+ }
54
+
55
+ // 提取远程合并失败原因,优先使用 IcodeError.meta,其次回退到 error.message。
56
+ function resolveRemoteMergeFailureReason(error) {
57
+ const metaStep = typeof error?.meta?.step === 'string' ? error.meta.step.trim() : ''
58
+ const metaReason = typeof error?.meta?.reason === 'string' ? error.meta.reason.trim() : ''
59
+ if (metaReason) {
60
+ return metaStep ? `${metaReason} [step=${metaStep}]` : metaReason
61
+ }
62
+
63
+ const message = typeof error?.message === 'string' ? error.message.trim() : ''
64
+ return message || '远程合并失败'
65
+ }
66
+
14
67
  // Run AI commit before push and keep the full generated message in follow-up logs.
15
68
  async function prepareAiCommitIfEnabled(inputOptions) {
16
69
  if (!inputOptions.aiCommit) {
@@ -64,9 +117,9 @@ async function prepareAiCommitIfEnabled(inputOptions) {
64
117
  }
65
118
  }
66
119
 
67
- async function prepareCommitIfNeeded(git, options) {
68
- const hasChanges = await git.hasChanges()
69
- if (!hasChanges) {
120
+ // 在 push 场景下按需创建本地提交,输入 GitService、命令参数和已读取的改动摘要,输出是否提交。
121
+ async function prepareCommitIfNeeded(git, options, changes) {
122
+ if (changes.clean) {
70
123
  logger.info('工作区无改动,跳过 commit。')
71
124
  return false
72
125
  }
@@ -92,7 +145,52 @@ async function prepareCommitIfNeeded(git, options) {
92
145
  return true
93
146
  }
94
147
 
95
- async function checkoutTargetBranch(git, targetBranch, sourceBranch) {
148
+ // 提取底层 Git 失败细节,输入异常对象,输出用于暂停错误的简洁原因。
149
+ function resolveGitFailureReason(error) {
150
+ const stderr = typeof error?.meta?.stderr === 'string' ? error.meta.stderr.trim() : ''
151
+ const stdout = typeof error?.meta?.stdout === 'string' ? error.meta.stdout.trim() : ''
152
+ const output = [stderr, stdout].filter(Boolean).join('\n').trim()
153
+ const message = typeof error?.message === 'string' ? error.message.trim() : ''
154
+ const reason = output || message || 'Git 操作失败'
155
+
156
+ return reason.length > 2000 ? `${reason.slice(0, 2000)}...` : reason
157
+ }
158
+
159
+ // 判断 Git 失败是否来自合并冲突,输入异常对象,输出布尔结果。
160
+ function isGitConflictFailure(error) {
161
+ const text = [
162
+ error?.meta?.stderr || '',
163
+ error?.meta?.stdout || '',
164
+ error?.message || ''
165
+ ].join('\n')
166
+
167
+ return /CONFLICT \(|Automatic merge failed|fix conflicts|unmerged files|needs merge|you need to resolve your current index first/i.test(text)
168
+ }
169
+
170
+ // 构建本地合并暂停错误,输入分支、阶段和原始异常,输出可被命令层汇总的 IcodeError。
171
+ function buildLocalMergePauseError({ sourceBranch, targetBranch, step, error, summary = [] }) {
172
+ const reason = resolveGitFailureReason(error)
173
+ const conflict = isGitConflictFailure(error)
174
+ const prefix = conflict ? '本地合并冲突' : '本地合并失败'
175
+ const nextStep = conflict
176
+ ? '请解决冲突后执行 `icode undo --recover continue`,或执行 `icode undo --recover abort` 中止本次合并。'
177
+ : '请根据上面的 Git 错误修复后重试。'
178
+
179
+ return new IcodeError(`${prefix}(${step}): ${sourceBranch} -> ${targetBranch}\n${reason}\n${nextStep}`, {
180
+ code: 'PUSH_LOCAL_MERGE_PAUSED',
181
+ exitCode: 2,
182
+ cause: error,
183
+ meta: {
184
+ sourceBranch,
185
+ targetBranch,
186
+ step,
187
+ reason,
188
+ summary
189
+ }
190
+ })
191
+ }
192
+
193
+ async function checkoutTargetBranch(git, targetBranch, sourceBranch, summary = []) {
96
194
  const localExists = await git.branchExistsLocal(targetBranch)
97
195
  const remoteExists = await git.branchExistsRemote(targetBranch)
98
196
  let checkoutMode = 'local'
@@ -110,10 +208,19 @@ async function checkoutTargetBranch(git, targetBranch, sourceBranch) {
110
208
  }
111
209
 
112
210
  if (remoteExists) {
113
- await git.pull(targetBranch, {
114
- allowUnrelatedHistories: true,
115
- noRebase: true
116
- })
211
+ try {
212
+ await git.pull(targetBranch, {
213
+ noRebase: true
214
+ })
215
+ } catch (error) {
216
+ throw buildLocalMergePauseError({
217
+ sourceBranch,
218
+ targetBranch,
219
+ step: 'pull-target',
220
+ error,
221
+ summary
222
+ })
223
+ }
117
224
  }
118
225
 
119
226
  return {
@@ -122,6 +229,233 @@ async function checkoutTargetBranch(git, targetBranch, sourceBranch) {
122
229
  }
123
230
  }
124
231
 
232
+ function normalizeRemoteMergePolicy(policy = {}) {
233
+ const remoteMerge = policy.remoteMerge && typeof policy.remoteMerge === 'object' ? policy.remoteMerge : {}
234
+ return {
235
+ enabled: remoteMerge.enabled !== false
236
+ }
237
+ }
238
+
239
+ // 归一化平台级远程合并配置,避免命令层拿到带空白的原始值。
240
+ function normalizeRemoteMergePlatformConfig(config = {}) {
241
+ return {
242
+ provider: typeof config.provider === 'string' ? config.provider.trim() : '',
243
+ apiKey: typeof config.apiKey === 'string' ? config.apiKey.trim() : ''
244
+ }
245
+ }
246
+
247
+ function buildRemoteMergePauseError(targetBranch, reason, meta = {}) {
248
+ return new IcodeError(`远程合并已暂停(${targetBranch}): ${reason}`, {
249
+ code: 'PUSH_REMOTE_MERGE_PAUSED',
250
+ exitCode: 2,
251
+ meta: {
252
+ targetBranch,
253
+ reason,
254
+ ...meta
255
+ }
256
+ })
257
+ }
258
+
259
+ // 统一把 provider 返回的 step 注入错误文案,便于定位 create/merge/inspect 具体失败点。
260
+ function buildRemoteMergeResultMessage(prefix, result) {
261
+ const reason = typeof result?.reason === 'string' && result.reason.trim()
262
+ ? result.reason.trim()
263
+ : '未知原因'
264
+ const step = typeof result?.step === 'string' && result.step.trim()
265
+ ? result.step.trim()
266
+ : ''
267
+
268
+ return step ? `${prefix}(${step}): ${reason}` : `${prefix}: ${reason}`
269
+ }
270
+
271
+ // 构建批量远程合并暂停错误,明确指出具体失败分支与原因。
272
+ function buildRemoteMergeBatchPauseError(failures, summary) {
273
+ const branchesLabel = failures.map((item) => item.branch).join(', ')
274
+ const detail = failures.map((item) => `${item.branch}: ${item.reason}`).join(' | ')
275
+
276
+ return new IcodeError(`远程合并已暂停(${branchesLabel}): ${detail}`, {
277
+ code: 'PUSH_REMOTE_MERGE_PAUSED',
278
+ exitCode: 2,
279
+ meta: {
280
+ failures,
281
+ summary
282
+ }
283
+ })
284
+ }
285
+
286
+ // 构建 push dry-run 步骤,输入分支目标和策略,输出不会产生副作用的计划列表。
287
+ function buildPushDryRunPlan({ currentBranch, branchTargets, protectedBranches, inputOptions }) {
288
+ const steps = []
289
+
290
+ if (inputOptions.aiCommit) {
291
+ steps.push({
292
+ action: 'ai-commit',
293
+ branch: currentBranch,
294
+ reason: '将生成并提交 AI commit'
295
+ })
296
+ } else {
297
+ steps.push({
298
+ action: 'commit-if-needed',
299
+ branch: currentBranch,
300
+ reason: inputOptions.message ? '检测到改动时使用 -m/--message 提交' : '检测到改动时会要求输入提交信息'
301
+ })
302
+ }
303
+
304
+ if (inputOptions.pullMain) {
305
+ steps.push({
306
+ action: 'pull-main',
307
+ branch: currentBranch,
308
+ reason: '将主分支同步到当前分支'
309
+ })
310
+ }
311
+
312
+ branchTargets.forEach((targetBranch) => {
313
+ if (protectedBranches.has(targetBranch) && !inputOptions.forceProtected) {
314
+ steps.push({
315
+ action: 'skip-protected',
316
+ branch: targetBranch,
317
+ reason: '受保护分支,未传 --force-protected'
318
+ })
319
+ return
320
+ }
321
+
322
+ if (targetBranch === currentBranch) {
323
+ steps.push({
324
+ action: 'push-current',
325
+ branch: targetBranch,
326
+ reason: '将同步并推送当前分支'
327
+ })
328
+ return
329
+ }
330
+
331
+ steps.push({
332
+ action: inputOptions.remoteMerge ? 'remote-merge' : 'local-merge',
333
+ branch: targetBranch,
334
+ reason: inputOptions.remoteMerge ? '将创建并合并远程 MR' : '将切换目标分支、本地 merge 后推送'
335
+ })
336
+ })
337
+
338
+ return {
339
+ currentBranch,
340
+ branchTargets,
341
+ remoteMerge: Boolean(inputOptions.remoteMerge),
342
+ steps
343
+ }
344
+ }
345
+
346
+ // 根据 origin 推导远程合并项目 URL,输入平台配置与 remote,输出项目 URL。
347
+ function resolveRemoteMergeProjectUrlFromOrigin(remoteMergePlatform, remoteOriginUrl) {
348
+ return {
349
+ projectUrl: resolveRemoteMergeProjectUrl({
350
+ provider: remoteMergePlatform.provider,
351
+ remoteUrl: remoteOriginUrl
352
+ }),
353
+ source: 'origin'
354
+ }
355
+ }
356
+
357
+ // 基于 origin 推导 GitLab 项目地址并发起固定 API 的 MR 创建请求。
358
+ async function performRemoteMerge({ git, currentBranch, targetBranch, remoteExists, inputOptions, remoteMergePolicy, remoteMergePlatform }) {
359
+ if (!remoteExists) {
360
+ throw buildRemoteMergePauseError(targetBranch, '远程合并要求目标分支已存在于 origin。', {
361
+ remoteExists: false
362
+ })
363
+ }
364
+
365
+ if (!remoteMergePolicy.enabled) {
366
+ throw buildRemoteMergePauseError(targetBranch, '仓库未启用远程合并配置,请先配置 repositories.<repo>.remoteMerge.enabled=true。')
367
+ }
368
+
369
+ if (!remoteMergePlatform.apiKey) {
370
+ throw buildRemoteMergePauseError(targetBranch, '平台未配置远程合并密钥,请先执行 icode config platform remote-merge set --provider <name> --api-key <key>。')
371
+ }
372
+
373
+ const remoteOriginUrl = await git.getRemoteUrl('origin')
374
+ const { projectUrl, source } = resolveRemoteMergeProjectUrlFromOrigin(remoteMergePlatform, remoteOriginUrl)
375
+ if (!projectUrl) {
376
+ throw buildRemoteMergePauseError(targetBranch, '无法从 origin 自动识别 GitLab 项目 URL,请检查 origin 是否指向 GitLab 仓库。', {
377
+ origin: remoteOriginUrl
378
+ })
379
+ }
380
+ logger.info(`GitLab 项目 URL(${source}): ${logger.color('green', projectUrl)}`)
381
+
382
+ logger.info(`发起远程合并: ${logger.color('yellow', currentBranch)} -> ${logger.color('yellow', targetBranch)}`)
383
+ const mergeRequestContent = await buildRemoteMergeCommitContent(git, currentBranch, targetBranch)
384
+ const result = await requestRemoteMerge({
385
+ provider: remoteMergePlatform.provider,
386
+ apiKey: remoteMergePlatform.apiKey,
387
+ projectUrl,
388
+ sourceBranch: currentBranch,
389
+ targetBranch,
390
+ mergeRequestTitle: mergeRequestContent.title,
391
+ mergeRequestDescription: mergeRequestContent.description,
392
+ repoRoot: git.cwd,
393
+ noVerify: inputOptions.noVerify
394
+ })
395
+
396
+ if (result.conflict) {
397
+ throw buildRemoteMergePauseError(targetBranch, buildRemoteMergeResultMessage('远程合并冲突', result), {
398
+ sourceBranch: currentBranch,
399
+ step: result.step || '',
400
+ result
401
+ })
402
+ }
403
+
404
+ if (!result.ok) {
405
+ throw buildRemoteMergePauseError(targetBranch, buildRemoteMergeResultMessage('远程合并失败', result), {
406
+ sourceBranch: currentBranch,
407
+ step: result.step || '',
408
+ result
409
+ })
410
+ }
411
+
412
+ const successReason = typeof result.reason === 'string' && result.reason.trim()
413
+ ? result.reason.trim()
414
+ : '远程合并已完成'
415
+
416
+ if (result.mergeRequestUrl) {
417
+ logger.success(`${successReason}: ${logger.color('green', result.mergeRequestUrl)}`)
418
+ } else {
419
+ logger.success(`${successReason}: ${logger.color('yellow', currentBranch)} -> ${logger.color('yellow', targetBranch)}`)
420
+ }
421
+
422
+ return result
423
+ }
424
+
425
+ // 并发执行多个远程合并目标分支,并把冲突/失败精确归档到对应分支。
426
+ async function runConcurrentRemoteMerges(options) {
427
+ const tasks = options.targetBranches.map(async (targetBranch) => {
428
+ try {
429
+ logger.info(`处理分支: ${targetBranch}`)
430
+ const remoteExists = await options.git.branchExistsRemote(targetBranch)
431
+ const remoteMergeResult = await performRemoteMerge({
432
+ ...options,
433
+ targetBranch,
434
+ remoteExists
435
+ })
436
+
437
+ return {
438
+ branch: targetBranch,
439
+ status: 'remote-merged-and-pushed',
440
+ mergeRequestUrl: remoteMergeResult.mergeRequestUrl || '',
441
+ mergeRequestId: remoteMergeResult.mergeRequestId || ''
442
+ }
443
+ } catch (error) {
444
+ return {
445
+ branch: targetBranch,
446
+ status: 'paused',
447
+ reason: resolveRemoteMergeFailureReason(error)
448
+ }
449
+ }
450
+ })
451
+
452
+ return Promise.all(tasks)
453
+ }
454
+
455
+ /**
456
+ * 执行 push 工作流。
457
+ * 输入为 push 命令选项,输出推送/合并摘要;关键假设是实际执行前必须没有未完成操作和冲突文件。
458
+ */
125
459
  export async function runPushWorkflow(inputOptions) {
126
460
  const context = await resolveGitContext({
127
461
  cwd: inputOptions.cwd,
@@ -131,6 +465,8 @@ export async function runPushWorkflow(inputOptions) {
131
465
  const git = new GitService(context)
132
466
  const policy = getRepoPolicy(context.topLevelPath)
133
467
  const protectedBranches = new Set((policy.protectedBranches || []).map((item) => item.trim()).filter(Boolean))
468
+ const remoteMergePolicy = normalizeRemoteMergePolicy(policy)
469
+ const remoteMergePlatform = normalizeRemoteMergePlatformConfig(getPlatformConfig('remoteMerge'))
134
470
 
135
471
  logger.info(`仓库根目录: ${context.topLevelPath}`)
136
472
  if (context.inheritedFromParent) {
@@ -145,6 +481,40 @@ export async function runPushWorkflow(inputOptions) {
145
481
  })
146
482
  }
147
483
 
484
+ let branchTargets = uniqueBranches([
485
+ ...(inputOptions.notPushCurrent ? [] : [currentBranch]),
486
+ ...(inputOptions.targetBranches || [])
487
+ ])
488
+ if (inputOptions.notPushCurrent) {
489
+ branchTargets = branchTargets.filter((branchName) => branchName !== currentBranch)
490
+ }
491
+
492
+ if (!branchTargets.length) {
493
+ throw new IcodeError('没有可执行的目标分支。', {
494
+ code: 'PUSH_EMPTY_TARGETS',
495
+ exitCode: 2
496
+ })
497
+ }
498
+
499
+ if (inputOptions.dryRun) {
500
+ return {
501
+ dryRun: true,
502
+ repoRoot: context.topLevelPath,
503
+ currentBranch,
504
+ inheritedFromParent: context.inheritedFromParent,
505
+ plan: buildPushDryRunPlan({
506
+ currentBranch,
507
+ branchTargets,
508
+ protectedBranches,
509
+ inputOptions
510
+ })
511
+ }
512
+ }
513
+
514
+ const safetyState = await readGitSafetyState(git)
515
+ assertNoPendingOperation(safetyState.operation, 'push', 'PUSH_GIT_OPERATION_IN_PROGRESS')
516
+ assertNoConflictedFiles(safetyState.changes, 'push', 'PUSH_CONFLICTED_FILES')
517
+
148
518
  const aiCommitResult = await prepareAiCommitIfEnabled(inputOptions)
149
519
  if (aiCommitResult.canceled) {
150
520
  return {
@@ -158,31 +528,27 @@ export async function runPushWorkflow(inputOptions) {
158
528
 
159
529
  const shouldRunManualCommit = !aiCommitResult.applied && aiCommitResult.reason !== 'no-diff'
160
530
  if (shouldRunManualCommit) {
161
- await prepareCommitIfNeeded(git, inputOptions)
531
+ await prepareCommitIfNeeded(git, inputOptions, safetyState.changes)
162
532
  }
163
533
  await git.fetch()
164
534
 
535
+ const summary = []
536
+
165
537
  if (inputOptions.pullMain && context.defaultBranch !== currentBranch) {
166
538
  logger.info(`先同步主分支 ${context.defaultBranch} 到当前分支 ${currentBranch}`)
167
- await git.pull(context.defaultBranch, {
168
- allowUnrelatedHistories: true,
169
- noRebase: true
170
- })
171
- }
172
-
173
- let branchTargets = uniqueBranches([
174
- ...(inputOptions.notPushCurrent ? [] : [currentBranch]),
175
- ...(inputOptions.targetBranches || [])
176
- ])
177
- if (inputOptions.notPushCurrent) {
178
- branchTargets = branchTargets.filter((branchName) => branchName !== currentBranch)
179
- }
180
-
181
- if (!branchTargets.length) {
182
- throw new IcodeError('没有可执行的目标分支。', {
183
- code: 'PUSH_EMPTY_TARGETS',
184
- exitCode: 2
185
- })
539
+ try {
540
+ await git.pull(context.defaultBranch, {
541
+ noRebase: true
542
+ })
543
+ } catch (error) {
544
+ throw buildLocalMergePauseError({
545
+ sourceBranch: context.defaultBranch,
546
+ targetBranch: currentBranch,
547
+ step: 'pull-main',
548
+ error,
549
+ summary
550
+ })
551
+ }
186
552
  }
187
553
 
188
554
  if (!inputOptions.yes) {
@@ -201,8 +567,8 @@ export async function runPushWorkflow(inputOptions) {
201
567
  }
202
568
  }
203
569
 
204
- const summary = []
205
570
  const originalBranch = currentBranch
571
+ const remoteMergeTargets = []
206
572
 
207
573
  try {
208
574
  for (const targetBranch of branchTargets) {
@@ -212,16 +578,24 @@ export async function runPushWorkflow(inputOptions) {
212
578
  continue
213
579
  }
214
580
 
215
- logger.info(`处理分支: ${targetBranch}`)
216
-
217
581
  if (targetBranch === currentBranch) {
582
+ logger.info(`处理分支: ${targetBranch}`)
218
583
  const remoteExists = await git.branchExistsRemote(targetBranch)
219
584
  if (remoteExists) {
220
585
  logger.info(`同步远程分支: ${targetBranch}`)
221
- await git.pull(targetBranch, {
222
- allowUnrelatedHistories: true,
223
- noRebase: true
224
- })
586
+ try {
587
+ await git.pull(targetBranch, {
588
+ noRebase: true
589
+ })
590
+ } catch (error) {
591
+ throw buildLocalMergePauseError({
592
+ sourceBranch: `origin/${targetBranch}`,
593
+ targetBranch,
594
+ step: 'pull-current',
595
+ error,
596
+ summary
597
+ })
598
+ }
225
599
  }
226
600
 
227
601
  logger.info(`推送当前分支: ${targetBranch}`)
@@ -235,30 +609,73 @@ export async function runPushWorkflow(inputOptions) {
235
609
  continue
236
610
  }
237
611
 
238
- logger.info(`切换到目标分支: ${targetBranch}`)
239
- const { remoteExists, checkoutMode } = await checkoutTargetBranch(git, targetBranch, currentBranch)
240
- logger.info(`目标分支准备完成: ${targetBranch} (${checkoutMode})`)
612
+ if (inputOptions.remoteMerge) {
613
+ remoteMergeTargets.push(targetBranch)
614
+ } else {
615
+ logger.info(`处理分支: ${targetBranch}`)
616
+ logger.info(`切换到目标分支: ${targetBranch}`)
617
+ const { remoteExists, checkoutMode } = await checkoutTargetBranch(git, targetBranch, currentBranch, summary)
618
+ logger.info(`目标分支准备完成: ${targetBranch} (${checkoutMode})`)
619
+
620
+ // 保留 merge commit,方便后续追溯“从哪个分支合并过来”。
621
+ logger.info(`合并分支: ${currentBranch} -> ${targetBranch}`)
622
+ try {
623
+ await git.merge(currentBranch, {
624
+ noFf: true,
625
+ noEdit: true
626
+ })
627
+ } catch (error) {
628
+ throw buildLocalMergePauseError({
629
+ sourceBranch: currentBranch,
630
+ targetBranch,
631
+ step: 'merge-target',
632
+ error,
633
+ summary
634
+ })
635
+ }
636
+ logger.success(`合并成功: ${currentBranch} -> ${targetBranch}`)
637
+
638
+ logger.info(`推送目标分支: ${targetBranch}`)
639
+ await git.push(targetBranch, {
640
+ setUpstream: !remoteExists,
641
+ noVerify: inputOptions.noVerify
642
+ })
241
643
 
242
- // 保留 merge commit,方便后续追溯“从哪个分支合并过来”。
243
- logger.info(`合并分支: ${currentBranch} -> ${targetBranch}`)
244
- await git.merge(currentBranch, {
245
- noFf: true,
246
- noEdit: true
247
- })
248
- logger.success(`合并成功: ${currentBranch} -> ${targetBranch}`)
644
+ logger.success(`目标分支推送成功: ${targetBranch}`)
645
+ summary.push({ branch: targetBranch, status: 'merged-and-pushed' })
646
+ }
647
+ }
249
648
 
250
- logger.info(`推送目标分支: ${targetBranch}`)
251
- await git.push(targetBranch, {
252
- setUpstream: !remoteExists,
253
- noVerify: inputOptions.noVerify
649
+ if (inputOptions.remoteMerge && remoteMergeTargets.length) {
650
+ logger.info(`并发处理远程合并分支: ${remoteMergeTargets.join(', ')}`)
651
+ const remoteMergeSummary = await runConcurrentRemoteMerges({
652
+ git,
653
+ currentBranch,
654
+ targetBranches: remoteMergeTargets,
655
+ inputOptions,
656
+ remoteMergePolicy,
657
+ remoteMergePlatform
254
658
  })
659
+ summary.push(...remoteMergeSummary)
255
660
 
256
- logger.success(`目标分支推送成功: ${targetBranch}`)
257
- summary.push({ branch: targetBranch, status: 'merged-and-pushed' })
661
+ const failures = remoteMergeSummary
662
+ .filter((item) => item.status === 'paused')
663
+ .map((item) => ({
664
+ branch: item.branch,
665
+ reason: item.reason || '远程合并失败'
666
+ }))
667
+
668
+ if (failures.length) {
669
+ throw buildRemoteMergeBatchPauseError(failures, summary)
670
+ }
258
671
  }
259
672
  } finally {
260
673
  const branchAfterWorkflow = await git.getCurrentBranch()
261
- if (branchAfterWorkflow && branchAfterWorkflow !== originalBranch) {
674
+ const operationAfterWorkflow = await git.getInProgressOperation()
675
+ if (operationAfterWorkflow === 'merge') {
676
+ logger.warn(`检测到未完成的 merge,已保留在当前分支 ${branchAfterWorkflow || '(unknown)'}。`)
677
+ logger.warn('解决冲突后执行 `icode undo --recover continue`,或执行 `icode undo --recover abort` 中止。')
678
+ } else if (branchAfterWorkflow && branchAfterWorkflow !== originalBranch) {
262
679
  try {
263
680
  await git.checkout(originalBranch)
264
681
  } catch (error) {