@icode-js/icode 3.2.0 → 3.4.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 +287 -24
- package/bin/icode.js +10 -0
- package/package.json +3 -3
- package/src/cli.js +175 -8
- package/src/commands/checkout.js +73 -6
- package/src/commands/clean.js +47 -7
- package/src/commands/config-ux.js +292 -0
- package/src/commands/config.js +199 -48
- package/src/commands/continue.js +104 -0
- package/src/commands/doctor.js +15 -29
- package/src/commands/help.js +54 -34
- package/src/commands/history.js +147 -0
- package/src/commands/info.js +39 -4
- package/src/commands/log.js +142 -0
- package/src/commands/migrate.js +19 -12
- package/src/commands/open.js +138 -0
- package/src/commands/pr.js +219 -0
- package/src/commands/push.js +62 -22
- package/src/commands/ready.js +18 -48
- package/src/commands/release.js +112 -0
- package/src/commands/rescue.js +164 -0
- package/src/commands/save.js +52 -11
- package/src/commands/stash.js +210 -0
- package/src/commands/sw.js +89 -0
- package/src/commands/sync.js +19 -2
- package/src/commands/tag.js +40 -1
- package/src/commands/undo.js +9 -3
- package/src/core/ai/diff-range.js +1 -1
- package/src/core/cli/completion.js +237 -1
- package/src/core/cli/output.js +46 -0
- package/src/core/cli/render.js +121 -0
- package/src/core/commands/shell.js +9 -1
- package/src/core/config/repo-config.js +93 -0
- package/src/core/config/store.js +60 -10
- package/src/core/git/autostash.js +46 -0
- package/src/core/git/backup.js +170 -0
- package/src/core/git/conflicts.js +58 -0
- package/src/core/git/context.js +72 -5
- package/src/core/git/file-guard.js +193 -0
- package/src/core/git/operation-state.js +63 -0
- package/src/core/git/operations.js +32 -0
- package/src/core/git/semver.js +194 -0
- package/src/core/git/service.js +454 -24
- package/src/core/platform/http.js +53 -0
- package/src/core/platform/index.js +165 -0
- package/src/core/platform/providers.js +454 -0
- package/src/core/platform/remote-url.js +92 -0
- package/src/core/remote-merge/client.js +33 -3
- package/src/core/tools/browser.js +28 -0
- package/src/core/tools/commit-prompt.js +137 -0
- package/src/core/tools/interactive.js +272 -40
- package/src/core/tools/logger.js +33 -23
- package/src/core/tools/theme.js +228 -0
- package/src/workflows/ai-commit-workflow.js +52 -20
- package/src/workflows/checkout-workflow.js +24 -10
- package/src/workflows/clean-workflow.js +135 -83
- package/src/workflows/continue-workflow.js +233 -0
- package/src/workflows/doctor-workflow.js +10 -7
- package/src/workflows/history-workflow.js +211 -0
- package/src/workflows/migrate-workflow.js +39 -14
- package/src/workflows/pr-workflow.js +252 -0
- package/src/workflows/push/commit-step.js +123 -0
- package/src/workflows/push/local-merge-step.js +213 -0
- package/src/workflows/push/plan.js +63 -0
- package/src/workflows/push/remote-merge-step.js +224 -0
- package/src/workflows/push-workflow.js +185 -492
- package/src/workflows/ready-workflow.js +14 -10
- package/src/workflows/release-workflow.js +189 -0
- package/src/workflows/rescue-workflow.js +137 -0
- package/src/workflows/save-workflow.js +106 -41
- package/src/workflows/switch-workflow.js +171 -0
- package/src/workflows/sync-workflow.js +143 -44
- package/src/workflows/tag-workflow.js +103 -17
- package/src/workflows/undo-workflow.js +113 -34
- package/src/workflows/rollback-workflow.js +0 -99
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { getRepoPolicy } from '../core/config/store.js'
|
|
2
|
+
import { ui, shortenHome } from '../core/tools/theme.js'
|
|
3
3
|
import { IcodeError } from '../core/errors.js'
|
|
4
|
+
import { createBackup } from '../core/git/backup.js'
|
|
5
|
+
import { saveOperationState } from '../core/git/operation-state.js'
|
|
4
6
|
import { resolveGitContext } from '../core/git/context.js'
|
|
5
7
|
import {
|
|
6
8
|
assertNoConflictedFiles,
|
|
@@ -8,448 +10,134 @@ import {
|
|
|
8
10
|
readGitSafetyState
|
|
9
11
|
} from '../core/git/preconditions.js'
|
|
10
12
|
import { GitService } from '../core/git/service.js'
|
|
11
|
-
import { confirm,
|
|
12
|
-
import { requestRemoteMerge, resolveRemoteMergeProjectUrl } from '../core/remote-merge/client.js'
|
|
13
|
+
import { chooseMany, chooseOne, confirm, isInteractiveTerminal } from '../core/tools/interactive.js'
|
|
13
14
|
import { logger } from '../core/tools/logger.js'
|
|
14
|
-
import {
|
|
15
|
+
import { prepareAiCommitIfEnabled, prepareCommitIfNeeded } from './push/commit-step.js'
|
|
16
|
+
import { buildLocalMergePauseError, mergeIntoTargetAndPush, pushCurrentBranch } from './push/local-merge-step.js'
|
|
17
|
+
import { buildPushDryRunPlan } from './push/plan.js'
|
|
18
|
+
import {
|
|
19
|
+
buildRemoteMergeBatchPauseError,
|
|
20
|
+
normalizeRemoteMergePolicy,
|
|
21
|
+
resolveRemoteMergePlatform,
|
|
22
|
+
runConcurrentRemoteMerges
|
|
23
|
+
} from './push/remote-merge-step.js'
|
|
24
|
+
|
|
25
|
+
// push 只负责编排:提交 -> 同步主分支 -> 逐个处理目标分支(推送当前 / 本地 merge / 远程合并)。
|
|
26
|
+
// 各阶段的具体实现见 ./push/ 目录。
|
|
15
27
|
|
|
16
28
|
function uniqueBranches(branches) {
|
|
17
29
|
return [...new Set(branches.map((item) => item.trim()).filter(Boolean))]
|
|
18
30
|
}
|
|
19
31
|
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
// 合并配置里的受保护分支:全局配置 + 仓库 .icoderc。
|
|
33
|
+
function resolveProtectedBranches(policy, context) {
|
|
34
|
+
return new Set(
|
|
35
|
+
[...(policy.protectedBranches || []), ...(context.repoConfig?.protectedBranches || [])]
|
|
36
|
+
.map((item) => String(item).trim())
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
)
|
|
39
|
+
}
|
|
24
40
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
41
|
+
/**
|
|
42
|
+
* 强推只允许作用在当前分支,并拒绝主分支和受保护分支(除非 --force-protected)。
|
|
43
|
+
*/
|
|
44
|
+
function validateForcePush(inputOptions, branchTargets, currentBranch, context, protectedBranches) {
|
|
45
|
+
if (!inputOptions.force) {
|
|
46
|
+
return
|
|
30
47
|
}
|
|
31
48
|
|
|
32
|
-
if (
|
|
33
|
-
throw new IcodeError(
|
|
34
|
-
code: '
|
|
49
|
+
if (inputOptions.remoteMerge || branchTargets.some((branch) => branch !== currentBranch)) {
|
|
50
|
+
throw new IcodeError('--force 只能用于推送当前分支,不能和目标分支或远程合并一起使用。', {
|
|
51
|
+
code: 'PUSH_FORCE_SCOPE_INVALID',
|
|
35
52
|
exitCode: 2
|
|
36
53
|
})
|
|
37
54
|
}
|
|
38
55
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return `${commit.subject}\n\n${commit.body}`
|
|
44
|
-
}
|
|
45
|
-
return commit.subject
|
|
46
|
-
})
|
|
47
|
-
.join('\n\n')
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
title: latestCommit.subject,
|
|
51
|
-
description
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// 提取远程合并失败原因,优先使用 IcodeError.meta,其次回退到 error.message。
|
|
56
|
-
function resolveRemoteMergeFailureReason(error) {
|
|
57
|
-
const metaStep = typeof error?.meta?.step === 'string' ? error.meta.step.trim() : ''
|
|
58
|
-
const metaReason = typeof error?.meta?.reason === 'string' ? error.meta.reason.trim() : ''
|
|
59
|
-
if (metaReason) {
|
|
60
|
-
return metaStep ? `${metaReason} [step=${metaStep}]` : metaReason
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const message = typeof error?.message === 'string' ? error.message.trim() : ''
|
|
64
|
-
return message || '远程合并失败'
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Run AI commit before push and keep the full generated message in follow-up logs.
|
|
68
|
-
async function prepareAiCommitIfEnabled(inputOptions) {
|
|
69
|
-
if (!inputOptions.aiCommit) {
|
|
70
|
-
return {
|
|
71
|
-
enabled: false
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (inputOptions.message?.trim()) {
|
|
76
|
-
logger.warn('--ai-commit 已启用,将优先使用 AI 生成的提交信息。')
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
const result = await runAiCommitWorkflow({
|
|
81
|
-
apply: true,
|
|
82
|
-
lang: inputOptions.aiCommitLang || 'zh',
|
|
83
|
-
profile: inputOptions.aiProfile,
|
|
84
|
-
repoMode: inputOptions.repoMode,
|
|
85
|
-
noVerify: inputOptions.noVerify,
|
|
86
|
-
yes: inputOptions.yes,
|
|
87
|
-
cwd: inputOptions.cwd,
|
|
88
|
-
silentContextLog: true
|
|
56
|
+
if (!branchTargets.includes(currentBranch)) {
|
|
57
|
+
throw new IcodeError('--force 需要推送当前分支,不能和 --not-push-current 一起使用。', {
|
|
58
|
+
code: 'PUSH_FORCE_SCOPE_INVALID',
|
|
59
|
+
exitCode: 2
|
|
89
60
|
})
|
|
90
|
-
|
|
91
|
-
if (result.canceled) {
|
|
92
|
-
logger.warn('已取消 AI 自动提交。')
|
|
93
|
-
return {
|
|
94
|
-
enabled: true,
|
|
95
|
-
canceled: true
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
logger.success(`AI 自动提交完成:\n${formatAiCommitSummary(result.commitId, result.commitMessage)}`)
|
|
100
|
-
return {
|
|
101
|
-
enabled: true,
|
|
102
|
-
applied: true,
|
|
103
|
-
commitId: result.commitId,
|
|
104
|
-
commitMessage: result.commitMessage
|
|
105
|
-
}
|
|
106
|
-
} catch (error) {
|
|
107
|
-
if (error?.code === 'AI_COMMIT_EMPTY_DIFF') {
|
|
108
|
-
logger.info('未检测到可提交改动,跳过 --ai-commit。')
|
|
109
|
-
return {
|
|
110
|
-
enabled: true,
|
|
111
|
-
skipped: true,
|
|
112
|
-
reason: 'no-diff'
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
throw error
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// 在 push 场景下按需创建本地提交,输入 GitService、命令参数和已读取的改动摘要,输出是否提交。
|
|
121
|
-
async function prepareCommitIfNeeded(git, options, changes) {
|
|
122
|
-
if (changes.clean) {
|
|
123
|
-
logger.info('工作区无改动,跳过 commit。')
|
|
124
|
-
return false
|
|
125
61
|
}
|
|
126
62
|
|
|
127
|
-
|
|
128
|
-
if (!
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (!message) {
|
|
133
|
-
throw new IcodeError('检测到代码改动但未提供提交信息,请使用 -m 或 --message。', {
|
|
134
|
-
code: 'PUSH_COMMIT_MESSAGE_REQUIRED',
|
|
63
|
+
const isGuarded = currentBranch === context.defaultBranch || protectedBranches.has(currentBranch)
|
|
64
|
+
if (isGuarded && !inputOptions.forceProtected) {
|
|
65
|
+
throw new IcodeError(`拒绝强推主分支/受保护分支: ${currentBranch}。确有必要请加 --force-protected。`, {
|
|
66
|
+
code: 'PUSH_FORCE_PROTECTED',
|
|
135
67
|
exitCode: 2
|
|
136
68
|
})
|
|
137
69
|
}
|
|
138
|
-
|
|
139
|
-
// 统一自动暂存,降低同学手动 add 的负担。
|
|
140
|
-
await git.stageAll()
|
|
141
|
-
await git.commit(message, {
|
|
142
|
-
noVerify: options.noVerify
|
|
143
|
-
})
|
|
144
|
-
logger.success(`提交完成: ${message}`)
|
|
145
|
-
return true
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// 提取底层 Git 失败细节,输入异常对象,输出用于暂停错误的简洁原因。
|
|
149
|
-
function resolveGitFailureReason(error) {
|
|
150
|
-
const stderr = typeof error?.meta?.stderr === 'string' ? error.meta.stderr.trim() : ''
|
|
151
|
-
const stdout = typeof error?.meta?.stdout === 'string' ? error.meta.stdout.trim() : ''
|
|
152
|
-
const output = [stderr, stdout].filter(Boolean).join('\n').trim()
|
|
153
|
-
const message = typeof error?.message === 'string' ? error.message.trim() : ''
|
|
154
|
-
const reason = output || message || 'Git 操作失败'
|
|
155
|
-
|
|
156
|
-
return reason.length > 2000 ? `${reason.slice(0, 2000)}...` : reason
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// 判断 Git 失败是否来自合并冲突,输入异常对象,输出布尔结果。
|
|
160
|
-
function isGitConflictFailure(error) {
|
|
161
|
-
const text = [
|
|
162
|
-
error?.meta?.stderr || '',
|
|
163
|
-
error?.meta?.stdout || '',
|
|
164
|
-
error?.message || ''
|
|
165
|
-
].join('\n')
|
|
166
|
-
|
|
167
|
-
return /CONFLICT \(|Automatic merge failed|fix conflicts|unmerged files|needs merge|you need to resolve your current index first/i.test(text)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// 构建本地合并暂停错误,输入分支、阶段和原始异常,输出可被命令层汇总的 IcodeError。
|
|
171
|
-
function buildLocalMergePauseError({ sourceBranch, targetBranch, step, error, summary = [] }) {
|
|
172
|
-
const reason = resolveGitFailureReason(error)
|
|
173
|
-
const conflict = isGitConflictFailure(error)
|
|
174
|
-
const prefix = conflict ? '本地合并冲突' : '本地合并失败'
|
|
175
|
-
const nextStep = conflict
|
|
176
|
-
? '请解决冲突后执行 `icode undo --recover continue`,或执行 `icode undo --recover abort` 中止本次合并。'
|
|
177
|
-
: '请根据上面的 Git 错误修复后重试。'
|
|
178
|
-
|
|
179
|
-
return new IcodeError(`${prefix}(${step}): ${sourceBranch} -> ${targetBranch}\n${reason}\n${nextStep}`, {
|
|
180
|
-
code: 'PUSH_LOCAL_MERGE_PAUSED',
|
|
181
|
-
exitCode: 2,
|
|
182
|
-
cause: error,
|
|
183
|
-
meta: {
|
|
184
|
-
sourceBranch,
|
|
185
|
-
targetBranch,
|
|
186
|
-
step,
|
|
187
|
-
reason,
|
|
188
|
-
summary
|
|
189
|
-
}
|
|
190
|
-
})
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async function checkoutTargetBranch(git, targetBranch, sourceBranch, summary = []) {
|
|
194
|
-
const localExists = await git.branchExistsLocal(targetBranch)
|
|
195
|
-
const remoteExists = await git.branchExistsRemote(targetBranch)
|
|
196
|
-
let checkoutMode = 'local'
|
|
197
|
-
|
|
198
|
-
if (localExists) {
|
|
199
|
-
await git.checkout(targetBranch)
|
|
200
|
-
} else if (remoteExists) {
|
|
201
|
-
await git.checkoutTracking(targetBranch)
|
|
202
|
-
checkoutMode = 'tracking'
|
|
203
|
-
} else {
|
|
204
|
-
// 目标分支不存在时,默认从 source 分支切出,方便“临时发布分支”场景。
|
|
205
|
-
logger.warn(`目标分支 ${targetBranch} 不存在,将从 ${sourceBranch} 创建。`)
|
|
206
|
-
await git.checkoutNewBranch(targetBranch, sourceBranch)
|
|
207
|
-
checkoutMode = 'created'
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (remoteExists) {
|
|
211
|
-
try {
|
|
212
|
-
await git.pull(targetBranch, {
|
|
213
|
-
noRebase: true
|
|
214
|
-
})
|
|
215
|
-
} catch (error) {
|
|
216
|
-
throw buildLocalMergePauseError({
|
|
217
|
-
sourceBranch,
|
|
218
|
-
targetBranch,
|
|
219
|
-
step: 'pull-target',
|
|
220
|
-
error,
|
|
221
|
-
summary
|
|
222
|
-
})
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return {
|
|
227
|
-
remoteExists,
|
|
228
|
-
checkoutMode
|
|
229
|
-
}
|
|
230
70
|
}
|
|
231
71
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
return
|
|
235
|
-
enabled: remoteMerge.enabled !== false
|
|
236
|
-
}
|
|
72
|
+
// 去掉会被跳过的受保护分支,只保留续做时真正要处理的分支。
|
|
73
|
+
function filterRunnable(branches, protectedBranches, inputOptions) {
|
|
74
|
+
return branches.filter((branch) => !(protectedBranches.has(branch) && !inputOptions.forceProtected))
|
|
237
75
|
}
|
|
238
76
|
|
|
239
|
-
//
|
|
240
|
-
function
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
reason,
|
|
254
|
-
...meta
|
|
255
|
-
}
|
|
256
|
-
})
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// 统一把 provider 返回的 step 注入错误文案,便于定位 create/merge/inspect 具体失败点。
|
|
260
|
-
function buildRemoteMergeResultMessage(prefix, result) {
|
|
261
|
-
const reason = typeof result?.reason === 'string' && result.reason.trim()
|
|
262
|
-
? result.reason.trim()
|
|
263
|
-
: '未知原因'
|
|
264
|
-
const step = typeof result?.step === 'string' && result.step.trim()
|
|
265
|
-
? result.step.trim()
|
|
266
|
-
: ''
|
|
267
|
-
|
|
268
|
-
return step ? `${prefix}(${step}): ${reason}` : `${prefix}: ${reason}`
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// 构建批量远程合并暂停错误,明确指出具体失败分支与原因。
|
|
272
|
-
function buildRemoteMergeBatchPauseError(failures, summary) {
|
|
273
|
-
const branchesLabel = failures.map((item) => item.branch).join(', ')
|
|
274
|
-
const detail = failures.map((item) => `${item.branch}: ${item.reason}`).join(' | ')
|
|
275
|
-
|
|
276
|
-
return new IcodeError(`远程合并已暂停(${branchesLabel}): ${detail}`, {
|
|
277
|
-
code: 'PUSH_REMOTE_MERGE_PAUSED',
|
|
278
|
-
exitCode: 2,
|
|
279
|
-
meta: {
|
|
280
|
-
failures,
|
|
281
|
-
summary
|
|
282
|
-
}
|
|
77
|
+
// 保存 push 中断时的进度:pendingPush 是已合并但还没推送的分支,remainingTargets 是还没处理的分支。
|
|
78
|
+
function savePushState(context, inputOptions, sourceBranch, pendingPush, remainingTargets, summary) {
|
|
79
|
+
saveOperationState(context, {
|
|
80
|
+
command: 'push',
|
|
81
|
+
sourceBranch,
|
|
82
|
+
originalBranch: sourceBranch,
|
|
83
|
+
pendingPush,
|
|
84
|
+
remainingTargets,
|
|
85
|
+
options: {
|
|
86
|
+
noVerify: Boolean(inputOptions.noVerify),
|
|
87
|
+
createTarget: Boolean(inputOptions.createTarget),
|
|
88
|
+
yes: true
|
|
89
|
+
},
|
|
90
|
+
summary: summary.slice()
|
|
283
91
|
})
|
|
284
92
|
}
|
|
285
93
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (targetBranch === currentBranch) {
|
|
323
|
-
steps.push({
|
|
324
|
-
action: 'push-current',
|
|
325
|
-
branch: targetBranch,
|
|
326
|
-
reason: '将同步并推送当前分支'
|
|
327
|
-
})
|
|
328
|
-
return
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
steps.push({
|
|
332
|
-
action: inputOptions.remoteMerge ? 'remote-merge' : 'local-merge',
|
|
333
|
-
branch: targetBranch,
|
|
334
|
-
reason: inputOptions.remoteMerge ? '将创建并合并远程 MR' : '将切换目标分支、本地 merge 后推送'
|
|
335
|
-
})
|
|
94
|
+
/**
|
|
95
|
+
* 交互选择推送目标:只推当前分支(默认)/ 同时本地合并到其他分支 / 通过 PR 合并。
|
|
96
|
+
* 输出 { targets, remoteMerge } 或 null(取消)。
|
|
97
|
+
*/
|
|
98
|
+
async function pickPushTargets(git, currentBranch, context, protectedBranches) {
|
|
99
|
+
const unpushed = await git.countCommits(`${git.remoteRef(currentBranch)}..HEAD`)
|
|
100
|
+
const hint = (await git.refExists(git.remoteRef(currentBranch)))
|
|
101
|
+
? (unpushed ? `${unpushed} 个提交待推送` : '远程已是最新')
|
|
102
|
+
: '远程还没有这个分支'
|
|
103
|
+
const mode = await chooseOne(`推送 ${ui.branch(currentBranch)}`, [
|
|
104
|
+
{ value: 'current', label: `只推送 ${currentBranch}`, hint },
|
|
105
|
+
{ value: 'local', label: '同时合并到其他分支(本地 merge)', hint: '例如 test / release' },
|
|
106
|
+
{ value: 'remote', label: '同时通过 PR/MR 合并到其他分支(远程合并)' },
|
|
107
|
+
{ value: 'cancel', label: '取消' }
|
|
108
|
+
], 0, { filter: false })
|
|
109
|
+
|
|
110
|
+
if (mode === 'cancel') {
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
if (mode === 'current') {
|
|
114
|
+
return { targets: [currentBranch], remoteMerge: false }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const names = [...new Set([...(await git.listLocalBranches()), ...(await git.listRemoteBranches())])]
|
|
118
|
+
.filter((name) => name && name !== currentBranch)
|
|
119
|
+
if (!names.length) {
|
|
120
|
+
logger.warn('没有其它分支可选,只推送当前分支。')
|
|
121
|
+
return { targets: [currentBranch], remoteMerge: false }
|
|
122
|
+
}
|
|
123
|
+
// 常见的部署分支排在前面。
|
|
124
|
+
const priority = ['test', 'dev', 'develop', 'staging', 'pre', 'uat', 'release', context.defaultBranch]
|
|
125
|
+
names.sort((a, b) => {
|
|
126
|
+
const pa = priority.indexOf(a) === -1 ? 99 : priority.indexOf(a)
|
|
127
|
+
const pb = priority.indexOf(b) === -1 ? 99 : priority.indexOf(b)
|
|
128
|
+
return pa - pb || a.localeCompare(b)
|
|
336
129
|
})
|
|
337
130
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
-
}
|
|
131
|
+
const picked = await chooseMany('选择要合并到的分支', names.map((name) => ({
|
|
132
|
+
value: name,
|
|
133
|
+
label: name,
|
|
134
|
+
hint: protectedBranches.has(name) ? '受保护,会被跳过(--force-protected 可覆盖)' : ''
|
|
135
|
+
})), { minSelections: 1, doneLabel: '开始推送' })
|
|
345
136
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
return {
|
|
349
|
-
projectUrl: resolveRemoteMergeProjectUrl({
|
|
350
|
-
provider: remoteMergePlatform.provider,
|
|
351
|
-
remoteUrl: remoteOriginUrl
|
|
352
|
-
}),
|
|
353
|
-
source: 'origin'
|
|
137
|
+
if (!picked) {
|
|
138
|
+
return null
|
|
354
139
|
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// 基于 origin 推导 GitLab 项目地址并发起固定 API 的 MR 创建请求。
|
|
358
|
-
async function performRemoteMerge({ git, currentBranch, targetBranch, remoteExists, inputOptions, remoteMergePolicy, remoteMergePlatform }) {
|
|
359
|
-
if (!remoteExists) {
|
|
360
|
-
throw buildRemoteMergePauseError(targetBranch, '远程合并要求目标分支已存在于 origin。', {
|
|
361
|
-
remoteExists: false
|
|
362
|
-
})
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
if (!remoteMergePolicy.enabled) {
|
|
366
|
-
throw buildRemoteMergePauseError(targetBranch, '仓库未启用远程合并配置,请先配置 repositories.<repo>.remoteMerge.enabled=true。')
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (!remoteMergePlatform.apiKey) {
|
|
370
|
-
throw buildRemoteMergePauseError(targetBranch, '平台未配置远程合并密钥,请先执行 icode config platform remote-merge set --provider <name> --api-key <key>。')
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const remoteOriginUrl = await git.getRemoteUrl('origin')
|
|
374
|
-
const { projectUrl, source } = resolveRemoteMergeProjectUrlFromOrigin(remoteMergePlatform, remoteOriginUrl)
|
|
375
|
-
if (!projectUrl) {
|
|
376
|
-
throw buildRemoteMergePauseError(targetBranch, '无法从 origin 自动识别 GitLab 项目 URL,请检查 origin 是否指向 GitLab 仓库。', {
|
|
377
|
-
origin: remoteOriginUrl
|
|
378
|
-
})
|
|
379
|
-
}
|
|
380
|
-
logger.info(`GitLab 项目 URL(${source}): ${logger.color('green', projectUrl)}`)
|
|
381
|
-
|
|
382
|
-
logger.info(`发起远程合并: ${logger.color('yellow', currentBranch)} -> ${logger.color('yellow', targetBranch)}`)
|
|
383
|
-
const mergeRequestContent = await buildRemoteMergeCommitContent(git, currentBranch, targetBranch)
|
|
384
|
-
const result = await requestRemoteMerge({
|
|
385
|
-
provider: remoteMergePlatform.provider,
|
|
386
|
-
apiKey: remoteMergePlatform.apiKey,
|
|
387
|
-
projectUrl,
|
|
388
|
-
sourceBranch: currentBranch,
|
|
389
|
-
targetBranch,
|
|
390
|
-
mergeRequestTitle: mergeRequestContent.title,
|
|
391
|
-
mergeRequestDescription: mergeRequestContent.description,
|
|
392
|
-
repoRoot: git.cwd,
|
|
393
|
-
noVerify: inputOptions.noVerify
|
|
394
|
-
})
|
|
395
|
-
|
|
396
|
-
if (result.conflict) {
|
|
397
|
-
throw buildRemoteMergePauseError(targetBranch, buildRemoteMergeResultMessage('远程合并冲突', result), {
|
|
398
|
-
sourceBranch: currentBranch,
|
|
399
|
-
step: result.step || '',
|
|
400
|
-
result
|
|
401
|
-
})
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
if (!result.ok) {
|
|
405
|
-
throw buildRemoteMergePauseError(targetBranch, buildRemoteMergeResultMessage('远程合并失败', result), {
|
|
406
|
-
sourceBranch: currentBranch,
|
|
407
|
-
step: result.step || '',
|
|
408
|
-
result
|
|
409
|
-
})
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const successReason = typeof result.reason === 'string' && result.reason.trim()
|
|
413
|
-
? result.reason.trim()
|
|
414
|
-
: '远程合并已完成'
|
|
415
|
-
|
|
416
|
-
if (result.mergeRequestUrl) {
|
|
417
|
-
logger.success(`${successReason}: ${logger.color('green', result.mergeRequestUrl)}`)
|
|
418
|
-
} else {
|
|
419
|
-
logger.success(`${successReason}: ${logger.color('yellow', currentBranch)} -> ${logger.color('yellow', targetBranch)}`)
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
return result
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// 并发执行多个远程合并目标分支,并把冲突/失败精确归档到对应分支。
|
|
426
|
-
async function runConcurrentRemoteMerges(options) {
|
|
427
|
-
const tasks = options.targetBranches.map(async (targetBranch) => {
|
|
428
|
-
try {
|
|
429
|
-
logger.info(`处理分支: ${targetBranch}`)
|
|
430
|
-
const remoteExists = await options.git.branchExistsRemote(targetBranch)
|
|
431
|
-
const remoteMergeResult = await performRemoteMerge({
|
|
432
|
-
...options,
|
|
433
|
-
targetBranch,
|
|
434
|
-
remoteExists
|
|
435
|
-
})
|
|
436
|
-
|
|
437
|
-
return {
|
|
438
|
-
branch: targetBranch,
|
|
439
|
-
status: 'remote-merged-and-pushed',
|
|
440
|
-
mergeRequestUrl: remoteMergeResult.mergeRequestUrl || '',
|
|
441
|
-
mergeRequestId: remoteMergeResult.mergeRequestId || ''
|
|
442
|
-
}
|
|
443
|
-
} catch (error) {
|
|
444
|
-
return {
|
|
445
|
-
branch: targetBranch,
|
|
446
|
-
status: 'paused',
|
|
447
|
-
reason: resolveRemoteMergeFailureReason(error)
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
})
|
|
451
|
-
|
|
452
|
-
return Promise.all(tasks)
|
|
140
|
+
return { targets: [currentBranch, ...picked], remoteMerge: mode === 'remote' }
|
|
453
141
|
}
|
|
454
142
|
|
|
455
143
|
/**
|
|
@@ -457,18 +145,20 @@ async function runConcurrentRemoteMerges(options) {
|
|
|
457
145
|
* 输入为 push 命令选项,输出推送/合并摘要;关键假设是实际执行前必须没有未完成操作和冲突文件。
|
|
458
146
|
*/
|
|
459
147
|
export async function runPushWorkflow(inputOptions) {
|
|
148
|
+
// 交互选择可能切换到远程合并,这里复制一份,避免修改调用方传入的对象。
|
|
149
|
+
inputOptions = { ...inputOptions }
|
|
460
150
|
const context = await resolveGitContext({
|
|
461
151
|
cwd: inputOptions.cwd,
|
|
462
|
-
repoMode: inputOptions.repoMode
|
|
152
|
+
repoMode: inputOptions.repoMode,
|
|
153
|
+
remote: inputOptions.remote
|
|
463
154
|
})
|
|
464
155
|
|
|
465
156
|
const git = new GitService(context)
|
|
466
157
|
const policy = getRepoPolicy(context.topLevelPath)
|
|
467
|
-
const protectedBranches =
|
|
158
|
+
const protectedBranches = resolveProtectedBranches(policy, context)
|
|
468
159
|
const remoteMergePolicy = normalizeRemoteMergePolicy(policy)
|
|
469
|
-
const remoteMergePlatform = normalizeRemoteMergePlatformConfig(getPlatformConfig('remoteMerge'))
|
|
470
160
|
|
|
471
|
-
logger.
|
|
161
|
+
logger.hint(`仓库 ${shortenHome(context.topLevelPath)}`)
|
|
472
162
|
if (context.inheritedFromParent) {
|
|
473
163
|
logger.warn('当前目录继承了父级 Git 仓库,命令将基于父仓库根目录执行。')
|
|
474
164
|
}
|
|
@@ -496,17 +186,21 @@ export async function runPushWorkflow(inputOptions) {
|
|
|
496
186
|
})
|
|
497
187
|
}
|
|
498
188
|
|
|
189
|
+
validateForcePush(inputOptions, branchTargets, currentBranch, context, protectedBranches)
|
|
190
|
+
|
|
499
191
|
if (inputOptions.dryRun) {
|
|
500
192
|
return {
|
|
501
193
|
dryRun: true,
|
|
502
194
|
repoRoot: context.topLevelPath,
|
|
503
195
|
currentBranch,
|
|
196
|
+
remote: git.remote,
|
|
504
197
|
inheritedFromParent: context.inheritedFromParent,
|
|
505
198
|
plan: buildPushDryRunPlan({
|
|
506
199
|
currentBranch,
|
|
507
200
|
branchTargets,
|
|
508
201
|
protectedBranches,
|
|
509
|
-
inputOptions
|
|
202
|
+
inputOptions,
|
|
203
|
+
remote: git.remote
|
|
510
204
|
})
|
|
511
205
|
}
|
|
512
206
|
}
|
|
@@ -515,7 +209,7 @@ export async function runPushWorkflow(inputOptions) {
|
|
|
515
209
|
assertNoPendingOperation(safetyState.operation, 'push', 'PUSH_GIT_OPERATION_IN_PROGRESS')
|
|
516
210
|
assertNoConflictedFiles(safetyState.changes, 'push', 'PUSH_CONFLICTED_FILES')
|
|
517
211
|
|
|
518
|
-
const aiCommitResult = await prepareAiCommitIfEnabled(inputOptions)
|
|
212
|
+
const aiCommitResult = await prepareAiCommitIfEnabled(git, context, inputOptions, safetyState.changes)
|
|
519
213
|
if (aiCommitResult.canceled) {
|
|
520
214
|
return {
|
|
521
215
|
canceled: true,
|
|
@@ -528,33 +222,60 @@ export async function runPushWorkflow(inputOptions) {
|
|
|
528
222
|
|
|
529
223
|
const shouldRunManualCommit = !aiCommitResult.applied && aiCommitResult.reason !== 'no-diff'
|
|
530
224
|
if (shouldRunManualCommit) {
|
|
531
|
-
await prepareCommitIfNeeded(git, inputOptions, safetyState.changes)
|
|
225
|
+
await prepareCommitIfNeeded(git, context, inputOptions, safetyState.changes)
|
|
532
226
|
}
|
|
227
|
+
|
|
228
|
+
// 强推的 lease 基准必须是 fetch 之前看到的远程 sha,否则 fetch 后 lease 形同虚设。
|
|
229
|
+
const expectedRemoteSha = inputOptions.force ? await git.getRemoteBranchSha(currentBranch) : ''
|
|
533
230
|
await git.fetch()
|
|
534
231
|
|
|
535
232
|
const summary = []
|
|
536
233
|
|
|
537
234
|
if (inputOptions.pullMain && context.defaultBranch !== currentBranch) {
|
|
538
|
-
logger.info(`先同步主分支 ${context.defaultBranch} 到当前分支 ${currentBranch}`)
|
|
235
|
+
logger.info(`先同步主分支 ${ui.branch(context.defaultBranch)} 到当前分支 ${ui.branch(currentBranch)}`)
|
|
539
236
|
try {
|
|
540
237
|
await git.pull(context.defaultBranch, {
|
|
541
238
|
noRebase: true
|
|
542
239
|
})
|
|
543
240
|
} catch (error) {
|
|
544
|
-
|
|
241
|
+
const pauseError = buildLocalMergePauseError({
|
|
545
242
|
sourceBranch: context.defaultBranch,
|
|
546
243
|
targetBranch: currentBranch,
|
|
547
244
|
step: 'pull-main',
|
|
548
245
|
error,
|
|
549
246
|
summary
|
|
550
247
|
})
|
|
248
|
+
if (await git.getInProgressOperation()) {
|
|
249
|
+
savePushState(context, inputOptions, currentBranch, null, filterRunnable(branchTargets, protectedBranches, inputOptions), summary)
|
|
250
|
+
}
|
|
251
|
+
throw pauseError
|
|
551
252
|
}
|
|
552
253
|
}
|
|
553
254
|
|
|
554
|
-
|
|
255
|
+
// 交互终端里没写目标分支时,用选择代替手输分支名;这一步的选择本身就是确认。
|
|
256
|
+
let confirmedByPicker = false
|
|
257
|
+
if (!inputOptions.yes && !inputOptions.force && !inputOptions.remoteMerge && !(inputOptions.targetBranches || []).length && !inputOptions.notPushCurrent && isInteractiveTerminal()) {
|
|
258
|
+
const picked = await pickPushTargets(git, currentBranch, context, protectedBranches)
|
|
259
|
+
if (!picked) {
|
|
260
|
+
logger.warn('已取消执行。')
|
|
261
|
+
return {
|
|
262
|
+
canceled: true,
|
|
263
|
+
branchTargets,
|
|
264
|
+
repoRoot: context.topLevelPath
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
branchTargets = picked.targets
|
|
268
|
+
if (picked.remoteMerge) {
|
|
269
|
+
inputOptions = { ...inputOptions, remoteMerge: true }
|
|
270
|
+
}
|
|
271
|
+
confirmedByPicker = true
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!inputOptions.yes && !confirmedByPicker) {
|
|
275
|
+
const action = inputOptions.force ? '强推(--force-with-lease)' : '推送/合并'
|
|
555
276
|
const confirmed = await confirm(
|
|
556
|
-
`确认将 ${currentBranch}
|
|
557
|
-
|
|
277
|
+
`确认将 ${currentBranch} ${action}到以下分支: ${branchTargets.join(', ')} ?`,
|
|
278
|
+
!inputOptions.force
|
|
558
279
|
)
|
|
559
280
|
|
|
560
281
|
if (!confirmed) {
|
|
@@ -567,82 +288,52 @@ export async function runPushWorkflow(inputOptions) {
|
|
|
567
288
|
}
|
|
568
289
|
}
|
|
569
290
|
|
|
291
|
+
// 本地 merge 会改动目标分支,强推会覆盖远程分支:执行前先记录恢复点。
|
|
292
|
+
const localMergeTargets = inputOptions.remoteMerge ? [] : branchTargets.filter((branch) => branch !== currentBranch)
|
|
293
|
+
const backupBranches = []
|
|
294
|
+
for (const branch of [currentBranch, ...localMergeTargets]) {
|
|
295
|
+
if (await git.branchExistsLocal(branch)) {
|
|
296
|
+
backupBranches.push(branch)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const backup = (localMergeTargets.length || inputOptions.force)
|
|
300
|
+
? await createBackup(git, {
|
|
301
|
+
reason: inputOptions.force ? `push --force ${currentBranch}` : `push ${currentBranch} -> ${localMergeTargets.join(',')}`,
|
|
302
|
+
branches: backupBranches,
|
|
303
|
+
remoteBranches: inputOptions.force ? [currentBranch] : []
|
|
304
|
+
})
|
|
305
|
+
: null
|
|
306
|
+
|
|
570
307
|
const originalBranch = currentBranch
|
|
571
308
|
const remoteMergeTargets = []
|
|
309
|
+
const branchOptions = { ...inputOptions, expectedRemoteSha }
|
|
572
310
|
|
|
573
311
|
try {
|
|
574
|
-
for (
|
|
312
|
+
for (let index = 0; index < branchTargets.length; index += 1) {
|
|
313
|
+
const targetBranch = branchTargets[index]
|
|
575
314
|
if (protectedBranches.has(targetBranch) && !inputOptions.forceProtected) {
|
|
576
|
-
logger.warn(`跳过受保护分支: ${targetBranch}(可用 --force-protected 覆盖)`)
|
|
315
|
+
logger.warn(`跳过受保护分支: ${ui.branch(targetBranch)}(可用 --force-protected 覆盖)`)
|
|
577
316
|
summary.push({ branch: targetBranch, status: 'skipped-protected' })
|
|
578
317
|
continue
|
|
579
318
|
}
|
|
580
319
|
|
|
581
|
-
if (targetBranch
|
|
582
|
-
|
|
583
|
-
const remoteExists = await git.branchExistsRemote(targetBranch)
|
|
584
|
-
if (remoteExists) {
|
|
585
|
-
logger.info(`同步远程分支: ${targetBranch}`)
|
|
586
|
-
try {
|
|
587
|
-
await git.pull(targetBranch, {
|
|
588
|
-
noRebase: true
|
|
589
|
-
})
|
|
590
|
-
} catch (error) {
|
|
591
|
-
throw buildLocalMergePauseError({
|
|
592
|
-
sourceBranch: `origin/${targetBranch}`,
|
|
593
|
-
targetBranch,
|
|
594
|
-
step: 'pull-current',
|
|
595
|
-
error,
|
|
596
|
-
summary
|
|
597
|
-
})
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
logger.info(`推送当前分支: ${targetBranch}`)
|
|
602
|
-
await git.push(targetBranch, {
|
|
603
|
-
setUpstream: !remoteExists,
|
|
604
|
-
noVerify: inputOptions.noVerify
|
|
605
|
-
})
|
|
606
|
-
|
|
607
|
-
logger.success(`推送成功: ${targetBranch}`)
|
|
608
|
-
summary.push({ branch: targetBranch, status: 'pushed' })
|
|
320
|
+
if (inputOptions.remoteMerge && targetBranch !== currentBranch) {
|
|
321
|
+
remoteMergeTargets.push(targetBranch)
|
|
609
322
|
continue
|
|
610
323
|
}
|
|
611
324
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
try {
|
|
623
|
-
await git.merge(currentBranch, {
|
|
624
|
-
noFf: true,
|
|
625
|
-
noEdit: true
|
|
626
|
-
})
|
|
627
|
-
} catch (error) {
|
|
628
|
-
throw buildLocalMergePauseError({
|
|
629
|
-
sourceBranch: currentBranch,
|
|
630
|
-
targetBranch,
|
|
631
|
-
step: 'merge-target',
|
|
632
|
-
error,
|
|
633
|
-
summary
|
|
634
|
-
})
|
|
325
|
+
try {
|
|
326
|
+
summary.push(targetBranch === currentBranch
|
|
327
|
+
? await pushCurrentBranch(git, targetBranch, branchOptions, summary)
|
|
328
|
+
: await mergeIntoTargetAndPush(git, currentBranch, targetBranch, branchOptions, summary))
|
|
329
|
+
} catch (error) {
|
|
330
|
+
// 冲突暂停:记下剩余步骤,icode continue 会推送当前目标并处理剩下的分支。
|
|
331
|
+
if (error?.code === 'PUSH_LOCAL_MERGE_PAUSED' && await git.getInProgressOperation()) {
|
|
332
|
+
const mergedTarget = error.meta?.step === 'merge-target'
|
|
333
|
+
const rest = filterRunnable(branchTargets.slice(mergedTarget ? index + 1 : index), protectedBranches, inputOptions)
|
|
334
|
+
savePushState(context, inputOptions, currentBranch, mergedTarget ? targetBranch : null, rest, summary)
|
|
635
335
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
logger.info(`推送目标分支: ${targetBranch}`)
|
|
639
|
-
await git.push(targetBranch, {
|
|
640
|
-
setUpstream: !remoteExists,
|
|
641
|
-
noVerify: inputOptions.noVerify
|
|
642
|
-
})
|
|
643
|
-
|
|
644
|
-
logger.success(`目标分支推送成功: ${targetBranch}`)
|
|
645
|
-
summary.push({ branch: targetBranch, status: 'merged-and-pushed' })
|
|
336
|
+
throw error
|
|
646
337
|
}
|
|
647
338
|
}
|
|
648
339
|
|
|
@@ -654,7 +345,7 @@ export async function runPushWorkflow(inputOptions) {
|
|
|
654
345
|
targetBranches: remoteMergeTargets,
|
|
655
346
|
inputOptions,
|
|
656
347
|
remoteMergePolicy,
|
|
657
|
-
remoteMergePlatform
|
|
348
|
+
remoteMergePlatform: await resolveRemoteMergePlatform(git)
|
|
658
349
|
})
|
|
659
350
|
summary.push(...remoteMergeSummary)
|
|
660
351
|
|
|
@@ -674,12 +365,12 @@ export async function runPushWorkflow(inputOptions) {
|
|
|
674
365
|
const operationAfterWorkflow = await git.getInProgressOperation()
|
|
675
366
|
if (operationAfterWorkflow === 'merge') {
|
|
676
367
|
logger.warn(`检测到未完成的 merge,已保留在当前分支 ${branchAfterWorkflow || '(unknown)'}。`)
|
|
677
|
-
logger.warn('解决冲突后执行 `icode
|
|
368
|
+
logger.warn('解决冲突后执行 `icode continue` 接着完成剩下的步骤,或执行 `icode abort` 放弃并切回原分支。')
|
|
678
369
|
} else if (branchAfterWorkflow && branchAfterWorkflow !== originalBranch) {
|
|
679
370
|
try {
|
|
680
371
|
await git.checkout(originalBranch)
|
|
681
372
|
} catch (error) {
|
|
682
|
-
logger.warn(`未能自动切回原分支 ${originalBranch}: ${error.message}`)
|
|
373
|
+
logger.warn(`未能自动切回原分支 ${ui.branch(originalBranch)}: ${error.message}`)
|
|
683
374
|
}
|
|
684
375
|
}
|
|
685
376
|
}
|
|
@@ -687,6 +378,8 @@ export async function runPushWorkflow(inputOptions) {
|
|
|
687
378
|
return {
|
|
688
379
|
repoRoot: context.topLevelPath,
|
|
689
380
|
currentBranch,
|
|
381
|
+
remote: git.remote,
|
|
382
|
+
backupId: backup?.entries.length ? backup.id : '',
|
|
690
383
|
summary,
|
|
691
384
|
inheritedFromParent: context.inheritedFromParent
|
|
692
385
|
}
|