@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.
- package/LICENSE +21 -0
- package/README.md +142 -5
- package/bin/icode.js +4 -4
- package/package.json +1 -1
- package/src/cli.js +60 -23
- package/src/commands/ai.js +4 -4
- package/src/commands/checkout.js +2 -2
- package/src/commands/clean.js +5 -2
- package/src/commands/codereview.js +5 -2
- package/src/commands/completion.js +93 -0
- package/src/commands/config.js +123 -10
- package/src/commands/doctor.js +80 -0
- package/src/commands/explain.js +6 -3
- package/src/commands/help.js +17 -0
- package/src/commands/info.js +5 -2
- package/src/commands/migrate.js +40 -3
- package/src/commands/push.js +72 -24
- package/src/commands/ready.js +110 -0
- package/src/commands/save.js +98 -0
- package/src/commands/sync.js +2 -2
- package/src/commands/tag.js +2 -2
- package/src/commands/undo.js +73 -10
- package/src/core/{ai-client.js → ai/client.js} +3 -3
- package/src/core/{ai-config.js → ai/config.js} +2 -2
- package/src/core/ai/diff-prompt.js +204 -0
- package/src/core/{ai-diff-range.js → ai/diff-range.js} +2 -2
- package/src/core/{args.js → cli/args.js} +23 -2
- package/src/core/cli/completion.js +720 -0
- package/src/core/cli/update-notifier.js +138 -0
- package/src/core/{shell.js → commands/shell.js} +13 -1
- package/src/core/{config-store.js → config/store.js} +53 -3
- package/src/core/{git-context.js → git/context.js} +24 -10
- package/src/core/git/preconditions.js +88 -0
- package/src/core/{git-service.js → git/service.js} +195 -5
- package/src/core/remote-merge/client.js +42 -0
- package/src/core/remote-merge/providers/gitlab.js +596 -0
- package/src/core/remote-merge/shared.js +49 -0
- package/src/core/tools/interactive.js +368 -0
- package/src/core/{loading.js → tools/loading.js} +5 -1
- package/src/core/{logger.js → tools/logger.js} +5 -0
- package/src/workflows/ai-codereview-workflow.js +18 -6
- package/src/workflows/ai-commit-workflow.js +20 -12
- package/src/workflows/ai-conflict-workflow.js +4 -4
- package/src/workflows/ai-explain-workflow.js +4 -4
- package/src/workflows/ai-risk-review-workflow.js +1 -1
- package/src/workflows/checkout-workflow.js +17 -5
- package/src/workflows/clean-workflow.js +92 -75
- package/src/workflows/doctor-workflow.js +131 -0
- package/src/workflows/info-workflow.js +4 -4
- package/src/workflows/migrate-workflow.js +269 -62
- package/src/workflows/push-workflow.js +475 -58
- package/src/workflows/ready-workflow.js +278 -0
- package/src/workflows/rollback-workflow.js +24 -9
- package/src/workflows/save-workflow.js +152 -0
- package/src/workflows/sync-workflow.js +22 -6
- package/src/workflows/tag-workflow.js +4 -4
- package/src/workflows/undo-workflow.js +37 -12
- package/src/core/prompts.js +0 -152
- /package/src/core/{ai-commit-summary.js → ai/commit-summary.js} +0 -0
- /package/src/core/{commit-conventions.js → git/commit-conventions.js} +0 -0
- /package/src/core/{hook-diagnostics.js → git/hook-diagnostics.js} +0 -0
|
@@ -1,16 +1,69 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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) {
|