@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,276 @@
|
|
|
1
|
+
import { getRepoPolicy } from '../core/config-store.js'
|
|
2
|
+
import { IcodeError } from '../core/errors.js'
|
|
3
|
+
import { GitService } from '../core/git-service.js'
|
|
4
|
+
import { resolveGitContext } from '../core/git-context.js'
|
|
5
|
+
import { formatAiCommitSummary } from '../core/ai-commit-summary.js'
|
|
6
|
+
import { logger } from '../core/logger.js'
|
|
7
|
+
import { confirm, input } from '../core/prompts.js'
|
|
8
|
+
import { runAiCommitWorkflow } from './ai-commit-workflow.js'
|
|
9
|
+
|
|
10
|
+
function uniqueBranches(branches) {
|
|
11
|
+
return [...new Set(branches.map((item) => item.trim()).filter(Boolean))]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Run AI commit before push and keep the full generated message in follow-up logs.
|
|
15
|
+
async function prepareAiCommitIfEnabled(inputOptions) {
|
|
16
|
+
if (!inputOptions.aiCommit) {
|
|
17
|
+
return {
|
|
18
|
+
enabled: false
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (inputOptions.message?.trim()) {
|
|
23
|
+
logger.warn('--ai-commit 已启用,将优先使用 AI 生成的提交信息。')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const result = await runAiCommitWorkflow({
|
|
28
|
+
apply: true,
|
|
29
|
+
lang: inputOptions.aiCommitLang || 'zh',
|
|
30
|
+
profile: inputOptions.aiProfile,
|
|
31
|
+
repoMode: inputOptions.repoMode,
|
|
32
|
+
noVerify: inputOptions.noVerify,
|
|
33
|
+
yes: inputOptions.yes,
|
|
34
|
+
cwd: inputOptions.cwd,
|
|
35
|
+
silentContextLog: true
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
if (result.canceled) {
|
|
39
|
+
logger.warn('已取消 AI 自动提交。')
|
|
40
|
+
return {
|
|
41
|
+
enabled: true,
|
|
42
|
+
canceled: true
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
logger.success(`AI 自动提交完成:\n${formatAiCommitSummary(result.commitId, result.commitMessage)}`)
|
|
47
|
+
return {
|
|
48
|
+
enabled: true,
|
|
49
|
+
applied: true,
|
|
50
|
+
commitId: result.commitId,
|
|
51
|
+
commitMessage: result.commitMessage
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error?.code === 'AI_COMMIT_EMPTY_DIFF') {
|
|
55
|
+
logger.info('未检测到可提交改动,跳过 --ai-commit。')
|
|
56
|
+
return {
|
|
57
|
+
enabled: true,
|
|
58
|
+
skipped: true,
|
|
59
|
+
reason: 'no-diff'
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
throw error
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function prepareCommitIfNeeded(git, options) {
|
|
68
|
+
const hasChanges = await git.hasChanges()
|
|
69
|
+
if (!hasChanges) {
|
|
70
|
+
logger.info('工作区无改动,跳过 commit。')
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let message = options.message?.trim()
|
|
75
|
+
if (!message) {
|
|
76
|
+
message = (await input('请输入提交信息', '')).trim()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!message) {
|
|
80
|
+
throw new IcodeError('检测到代码改动但未提供提交信息,请使用 -m 或 --message。', {
|
|
81
|
+
code: 'PUSH_COMMIT_MESSAGE_REQUIRED',
|
|
82
|
+
exitCode: 2
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 统一自动暂存,降低同学手动 add 的负担。
|
|
87
|
+
await git.stageAll()
|
|
88
|
+
await git.commit(message, {
|
|
89
|
+
noVerify: options.noVerify
|
|
90
|
+
})
|
|
91
|
+
logger.success(`提交完成: ${message}`)
|
|
92
|
+
return true
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function checkoutTargetBranch(git, targetBranch, sourceBranch) {
|
|
96
|
+
const localExists = await git.branchExistsLocal(targetBranch)
|
|
97
|
+
const remoteExists = await git.branchExistsRemote(targetBranch)
|
|
98
|
+
let checkoutMode = 'local'
|
|
99
|
+
|
|
100
|
+
if (localExists) {
|
|
101
|
+
await git.checkout(targetBranch)
|
|
102
|
+
} else if (remoteExists) {
|
|
103
|
+
await git.checkoutTracking(targetBranch)
|
|
104
|
+
checkoutMode = 'tracking'
|
|
105
|
+
} else {
|
|
106
|
+
// 目标分支不存在时,默认从 source 分支切出,方便“临时发布分支”场景。
|
|
107
|
+
logger.warn(`目标分支 ${targetBranch} 不存在,将从 ${sourceBranch} 创建。`)
|
|
108
|
+
await git.checkoutNewBranch(targetBranch, sourceBranch)
|
|
109
|
+
checkoutMode = 'created'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (remoteExists) {
|
|
113
|
+
await git.pull(targetBranch, {
|
|
114
|
+
allowUnrelatedHistories: true,
|
|
115
|
+
noRebase: true
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
remoteExists,
|
|
121
|
+
checkoutMode
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function runPushWorkflow(inputOptions) {
|
|
126
|
+
const context = await resolveGitContext({
|
|
127
|
+
cwd: inputOptions.cwd,
|
|
128
|
+
repoMode: inputOptions.repoMode
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const git = new GitService(context)
|
|
132
|
+
const policy = getRepoPolicy(context.topLevelPath)
|
|
133
|
+
const protectedBranches = new Set((policy.protectedBranches || []).map((item) => item.trim()).filter(Boolean))
|
|
134
|
+
|
|
135
|
+
logger.info(`仓库根目录: ${context.topLevelPath}`)
|
|
136
|
+
if (context.inheritedFromParent) {
|
|
137
|
+
logger.warn('当前目录继承了父级 Git 仓库,命令将基于父仓库根目录执行。')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const currentBranch = (await git.getCurrentBranch()) || context.currentBranch
|
|
141
|
+
if (!currentBranch) {
|
|
142
|
+
throw new IcodeError('无法识别当前分支,请检查仓库状态。', {
|
|
143
|
+
code: 'PUSH_BRANCH_UNKNOWN',
|
|
144
|
+
exitCode: 2
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const aiCommitResult = await prepareAiCommitIfEnabled(inputOptions)
|
|
149
|
+
if (aiCommitResult.canceled) {
|
|
150
|
+
return {
|
|
151
|
+
canceled: true,
|
|
152
|
+
reason: 'ai-commit-canceled',
|
|
153
|
+
repoRoot: context.topLevelPath,
|
|
154
|
+
currentBranch,
|
|
155
|
+
inheritedFromParent: context.inheritedFromParent
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const shouldRunManualCommit = !aiCommitResult.applied && aiCommitResult.reason !== 'no-diff'
|
|
160
|
+
if (shouldRunManualCommit) {
|
|
161
|
+
await prepareCommitIfNeeded(git, inputOptions)
|
|
162
|
+
}
|
|
163
|
+
await git.fetch()
|
|
164
|
+
|
|
165
|
+
if (inputOptions.pullMain && context.defaultBranch !== currentBranch) {
|
|
166
|
+
logger.info(`先同步主分支 ${context.defaultBranch} 到当前分支 ${currentBranch}`)
|
|
167
|
+
await git.pull(context.defaultBranch, {
|
|
168
|
+
allowUnrelatedHistories: true,
|
|
169
|
+
noRebase: true
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let branchTargets = uniqueBranches([
|
|
174
|
+
...(inputOptions.notPushCurrent ? [] : [currentBranch]),
|
|
175
|
+
...(inputOptions.targetBranches || [])
|
|
176
|
+
])
|
|
177
|
+
if (inputOptions.notPushCurrent) {
|
|
178
|
+
branchTargets = branchTargets.filter((branchName) => branchName !== currentBranch)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!branchTargets.length) {
|
|
182
|
+
throw new IcodeError('没有可执行的目标分支。', {
|
|
183
|
+
code: 'PUSH_EMPTY_TARGETS',
|
|
184
|
+
exitCode: 2
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!inputOptions.yes) {
|
|
189
|
+
const confirmed = await confirm(
|
|
190
|
+
`确认将 ${currentBranch} 推送/合并到以下分支: ${branchTargets.join(', ')} ?`,
|
|
191
|
+
true
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
if (!confirmed) {
|
|
195
|
+
logger.warn('已取消执行。')
|
|
196
|
+
return {
|
|
197
|
+
canceled: true,
|
|
198
|
+
branchTargets,
|
|
199
|
+
repoRoot: context.topLevelPath
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const summary = []
|
|
205
|
+
const originalBranch = currentBranch
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
for (const targetBranch of branchTargets) {
|
|
209
|
+
if (protectedBranches.has(targetBranch) && !inputOptions.forceProtected) {
|
|
210
|
+
logger.warn(`跳过受保护分支: ${targetBranch}(可用 --force-protected 覆盖)`)
|
|
211
|
+
summary.push({ branch: targetBranch, status: 'skipped-protected' })
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
logger.info(`处理分支: ${targetBranch}`)
|
|
216
|
+
|
|
217
|
+
if (targetBranch === currentBranch) {
|
|
218
|
+
const remoteExists = await git.branchExistsRemote(targetBranch)
|
|
219
|
+
if (remoteExists) {
|
|
220
|
+
logger.info(`同步远程分支: ${targetBranch}`)
|
|
221
|
+
await git.pull(targetBranch, {
|
|
222
|
+
allowUnrelatedHistories: true,
|
|
223
|
+
noRebase: true
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
logger.info(`推送当前分支: ${targetBranch}`)
|
|
228
|
+
await git.push(targetBranch, {
|
|
229
|
+
setUpstream: !remoteExists,
|
|
230
|
+
noVerify: inputOptions.noVerify
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
logger.success(`推送成功: ${targetBranch}`)
|
|
234
|
+
summary.push({ branch: targetBranch, status: 'pushed' })
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
logger.info(`切换到目标分支: ${targetBranch}`)
|
|
239
|
+
const { remoteExists, checkoutMode } = await checkoutTargetBranch(git, targetBranch, currentBranch)
|
|
240
|
+
logger.info(`目标分支准备完成: ${targetBranch} (${checkoutMode})`)
|
|
241
|
+
|
|
242
|
+
// 保留 merge commit,方便后续追溯“从哪个分支合并过来”。
|
|
243
|
+
logger.info(`合并分支: ${currentBranch} -> ${targetBranch}`)
|
|
244
|
+
await git.merge(currentBranch, {
|
|
245
|
+
noFf: true,
|
|
246
|
+
noEdit: true
|
|
247
|
+
})
|
|
248
|
+
logger.success(`合并成功: ${currentBranch} -> ${targetBranch}`)
|
|
249
|
+
|
|
250
|
+
logger.info(`推送目标分支: ${targetBranch}`)
|
|
251
|
+
await git.push(targetBranch, {
|
|
252
|
+
setUpstream: !remoteExists,
|
|
253
|
+
noVerify: inputOptions.noVerify
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
logger.success(`目标分支推送成功: ${targetBranch}`)
|
|
257
|
+
summary.push({ branch: targetBranch, status: 'merged-and-pushed' })
|
|
258
|
+
}
|
|
259
|
+
} finally {
|
|
260
|
+
const branchAfterWorkflow = await git.getCurrentBranch()
|
|
261
|
+
if (branchAfterWorkflow && branchAfterWorkflow !== originalBranch) {
|
|
262
|
+
try {
|
|
263
|
+
await git.checkout(originalBranch)
|
|
264
|
+
} catch (error) {
|
|
265
|
+
logger.warn(`未能自动切回原分支 ${originalBranch}: ${error.message}`)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
repoRoot: context.topLevelPath,
|
|
272
|
+
currentBranch,
|
|
273
|
+
summary,
|
|
274
|
+
inheritedFromParent: context.inheritedFromParent
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
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 { confirm } from '../core/prompts.js'
|
|
6
|
+
|
|
7
|
+
function isRevertConflict(error) {
|
|
8
|
+
const output = `${error?.meta?.stdout || ''}\n${error?.meta?.stderr || ''}\n${error?.message || ''}`
|
|
9
|
+
return /revert is already in progress|could not revert|after resolving the conflicts|CONFLICT \(/i.test(output)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function runRollbackWorkflow(options) {
|
|
13
|
+
const mode = options.mode || 'revert'
|
|
14
|
+
const ref = options.ref || (mode === 'revert' ? 'HEAD' : 'HEAD~1')
|
|
15
|
+
|
|
16
|
+
const context = await resolveGitContext({
|
|
17
|
+
cwd: options.cwd,
|
|
18
|
+
repoMode: options.repoMode
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const git = new GitService(context)
|
|
22
|
+
|
|
23
|
+
logger.info(`仓库根目录: ${context.topLevelPath}`)
|
|
24
|
+
if (context.inheritedFromParent) {
|
|
25
|
+
logger.warn('当前目录继承了父级 Git 仓库,命令将基于父仓库根目录执行。')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (mode === 'revert') {
|
|
29
|
+
logger.info(`执行回滚(revert): ${ref}`)
|
|
30
|
+
try {
|
|
31
|
+
await git.revert(ref)
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (isRevertConflict(error)) {
|
|
34
|
+
throw new IcodeError(
|
|
35
|
+
'revert 发生冲突。请解决冲突后执行 `git revert --continue`,或使用 `icode undo --recover abort` 直接中止。',
|
|
36
|
+
{
|
|
37
|
+
code: 'REVERT_CONFLICT',
|
|
38
|
+
cause: error,
|
|
39
|
+
meta: error.meta
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
throw error
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
mode,
|
|
47
|
+
ref,
|
|
48
|
+
repoRoot: context.topLevelPath
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!['soft', 'mixed', 'hard'].includes(mode)) {
|
|
53
|
+
throw new IcodeError('mode 仅支持: revert | soft | mixed | hard', {
|
|
54
|
+
code: 'ROLLBACK_MODE_INVALID',
|
|
55
|
+
exitCode: 2
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (mode === 'hard' && !options.yes) {
|
|
60
|
+
// hard reset 会直接丢弃工作区改动,这里强制确认一次降低误操作风险。
|
|
61
|
+
const accepted = await confirm(
|
|
62
|
+
`你将执行 git reset --hard ${ref},这会丢失未提交改动,是否继续?`,
|
|
63
|
+
false
|
|
64
|
+
)
|
|
65
|
+
if (!accepted) {
|
|
66
|
+
logger.warn('已取消 hard 回滚。')
|
|
67
|
+
return {
|
|
68
|
+
canceled: true,
|
|
69
|
+
mode,
|
|
70
|
+
ref,
|
|
71
|
+
repoRoot: context.topLevelPath
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
logger.info(`执行回滚(reset --${mode}): ${ref}`)
|
|
77
|
+
await git.reset(mode, ref)
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
mode,
|
|
81
|
+
ref,
|
|
82
|
+
repoRoot: context.topLevelPath
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
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 { confirm } from '../core/prompts.js'
|
|
6
|
+
|
|
7
|
+
function unique(list) {
|
|
8
|
+
return [...new Set(list.map((item) => item.trim()).filter(Boolean))]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function ensureBranchReady(git, branchName) {
|
|
12
|
+
const localExists = await git.branchExistsLocal(branchName)
|
|
13
|
+
const remoteExists = await git.branchExistsRemote(branchName)
|
|
14
|
+
|
|
15
|
+
if (!localExists && !remoteExists) {
|
|
16
|
+
return {
|
|
17
|
+
available: false,
|
|
18
|
+
localExists,
|
|
19
|
+
remoteExists
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (localExists) {
|
|
24
|
+
await git.checkout(branchName)
|
|
25
|
+
} else {
|
|
26
|
+
await git.checkoutTracking(branchName)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
available: true,
|
|
31
|
+
localExists,
|
|
32
|
+
remoteExists
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function runSyncWorkflow(options) {
|
|
37
|
+
const context = await resolveGitContext({
|
|
38
|
+
cwd: options.cwd,
|
|
39
|
+
repoMode: options.repoMode
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const git = new GitService(context)
|
|
43
|
+
const currentBranch = (await git.getCurrentBranch()) || context.currentBranch
|
|
44
|
+
|
|
45
|
+
let targetInput = options.branches?.length
|
|
46
|
+
? [...options.branches]
|
|
47
|
+
: [currentBranch, context.defaultBranch]
|
|
48
|
+
|
|
49
|
+
if (options.allLocal) {
|
|
50
|
+
// all-local 场景下自动拉取全部本地分支,降低人工逐个输入分支名的成本。
|
|
51
|
+
const localBranches = await git.listLocalBranches()
|
|
52
|
+
targetInput = [...localBranches, ...targetInput]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const targets = unique(targetInput)
|
|
56
|
+
|
|
57
|
+
if (!targets.length) {
|
|
58
|
+
throw new IcodeError('没有可同步的分支。', {
|
|
59
|
+
code: 'SYNC_EMPTY_TARGETS',
|
|
60
|
+
exitCode: 2
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
logger.info(`仓库根目录: ${context.topLevelPath}`)
|
|
65
|
+
if (context.inheritedFromParent) {
|
|
66
|
+
logger.warn('当前目录继承了父级 Git 仓库,命令将基于父仓库根目录执行。')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!options.yes) {
|
|
70
|
+
const ok = await confirm(`确认同步以下分支: ${targets.join(', ')} ?`, true)
|
|
71
|
+
if (!ok) {
|
|
72
|
+
logger.warn('已取消同步。')
|
|
73
|
+
return {
|
|
74
|
+
canceled: true,
|
|
75
|
+
targets,
|
|
76
|
+
repoRoot: context.topLevelPath
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const summary = []
|
|
82
|
+
await git.fetch()
|
|
83
|
+
const originalBranch = currentBranch
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
for (const branchName of targets) {
|
|
87
|
+
logger.info(`同步分支: ${branchName}`)
|
|
88
|
+
const setup = await ensureBranchReady(git, branchName)
|
|
89
|
+
|
|
90
|
+
if (!setup.available) {
|
|
91
|
+
logger.warn(`分支不存在(本地+远程): ${branchName}`)
|
|
92
|
+
summary.push({
|
|
93
|
+
branch: branchName,
|
|
94
|
+
status: 'missing'
|
|
95
|
+
})
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (setup.remoteExists) {
|
|
100
|
+
await git.pull(branchName, {
|
|
101
|
+
noRebase: !options.rebase,
|
|
102
|
+
allowUnrelatedHistories: true
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (options.mergeMain && branchName !== context.defaultBranch) {
|
|
107
|
+
// 把最新主分支合入目标分支,降低后续提测/发布时的冲突概率。
|
|
108
|
+
await git.merge(context.defaultBranch, {
|
|
109
|
+
noFf: true,
|
|
110
|
+
noEdit: true
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (options.push) {
|
|
115
|
+
await git.push(branchName, {
|
|
116
|
+
setUpstream: !setup.remoteExists,
|
|
117
|
+
noVerify: options.noVerify
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
summary.push({
|
|
122
|
+
branch: branchName,
|
|
123
|
+
status: options.push ? 'synced-and-pushed' : 'synced'
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
} finally {
|
|
127
|
+
if (originalBranch) {
|
|
128
|
+
try {
|
|
129
|
+
await git.checkout(originalBranch)
|
|
130
|
+
} catch (error) {
|
|
131
|
+
logger.warn(`未能自动切回原分支 ${originalBranch}: ${error.message}`)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
repoRoot: context.topLevelPath,
|
|
138
|
+
targets,
|
|
139
|
+
summary
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { IcodeError } from '../core/errors.js'
|
|
2
|
+
import { GitService } from '../core/git-service.js'
|
|
3
|
+
import { resolveGitContext } from '../core/git-context.js'
|
|
4
|
+
import { logger } from '../core/logger.js'
|
|
5
|
+
import { input } from '../core/prompts.js'
|
|
6
|
+
|
|
7
|
+
function todayStamp() {
|
|
8
|
+
const now = new Date()
|
|
9
|
+
const year = String(now.getFullYear())
|
|
10
|
+
const month = String(now.getMonth() + 1).padStart(2, '0')
|
|
11
|
+
const date = String(now.getDate()).padStart(2, '0')
|
|
12
|
+
return `${year}${month}${date}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function nextDailyTag(existingTags, dateStamp) {
|
|
16
|
+
const prefix = `v${dateStamp}_`
|
|
17
|
+
const serials = existingTags
|
|
18
|
+
.filter((tag) => tag.startsWith(prefix))
|
|
19
|
+
.map((tag) => Number(tag.split('_')[1]))
|
|
20
|
+
.filter((value) => Number.isFinite(value))
|
|
21
|
+
|
|
22
|
+
const max = serials.length ? Math.max(...serials) : 0
|
|
23
|
+
const next = String(max + 1).padStart(2, '0')
|
|
24
|
+
return `${prefix}${next}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function runTagWorkflow(options) {
|
|
28
|
+
const context = await resolveGitContext({
|
|
29
|
+
cwd: options.cwd,
|
|
30
|
+
repoMode: options.repoMode
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const git = new GitService(context)
|
|
34
|
+
const dateStamp = todayStamp()
|
|
35
|
+
const candidateTags = await git.listTags(`v${dateStamp}_*`)
|
|
36
|
+
|
|
37
|
+
let tagName = options.tagName?.trim() || nextDailyTag(candidateTags, dateStamp)
|
|
38
|
+
if (!options.tagName) {
|
|
39
|
+
tagName = (await input('请输入 tag 名称', tagName)).trim() || tagName
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!tagName) {
|
|
43
|
+
throw new IcodeError('tag 名称不能为空。', {
|
|
44
|
+
code: 'TAG_NAME_REQUIRED',
|
|
45
|
+
exitCode: 2
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const message = options.message?.trim() || `release: ${tagName}`
|
|
50
|
+
|
|
51
|
+
logger.info(`创建 tag: ${tagName}`)
|
|
52
|
+
await git.createAnnotatedTag(tagName, message, options.fromRef)
|
|
53
|
+
|
|
54
|
+
logger.info(`推送 tag: ${tagName}`)
|
|
55
|
+
await git.pushTag(tagName, {
|
|
56
|
+
noVerify: options.noVerify
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
repoRoot: context.topLevelPath,
|
|
61
|
+
tagName,
|
|
62
|
+
message
|
|
63
|
+
}
|
|
64
|
+
}
|