@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
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import { runCommand } from '../commands/shell.js'
|
|
3
|
+
import { logger } from '../tools/logger.js'
|
|
4
|
+
|
|
5
|
+
const require = createRequire(import.meta.url)
|
|
6
|
+
const packageJson = require('../../../package.json')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 规范化版本号,输出仅包含数字段的 semver 主体数组。
|
|
10
|
+
* 输入为字符串版本号,输出为数字数组;若格式不合法则返回 null。
|
|
11
|
+
*/
|
|
12
|
+
function normalizeVersionSegments(version) {
|
|
13
|
+
const normalized = String(version || '').trim()
|
|
14
|
+
if (!normalized) {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const coreVersion = normalized.split('-')[0]
|
|
19
|
+
if (!/^\d+(?:\.\d+)*$/.test(coreVersion)) {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return coreVersion.split('.').map((segment) => Number(segment))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 解析 npm view 返回的版本文本,兼容 JSON 字符串与纯文本两种输出。
|
|
28
|
+
* 输入为命令 stdout,输出为去空白后的版本号字符串。
|
|
29
|
+
*/
|
|
30
|
+
function parsePublishedVersion(stdout) {
|
|
31
|
+
const normalized = String(stdout || '').trim()
|
|
32
|
+
if (!normalized) {
|
|
33
|
+
return ''
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(normalized)
|
|
38
|
+
if (typeof parsed === 'string') {
|
|
39
|
+
return parsed.trim()
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// npm 在异常场景下也可能输出纯文本版本号,这里保留原始文本继续解析。
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return normalized.replace(/^"|"$/g, '').trim()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 比较两个版本号,判断远端版本是否严格高于当前版本。
|
|
50
|
+
* 输入为当前版本和远端版本,输出为布尔值。
|
|
51
|
+
*/
|
|
52
|
+
export function hasNewerVersion(currentVersion, latestVersion) {
|
|
53
|
+
const currentSegments = normalizeVersionSegments(currentVersion)
|
|
54
|
+
const latestSegments = normalizeVersionSegments(latestVersion)
|
|
55
|
+
|
|
56
|
+
if (!currentSegments || !latestSegments) {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const maxLength = Math.max(currentSegments.length, latestSegments.length)
|
|
61
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
62
|
+
const currentValue = currentSegments[index] || 0
|
|
63
|
+
const latestValue = latestSegments[index] || 0
|
|
64
|
+
|
|
65
|
+
if (latestValue > currentValue) {
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (latestValue < currentValue) {
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return false
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 查询 npm registry 上当前包的最新发布版本。
|
|
79
|
+
* 输入为可选 runner/packageName,输出为最新版本号;失败时返回空字符串。
|
|
80
|
+
*/
|
|
81
|
+
export async function fetchLatestPublishedVersion(options = {}) {
|
|
82
|
+
const packageName = options.packageName || packageJson.name
|
|
83
|
+
const runner = options.runner || runCommand
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const result = await runner('npm', ['view', packageName, 'version', '--json'], {
|
|
87
|
+
allowFailure: true,
|
|
88
|
+
env: {
|
|
89
|
+
...process.env,
|
|
90
|
+
NO_UPDATE_NOTIFIER: '1',
|
|
91
|
+
npm_config_update_notifier: 'false',
|
|
92
|
+
npm_config_loglevel: 'silent'
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
if (result.exitCode !== 0) {
|
|
97
|
+
return ''
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return parsePublishedVersion(result.stdout)
|
|
101
|
+
} catch {
|
|
102
|
+
return ''
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 在命令成功结束后提示可用更新版本。
|
|
108
|
+
* 输入为可选 runner/warn/packageName/currentVersion,输出为通知结果对象。
|
|
109
|
+
*/
|
|
110
|
+
export async function notifyIfCliUpdateAvailable(options = {}) {
|
|
111
|
+
const packageName = options.packageName || packageJson.name
|
|
112
|
+
const currentVersion = options.currentVersion || packageJson.version
|
|
113
|
+
const warn = options.warn || logger.warn
|
|
114
|
+
|
|
115
|
+
const latestVersion = await fetchLatestPublishedVersion({
|
|
116
|
+
runner: options.runner,
|
|
117
|
+
packageName
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
if (!latestVersion || !hasNewerVersion(currentVersion, latestVersion)) {
|
|
121
|
+
return {
|
|
122
|
+
notified: false,
|
|
123
|
+
packageName,
|
|
124
|
+
currentVersion,
|
|
125
|
+
latestVersion
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
warn(`发现新版本可用: ${currentVersion} -> ${latestVersion}`)
|
|
130
|
+
warn(`更新命令: npm i -g ${packageName}@latest`)
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
notified: true,
|
|
134
|
+
packageName,
|
|
135
|
+
currentVersion,
|
|
136
|
+
latestVersion
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
|
-
import { IcodeError } from '
|
|
2
|
+
import { IcodeError } from '../errors.js'
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* 把命令与参数拼成可读字符串,供日志与报错复用。
|
|
6
|
+
* 输入为命令名与参数数组,输出为单行命令文本。
|
|
7
|
+
*/
|
|
4
8
|
function stringifyCommand(command, args) {
|
|
5
9
|
return [command, ...args].join(' ')
|
|
6
10
|
}
|
|
7
11
|
|
|
12
|
+
/**
|
|
13
|
+
* 组合命令失败消息,尽量附带 stderr/stdout 便于定位问题。
|
|
14
|
+
* 输入为命令信息与执行结果,输出为最终错误文案。
|
|
15
|
+
*/
|
|
8
16
|
function formatFailureMessage(command, args, result) {
|
|
9
17
|
const baseMessage = `命令执行失败(${result.exitCode}): ${stringifyCommand(command, args)}`
|
|
10
18
|
const detailLines = [result.stderr, result.stdout].map((item) => item.trim()).filter(Boolean)
|
|
@@ -16,6 +24,10 @@ function formatFailureMessage(command, args, result) {
|
|
|
16
24
|
return `${baseMessage}\n${detailLines.join('\n')}`
|
|
17
25
|
}
|
|
18
26
|
|
|
27
|
+
/**
|
|
28
|
+
* 执行外部命令并收集 stdout/stderr;允许按需把非零退出码作为普通结果返回。
|
|
29
|
+
* 输入为命令名、参数数组与执行选项,输出为标准化执行结果。
|
|
30
|
+
*/
|
|
19
31
|
export async function runCommand(command, args = [], options = {}) {
|
|
20
32
|
const { cwd = process.cwd(), env = process.env, allowFailure = false } = options
|
|
21
33
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import os from 'node:os'
|
|
3
3
|
import path from 'node:path'
|
|
4
|
-
import { IcodeError } from '
|
|
4
|
+
import { IcodeError } from '../errors.js'
|
|
5
5
|
|
|
6
6
|
const DEFAULT_CONFIG = {
|
|
7
7
|
version: 1,
|
|
@@ -9,6 +9,12 @@ const DEFAULT_CONFIG = {
|
|
|
9
9
|
repoMode: 'auto',
|
|
10
10
|
defaultMainBranches: ['main', 'master']
|
|
11
11
|
},
|
|
12
|
+
platforms: {
|
|
13
|
+
remoteMerge: {
|
|
14
|
+
provider: '',
|
|
15
|
+
apiKey: ''
|
|
16
|
+
}
|
|
17
|
+
},
|
|
12
18
|
ai: {
|
|
13
19
|
activeProfile: '',
|
|
14
20
|
profiles: {},
|
|
@@ -74,6 +80,14 @@ export function readConfig() {
|
|
|
74
80
|
...cloneDefault().defaults,
|
|
75
81
|
...(parsed.defaults || {})
|
|
76
82
|
},
|
|
83
|
+
platforms: {
|
|
84
|
+
...cloneDefault().platforms,
|
|
85
|
+
...(parsed.platforms || {}),
|
|
86
|
+
remoteMerge: {
|
|
87
|
+
...cloneDefault().platforms.remoteMerge,
|
|
88
|
+
...(parsed.platforms?.remoteMerge || {})
|
|
89
|
+
}
|
|
90
|
+
},
|
|
77
91
|
ai: {
|
|
78
92
|
...cloneDefault().ai,
|
|
79
93
|
...(parsed.ai || {}),
|
|
@@ -176,8 +190,13 @@ function normalizeRepoKey(repoRootPath) {
|
|
|
176
190
|
export function getRepoPolicy(repoRootPath) {
|
|
177
191
|
const key = normalizeRepoKey(repoRootPath)
|
|
178
192
|
const config = readConfig()
|
|
179
|
-
return
|
|
180
|
-
protectedBranches: []
|
|
193
|
+
return {
|
|
194
|
+
protectedBranches: [],
|
|
195
|
+
remoteMerge: {
|
|
196
|
+
enabled: true,
|
|
197
|
+
projectUrl: ''
|
|
198
|
+
},
|
|
199
|
+
...(config.repositories[key] || {})
|
|
181
200
|
}
|
|
182
201
|
}
|
|
183
202
|
|
|
@@ -186,9 +205,42 @@ export function setRepoPolicy(repoRootPath, policy) {
|
|
|
186
205
|
const config = readConfig()
|
|
187
206
|
config.repositories[key] = {
|
|
188
207
|
protectedBranches: [],
|
|
208
|
+
remoteMerge: {
|
|
209
|
+
enabled: true,
|
|
210
|
+
projectUrl: ''
|
|
211
|
+
},
|
|
189
212
|
...(config.repositories[key] || {}),
|
|
190
213
|
...policy
|
|
191
214
|
}
|
|
192
215
|
writeConfig(config)
|
|
193
216
|
return config.repositories[key]
|
|
194
217
|
}
|
|
218
|
+
|
|
219
|
+
export function getPlatformConfig(platformName) {
|
|
220
|
+
const config = readConfig()
|
|
221
|
+
const name = String(platformName || '').trim()
|
|
222
|
+
if (!name) {
|
|
223
|
+
return {}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return config.platforms?.[name] || {}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function setPlatformConfig(platformName, value) {
|
|
230
|
+
const config = readConfig()
|
|
231
|
+
const name = String(platformName || '').trim()
|
|
232
|
+
if (!name) {
|
|
233
|
+
throw new IcodeError('平台名称不能为空', { code: 'CONFIG_PLATFORM_EMPTY', exitCode: 2 })
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
config.platforms = {
|
|
237
|
+
...(config.platforms || {}),
|
|
238
|
+
[name]: {
|
|
239
|
+
...(config.platforms?.[name] || {}),
|
|
240
|
+
...(value || {})
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
writeConfig(config)
|
|
245
|
+
return config.platforms[name]
|
|
246
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { runCommand } from '../commands/shell.js'
|
|
4
|
+
import { readConfig } from '../config/store.js'
|
|
5
|
+
import { IcodeError } from '../errors.js'
|
|
6
|
+
import { confirm } from '../tools/interactive.js'
|
|
6
7
|
|
|
7
8
|
function cleanOutput(text) {
|
|
8
9
|
return (text || '').trim()
|
|
@@ -13,6 +14,24 @@ async function commandOutput(cwd, args, allowFailure = false) {
|
|
|
13
14
|
return result
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
async function ensureGitRepository(cwd, options = {}) {
|
|
18
|
+
const confirmInit = options.confirm || confirm
|
|
19
|
+
const inside = await commandOutput(cwd, ['rev-parse', '--is-inside-work-tree'], true)
|
|
20
|
+
if (inside.exitCode === 0 && cleanOutput(inside.stdout) === 'true') {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const shouldInit = await confirmInit('当前目录还没有初始化 Git 仓库,是否现在执行 git init?')
|
|
25
|
+
if (!shouldInit) {
|
|
26
|
+
throw new IcodeError('当前目录不在 Git 仓库中。', {
|
|
27
|
+
code: 'NOT_IN_GIT_REPO',
|
|
28
|
+
exitCode: 2
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await commandOutput(cwd, ['init'])
|
|
33
|
+
}
|
|
34
|
+
|
|
16
35
|
async function detectDefaultBranch(topLevelPath, fallbackCandidates = ['main', 'master']) {
|
|
17
36
|
const headRefResult = await commandOutput(topLevelPath, ['symbolic-ref', 'refs/remotes/origin/HEAD'], true)
|
|
18
37
|
if (headRefResult.exitCode === 0) {
|
|
@@ -44,13 +63,9 @@ export async function resolveGitContext(options = {}) {
|
|
|
44
63
|
const configRepoMode = config.defaults?.repoMode || 'auto'
|
|
45
64
|
const repoMode = options.repoMode || configRepoMode
|
|
46
65
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
code: 'NOT_IN_GIT_REPO',
|
|
51
|
-
exitCode: 2
|
|
52
|
-
})
|
|
53
|
-
}
|
|
66
|
+
await ensureGitRepository(cwd, {
|
|
67
|
+
confirm: options.confirm
|
|
68
|
+
})
|
|
54
69
|
|
|
55
70
|
const topLevelPath = cleanOutput((await commandOutput(cwd, ['rev-parse', '--show-toplevel'])).stdout)
|
|
56
71
|
const gitDirRaw = cleanOutput((await commandOutput(cwd, ['rev-parse', '--git-dir'])).stdout)
|
|
@@ -1,14 +1,30 @@
|
|
|
1
|
-
import { IcodeError } from './errors.js'
|
|
2
|
-
import { buildHookHint, detectHookFailure } from './hook-diagnostics.js'
|
|
3
|
-
import { logger } from './logger.js'
|
|
4
|
-
import { runCommand } from './shell.js'
|
|
5
1
|
import fs from 'node:fs'
|
|
6
2
|
import path from 'node:path'
|
|
3
|
+
import { buildHookHint, detectHookFailure } from './hook-diagnostics.js'
|
|
4
|
+
import { runCommand } from '../commands/shell.js'
|
|
5
|
+
import { IcodeError } from '../errors.js'
|
|
6
|
+
import { logger } from '../tools/logger.js'
|
|
7
7
|
|
|
8
8
|
function cleanOutput(text) {
|
|
9
9
|
return (text || '').trim()
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
function parseGitLogRecords(output) {
|
|
13
|
+
return output
|
|
14
|
+
.split('\x1e')
|
|
15
|
+
.map((record) => record.trim())
|
|
16
|
+
.filter(Boolean)
|
|
17
|
+
.map((record) => {
|
|
18
|
+
const [hash = '', subject = '', body = ''] = record.split('\x1f')
|
|
19
|
+
return {
|
|
20
|
+
hash: hash.trim(),
|
|
21
|
+
subject: subject.trim(),
|
|
22
|
+
body: body.trim()
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
.filter((item) => item.hash && item.subject)
|
|
26
|
+
}
|
|
27
|
+
|
|
12
28
|
export class GitService {
|
|
13
29
|
constructor(context) {
|
|
14
30
|
this.context = context
|
|
@@ -77,6 +93,13 @@ export class GitService {
|
|
|
77
93
|
return result.exitCode === 0
|
|
78
94
|
}
|
|
79
95
|
|
|
96
|
+
async getRemoteUrl(remoteName = 'origin') {
|
|
97
|
+
const result = await this.exec(['remote', 'get-url', remoteName], {
|
|
98
|
+
allowFailure: true
|
|
99
|
+
})
|
|
100
|
+
return cleanOutput(result.stdout)
|
|
101
|
+
}
|
|
102
|
+
|
|
80
103
|
async listLocalBranches() {
|
|
81
104
|
const result = await this.exec(['for-each-ref', '--format=%(refname:short)', 'refs/heads'])
|
|
82
105
|
return result.stdout
|
|
@@ -162,18 +185,50 @@ export class GitService {
|
|
|
162
185
|
}
|
|
163
186
|
|
|
164
187
|
async statusPorcelain() {
|
|
188
|
+
// 返回标准 porcelain 状态,供上层统一判断“是否存在未提交改动”。
|
|
165
189
|
const result = await this.exec(['status', '--porcelain'])
|
|
166
190
|
return result.stdout
|
|
167
191
|
}
|
|
168
192
|
|
|
169
193
|
async hasChanges() {
|
|
194
|
+
// 只要 porcelain 有输出,就说明暂存区、工作区或未跟踪文件存在改动。
|
|
170
195
|
const output = await this.statusPorcelain()
|
|
171
196
|
return cleanOutput(output).length > 0
|
|
172
197
|
}
|
|
173
198
|
|
|
174
|
-
|
|
175
|
-
|
|
199
|
+
// 列出未被忽略的未跟踪文件,供 AI 工作流补全新文件 diff。
|
|
200
|
+
async listUntrackedFiles() {
|
|
201
|
+
const result = await this.exec(['ls-files', '--others', '--exclude-standard'])
|
|
176
202
|
return result.stdout
|
|
203
|
+
.split('\n')
|
|
204
|
+
.map((item) => item.trim())
|
|
205
|
+
.filter(Boolean)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 通过 no-index diff 生成“/dev/null -> 新文件”的补丁,覆盖未跟踪文件内容。
|
|
209
|
+
async buildUntrackedFileDiff(filePath) {
|
|
210
|
+
const result = await this.exec(['diff', '--no-index', '--binary', '--', '/dev/null', filePath], {
|
|
211
|
+
allowFailure: true
|
|
212
|
+
})
|
|
213
|
+
return result.stdout
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async diffWorkingTree() {
|
|
217
|
+
// git diff 默认不包含 untracked 文件,这里显式补齐,避免新仓库被误判成“无改动”。
|
|
218
|
+
const trackedResult = await this.exec(['diff'])
|
|
219
|
+
const untrackedFiles = await this.listUntrackedFiles()
|
|
220
|
+
const untrackedDiffs = []
|
|
221
|
+
|
|
222
|
+
for (const filePath of untrackedFiles) {
|
|
223
|
+
const diff = await this.buildUntrackedFileDiff(filePath)
|
|
224
|
+
if (cleanOutput(diff)) {
|
|
225
|
+
untrackedDiffs.push(diff.trimEnd())
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return [trackedResult.stdout.trimEnd(), ...untrackedDiffs]
|
|
230
|
+
.filter(Boolean)
|
|
231
|
+
.join('\n\n')
|
|
177
232
|
}
|
|
178
233
|
|
|
179
234
|
async diffStaged() {
|
|
@@ -399,6 +454,23 @@ export class GitService {
|
|
|
399
454
|
.filter(Boolean)
|
|
400
455
|
}
|
|
401
456
|
|
|
457
|
+
// 读取指定 range 内的提交信息,返回按时间顺序排列的 hash/subject/body 列表。
|
|
458
|
+
async listCommitMessages(rangeSpec) {
|
|
459
|
+
const result = await this.exec(['log', '--reverse', '--format=%H%x1f%s%x1f%b%x1e', rangeSpec], {
|
|
460
|
+
allowFailure: true
|
|
461
|
+
})
|
|
462
|
+
return parseGitLogRecords(result.stdout)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// 读取单个 ref 的最新提交信息,供无法从 range 提取提交时回退使用。
|
|
466
|
+
async getCommitMessage(ref = 'HEAD') {
|
|
467
|
+
const result = await this.exec(['log', '-1', '--format=%H%x1f%s%x1f%b%x1e', ref], {
|
|
468
|
+
allowFailure: true
|
|
469
|
+
})
|
|
470
|
+
const commits = parseGitLogRecords(result.stdout)
|
|
471
|
+
return commits[0] || null
|
|
472
|
+
}
|
|
473
|
+
|
|
402
474
|
async cherryPick(commits) {
|
|
403
475
|
const normalized = Array.isArray(commits) ? commits : [commits]
|
|
404
476
|
const list = normalized.map((item) => item.trim()).filter(Boolean)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { IcodeError } from '../errors.js'
|
|
2
|
+
import { requestGitLabRemoteMerge, resolveGitLabProjectUrlFromRemote } from './providers/gitlab.js'
|
|
3
|
+
|
|
4
|
+
const REMOTE_MERGE_PROVIDERS = {
|
|
5
|
+
gitlab: {
|
|
6
|
+
requestRemoteMerge: requestGitLabRemoteMerge,
|
|
7
|
+
resolveProjectUrlFromRemote: resolveGitLabProjectUrlFromRemote
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 按 provider 读取远程合并实现,缺失或不支持时统一抛出明确错误。
|
|
12
|
+
function getRemoteMergeProvider(provider) {
|
|
13
|
+
const normalizedProvider = String(provider || '').trim()
|
|
14
|
+
if (!normalizedProvider) {
|
|
15
|
+
throw new IcodeError('远程合并缺少 provider', {
|
|
16
|
+
code: 'REMOTE_MERGE_PROVIDER_EMPTY',
|
|
17
|
+
exitCode: 2
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const handler = REMOTE_MERGE_PROVIDERS[normalizedProvider]
|
|
22
|
+
if (!handler) {
|
|
23
|
+
throw new IcodeError(`当前仅支持: ${Object.keys(REMOTE_MERGE_PROVIDERS).join(', ')},收到 provider=${normalizedProvider}`, {
|
|
24
|
+
code: 'REMOTE_MERGE_PROVIDER_UNSUPPORTED',
|
|
25
|
+
exitCode: 2
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return handler
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 统一根据 provider 从 remote URL 解析项目地址,避免工作流层感知平台细节。
|
|
33
|
+
export function resolveRemoteMergeProjectUrl(options = {}) {
|
|
34
|
+
const handler = getRemoteMergeProvider(options.provider)
|
|
35
|
+
return handler.resolveProjectUrlFromRemote(options.remoteUrl || '')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 统一按 provider 调度远程合并请求,后续新增平台只需补 provider 实现。
|
|
39
|
+
export async function requestRemoteMerge(options = {}) {
|
|
40
|
+
const handler = getRemoteMergeProvider(options.provider)
|
|
41
|
+
return handler.requestRemoteMerge(options)
|
|
42
|
+
}
|