@icode-js/icode 3.0.2
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 +346 -0
- package/bin/icode.js +6 -0
- package/package.json +34 -0
- package/src/cli.js +131 -0
- package/src/commands/ai.js +287 -0
- package/src/commands/checkout.js +59 -0
- package/src/commands/clean.js +65 -0
- package/src/commands/codereview.js +52 -0
- package/src/commands/config.js +513 -0
- package/src/commands/explain.js +80 -0
- package/src/commands/help.js +49 -0
- package/src/commands/info.js +57 -0
- package/src/commands/migrate.js +86 -0
- package/src/commands/push.js +125 -0
- package/src/commands/sync.js +74 -0
- package/src/commands/tag.js +53 -0
- package/src/commands/undo.js +66 -0
- package/src/core/ai-client.js +1125 -0
- package/src/core/ai-commit-summary.js +18 -0
- package/src/core/ai-config.js +342 -0
- package/src/core/ai-diff-range.js +117 -0
- package/src/core/args.js +47 -0
- package/src/core/commit-conventions.js +169 -0
- package/src/core/config-store.js +194 -0
- package/src/core/errors.js +25 -0
- package/src/core/git-context.js +105 -0
- package/src/core/git-service.js +428 -0
- package/src/core/hook-diagnostics.js +23 -0
- package/src/core/loading.js +36 -0
- package/src/core/logger.js +55 -0
- package/src/core/prompts.js +152 -0
- package/src/core/shell.js +77 -0
- package/src/workflows/ai-codereview-workflow.js +126 -0
- package/src/workflows/ai-commit-workflow.js +128 -0
- package/src/workflows/ai-conflict-workflow.js +102 -0
- package/src/workflows/ai-explain-workflow.js +116 -0
- package/src/workflows/ai-risk-review-workflow.js +49 -0
- package/src/workflows/checkout-workflow.js +85 -0
- package/src/workflows/clean-workflow.js +131 -0
- package/src/workflows/info-workflow.js +30 -0
- package/src/workflows/migrate-workflow.js +449 -0
- package/src/workflows/push-workflow.js +276 -0
- package/src/workflows/rollback-workflow.js +84 -0
- package/src/workflows/sync-workflow.js +141 -0
- package/src/workflows/tag-workflow.js +64 -0
- package/src/workflows/undo-workflow.js +328 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { getRepoPolicy } from '../core/config-store.js'
|
|
2
|
+
import { IcodeError } from '../core/errors.js'
|
|
3
|
+
import { resolveGitContext } from '../core/git-context.js'
|
|
4
|
+
import { GitService } from '../core/git-service.js'
|
|
5
|
+
import { logger } from '../core/logger.js'
|
|
6
|
+
import { confirm } from '../core/prompts.js'
|
|
7
|
+
|
|
8
|
+
function normalizeKeepList(values = []) {
|
|
9
|
+
return new Set(
|
|
10
|
+
values
|
|
11
|
+
.flatMap((value) => value.split(','))
|
|
12
|
+
.map((item) => item.trim())
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function runCleanWorkflow(options) {
|
|
18
|
+
const context = await resolveGitContext({
|
|
19
|
+
cwd: options.cwd,
|
|
20
|
+
repoMode: options.repoMode
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const git = new GitService(context)
|
|
24
|
+
const mergedTarget = (options.mergedTarget || context.defaultBranch).trim()
|
|
25
|
+
const currentBranch = (await git.getCurrentBranch()) || context.currentBranch
|
|
26
|
+
const policy = getRepoPolicy(context.topLevelPath)
|
|
27
|
+
|
|
28
|
+
logger.info(`仓库根目录: ${context.topLevelPath}`)
|
|
29
|
+
if (context.inheritedFromParent) {
|
|
30
|
+
logger.warn('当前目录继承了父级 Git 仓库,命令将基于父仓库根目录执行。')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await git.fetch()
|
|
34
|
+
|
|
35
|
+
const targetLocalExists = await git.branchExistsLocal(mergedTarget)
|
|
36
|
+
const targetRemoteExists = await git.branchExistsRemote(mergedTarget)
|
|
37
|
+
if (!targetLocalExists && !targetRemoteExists) {
|
|
38
|
+
throw new IcodeError(`清理基线分支不存在: ${mergedTarget}`, {
|
|
39
|
+
code: 'CLEAN_TARGET_MISSING',
|
|
40
|
+
exitCode: 2
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (targetLocalExists) {
|
|
45
|
+
await git.checkout(mergedTarget)
|
|
46
|
+
} else {
|
|
47
|
+
await git.checkoutTracking(mergedTarget)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (targetRemoteExists) {
|
|
51
|
+
await git.pull(mergedTarget, {
|
|
52
|
+
allowUnrelatedHistories: true,
|
|
53
|
+
noRebase: true
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const mergedBranches = await git.listMergedLocalBranches(mergedTarget)
|
|
58
|
+
const protectedBranches = new Set((policy.protectedBranches || []).map((item) => item.trim()).filter(Boolean))
|
|
59
|
+
const keepSet = normalizeKeepList(options.keep || [])
|
|
60
|
+
|
|
61
|
+
// 这些分支永远不进入清理列表,避免误删核心分支。
|
|
62
|
+
keepSet.add(mergedTarget)
|
|
63
|
+
if (context.defaultBranch) {
|
|
64
|
+
keepSet.add(context.defaultBranch)
|
|
65
|
+
}
|
|
66
|
+
if (currentBranch) {
|
|
67
|
+
keepSet.add(currentBranch)
|
|
68
|
+
}
|
|
69
|
+
protectedBranches.forEach((branch) => keepSet.add(branch))
|
|
70
|
+
|
|
71
|
+
const candidates = mergedBranches.filter((branch) => !keepSet.has(branch))
|
|
72
|
+
|
|
73
|
+
if (!candidates.length) {
|
|
74
|
+
logger.info('没有可清理的本地分支。')
|
|
75
|
+
return {
|
|
76
|
+
repoRoot: context.topLevelPath,
|
|
77
|
+
mergedTarget,
|
|
78
|
+
deletedLocal: [],
|
|
79
|
+
deletedRemote: []
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!options.yes) {
|
|
84
|
+
const accepted = await confirm(
|
|
85
|
+
`确认清理以下分支吗: ${candidates.join(', ')} ?`,
|
|
86
|
+
false
|
|
87
|
+
)
|
|
88
|
+
if (!accepted) {
|
|
89
|
+
logger.warn('已取消清理。')
|
|
90
|
+
return {
|
|
91
|
+
canceled: true,
|
|
92
|
+
repoRoot: context.topLevelPath,
|
|
93
|
+
mergedTarget,
|
|
94
|
+
candidates
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const deletedLocal = []
|
|
100
|
+
const deletedRemote = []
|
|
101
|
+
|
|
102
|
+
for (const branch of candidates) {
|
|
103
|
+
await git.deleteLocalBranch(branch, {
|
|
104
|
+
force: options.force
|
|
105
|
+
})
|
|
106
|
+
deletedLocal.push(branch)
|
|
107
|
+
|
|
108
|
+
if (options.remote) {
|
|
109
|
+
const existsRemote = await git.branchExistsRemote(branch)
|
|
110
|
+
if (existsRemote) {
|
|
111
|
+
await git.deleteRemoteBranch(branch)
|
|
112
|
+
deletedRemote.push(branch)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
if (currentBranch && currentBranch !== mergedTarget) {
|
|
119
|
+
await git.checkout(currentBranch)
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
logger.warn(`未能切回原分支 ${currentBranch}: ${error.message}`)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
repoRoot: context.topLevelPath,
|
|
127
|
+
mergedTarget,
|
|
128
|
+
deletedLocal,
|
|
129
|
+
deletedRemote
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { getAiConfig } from '../core/ai-config.js'
|
|
2
|
+
import { getConfigFilePath, getRepoPolicy } from '../core/config-store.js'
|
|
3
|
+
import { resolveGitContext } from '../core/git-context.js'
|
|
4
|
+
import { runCommand } from '../core/shell.js'
|
|
5
|
+
|
|
6
|
+
export async function runInfoWorkflow(options) {
|
|
7
|
+
const context = await resolveGitContext({
|
|
8
|
+
cwd: options.cwd,
|
|
9
|
+
repoMode: options.repoMode
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const gitVersion = await runCommand('git', ['--version'], {
|
|
13
|
+
cwd: context.topLevelPath,
|
|
14
|
+
allowFailure: true
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const repoPolicy = getRepoPolicy(context.topLevelPath)
|
|
18
|
+
const aiConfig = getAiConfig()
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
configPath: getConfigFilePath(),
|
|
22
|
+
gitVersion: (gitVersion.stdout || '').trim(),
|
|
23
|
+
context,
|
|
24
|
+
repoPolicy,
|
|
25
|
+
aiConfig: {
|
|
26
|
+
activeProfile: aiConfig.activeProfile,
|
|
27
|
+
profileCount: Object.keys(aiConfig.profiles || {}).length
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { IcodeError } from '../core/errors.js'
|
|
2
|
+
import { resolveGitContext } from '../core/git-context.js'
|
|
3
|
+
import { GitService } from '../core/git-service.js'
|
|
4
|
+
import { logger } from '../core/logger.js'
|
|
5
|
+
import { chooseMany, chooseOne, isInteractiveTerminal } from '../core/prompts.js'
|
|
6
|
+
|
|
7
|
+
const INTERACTIVE_COMMIT_LIMIT = 30
|
|
8
|
+
|
|
9
|
+
function normalizeBranchName(value) {
|
|
10
|
+
return (value || '').trim()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeRangeSpec(value) {
|
|
14
|
+
return (value || '').trim()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function buildRecentCountChoices(maxCount) {
|
|
18
|
+
const candidates = [1, 2, 3, 5, 8, 10]
|
|
19
|
+
.filter((count) => count <= maxCount)
|
|
20
|
+
.map((count) => ({
|
|
21
|
+
value: String(count),
|
|
22
|
+
label: `最近 ${count} 条提交`
|
|
23
|
+
}))
|
|
24
|
+
|
|
25
|
+
if (!candidates.length || candidates[candidates.length - 1].value !== String(maxCount)) {
|
|
26
|
+
candidates.push({
|
|
27
|
+
value: String(maxCount),
|
|
28
|
+
label: `最近 ${maxCount} 条提交(全部可选增量)`
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return candidates
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function uniqueBranches(branches) {
|
|
36
|
+
return Array.from(new Set((branches || []).map((item) => item.trim()).filter(Boolean))).sort((a, b) => a.localeCompare(b))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function listBranchCandidates(git) {
|
|
40
|
+
const [localBranches, remoteBranches] = await Promise.all([
|
|
41
|
+
git.listLocalBranches(),
|
|
42
|
+
git.listRemoteBranches('origin')
|
|
43
|
+
])
|
|
44
|
+
return uniqueBranches([...localBranches, ...remoteBranches])
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function pickBranch({ label, candidates, defaultValue, excludedBranch = '' }) {
|
|
48
|
+
const excluded = normalizeBranchName(excludedBranch)
|
|
49
|
+
const normalizedDefault = normalizeBranchName(defaultValue)
|
|
50
|
+
const filteredCandidates = candidates.filter((item) => item !== excluded)
|
|
51
|
+
|
|
52
|
+
if (!filteredCandidates.length && !normalizedDefault) {
|
|
53
|
+
throw new IcodeError(`无法选择${label},当前仓库没有可用分支。`, {
|
|
54
|
+
code: 'MIGRATE_BRANCH_PICK_EMPTY',
|
|
55
|
+
exitCode: 2
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const defaultCandidate = normalizedDefault && normalizedDefault !== excluded
|
|
60
|
+
? normalizedDefault
|
|
61
|
+
: (filteredCandidates[0] || '')
|
|
62
|
+
|
|
63
|
+
if (!isInteractiveTerminal()) {
|
|
64
|
+
return defaultCandidate
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const choices = filteredCandidates.map((branch) => ({
|
|
68
|
+
label: branch,
|
|
69
|
+
value: branch
|
|
70
|
+
}))
|
|
71
|
+
choices.push({
|
|
72
|
+
label: '取消',
|
|
73
|
+
value: 'cancel'
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const defaultIndex = Math.max(0, choices.findIndex((item) => item.value === defaultCandidate))
|
|
77
|
+
const selected = await chooseOne(`请选择${label}:`, choices, defaultIndex)
|
|
78
|
+
if (selected === 'cancel') {
|
|
79
|
+
return ''
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return normalizeBranchName(selected)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function pickManualCommits(git, commits) {
|
|
86
|
+
const displayCommits = commits.length > INTERACTIVE_COMMIT_LIMIT
|
|
87
|
+
? commits.slice(-INTERACTIVE_COMMIT_LIMIT)
|
|
88
|
+
: commits
|
|
89
|
+
|
|
90
|
+
if (commits.length > INTERACTIVE_COMMIT_LIMIT) {
|
|
91
|
+
logger.warn(`提交较多,仅展示最近 ${INTERACTIVE_COMMIT_LIMIT} 条供手动选择。`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const choices = []
|
|
95
|
+
for (const commitHash of displayCommits) {
|
|
96
|
+
const summary = await git.showCommitSummary(commitHash)
|
|
97
|
+
choices.push({
|
|
98
|
+
label: summary || commitHash,
|
|
99
|
+
value: commitHash
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const selected = await chooseMany('请选择要迁移的提交(可多选):', choices, {
|
|
104
|
+
minSelections: 1,
|
|
105
|
+
doneLabel: '完成提交选择',
|
|
106
|
+
cancelLabel: '取消迁移'
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
return Array.isArray(selected) ? selected : []
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function resolveInteractivePlan(git, options) {
|
|
113
|
+
const candidates = await listBranchCandidates(git)
|
|
114
|
+
const sourceBranch = await pickBranch({
|
|
115
|
+
label: 'source 分支',
|
|
116
|
+
candidates,
|
|
117
|
+
defaultValue: options.sourceBranch
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const targetBranch = await pickBranch({
|
|
121
|
+
label: 'target 分支',
|
|
122
|
+
candidates,
|
|
123
|
+
defaultValue: options.targetBranch,
|
|
124
|
+
excludedBranch: sourceBranch
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
if (!sourceBranch || !targetBranch) {
|
|
128
|
+
throw new IcodeError('source/target 分支不能为空。', {
|
|
129
|
+
code: 'MIGRATE_BRANCH_REQUIRED',
|
|
130
|
+
exitCode: 2
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (sourceBranch === targetBranch) {
|
|
135
|
+
throw new IcodeError('sourceBranch 和 targetBranch 不能相同。', {
|
|
136
|
+
code: 'MIGRATE_BRANCH_DUPLICATED',
|
|
137
|
+
exitCode: 2
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (options.range) {
|
|
142
|
+
return {
|
|
143
|
+
sourceBranch,
|
|
144
|
+
targetBranch,
|
|
145
|
+
range: options.range,
|
|
146
|
+
selectedCommits: [],
|
|
147
|
+
rangeMode: 'custom-range'
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const defaultRange = `${targetBranch}..${sourceBranch}`
|
|
152
|
+
const defaultCommits = await git.revList(defaultRange)
|
|
153
|
+
if (!defaultCommits.length) {
|
|
154
|
+
logger.warn(`默认范围 ${defaultRange} 内没有可迁移提交。`)
|
|
155
|
+
const nextStep = await chooseOne(
|
|
156
|
+
'当前增量为空,下一步:',
|
|
157
|
+
[
|
|
158
|
+
{ label: '改为从 source 分支最近提交中多选', value: 'pick-source' },
|
|
159
|
+
{ label: '取消迁移', value: 'cancel' }
|
|
160
|
+
],
|
|
161
|
+
0
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if (nextStep === 'cancel') {
|
|
165
|
+
return {
|
|
166
|
+
canceled: true,
|
|
167
|
+
sourceBranch,
|
|
168
|
+
targetBranch,
|
|
169
|
+
range: defaultRange,
|
|
170
|
+
selectedCommits: [],
|
|
171
|
+
rangeMode: 'all'
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const sourceCommits = await git.revList(sourceBranch)
|
|
176
|
+
const selectedCommits = await pickManualCommits(git, sourceCommits)
|
|
177
|
+
if (!selectedCommits.length) {
|
|
178
|
+
return {
|
|
179
|
+
canceled: true,
|
|
180
|
+
sourceBranch,
|
|
181
|
+
targetBranch,
|
|
182
|
+
range: defaultRange,
|
|
183
|
+
selectedCommits: [],
|
|
184
|
+
rangeMode: 'pick-commits-source'
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
sourceBranch,
|
|
190
|
+
targetBranch,
|
|
191
|
+
range: defaultRange,
|
|
192
|
+
selectedCommits,
|
|
193
|
+
rangeMode: 'pick-commits-source'
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const mode = await chooseOne(
|
|
198
|
+
'请选择迁移范围:',
|
|
199
|
+
[
|
|
200
|
+
{ label: `迁移全部增量提交 (${defaultCommits.length} 条)`, value: 'all' },
|
|
201
|
+
{ label: '迁移最近 N 条提交', value: 'recent' },
|
|
202
|
+
{ label: '手动多选提交', value: 'pick-commits' },
|
|
203
|
+
{ label: '取消', value: 'cancel' }
|
|
204
|
+
],
|
|
205
|
+
0
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if (mode === 'cancel') {
|
|
209
|
+
return {
|
|
210
|
+
canceled: true,
|
|
211
|
+
sourceBranch,
|
|
212
|
+
targetBranch,
|
|
213
|
+
range: defaultRange,
|
|
214
|
+
selectedCommits: [],
|
|
215
|
+
rangeMode: 'all'
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (mode === 'recent') {
|
|
220
|
+
const countChoices = buildRecentCountChoices(defaultCommits.length)
|
|
221
|
+
const defaultIndex = Math.max(0, countChoices.findIndex((choice) => choice.value === String(Math.min(5, defaultCommits.length))))
|
|
222
|
+
const selectedCount = await chooseOne('请选择迁移提交数:', countChoices, defaultIndex)
|
|
223
|
+
const count = Number(selectedCount)
|
|
224
|
+
return {
|
|
225
|
+
sourceBranch,
|
|
226
|
+
targetBranch,
|
|
227
|
+
range: defaultRange,
|
|
228
|
+
selectedCommits: defaultCommits.slice(-count),
|
|
229
|
+
rangeMode: `recent-${count}`
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (mode === 'pick-commits') {
|
|
234
|
+
const selectedCommits = await pickManualCommits(git, defaultCommits)
|
|
235
|
+
if (!selectedCommits.length) {
|
|
236
|
+
return {
|
|
237
|
+
canceled: true,
|
|
238
|
+
sourceBranch,
|
|
239
|
+
targetBranch,
|
|
240
|
+
range: defaultRange,
|
|
241
|
+
selectedCommits: [],
|
|
242
|
+
rangeMode: 'pick-commits'
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
sourceBranch,
|
|
247
|
+
targetBranch,
|
|
248
|
+
range: defaultRange,
|
|
249
|
+
selectedCommits,
|
|
250
|
+
rangeMode: 'pick-commits'
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
sourceBranch,
|
|
256
|
+
targetBranch,
|
|
257
|
+
range: defaultRange,
|
|
258
|
+
selectedCommits: [],
|
|
259
|
+
rangeMode: 'all'
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export async function runMigrateWorkflow(options) {
|
|
264
|
+
let sourceBranch = normalizeBranchName(options.sourceBranch)
|
|
265
|
+
let targetBranch = normalizeBranchName(options.targetBranch)
|
|
266
|
+
let rangeSpec = normalizeRangeSpec(options.range)
|
|
267
|
+
let selectedCommits = Array.isArray(options.selectedCommits)
|
|
268
|
+
? options.selectedCommits.map((item) => String(item).trim()).filter(Boolean)
|
|
269
|
+
: []
|
|
270
|
+
let rangeMode = selectedCommits.length ? 'selected-commits' : 'range'
|
|
271
|
+
const shouldInteractive = Boolean(options.interactive || !sourceBranch || !targetBranch)
|
|
272
|
+
|
|
273
|
+
const context = await resolveGitContext({
|
|
274
|
+
cwd: options.cwd,
|
|
275
|
+
repoMode: options.repoMode
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
const git = new GitService(context)
|
|
279
|
+
const originalBranch = await git.getCurrentBranch()
|
|
280
|
+
|
|
281
|
+
logger.info(`仓库根目录: ${context.topLevelPath}`)
|
|
282
|
+
if (context.inheritedFromParent) {
|
|
283
|
+
logger.warn('当前目录继承了父级 Git 仓库,命令将基于父仓库根目录执行。')
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
await git.fetch()
|
|
287
|
+
|
|
288
|
+
if (shouldInteractive) {
|
|
289
|
+
if (!isInteractiveTerminal()) {
|
|
290
|
+
throw new IcodeError('当前终端不支持交互,请显式传入 source/target 分支参数。', {
|
|
291
|
+
code: 'MIGRATE_INTERACTIVE_TTY_REQUIRED',
|
|
292
|
+
exitCode: 2
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const interactivePlan = await resolveInteractivePlan(git, {
|
|
297
|
+
sourceBranch,
|
|
298
|
+
targetBranch,
|
|
299
|
+
range: rangeSpec
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
if (interactivePlan.canceled) {
|
|
303
|
+
logger.warn('已取消迁移。')
|
|
304
|
+
return {
|
|
305
|
+
canceled: true,
|
|
306
|
+
sourceBranch: interactivePlan.sourceBranch || sourceBranch,
|
|
307
|
+
targetBranch: interactivePlan.targetBranch || targetBranch,
|
|
308
|
+
repoRoot: context.topLevelPath
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
sourceBranch = normalizeBranchName(interactivePlan.sourceBranch)
|
|
313
|
+
targetBranch = normalizeBranchName(interactivePlan.targetBranch)
|
|
314
|
+
rangeSpec = normalizeRangeSpec(interactivePlan.range)
|
|
315
|
+
selectedCommits = Array.isArray(interactivePlan.selectedCommits)
|
|
316
|
+
? interactivePlan.selectedCommits.map((item) => String(item).trim()).filter(Boolean)
|
|
317
|
+
: []
|
|
318
|
+
rangeMode = interactivePlan.rangeMode || rangeMode
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (!sourceBranch || !targetBranch) {
|
|
322
|
+
throw new IcodeError('缺少参数: icode migrate <sourceBranch> <targetBranch>', {
|
|
323
|
+
code: 'MIGRATE_BRANCH_REQUIRED',
|
|
324
|
+
exitCode: 2
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (sourceBranch === targetBranch) {
|
|
329
|
+
throw new IcodeError('sourceBranch 和 targetBranch 不能相同。', {
|
|
330
|
+
code: 'MIGRATE_BRANCH_DUPLICATED',
|
|
331
|
+
exitCode: 2
|
|
332
|
+
})
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const sourceExistsLocal = await git.branchExistsLocal(sourceBranch)
|
|
336
|
+
const sourceExistsRemote = await git.branchExistsRemote(sourceBranch)
|
|
337
|
+
if (!sourceExistsLocal && !sourceExistsRemote) {
|
|
338
|
+
throw new IcodeError(`source 分支不存在: ${sourceBranch}`, {
|
|
339
|
+
code: 'MIGRATE_SOURCE_MISSING',
|
|
340
|
+
exitCode: 2
|
|
341
|
+
})
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const targetExistsLocal = await git.branchExistsLocal(targetBranch)
|
|
345
|
+
const targetExistsRemote = await git.branchExistsRemote(targetBranch)
|
|
346
|
+
if (!targetExistsLocal && !targetExistsRemote) {
|
|
347
|
+
throw new IcodeError(`target 分支不存在: ${targetBranch}`, {
|
|
348
|
+
code: 'MIGRATE_TARGET_MISSING',
|
|
349
|
+
exitCode: 2
|
|
350
|
+
})
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
if (targetExistsLocal) {
|
|
355
|
+
await git.checkout(targetBranch)
|
|
356
|
+
} else {
|
|
357
|
+
await git.checkoutTracking(targetBranch)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (targetExistsRemote) {
|
|
361
|
+
await git.pull(targetBranch, {
|
|
362
|
+
allowUnrelatedHistories: true,
|
|
363
|
+
noRebase: true
|
|
364
|
+
})
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// 默认迁移 source 相对 target 的增量提交;交互模式可选择最近 N 条或手动挑选提交。
|
|
368
|
+
const effectiveRangeSpec = rangeSpec || `${targetBranch}..${sourceBranch}`
|
|
369
|
+
const commits = selectedCommits.length ? selectedCommits : await git.revList(effectiveRangeSpec)
|
|
370
|
+
|
|
371
|
+
if (selectedCommits.length) {
|
|
372
|
+
logger.info(`迁移范围: 手动选择 ${selectedCommits.length} 个提交(mode=${rangeMode})`)
|
|
373
|
+
} else {
|
|
374
|
+
logger.info(`迁移范围: ${effectiveRangeSpec}`)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (!commits.length) {
|
|
378
|
+
logger.warn('没有可迁移的提交。')
|
|
379
|
+
return {
|
|
380
|
+
sourceBranch,
|
|
381
|
+
targetBranch,
|
|
382
|
+
migratedCommits: 0,
|
|
383
|
+
rangeSpec: effectiveRangeSpec,
|
|
384
|
+
rangeMode,
|
|
385
|
+
repoRoot: context.topLevelPath
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!options.yes) {
|
|
390
|
+
const accepted = (await chooseOne(
|
|
391
|
+
`确认将 ${commits.length} 个提交从 ${sourceBranch} 迁移到 ${targetBranch} 吗?`,
|
|
392
|
+
[
|
|
393
|
+
{ value: 'yes', label: '确认迁移' },
|
|
394
|
+
{ value: 'no', label: '取消' }
|
|
395
|
+
],
|
|
396
|
+
0
|
|
397
|
+
)) === 'yes'
|
|
398
|
+
if (!accepted) {
|
|
399
|
+
logger.warn('已取消迁移。')
|
|
400
|
+
return {
|
|
401
|
+
canceled: true,
|
|
402
|
+
sourceBranch,
|
|
403
|
+
targetBranch,
|
|
404
|
+
rangeSpec: effectiveRangeSpec,
|
|
405
|
+
rangeMode,
|
|
406
|
+
repoRoot: context.topLevelPath
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
await git.cherryPick(commits)
|
|
413
|
+
} catch (error) {
|
|
414
|
+
throw new IcodeError(
|
|
415
|
+
'迁移失败: cherry-pick 发生冲突。请先解决冲突后执行 `git cherry-pick --continue`,或执行 `git cherry-pick --abort` 回滚。',
|
|
416
|
+
{
|
|
417
|
+
code: 'MIGRATE_CHERRY_PICK_FAILED',
|
|
418
|
+
cause: error,
|
|
419
|
+
meta: error.meta
|
|
420
|
+
}
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (options.push) {
|
|
425
|
+
await git.push(targetBranch, {
|
|
426
|
+
setUpstream: !targetExistsRemote,
|
|
427
|
+
noVerify: options.noVerify
|
|
428
|
+
})
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
sourceBranch,
|
|
433
|
+
targetBranch,
|
|
434
|
+
migratedCommits: commits.length,
|
|
435
|
+
rangeSpec: effectiveRangeSpec,
|
|
436
|
+
rangeMode,
|
|
437
|
+
pushed: Boolean(options.push),
|
|
438
|
+
repoRoot: context.topLevelPath
|
|
439
|
+
}
|
|
440
|
+
} finally {
|
|
441
|
+
if (originalBranch && originalBranch !== targetBranch) {
|
|
442
|
+
try {
|
|
443
|
+
await git.checkout(originalBranch)
|
|
444
|
+
} catch (error) {
|
|
445
|
+
logger.warn(`未能自动切回原分支 ${originalBranch}: ${error.message}`)
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|