@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
package/src/commands/migrate.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util'
|
|
2
2
|
import { normalizeLegacyArgs } from '../core/cli/args.js'
|
|
3
3
|
import { IcodeError } from '../core/errors.js'
|
|
4
|
+
import { c, keyValues, print, title, ui } from '../core/cli/render.js'
|
|
4
5
|
import { logger } from '../core/tools/logger.js'
|
|
5
6
|
import { runMigrateWorkflow } from '../workflows/migrate-workflow.js'
|
|
6
7
|
|
|
@@ -42,18 +43,26 @@ function printDryRunPlan(result) {
|
|
|
42
43
|
const previewLimit = 10
|
|
43
44
|
const previewCommits = result.commits.slice(0, previewLimit)
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
print(title('迁移计划(dry-run)'))
|
|
47
|
+
print(keyValues([
|
|
48
|
+
['来源', `${ui.branch(result.sourceBranch)} ${c.gray(`(${result.sourceRef})`)}`],
|
|
49
|
+
['目标', `${ui.branch(result.targetBranch)} ${c.gray(`(${result.targetRef})`)}`],
|
|
50
|
+
['范围', c.cyan(result.rangeSpec)],
|
|
51
|
+
['提交数', c.bold(String(result.migratedCommits))]
|
|
52
|
+
]))
|
|
53
|
+
if (previewCommits.length) {
|
|
54
|
+
print('')
|
|
55
|
+
previewCommits.forEach((commit) => {
|
|
56
|
+
const [hash, ...rest] = String(commit.summary || commit.hash).split(' ')
|
|
57
|
+
print(` ${ui.hash(hash)} ${ui.subject(rest.join(' '))}`)
|
|
58
|
+
})
|
|
59
|
+
}
|
|
52
60
|
if (result.commits.length > previewLimit) {
|
|
53
|
-
|
|
61
|
+
print(c.gray(` … 还有 ${result.commits.length - previewLimit} 个提交,可用 --json 查看完整计划`))
|
|
54
62
|
}
|
|
63
|
+
print('')
|
|
55
64
|
result.steps.forEach((step, index) => {
|
|
56
|
-
|
|
65
|
+
print(` ${c.gray(`${index + 1}.`)} ${step.detail}`)
|
|
57
66
|
})
|
|
58
67
|
}
|
|
59
68
|
|
|
@@ -117,7 +126,5 @@ export async function runMigrateCommand(rawArgs) {
|
|
|
117
126
|
return result
|
|
118
127
|
}
|
|
119
128
|
|
|
120
|
-
logger.success(
|
|
121
|
-
`migrate 完成: ${result.sourceBranch} -> ${result.targetBranch}, commits=${result.migratedCommits}, mode=${result.rangeMode || 'range'}`
|
|
122
|
-
)
|
|
129
|
+
logger.success(`已迁移 ${c.bold(String(result.migratedCommits))} 个提交: ${ui.branch(result.sourceBranch)} ${c.gray('→')} ${ui.branch(result.targetBranch)}${result.pushed ? c.gray(',已推送') : ''}`)
|
|
123
130
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { IcodeError } from '../core/errors.js'
|
|
4
|
+
import { parseCommandArgs, printJson } from '../core/cli/output.js'
|
|
5
|
+
import { resolveGitContext } from '../core/git/context.js'
|
|
6
|
+
import { GitService } from '../core/git/service.js'
|
|
7
|
+
import { createPlatformClientForGit } from '../core/platform/index.js'
|
|
8
|
+
import { openInBrowser } from '../core/tools/browser.js'
|
|
9
|
+
import { logger } from '../core/tools/logger.js'
|
|
10
|
+
|
|
11
|
+
function printHelp() {
|
|
12
|
+
process.stdout.write(`
|
|
13
|
+
Usage:
|
|
14
|
+
icode open [target] [options]
|
|
15
|
+
|
|
16
|
+
Targets:
|
|
17
|
+
(不传) 当前分支页面(在主分支上时打开仓库首页)
|
|
18
|
+
repo 仓库首页
|
|
19
|
+
<file>[:line] 文件(当前分支),例如 src/cli.js:42
|
|
20
|
+
commit [ref] 提交页面(默认 HEAD)
|
|
21
|
+
compare [base] 当前分支与 base 的对比(默认主分支)
|
|
22
|
+
pr 当前分支的 PR/MR;没有时打开创建页面
|
|
23
|
+
prs PR/MR 列表
|
|
24
|
+
ci CI / 流水线页面
|
|
25
|
+
releases Release 页面
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
-p, --print 只打印链接,不打开浏览器
|
|
29
|
+
--branch <name> 指定分支(默认当前分支)
|
|
30
|
+
--json 输出机器可读 JSON
|
|
31
|
+
--repo-mode <mode> 仓库模式: auto | strict
|
|
32
|
+
-h, --help 查看帮助
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
icode open
|
|
36
|
+
icode open src/cli.js:120
|
|
37
|
+
icode open pr
|
|
38
|
+
icode open compare release -p
|
|
39
|
+
`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 把用户给的文件路径转换成仓库根目录相对路径,并解析 :行号 后缀。
|
|
43
|
+
function resolveFileTarget(context, target) {
|
|
44
|
+
const match = String(target).match(/^(.*?)(?::(\d+))?$/)
|
|
45
|
+
const filePart = match?.[1] || target
|
|
46
|
+
const line = match?.[2] ? Number(match[2]) : 0
|
|
47
|
+
const absolute = path.resolve(process.cwd(), filePart)
|
|
48
|
+
if (!fs.existsSync(absolute)) {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
const relative = path.relative(context.topLevelPath, absolute).split(path.sep).join('/')
|
|
52
|
+
if (relative.startsWith('..')) {
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
return { file: relative, line }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function resolveUrl(git, context, client, target, options) {
|
|
59
|
+
const branch = options.branch || (await git.getCurrentBranch()) || context.defaultBranch
|
|
60
|
+
const urls = client.urls
|
|
61
|
+
|
|
62
|
+
switch (target) {
|
|
63
|
+
case '':
|
|
64
|
+
return branch && branch !== context.defaultBranch ? urls.branch(branch) : urls.repo()
|
|
65
|
+
case 'repo':
|
|
66
|
+
return urls.repo()
|
|
67
|
+
case 'prs':
|
|
68
|
+
case 'mrs':
|
|
69
|
+
return urls.prs()
|
|
70
|
+
case 'ci':
|
|
71
|
+
case 'actions':
|
|
72
|
+
case 'pipelines':
|
|
73
|
+
return urls.pipelines()
|
|
74
|
+
case 'releases':
|
|
75
|
+
return urls.releases()
|
|
76
|
+
case 'commit': {
|
|
77
|
+
const sha = await git.revParse(options.ref || 'HEAD')
|
|
78
|
+
if (!sha) {
|
|
79
|
+
throw new IcodeError(`提交不存在: ${options.ref}`, { code: 'OPEN_REF_NOT_FOUND', exitCode: 2 })
|
|
80
|
+
}
|
|
81
|
+
return urls.commit(sha)
|
|
82
|
+
}
|
|
83
|
+
case 'compare':
|
|
84
|
+
return urls.compare(options.ref || context.defaultBranch, branch)
|
|
85
|
+
case 'pr':
|
|
86
|
+
case 'mr': {
|
|
87
|
+
if (client.hasToken) {
|
|
88
|
+
try {
|
|
89
|
+
const [pr] = await client.listPullRequests({ head: branch, state: 'open' })
|
|
90
|
+
if (pr) {
|
|
91
|
+
return pr.url || urls.pr(pr.number)
|
|
92
|
+
}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
logger.debug(`查询 PR 失败,改为打开创建页面: ${error.message}`)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return urls.newPr(context.defaultBranch, branch)
|
|
98
|
+
}
|
|
99
|
+
default: {
|
|
100
|
+
const fileTarget = resolveFileTarget(context, target)
|
|
101
|
+
if (!fileTarget) {
|
|
102
|
+
throw new IcodeError(`无法识别的目标: ${target}(不是已存在的文件,也不是 repo/pr/commit/compare/ci/releases)`, {
|
|
103
|
+
code: 'OPEN_TARGET_UNKNOWN',
|
|
104
|
+
exitCode: 2
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
return urls.file(branch, fileTarget.file, fileTarget.line)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function runOpenCommand(rawArgs) {
|
|
113
|
+
const { values, positionals } = parseCommandArgs(rawArgs, {
|
|
114
|
+
print: { type: 'boolean', short: 'p', default: false },
|
|
115
|
+
branch: { type: 'string' }
|
|
116
|
+
})
|
|
117
|
+
if (values.help) {
|
|
118
|
+
printHelp()
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const context = await resolveGitContext({ repoMode: values['repo-mode'] })
|
|
123
|
+
const git = new GitService(context)
|
|
124
|
+
const client = await createPlatformClientForGit(git)
|
|
125
|
+
const [target = '', ref] = positionals
|
|
126
|
+
const url = await resolveUrl(git, context, client, target, { branch: values.branch, ref })
|
|
127
|
+
|
|
128
|
+
if (values.json) {
|
|
129
|
+
printJson({ url, provider: client.name })
|
|
130
|
+
return { url }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
process.stdout.write(`${url}\n`)
|
|
134
|
+
if (!values.print) {
|
|
135
|
+
openInBrowser(url)
|
|
136
|
+
}
|
|
137
|
+
return { url }
|
|
138
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { IcodeError } from '../core/errors.js'
|
|
2
|
+
import { parseCommandArgs, printJson } from '../core/cli/output.js'
|
|
3
|
+
import { c, keyValues, print, table, ui } from '../core/cli/render.js'
|
|
4
|
+
import { openInBrowser } from '../core/tools/browser.js'
|
|
5
|
+
import { confirm, isInteractiveTerminal } from '../core/tools/interactive.js'
|
|
6
|
+
import { logger } from '../core/tools/logger.js'
|
|
7
|
+
import {
|
|
8
|
+
runPrCheckoutWorkflow,
|
|
9
|
+
runPrCreateWorkflow,
|
|
10
|
+
runPrListWorkflow,
|
|
11
|
+
runPrMergeWorkflow,
|
|
12
|
+
runPrViewWorkflow
|
|
13
|
+
} from '../workflows/pr-workflow.js'
|
|
14
|
+
|
|
15
|
+
function printHelp() {
|
|
16
|
+
process.stdout.write(`
|
|
17
|
+
Usage:
|
|
18
|
+
icode pr create [options] 为当前分支创建 PR/MR(会先推送当前分支)
|
|
19
|
+
icode pr list [options] 列出 PR/MR
|
|
20
|
+
icode pr view [number] 查看 PR/MR(默认当前分支对应的)
|
|
21
|
+
icode pr checkout <number> 检出 PR/MR 到本地分支 pr/<number>
|
|
22
|
+
icode pr merge [number] 合并 PR/MR(会确认)
|
|
23
|
+
icode pr open [number] 在浏览器打开 PR/MR(无编号时打开列表)
|
|
24
|
+
|
|
25
|
+
Options:
|
|
26
|
+
-b, --base <branch> 目标分支(默认主分支)
|
|
27
|
+
-t, --title <text> 标题(默认取提交信息)
|
|
28
|
+
--body <text> 正文
|
|
29
|
+
--draft 创建草稿
|
|
30
|
+
--ai 用 AI 生成标题和描述
|
|
31
|
+
--profile <name> AI profile(配合 --ai)
|
|
32
|
+
--lang <zh|en> AI 输出语言
|
|
33
|
+
--no-push 不自动推送当前分支
|
|
34
|
+
--web 不调 API,直接在浏览器打开创建页面
|
|
35
|
+
--state <state> list 的状态: open | closed | merged | all
|
|
36
|
+
--method <method> merge 方式: merge | squash | rebase
|
|
37
|
+
--branch <name> checkout 的本地分支名
|
|
38
|
+
--provider <name> 强制指定平台: github | gitlab | gitee | gitea
|
|
39
|
+
--json 输出机器可读 JSON
|
|
40
|
+
-y, --yes 自动确认
|
|
41
|
+
--repo-mode <mode> 仓库模式: auto | strict
|
|
42
|
+
-h, --help 查看帮助
|
|
43
|
+
|
|
44
|
+
Notes:
|
|
45
|
+
平台按 remote 地址自动识别。需要 token: icode config platform token set <provider> <token>
|
|
46
|
+
(也可用 GITHUB_TOKEN / GITLAB_TOKEN 等环境变量)。没有 token 时 create 会打开网页创建页面。
|
|
47
|
+
|
|
48
|
+
Examples:
|
|
49
|
+
icode pr create
|
|
50
|
+
icode pr create -b release --ai
|
|
51
|
+
icode pr list --state all
|
|
52
|
+
icode pr checkout 42
|
|
53
|
+
icode pr merge 42 --method squash -y
|
|
54
|
+
`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatState(pr) {
|
|
58
|
+
if (pr.merged) {
|
|
59
|
+
return c.magenta('已合并')
|
|
60
|
+
}
|
|
61
|
+
if (pr.state === 'open') {
|
|
62
|
+
return pr.draft ? c.gray('草稿') : c.green('打开')
|
|
63
|
+
}
|
|
64
|
+
return c.gray(pr.state === 'closed' ? '已关闭' : pr.state)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function printPr(pr, noun) {
|
|
68
|
+
print(` ${c.yellow(`${noun} #${pr.number}`)} ${c.bold(ui.subject(pr.title))}`)
|
|
69
|
+
print(keyValues([
|
|
70
|
+
['分支', `${ui.branch(pr.head)} ${c.gray('→')} ${ui.branch(pr.base)}`],
|
|
71
|
+
['状态', `${formatState(pr)}${pr.conflict ? ` ${c.red('存在冲突')}` : ''}`],
|
|
72
|
+
['作者', pr.author ? ui.author(`@${pr.author}`) : ''],
|
|
73
|
+
['链接', ui.url(pr.url)]
|
|
74
|
+
], { indent: 4 }))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function runPrCommand(rawArgs) {
|
|
78
|
+
const { values, positionals } = parseCommandArgs(rawArgs, {
|
|
79
|
+
base: { type: 'string', short: 'b' },
|
|
80
|
+
title: { type: 'string', short: 't' },
|
|
81
|
+
body: { type: 'string' },
|
|
82
|
+
draft: { type: 'boolean', default: false },
|
|
83
|
+
ai: { type: 'boolean', default: false },
|
|
84
|
+
profile: { type: 'string' },
|
|
85
|
+
lang: { type: 'string', default: 'zh' },
|
|
86
|
+
'no-push': { type: 'boolean', default: false },
|
|
87
|
+
'no-verify': { type: 'boolean', default: false },
|
|
88
|
+
web: { type: 'boolean', default: false },
|
|
89
|
+
state: { type: 'string' },
|
|
90
|
+
method: { type: 'string' },
|
|
91
|
+
branch: { type: 'string' },
|
|
92
|
+
provider: { type: 'string' },
|
|
93
|
+
limit: { type: 'string' }
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const [subcommand = '', target] = positionals
|
|
97
|
+
if (values.help || !subcommand) {
|
|
98
|
+
printHelp()
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const base = { repoMode: values['repo-mode'], provider: values.provider }
|
|
103
|
+
|
|
104
|
+
if (subcommand === 'create') {
|
|
105
|
+
const result = await runPrCreateWorkflow({
|
|
106
|
+
...base,
|
|
107
|
+
base: values.base,
|
|
108
|
+
title: values.title,
|
|
109
|
+
body: values.body,
|
|
110
|
+
draft: values.draft,
|
|
111
|
+
ai: values.ai,
|
|
112
|
+
profile: values.profile,
|
|
113
|
+
lang: values.lang,
|
|
114
|
+
noPush: values['no-push'],
|
|
115
|
+
noVerify: values['no-verify'],
|
|
116
|
+
web: values.web,
|
|
117
|
+
interactive: !values.json && !values.yes && isInteractiveTerminal()
|
|
118
|
+
})
|
|
119
|
+
if (values.json) {
|
|
120
|
+
printJson(result)
|
|
121
|
+
return result
|
|
122
|
+
}
|
|
123
|
+
if (result.canceled) {
|
|
124
|
+
logger.warn('已取消。')
|
|
125
|
+
return result
|
|
126
|
+
}
|
|
127
|
+
if (!result.created && !result.existed) {
|
|
128
|
+
if (result.reason === 'no-token') {
|
|
129
|
+
logger.warn(`未配置 ${result.provider} token,改为在浏览器中创建。配置: icode config platform token set ${result.provider} <token>`)
|
|
130
|
+
}
|
|
131
|
+
logger.info(`在浏览器中完成创建: ${ui.url(result.webUrl)}`)
|
|
132
|
+
openInBrowser(result.webUrl)
|
|
133
|
+
return result
|
|
134
|
+
}
|
|
135
|
+
if (result.existed) {
|
|
136
|
+
logger.info('该分支已有打开的 PR/MR:')
|
|
137
|
+
} else {
|
|
138
|
+
logger.success('已创建:')
|
|
139
|
+
}
|
|
140
|
+
printPr(result.pr, result.provider === 'gitlab' ? 'MR' : 'PR')
|
|
141
|
+
return result
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (subcommand === 'list' || subcommand === 'ls') {
|
|
145
|
+
const result = await runPrListWorkflow({ ...base, state: values.state, base: values.base, limit: values.limit ? Number(values.limit) : undefined })
|
|
146
|
+
if (values.json) {
|
|
147
|
+
printJson(result)
|
|
148
|
+
return result
|
|
149
|
+
}
|
|
150
|
+
if (!result.items.length) {
|
|
151
|
+
logger.info(`没有${values.state && values.state !== 'open' ? '' : '打开的'} ${result.prNoun}。`)
|
|
152
|
+
return result
|
|
153
|
+
}
|
|
154
|
+
print(table(result.items, [
|
|
155
|
+
{ key: 'number', label: '#', format: (value) => c.yellow(`#${value}`) },
|
|
156
|
+
{ key: 'title', label: '标题', maxWidth: 50, format: (value) => ui.subject(value) },
|
|
157
|
+
{ key: 'head', label: '分支', format: (_, pr) => `${ui.branch(pr.head)} ${c.gray('→')} ${ui.branch(pr.base)}` },
|
|
158
|
+
{ key: 'state', label: '状态', format: (_, pr) => formatState(pr) + (pr.conflict ? ` ${c.red('冲突')}` : '') },
|
|
159
|
+
{ key: 'author', label: '作者', format: (value) => ui.author(value ? `@${value}` : '') }
|
|
160
|
+
]))
|
|
161
|
+
return result
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (subcommand === 'view') {
|
|
165
|
+
const result = await runPrViewWorkflow({ ...base, number: target })
|
|
166
|
+
if (values.json) {
|
|
167
|
+
printJson(result)
|
|
168
|
+
return result
|
|
169
|
+
}
|
|
170
|
+
printPr(result.pr, result.prNoun)
|
|
171
|
+
return result
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (subcommand === 'open') {
|
|
175
|
+
// 有编号打开该 PR;没有编号时先找当前分支的 PR,找不到就打开列表页。
|
|
176
|
+
const result = await runPrViewWorkflow({ ...base, number: target }).catch((error) => {
|
|
177
|
+
if (!target && error.code === 'PR_NOT_FOUND') {
|
|
178
|
+
return null
|
|
179
|
+
}
|
|
180
|
+
throw error
|
|
181
|
+
})
|
|
182
|
+
const url = result ? result.pr.url : (await runPrListWorkflow(base)).webUrl
|
|
183
|
+
logger.info(ui.url(url))
|
|
184
|
+
openInBrowser(url)
|
|
185
|
+
return { url }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (subcommand === 'checkout' || subcommand === 'co') {
|
|
189
|
+
if (!target) {
|
|
190
|
+
throw new IcodeError('缺少编号: icode pr checkout <number>', { code: 'PR_NUMBER_REQUIRED', exitCode: 2 })
|
|
191
|
+
}
|
|
192
|
+
const result = await runPrCheckoutWorkflow({ ...base, number: target, branch: values.branch })
|
|
193
|
+
if (values.json) {
|
|
194
|
+
printJson(result)
|
|
195
|
+
return result
|
|
196
|
+
}
|
|
197
|
+
logger.success(`已检出到本地分支 ${ui.branch(result.branch)}`)
|
|
198
|
+
return result
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (subcommand === 'merge') {
|
|
202
|
+
if (!values.yes) {
|
|
203
|
+
const accepted = await confirm(`确认合并 ${target ? `#${target}` : '当前分支的 PR/MR'}?`, false)
|
|
204
|
+
if (!accepted) {
|
|
205
|
+
logger.warn('已取消。')
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const result = await runPrMergeWorkflow({ ...base, number: target, method: values.method })
|
|
210
|
+
if (values.json) {
|
|
211
|
+
printJson(result)
|
|
212
|
+
return result
|
|
213
|
+
}
|
|
214
|
+
logger.success(result.alreadyMerged ? `#${result.number} 之前已合并。` : `#${result.number} 已合并。`)
|
|
215
|
+
return result
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
throw new IcodeError(`未知 pr 子命令: ${subcommand}`, { code: 'PR_SUBCOMMAND_UNKNOWN', exitCode: 2 })
|
|
219
|
+
}
|
package/src/commands/push.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util'
|
|
2
2
|
import { getAiCommandOptions } from '../core/ai/config.js'
|
|
3
3
|
import { normalizeLegacyArgs } from '../core/cli/args.js'
|
|
4
|
+
import { c, keyValues, print, renderBranchResults, title, ui } from '../core/cli/render.js'
|
|
4
5
|
import { logger } from '../core/tools/logger.js'
|
|
5
6
|
import { runPushWorkflow } from '../workflows/push-workflow.js'
|
|
6
7
|
|
|
@@ -16,22 +17,14 @@ const PUSH_PARSE_OPTIONS = {
|
|
|
16
17
|
'pull-main': { type: 'boolean' },
|
|
17
18
|
'not-push-current': { type: 'boolean' },
|
|
18
19
|
'force-protected': { type: 'boolean' },
|
|
20
|
+
force: { type: 'boolean', short: 'f' },
|
|
21
|
+
'create-target': { type: 'boolean' },
|
|
22
|
+
'allow-risky': { type: 'boolean' },
|
|
23
|
+
json: { type: 'boolean' },
|
|
19
24
|
'repo-mode': { type: 'string' },
|
|
20
25
|
'no-verify': { type: 'boolean' }
|
|
21
26
|
}
|
|
22
27
|
|
|
23
|
-
function formatBranchStatus(status) {
|
|
24
|
-
const map = {
|
|
25
|
-
pushed: '已推送',
|
|
26
|
-
'merged-and-pushed': '已合并并推送',
|
|
27
|
-
'remote-merged-and-pushed': '已远程合并并推送',
|
|
28
|
-
'skipped-protected': '已跳过(受保护)',
|
|
29
|
-
'skipped-missing-remote': '已跳过(远程分支不存在)',
|
|
30
|
-
paused: '已暂停'
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return map[status] || status
|
|
34
|
-
}
|
|
35
28
|
|
|
36
29
|
function printHelp() {
|
|
37
30
|
process.stdout.write(`
|
|
@@ -52,6 +45,10 @@ Options:
|
|
|
52
45
|
--pull-main 提交前将主分支同步到当前分支
|
|
53
46
|
--not-push-current 不推送当前分支,只处理目标分支
|
|
54
47
|
--force-protected 强制处理配置里的受保护分支
|
|
48
|
+
-f, --force 安全强推当前分支(只用 --force-with-lease,拒绝主分支/受保护分支)
|
|
49
|
+
--create-target 目标分支不存在时允许从当前分支新建(默认会询问或报错)
|
|
50
|
+
--allow-risky 检测到敏感文件/疑似密钥/大文件时仍继续提交
|
|
51
|
+
--json 输出机器可读 JSON
|
|
55
52
|
--repo-mode <mode> 仓库模式: auto(自动继承父仓库) | strict(禁止继承)
|
|
56
53
|
--no-verify 跳过 hook/husky 校验
|
|
57
54
|
-h, --help 查看帮助
|
|
@@ -62,6 +59,14 @@ Notes:
|
|
|
62
59
|
远程 merge 通过 PR/MR API 发起,不会本地切换到目标分支。
|
|
63
60
|
远程 merge 若出现冲突会暂停流程,并保留明确失败原因。
|
|
64
61
|
布尔开关仅在命令行显式传入时生效(如 --ai-commit / --pull-main / --no-verify / -y)。
|
|
62
|
+
本地 merge 和强推前会自动创建恢复点,可用 icode rescue 找回。
|
|
63
|
+
|
|
64
|
+
Examples:
|
|
65
|
+
icode push -m "feat: login"
|
|
66
|
+
icode push release test -y
|
|
67
|
+
icode push release -r -y
|
|
68
|
+
icode push --force -y
|
|
69
|
+
icode --remote upstream push -m "fix: typo"
|
|
65
70
|
`)
|
|
66
71
|
}
|
|
67
72
|
|
|
@@ -91,23 +96,46 @@ function resolveStringOption(cliValue, configValue, fallback = '') {
|
|
|
91
96
|
return fallback
|
|
92
97
|
}
|
|
93
98
|
|
|
99
|
+
const STATUS_LABELS = {
|
|
100
|
+
pushed: '已推送',
|
|
101
|
+
'merged-and-pushed': '已合并并推送',
|
|
102
|
+
'remote-merged-and-pushed': '已远程合并',
|
|
103
|
+
'force-pushed': '已强推 (--force-with-lease)',
|
|
104
|
+
'skipped-protected': '已跳过(受保护分支,可加 --force-protected)',
|
|
105
|
+
'skipped-missing-remote': '已跳过(远程分支不存在)',
|
|
106
|
+
paused: '已暂停'
|
|
107
|
+
}
|
|
108
|
+
|
|
94
109
|
// 统一输出 push 结果摘要,失败分支会追加具体原因,方便定位问题目标分支。
|
|
95
110
|
function printPushSummary(summary = []) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
logger.info(`[结果] ${item.branch}: ${formatBranchStatus(item.status)}${reason}`)
|
|
99
|
-
})
|
|
111
|
+
print('')
|
|
112
|
+
renderBranchResults(summary, STATUS_LABELS)
|
|
100
113
|
}
|
|
101
114
|
|
|
102
115
|
// 输出 dry-run 计划摘要,输入 workflow 返回的计划,输出对用户可读的阶段列表。
|
|
103
116
|
function printDryRunPlan(plan) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
117
|
+
const actionLabels = {
|
|
118
|
+
'ai-commit': 'AI 生成提交信息并提交',
|
|
119
|
+
'commit-if-needed': '有改动时提交',
|
|
120
|
+
'pull-main': '同步主分支',
|
|
121
|
+
'skip-protected': '跳过受保护分支',
|
|
122
|
+
'push-current': '推送当前分支',
|
|
123
|
+
'force-push-current': '强推当前分支',
|
|
124
|
+
'local-merge': '本地 merge 后推送',
|
|
125
|
+
'remote-merge': '创建并合并 PR/MR'
|
|
126
|
+
}
|
|
127
|
+
print(title('push 计划(dry-run)'))
|
|
128
|
+
print(keyValues([
|
|
129
|
+
['当前分支', ui.branch(plan.currentBranch)],
|
|
130
|
+
['remote', ui.remote(plan.remote || 'origin')],
|
|
131
|
+
['目标分支', plan.branchTargets.map(ui.branch).join(', ')],
|
|
132
|
+
['合并方式', plan.remoteMerge ? '远程 PR/MR' : '本地 merge']
|
|
133
|
+
]))
|
|
134
|
+
print('')
|
|
107
135
|
plan.steps.forEach((step, index) => {
|
|
108
|
-
|
|
109
|
-
logger.info(`[dry-run] ${index + 1}. ${step.action}: ${step.branch}${reason}`)
|
|
136
|
+
print(` ${c.gray(`${index + 1}.`)} ${actionLabels[step.action] || step.action} ${ui.branch(step.branch)} ${c.gray(step.reason || '')}`)
|
|
110
137
|
})
|
|
138
|
+
print(c.gray('\n 未执行任何操作。去掉 --dry-run 即可执行。'))
|
|
111
139
|
}
|
|
112
140
|
|
|
113
141
|
export function resolvePushWorkflowOptions(parsedValues, parsedPositionals, scopedOptions = {}) {
|
|
@@ -124,6 +152,9 @@ export function resolvePushWorkflowOptions(parsedValues, parsedPositionals, scop
|
|
|
124
152
|
pullMain: resolveBooleanOption(parsedValues['pull-main'], false),
|
|
125
153
|
notPushCurrent: resolveBooleanOption(parsedValues['not-push-current'], false),
|
|
126
154
|
forceProtected: resolveBooleanOption(parsedValues['force-protected'], false),
|
|
155
|
+
force: resolveBooleanOption(parsedValues.force, false),
|
|
156
|
+
createTarget: resolveBooleanOption(parsedValues['create-target'], false),
|
|
157
|
+
allowRisky: resolveBooleanOption(parsedValues['allow-risky'], false),
|
|
127
158
|
repoMode: resolveStringOption(parsedValues['repo-mode'], undefined, 'auto'),
|
|
128
159
|
noVerify: resolveBooleanOption(parsedValues['no-verify'], false)
|
|
129
160
|
}
|
|
@@ -159,6 +190,11 @@ export async function runPushCommand(rawArgs) {
|
|
|
159
190
|
throw error
|
|
160
191
|
}
|
|
161
192
|
|
|
193
|
+
if (parsed.values.json) {
|
|
194
|
+
process.stdout.write(`${JSON.stringify({ ok: !result.canceled, ...result }, null, 2)}\n`)
|
|
195
|
+
return result
|
|
196
|
+
}
|
|
197
|
+
|
|
162
198
|
if (result.canceled) {
|
|
163
199
|
return
|
|
164
200
|
}
|
|
@@ -169,5 +205,9 @@ export async function runPushCommand(rawArgs) {
|
|
|
169
205
|
}
|
|
170
206
|
|
|
171
207
|
printPushSummary(result.summary)
|
|
172
|
-
|
|
208
|
+
print('')
|
|
209
|
+
logger.success(`push 完成,共处理 ${result.summary.length} 个分支`)
|
|
210
|
+
if (result.backupId) {
|
|
211
|
+
logger.hint(`已创建恢复点 ${result.backupId},需要时可用 icode rescue 找回`)
|
|
212
|
+
}
|
|
173
213
|
}
|
package/src/commands/ready.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util'
|
|
2
2
|
import { normalizeLegacyArgs } from '../core/cli/args.js'
|
|
3
|
-
import {
|
|
3
|
+
import { c, formatChanges, keyValues, print, renderChecks, renderOverallStatus, title, ui } from '../core/cli/render.js'
|
|
4
4
|
import { withSpinner } from '../core/tools/loading.js'
|
|
5
5
|
import { runReadyWorkflow } from '../workflows/ready-workflow.js'
|
|
6
6
|
|
|
@@ -23,55 +23,25 @@ Notes:
|
|
|
23
23
|
`)
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/**
|
|
27
|
-
* 根据检查状态选择彩色输出函数。
|
|
28
|
-
* 输入为 pass/warn/fail,输出 logger 方法。
|
|
29
|
-
*/
|
|
30
|
-
function resolveLogMethod(status) {
|
|
31
|
-
if (status === 'fail') {
|
|
32
|
-
return logger.error
|
|
33
|
-
}
|
|
34
|
-
if (status === 'warn') {
|
|
35
|
-
return logger.warn
|
|
36
|
-
}
|
|
37
|
-
return logger.success
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* 输出单条 ready 检查结果。
|
|
42
|
-
* 输入为检查项,输出彩色日志和下一步建议。
|
|
43
|
-
*/
|
|
44
|
-
function printCheck(check) {
|
|
45
|
-
const log = resolveLogMethod(check.status).bind(logger)
|
|
46
|
-
const statusLabel = check.status.toUpperCase()
|
|
47
|
-
log(`[${statusLabel}] ${check.title}: ${check.detail}`)
|
|
48
|
-
if (check.nextStep) {
|
|
49
|
-
logger.info(`下一步: ${check.nextStep}`)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* 输出 ready 的人类可读摘要。
|
|
55
|
-
* 输入为 workflow 返回结果,输出分色日志。
|
|
56
|
-
*/
|
|
57
26
|
function printReadyResult(result) {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
logger.info(`仓库根目录: ${result.context.topLevelPath}`)
|
|
63
|
-
logger.info(`当前分支: ${branchText}`)
|
|
64
|
-
logger.info(`整体状态: ${statusText}`)
|
|
65
|
-
logger.info(`改动统计: staged=${result.changes.staged}, unstaged=${result.changes.unstaged}, untracked=${result.changes.untracked}, conflicted=${result.changes.conflicted}`)
|
|
66
|
-
|
|
67
|
-
if (result.upstream.configured) {
|
|
68
|
-
logger.info(`upstream: ${result.upstream.upstream} (ahead=${result.upstream.ahead}, behind=${result.upstream.behind})`)
|
|
69
|
-
} else {
|
|
70
|
-
logger.warn('upstream: 未配置')
|
|
71
|
-
}
|
|
27
|
+
const upstream = result.upstream.configured
|
|
28
|
+
? `${ui.remote(result.upstream.upstream)} ${ui.sync(result.upstream.ahead, result.upstream.behind) || c.gray('已同步')}`
|
|
29
|
+
: c.yellow('未配置')
|
|
72
30
|
|
|
73
|
-
|
|
74
|
-
|
|
31
|
+
print(title('仓库状态'))
|
|
32
|
+
print(keyValues([
|
|
33
|
+
['仓库', ui.path(result.context.topLevelPath)],
|
|
34
|
+
['分支', result.context.currentBranch ? ui.branch(result.context.currentBranch) : c.red('DETACHED')],
|
|
35
|
+
['主分支', ui.branch(result.context.defaultBranch)],
|
|
36
|
+
['upstream', upstream],
|
|
37
|
+
['改动', formatChanges(result.changes)]
|
|
38
|
+
]))
|
|
39
|
+
print('')
|
|
40
|
+
renderChecks(result.checks)
|
|
41
|
+
renderOverallStatus(result.status, {
|
|
42
|
+
通过: result.summary?.pass,
|
|
43
|
+
注意: result.summary?.warn,
|
|
44
|
+
失败: result.summary?.fail
|
|
75
45
|
})
|
|
76
46
|
}
|
|
77
47
|
|