@icode-js/icode 3.1.0 → 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/README.md +105 -5
- package/package.json +1 -1
- package/src/cli.js +6 -0
- package/src/commands/clean.js +4 -1
- package/src/commands/codereview.js +4 -1
- package/src/commands/completion.js +9 -6
- package/src/commands/config.js +9 -6
- package/src/commands/doctor.js +80 -0
- package/src/commands/explain.js +4 -1
- package/src/commands/help.js +10 -0
- package/src/commands/info.js +4 -1
- package/src/commands/migrate.js +38 -1
- package/src/commands/push.js +45 -18
- package/src/commands/ready.js +110 -0
- package/src/commands/save.js +98 -0
- package/src/commands/undo.js +13 -1
- package/src/core/ai/diff-prompt.js +204 -0
- package/src/core/cli/args.js +23 -2
- package/src/core/cli/completion.js +33 -0
- package/src/core/config/store.js +2 -4
- package/src/core/git/context.js +14 -15
- package/src/core/git/preconditions.js +88 -0
- package/src/core/git/service.js +151 -1
- package/src/core/remote-merge/providers/gitlab.js +149 -90
- package/src/core/tools/interactive.js +209 -34
- package/src/core/tools/loading.js +5 -1
- package/src/core/tools/logger.js +5 -0
- package/src/workflows/ai-codereview-workflow.js +14 -2
- package/src/workflows/ai-commit-workflow.js +14 -6
- package/src/workflows/checkout-workflow.js +14 -2
- package/src/workflows/clean-workflow.js +87 -70
- package/src/workflows/doctor-workflow.js +131 -0
- package/src/workflows/migrate-workflow.js +265 -58
- package/src/workflows/push-workflow.js +232 -50
- package/src/workflows/ready-workflow.js +278 -0
- package/src/workflows/rollback-workflow.js +20 -5
- package/src/workflows/save-workflow.js +152 -0
- package/src/workflows/sync-workflow.js +18 -2
- package/src/workflows/undo-workflow.js +25 -7
|
@@ -2,6 +2,11 @@ import { formatAiCommitSummary } from '../core/ai/commit-summary.js'
|
|
|
2
2
|
import { getPlatformConfig, getRepoPolicy } from '../core/config/store.js'
|
|
3
3
|
import { IcodeError } from '../core/errors.js'
|
|
4
4
|
import { resolveGitContext } from '../core/git/context.js'
|
|
5
|
+
import {
|
|
6
|
+
assertNoConflictedFiles,
|
|
7
|
+
assertNoPendingOperation,
|
|
8
|
+
readGitSafetyState
|
|
9
|
+
} from '../core/git/preconditions.js'
|
|
5
10
|
import { GitService } from '../core/git/service.js'
|
|
6
11
|
import { confirm, input } from '../core/tools/interactive.js'
|
|
7
12
|
import { requestRemoteMerge, resolveRemoteMergeProjectUrl } from '../core/remote-merge/client.js'
|
|
@@ -112,9 +117,9 @@ async function prepareAiCommitIfEnabled(inputOptions) {
|
|
|
112
117
|
}
|
|
113
118
|
}
|
|
114
119
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (
|
|
120
|
+
// 在 push 场景下按需创建本地提交,输入 GitService、命令参数和已读取的改动摘要,输出是否提交。
|
|
121
|
+
async function prepareCommitIfNeeded(git, options, changes) {
|
|
122
|
+
if (changes.clean) {
|
|
118
123
|
logger.info('工作区无改动,跳过 commit。')
|
|
119
124
|
return false
|
|
120
125
|
}
|
|
@@ -140,7 +145,52 @@ async function prepareCommitIfNeeded(git, options) {
|
|
|
140
145
|
return true
|
|
141
146
|
}
|
|
142
147
|
|
|
143
|
-
|
|
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 = []) {
|
|
144
194
|
const localExists = await git.branchExistsLocal(targetBranch)
|
|
145
195
|
const remoteExists = await git.branchExistsRemote(targetBranch)
|
|
146
196
|
let checkoutMode = 'local'
|
|
@@ -158,10 +208,19 @@ async function checkoutTargetBranch(git, targetBranch, sourceBranch) {
|
|
|
158
208
|
}
|
|
159
209
|
|
|
160
210
|
if (remoteExists) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
+
}
|
|
165
224
|
}
|
|
166
225
|
|
|
167
226
|
return {
|
|
@@ -173,8 +232,7 @@ async function checkoutTargetBranch(git, targetBranch, sourceBranch) {
|
|
|
173
232
|
function normalizeRemoteMergePolicy(policy = {}) {
|
|
174
233
|
const remoteMerge = policy.remoteMerge && typeof policy.remoteMerge === 'object' ? policy.remoteMerge : {}
|
|
175
234
|
return {
|
|
176
|
-
enabled: remoteMerge.enabled !== false
|
|
177
|
-
projectUrl: typeof remoteMerge.projectUrl === 'string' ? remoteMerge.projectUrl.trim() : ''
|
|
235
|
+
enabled: remoteMerge.enabled !== false
|
|
178
236
|
}
|
|
179
237
|
}
|
|
180
238
|
|
|
@@ -198,7 +256,7 @@ function buildRemoteMergePauseError(targetBranch, reason, meta = {}) {
|
|
|
198
256
|
})
|
|
199
257
|
}
|
|
200
258
|
|
|
201
|
-
// 统一把 provider 返回的 step 注入错误文案,便于定位 create/
|
|
259
|
+
// 统一把 provider 返回的 step 注入错误文案,便于定位 create/merge/inspect 具体失败点。
|
|
202
260
|
function buildRemoteMergeResultMessage(prefix, result) {
|
|
203
261
|
const reason = typeof result?.reason === 'string' && result.reason.trim()
|
|
204
262
|
? result.reason.trim()
|
|
@@ -225,6 +283,77 @@ function buildRemoteMergeBatchPauseError(failures, summary) {
|
|
|
225
283
|
})
|
|
226
284
|
}
|
|
227
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
|
+
|
|
228
357
|
// 基于 origin 推导 GitLab 项目地址并发起固定 API 的 MR 创建请求。
|
|
229
358
|
async function performRemoteMerge({ git, currentBranch, targetBranch, remoteExists, inputOptions, remoteMergePolicy, remoteMergePlatform }) {
|
|
230
359
|
if (!remoteExists) {
|
|
@@ -242,18 +371,15 @@ async function performRemoteMerge({ git, currentBranch, targetBranch, remoteExis
|
|
|
242
371
|
}
|
|
243
372
|
|
|
244
373
|
const remoteOriginUrl = await git.getRemoteUrl('origin')
|
|
245
|
-
const projectUrl =
|
|
246
|
-
provider: remoteMergePlatform.provider,
|
|
247
|
-
remoteUrl: remoteOriginUrl
|
|
248
|
-
})
|
|
374
|
+
const { projectUrl, source } = resolveRemoteMergeProjectUrlFromOrigin(remoteMergePlatform, remoteOriginUrl)
|
|
249
375
|
if (!projectUrl) {
|
|
250
376
|
throw buildRemoteMergePauseError(targetBranch, '无法从 origin 自动识别 GitLab 项目 URL,请检查 origin 是否指向 GitLab 仓库。', {
|
|
251
377
|
origin: remoteOriginUrl
|
|
252
378
|
})
|
|
253
379
|
}
|
|
254
|
-
logger.info(
|
|
380
|
+
logger.info(`GitLab 项目 URL(${source}): ${logger.color('green', projectUrl)}`)
|
|
255
381
|
|
|
256
|
-
logger.info(`发起远程合并: ${currentBranch} -> ${targetBranch}`)
|
|
382
|
+
logger.info(`发起远程合并: ${logger.color('yellow', currentBranch)} -> ${logger.color('yellow', targetBranch)}`)
|
|
257
383
|
const mergeRequestContent = await buildRemoteMergeCommitContent(git, currentBranch, targetBranch)
|
|
258
384
|
const result = await requestRemoteMerge({
|
|
259
385
|
provider: remoteMergePlatform.provider,
|
|
@@ -288,9 +414,9 @@ async function performRemoteMerge({ git, currentBranch, targetBranch, remoteExis
|
|
|
288
414
|
: '远程合并已完成'
|
|
289
415
|
|
|
290
416
|
if (result.mergeRequestUrl) {
|
|
291
|
-
logger.success(`${successReason}: ${result.mergeRequestUrl}`)
|
|
417
|
+
logger.success(`${successReason}: ${logger.color('green', result.mergeRequestUrl)}`)
|
|
292
418
|
} else {
|
|
293
|
-
logger.success(`${successReason}: ${currentBranch} -> ${targetBranch}`)
|
|
419
|
+
logger.success(`${successReason}: ${logger.color('yellow', currentBranch)} -> ${logger.color('yellow', targetBranch)}`)
|
|
294
420
|
}
|
|
295
421
|
|
|
296
422
|
return result
|
|
@@ -326,6 +452,10 @@ async function runConcurrentRemoteMerges(options) {
|
|
|
326
452
|
return Promise.all(tasks)
|
|
327
453
|
}
|
|
328
454
|
|
|
455
|
+
/**
|
|
456
|
+
* 执行 push 工作流。
|
|
457
|
+
* 输入为 push 命令选项,输出推送/合并摘要;关键假设是实际执行前必须没有未完成操作和冲突文件。
|
|
458
|
+
*/
|
|
329
459
|
export async function runPushWorkflow(inputOptions) {
|
|
330
460
|
const context = await resolveGitContext({
|
|
331
461
|
cwd: inputOptions.cwd,
|
|
@@ -351,6 +481,40 @@ export async function runPushWorkflow(inputOptions) {
|
|
|
351
481
|
})
|
|
352
482
|
}
|
|
353
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
|
+
|
|
354
518
|
const aiCommitResult = await prepareAiCommitIfEnabled(inputOptions)
|
|
355
519
|
if (aiCommitResult.canceled) {
|
|
356
520
|
return {
|
|
@@ -364,31 +528,27 @@ export async function runPushWorkflow(inputOptions) {
|
|
|
364
528
|
|
|
365
529
|
const shouldRunManualCommit = !aiCommitResult.applied && aiCommitResult.reason !== 'no-diff'
|
|
366
530
|
if (shouldRunManualCommit) {
|
|
367
|
-
await prepareCommitIfNeeded(git, inputOptions)
|
|
531
|
+
await prepareCommitIfNeeded(git, inputOptions, safetyState.changes)
|
|
368
532
|
}
|
|
369
533
|
await git.fetch()
|
|
370
534
|
|
|
535
|
+
const summary = []
|
|
536
|
+
|
|
371
537
|
if (inputOptions.pullMain && context.defaultBranch !== currentBranch) {
|
|
372
538
|
logger.info(`先同步主分支 ${context.defaultBranch} 到当前分支 ${currentBranch}`)
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (!branchTargets.length) {
|
|
388
|
-
throw new IcodeError('没有可执行的目标分支。', {
|
|
389
|
-
code: 'PUSH_EMPTY_TARGETS',
|
|
390
|
-
exitCode: 2
|
|
391
|
-
})
|
|
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
|
+
}
|
|
392
552
|
}
|
|
393
553
|
|
|
394
554
|
if (!inputOptions.yes) {
|
|
@@ -407,7 +567,6 @@ export async function runPushWorkflow(inputOptions) {
|
|
|
407
567
|
}
|
|
408
568
|
}
|
|
409
569
|
|
|
410
|
-
const summary = []
|
|
411
570
|
const originalBranch = currentBranch
|
|
412
571
|
const remoteMergeTargets = []
|
|
413
572
|
|
|
@@ -424,10 +583,19 @@ export async function runPushWorkflow(inputOptions) {
|
|
|
424
583
|
const remoteExists = await git.branchExistsRemote(targetBranch)
|
|
425
584
|
if (remoteExists) {
|
|
426
585
|
logger.info(`同步远程分支: ${targetBranch}`)
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
+
}
|
|
431
599
|
}
|
|
432
600
|
|
|
433
601
|
logger.info(`推送当前分支: ${targetBranch}`)
|
|
@@ -446,15 +614,25 @@ export async function runPushWorkflow(inputOptions) {
|
|
|
446
614
|
} else {
|
|
447
615
|
logger.info(`处理分支: ${targetBranch}`)
|
|
448
616
|
logger.info(`切换到目标分支: ${targetBranch}`)
|
|
449
|
-
const { remoteExists, checkoutMode } = await checkoutTargetBranch(git, targetBranch, currentBranch)
|
|
617
|
+
const { remoteExists, checkoutMode } = await checkoutTargetBranch(git, targetBranch, currentBranch, summary)
|
|
450
618
|
logger.info(`目标分支准备完成: ${targetBranch} (${checkoutMode})`)
|
|
451
619
|
|
|
452
620
|
// 保留 merge commit,方便后续追溯“从哪个分支合并过来”。
|
|
453
621
|
logger.info(`合并分支: ${currentBranch} -> ${targetBranch}`)
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
+
}
|
|
458
636
|
logger.success(`合并成功: ${currentBranch} -> ${targetBranch}`)
|
|
459
637
|
|
|
460
638
|
logger.info(`推送目标分支: ${targetBranch}`)
|
|
@@ -493,7 +671,11 @@ export async function runPushWorkflow(inputOptions) {
|
|
|
493
671
|
}
|
|
494
672
|
} finally {
|
|
495
673
|
const branchAfterWorkflow = await git.getCurrentBranch()
|
|
496
|
-
|
|
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) {
|
|
497
679
|
try {
|
|
498
680
|
await git.checkout(originalBranch)
|
|
499
681
|
} catch (error) {
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { getAiConfig } from '../core/ai/config.js'
|
|
2
|
+
import { getPlatformConfig, getRepoPolicy } from '../core/config/store.js'
|
|
3
|
+
import { resolveGitContext } from '../core/git/context.js'
|
|
4
|
+
import { GitService } from '../core/git/service.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 构建 ready 检查项。
|
|
8
|
+
* 输入为检查字段,输出稳定的检查项对象;关键假设是 status 只能是 pass/warn/fail。
|
|
9
|
+
*/
|
|
10
|
+
function buildCheck(id, status, title, detail, nextStep = '') {
|
|
11
|
+
return {
|
|
12
|
+
id,
|
|
13
|
+
status,
|
|
14
|
+
title,
|
|
15
|
+
detail,
|
|
16
|
+
nextStep
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 汇总 ready 检查状态。
|
|
22
|
+
* 输入为检查项数组,输出整体 ok/status 和计数。
|
|
23
|
+
*/
|
|
24
|
+
function summarizeReadyChecks(checks) {
|
|
25
|
+
const summary = {
|
|
26
|
+
ok: true,
|
|
27
|
+
status: 'pass',
|
|
28
|
+
pass: 0,
|
|
29
|
+
warn: 0,
|
|
30
|
+
fail: 0
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const check of checks) {
|
|
34
|
+
if (check.status === 'fail') {
|
|
35
|
+
summary.ok = false
|
|
36
|
+
summary.status = 'fail'
|
|
37
|
+
summary.fail += 1
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (check.status === 'warn') {
|
|
42
|
+
summary.warn += 1
|
|
43
|
+
if (summary.status !== 'fail') {
|
|
44
|
+
summary.status = 'warn'
|
|
45
|
+
}
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
summary.pass += 1
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return summary
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 从检查项提取下一步建议。
|
|
57
|
+
* 输入为检查项数组,输出去重后的建议文本数组。
|
|
58
|
+
*/
|
|
59
|
+
function collectNextSteps(checks) {
|
|
60
|
+
const nextSteps = []
|
|
61
|
+
const seen = new Set()
|
|
62
|
+
|
|
63
|
+
for (const check of checks) {
|
|
64
|
+
const nextStep = String(check.nextStep || '').trim()
|
|
65
|
+
if (!nextStep || seen.has(nextStep)) {
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
seen.add(nextStep)
|
|
69
|
+
nextSteps.push(nextStep)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return nextSteps
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 检查当前分支是否存在。
|
|
77
|
+
* 输入为分支名,输出 ready 检查项。
|
|
78
|
+
*/
|
|
79
|
+
function checkCurrentBranch(currentBranch) {
|
|
80
|
+
if (!currentBranch) {
|
|
81
|
+
return buildCheck(
|
|
82
|
+
'git.branch',
|
|
83
|
+
'fail',
|
|
84
|
+
'当前不在普通分支上',
|
|
85
|
+
'检测到 detached HEAD 或无法读取当前分支。',
|
|
86
|
+
'请先切换到明确分支,例如: icode checkout <branch>'
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return buildCheck('git.branch', 'pass', '当前分支明确', currentBranch)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 检查是否存在未完成的 Git 操作。
|
|
95
|
+
* 输入为 operation 字符串,输出 ready 检查项。
|
|
96
|
+
*/
|
|
97
|
+
function checkInProgressOperation(operation) {
|
|
98
|
+
if (!operation) {
|
|
99
|
+
return buildCheck('git.operation', 'pass', '没有未完成的 Git 操作', '工作区未处于 merge/rebase/cherry-pick/revert 流程中。')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return buildCheck(
|
|
103
|
+
'git.operation',
|
|
104
|
+
'fail',
|
|
105
|
+
'存在未完成的 Git 操作',
|
|
106
|
+
`当前操作: ${operation}`,
|
|
107
|
+
`请先完成或中止 ${operation},再继续执行 save/push。`
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 检查工作区改动情况。
|
|
113
|
+
* 输入为结构化改动摘要,输出 ready 检查项。
|
|
114
|
+
*/
|
|
115
|
+
function checkChanges(changes) {
|
|
116
|
+
if (changes.conflicted > 0) {
|
|
117
|
+
return buildCheck(
|
|
118
|
+
'git.changes',
|
|
119
|
+
'fail',
|
|
120
|
+
'存在未解决冲突',
|
|
121
|
+
`冲突文件 ${changes.conflicted} 个,全部改动 ${changes.total} 个。`,
|
|
122
|
+
'请先解决冲突;如需要建议可执行: icode ai conflict'
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!changes.clean) {
|
|
127
|
+
return buildCheck(
|
|
128
|
+
'git.changes',
|
|
129
|
+
'warn',
|
|
130
|
+
'存在未提交改动',
|
|
131
|
+
`staged=${changes.staged}, unstaged=${changes.unstaged}, untracked=${changes.untracked}`,
|
|
132
|
+
'如需安全快速提交,可执行: icode save -m "type: subject"'
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return buildCheck('git.changes', 'pass', '工作区干净', '没有暂存、未暂存或未跟踪改动。')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 检查 upstream 关系。
|
|
141
|
+
* 输入为 upstream 状态,输出 ready 检查项。
|
|
142
|
+
*/
|
|
143
|
+
function checkUpstream(upstreamStatus) {
|
|
144
|
+
if (!upstreamStatus.configured) {
|
|
145
|
+
return buildCheck(
|
|
146
|
+
'git.upstream',
|
|
147
|
+
'warn',
|
|
148
|
+
'当前分支未设置 upstream',
|
|
149
|
+
'无法判断本地与远程领先/落后关系。',
|
|
150
|
+
'如需建立远程跟踪关系,可在首次推送时使用: icode push -m "type: subject"'
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (upstreamStatus.behind > 0) {
|
|
155
|
+
return buildCheck(
|
|
156
|
+
'git.upstream',
|
|
157
|
+
'warn',
|
|
158
|
+
'当前分支落后 upstream',
|
|
159
|
+
`${upstreamStatus.upstream}: ahead=${upstreamStatus.ahead}, behind=${upstreamStatus.behind}`,
|
|
160
|
+
'建议先同步远程变更: icode sync'
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (upstreamStatus.ahead > 0) {
|
|
165
|
+
return buildCheck(
|
|
166
|
+
'git.upstream',
|
|
167
|
+
'warn',
|
|
168
|
+
'当前分支有未推送提交',
|
|
169
|
+
`${upstreamStatus.upstream}: ahead=${upstreamStatus.ahead}, behind=${upstreamStatus.behind}`,
|
|
170
|
+
'如需发布当前分支,可执行: icode push'
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return buildCheck(
|
|
175
|
+
'git.upstream',
|
|
176
|
+
'pass',
|
|
177
|
+
'upstream 已同步',
|
|
178
|
+
`${upstreamStatus.upstream}: ahead=0, behind=0`
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 检查 AI profile 是否可用于 AI 场景。
|
|
184
|
+
* 输入为 AI 配置,输出 ready 检查项。
|
|
185
|
+
*/
|
|
186
|
+
function checkAiProfile(aiConfig) {
|
|
187
|
+
const activeProfile = String(aiConfig.activeProfile || '').trim()
|
|
188
|
+
const profileCount = Object.keys(aiConfig.profiles || {}).length
|
|
189
|
+
|
|
190
|
+
if (!activeProfile || profileCount === 0) {
|
|
191
|
+
return buildCheck(
|
|
192
|
+
'ai.profile',
|
|
193
|
+
'warn',
|
|
194
|
+
'AI profile 未配置',
|
|
195
|
+
'save --ai-commit、codereview、explain 等能力不可用。',
|
|
196
|
+
'如需 AI 能力,请先执行: icode config ai set <name> ... --activate'
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return buildCheck('ai.profile', 'pass', 'AI profile 可用', `activeProfile=${activeProfile}, profileCount=${profileCount}`)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 检查远程合并配置是否可用于 remote-merge 场景。
|
|
205
|
+
* 输入为平台配置和仓库策略,输出 ready 检查项。
|
|
206
|
+
*/
|
|
207
|
+
function checkRemoteMerge(remoteMergePlatform, repoPolicy) {
|
|
208
|
+
const repoRemoteMerge = repoPolicy.remoteMerge && typeof repoPolicy.remoteMerge === 'object'
|
|
209
|
+
? repoPolicy.remoteMerge
|
|
210
|
+
: {}
|
|
211
|
+
|
|
212
|
+
if (repoRemoteMerge.enabled === false) {
|
|
213
|
+
return buildCheck('remoteMerge.config', 'pass', '远程合并已禁用', '当前仓库策略明确禁用 remote merge。')
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!remoteMergePlatform.provider || !remoteMergePlatform.apiKey) {
|
|
217
|
+
return buildCheck(
|
|
218
|
+
'remoteMerge.config',
|
|
219
|
+
'warn',
|
|
220
|
+
'远程合并配置不完整',
|
|
221
|
+
'缺少 provider 或 apiKey,push -r 不可用。',
|
|
222
|
+
'如需远程合并,请执行: icode config platform remote-merge set --provider gitlab --api-key <key>'
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return buildCheck('remoteMerge.config', 'pass', '远程合并配置可用', `provider=${remoteMergePlatform.provider}`)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* 执行场景化就绪检查。
|
|
231
|
+
* 输入为仓库选项,输出面向人类和 AI agent 的稳定状态结构。
|
|
232
|
+
*/
|
|
233
|
+
export async function runReadyWorkflow(options = {}) {
|
|
234
|
+
const context = await resolveGitContext({
|
|
235
|
+
cwd: options.cwd,
|
|
236
|
+
repoMode: options.repoMode
|
|
237
|
+
})
|
|
238
|
+
const git = new GitService(context)
|
|
239
|
+
const [currentBranch, operation, changes, upstream] = await Promise.all([
|
|
240
|
+
git.getCurrentBranch(),
|
|
241
|
+
git.getInProgressOperation(),
|
|
242
|
+
git.getStatusSummary(),
|
|
243
|
+
git.getUpstreamStatus()
|
|
244
|
+
])
|
|
245
|
+
const aiConfig = getAiConfig()
|
|
246
|
+
const repoPolicy = getRepoPolicy(context.topLevelPath)
|
|
247
|
+
const remoteMergePlatform = getPlatformConfig('remoteMerge')
|
|
248
|
+
|
|
249
|
+
const checks = [
|
|
250
|
+
checkCurrentBranch(currentBranch),
|
|
251
|
+
checkInProgressOperation(operation),
|
|
252
|
+
checkChanges(changes),
|
|
253
|
+
checkUpstream(upstream),
|
|
254
|
+
checkAiProfile(aiConfig),
|
|
255
|
+
checkRemoteMerge(remoteMergePlatform, repoPolicy)
|
|
256
|
+
]
|
|
257
|
+
const summary = summarizeReadyChecks(checks)
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
ok: summary.ok,
|
|
261
|
+
status: summary.status,
|
|
262
|
+
summary,
|
|
263
|
+
context: {
|
|
264
|
+
cwd: context.cwd,
|
|
265
|
+
topLevelPath: context.topLevelPath,
|
|
266
|
+
repoMode: context.repoMode,
|
|
267
|
+
currentBranch,
|
|
268
|
+
defaultBranch: context.defaultBranch,
|
|
269
|
+
inheritedFromParent: context.inheritedFromParent,
|
|
270
|
+
isSubmodule: context.isSubmodule
|
|
271
|
+
},
|
|
272
|
+
operation,
|
|
273
|
+
changes,
|
|
274
|
+
upstream,
|
|
275
|
+
checks,
|
|
276
|
+
nextSteps: collectNextSteps(checks)
|
|
277
|
+
}
|
|
278
|
+
}
|