@icode-js/icode 3.3.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.
package/README.md CHANGED
@@ -334,7 +334,7 @@ icode push release test -r -y # 使用远程 MR 模式合并到多个目标分
334
334
 
335
335
  - 自动 `add + commit + push`
336
336
  - 存在冲突或未完成 Git 操作时会直接停止
337
- - 本地 merge 冲突会暂停在目标分支现场;解决后执行 `icode undo --recover continue`,或执行 `icode undo --recover abort` 中止
337
+ - 本地 merge 冲突会暂停在目标分支现场;解决冲突后执行 `icode continue`,会自动暂存、推送该分支、继续处理剩下的目标分支并切回原分支;执行 `icode abort` 则放弃并切回原分支
338
338
  - 支持 `--ai-commit` 在 push 前自动生成并应用 AI 提交信息
339
339
  - 若仓库存在 husky/hooks/commitlint 规则,AI commit 会先扫描本地规范再生成提交信息
340
340
  - 支持把当前分支合并到多个目标分支
@@ -567,7 +567,31 @@ icode fixup a1b2c3d -a
567
567
  icode fixup a1b2c3d --no-rebase
568
568
  ```
569
569
 
570
- 等同 `git commit --fixup=<commit>` + `git rebase -i --autosquash`,不会打开编辑器。冲突时用 `icode undo --recover continue|abort` 处理。
570
+ 等同 `git commit --fixup=<commit>` + `git rebase -i --autosquash`,不会打开编辑器。冲突时用 `icode continue` / `icode abort` 处理。
571
+
572
+ ### continue / abort
573
+
574
+ 冲突解决后继续:
575
+
576
+ ```bash
577
+ icode continue
578
+ ```
579
+
580
+ 1. 检查冲突文件里是否还有 `<<<<<<<` / `>>>>>>>`,有就列出文件和行号,不会继续
581
+ 2. 自动 `git add` 已解决的文件
582
+ 3. 继续 merge / rebase / cherry-pick / revert(不会打开编辑器)
583
+ 4. 如果是 `push` / `migrate` / `sync` 中途冲突,接着把剩下的步骤做完:推送当前分支、处理剩余目标分支、切回原分支
584
+
585
+ 放弃:
586
+
587
+ ```bash
588
+ icode abort
589
+ ```
590
+
591
+ 中止进行中的 Git 操作,切回原分支。已经推送成功的分支不会被撤回。
592
+
593
+ 有被中断的操作时,`icode ready` 会标出来,直接运行 `icode` 的菜单也会把“继续 / 放弃”放在最前面。
594
+ `icode undo --recover continue|abort` 仍然可用,效果相同。
571
595
 
572
596
  ### rescue
573
597
 
@@ -619,7 +643,7 @@ icode migrate <sourceBranch> <targetBranch> --dry-run --json
619
643
  - 执行前要求工作区干净,且不能存在未完成的 merge/rebase/cherry-pick/revert
620
644
  - 支持 `--dry-run` 输出 checkout/pull/cherry-pick/push 计划,不产生 Git 副作用
621
645
  - 支持 `--json` 输出结构化计划或执行结果,适合 AI agent 消费
622
- - 如果迁移冲突,可使用 `icode undo --recover continue|abort|keep` 处理现场
646
+ - 如果迁移冲突,解决后执行 `icode continue`(会接着迁移剩下的提交,带 `--push` 时自动推送并切回原分支),或执行 `icode abort` 放弃
623
647
 
624
648
  参数说明:
625
649
 
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@icode-js/icode",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "Modern git workflow CLI with robust parent-repo and hook handling",
5
5
  "type": "module",
6
6
  "author": "C",
7
7
  "bin": {
8
- "icode": "./bin/icode.js"
8
+ "icode": "bin/icode.js"
9
9
  },
10
10
  "scripts": {
11
11
  "test": "node --test",
@@ -19,7 +19,7 @@
19
19
  ],
20
20
  "repository": {
21
21
  "type": "git",
22
- "url": "https://github.com/wandou-cc/icode-js.git"
22
+ "url": "git+https://github.com/wandou-cc/icode-js.git"
23
23
  },
24
24
  "homepage": "https://github.com/wandou-cc/icode-js",
25
25
  "license": "MIT",
package/src/cli.js CHANGED
@@ -4,6 +4,7 @@ import { runCodeReviewCommand } from './commands/codereview.js'
4
4
  import { runCompletionCommand, runHiddenCompletionCommand } from './commands/completion.js'
5
5
  import { runCheckoutCommand } from './commands/checkout.js'
6
6
  import { runConfigCommand } from './commands/config.js'
7
+ import { runAbortCommand, runContinueCommand } from './commands/continue.js'
7
8
  import { runDoctorCommand } from './commands/doctor.js'
8
9
  import { runExplainCommand } from './commands/explain.js'
9
10
  import { printMainHelp } from './commands/help.js'
@@ -27,6 +28,8 @@ import { asIcodeError, IcodeError } from './core/errors.js'
27
28
  import { normalizeLegacyArgs } from './core/cli/args.js'
28
29
  import { notifyIfCliUpdateAvailable } from './core/cli/update-notifier.js'
29
30
  import { chooseOne, isInteractiveTerminal } from './core/tools/interactive.js'
31
+ import { c } from './core/tools/theme.js'
32
+ import { inspectPausedWork } from './workflows/continue-workflow.js'
30
33
  import { logger } from './core/tools/logger.js'
31
34
 
32
35
  function isTruthy(value) {
@@ -80,6 +83,8 @@ const COMMANDS = {
80
83
  sync: runSyncCommand,
81
84
  clean: runCleanCommand,
82
85
  undo: runUndoCommand,
86
+ continue: runContinueCommand,
87
+ abort: runAbortCommand,
83
88
  rescue: runRescueCommand,
84
89
  stash: runStashCommand,
85
90
  sw: runSwCommand,
@@ -117,6 +122,7 @@ const MAIN_MENU = [
117
122
  { value: 'amend', label: 'amend', hint: '修改上一个提交' },
118
123
  { value: 'fixup', label: 'fixup', hint: '把改动合进更早的提交' },
119
124
  { value: 'undo', label: 'undo', hint: '撤销 / 回滚' },
125
+ { value: 'continue', label: 'continue', hint: '冲突解决后继续' },
120
126
  { value: 'rescue', label: 'rescue', hint: '找回丢失的提交和改动' },
121
127
  { value: 'clean', label: 'clean', hint: '清理已合并的分支' },
122
128
  { value: 'pr', label: 'pr create', hint: '创建 PR / MR' },
@@ -129,7 +135,21 @@ const MAIN_MENU = [
129
135
  ]
130
136
 
131
137
  async function pickCommandFromMenu() {
132
- const picked = await chooseOne('要做什么?', MAIN_MENU, 0)
138
+ // 有被冲突打断的操作时,把“继续 / 放弃”放在最前面。
139
+ let paused = null
140
+ try {
141
+ paused = await inspectPausedWork({})
142
+ } catch {
143
+ paused = null
144
+ }
145
+ const menu = paused
146
+ ? [
147
+ { value: 'continue', label: c.yellow('continue'), hint: `冲突解决后继续:${paused.description}` },
148
+ { value: 'abort', label: 'abort', hint: '放弃被中断的操作并切回原分支' },
149
+ ...MAIN_MENU
150
+ ]
151
+ : MAIN_MENU
152
+ const picked = await chooseOne(paused ? `有被中断的操作(${paused.description}),要做什么?` : '要做什么?', menu, 0)
133
153
  // pr 从菜单进入时直接走创建流程。
134
154
  if (picked === 'pr') {
135
155
  await COMMANDS.pr(['create'])
@@ -0,0 +1,104 @@
1
+ import { parseCommandArgs, printJson } from '../core/cli/output.js'
2
+ import { c, print, renderBranchResults } from '../core/cli/render.js'
3
+ import { confirm, isInteractiveTerminal } from '../core/tools/interactive.js'
4
+ import { logger } from '../core/tools/logger.js'
5
+ import { inspectPausedWork, runAbortWorkflow, runContinueWorkflow } from '../workflows/continue-workflow.js'
6
+
7
+ const RESULT_LABELS = {
8
+ pushed: '已推送',
9
+ 'merged-and-pushed': '已合并并推送',
10
+ 'force-pushed': '已强推',
11
+ 'skipped-protected': '已跳过(受保护分支)',
12
+ synced: '已同步',
13
+ 'synced-and-pushed': '已同步并推送',
14
+ missing: '本地和远程都不存在'
15
+ }
16
+
17
+ function printContinueHelp() {
18
+ process.stdout.write(`
19
+ Usage:
20
+ icode continue [options]
21
+
22
+ 解决冲突后继续刚才被打断的操作:
23
+ 1. 检查冲突文件里是否还有 <<<<<<< / >>>>>>> 标记,有就列出文件和行号
24
+ 2. 自动 git add 已解决的文件
25
+ 3. 继续 merge / rebase / cherry-pick / revert
26
+ 4. 如果是 push / migrate / sync 中途冲突,接着把剩下的步骤做完(推送、处理剩余分支、切回原分支)
27
+
28
+ Options:
29
+ --json 输出机器可读 JSON
30
+ --repo-mode <mode> 仓库模式: auto | strict
31
+ -h, --help 查看帮助
32
+ `)
33
+ }
34
+
35
+ function printAbortHelp() {
36
+ process.stdout.write(`
37
+ Usage:
38
+ icode abort [options]
39
+
40
+ 放弃刚才被冲突打断的操作:中止 merge / rebase / cherry-pick / revert,并切回原分支。
41
+ 已经推送成功的分支不会被撤回。
42
+
43
+ Options:
44
+ -y, --yes 不再确认
45
+ --json 输出机器可读 JSON
46
+ --repo-mode <mode> 仓库模式: auto | strict
47
+ -h, --help 查看帮助
48
+ `)
49
+ }
50
+
51
+ export async function runContinueCommand(rawArgs) {
52
+ const { values } = parseCommandArgs(rawArgs)
53
+ if (values.help) {
54
+ printContinueHelp()
55
+ return
56
+ }
57
+
58
+ const result = await runContinueWorkflow({ repoMode: values['repo-mode'], silentLog: values.json })
59
+ if (values.json) {
60
+ printJson(result)
61
+ return result
62
+ }
63
+
64
+ if (result.summary.length) {
65
+ print('')
66
+ renderBranchResults(result.summary, RESULT_LABELS)
67
+ print('')
68
+ }
69
+ if (result.resumed) {
70
+ logger.success(`${result.description} 已全部完成${result.returnedTo ? c.gray(`,已切回 ${result.returnedTo}`) : ''}`)
71
+ }
72
+ return result
73
+ }
74
+
75
+ export async function runAbortCommand(rawArgs) {
76
+ const { values } = parseCommandArgs(rawArgs)
77
+ if (values.help) {
78
+ printAbortHelp()
79
+ return
80
+ }
81
+
82
+ if (!values.yes && !values.json && isInteractiveTerminal()) {
83
+ const paused = await inspectPausedWork({ repoMode: values['repo-mode'] })
84
+ if (paused) {
85
+ const accepted = await confirm(`放弃 ${paused.description}?冲突处理到一半的改动会丢弃`, false)
86
+ if (!accepted) {
87
+ logger.hint('已保留现场。处理完冲突后执行 icode continue。')
88
+ return
89
+ }
90
+ }
91
+ }
92
+
93
+ const result = await runAbortWorkflow({ repoMode: values['repo-mode'] })
94
+ if (values.json) {
95
+ printJson(result)
96
+ return result
97
+ }
98
+
99
+ logger.success(`已放弃 ${result.description || result.operation}${result.returnedTo ? c.gray(`,已切回 ${result.returnedTo}`) : ''}`)
100
+ if (result.done.length) {
101
+ logger.hint(`这些分支在中断前已推送,不会撤回: ${result.done.join(', ')}`)
102
+ }
103
+ return result
104
+ }
@@ -26,7 +26,9 @@ Usage:
26
26
  clean 清理已合并 / 远程已删除 / 长期未动的分支
27
27
 
28
28
  兜底:
29
- undo 向导式撤销/回滚(revert/reset),处理未完成的 merge/rebase
29
+ continue 冲突解决后继续(自动暂存,并接着完成被打断的 push/migrate/sync)
30
+ abort 放弃被冲突打断的操作并切回原分支
31
+ undo 向导式撤销/回滚(revert/reset)
30
32
  rescue 找回恢复点和 reflog 里的提交
31
33
 
32
34
  协作与发布:
@@ -71,6 +73,7 @@ Examples:
71
73
  icode log --unpushed
72
74
  icode stash save -m "wip"
73
75
  icode clean --gone -y
76
+ icode continue
74
77
  icode undo --mode hard --ref HEAD~1
75
78
  icode rescue
76
79
  icode pr create --ai
@@ -59,7 +59,7 @@ Options:
59
59
 
60
60
  Notes:
61
61
  等同于 git commit --fixup=<commit> && git rebase -i --autosquash(不会打开编辑器)。
62
- 发生冲突时用 icode undo --recover continue|abort 处理。执行前会自动创建恢复点。
62
+ 发生冲突时用 icode continue / icode abort 处理。执行前会自动创建恢复点。
63
63
 
64
64
  Examples:
65
65
  icode fixup -a
@@ -18,7 +18,7 @@ Options:
18
18
  --mode <mode> 回滚模式: revert | soft | mixed | hard
19
19
  --ref <ref> 回滚目标,默认按 mode 自动给出
20
20
  --hash <hash> 按 commit hash 指定回滚目标(等同 --ref)
21
- --recover <action> 冲突恢复策略: continue | abort | keep
21
+ --recover <action> 冲突恢复策略: continue | abort | keep(推荐直接用 icode continue / icode abort)
22
22
  --json 输出机器可读 JSON,适合 AI agent 或脚本消费
23
23
  -y, --yes 自动确认(跳过确认提示)
24
24
  --repo-mode <mode> 仓库模式: auto(自动继承父仓库) | strict(禁止继承)
@@ -385,6 +385,8 @@ const ROOT_COMPLETION_SPEC = {
385
385
  { long: '--help', short: '-h' }
386
386
  ]
387
387
  },
388
+ continue: { options: [JSON_OPTION, REPO_MODE_OPTION, HELP_OPTION] },
389
+ abort: { options: BASIC_OPTIONS },
388
390
  sw: SW_NODE,
389
391
  switch: SW_NODE,
390
392
  stash: {
@@ -25,6 +25,11 @@ export async function withAutoStash(git, enabled, label, fn) {
25
25
  return await fn({ stashed: true, stashSha })
26
26
  } finally {
27
27
  const entry = (await git.stashList()).find((item) => item.hash === stashSha)
28
+ // 操作因冲突暂停时不要把改动倒回冲突现场,留在 stash 里等用户处理完再取回。
29
+ if (entry && (await git.getInProgressOperation())) {
30
+ logger.warn(`未提交的改动保存在 ${entry.ref},处理完冲突(icode continue)后执行 \`icode stash pop\` 取回。`)
31
+ return
32
+ }
28
33
  if (entry) {
29
34
  try {
30
35
  await git.stashApply(entry.ref, { pop: true })
@@ -0,0 +1,58 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ // 只认 <<<<<<< 和 >>>>>>>:单独的 ======= 在 Markdown 等文件里可能是正常内容。
5
+ const MARKER_PATTERN = /^(<{7}|>{7})(?: |$)/
6
+
7
+ /**
8
+ * 找出文件中残留的冲突标记所在行号(从 1 开始)。
9
+ * 输入为文件绝对路径,输出行号数组;文件不存在(被删除)时返回空数组。
10
+ */
11
+ export function findConflictMarkers(filePath) {
12
+ let content
13
+ try {
14
+ content = fs.readFileSync(filePath, 'utf8')
15
+ } catch {
16
+ return []
17
+ }
18
+ const lines = []
19
+ content.split('\n').forEach((line, index) => {
20
+ if (MARKER_PATTERN.test(line)) {
21
+ lines.push(index + 1)
22
+ }
23
+ })
24
+ return lines
25
+ }
26
+
27
+ /**
28
+ * 检查当前的冲突文件:已经没有冲突标记的视为已解决,还有标记的列出行号。
29
+ * 输入为 GitService,输出 { resolved: [path], unresolved: [{ file, lines }] }。
30
+ */
31
+ export async function inspectConflicts(git) {
32
+ const conflicted = await git.listConflictedFiles()
33
+ const resolved = []
34
+ const unresolved = []
35
+ for (const file of conflicted) {
36
+ const lines = findConflictMarkers(path.resolve(git.cwd, file))
37
+ if (lines.length) {
38
+ unresolved.push({ file, lines })
39
+ } else {
40
+ resolved.push(file)
41
+ }
42
+ }
43
+ return { resolved, unresolved }
44
+ }
45
+
46
+ /**
47
+ * 把已解决(没有冲突标记)的文件自动 git add,省去手动暂存。
48
+ * 被删除的冲突文件用 git rm 标记。输出已暂存的文件列表。
49
+ */
50
+ export async function stageResolvedConflicts(git, files) {
51
+ const staged = []
52
+ for (const file of files) {
53
+ const exists = fs.existsSync(path.resolve(git.cwd, file))
54
+ await git.exec(exists ? ['add', '--', file] : ['rm', '--quiet', '--', file])
55
+ staged.push(file)
56
+ }
57
+ return staged
58
+ }
@@ -0,0 +1,63 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ // 多步骤命令(push 合并多个分支、migrate、sync)被冲突打断时,把剩下的步骤记在这里,
5
+ // 解决冲突后 icode continue 读取它接着做完,icode abort 读取它切回原分支。
6
+ // 放在 gitDir(每个 worktree 独立)下,不会被提交。
7
+
8
+ const STATE_DIR = 'icode'
9
+ const STATE_FILE = 'operation.json'
10
+
11
+ function statePath(context) {
12
+ return path.resolve(context.gitDir, STATE_DIR, STATE_FILE)
13
+ }
14
+
15
+ /**
16
+ * 保存中断的操作。state 至少包含 command 与 originalBranch。
17
+ */
18
+ export function saveOperationState(context, state) {
19
+ const filePath = statePath(context)
20
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
21
+ fs.writeFileSync(filePath, `${JSON.stringify({ ...state, savedAt: new Date().toISOString() }, null, 2)}\n`, 'utf8')
22
+ return filePath
23
+ }
24
+
25
+ /**
26
+ * 读取中断的操作,没有或损坏时返回 null。
27
+ */
28
+ export function loadOperationState(context) {
29
+ const filePath = statePath(context)
30
+ if (!fs.existsSync(filePath)) {
31
+ return null
32
+ }
33
+ try {
34
+ const state = JSON.parse(fs.readFileSync(filePath, 'utf8'))
35
+ return state && typeof state === 'object' && state.command ? state : null
36
+ } catch {
37
+ return null
38
+ }
39
+ }
40
+
41
+ export function clearOperationState(context) {
42
+ fs.rmSync(statePath(context), { force: true })
43
+ }
44
+
45
+ /**
46
+ * 用一句话描述中断的操作,供 ready / 菜单 / continue 展示。
47
+ */
48
+ export function describeOperationState(state) {
49
+ if (!state) {
50
+ return ''
51
+ }
52
+ if (state.command === 'push') {
53
+ const pending = [state.pendingPush, ...(state.remainingTargets || [])].filter(Boolean)
54
+ return `push ${state.sourceBranch}${pending.length ? ` → ${pending.join(', ')}` : ''}`
55
+ }
56
+ if (state.command === 'migrate') {
57
+ return `migrate ${state.sourceBranch} → ${state.targetBranch}`
58
+ }
59
+ if (state.command === 'sync') {
60
+ return `sync ${[state.currentBranch, ...(state.remainingTargets || [])].filter(Boolean).join(', ')}`
61
+ }
62
+ return state.command
63
+ }
@@ -0,0 +1,32 @@
1
+ // 按 Git 操作类型执行 continue / abort,undo 与 continue/abort 命令共用。
2
+
3
+ export const OPERATION_LABELS = {
4
+ merge: '合并 (merge)',
5
+ rebase: '变基 (rebase)',
6
+ 'cherry-pick': '摘取提交 (cherry-pick)',
7
+ revert: '回滚 (revert)'
8
+ }
9
+
10
+ export async function continueOperation(git, operation) {
11
+ if (operation === 'revert') {
12
+ await git.revertContinue()
13
+ } else if (operation === 'merge') {
14
+ await git.mergeContinue()
15
+ } else if (operation === 'rebase') {
16
+ await git.rebaseContinue()
17
+ } else {
18
+ await git.cherryPickContinue()
19
+ }
20
+ }
21
+
22
+ export async function abortOperation(git, operation) {
23
+ if (operation === 'revert') {
24
+ await git.revertAbort()
25
+ } else if (operation === 'merge') {
26
+ await git.mergeAbort()
27
+ } else if (operation === 'rebase') {
28
+ await git.rebaseAbort()
29
+ } else {
30
+ await git.abortCherryPick()
31
+ }
32
+ }
@@ -2,7 +2,9 @@ import { c, setColorStream } from './theme.js'
2
2
 
3
3
  let verboseEnabled = process.env.ICODE_LOG_LEVEL === 'verbose'
4
4
  // JSON 输出模式下 stdout 只留给结构化结果,普通日志改走 stderr。
5
- let logsToStderr = false
5
+ // node --test 的子进程用 stdout 和主进程传递序列化数据,日志混在里面偶尔会导致解析失败,
6
+ // 所以在测试子进程中默认把日志写到 stderr。
7
+ let logsToStderr = Boolean(process.env.NODE_TEST_CONTEXT)
6
8
 
7
9
  function infoStream() {
8
10
  return logsToStderr ? process.stderr : process.stdout
@@ -0,0 +1,233 @@
1
+ import { IcodeError } from '../core/errors.js'
2
+ import { inspectConflicts, stageResolvedConflicts } from '../core/git/conflicts.js'
3
+ import { resolveGitContext } from '../core/git/context.js'
4
+ import { clearOperationState, describeOperationState, loadOperationState, saveOperationState } from '../core/git/operation-state.js'
5
+ import { abortOperation, continueOperation, OPERATION_LABELS } from '../core/git/operations.js'
6
+ import { GitService } from '../core/git/service.js'
7
+ import { logger } from '../core/tools/logger.js'
8
+ import { ui } from '../core/tools/theme.js'
9
+ import { mergeIntoTargetAndPush, pushCurrentBranch } from './push/local-merge-step.js'
10
+ import { runSyncBranches } from './sync-workflow.js'
11
+
12
+ async function openGit(options) {
13
+ const context = await resolveGitContext({ cwd: options.cwd, repoMode: options.repoMode })
14
+ return { context, git: new GitService(context) }
15
+ }
16
+
17
+ function formatUnresolved(unresolved) {
18
+ return unresolved
19
+ .map((item) => ` - ${item.file}(第 ${item.lines.slice(0, 5).join(', ')}${item.lines.length > 5 ? ' …' : ''} 行)`)
20
+ .join('\n')
21
+ }
22
+
23
+ /**
24
+ * 读取当前是否有需要继续/放弃的内容,供 ready / 主菜单提示。
25
+ * 输出 { operation, state, description } 或 null。
26
+ */
27
+ export async function inspectPausedWork(options = {}) {
28
+ const { context, git } = await openGit(options)
29
+ const operation = await git.getInProgressOperation()
30
+ const state = loadOperationState(context)
31
+ if (!operation && !state) {
32
+ return null
33
+ }
34
+ return {
35
+ operation,
36
+ state,
37
+ description: [operation ? OPERATION_LABELS[operation] || operation : '', describeOperationState(state)].filter(Boolean).join(' · ')
38
+ }
39
+ }
40
+
41
+ // 冲突解决后把 push 剩下的步骤做完:推送已合并的目标分支、处理剩余目标、切回原分支。
42
+ async function resumePush(git, context, state) {
43
+ const summary = [...(state.summary || [])]
44
+ const options = { ...(state.options || {}), yes: true }
45
+ const remaining = [...(state.remainingTargets || [])]
46
+
47
+ if (state.pendingPush) {
48
+ const remoteExists = await git.branchExistsRemote(state.pendingPush)
49
+ logger.info(`推送目标分支: ${ui.branch(state.pendingPush)}`)
50
+ await git.push(state.pendingPush, { setUpstream: !remoteExists, noVerify: options.noVerify })
51
+ summary.push({ branch: state.pendingPush, status: 'merged-and-pushed' })
52
+ }
53
+
54
+ for (let index = 0; index < remaining.length; index += 1) {
55
+ const branch = remaining[index]
56
+ try {
57
+ if (branch === state.sourceBranch) {
58
+ await git.checkout(state.sourceBranch)
59
+ summary.push(await pushCurrentBranch(git, branch, options, summary))
60
+ } else {
61
+ summary.push(await mergeIntoTargetAndPush(git, state.sourceBranch, branch, options, summary))
62
+ }
63
+ } catch (error) {
64
+ // 又遇到冲突:更新进度,等下一次 icode continue。
65
+ if (error?.code === 'PUSH_LOCAL_MERGE_PAUSED' && await git.getInProgressOperation()) {
66
+ const mergedTarget = error.meta?.step === 'merge-target'
67
+ saveOperationState(context, {
68
+ ...state,
69
+ pendingPush: mergedTarget ? branch : null,
70
+ remainingTargets: remaining.slice(mergedTarget ? index + 1 : index),
71
+ summary
72
+ })
73
+ }
74
+ throw error
75
+ }
76
+ }
77
+
78
+ return summary
79
+ }
80
+
81
+ async function resumeMigrate(git, state) {
82
+ const summary = []
83
+ if (state.push) {
84
+ logger.info(`推送目标分支: ${ui.branch(state.targetBranch)}`)
85
+ await git.push(state.targetBranch, { setUpstream: !state.targetExistsRemote, noVerify: state.options?.noVerify })
86
+ summary.push({ branch: state.targetBranch, status: 'pushed' })
87
+ }
88
+ return summary
89
+ }
90
+
91
+ async function resumeSync(git, context, state) {
92
+ const targets = [state.currentBranch, ...(state.remainingTargets || [])].filter(Boolean)
93
+ // runSyncBranches 结束时会自己切回原分支,并在再次冲突时更新进度。
94
+ clearOperationState(context)
95
+ const summary = await runSyncBranches(git, context, targets, state.options || {}, state.originalBranch, {
96
+ branch: state.currentBranch,
97
+ startAt: state.startAt || 'push'
98
+ })
99
+ return [...(state.summary || []), ...summary]
100
+ }
101
+
102
+ async function returnToOriginalBranch(git, state) {
103
+ const original = state?.originalBranch
104
+ if (!original) {
105
+ return ''
106
+ }
107
+ const current = await git.getCurrentBranch()
108
+ if (current !== original) {
109
+ await git.checkout(original)
110
+ }
111
+ return original
112
+ }
113
+
114
+ /**
115
+ * icode continue:检查冲突是否都已解决 -> 自动暂存 -> 继续 Git 操作 -> 接着完成 push/migrate/sync 剩下的步骤。
116
+ * 输出 { operation, staged, resumed, summary, returnedTo }。
117
+ */
118
+ export async function runContinueWorkflow(options = {}) {
119
+ const { context, git } = await openGit(options)
120
+ const operation = await git.getInProgressOperation()
121
+ const state = loadOperationState(context)
122
+
123
+ if (!operation && !state) {
124
+ throw new IcodeError('没有需要继续的操作。', { code: 'CONTINUE_NOTHING', exitCode: 2 })
125
+ }
126
+
127
+ let staged = []
128
+ if (operation) {
129
+ const { resolved, unresolved } = await inspectConflicts(git)
130
+ if (unresolved.length) {
131
+ throw new IcodeError(
132
+ `还有 ${unresolved.length} 个文件存在冲突标记,请先处理:\n${formatUnresolved(unresolved)}\n处理完再执行 icode continue;想放弃可执行 icode abort。`,
133
+ { code: 'CONTINUE_UNRESOLVED_CONFLICTS', exitCode: 2, meta: { unresolved } }
134
+ )
135
+ }
136
+ staged = await stageResolvedConflicts(git, resolved)
137
+ if (staged.length && !options.silentLog) {
138
+ logger.info(`已自动暂存 ${staged.length} 个已解决的文件: ${staged.map(ui.path).join(' ')}`)
139
+ }
140
+
141
+ try {
142
+ await continueOperation(git, operation)
143
+ } catch (error) {
144
+ // rebase / cherry-pick 的下一个提交可能又冲突。
145
+ const still = await git.getInProgressOperation()
146
+ const next = still ? await inspectConflicts(git) : { unresolved: [] }
147
+ if (next.unresolved.length) {
148
+ throw new IcodeError(
149
+ `下一个提交又出现冲突:\n${formatUnresolved(next.unresolved)}\n处理完再执行 icode continue。`,
150
+ { code: 'CONTINUE_NEXT_CONFLICT', exitCode: 2, meta: { unresolved: next.unresolved }, cause: error }
151
+ )
152
+ }
153
+ throw new IcodeError(`继续 ${operation} 失败:${error.message.split('\n').slice(0, 4).join('\n')}`, {
154
+ code: 'CONTINUE_FAILED',
155
+ exitCode: 2,
156
+ cause: error
157
+ })
158
+ }
159
+ }
160
+
161
+ if (operation && !options.silentLog) {
162
+ logger.success(`${OPERATION_LABELS[operation] || operation} 已完成`)
163
+ }
164
+
165
+ let summary = []
166
+ if (state) {
167
+ if (state.command === 'push') {
168
+ summary = await resumePush(git, context, state)
169
+ } else if (state.command === 'migrate') {
170
+ summary = await resumeMigrate(git, state)
171
+ } else if (state.command === 'sync') {
172
+ summary = await resumeSync(git, context, state)
173
+ }
174
+ }
175
+
176
+ // 进度只在全部完成后清除;中途再次冲突时上面已经抛错并更新了进度。
177
+ let returnedTo = ''
178
+ if (state) {
179
+ if (state.command !== 'sync') {
180
+ returnedTo = await returnToOriginalBranch(git, state)
181
+ } else {
182
+ returnedTo = state.originalBranch || ''
183
+ }
184
+ clearOperationState(context)
185
+ }
186
+
187
+ return {
188
+ operation,
189
+ staged,
190
+ resumed: state ? state.command : '',
191
+ description: describeOperationState(state),
192
+ summary,
193
+ returnedTo,
194
+ repoRoot: context.topLevelPath
195
+ }
196
+ }
197
+
198
+ /**
199
+ * icode abort:中止进行中的 Git 操作,清除中断进度并切回原分支。
200
+ * 已经推送成功的分支不会被撤回。
201
+ */
202
+ export async function runAbortWorkflow(options = {}) {
203
+ const { context, git } = await openGit(options)
204
+ const operation = await git.getInProgressOperation()
205
+ const state = loadOperationState(context)
206
+
207
+ if (!operation && !state) {
208
+ throw new IcodeError('没有需要放弃的操作。', { code: 'ABORT_NOTHING', exitCode: 2 })
209
+ }
210
+
211
+ if (operation) {
212
+ await abortOperation(git, operation)
213
+ }
214
+
215
+ let returnedTo = ''
216
+ if (state) {
217
+ try {
218
+ returnedTo = await returnToOriginalBranch(git, state)
219
+ } catch (error) {
220
+ logger.warn(`未能切回 ${state.originalBranch}: ${error.message.split('\n')[0]}`)
221
+ }
222
+ clearOperationState(context)
223
+ }
224
+
225
+ return {
226
+ operation,
227
+ aborted: state ? state.command : '',
228
+ description: describeOperationState(state),
229
+ done: (state?.summary || []).filter((item) => /pushed/.test(item.status)).map((item) => item.branch),
230
+ returnedTo,
231
+ repoRoot: context.topLevelPath
232
+ }
233
+ }
@@ -193,8 +193,8 @@ export async function runFixupWorkflow(options = {}) {
193
193
  await git.rebaseOnto(parentSha, { autosquash: true, autostash: true, root: !parentSha })
194
194
  } catch (error) {
195
195
  throw new IcodeError(
196
- 'fixup 自动整理提交时发生冲突。解决冲突并 git add 后执行 `icode undo --recover continue`,' +
197
- `或执行 \`icode undo --recover abort\` 放弃(之前的状态在恢复点 ${backup.id})。`,
196
+ 'fixup 自动整理提交时发生冲突。解决冲突后执行 `icode continue`,' +
197
+ `或执行 \`icode abort\` 放弃(之前的状态在恢复点 ${backup.id})。`,
198
198
  { code: 'FIXUP_REBASE_CONFLICT', exitCode: 2, cause: error, meta: error.meta }
199
199
  )
200
200
  }
@@ -1,6 +1,7 @@
1
1
  import { IcodeError } from '../core/errors.js'
2
2
  import { ui, shortenHome } from '../core/tools/theme.js'
3
3
  import { createBackup } from '../core/git/backup.js'
4
+ import { saveOperationState } from '../core/git/operation-state.js'
4
5
  import { resolveGitContext } from '../core/git/context.js'
5
6
  import { GitService } from '../core/git/service.js'
6
7
  import { chooseMany, chooseOne, isInteractiveTerminal } from '../core/tools/interactive.js'
@@ -628,8 +629,19 @@ export async function runMigrateWorkflow(options) {
628
629
  try {
629
630
  await git.cherryPick(commits)
630
631
  } catch (error) {
632
+ if (await git.getInProgressOperation()) {
633
+ saveOperationState(context, {
634
+ command: 'migrate',
635
+ sourceBranch,
636
+ targetBranch,
637
+ originalBranch,
638
+ push: Boolean(options.push),
639
+ targetExistsRemote,
640
+ options: { noVerify: Boolean(options.noVerify) }
641
+ })
642
+ }
631
643
  throw new IcodeError(
632
- '迁移失败: cherry-pick 发生冲突。请先解决冲突后执行 `icode undo --recover continue`,或执行 `icode undo --recover abort` 回滚。',
644
+ `迁移暂停: cherry-pick 发生冲突,已停在 ${targetBranch}。解决冲突后执行 \`icode continue\` 接着迁移剩下的提交${options.push ? '并推送' : ''},或执行 \`icode abort\` 放弃并切回原分支。`,
633
645
  {
634
646
  code: 'MIGRATE_CHERRY_PICK_FAILED',
635
647
  cause: error,
@@ -657,7 +669,8 @@ export async function runMigrateWorkflow(options) {
657
669
  repoRoot: context.topLevelPath
658
670
  }
659
671
  } finally {
660
- if (originalBranch && originalBranch !== targetBranch) {
672
+ // 冲突暂停时留在目标分支上,方便直接解决冲突。
673
+ if (originalBranch && originalBranch !== targetBranch && !(await git.getInProgressOperation())) {
661
674
  try {
662
675
  await git.checkout(originalBranch)
663
676
  } catch (error) {
@@ -33,7 +33,7 @@ export function buildLocalMergePauseError({ sourceBranch, targetBranch, step, er
33
33
  const conflict = isGitConflictFailure(error)
34
34
  const prefix = conflict ? '本地合并冲突' : '本地合并失败'
35
35
  const nextStep = conflict
36
- ? '请解决冲突后执行 `icode undo --recover continue`,或执行 `icode undo --recover abort` 中止本次合并。'
36
+ ? '解决冲突后执行 `icode continue`(会自动暂存已解决的文件,并接着完成剩下的推送),或执行 `icode abort` 放弃并切回原分支。'
37
37
  : '请根据上面的 Git 错误修复后重试。'
38
38
 
39
39
  return new IcodeError(`${prefix}(${step}): ${sourceBranch} -> ${targetBranch}\n${reason}\n${nextStep}`, {
@@ -2,6 +2,7 @@ import { getRepoPolicy } from '../core/config/store.js'
2
2
  import { ui, shortenHome } from '../core/tools/theme.js'
3
3
  import { IcodeError } from '../core/errors.js'
4
4
  import { createBackup } from '../core/git/backup.js'
5
+ import { saveOperationState } from '../core/git/operation-state.js'
5
6
  import { resolveGitContext } from '../core/git/context.js'
6
7
  import {
7
8
  assertNoConflictedFiles,
@@ -68,6 +69,28 @@ function validateForcePush(inputOptions, branchTargets, currentBranch, context,
68
69
  }
69
70
  }
70
71
 
72
+ // 去掉会被跳过的受保护分支,只保留续做时真正要处理的分支。
73
+ function filterRunnable(branches, protectedBranches, inputOptions) {
74
+ return branches.filter((branch) => !(protectedBranches.has(branch) && !inputOptions.forceProtected))
75
+ }
76
+
77
+ // 保存 push 中断时的进度:pendingPush 是已合并但还没推送的分支,remainingTargets 是还没处理的分支。
78
+ function savePushState(context, inputOptions, sourceBranch, pendingPush, remainingTargets, summary) {
79
+ saveOperationState(context, {
80
+ command: 'push',
81
+ sourceBranch,
82
+ originalBranch: sourceBranch,
83
+ pendingPush,
84
+ remainingTargets,
85
+ options: {
86
+ noVerify: Boolean(inputOptions.noVerify),
87
+ createTarget: Boolean(inputOptions.createTarget),
88
+ yes: true
89
+ },
90
+ summary: summary.slice()
91
+ })
92
+ }
93
+
71
94
  /**
72
95
  * 交互选择推送目标:只推当前分支(默认)/ 同时本地合并到其他分支 / 通过 PR 合并。
73
96
  * 输出 { targets, remoteMerge } 或 null(取消)。
@@ -215,13 +238,17 @@ export async function runPushWorkflow(inputOptions) {
215
238
  noRebase: true
216
239
  })
217
240
  } catch (error) {
218
- throw buildLocalMergePauseError({
241
+ const pauseError = buildLocalMergePauseError({
219
242
  sourceBranch: context.defaultBranch,
220
243
  targetBranch: currentBranch,
221
244
  step: 'pull-main',
222
245
  error,
223
246
  summary
224
247
  })
248
+ if (await git.getInProgressOperation()) {
249
+ savePushState(context, inputOptions, currentBranch, null, filterRunnable(branchTargets, protectedBranches, inputOptions), summary)
250
+ }
251
+ throw pauseError
225
252
  }
226
253
  }
227
254
 
@@ -282,24 +309,32 @@ export async function runPushWorkflow(inputOptions) {
282
309
  const branchOptions = { ...inputOptions, expectedRemoteSha }
283
310
 
284
311
  try {
285
- for (const targetBranch of branchTargets) {
312
+ for (let index = 0; index < branchTargets.length; index += 1) {
313
+ const targetBranch = branchTargets[index]
286
314
  if (protectedBranches.has(targetBranch) && !inputOptions.forceProtected) {
287
315
  logger.warn(`跳过受保护分支: ${ui.branch(targetBranch)}(可用 --force-protected 覆盖)`)
288
316
  summary.push({ branch: targetBranch, status: 'skipped-protected' })
289
317
  continue
290
318
  }
291
319
 
292
- if (targetBranch === currentBranch) {
293
- summary.push(await pushCurrentBranch(git, targetBranch, branchOptions, summary))
294
- continue
295
- }
296
-
297
- if (inputOptions.remoteMerge) {
320
+ if (inputOptions.remoteMerge && targetBranch !== currentBranch) {
298
321
  remoteMergeTargets.push(targetBranch)
299
322
  continue
300
323
  }
301
324
 
302
- summary.push(await mergeIntoTargetAndPush(git, currentBranch, targetBranch, branchOptions, summary))
325
+ try {
326
+ summary.push(targetBranch === currentBranch
327
+ ? await pushCurrentBranch(git, targetBranch, branchOptions, summary)
328
+ : await mergeIntoTargetAndPush(git, currentBranch, targetBranch, branchOptions, summary))
329
+ } catch (error) {
330
+ // 冲突暂停:记下剩余步骤,icode continue 会推送当前目标并处理剩下的分支。
331
+ if (error?.code === 'PUSH_LOCAL_MERGE_PAUSED' && await git.getInProgressOperation()) {
332
+ const mergedTarget = error.meta?.step === 'merge-target'
333
+ const rest = filterRunnable(branchTargets.slice(mergedTarget ? index + 1 : index), protectedBranches, inputOptions)
334
+ savePushState(context, inputOptions, currentBranch, mergedTarget ? targetBranch : null, rest, summary)
335
+ }
336
+ throw error
337
+ }
303
338
  }
304
339
 
305
340
  if (inputOptions.remoteMerge && remoteMergeTargets.length) {
@@ -330,7 +365,7 @@ export async function runPushWorkflow(inputOptions) {
330
365
  const operationAfterWorkflow = await git.getInProgressOperation()
331
366
  if (operationAfterWorkflow === 'merge') {
332
367
  logger.warn(`检测到未完成的 merge,已保留在当前分支 ${branchAfterWorkflow || '(unknown)'}。`)
333
- logger.warn('解决冲突后执行 `icode undo --recover continue`,或执行 `icode undo --recover abort` 中止。')
368
+ logger.warn('解决冲突后执行 `icode continue` 接着完成剩下的步骤,或执行 `icode abort` 放弃并切回原分支。')
334
369
  } else if (branchAfterWorkflow && branchAfterWorkflow !== originalBranch) {
335
370
  try {
336
371
  await git.checkout(originalBranch)
@@ -1,4 +1,5 @@
1
1
  import { resolveRemoteMergePlatform } from './push/remote-merge-step.js'
2
+ import { describeOperationState, loadOperationState } from '../core/git/operation-state.js'
2
3
  import { getAiConfig } from '../core/ai/config.js'
3
4
  import { getRepoPolicy } from '../core/config/store.js'
4
5
  import { resolveGitContext } from '../core/git/context.js'
@@ -95,17 +96,19 @@ function checkCurrentBranch(currentBranch) {
95
96
  * 检查是否存在未完成的 Git 操作。
96
97
  * 输入为 operation 字符串,输出 ready 检查项。
97
98
  */
98
- function checkInProgressOperation(operation) {
99
- if (!operation) {
99
+ function checkInProgressOperation(operation, pausedState) {
100
+ const paused = describeOperationState(pausedState)
101
+ if (!operation && !pausedState) {
100
102
  return buildCheck('git.operation', 'pass', '没有未完成的 Git 操作', '工作区未处于 merge/rebase/cherry-pick/revert 流程中。')
101
103
  }
102
104
 
105
+ const detail = [operation ? `当前操作: ${operation}` : '', paused ? `被中断的 icode 命令: ${paused}` : ''].filter(Boolean).join(';')
103
106
  return buildCheck(
104
107
  'git.operation',
105
108
  'fail',
106
- '存在未完成的 Git 操作',
107
- `当前操作: ${operation}`,
108
- `请先完成或中止 ${operation},再继续执行 save/push。`
109
+ '有被中断的操作',
110
+ detail,
111
+ '解决冲突后执行 icode continue 接着完成;或执行 icode abort 放弃并切回原分支。'
109
112
  )
110
113
  }
111
114
 
@@ -249,7 +252,7 @@ export async function runReadyWorkflow(options = {}) {
249
252
 
250
253
  const checks = [
251
254
  checkCurrentBranch(currentBranch),
252
- checkInProgressOperation(operation),
255
+ checkInProgressOperation(operation, loadOperationState(context)),
253
256
  checkChanges(changes),
254
257
  checkUpstream(upstream),
255
258
  checkAiProfile(aiConfig),
@@ -2,6 +2,7 @@ import { IcodeError } from '../core/errors.js'
2
2
  import { ui, shortenHome } from '../core/tools/theme.js'
3
3
  import { withAutoStash } from '../core/git/autostash.js'
4
4
  import { createBackup } from '../core/git/backup.js'
5
+ import { saveOperationState } from '../core/git/operation-state.js'
5
6
  import { resolveGitContext } from '../core/git/context.js'
6
7
  import {
7
8
  assertCleanWorktree,
@@ -136,7 +137,7 @@ export async function runSyncWorkflow(options) {
136
137
  })
137
138
 
138
139
  const summary = await withAutoStash(git, options.autostash === true, `sync ${targets.join(',')}`, () =>
139
- syncBranches(git, context, targets, options, originalBranch)
140
+ runSyncBranches(git, context, targets, options, originalBranch)
140
141
  )
141
142
 
142
143
  return {
@@ -148,51 +149,101 @@ export async function runSyncWorkflow(options) {
148
149
  }
149
150
  }
150
151
 
151
- async function syncBranches(git, context, targets, options, originalBranch) {
152
- const summary = []
152
+ // 单个分支的同步步骤,冲突中断后可以从 startAt 指定的步骤接着做。
153
+ const SYNC_STEPS = ['checkout', 'pull', 'merge-main', 'push']
154
+
155
+ async function syncOneBranch(git, context, branchName, options, startAt = 'checkout') {
156
+ const from = SYNC_STEPS.indexOf(startAt)
157
+ let step = startAt
153
158
  try {
154
- for (const branchName of targets) {
155
- logger.info(`同步分支: ${ui.branch(branchName)}`)
159
+ let remoteExists = await git.branchExistsRemote(branchName)
160
+ if (from <= 0) {
156
161
  const setup = await ensureBranchReady(git, branchName)
157
-
158
162
  if (!setup.available) {
159
163
  logger.warn(`分支不存在(本地+远程): ${ui.branch(branchName)}`)
160
- summary.push({
161
- branch: branchName,
162
- status: 'missing'
163
- })
164
- continue
164
+ return { branch: branchName, status: 'missing' }
165
165
  }
166
+ remoteExists = setup.remoteExists
167
+ }
166
168
 
167
- if (setup.remoteExists) {
168
- await git.pull(branchName, {
169
- noRebase: !options.rebase,
170
- rebase: options.rebase
171
- })
172
- }
169
+ if (from <= 1 && remoteExists) {
170
+ step = 'pull'
171
+ await git.pull(branchName, {
172
+ noRebase: !options.rebase,
173
+ rebase: options.rebase
174
+ })
175
+ }
173
176
 
174
- if (options.mergeMain && branchName !== context.defaultBranch) {
175
- // 把最新主分支合入目标分支,降低后续提测/发布时的冲突概率。
176
- await git.merge(context.defaultBranch, {
177
- noFf: true,
178
- noEdit: true
179
- })
180
- }
177
+ if (from <= 2 && options.mergeMain && branchName !== context.defaultBranch) {
178
+ step = 'merge-main'
179
+ // 把最新主分支合入目标分支,降低后续提测/发布时的冲突概率。
180
+ await git.merge(context.defaultBranch, {
181
+ noFf: true,
182
+ noEdit: true
183
+ })
184
+ }
181
185
 
182
- if (options.push) {
183
- await git.push(branchName, {
184
- setUpstream: !setup.remoteExists,
185
- noVerify: options.noVerify
186
+ if (options.push) {
187
+ step = 'push'
188
+ await git.push(branchName, {
189
+ setUpstream: !remoteExists,
190
+ noVerify: options.noVerify
191
+ })
192
+ }
193
+ } catch (error) {
194
+ error.syncStep = step
195
+ throw error
196
+ }
197
+
198
+ return {
199
+ branch: branchName,
200
+ status: options.push ? 'synced-and-pushed' : 'synced'
201
+ }
202
+ }
203
+
204
+ /**
205
+ * 依次同步多个分支。某个分支发生冲突时保存进度(icode continue 接着做),并留在冲突分支上。
206
+ * resume = { branch, startAt } 表示从某个分支的某一步继续。
207
+ */
208
+ export async function runSyncBranches(git, context, targets, options, originalBranch, resume = null) {
209
+ const summary = []
210
+ let paused = false
211
+ try {
212
+ for (let index = 0; index < targets.length; index += 1) {
213
+ const branchName = targets[index]
214
+ const startAt = resume && resume.branch === branchName ? resume.startAt : 'checkout'
215
+ logger.info(`同步分支: ${ui.branch(branchName)}`)
216
+ try {
217
+ summary.push(await syncOneBranch(git, context, branchName, options, startAt))
218
+ } catch (error) {
219
+ if (!(await git.getInProgressOperation())) {
220
+ throw error
221
+ }
222
+ // 冲突:记录剩余步骤,下一步从冲突步骤之后开始。
223
+ const nextStep = SYNC_STEPS[SYNC_STEPS.indexOf(error.syncStep || 'pull') + 1] || 'push'
224
+ saveOperationState(context, {
225
+ command: 'sync',
226
+ originalBranch,
227
+ currentBranch: branchName,
228
+ startAt: nextStep,
229
+ remainingTargets: targets.slice(index + 1),
230
+ options: {
231
+ push: Boolean(options.push),
232
+ mergeMain: Boolean(options.mergeMain),
233
+ rebase: Boolean(options.rebase),
234
+ noVerify: Boolean(options.noVerify)
235
+ },
236
+ summary
186
237
  })
238
+ paused = true
239
+ throw new IcodeError(
240
+ `同步 ${branchName} 时发生冲突,已停在该分支。\n解决冲突后执行 \`icode continue\` 接着同步剩下的分支,或执行 \`icode abort\` 放弃并切回 ${originalBranch}。`,
241
+ { code: 'SYNC_CONFLICT_PAUSED', exitCode: 2, cause: error, meta: { branch: branchName, summary } }
242
+ )
187
243
  }
188
-
189
- summary.push({
190
- branch: branchName,
191
- status: options.push ? 'synced-and-pushed' : 'synced'
192
- })
193
244
  }
194
245
  } finally {
195
- if (originalBranch) {
246
+ if (originalBranch && !paused) {
196
247
  try {
197
248
  await git.checkout(originalBranch)
198
249
  } catch (error) {
@@ -1,6 +1,7 @@
1
1
  import { IcodeError } from '../core/errors.js'
2
2
  import { shortenHome } from '../core/tools/theme.js'
3
3
  import { createBackup } from '../core/git/backup.js'
4
+ import { runAbortWorkflow, runContinueWorkflow } from './continue-workflow.js'
4
5
  import { resolveGitContext } from '../core/git/context.js'
5
6
  import { GitService } from '../core/git/service.js'
6
7
  import { chooseOne, confirm } from '../core/tools/interactive.js'
@@ -82,7 +83,7 @@ async function performRollback(options) {
82
83
  } catch (error) {
83
84
  if (isRevertConflict(error)) {
84
85
  throw new IcodeError(
85
- 'revert 发生冲突。请解决冲突后执行 `icode undo --recover continue`,或使用 `icode undo --recover abort` 直接中止。',
86
+ 'revert 发生冲突。解决冲突后执行 `icode continue`,或执行 `icode abort` 直接中止。',
86
87
  {
87
88
  code: 'REVERT_CONFLICT',
88
89
  cause: error,
@@ -140,30 +141,7 @@ async function performRollback(options) {
140
141
  }
141
142
  }
142
143
 
143
- // 按操作类型调用对应的 continue / abort。
144
- async function continueOperation(git, operation) {
145
- if (operation === 'revert') {
146
- await git.revertContinue()
147
- } else if (operation === 'merge') {
148
- await git.mergeContinue()
149
- } else if (operation === 'rebase') {
150
- await git.rebaseContinue()
151
- } else {
152
- await git.cherryPickContinue()
153
- }
154
- }
155
144
 
156
- async function abortOperation(git, operation) {
157
- if (operation === 'revert') {
158
- await git.revertAbort()
159
- } else if (operation === 'merge') {
160
- await git.mergeAbort()
161
- } else if (operation === 'rebase') {
162
- await git.rebaseAbort()
163
- } else {
164
- await git.abortCherryPick()
165
- }
166
- }
167
145
 
168
146
  // 解析交互菜单选择,输入菜单 value,输出 mode/ref 或 canceled。
169
147
  function parseSelection(selection) {
@@ -295,31 +273,21 @@ async function resolvePendingOperation(options) {
295
273
  }
296
274
  }
297
275
 
276
+ // 与 icode continue / icode abort 走同一套流程:自动暂存已解决文件,并续做被打断的 push/migrate/sync。
298
277
  if (action === 'continue') {
299
- try {
300
- await continueOperation(git, pending)
301
-
302
- return {
303
- resolvedOperation: pending,
304
- recoverAction: 'continue'
305
- }
306
- } catch (error) {
307
- throw new IcodeError(
308
- `${pending} --continue 失败,请先解决冲突后重试,或使用 --recover abort。`,
309
- {
310
- code: 'UNDO_CONTINUE_FAILED',
311
- cause: error,
312
- meta: error.meta
313
- }
314
- )
278
+ const result = await runContinueWorkflow({ cwd: options.cwd, repoMode: options.repoMode })
279
+ return {
280
+ resolvedOperation: pending,
281
+ recoverAction: 'continue',
282
+ ...(result.resumed ? { resumed: result.resumed, summary: result.summary } : {})
315
283
  }
316
284
  }
317
285
 
318
- await abortOperation(git, pending)
319
-
286
+ const result = await runAbortWorkflow({ cwd: options.cwd, repoMode: options.repoMode })
320
287
  return {
321
288
  resolvedOperation: pending,
322
- recoverAction: 'abort'
289
+ recoverAction: 'abort',
290
+ ...(result.aborted ? { aborted: result.aborted, returnedTo: result.returnedTo } : {})
323
291
  }
324
292
  }
325
293