@icode-js/icode 3.0.4 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +142 -5
- package/bin/icode.js +4 -4
- package/package.json +1 -1
- package/src/cli.js +60 -23
- package/src/commands/ai.js +4 -4
- package/src/commands/checkout.js +2 -2
- package/src/commands/clean.js +5 -2
- package/src/commands/codereview.js +5 -2
- package/src/commands/completion.js +93 -0
- package/src/commands/config.js +123 -10
- package/src/commands/doctor.js +80 -0
- package/src/commands/explain.js +6 -3
- package/src/commands/help.js +17 -0
- package/src/commands/info.js +5 -2
- package/src/commands/migrate.js +40 -3
- package/src/commands/push.js +72 -24
- package/src/commands/ready.js +110 -0
- package/src/commands/save.js +98 -0
- package/src/commands/sync.js +2 -2
- package/src/commands/tag.js +2 -2
- package/src/commands/undo.js +73 -10
- package/src/core/{ai-client.js → ai/client.js} +3 -3
- package/src/core/{ai-config.js → ai/config.js} +2 -2
- package/src/core/ai/diff-prompt.js +204 -0
- package/src/core/{ai-diff-range.js → ai/diff-range.js} +2 -2
- package/src/core/{args.js → cli/args.js} +23 -2
- package/src/core/cli/completion.js +720 -0
- package/src/core/cli/update-notifier.js +138 -0
- package/src/core/{shell.js → commands/shell.js} +13 -1
- package/src/core/{config-store.js → config/store.js} +53 -3
- package/src/core/{git-context.js → git/context.js} +24 -10
- package/src/core/git/preconditions.js +88 -0
- package/src/core/{git-service.js → git/service.js} +195 -5
- package/src/core/remote-merge/client.js +42 -0
- package/src/core/remote-merge/providers/gitlab.js +596 -0
- package/src/core/remote-merge/shared.js +49 -0
- package/src/core/tools/interactive.js +368 -0
- package/src/core/{loading.js → tools/loading.js} +5 -1
- package/src/core/{logger.js → tools/logger.js} +5 -0
- package/src/workflows/ai-codereview-workflow.js +18 -6
- package/src/workflows/ai-commit-workflow.js +20 -12
- package/src/workflows/ai-conflict-workflow.js +4 -4
- package/src/workflows/ai-explain-workflow.js +4 -4
- package/src/workflows/ai-risk-review-workflow.js +1 -1
- package/src/workflows/checkout-workflow.js +17 -5
- package/src/workflows/clean-workflow.js +92 -75
- package/src/workflows/doctor-workflow.js +131 -0
- package/src/workflows/info-workflow.js +4 -4
- package/src/workflows/migrate-workflow.js +269 -62
- package/src/workflows/push-workflow.js +475 -58
- package/src/workflows/ready-workflow.js +278 -0
- package/src/workflows/rollback-workflow.js +24 -9
- package/src/workflows/save-workflow.js +152 -0
- package/src/workflows/sync-workflow.js +22 -6
- package/src/workflows/tag-workflow.js +4 -4
- package/src/workflows/undo-workflow.js +37 -12
- package/src/core/prompts.js +0 -152
- /package/src/core/{ai-commit-summary.js → ai/commit-summary.js} +0 -0
- /package/src/core/{commit-conventions.js → git/commit-conventions.js} +0 -0
- /package/src/core/{hook-diagnostics.js → git/hook-diagnostics.js} +0 -0
|
@@ -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
|
+
}
|
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
import { IcodeError } from '../core/errors.js'
|
|
2
|
-
import { resolveGitContext } from '../core/git
|
|
3
|
-
import { GitService } from '../core/git
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { resolveGitContext } from '../core/git/context.js'
|
|
3
|
+
import { GitService } from '../core/git/service.js'
|
|
4
|
+
import { confirm } from '../core/tools/interactive.js'
|
|
5
|
+
import { logger } from '../core/tools/logger.js'
|
|
6
6
|
|
|
7
7
|
function isRevertConflict(error) {
|
|
8
8
|
const output = `${error?.meta?.stdout || ''}\n${error?.meta?.stderr || ''}\n${error?.message || ''}`
|
|
9
9
|
return /revert is already in progress|could not revert|after resolving the conflicts|CONFLICT \(/i.test(output)
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
// 静默模式下输出 info 日志,输入执行选项和日志文本,输出为空。
|
|
13
|
+
function logInfo(options, message) {
|
|
14
|
+
if (!options.silentLog) {
|
|
15
|
+
logger.info(message)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 静默模式下输出 warn 日志,输入执行选项和日志文本,输出为空。
|
|
20
|
+
function logWarn(options, message) {
|
|
21
|
+
if (!options.silentLog) {
|
|
22
|
+
logger.warn(message)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 执行底层回滚操作,输入 mode/ref/仓库选项,输出执行结果。
|
|
12
27
|
export async function runRollbackWorkflow(options) {
|
|
13
28
|
const mode = options.mode || 'revert'
|
|
14
29
|
const ref = options.ref || (mode === 'revert' ? 'HEAD' : 'HEAD~1')
|
|
@@ -20,13 +35,13 @@ export async function runRollbackWorkflow(options) {
|
|
|
20
35
|
|
|
21
36
|
const git = new GitService(context)
|
|
22
37
|
|
|
23
|
-
|
|
38
|
+
logInfo(options, `仓库根目录: ${context.topLevelPath}`)
|
|
24
39
|
if (context.inheritedFromParent) {
|
|
25
|
-
|
|
40
|
+
logWarn(options, '当前目录继承了父级 Git 仓库,命令将基于父仓库根目录执行。')
|
|
26
41
|
}
|
|
27
42
|
|
|
28
43
|
if (mode === 'revert') {
|
|
29
|
-
|
|
44
|
+
logInfo(options, `执行回滚(revert): ${ref}`)
|
|
30
45
|
try {
|
|
31
46
|
await git.revert(ref)
|
|
32
47
|
} catch (error) {
|
|
@@ -63,7 +78,7 @@ export async function runRollbackWorkflow(options) {
|
|
|
63
78
|
false
|
|
64
79
|
)
|
|
65
80
|
if (!accepted) {
|
|
66
|
-
|
|
81
|
+
logWarn(options, '已取消 hard 回滚。')
|
|
67
82
|
return {
|
|
68
83
|
canceled: true,
|
|
69
84
|
mode,
|
|
@@ -73,7 +88,7 @@ export async function runRollbackWorkflow(options) {
|
|
|
73
88
|
}
|
|
74
89
|
}
|
|
75
90
|
|
|
76
|
-
|
|
91
|
+
logInfo(options, `执行回滚(reset --${mode}): ${ref}`)
|
|
77
92
|
await git.reset(mode, ref)
|
|
78
93
|
|
|
79
94
|
return {
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { getAiProfile } from '../core/ai/config.js'
|
|
2
|
+
import { IcodeError } from '../core/errors.js'
|
|
3
|
+
import { resolveGitContext } from '../core/git/context.js'
|
|
4
|
+
import {
|
|
5
|
+
assertNoConflictedFiles,
|
|
6
|
+
assertNoPendingOperation,
|
|
7
|
+
assertOnBranch,
|
|
8
|
+
readGitSafetyState
|
|
9
|
+
} from '../core/git/preconditions.js'
|
|
10
|
+
import { GitService } from '../core/git/service.js'
|
|
11
|
+
import { runAiCommitWorkflow } from './ai-commit-workflow.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 清理提交信息。
|
|
15
|
+
* 输入为任意字符串,输出去掉首尾空白后的提交信息。
|
|
16
|
+
*/
|
|
17
|
+
function normalizeMessage(message) {
|
|
18
|
+
return String(message || '').trim()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 校验 save 的提交信息来源。
|
|
23
|
+
* 输入为 workflow options,输出为空;发现冲突配置时抛错。
|
|
24
|
+
*/
|
|
25
|
+
function validateMessageSource(options) {
|
|
26
|
+
const hasMessage = Boolean(normalizeMessage(options.message))
|
|
27
|
+
const useAiCommit = options.aiCommit === true
|
|
28
|
+
|
|
29
|
+
if (hasMessage && useAiCommit) {
|
|
30
|
+
throw new IcodeError('不能同时使用 --message 和 --ai-commit。请明确选择一种提交信息来源。', {
|
|
31
|
+
code: 'SAVE_MESSAGE_SOURCE_CONFLICT',
|
|
32
|
+
exitCode: 2
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!hasMessage && !useAiCommit) {
|
|
37
|
+
throw new IcodeError('缺少提交信息。请使用 --message <msg>,或显式使用 --ai-commit。', {
|
|
38
|
+
code: 'SAVE_MESSAGE_REQUIRED',
|
|
39
|
+
exitCode: 2
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 校验当前仓库是否允许执行 save。
|
|
46
|
+
* 输入为当前分支、未完成操作和改动摘要,输出为空;不满足条件时抛出明确错误。
|
|
47
|
+
*/
|
|
48
|
+
function validateSavePreconditions(currentBranch, operation, changes) {
|
|
49
|
+
assertOnBranch(currentBranch, 'save', 'SAVE_DETACHED_HEAD')
|
|
50
|
+
assertNoPendingOperation(operation, 'save', 'SAVE_GIT_OPERATION_IN_PROGRESS')
|
|
51
|
+
assertNoConflictedFiles(changes, 'save', 'SAVE_CONFLICTED_FILES')
|
|
52
|
+
|
|
53
|
+
if (changes.clean) {
|
|
54
|
+
throw new IcodeError('没有可提交的改动,save 已停止。', {
|
|
55
|
+
code: 'SAVE_NO_CHANGES',
|
|
56
|
+
exitCode: 2
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 构建 dry-run 结果。
|
|
63
|
+
* 输入为上下文、分支、改动和计划提交信息,输出不会产生副作用的预览结构。
|
|
64
|
+
*/
|
|
65
|
+
function buildDryRunResult(context, currentBranch, changes, commitMessage, source) {
|
|
66
|
+
return {
|
|
67
|
+
applied: false,
|
|
68
|
+
dryRun: true,
|
|
69
|
+
commitId: '',
|
|
70
|
+
commitMessage,
|
|
71
|
+
source,
|
|
72
|
+
context: {
|
|
73
|
+
topLevelPath: context.topLevelPath,
|
|
74
|
+
currentBranch
|
|
75
|
+
},
|
|
76
|
+
changes
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 执行安全快速提交。
|
|
82
|
+
* 输入为 save 参数,输出提交结果;关键假设:dry-run 不执行 add/commit,AI 模式必须使用有效 profile。
|
|
83
|
+
*/
|
|
84
|
+
export async function runSaveWorkflow(options = {}) {
|
|
85
|
+
validateMessageSource(options)
|
|
86
|
+
|
|
87
|
+
const context = await resolveGitContext({
|
|
88
|
+
cwd: options.cwd,
|
|
89
|
+
repoMode: options.repoMode
|
|
90
|
+
})
|
|
91
|
+
const git = new GitService(context)
|
|
92
|
+
const { currentBranch, operation, changes } = await readGitSafetyState(git)
|
|
93
|
+
|
|
94
|
+
validateSavePreconditions(currentBranch, operation, changes)
|
|
95
|
+
|
|
96
|
+
if (options.aiCommit === true) {
|
|
97
|
+
// AI 模式必须先校验 profile 存在,避免 askAiJson 在缺配置时隐式选择其它配置。
|
|
98
|
+
getAiProfile(options.profile)
|
|
99
|
+
const aiResult = await runAiCommitWorkflow({
|
|
100
|
+
cwd: context.topLevelPath,
|
|
101
|
+
repoMode: 'strict',
|
|
102
|
+
profile: options.profile,
|
|
103
|
+
lang: options.lang || 'zh',
|
|
104
|
+
apply: options.dryRun !== true,
|
|
105
|
+
yes: options.yes === true,
|
|
106
|
+
noVerify: options.noVerify === true,
|
|
107
|
+
silentContextLog: true,
|
|
108
|
+
silentResultLog: true
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
if (options.dryRun === true) {
|
|
112
|
+
return buildDryRunResult(context, currentBranch, changes, aiResult.commitMessage, 'ai')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
applied: aiResult.applied,
|
|
117
|
+
dryRun: false,
|
|
118
|
+
commitId: aiResult.commitId || '',
|
|
119
|
+
commitMessage: aiResult.commitMessage,
|
|
120
|
+
source: 'ai',
|
|
121
|
+
context: {
|
|
122
|
+
topLevelPath: context.topLevelPath,
|
|
123
|
+
currentBranch
|
|
124
|
+
},
|
|
125
|
+
changes
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const commitMessage = normalizeMessage(options.message)
|
|
130
|
+
if (options.dryRun === true) {
|
|
131
|
+
return buildDryRunResult(context, currentBranch, changes, commitMessage, 'message')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await git.stageAll()
|
|
135
|
+
await git.commit(commitMessage, {
|
|
136
|
+
noVerify: options.noVerify === true
|
|
137
|
+
})
|
|
138
|
+
const commitId = await git.revParseShort('HEAD')
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
applied: true,
|
|
142
|
+
dryRun: false,
|
|
143
|
+
commitId,
|
|
144
|
+
commitMessage,
|
|
145
|
+
source: 'message',
|
|
146
|
+
context: {
|
|
147
|
+
topLevelPath: context.topLevelPath,
|
|
148
|
+
currentBranch
|
|
149
|
+
},
|
|
150
|
+
changes
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { IcodeError } from '../core/errors.js'
|
|
2
|
-
import { resolveGitContext } from '../core/git
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { resolveGitContext } from '../core/git/context.js'
|
|
3
|
+
import {
|
|
4
|
+
assertCleanWorktree,
|
|
5
|
+
assertNoConflictedFiles,
|
|
6
|
+
assertNoPendingOperation,
|
|
7
|
+
assertOnBranch,
|
|
8
|
+
readGitSafetyState
|
|
9
|
+
} from '../core/git/preconditions.js'
|
|
10
|
+
import { GitService } from '../core/git/service.js'
|
|
11
|
+
import { confirm } from '../core/tools/interactive.js'
|
|
12
|
+
import { logger } from '../core/tools/logger.js'
|
|
6
13
|
|
|
7
14
|
function unique(list) {
|
|
8
15
|
return [...new Set(list.map((item) => item.trim()).filter(Boolean))]
|
|
@@ -33,6 +40,10 @@ async function ensureBranchReady(git, branchName) {
|
|
|
33
40
|
}
|
|
34
41
|
}
|
|
35
42
|
|
|
43
|
+
/**
|
|
44
|
+
* 执行 sync 工作流。
|
|
45
|
+
* 输入为目标分支与同步选项,输出每个分支的同步摘要;实际切换分支前要求当前仓库处于干净状态。
|
|
46
|
+
*/
|
|
36
47
|
export async function runSyncWorkflow(options) {
|
|
37
48
|
const context = await resolveGitContext({
|
|
38
49
|
cwd: options.cwd,
|
|
@@ -40,7 +51,12 @@ export async function runSyncWorkflow(options) {
|
|
|
40
51
|
})
|
|
41
52
|
|
|
42
53
|
const git = new GitService(context)
|
|
43
|
-
const
|
|
54
|
+
const safetyState = await readGitSafetyState(git)
|
|
55
|
+
const currentBranch = safetyState.currentBranch || context.currentBranch
|
|
56
|
+
assertOnBranch(currentBranch, 'sync', 'SYNC_DETACHED_HEAD')
|
|
57
|
+
assertNoPendingOperation(safetyState.operation, 'sync', 'SYNC_GIT_OPERATION_IN_PROGRESS')
|
|
58
|
+
assertNoConflictedFiles(safetyState.changes, 'sync', 'SYNC_CONFLICTED_FILES')
|
|
59
|
+
assertCleanWorktree(safetyState.changes, 'sync', 'SYNC_DIRTY_WORKTREE')
|
|
44
60
|
|
|
45
61
|
let targetInput = options.branches?.length
|
|
46
62
|
? [...options.branches]
|
|
@@ -99,7 +115,7 @@ export async function runSyncWorkflow(options) {
|
|
|
99
115
|
if (setup.remoteExists) {
|
|
100
116
|
await git.pull(branchName, {
|
|
101
117
|
noRebase: !options.rebase,
|
|
102
|
-
|
|
118
|
+
rebase: options.rebase
|
|
103
119
|
})
|
|
104
120
|
}
|
|
105
121
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { IcodeError } from '../core/errors.js'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { resolveGitContext } from '../core/git/context.js'
|
|
3
|
+
import { GitService } from '../core/git/service.js'
|
|
4
|
+
import { input } from '../core/tools/interactive.js'
|
|
5
|
+
import { logger } from '../core/tools/logger.js'
|
|
6
6
|
|
|
7
7
|
function todayStamp() {
|
|
8
8
|
const now = new Date()
|