@icode-js/icode 3.2.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +287 -24
- package/bin/icode.js +10 -0
- package/package.json +3 -3
- package/src/cli.js +175 -8
- package/src/commands/checkout.js +73 -6
- package/src/commands/clean.js +47 -7
- package/src/commands/config-ux.js +292 -0
- package/src/commands/config.js +199 -48
- package/src/commands/continue.js +104 -0
- package/src/commands/doctor.js +15 -29
- package/src/commands/help.js +54 -34
- package/src/commands/history.js +147 -0
- package/src/commands/info.js +39 -4
- package/src/commands/log.js +142 -0
- package/src/commands/migrate.js +19 -12
- package/src/commands/open.js +138 -0
- package/src/commands/pr.js +219 -0
- package/src/commands/push.js +62 -22
- package/src/commands/ready.js +18 -48
- package/src/commands/release.js +112 -0
- package/src/commands/rescue.js +164 -0
- package/src/commands/save.js +52 -11
- package/src/commands/stash.js +210 -0
- package/src/commands/sw.js +89 -0
- package/src/commands/sync.js +19 -2
- package/src/commands/tag.js +40 -1
- package/src/commands/undo.js +9 -3
- package/src/core/ai/diff-range.js +1 -1
- package/src/core/cli/completion.js +237 -1
- package/src/core/cli/output.js +46 -0
- package/src/core/cli/render.js +121 -0
- package/src/core/commands/shell.js +9 -1
- package/src/core/config/repo-config.js +93 -0
- package/src/core/config/store.js +60 -10
- package/src/core/git/autostash.js +46 -0
- package/src/core/git/backup.js +170 -0
- package/src/core/git/conflicts.js +58 -0
- package/src/core/git/context.js +72 -5
- package/src/core/git/file-guard.js +193 -0
- package/src/core/git/operation-state.js +63 -0
- package/src/core/git/operations.js +32 -0
- package/src/core/git/semver.js +194 -0
- package/src/core/git/service.js +454 -24
- package/src/core/platform/http.js +53 -0
- package/src/core/platform/index.js +165 -0
- package/src/core/platform/providers.js +454 -0
- package/src/core/platform/remote-url.js +92 -0
- package/src/core/remote-merge/client.js +33 -3
- package/src/core/tools/browser.js +28 -0
- package/src/core/tools/commit-prompt.js +137 -0
- package/src/core/tools/interactive.js +272 -40
- package/src/core/tools/logger.js +33 -23
- package/src/core/tools/theme.js +228 -0
- package/src/workflows/ai-commit-workflow.js +52 -20
- package/src/workflows/checkout-workflow.js +24 -10
- package/src/workflows/clean-workflow.js +135 -83
- package/src/workflows/continue-workflow.js +233 -0
- package/src/workflows/doctor-workflow.js +10 -7
- package/src/workflows/history-workflow.js +211 -0
- package/src/workflows/migrate-workflow.js +39 -14
- package/src/workflows/pr-workflow.js +252 -0
- package/src/workflows/push/commit-step.js +123 -0
- package/src/workflows/push/local-merge-step.js +213 -0
- package/src/workflows/push/plan.js +63 -0
- package/src/workflows/push/remote-merge-step.js +224 -0
- package/src/workflows/push-workflow.js +185 -492
- package/src/workflows/ready-workflow.js +14 -10
- package/src/workflows/release-workflow.js +189 -0
- package/src/workflows/rescue-workflow.js +137 -0
- package/src/workflows/save-workflow.js +106 -41
- package/src/workflows/switch-workflow.js +171 -0
- package/src/workflows/sync-workflow.js +143 -44
- package/src/workflows/tag-workflow.js +103 -17
- package/src/workflows/undo-workflow.js +113 -34
- package/src/workflows/rollback-workflow.js +0 -99
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { getRepoPolicy } from '../core/config/store.js'
|
|
2
|
+
import { ui, shortenHome } from '../core/tools/theme.js'
|
|
2
3
|
import { IcodeError } from '../core/errors.js'
|
|
4
|
+
import { createBackup } from '../core/git/backup.js'
|
|
3
5
|
import { resolveGitContext } from '../core/git/context.js'
|
|
4
6
|
import {
|
|
5
|
-
assertCleanWorktree,
|
|
6
|
-
assertNoConflictedFiles,
|
|
7
7
|
assertNoPendingOperation,
|
|
8
8
|
assertOnBranch,
|
|
9
9
|
readGitSafetyState
|
|
10
10
|
} from '../core/git/preconditions.js'
|
|
11
11
|
import { GitService } from '../core/git/service.js'
|
|
12
|
-
import { confirm } from '../core/tools/interactive.js'
|
|
12
|
+
import { chooseMany, confirm, isInteractiveTerminal } from '../core/tools/interactive.js'
|
|
13
13
|
import { logger } from '../core/tools/logger.js'
|
|
14
14
|
|
|
15
15
|
function normalizeKeepList(values = []) {
|
|
@@ -21,14 +21,28 @@ function normalizeKeepList(values = []) {
|
|
|
21
21
|
)
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function parseStaleDays(value) {
|
|
25
|
+
if (value == null || value === '') {
|
|
26
|
+
return 0
|
|
27
|
+
}
|
|
28
|
+
const days = Number(value)
|
|
29
|
+
if (!Number.isFinite(days) || days <= 0) {
|
|
30
|
+
throw new IcodeError('--stale 需要正数天数,例如 --stale 90', { code: 'CLEAN_STALE_INVALID', exitCode: 2 })
|
|
31
|
+
}
|
|
32
|
+
return days
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
/**
|
|
25
36
|
* 执行 clean 工作流。
|
|
26
|
-
*
|
|
37
|
+
* 清理范围:已合并(默认)、--gone(远程分支已删除)、--stale <天>(超过 N 天没有提交)。
|
|
38
|
+
* 不切换分支、不要求工作区干净;删除前会为所有待删分支创建恢复点。
|
|
39
|
+
* 输出 { candidates: [{ branch, reasons }], deletedLocal, deletedRemote, backupId }。
|
|
27
40
|
*/
|
|
28
41
|
export async function runCleanWorkflow(options) {
|
|
29
42
|
const context = await resolveGitContext({
|
|
30
43
|
cwd: options.cwd,
|
|
31
|
-
repoMode: options.repoMode
|
|
44
|
+
repoMode: options.repoMode,
|
|
45
|
+
remote: options.remote
|
|
32
46
|
})
|
|
33
47
|
|
|
34
48
|
const git = new GitService(context)
|
|
@@ -38,111 +52,149 @@ export async function runCleanWorkflow(options) {
|
|
|
38
52
|
const policy = getRepoPolicy(context.topLevelPath)
|
|
39
53
|
assertOnBranch(currentBranch, 'clean', 'CLEAN_DETACHED_HEAD')
|
|
40
54
|
assertNoPendingOperation(safetyState.operation, 'clean', 'CLEAN_GIT_OPERATION_IN_PROGRESS')
|
|
41
|
-
assertNoConflictedFiles(safetyState.changes, 'clean', 'CLEAN_CONFLICTED_FILES')
|
|
42
|
-
assertCleanWorktree(safetyState.changes, 'clean', 'CLEAN_DIRTY_WORKTREE')
|
|
43
55
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
const staleDays = parseStaleDays(options.stale)
|
|
57
|
+
const selectGone = options.gone === true
|
|
58
|
+
// 只传 --gone/--stale 时只清理这些;同时传 --merged 时三者合并。
|
|
59
|
+
const selectMerged = options.merged === true || (!selectGone && !staleDays)
|
|
60
|
+
|
|
61
|
+
if (!options.json) {
|
|
62
|
+
logger.hint(`仓库 ${shortenHome(context.topLevelPath)}`)
|
|
47
63
|
}
|
|
48
64
|
|
|
49
65
|
await git.fetch()
|
|
50
66
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
if (targetLocalExists) {
|
|
62
|
-
await git.checkout(mergedTarget)
|
|
67
|
+
// 优先用远程的基线分支判断,避免本地主分支落后导致漏判。
|
|
68
|
+
let mergedRef = ''
|
|
69
|
+
if (selectMerged) {
|
|
70
|
+
const remoteTarget = git.remoteRef(mergedTarget)
|
|
71
|
+
if (await git.refExists(remoteTarget)) {
|
|
72
|
+
mergedRef = remoteTarget
|
|
73
|
+
} else if (await git.branchExistsLocal(mergedTarget)) {
|
|
74
|
+
mergedRef = mergedTarget
|
|
63
75
|
} else {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (targetRemoteExists) {
|
|
68
|
-
await git.pull(mergedTarget, {
|
|
69
|
-
noRebase: true
|
|
76
|
+
throw new IcodeError(`清理基线分支不存在: ${mergedTarget}`, {
|
|
77
|
+
code: 'CLEAN_TARGET_MISSING',
|
|
78
|
+
exitCode: 2
|
|
70
79
|
})
|
|
71
80
|
}
|
|
81
|
+
}
|
|
72
82
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
83
|
+
const protectedBranches = [...(policy.protectedBranches || []), ...(context.repoConfig?.protectedBranches || [])]
|
|
84
|
+
const keepSet = normalizeKeepList(options.keep || [])
|
|
85
|
+
// 这些分支永远不进入清理列表,避免误删核心分支。
|
|
86
|
+
;[mergedTarget, context.defaultBranch, currentBranch, ...protectedBranches]
|
|
87
|
+
.map((branch) => String(branch || '').trim())
|
|
88
|
+
.filter(Boolean)
|
|
89
|
+
.forEach((branch) => keepSet.add(branch))
|
|
90
|
+
|
|
91
|
+
const reasonsByBranch = new Map()
|
|
92
|
+
const addReason = (branch, reason) => {
|
|
93
|
+
if (keepSet.has(branch)) {
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
if (!reasonsByBranch.has(branch)) {
|
|
97
|
+
reasonsByBranch.set(branch, [])
|
|
98
|
+
}
|
|
99
|
+
reasonsByBranch.get(branch).push(reason)
|
|
100
|
+
}
|
|
76
101
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
keepSet.add(context.defaultBranch)
|
|
102
|
+
if (selectMerged) {
|
|
103
|
+
for (const branch of await git.listMergedLocalBranches(mergedRef)) {
|
|
104
|
+
addReason(branch, `已合并到 ${mergedRef}`)
|
|
81
105
|
}
|
|
82
|
-
|
|
83
|
-
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (selectGone || staleDays) {
|
|
109
|
+
const now = Date.now() / 1000
|
|
110
|
+
for (const detail of await git.listBranchDetails()) {
|
|
111
|
+
if (selectGone && detail.gone) {
|
|
112
|
+
addReason(detail.name, '远程分支已删除')
|
|
113
|
+
}
|
|
114
|
+
if (staleDays && detail.timestamp && now - detail.timestamp > staleDays * 86400) {
|
|
115
|
+
addReason(detail.name, `超过 ${staleDays} 天未提交(${detail.relativeDate})`)
|
|
116
|
+
}
|
|
84
117
|
}
|
|
85
|
-
|
|
118
|
+
}
|
|
86
119
|
|
|
87
|
-
|
|
120
|
+
const candidates = [...reasonsByBranch.entries()].map(([branch, reasons]) => ({ branch, reasons }))
|
|
121
|
+
const baseResult = {
|
|
122
|
+
repoRoot: context.topLevelPath,
|
|
123
|
+
mergedTarget,
|
|
124
|
+
candidates,
|
|
125
|
+
deletedLocal: [],
|
|
126
|
+
deletedRemote: []
|
|
127
|
+
}
|
|
88
128
|
|
|
89
|
-
|
|
129
|
+
if (!candidates.length) {
|
|
130
|
+
if (!options.json) {
|
|
90
131
|
logger.info('没有可清理的本地分支。')
|
|
91
|
-
return {
|
|
92
|
-
repoRoot: context.topLevelPath,
|
|
93
|
-
mergedTarget,
|
|
94
|
-
deletedLocal: [],
|
|
95
|
-
deletedRemote: []
|
|
96
|
-
}
|
|
97
132
|
}
|
|
133
|
+
return baseResult
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (options.dryRun) {
|
|
137
|
+
return { ...baseResult, dryRun: true }
|
|
138
|
+
}
|
|
98
139
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
)
|
|
140
|
+
let selected = candidates
|
|
141
|
+
if (!options.yes) {
|
|
142
|
+
if (isInteractiveTerminal()) {
|
|
143
|
+
// 勾选要删的分支(默认全选),不想删的取消勾选即可。
|
|
144
|
+
const picked = await chooseMany(`选择要删除的分支(共 ${candidates.length} 个)`, candidates.map((item) => ({
|
|
145
|
+
value: item.branch,
|
|
146
|
+
label: item.branch,
|
|
147
|
+
hint: item.reasons.join(';')
|
|
148
|
+
})), { defaultValues: candidates.map((item) => item.branch), doneLabel: '删除' })
|
|
149
|
+
if (!picked || !picked.length) {
|
|
150
|
+
logger.warn('已取消清理。')
|
|
151
|
+
return { ...baseResult, canceled: true }
|
|
152
|
+
}
|
|
153
|
+
selected = candidates.filter((item) => picked.includes(item.branch))
|
|
154
|
+
} else {
|
|
155
|
+
const accepted = await confirm(`确认删除 ${candidates.length} 个分支吗?`, false)
|
|
104
156
|
if (!accepted) {
|
|
105
157
|
logger.warn('已取消清理。')
|
|
106
|
-
return {
|
|
107
|
-
canceled: true,
|
|
108
|
-
repoRoot: context.topLevelPath,
|
|
109
|
-
mergedTarget,
|
|
110
|
-
candidates
|
|
111
|
-
}
|
|
158
|
+
return { ...baseResult, canceled: true }
|
|
112
159
|
}
|
|
113
160
|
}
|
|
161
|
+
}
|
|
114
162
|
|
|
115
|
-
|
|
116
|
-
|
|
163
|
+
// 有恢复点兜底,未合并的 gone/stale 分支也可以安全地用 -D 删除。
|
|
164
|
+
const backup = await createBackup(git, {
|
|
165
|
+
reason: `clean ${selected.length} branches`,
|
|
166
|
+
branches: selected.map((item) => item.branch),
|
|
167
|
+
silent: options.json
|
|
168
|
+
})
|
|
117
169
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
})
|
|
122
|
-
deletedLocal.push(branch)
|
|
170
|
+
const deletedLocal = []
|
|
171
|
+
const deletedRemote = []
|
|
172
|
+
const failed = []
|
|
123
173
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
174
|
+
for (const { branch, reasons } of selected) {
|
|
175
|
+
const merged = reasons.some((reason) => reason.startsWith('已合并'))
|
|
176
|
+
try {
|
|
177
|
+
await git.deleteLocalBranch(branch, { force: options.force || !merged })
|
|
178
|
+
deletedLocal.push(branch)
|
|
179
|
+
} catch (error) {
|
|
180
|
+
failed.push({ branch, reason: error.message.split('\n')[0] })
|
|
181
|
+
continue
|
|
131
182
|
}
|
|
132
183
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
} finally {
|
|
140
|
-
if (currentBranch && currentBranch !== mergedTarget) {
|
|
141
|
-
try {
|
|
142
|
-
await git.checkout(currentBranch)
|
|
143
|
-
} catch (error) {
|
|
144
|
-
logger.warn(`未能切回原分支 ${currentBranch}: ${error.message}`)
|
|
184
|
+
if (options.remote && merged) {
|
|
185
|
+
const existsRemote = await git.branchExistsRemote(branch)
|
|
186
|
+
if (existsRemote) {
|
|
187
|
+
await git.deleteRemoteBranch(branch)
|
|
188
|
+
deletedRemote.push(branch)
|
|
145
189
|
}
|
|
146
190
|
}
|
|
147
191
|
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
...baseResult,
|
|
195
|
+
deletedLocal,
|
|
196
|
+
deletedRemote,
|
|
197
|
+
failed,
|
|
198
|
+
backupId: backup.entries.length ? backup.id : ''
|
|
199
|
+
}
|
|
148
200
|
}
|
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { resolveRemoteMergePlatform } from './push/remote-merge-step.js'
|
|
1
2
|
import { getAiConfig } from '../core/ai/config.js'
|
|
2
|
-
import { getConfigFilePath,
|
|
3
|
+
import { getConfigFilePath, getRepoPolicy } from '../core/config/store.js'
|
|
3
4
|
import { resolveGitContext } from '../core/git/context.js'
|
|
4
5
|
import { GitService } from '../core/git/service.js'
|
|
5
6
|
import { runCommand } from '../core/commands/shell.js'
|
|
@@ -57,14 +58,16 @@ async function checkGitVersion(repoRoot) {
|
|
|
57
58
|
return buildCheck('git.version', 'pass', 'Git 命令可用', result.stdout.trim())
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
//
|
|
61
|
+
// 检查当前使用的 remote 是否存在,输入 GitService,输出健康检查项(id 保持 git.origin 兼容旧脚本)。
|
|
61
62
|
async function checkOriginRemote(git) {
|
|
62
|
-
const
|
|
63
|
+
const remoteName = git.remote
|
|
64
|
+
const remoteUrl = await git.getRemoteUrl(remoteName)
|
|
63
65
|
if (!remoteUrl) {
|
|
64
|
-
return buildCheck('git.origin', 'warn',
|
|
66
|
+
return buildCheck('git.origin', 'warn', `${remoteName} remote 未配置`, `当前仓库没有 ${remoteName} remote。`, `需要 push 或远程合并前,请先配置 git remote add ${remoteName} <url>,或用 icode --remote <name> 指定其它 remote。`)
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
|
|
69
|
+
const source = git.context.remoteSource && git.context.remoteSource !== 'default' ? `,来源: ${git.context.remoteSource}` : ''
|
|
70
|
+
return buildCheck('git.origin', 'pass', `${remoteName} remote 已配置${source}`, remoteUrl)
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
// 检查 hook 配置,输入 git context,输出健康检查项。
|
|
@@ -97,7 +100,7 @@ function checkRemoteMerge(remoteMergePlatform, repoPolicy) {
|
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
if (!remoteMergePlatform.provider || !remoteMergePlatform.apiKey) {
|
|
100
|
-
return buildCheck('remoteMerge.config', 'warn', '远程合并平台未完整配置',
|
|
103
|
+
return buildCheck('remoteMerge.config', 'warn', '远程合并平台未完整配置', remoteMergePlatform.provider ? `未找到 ${remoteMergePlatform.provider} token。` : '无法从 remote 识别代码托管平台。', `如需 icode push -r / icode pr,请执行 icode config platform token set ${remoteMergePlatform.provider || '<provider>'} <token>。`)
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
return buildCheck('remoteMerge.config', 'pass', '远程合并平台已配置', `provider=${remoteMergePlatform.provider}`)
|
|
@@ -112,7 +115,7 @@ export async function runDoctorWorkflow(options = {}) {
|
|
|
112
115
|
const git = new GitService(context)
|
|
113
116
|
const repoPolicy = getRepoPolicy(context.topLevelPath)
|
|
114
117
|
const aiConfig = getAiConfig()
|
|
115
|
-
const remoteMergePlatform =
|
|
118
|
+
const remoteMergePlatform = await resolveRemoteMergePlatform(git)
|
|
116
119
|
|
|
117
120
|
const checks = [
|
|
118
121
|
await checkGitVersion(context.topLevelPath),
|