@icode-js/icode 3.2.0 → 3.4.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 (75) hide show
  1. package/README.md +287 -24
  2. package/bin/icode.js +10 -0
  3. package/package.json +3 -3
  4. package/src/cli.js +175 -8
  5. package/src/commands/checkout.js +73 -6
  6. package/src/commands/clean.js +47 -7
  7. package/src/commands/config-ux.js +292 -0
  8. package/src/commands/config.js +199 -48
  9. package/src/commands/continue.js +104 -0
  10. package/src/commands/doctor.js +15 -29
  11. package/src/commands/help.js +54 -34
  12. package/src/commands/history.js +147 -0
  13. package/src/commands/info.js +39 -4
  14. package/src/commands/log.js +142 -0
  15. package/src/commands/migrate.js +19 -12
  16. package/src/commands/open.js +138 -0
  17. package/src/commands/pr.js +219 -0
  18. package/src/commands/push.js +62 -22
  19. package/src/commands/ready.js +18 -48
  20. package/src/commands/release.js +112 -0
  21. package/src/commands/rescue.js +164 -0
  22. package/src/commands/save.js +52 -11
  23. package/src/commands/stash.js +210 -0
  24. package/src/commands/sw.js +89 -0
  25. package/src/commands/sync.js +19 -2
  26. package/src/commands/tag.js +40 -1
  27. package/src/commands/undo.js +9 -3
  28. package/src/core/ai/diff-range.js +1 -1
  29. package/src/core/cli/completion.js +237 -1
  30. package/src/core/cli/output.js +46 -0
  31. package/src/core/cli/render.js +121 -0
  32. package/src/core/commands/shell.js +9 -1
  33. package/src/core/config/repo-config.js +93 -0
  34. package/src/core/config/store.js +60 -10
  35. package/src/core/git/autostash.js +46 -0
  36. package/src/core/git/backup.js +170 -0
  37. package/src/core/git/conflicts.js +58 -0
  38. package/src/core/git/context.js +72 -5
  39. package/src/core/git/file-guard.js +193 -0
  40. package/src/core/git/operation-state.js +63 -0
  41. package/src/core/git/operations.js +32 -0
  42. package/src/core/git/semver.js +194 -0
  43. package/src/core/git/service.js +454 -24
  44. package/src/core/platform/http.js +53 -0
  45. package/src/core/platform/index.js +165 -0
  46. package/src/core/platform/providers.js +454 -0
  47. package/src/core/platform/remote-url.js +92 -0
  48. package/src/core/remote-merge/client.js +33 -3
  49. package/src/core/tools/browser.js +28 -0
  50. package/src/core/tools/commit-prompt.js +137 -0
  51. package/src/core/tools/interactive.js +272 -40
  52. package/src/core/tools/logger.js +33 -23
  53. package/src/core/tools/theme.js +228 -0
  54. package/src/workflows/ai-commit-workflow.js +52 -20
  55. package/src/workflows/checkout-workflow.js +24 -10
  56. package/src/workflows/clean-workflow.js +135 -83
  57. package/src/workflows/continue-workflow.js +233 -0
  58. package/src/workflows/doctor-workflow.js +10 -7
  59. package/src/workflows/history-workflow.js +211 -0
  60. package/src/workflows/migrate-workflow.js +39 -14
  61. package/src/workflows/pr-workflow.js +252 -0
  62. package/src/workflows/push/commit-step.js +123 -0
  63. package/src/workflows/push/local-merge-step.js +213 -0
  64. package/src/workflows/push/plan.js +63 -0
  65. package/src/workflows/push/remote-merge-step.js +224 -0
  66. package/src/workflows/push-workflow.js +185 -492
  67. package/src/workflows/ready-workflow.js +14 -10
  68. package/src/workflows/release-workflow.js +189 -0
  69. package/src/workflows/rescue-workflow.js +137 -0
  70. package/src/workflows/save-workflow.js +106 -41
  71. package/src/workflows/switch-workflow.js +171 -0
  72. package/src/workflows/sync-workflow.js +143 -44
  73. package/src/workflows/tag-workflow.js +103 -17
  74. package/src/workflows/undo-workflow.js +113 -34
  75. package/src/workflows/rollback-workflow.js +0 -99
