@icode-js/icode 3.0.3 → 3.1.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/LICENSE +21 -0
- package/README.md +40 -3
- package/bin/icode.js +4 -4
- package/package.json +1 -1
- package/src/cli.js +54 -23
- package/src/commands/ai.js +4 -4
- package/src/commands/checkout.js +2 -2
- package/src/commands/clean.js +1 -1
- package/src/commands/codereview.js +1 -1
- package/src/commands/completion.js +90 -0
- package/src/commands/config.js +117 -7
- package/src/commands/explain.js +2 -2
- package/src/commands/help.js +7 -0
- package/src/commands/info.js +1 -1
- package/src/commands/migrate.js +2 -2
- package/src/commands/push.js +29 -8
- package/src/commands/sync.js +2 -2
- package/src/commands/tag.js +2 -2
- package/src/commands/undo.js +60 -9
- package/src/core/{ai-client.js → ai/client.js} +3 -3
- package/src/core/{ai-config.js → ai/config.js} +2 -2
- package/src/core/{ai-diff-range.js → ai/diff-range.js} +2 -2
- package/src/core/cli/completion.js +687 -0
- package/src/core/cli/update-notifier.js +138 -0
- package/src/core/{shell.js → commands/shell.js} +13 -1
- package/src/core/{config-store.js → config/store.js} +55 -3
- package/src/core/{git-context.js → git/context.js} +25 -10
- package/src/core/{git-service.js → git/service.js} +78 -6
- package/src/core/remote-merge/client.js +42 -0
- package/src/core/remote-merge/providers/gitlab.js +537 -0
- package/src/core/remote-merge/shared.js +49 -0
- package/src/core/{prompts.js → tools/interactive.js} +46 -5
- package/src/workflows/ai-codereview-workflow.js +4 -4
- package/src/workflows/ai-commit-workflow.js +30 -15
- package/src/workflows/ai-conflict-workflow.js +4 -4
- package/src/workflows/ai-explain-workflow.js +4 -4
- package/src/workflows/ai-risk-review-workflow.js +1 -1
- package/src/workflows/checkout-workflow.js +3 -3
- package/src/workflows/clean-workflow.js +5 -5
- package/src/workflows/info-workflow.js +4 -4
- package/src/workflows/migrate-workflow.js +4 -4
- package/src/workflows/push-workflow.js +259 -24
- package/src/workflows/rollback-workflow.js +4 -4
- package/src/workflows/sync-workflow.js +4 -4
- package/src/workflows/tag-workflow.js +4 -4
- package/src/workflows/undo-workflow.js +12 -5
- /package/src/core/{ai-commit-summary.js → ai/commit-summary.js} +0 -0
- /package/src/core/{args.js → cli/args.js} +0 -0
- /package/src/core/{commit-conventions.js → git/commit-conventions.js} +0 -0
- /package/src/core/{hook-diagnostics.js → git/hook-diagnostics.js} +0 -0
- /package/src/core/{loading.js → tools/loading.js} +0 -0
- /package/src/core/{logger.js → tools/logger.js} +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { IcodeError } from '../core/errors.js'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
2
|
+
import { askAiJson } from '../core/ai/client.js'
|
|
3
|
+
import { resolveGitContext } from '../core/git/context.js'
|
|
4
|
+
import { scanCommitConventions } from '../core/git/commit-conventions.js'
|
|
5
|
+
import { GitService } from '../core/git/service.js'
|
|
6
|
+
import { confirm } from '../core/tools/interactive.js'
|
|
7
|
+
import { logger } from '../core/tools/logger.js'
|
|
8
8
|
|
|
9
9
|
function normalizeCommitType(value) {
|
|
10
10
|
const allowed = new Set(['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'chore', 'build', 'ci', 'revert'])
|
|
@@ -33,6 +33,15 @@ function buildCommitMessage(parsed) {
|
|
|
33
33
|
return `${header}\n\n${body}`
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// 将多个 diff 片段稳定拼接,避免空块污染 prompt。
|
|
37
|
+
function joinDiffSections(sections) {
|
|
38
|
+
return sections
|
|
39
|
+
.map((item) => (item || '').trim())
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.join('\n\n')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 基于“全部未提交改动”生成 AI commit,确保未暂存新文件也能被识别并提交。
|
|
36
45
|
export async function runAiCommitWorkflow(options) {
|
|
37
46
|
const context = await resolveGitContext({
|
|
38
47
|
cwd: options.cwd,
|
|
@@ -44,13 +53,19 @@ export async function runAiCommitWorkflow(options) {
|
|
|
44
53
|
logger.info(`仓库根目录: ${context.topLevelPath}`)
|
|
45
54
|
}
|
|
46
55
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
const stagedDiff = await git.diffStaged()
|
|
57
|
+
const workingDiff = await git.diffWorkingTree()
|
|
58
|
+
const hasStagedDiff = Boolean(stagedDiff.trim())
|
|
59
|
+
const hasWorkingDiff = Boolean(workingDiff.trim())
|
|
60
|
+
const diff = joinDiffSections([
|
|
61
|
+
hasStagedDiff ? `--- STAGED DIFF ---\n${stagedDiff}` : '',
|
|
62
|
+
hasWorkingDiff ? `--- WORKING TREE DIFF ---\n${workingDiff}` : ''
|
|
63
|
+
])
|
|
64
|
+
const diffSource = hasStagedDiff && hasWorkingDiff
|
|
65
|
+
? 'staged+working-tree'
|
|
66
|
+
: hasStagedDiff
|
|
67
|
+
? 'staged'
|
|
68
|
+
: 'working-tree'
|
|
54
69
|
|
|
55
70
|
if (!diff.trim()) {
|
|
56
71
|
throw new IcodeError('没有可用于生成提交信息的代码改动。', {
|
|
@@ -105,8 +120,8 @@ export async function runAiCommitWorkflow(options) {
|
|
|
105
120
|
}
|
|
106
121
|
}
|
|
107
122
|
|
|
108
|
-
if (
|
|
109
|
-
//
|
|
123
|
+
if (hasWorkingDiff) {
|
|
124
|
+
// 统一暂存工作区改动,确保未暂存修改和新文件与 AI 看到的 diff 保持一致。
|
|
110
125
|
await git.stageAll()
|
|
111
126
|
}
|
|
112
127
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import { askAi } from '../core/ai
|
|
3
|
+
import { askAi } from '../core/ai/client.js'
|
|
4
4
|
import { IcodeError } from '../core/errors.js'
|
|
5
|
-
import { resolveGitContext } from '../core/git
|
|
6
|
-
import { GitService } from '../core/git
|
|
7
|
-
import { logger } from '../core/logger.js'
|
|
5
|
+
import { resolveGitContext } from '../core/git/context.js'
|
|
6
|
+
import { GitService } from '../core/git/service.js'
|
|
7
|
+
import { logger } from '../core/tools/logger.js'
|
|
8
8
|
|
|
9
9
|
function extractConflictBlocks(content, maxBlocks = 3, maxLines = 80) {
|
|
10
10
|
const lines = content.split('\n')
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { askAi } from '../core/ai
|
|
2
|
-
import { resolveAiDiffRange } from '../core/ai
|
|
1
|
+
import { askAi } from '../core/ai/client.js'
|
|
2
|
+
import { resolveAiDiffRange } from '../core/ai/diff-range.js'
|
|
3
3
|
import { IcodeError } from '../core/errors.js'
|
|
4
|
-
import { resolveGitContext } from '../core/git
|
|
5
|
-
import { GitService } from '../core/git
|
|
4
|
+
import { resolveGitContext } from '../core/git/context.js'
|
|
5
|
+
import { GitService } from '../core/git/service.js'
|
|
6
6
|
|
|
7
7
|
function truncate(value, limit) {
|
|
8
8
|
const text = value || ''
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { IcodeError } from '../core/errors.js'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { logger } from '../core/logger.js'
|
|
2
|
+
import { resolveGitContext } from '../core/git/context.js'
|
|
3
|
+
import { GitService } from '../core/git/service.js'
|
|
4
|
+
import { logger } from '../core/tools/logger.js'
|
|
5
5
|
|
|
6
6
|
export async function runCheckoutWorkflow(input) {
|
|
7
7
|
if (!input.branchName) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { getRepoPolicy } from '../core/config
|
|
1
|
+
import { getRepoPolicy } from '../core/config/store.js'
|
|
2
2
|
import { IcodeError } from '../core/errors.js'
|
|
3
|
-
import { resolveGitContext } from '../core/git
|
|
4
|
-
import { GitService } from '../core/git
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { resolveGitContext } from '../core/git/context.js'
|
|
4
|
+
import { GitService } from '../core/git/service.js'
|
|
5
|
+
import { confirm } from '../core/tools/interactive.js'
|
|
6
|
+
import { logger } from '../core/tools/logger.js'
|
|
7
7
|
|
|
8
8
|
function normalizeKeepList(values = []) {
|
|
9
9
|
return new Set(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { getAiConfig } from '../core/ai
|
|
2
|
-
import { getConfigFilePath, getRepoPolicy } from '../core/config
|
|
3
|
-
import { resolveGitContext } from '../core/git
|
|
4
|
-
import { runCommand } from '../core/shell.js'
|
|
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/commands/shell.js'
|
|
5
5
|
|
|
6
6
|
export async function runInfoWorkflow(options) {
|
|
7
7
|
const context = await resolveGitContext({
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { IcodeError } from '../core/errors.js'
|
|
2
|
-
import { resolveGitContext } from '../core/git
|
|
3
|
-
import { GitService } from '../core/git
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { resolveGitContext } from '../core/git/context.js'
|
|
3
|
+
import { GitService } from '../core/git/service.js'
|
|
4
|
+
import { chooseMany, chooseOne, isInteractiveTerminal } from '../core/tools/interactive.js'
|
|
5
|
+
import { logger } from '../core/tools/logger.js'
|
|
6
6
|
|
|
7
7
|
const INTERACTIVE_COMMIT_LIMIT = 30
|
|
8
8
|
|
|
@@ -1,16 +1,64 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { formatAiCommitSummary } from '../core/ai/commit-summary.js'
|
|
2
|
+
import { getPlatformConfig, getRepoPolicy } from '../core/config/store.js'
|
|
2
3
|
import { IcodeError } from '../core/errors.js'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import { resolveGitContext } from '../core/git/context.js'
|
|
5
|
+
import { GitService } from '../core/git/service.js'
|
|
6
|
+
import { confirm, input } from '../core/tools/interactive.js'
|
|
7
|
+
import { requestRemoteMerge, resolveRemoteMergeProjectUrl } from '../core/remote-merge/client.js'
|
|
8
|
+
import { logger } from '../core/tools/logger.js'
|
|
8
9
|
import { runAiCommitWorkflow } from './ai-commit-workflow.js'
|
|
9
10
|
|
|
10
11
|
function uniqueBranches(branches) {
|
|
11
12
|
return [...new Set(branches.map((item) => item.trim()).filter(Boolean))]
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
// 基于 commit 内容构建远程 MR 标题与正文,确保标题和内容都来自真实提交信息。
|
|
16
|
+
async function buildRemoteMergeCommitContent(git, sourceBranch, targetBranch) {
|
|
17
|
+
const rangeSpec = `origin/${targetBranch}..${sourceBranch}`
|
|
18
|
+
let commits = await git.listCommitMessages(rangeSpec)
|
|
19
|
+
|
|
20
|
+
if (!commits.length) {
|
|
21
|
+
const latestCommit = await git.getCommitMessage(sourceBranch)
|
|
22
|
+
if (latestCommit) {
|
|
23
|
+
commits = [latestCommit]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!commits.length) {
|
|
28
|
+
throw new IcodeError(`无法读取远程合并提交内容: ${sourceBranch} -> ${targetBranch}`, {
|
|
29
|
+
code: 'PUSH_REMOTE_MERGE_COMMIT_CONTENT_EMPTY',
|
|
30
|
+
exitCode: 2
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const latestCommit = commits[commits.length - 1]
|
|
35
|
+
const description = commits
|
|
36
|
+
.map((commit) => {
|
|
37
|
+
if (commit.body) {
|
|
38
|
+
return `${commit.subject}\n\n${commit.body}`
|
|
39
|
+
}
|
|
40
|
+
return commit.subject
|
|
41
|
+
})
|
|
42
|
+
.join('\n\n')
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
title: latestCommit.subject,
|
|
46
|
+
description
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 提取远程合并失败原因,优先使用 IcodeError.meta,其次回退到 error.message。
|
|
51
|
+
function resolveRemoteMergeFailureReason(error) {
|
|
52
|
+
const metaStep = typeof error?.meta?.step === 'string' ? error.meta.step.trim() : ''
|
|
53
|
+
const metaReason = typeof error?.meta?.reason === 'string' ? error.meta.reason.trim() : ''
|
|
54
|
+
if (metaReason) {
|
|
55
|
+
return metaStep ? `${metaReason} [step=${metaStep}]` : metaReason
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const message = typeof error?.message === 'string' ? error.message.trim() : ''
|
|
59
|
+
return message || '远程合并失败'
|
|
60
|
+
}
|
|
61
|
+
|
|
14
62
|
// Run AI commit before push and keep the full generated message in follow-up logs.
|
|
15
63
|
async function prepareAiCommitIfEnabled(inputOptions) {
|
|
16
64
|
if (!inputOptions.aiCommit) {
|
|
@@ -122,6 +170,162 @@ async function checkoutTargetBranch(git, targetBranch, sourceBranch) {
|
|
|
122
170
|
}
|
|
123
171
|
}
|
|
124
172
|
|
|
173
|
+
function normalizeRemoteMergePolicy(policy = {}) {
|
|
174
|
+
const remoteMerge = policy.remoteMerge && typeof policy.remoteMerge === 'object' ? policy.remoteMerge : {}
|
|
175
|
+
return {
|
|
176
|
+
enabled: remoteMerge.enabled !== false,
|
|
177
|
+
projectUrl: typeof remoteMerge.projectUrl === 'string' ? remoteMerge.projectUrl.trim() : ''
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 归一化平台级远程合并配置,避免命令层拿到带空白的原始值。
|
|
182
|
+
function normalizeRemoteMergePlatformConfig(config = {}) {
|
|
183
|
+
return {
|
|
184
|
+
provider: typeof config.provider === 'string' ? config.provider.trim() : '',
|
|
185
|
+
apiKey: typeof config.apiKey === 'string' ? config.apiKey.trim() : ''
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function buildRemoteMergePauseError(targetBranch, reason, meta = {}) {
|
|
190
|
+
return new IcodeError(`远程合并已暂停(${targetBranch}): ${reason}`, {
|
|
191
|
+
code: 'PUSH_REMOTE_MERGE_PAUSED',
|
|
192
|
+
exitCode: 2,
|
|
193
|
+
meta: {
|
|
194
|
+
targetBranch,
|
|
195
|
+
reason,
|
|
196
|
+
...meta
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 统一把 provider 返回的 step 注入错误文案,便于定位 create/approve/merge/inspect 具体失败点。
|
|
202
|
+
function buildRemoteMergeResultMessage(prefix, result) {
|
|
203
|
+
const reason = typeof result?.reason === 'string' && result.reason.trim()
|
|
204
|
+
? result.reason.trim()
|
|
205
|
+
: '未知原因'
|
|
206
|
+
const step = typeof result?.step === 'string' && result.step.trim()
|
|
207
|
+
? result.step.trim()
|
|
208
|
+
: ''
|
|
209
|
+
|
|
210
|
+
return step ? `${prefix}(${step}): ${reason}` : `${prefix}: ${reason}`
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 构建批量远程合并暂停错误,明确指出具体失败分支与原因。
|
|
214
|
+
function buildRemoteMergeBatchPauseError(failures, summary) {
|
|
215
|
+
const branchesLabel = failures.map((item) => item.branch).join(', ')
|
|
216
|
+
const detail = failures.map((item) => `${item.branch}: ${item.reason}`).join(' | ')
|
|
217
|
+
|
|
218
|
+
return new IcodeError(`远程合并已暂停(${branchesLabel}): ${detail}`, {
|
|
219
|
+
code: 'PUSH_REMOTE_MERGE_PAUSED',
|
|
220
|
+
exitCode: 2,
|
|
221
|
+
meta: {
|
|
222
|
+
failures,
|
|
223
|
+
summary
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 基于 origin 推导 GitLab 项目地址并发起固定 API 的 MR 创建请求。
|
|
229
|
+
async function performRemoteMerge({ git, currentBranch, targetBranch, remoteExists, inputOptions, remoteMergePolicy, remoteMergePlatform }) {
|
|
230
|
+
if (!remoteExists) {
|
|
231
|
+
throw buildRemoteMergePauseError(targetBranch, '远程合并要求目标分支已存在于 origin。', {
|
|
232
|
+
remoteExists: false
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!remoteMergePolicy.enabled) {
|
|
237
|
+
throw buildRemoteMergePauseError(targetBranch, '仓库未启用远程合并配置,请先配置 repositories.<repo>.remoteMerge.enabled=true。')
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!remoteMergePlatform.apiKey) {
|
|
241
|
+
throw buildRemoteMergePauseError(targetBranch, '平台未配置远程合并密钥,请先执行 icode config platform remote-merge set --provider <name> --api-key <key>。')
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const remoteOriginUrl = await git.getRemoteUrl('origin')
|
|
245
|
+
const projectUrl = resolveRemoteMergeProjectUrl({
|
|
246
|
+
provider: remoteMergePlatform.provider,
|
|
247
|
+
remoteUrl: remoteOriginUrl
|
|
248
|
+
})
|
|
249
|
+
if (!projectUrl) {
|
|
250
|
+
throw buildRemoteMergePauseError(targetBranch, '无法从 origin 自动识别 GitLab 项目 URL,请检查 origin 是否指向 GitLab 仓库。', {
|
|
251
|
+
origin: remoteOriginUrl
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
logger.info(`已根据 origin 自动识别 GitLab 项目 URL: ${projectUrl}`)
|
|
255
|
+
|
|
256
|
+
logger.info(`发起远程合并: ${currentBranch} -> ${targetBranch}`)
|
|
257
|
+
const mergeRequestContent = await buildRemoteMergeCommitContent(git, currentBranch, targetBranch)
|
|
258
|
+
const result = await requestRemoteMerge({
|
|
259
|
+
provider: remoteMergePlatform.provider,
|
|
260
|
+
apiKey: remoteMergePlatform.apiKey,
|
|
261
|
+
projectUrl,
|
|
262
|
+
sourceBranch: currentBranch,
|
|
263
|
+
targetBranch,
|
|
264
|
+
mergeRequestTitle: mergeRequestContent.title,
|
|
265
|
+
mergeRequestDescription: mergeRequestContent.description,
|
|
266
|
+
repoRoot: git.cwd,
|
|
267
|
+
noVerify: inputOptions.noVerify
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
if (result.conflict) {
|
|
271
|
+
throw buildRemoteMergePauseError(targetBranch, buildRemoteMergeResultMessage('远程合并冲突', result), {
|
|
272
|
+
sourceBranch: currentBranch,
|
|
273
|
+
step: result.step || '',
|
|
274
|
+
result
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!result.ok) {
|
|
279
|
+
throw buildRemoteMergePauseError(targetBranch, buildRemoteMergeResultMessage('远程合并失败', result), {
|
|
280
|
+
sourceBranch: currentBranch,
|
|
281
|
+
step: result.step || '',
|
|
282
|
+
result
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const successReason = typeof result.reason === 'string' && result.reason.trim()
|
|
287
|
+
? result.reason.trim()
|
|
288
|
+
: '远程合并已完成'
|
|
289
|
+
|
|
290
|
+
if (result.mergeRequestUrl) {
|
|
291
|
+
logger.success(`${successReason}: ${result.mergeRequestUrl}`)
|
|
292
|
+
} else {
|
|
293
|
+
logger.success(`${successReason}: ${currentBranch} -> ${targetBranch}`)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return result
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// 并发执行多个远程合并目标分支,并把冲突/失败精确归档到对应分支。
|
|
300
|
+
async function runConcurrentRemoteMerges(options) {
|
|
301
|
+
const tasks = options.targetBranches.map(async (targetBranch) => {
|
|
302
|
+
try {
|
|
303
|
+
logger.info(`处理分支: ${targetBranch}`)
|
|
304
|
+
const remoteExists = await options.git.branchExistsRemote(targetBranch)
|
|
305
|
+
const remoteMergeResult = await performRemoteMerge({
|
|
306
|
+
...options,
|
|
307
|
+
targetBranch,
|
|
308
|
+
remoteExists
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
branch: targetBranch,
|
|
313
|
+
status: 'remote-merged-and-pushed',
|
|
314
|
+
mergeRequestUrl: remoteMergeResult.mergeRequestUrl || '',
|
|
315
|
+
mergeRequestId: remoteMergeResult.mergeRequestId || ''
|
|
316
|
+
}
|
|
317
|
+
} catch (error) {
|
|
318
|
+
return {
|
|
319
|
+
branch: targetBranch,
|
|
320
|
+
status: 'paused',
|
|
321
|
+
reason: resolveRemoteMergeFailureReason(error)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
return Promise.all(tasks)
|
|
327
|
+
}
|
|
328
|
+
|
|
125
329
|
export async function runPushWorkflow(inputOptions) {
|
|
126
330
|
const context = await resolveGitContext({
|
|
127
331
|
cwd: inputOptions.cwd,
|
|
@@ -131,6 +335,8 @@ export async function runPushWorkflow(inputOptions) {
|
|
|
131
335
|
const git = new GitService(context)
|
|
132
336
|
const policy = getRepoPolicy(context.topLevelPath)
|
|
133
337
|
const protectedBranches = new Set((policy.protectedBranches || []).map((item) => item.trim()).filter(Boolean))
|
|
338
|
+
const remoteMergePolicy = normalizeRemoteMergePolicy(policy)
|
|
339
|
+
const remoteMergePlatform = normalizeRemoteMergePlatformConfig(getPlatformConfig('remoteMerge'))
|
|
134
340
|
|
|
135
341
|
logger.info(`仓库根目录: ${context.topLevelPath}`)
|
|
136
342
|
if (context.inheritedFromParent) {
|
|
@@ -203,6 +409,7 @@ export async function runPushWorkflow(inputOptions) {
|
|
|
203
409
|
|
|
204
410
|
const summary = []
|
|
205
411
|
const originalBranch = currentBranch
|
|
412
|
+
const remoteMergeTargets = []
|
|
206
413
|
|
|
207
414
|
try {
|
|
208
415
|
for (const targetBranch of branchTargets) {
|
|
@@ -212,9 +419,8 @@ export async function runPushWorkflow(inputOptions) {
|
|
|
212
419
|
continue
|
|
213
420
|
}
|
|
214
421
|
|
|
215
|
-
logger.info(`处理分支: ${targetBranch}`)
|
|
216
|
-
|
|
217
422
|
if (targetBranch === currentBranch) {
|
|
423
|
+
logger.info(`处理分支: ${targetBranch}`)
|
|
218
424
|
const remoteExists = await git.branchExistsRemote(targetBranch)
|
|
219
425
|
if (remoteExists) {
|
|
220
426
|
logger.info(`同步远程分支: ${targetBranch}`)
|
|
@@ -235,26 +441,55 @@ export async function runPushWorkflow(inputOptions) {
|
|
|
235
441
|
continue
|
|
236
442
|
}
|
|
237
443
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
444
|
+
if (inputOptions.remoteMerge) {
|
|
445
|
+
remoteMergeTargets.push(targetBranch)
|
|
446
|
+
} else {
|
|
447
|
+
logger.info(`处理分支: ${targetBranch}`)
|
|
448
|
+
logger.info(`切换到目标分支: ${targetBranch}`)
|
|
449
|
+
const { remoteExists, checkoutMode } = await checkoutTargetBranch(git, targetBranch, currentBranch)
|
|
450
|
+
logger.info(`目标分支准备完成: ${targetBranch} (${checkoutMode})`)
|
|
451
|
+
|
|
452
|
+
// 保留 merge commit,方便后续追溯“从哪个分支合并过来”。
|
|
453
|
+
logger.info(`合并分支: ${currentBranch} -> ${targetBranch}`)
|
|
454
|
+
await git.merge(currentBranch, {
|
|
455
|
+
noFf: true,
|
|
456
|
+
noEdit: true
|
|
457
|
+
})
|
|
458
|
+
logger.success(`合并成功: ${currentBranch} -> ${targetBranch}`)
|
|
241
459
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
})
|
|
248
|
-
logger.success(`合并成功: ${currentBranch} -> ${targetBranch}`)
|
|
460
|
+
logger.info(`推送目标分支: ${targetBranch}`)
|
|
461
|
+
await git.push(targetBranch, {
|
|
462
|
+
setUpstream: !remoteExists,
|
|
463
|
+
noVerify: inputOptions.noVerify
|
|
464
|
+
})
|
|
249
465
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
466
|
+
logger.success(`目标分支推送成功: ${targetBranch}`)
|
|
467
|
+
summary.push({ branch: targetBranch, status: 'merged-and-pushed' })
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (inputOptions.remoteMerge && remoteMergeTargets.length) {
|
|
472
|
+
logger.info(`并发处理远程合并分支: ${remoteMergeTargets.join(', ')}`)
|
|
473
|
+
const remoteMergeSummary = await runConcurrentRemoteMerges({
|
|
474
|
+
git,
|
|
475
|
+
currentBranch,
|
|
476
|
+
targetBranches: remoteMergeTargets,
|
|
477
|
+
inputOptions,
|
|
478
|
+
remoteMergePolicy,
|
|
479
|
+
remoteMergePlatform
|
|
254
480
|
})
|
|
481
|
+
summary.push(...remoteMergeSummary)
|
|
255
482
|
|
|
256
|
-
|
|
257
|
-
|
|
483
|
+
const failures = remoteMergeSummary
|
|
484
|
+
.filter((item) => item.status === 'paused')
|
|
485
|
+
.map((item) => ({
|
|
486
|
+
branch: item.branch,
|
|
487
|
+
reason: item.reason || '远程合并失败'
|
|
488
|
+
}))
|
|
489
|
+
|
|
490
|
+
if (failures.length) {
|
|
491
|
+
throw buildRemoteMergeBatchPauseError(failures, summary)
|
|
492
|
+
}
|
|
258
493
|
}
|
|
259
494
|
} finally {
|
|
260
495
|
const branchAfterWorkflow = await git.getCurrentBranch()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { IcodeError } from '../core/errors.js'
|
|
2
|
-
import { resolveGitContext } from '../core/git
|
|
3
|
-
import { GitService } from '../core/git
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { resolveGitContext } from '../core/git/context.js'
|
|
3
|
+
import { GitService } from '../core/git/service.js'
|
|
4
|
+
import { confirm } from '../core/tools/interactive.js'
|
|
5
|
+
import { logger } from '../core/tools/logger.js'
|
|
6
6
|
|
|
7
7
|
function isRevertConflict(error) {
|
|
8
8
|
const output = `${error?.meta?.stdout || ''}\n${error?.meta?.stderr || ''}\n${error?.message || ''}`
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { IcodeError } from '../core/errors.js'
|
|
2
|
-
import { resolveGitContext } from '../core/git
|
|
3
|
-
import { GitService } from '../core/git
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { resolveGitContext } from '../core/git/context.js'
|
|
3
|
+
import { GitService } from '../core/git/service.js'
|
|
4
|
+
import { confirm } from '../core/tools/interactive.js'
|
|
5
|
+
import { logger } from '../core/tools/logger.js'
|
|
6
6
|
|
|
7
7
|
function unique(list) {
|
|
8
8
|
return [...new Set(list.map((item) => item.trim()).filter(Boolean))]
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { IcodeError } from '../core/errors.js'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { resolveGitContext } from '../core/git/context.js'
|
|
3
|
+
import { GitService } from '../core/git/service.js'
|
|
4
|
+
import { input } from '../core/tools/interactive.js'
|
|
5
|
+
import { logger } from '../core/tools/logger.js'
|
|
6
6
|
|
|
7
7
|
function todayStamp() {
|
|
8
8
|
const now = new Date()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { IcodeError } from '../core/errors.js'
|
|
2
|
-
import { resolveGitContext } from '../core/git
|
|
3
|
-
import { GitService } from '../core/git
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { resolveGitContext } from '../core/git/context.js'
|
|
3
|
+
import { GitService } from '../core/git/service.js'
|
|
4
|
+
import { chooseOne } from '../core/tools/interactive.js'
|
|
5
|
+
import { logger } from '../core/tools/logger.js'
|
|
6
6
|
import { runRollbackWorkflow } from './rollback-workflow.js'
|
|
7
7
|
|
|
8
8
|
const UNDO_OPTIONS = [
|
|
@@ -247,6 +247,10 @@ async function handleRevertConflict(options, mode, ref) {
|
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
/**
|
|
251
|
+
* 执行 undo 工作流。
|
|
252
|
+
* 输入为已归一化的回滚模式、目标 ref/hash、恢复策略和仓库选项,输出为执行结果或取消状态。
|
|
253
|
+
*/
|
|
250
254
|
export async function runUndoWorkflow(options) {
|
|
251
255
|
const pendingResult = await resolvePendingOperation(options)
|
|
252
256
|
if (pendingResult) {
|
|
@@ -255,6 +259,7 @@ export async function runUndoWorkflow(options) {
|
|
|
255
259
|
|
|
256
260
|
let mode = options.mode?.trim()
|
|
257
261
|
let ref = options.ref?.trim()
|
|
262
|
+
const hasExplicitRef = Boolean(ref)
|
|
258
263
|
|
|
259
264
|
if (!mode) {
|
|
260
265
|
const selected = await chooseOne('请选择回滚策略:', UNDO_OPTIONS, 0)
|
|
@@ -267,7 +272,9 @@ export async function runUndoWorkflow(options) {
|
|
|
267
272
|
}
|
|
268
273
|
|
|
269
274
|
mode = parsed.mode
|
|
270
|
-
|
|
275
|
+
if (!hasExplicitRef) {
|
|
276
|
+
ref = parsed.ref
|
|
277
|
+
}
|
|
271
278
|
}
|
|
272
279
|
|
|
273
280
|
const defaultRef = resolveDefaultRef(mode)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|