@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.
Files changed (46) hide show
  1. package/README.md +346 -0
  2. package/bin/icode.js +6 -0
  3. package/package.json +34 -0
  4. package/src/cli.js +131 -0
  5. package/src/commands/ai.js +287 -0
  6. package/src/commands/checkout.js +59 -0
  7. package/src/commands/clean.js +65 -0
  8. package/src/commands/codereview.js +52 -0
  9. package/src/commands/config.js +513 -0
  10. package/src/commands/explain.js +80 -0
  11. package/src/commands/help.js +49 -0
  12. package/src/commands/info.js +57 -0
  13. package/src/commands/migrate.js +86 -0
  14. package/src/commands/push.js +125 -0
  15. package/src/commands/sync.js +74 -0
  16. package/src/commands/tag.js +53 -0
  17. package/src/commands/undo.js +66 -0
  18. package/src/core/ai-client.js +1125 -0
  19. package/src/core/ai-commit-summary.js +18 -0
  20. package/src/core/ai-config.js +342 -0
  21. package/src/core/ai-diff-range.js +117 -0
  22. package/src/core/args.js +47 -0
  23. package/src/core/commit-conventions.js +169 -0
  24. package/src/core/config-store.js +194 -0
  25. package/src/core/errors.js +25 -0
  26. package/src/core/git-context.js +105 -0
  27. package/src/core/git-service.js +428 -0
  28. package/src/core/hook-diagnostics.js +23 -0
  29. package/src/core/loading.js +36 -0
  30. package/src/core/logger.js +55 -0
  31. package/src/core/prompts.js +152 -0
  32. package/src/core/shell.js +77 -0
  33. package/src/workflows/ai-codereview-workflow.js +126 -0
  34. package/src/workflows/ai-commit-workflow.js +128 -0
  35. package/src/workflows/ai-conflict-workflow.js +102 -0
  36. package/src/workflows/ai-explain-workflow.js +116 -0
  37. package/src/workflows/ai-risk-review-workflow.js +49 -0
  38. package/src/workflows/checkout-workflow.js +85 -0
  39. package/src/workflows/clean-workflow.js +131 -0
  40. package/src/workflows/info-workflow.js +30 -0
  41. package/src/workflows/migrate-workflow.js +449 -0
  42. package/src/workflows/push-workflow.js +276 -0
  43. package/src/workflows/rollback-workflow.js +84 -0
  44. package/src/workflows/sync-workflow.js +141 -0
  45. package/src/workflows/tag-workflow.js +64 -0
  46. 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
+ }