@@ -0,0 +1,123 @@
1
+ import { formatAiCommitSummary } from '../../core/ai/commit-summary.js'
2
+ import { IcodeError } from '../../core/errors.js'
3
+ import { assertNoRiskyFiles, inspectPendingFiles } from '../../core/git/file-guard.js'
4
+ import { promptCommitMessage } from '../../core/tools/commit-prompt.js'
5
+ import { isInteractiveTerminal } from '../../core/tools/interactive.js'
6
+ import { logger } from '../../core/tools/logger.js'
7
+ import { runAiCommitWorkflow } from '../ai-commit-workflow.js'
8
+
9
+ // push 前的提交阶段:可选 AI commit,或用 -m 提交;两者都会先做风险扫描。
10
+
11
+ // 提交前扫描敏感文件/疑似密钥/大文件,发现风险时按 --allow-risky 决定是否阻断。
12
+ async function guardPendingFiles(git, context, options) {
13
+ const { findings } = await inspectPendingFiles(git, context, { mode: 'all' })
14
+ assertNoRiskyFiles(findings, 'push', 'PUSH_RISKY_FILES', options.allowRisky === true)
15
+ return findings
16
+ }
17
+
18
+ // Run AI commit before push and keep the full generated message in follow-up logs.
19
+ export async function prepareAiCommitIfEnabled(git, context, inputOptions, changes) {
20
+ if (!inputOptions.aiCommit) {
21
+ return {
22
+ enabled: false
23
+ }
24
+ }
25
+
26
+ if (inputOptions.message?.trim()) {
27
+ logger.warn('--ai-commit 已启用,将优先使用 AI 生成的提交信息。')
28
+ }
29
+
30
+ if (changes && !changes.clean) {
31
+ await guardPendingFiles(git, context, inputOptions)
32
+ }
33
+
34
+ try {
35
+ const result = await runAiCommitWorkflow({
36
+ apply: true,
37
+ lang: inputOptions.aiCommitLang || 'zh',
38
+ profile: inputOptions.aiProfile,
39
+ repoMode: inputOptions.repoMode,
40
+ noVerify: inputOptions.noVerify,
41
+ yes: inputOptions.yes,
42
+ cwd: inputOptions.cwd,
43
+ silentContextLog: true
44
+ })
45
+
46
+ if (result.canceled) {
47
+ logger.warn('已取消 AI 自动提交。')
48
+ return {
49
+ enabled: true,
50
+ canceled: true
51
+ }
52
+ }
53
+
54
+ logger.success(`AI 自动提交完成:\n${formatAiCommitSummary(result.commitId, result.commitMessage)}`)
55
+ return {
56
+ enabled: true,
57
+ applied: true,
58
+ commitId: result.commitId,
59
+ commitMessage: result.commitMessage
60
+ }
61
+ } catch (error) {
62
+ if (error?.code === 'AI_COMMIT_EMPTY_DIFF') {
63
+ logger.info('未检测到可提交改动,跳过 --ai-commit。')
64
+ return {
65
+ enabled: true,
66
+ skipped: true,
67
+ reason: 'no-diff'
68
+ }
69
+ }
70
+
71
+ throw error
72
+ }
73
+ }
74
+
75
+ // 在 push 场景下按需创建本地提交,输入 GitService、上下文、命令参数和已读取的改动摘要,输出是否提交。
76
+ export async function prepareCommitIfNeeded(git, context, options, changes) {
77
+ if (changes.clean) {
78
+ logger.info('工作区无改动,跳过 commit。')
79
+ return false
80
+ }
81
+
82
+ await guardPendingFiles(git, context, options)
83
+
84
+ let message = options.message?.trim()
85
+ if (!message && isInteractiveTerminal() && !options.yes) {
86
+ const { files } = await inspectPendingFiles(git, context, { mode: 'all' })
87
+ const answer = await promptCommitMessage({ files })
88
+ if (answer.canceled) {
89
+ throw new IcodeError('已取消提交,push 未执行。', { code: 'PUSH_COMMIT_CANCELED', exitCode: 1 })
90
+ }
91
+ if (answer.useAi) {
92
+ const result = await runAiCommitWorkflow({
93
+ apply: true,
94
+ lang: options.aiCommitLang || 'zh',
95
+ profile: options.aiProfile,
96
+ repoMode: options.repoMode,
97
+ noVerify: options.noVerify,
98
+ cwd: options.cwd,
99
+ silentContextLog: true
100
+ })
101
+ if (result.canceled) {
102
+ throw new IcodeError('已取消提交,push 未执行。', { code: 'PUSH_COMMIT_CANCELED', exitCode: 1 })
103
+ }
104
+ return true
105
+ }
106
+ message = answer.message
107
+ }
108
+
109
+ if (!message) {
110
+ throw new IcodeError('检测到代码改动但未提供提交信息,请使用 -m 或 --message。', {
111
+ code: 'PUSH_COMMIT_MESSAGE_REQUIRED',
112
+ exitCode: 2
113
+ })
114
+ }
115
+
116
+ // 统一自动暂存,降低同学手动 add 的负担。
117
+ await git.stageAll()
118
+ await git.commit(message, {
119
+ noVerify: options.noVerify
120
+ })
121
+ logger.success(`已提交 ${message}`)
122
+ return true
123
+ }
@@ -0,0 +1,213 @@
1
+ import { IcodeError } from '../../core/errors.js'
2
+ import { ui } from '../../core/tools/theme.js'
3
+ import { confirm, isInteractiveTerminal } from '../../core/tools/interactive.js'
4
+ import { logger } from '../../core/tools/logger.js'
5
+
6
+ // 本地 merge 阶段:切到目标分支 -> 同步远程 -> merge 源分支 -> push。
7
+
8
+ // 提取底层 Git 失败细节,输入异常对象,输出用于暂停错误的简洁原因。
9
+ export function resolveGitFailureReason(error) {
10
+ const stderr = typeof error?.meta?.stderr === 'string' ? error.meta.stderr.trim() : ''
11
+ const stdout = typeof error?.meta?.stdout === 'string' ? error.meta.stdout.trim() : ''
12
+ const output = [stderr, stdout].filter(Boolean).join('\n').trim()
13
+ const message = typeof error?.message === 'string' ? error.message.trim() : ''
14
+ const reason = output || message || 'Git 操作失败'
15
+
16
+ return reason.length > 2000 ? `${reason.slice(0, 2000)}...` : reason
17
+ }
18
+
19
+ // 判断 Git 失败是否来自合并冲突,输入异常对象,输出布尔结果。
20
+ export function isGitConflictFailure(error) {
21
+ const text = [
22
+ error?.meta?.stderr || '',
23
+ error?.meta?.stdout || '',
24
+ error?.message || ''
25
+ ].join('\n')
26
+
27
+ return /CONFLICT \(|Automatic merge failed|fix conflicts|unmerged files|needs merge|you need to resolve your current index first/i.test(text)
28
+ }
29
+
30
+ // 构建本地合并暂停错误,输入分支、阶段和原始异常,输出可被命令层汇总的 IcodeError。
31
+ export function buildLocalMergePauseError({ sourceBranch, targetBranch, step, error, summary = [] }) {
32
+ const reason = resolveGitFailureReason(error)
33
+ const conflict = isGitConflictFailure(error)
34
+ const prefix = conflict ? '本地合并冲突' : '本地合并失败'
35
+ const nextStep = conflict
36
+ ? '解决冲突后执行 `icode continue`(会自动暂存已解决的文件,并接着完成剩下的推送),或执行 `icode abort` 放弃并切回原分支。'
37
+ : '请根据上面的 Git 错误修复后重试。'
38
+
39
+ return new IcodeError(`${prefix}(${step}): ${sourceBranch} -> ${targetBranch}\n${reason}\n${nextStep}`, {
40
+ code: 'PUSH_LOCAL_MERGE_PAUSED',
41
+ exitCode: 2,
42
+ cause: error,
43
+ meta: {
44
+ sourceBranch,
45
+ targetBranch,
46
+ step,
47
+ reason,
48
+ summary
49
+ }
50
+ })
51
+ }
52
+
53
+ /**
54
+ * 目标分支本地和远程都不存在时,是否允许从源分支新建。
55
+ * 显式 --create-target 直接允许;交互终端里询问(默认否);-y 或非交互环境直接报错,避免拼错分支名时悄悄建新分支。
56
+ */
57
+ async function ensureTargetCreationAllowed(targetBranch, sourceBranch, options) {
58
+ if (options.createTarget) {
59
+ return
60
+ }
61
+
62
+ if (!options.yes && isInteractiveTerminal()) {
63
+ const accepted = await confirm(`目标分支 ${targetBranch} 在本地和远程都不存在,是否从 ${sourceBranch} 新建?`, false)
64
+ if (accepted) {
65
+ return
66
+ }
67
+ }
68
+
69
+ throw new IcodeError(
70
+ `目标分支不存在: ${targetBranch}。如确实要从 ${sourceBranch} 新建,请加 --create-target。`,
71
+ {
72
+ code: 'PUSH_TARGET_MISSING',
73
+ exitCode: 2,
74
+ meta: { targetBranch, sourceBranch }
75
+ }
76
+ )
77
+ }
78
+
79
+ export async function checkoutTargetBranch(git, targetBranch, sourceBranch, options = {}, summary = []) {
80
+ const localExists = await git.branchExistsLocal(targetBranch)
81
+ const remoteExists = await git.branchExistsRemote(targetBranch)
82
+ let checkoutMode = 'local'
83
+
84
+ if (localExists) {
85
+ await git.checkout(targetBranch)
86
+ } else if (remoteExists) {
87
+ await git.checkoutTracking(targetBranch)
88
+ checkoutMode = 'tracking'
89
+ } else {
90
+ await ensureTargetCreationAllowed(targetBranch, sourceBranch, options)
91
+ logger.warn(`目标分支 ${ui.branch(targetBranch)} 不存在,将从 ${ui.branch(sourceBranch)} 创建。`)
92
+ await git.checkoutNewBranch(targetBranch, sourceBranch)
93
+ checkoutMode = 'created'
94
+ }
95
+
96
+ if (remoteExists) {
97
+ try {
98
+ await git.pull(targetBranch, {
99
+ noRebase: true
100
+ })
101
+ } catch (error) {
102
+ throw buildLocalMergePauseError({
103
+ sourceBranch,
104
+ targetBranch,
105
+ step: 'pull-target',
106
+ error,
107
+ summary
108
+ })
109
+ }
110
+ }
111
+
112
+ return {
113
+ remoteExists,
114
+ checkoutMode
115
+ }
116
+ }
117
+
118
+ /**
119
+ * 把源分支本地 merge 到目标分支并推送。
120
+ * 输入为 GitService、源/目标分支、push 选项和摘要数组,输出该分支的摘要项。
121
+ */
122
+ export async function mergeIntoTargetAndPush(git, sourceBranch, targetBranch, options, summary) {
123
+ logger.info(`处理分支: ${ui.branch(targetBranch)}`)
124
+ logger.info(`切换到目标分支: ${ui.branch(targetBranch)}`)
125
+ const { remoteExists, checkoutMode } = await checkoutTargetBranch(git, targetBranch, sourceBranch, options, summary)
126
+ logger.info(`目标分支准备完成: ${ui.branch(targetBranch)} (${checkoutMode})`)
127
+
128
+ // 保留 merge commit,方便后续追溯“从哪个分支合并过来”。
129
+ logger.info(`合并分支: ${ui.branch(sourceBranch)} -> ${ui.branch(targetBranch)}`)
130
+ try {
131
+ await git.merge(sourceBranch, {
132
+ noFf: true,
133
+ noEdit: true
134
+ })
135
+ } catch (error) {
136
+ throw buildLocalMergePauseError({
137
+ sourceBranch,
138
+ targetBranch,
139
+ step: 'merge-target',
140
+ error,
141
+ summary
142
+ })
143
+ }
144
+ logger.success(`合并成功: ${ui.branch(sourceBranch)} -> ${ui.branch(targetBranch)}`)
145
+
146
+ logger.info(`推送目标分支: ${ui.branch(targetBranch)}`)
147
+ await git.push(targetBranch, {
148
+ setUpstream: !remoteExists,
149
+ noVerify: options.noVerify
150
+ })
151
+
152
+ logger.success(`目标分支推送成功: ${ui.branch(targetBranch)}`)
153
+ return { branch: targetBranch, status: 'merged-and-pushed' }
154
+ }
155
+
156
+ /**
157
+ * 同步并推送当前分支;force=true 时跳过 pull,使用 --force-with-lease 覆盖远程。
158
+ * expectedRemoteSha 是 fetch 之前记录的远程 sha,用来防止覆盖别人刚推上去的提交。
159
+ */
160
+ export async function pushCurrentBranch(git, branchName, options, summary) {
161
+ logger.info(`处理分支: ${ui.branch(branchName)}`)
162
+ const remoteExists = await git.branchExistsRemote(branchName)
163
+
164
+ if (options.force) {
165
+ logger.warn(`安全强推当前分支: ${ui.branch(branchName)}(--force-with-lease)`)
166
+ try {
167
+ await git.push(branchName, {
168
+ setUpstream: !remoteExists,
169
+ noVerify: options.noVerify,
170
+ forceWithLease: remoteExists,
171
+ expectedRemoteSha: options.expectedRemoteSha
172
+ })
173
+ } catch (error) {
174
+ const reason = resolveGitFailureReason(error)
175
+ if (/stale info|rejected/i.test(reason)) {
176
+ throw new IcodeError(
177
+ `强推被拒绝:远程 ${git.remoteRef(branchName)} 在你上次 fetch 之后有新提交,已停止以免覆盖别人的改动。\n` +
178
+ '请先查看远程改动(git log HEAD..' + git.remoteRef(branchName) + '),合并或 rebase 后再推。',
179
+ { code: 'PUSH_FORCE_LEASE_REJECTED', exitCode: 2, cause: error, meta: { reason, summary } }
180
+ )
181
+ }
182
+ throw error
183
+ }
184
+ logger.success(`强推成功: ${ui.branch(branchName)}`)
185
+ return { branch: branchName, status: 'force-pushed' }
186
+ }
187
+
188
+ if (remoteExists) {
189
+ logger.info(`同步远程分支: ${ui.branch(branchName)}`)
190
+ try {
191
+ await git.pull(branchName, {
192
+ noRebase: true
193
+ })
194
+ } catch (error) {
195
+ throw buildLocalMergePauseError({
196
+ sourceBranch: git.remoteRef(branchName),
197
+ targetBranch: branchName,
198
+ step: 'pull-current',
199
+ error,
200
+ summary
201
+ })
202
+ }
203
+ }
204
+
205
+ logger.info(`推送当前分支: ${ui.branch(branchName)}`)
206
+ await git.push(branchName, {
207
+ setUpstream: !remoteExists,
208
+ noVerify: options.noVerify
209
+ })
210
+
211
+ logger.success(`推送成功: ${ui.branch(branchName)}`)
212
+ return { branch: branchName, status: 'pushed' }
213
+ }
@@ -0,0 +1,63 @@
1
+ // 构建 push dry-run 步骤,输入分支目标和策略,输出不会产生副作用的计划列表。
2
+ export function buildPushDryRunPlan({ currentBranch, branchTargets, protectedBranches, inputOptions, remote = 'origin' }) {
3
+ const steps = []
4
+
5
+ if (inputOptions.aiCommit) {
6
+ steps.push({
7
+ action: 'ai-commit',
8
+ branch: currentBranch,
9
+ reason: '将生成并提交 AI commit'
10
+ })
11
+ } else {
12
+ steps.push({
13
+ action: 'commit-if-needed',
14
+ branch: currentBranch,
15
+ reason: inputOptions.message ? '检测到改动时使用 -m/--message 提交' : '检测到改动时会要求输入提交信息'
16
+ })
17
+ }
18
+
19
+ if (inputOptions.pullMain) {
20
+ steps.push({
21
+ action: 'pull-main',
22
+ branch: currentBranch,
23
+ reason: '将主分支同步到当前分支'
24
+ })
25
+ }
26
+
27
+ branchTargets.forEach((targetBranch) => {
28
+ if (protectedBranches.has(targetBranch) && !inputOptions.forceProtected) {
29
+ steps.push({
30
+ action: 'skip-protected',
31
+ branch: targetBranch,
32
+ reason: '受保护分支,未传 --force-protected'
33
+ })
34
+ return
35
+ }
36
+
37
+ if (targetBranch === currentBranch) {
38
+ steps.push({
39
+ action: inputOptions.force ? 'force-push-current' : 'push-current',
40
+ branch: targetBranch,
41
+ reason: inputOptions.force
42
+ ? `将以 --force-with-lease 覆盖 ${remote}/${targetBranch}`
43
+ : '将同步并推送当前分支'
44
+ })
45
+ return
46
+ }
47
+
48
+ steps.push({
49
+ action: inputOptions.remoteMerge ? 'remote-merge' : 'local-merge',
50
+ branch: targetBranch,
51
+ reason: inputOptions.remoteMerge ? '将创建并合并远程 PR/MR' : '将切换目标分支、本地 merge 后推送'
52
+ })
53
+ })
54
+
55
+ return {
56
+ currentBranch,
57
+ branchTargets,
58
+ remote,
59
+ remoteMerge: Boolean(inputOptions.remoteMerge),
60
+ force: Boolean(inputOptions.force),
61
+ steps
62
+ }
63
+ }
@@ -0,0 +1,224 @@
1
+ import { readConfig } from '../../core/config/store.js'
2
+ import { ui } from '../../core/tools/theme.js'
3
+ import { IcodeError } from '../../core/errors.js'
4
+ import { resolvePlatformToken, resolveProviderName } from '../../core/platform/index.js'
5
+ import { requestRemoteMerge, resolveRemoteMergeProjectUrl } from '../../core/remote-merge/client.js'
6
+ import { logger } from '../../core/tools/logger.js'
7
+
8
+ // 远程合并阶段:通过平台 API 创建 PR/MR 并合并,不在本地切换分支。
9
+
10
+ // 基于 commit 内容构建远程 MR 标题与正文,确保标题和内容都来自真实提交信息。
11
+ export async function buildRemoteMergeCommitContent(git, sourceBranch, targetBranch) {
12
+ const rangeSpec = `${git.remoteRef(targetBranch)}..${sourceBranch}`
13
+ let commits = await git.listCommitMessages(rangeSpec)
14
+
15
+ if (!commits.length) {
16
+ const latestCommit = await git.getCommitMessage(sourceBranch)
17
+ if (latestCommit) {
18
+ commits = [latestCommit]
19
+ }
20
+ }
21
+
22
+ if (!commits.length) {
23
+ throw new IcodeError(`无法读取远程合并提交内容: ${sourceBranch} -> ${targetBranch}`, {
24
+ code: 'PUSH_REMOTE_MERGE_COMMIT_CONTENT_EMPTY',
25
+ exitCode: 2
26
+ })
27
+ }
28
+
29
+ const latestCommit = commits[commits.length - 1]
30
+ const description = commits
31
+ .map((commit) => {
32
+ if (commit.body) {
33
+ return `${commit.subject}\n\n${commit.body}`
34
+ }
35
+ return commit.subject
36
+ })
37
+ .join('\n\n')
38
+
39
+ return {
40
+ title: latestCommit.subject,
41
+ description
42
+ }
43
+ }
44
+
45
+ // 提取远程合并失败原因,优先使用 IcodeError.meta,其次回退到 error.message。
46
+ function resolveRemoteMergeFailureReason(error) {
47
+ const metaStep = typeof error?.meta?.step === 'string' ? error.meta.step.trim() : ''
48
+ const metaReason = typeof error?.meta?.reason === 'string' ? error.meta.reason.trim() : ''
49
+ if (metaReason) {
50
+ return metaStep ? `${metaReason} [step=${metaStep}]` : metaReason
51
+ }
52
+
53
+ const message = typeof error?.message === 'string' ? error.message.trim() : ''
54
+ return message || '远程合并失败'
55
+ }
56
+
57
+ export function normalizeRemoteMergePolicy(policy = {}) {
58
+ const remoteMerge = policy.remoteMerge && typeof policy.remoteMerge === 'object' ? policy.remoteMerge : {}
59
+ return {
60
+ enabled: remoteMerge.enabled !== false
61
+ }
62
+ }
63
+
64
+ /**
65
+ * 解析远程合并使用的平台和 token。
66
+ * provider 优先按 remote 主机名识别(github/gitlab/gitee/gitea),识别不到再用旧配置 platforms.remoteMerge.provider。
67
+ */
68
+ export async function resolveRemoteMergePlatform(git) {
69
+ const config = readConfig()
70
+ const remoteUrl = await git.getRemoteUrl()
71
+ const provider = resolveProviderName(remoteUrl, {}, config)
72
+ const { token, source } = provider ? resolvePlatformToken(provider, config) : { token: '', source: '' }
73
+ return {
74
+ provider,
75
+ apiKey: token,
76
+ tokenSource: source,
77
+ remoteUrl
78
+ }
79
+ }
80
+
81
+ function buildRemoteMergePauseError(targetBranch, reason, meta = {}) {
82
+ return new IcodeError(`远程合并已暂停(${targetBranch}): ${reason}`, {
83
+ code: 'PUSH_REMOTE_MERGE_PAUSED',
84
+ exitCode: 2,
85
+ meta: {
86
+ targetBranch,
87
+ reason,
88
+ ...meta
89
+ }
90
+ })
91
+ }
92
+
93
+ // 统一把 provider 返回的 step 注入错误文案,便于定位 create/merge/inspect 具体失败点。
94
+ function buildRemoteMergeResultMessage(prefix, result) {
95
+ const reason = typeof result?.reason === 'string' && result.reason.trim()
96
+ ? result.reason.trim()
97
+ : '未知原因'
98
+ const step = typeof result?.step === 'string' && result.step.trim()
99
+ ? result.step.trim()
100
+ : ''
101
+
102
+ return step ? `${prefix}(${step}): ${reason}` : `${prefix}: ${reason}`
103
+ }
104
+
105
+ // 构建批量远程合并暂停错误,明确指出具体失败分支与原因。
106
+ export function buildRemoteMergeBatchPauseError(failures, summary) {
107
+ const branchesLabel = failures.map((item) => item.branch).join(', ')
108
+ const detail = failures.map((item) => `${item.branch}: ${item.reason}`).join(' | ')
109
+
110
+ return new IcodeError(`远程合并已暂停(${branchesLabel}): ${detail}`, {
111
+ code: 'PUSH_REMOTE_MERGE_PAUSED',
112
+ exitCode: 2,
113
+ meta: {
114
+ failures,
115
+ summary
116
+ }
117
+ })
118
+ }
119
+
120
+ // 基于当前 remote 推导项目地址并发起 PR/MR 创建 + 合并请求。
121
+ async function performRemoteMerge({ git, currentBranch, targetBranch, remoteExists, inputOptions, remoteMergePolicy, remoteMergePlatform }) {
122
+ if (!remoteExists) {
123
+ throw buildRemoteMergePauseError(targetBranch, `远程合并要求目标分支已存在于 ${git.remote}。`, {
124
+ remoteExists: false
125
+ })
126
+ }
127
+
128
+ if (!remoteMergePolicy.enabled) {
129
+ throw buildRemoteMergePauseError(targetBranch, '仓库未启用远程合并配置,请先配置 repositories.<repo>.remoteMerge.enabled=true。')
130
+ }
131
+
132
+ if (!remoteMergePlatform.provider) {
133
+ throw buildRemoteMergePauseError(targetBranch, `无法从 ${git.remote} 识别代码托管平台,请执行 icode config platform host set <host> github|gitlab|gitee|gitea。`, {
134
+ remote: remoteMergePlatform.remoteUrl
135
+ })
136
+ }
137
+
138
+ if (!remoteMergePlatform.apiKey) {
139
+ throw buildRemoteMergePauseError(targetBranch, `未配置 ${remoteMergePlatform.provider} 平台 token,请先执行 icode config platform token set ${remoteMergePlatform.provider} <token>。`)
140
+ }
141
+
142
+ const projectUrl = resolveRemoteMergeProjectUrl({
143
+ provider: remoteMergePlatform.provider,
144
+ remoteUrl: remoteMergePlatform.remoteUrl
145
+ })
146
+ if (!projectUrl) {
147
+ throw buildRemoteMergePauseError(targetBranch, `无法从 ${git.remote} 自动识别项目 URL,请检查 remote 地址。`, {
148
+ remote: remoteMergePlatform.remoteUrl
149
+ })
150
+ }
151
+ logger.info(`${remoteMergePlatform.provider} 项目 URL(${git.remote}): ${logger.color('green', projectUrl)}`)
152
+
153
+ logger.info(`发起远程合并: ${logger.color('yellow', currentBranch)} -> ${logger.color('yellow', targetBranch)}`)
154
+ const mergeRequestContent = await buildRemoteMergeCommitContent(git, currentBranch, targetBranch)
155
+ const result = await requestRemoteMerge({
156
+ provider: remoteMergePlatform.provider,
157
+ apiKey: remoteMergePlatform.apiKey,
158
+ projectUrl,
159
+ sourceBranch: currentBranch,
160
+ targetBranch,
161
+ mergeRequestTitle: mergeRequestContent.title,
162
+ mergeRequestDescription: mergeRequestContent.description,
163
+ repoRoot: git.cwd,
164
+ noVerify: inputOptions.noVerify
165
+ })
166
+
167
+ if (result.conflict) {
168
+ throw buildRemoteMergePauseError(targetBranch, buildRemoteMergeResultMessage('远程合并冲突', result), {
169
+ sourceBranch: currentBranch,
170
+ step: result.step || '',
171
+ result
172
+ })
173
+ }
174
+
175
+ if (!result.ok) {
176
+ throw buildRemoteMergePauseError(targetBranch, buildRemoteMergeResultMessage('远程合并失败', result), {
177
+ sourceBranch: currentBranch,
178
+ step: result.step || '',
179
+ result
180
+ })
181
+ }
182
+
183
+ const successReason = typeof result.reason === 'string' && result.reason.trim()
184
+ ? result.reason.trim()
185
+ : '远程合并已完成'
186
+
187
+ if (result.mergeRequestUrl) {
188
+ logger.success(`${successReason}: ${logger.color('green', result.mergeRequestUrl)}`)
189
+ } else {
190
+ logger.success(`${successReason}: ${logger.color('yellow', currentBranch)} -> ${logger.color('yellow', targetBranch)}`)
191
+ }
192
+
193
+ return result
194
+ }
195
+
196
+ // 并发执行多个远程合并目标分支,并把冲突/失败精确归档到对应分支。
197
+ export async function runConcurrentRemoteMerges(options) {
198
+ const tasks = options.targetBranches.map(async (targetBranch) => {
199
+ try {
200
+ logger.info(`处理分支: ${ui.branch(targetBranch)}`)
201
+ const remoteExists = await options.git.branchExistsRemote(targetBranch)
202
+ const remoteMergeResult = await performRemoteMerge({
203
+ ...options,
204
+ targetBranch,
205
+ remoteExists
206
+ })
207
+
208
+ return {
209
+ branch: targetBranch,
210
+ status: 'remote-merged-and-pushed',
211
+ mergeRequestUrl: remoteMergeResult.mergeRequestUrl || '',
212
+ mergeRequestId: remoteMergeResult.mergeRequestId || ''
213
+ }
214
+ } catch (error) {
215
+ return {
216
+ branch: targetBranch,
217
+ status: 'paused',
218
+ reason: resolveRemoteMergeFailureReason(error)
219
+ }
220
+ }
221
+ })
222
+
223
+ return Promise.all(tasks)
224
+ }