@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
|
@@ -9,6 +9,21 @@ function isRevertConflict(error) {
|
|
|
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,5 +1,12 @@
|
|
|
1
1
|
import { IcodeError } from '../core/errors.js'
|
|
2
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'
|
|
3
10
|
import { GitService } from '../core/git/service.js'
|
|
4
11
|
import { confirm } from '../core/tools/interactive.js'
|
|
5
12
|
import { logger } from '../core/tools/logger.js'
|
|
@@ -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
|
|
|
@@ -35,6 +35,14 @@ const UNDO_REF_SUGGESTIONS = {
|
|
|
35
35
|
hard: ['HEAD~1', 'HEAD~2', 'HEAD~3', 'HEAD~5', 'HEAD']
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// 静默模式下输出 warn 日志,输入执行选项和日志文本,输出为空。
|
|
39
|
+
function logWarn(options, message) {
|
|
40
|
+
if (!options.silentLog) {
|
|
41
|
+
logger.warn(message)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 解析交互菜单选择,输入菜单 value,输出 mode/ref 或 canceled。
|
|
38
46
|
function parseSelection(selection) {
|
|
39
47
|
if (!selection || selection === 'cancel') {
|
|
40
48
|
return {
|
|
@@ -49,10 +57,12 @@ function parseSelection(selection) {
|
|
|
49
57
|
}
|
|
50
58
|
}
|
|
51
59
|
|
|
60
|
+
// 解析指定模式的默认 ref,输入 mode,输出默认回滚目标。
|
|
52
61
|
function resolveDefaultRef(mode) {
|
|
53
62
|
return mode === 'revert' ? 'HEAD' : 'HEAD~1'
|
|
54
63
|
}
|
|
55
64
|
|
|
65
|
+
// 构建 undo 的 ref 选择项,输入 workflow options 和 mode,输出交互候选项。
|
|
56
66
|
async function buildUndoRefChoices(options, mode) {
|
|
57
67
|
const fallbackRef = resolveDefaultRef(mode)
|
|
58
68
|
const suggestedRefs = UNDO_REF_SUGGESTIONS[mode] || UNDO_REF_SUGGESTIONS.soft
|
|
@@ -98,6 +108,7 @@ async function buildUndoRefChoices(options, mode) {
|
|
|
98
108
|
}
|
|
99
109
|
}
|
|
100
110
|
|
|
111
|
+
// 校验并归一化 recover 参数,输入用户参数,输出 continue/abort/keep 或空字符串。
|
|
101
112
|
function normalizeRecover(value) {
|
|
102
113
|
const normalized = (value || '').trim().toLowerCase()
|
|
103
114
|
if (!normalized) {
|
|
@@ -115,6 +126,7 @@ function normalizeRecover(value) {
|
|
|
115
126
|
return normalized
|
|
116
127
|
}
|
|
117
128
|
|
|
129
|
+
// 处理未完成的 merge/revert/cherry-pick 操作,输入 workflow options,输出恢复结果或 null。
|
|
118
130
|
async function resolvePendingOperation(options) {
|
|
119
131
|
const context = await resolveGitContext({
|
|
120
132
|
cwd: options.cwd,
|
|
@@ -127,7 +139,7 @@ async function resolvePendingOperation(options) {
|
|
|
127
139
|
return null
|
|
128
140
|
}
|
|
129
141
|
|
|
130
|
-
|
|
142
|
+
logWarn(options, `检测到未完成的 ${pending} 操作。`)
|
|
131
143
|
|
|
132
144
|
let action = normalizeRecover(options.recover)
|
|
133
145
|
if (!action) {
|
|
@@ -153,7 +165,7 @@ async function resolvePendingOperation(options) {
|
|
|
153
165
|
}
|
|
154
166
|
|
|
155
167
|
if (action === 'keep') {
|
|
156
|
-
|
|
168
|
+
logWarn(options, '保持现场,未执行继续/中止。')
|
|
157
169
|
return {
|
|
158
170
|
canceled: true,
|
|
159
171
|
pendingOperation: pending
|
|
@@ -164,6 +176,8 @@ async function resolvePendingOperation(options) {
|
|
|
164
176
|
try {
|
|
165
177
|
if (pending === 'revert') {
|
|
166
178
|
await git.revertContinue()
|
|
179
|
+
} else if (pending === 'merge') {
|
|
180
|
+
await git.mergeContinue()
|
|
167
181
|
} else {
|
|
168
182
|
await git.cherryPickContinue()
|
|
169
183
|
}
|
|
@@ -186,6 +200,8 @@ async function resolvePendingOperation(options) {
|
|
|
186
200
|
|
|
187
201
|
if (pending === 'revert') {
|
|
188
202
|
await git.revertAbort()
|
|
203
|
+
} else if (pending === 'merge') {
|
|
204
|
+
await git.mergeAbort()
|
|
189
205
|
} else {
|
|
190
206
|
await git.abortCherryPick()
|
|
191
207
|
}
|
|
@@ -196,6 +212,7 @@ async function resolvePendingOperation(options) {
|
|
|
196
212
|
}
|
|
197
213
|
}
|
|
198
214
|
|
|
215
|
+
// 处理 revert 冲突后的恢复策略,输入 options/mode/ref,输出恢复结果。
|
|
199
216
|
async function handleRevertConflict(options, mode, ref) {
|
|
200
217
|
let action = normalizeRecover(options.recover)
|
|
201
218
|
|
|
@@ -265,7 +282,7 @@ export async function runUndoWorkflow(options) {
|
|
|
265
282
|
const selected = await chooseOne('请选择回滚策略:', UNDO_OPTIONS, 0)
|
|
266
283
|
const parsed = parseSelection(selected)
|
|
267
284
|
if (parsed.canceled) {
|
|
268
|
-
|
|
285
|
+
logWarn(options, '已取消 undo。')
|
|
269
286
|
return {
|
|
270
287
|
canceled: true
|
|
271
288
|
}
|
|
@@ -285,7 +302,7 @@ export async function runUndoWorkflow(options) {
|
|
|
285
302
|
const refChoice = await buildUndoRefChoices(options, mode)
|
|
286
303
|
const selectedRef = await chooseOne('请选择要回滚的 ref:', refChoice.choices, refChoice.defaultIndex)
|
|
287
304
|
if (selectedRef === 'cancel') {
|
|
288
|
-
|
|
305
|
+
logWarn(options, '已取消 undo。')
|
|
289
306
|
return {
|
|
290
307
|
canceled: true,
|
|
291
308
|
mode,
|
|
@@ -307,7 +324,7 @@ export async function runUndoWorkflow(options) {
|
|
|
307
324
|
mode !== 'hard' ? 0 : 1
|
|
308
325
|
)) === 'yes'
|
|
309
326
|
if (!accepted) {
|
|
310
|
-
|
|
327
|
+
logWarn(options, '已取消 undo。')
|
|
311
328
|
return {
|
|
312
329
|
canceled: true,
|
|
313
330
|
mode,
|
|
@@ -322,14 +339,15 @@ export async function runUndoWorkflow(options) {
|
|
|
322
339
|
mode,
|
|
323
340
|
yes: options.yes,
|
|
324
341
|
repoMode: options.repoMode,
|
|
325
|
-
cwd: options.cwd
|
|
342
|
+
cwd: options.cwd,
|
|
343
|
+
silentLog: options.silentLog
|
|
326
344
|
})
|
|
327
345
|
} catch (error) {
|
|
328
346
|
if (error?.code !== 'REVERT_CONFLICT') {
|
|
329
347
|
throw error
|
|
330
348
|
}
|
|
331
349
|
|
|
332
|
-
|
|
350
|
+
logWarn(options, error.message)
|
|
333
351
|
return handleRevertConflict(options, mode, ref)
|
|
334
352
|
}
|
|
335
353
|
}
|