@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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { parseCommandArgs, printJson } from '../core/cli/output.js'
|
|
2
|
+
import { c, print, renderBranchResults } from '../core/cli/render.js'
|
|
3
|
+
import { confirm, isInteractiveTerminal } from '../core/tools/interactive.js'
|
|
4
|
+
import { logger } from '../core/tools/logger.js'
|
|
5
|
+
import { inspectPausedWork, runAbortWorkflow, runContinueWorkflow } from '../workflows/continue-workflow.js'
|
|
6
|
+
|
|
7
|
+
const RESULT_LABELS = {
|
|
8
|
+
pushed: '已推送',
|
|
9
|
+
'merged-and-pushed': '已合并并推送',
|
|
10
|
+
'force-pushed': '已强推',
|
|
11
|
+
'skipped-protected': '已跳过(受保护分支)',
|
|
12
|
+
synced: '已同步',
|
|
13
|
+
'synced-and-pushed': '已同步并推送',
|
|
14
|
+
missing: '本地和远程都不存在'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function printContinueHelp() {
|
|
18
|
+
process.stdout.write(`
|
|
19
|
+
Usage:
|
|
20
|
+
icode continue [options]
|
|
21
|
+
|
|
22
|
+
解决冲突后继续刚才被打断的操作:
|
|
23
|
+
1. 检查冲突文件里是否还有 <<<<<<< / >>>>>>> 标记,有就列出文件和行号
|
|
24
|
+
2. 自动 git add 已解决的文件
|
|
25
|
+
3. 继续 merge / rebase / cherry-pick / revert
|
|
26
|
+
4. 如果是 push / migrate / sync 中途冲突,接着把剩下的步骤做完(推送、处理剩余分支、切回原分支)
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
--json 输出机器可读 JSON
|
|
30
|
+
--repo-mode <mode> 仓库模式: auto | strict
|
|
31
|
+
-h, --help 查看帮助
|
|
32
|
+
`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function printAbortHelp() {
|
|
36
|
+
process.stdout.write(`
|
|
37
|
+
Usage:
|
|
38
|
+
icode abort [options]
|
|
39
|
+
|
|
40
|
+
放弃刚才被冲突打断的操作:中止 merge / rebase / cherry-pick / revert,并切回原分支。
|
|
41
|
+
已经推送成功的分支不会被撤回。
|
|
42
|
+
|
|
43
|
+
Options:
|
|
44
|
+
-y, --yes 不再确认
|
|
45
|
+
--json 输出机器可读 JSON
|
|
46
|
+
--repo-mode <mode> 仓库模式: auto | strict
|
|
47
|
+
-h, --help 查看帮助
|
|
48
|
+
`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function runContinueCommand(rawArgs) {
|
|
52
|
+
const { values } = parseCommandArgs(rawArgs)
|
|
53
|
+
if (values.help) {
|
|
54
|
+
printContinueHelp()
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const result = await runContinueWorkflow({ repoMode: values['repo-mode'], silentLog: values.json })
|
|
59
|
+
if (values.json) {
|
|
60
|
+
printJson(result)
|
|
61
|
+
return result
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (result.summary.length) {
|
|
65
|
+
print('')
|
|
66
|
+
renderBranchResults(result.summary, RESULT_LABELS)
|
|
67
|
+
print('')
|
|
68
|
+
}
|
|
69
|
+
if (result.resumed) {
|
|
70
|
+
logger.success(`${result.description} 已全部完成${result.returnedTo ? c.gray(`,已切回 ${result.returnedTo}`) : ''}`)
|
|
71
|
+
}
|
|
72
|
+
return result
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function runAbortCommand(rawArgs) {
|
|
76
|
+
const { values } = parseCommandArgs(rawArgs)
|
|
77
|
+
if (values.help) {
|
|
78
|
+
printAbortHelp()
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!values.yes && !values.json && isInteractiveTerminal()) {
|
|
83
|
+
const paused = await inspectPausedWork({ repoMode: values['repo-mode'] })
|
|
84
|
+
if (paused) {
|
|
85
|
+
const accepted = await confirm(`放弃 ${paused.description}?冲突处理到一半的改动会丢弃`, false)
|
|
86
|
+
if (!accepted) {
|
|
87
|
+
logger.hint('已保留现场。处理完冲突后执行 icode continue。')
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const result = await runAbortWorkflow({ repoMode: values['repo-mode'] })
|
|
94
|
+
if (values.json) {
|
|
95
|
+
printJson(result)
|
|
96
|
+
return result
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
logger.success(`已放弃 ${result.description || result.operation}${result.returnedTo ? c.gray(`,已切回 ${result.returnedTo}`) : ''}`)
|
|
100
|
+
if (result.done.length) {
|
|
101
|
+
logger.hint(`这些分支在中断前已推送,不会撤回: ${result.done.join(', ')}`)
|
|
102
|
+
}
|
|
103
|
+
return result
|
|
104
|
+
}
|
package/src/commands/doctor.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 { keyValues, print, renderChecks, renderOverallStatus, title, ui } from '../core/cli/render.js'
|
|
4
4
|
import { runDoctorWorkflow } from '../workflows/doctor-workflow.js'
|
|
5
5
|
|
|
6
6
|
// 输出 doctor 子命令帮助,输入为空,输出标准帮助文本。
|
|
@@ -15,31 +15,10 @@ Options:
|
|
|
15
15
|
-h, --help 查看帮助
|
|
16
16
|
|
|
17
17
|
Notes:
|
|
18
|
-
检查 Git、
|
|
18
|
+
检查 Git、当前 remote、hooks、AI profile 与平台 token,并给出修复建议。
|
|
19
19
|
`)
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
// 根据检查状态选择输出函数,输入状态,输出 logger 方法。
|
|
23
|
-
function resolveLogMethod(status) {
|
|
24
|
-
if (status === 'fail') {
|
|
25
|
-
return logger.error
|
|
26
|
-
}
|
|
27
|
-
if (status === 'warn') {
|
|
28
|
-
return logger.warn
|
|
29
|
-
}
|
|
30
|
-
return logger.success
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// 输出单条 doctor 检查结果,输入检查项,输出彩色日志。
|
|
34
|
-
function printCheck(check) {
|
|
35
|
-
const log = resolveLogMethod(check.status).bind(logger)
|
|
36
|
-
const statusLabel = check.status.toUpperCase()
|
|
37
|
-
log(`[${statusLabel}] ${check.title}: ${check.detail}`)
|
|
38
|
-
if (check.suggestion) {
|
|
39
|
-
logger.info(`建议: ${check.suggestion}`)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
22
|
// 执行 doctor 子命令,输入原始参数,输出健康检查结果。
|
|
44
23
|
// 执行 doctor 子命令,输入原始参数,输出健康检查结果;参数会先归一化。
|
|
45
24
|
export async function runDoctorCommand(rawArgs) {
|
|
@@ -68,13 +47,20 @@ export async function runDoctorCommand(rawArgs) {
|
|
|
68
47
|
return result
|
|
69
48
|
}
|
|
70
49
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
50
|
+
print(title('icode doctor'))
|
|
51
|
+
print(keyValues([
|
|
52
|
+
['仓库', ui.path(result.context.topLevelPath)],
|
|
53
|
+
['remote', ui.remote(result.context.remote || 'origin')],
|
|
54
|
+
['配置文件', ui.path(result.configPath)]
|
|
55
|
+
]))
|
|
56
|
+
print('')
|
|
57
|
+
renderChecks(result.checks)
|
|
76
58
|
|
|
77
59
|
const summary = result.summary
|
|
78
|
-
|
|
60
|
+
renderOverallStatus(summary.fail ? 'fail' : summary.warn ? 'warn' : 'pass', {
|
|
61
|
+
通过: summary.pass,
|
|
62
|
+
注意: summary.warn,
|
|
63
|
+
失败: summary.fail
|
|
64
|
+
})
|
|
79
65
|
return result
|
|
80
66
|
}
|
package/src/commands/help.js
CHANGED
|
@@ -7,60 +7,80 @@ export function printMainHelp() {
|
|
|
7
7
|
icode v3 - Git workflow CLI
|
|
8
8
|
|
|
9
9
|
Usage:
|
|
10
|
-
icode <command> [options]
|
|
10
|
+
icode [global options] <command> [options]
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
ai AI 助手能力(提交信息/冲突建议/代码评审)
|
|
14
|
-
codereview AI 代码评审(不带 ai 前缀)
|
|
15
|
-
checkout 切换/创建分支(本地/远程自动识别)
|
|
12
|
+
日常:
|
|
16
13
|
ready 检查仓库是否适合继续 save/push(适合 AI agent 读取)
|
|
17
|
-
save
|
|
18
|
-
push
|
|
14
|
+
save 安全提交(风险扫描 + add + commit),可只提交指定路径
|
|
15
|
+
push 提交并推送,可合并到多个目标分支;--force 安全强推
|
|
16
|
+
sw 快速切换分支(模糊匹配 / 最近使用 / 切回上一个)
|
|
17
|
+
checkout 切换/创建分支(本地/远程自动识别)
|
|
18
|
+
stash 查看、暂存、恢复 stash
|
|
19
|
+
log 更易读的提交历史(--mine / --unpushed / --file)
|
|
19
20
|
sync 批量同步分支(fetch + pull)
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
|
|
22
|
+
整理历史:
|
|
23
|
+
amend 修改上一个提交(追加改动 / 改提交信息)
|
|
24
|
+
fixup 把改动合进更早的某个提交(自动 autosquash)
|
|
22
25
|
migrate 迁移分支提交(cherry-pick)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
clean 清理已合并 / 远程已删除 / 长期未动的分支
|
|
27
|
+
|
|
28
|
+
兜底:
|
|
29
|
+
continue 冲突解决后继续(自动暂存,并接着完成被打断的 push/migrate/sync)
|
|
30
|
+
abort 放弃被冲突打断的操作并切回原分支
|
|
31
|
+
undo 向导式撤销/回滚(revert/reset)
|
|
32
|
+
rescue 找回恢复点和 reflog 里的提交
|
|
33
|
+
|
|
34
|
+
协作与发布:
|
|
35
|
+
pr 创建/查看/检出/合并 PR、MR(GitHub/GitLab/Gitee/Gitea)
|
|
36
|
+
open 在浏览器打开仓库、分支、文件、PR、CI
|
|
37
|
+
tag 创建并推送 tag(日期命名或语义化版本)
|
|
38
|
+
release 一条命令发版(版本号 + changelog + tag + 平台 Release)
|
|
39
|
+
|
|
40
|
+
AI:
|
|
41
|
+
ai AI 助手能力(提交信息/冲突建议/代码评审)
|
|
42
|
+
codereview AI 代码评审(不带 ai 前缀)
|
|
26
43
|
explain AI 解释 Git diff(自然语言)
|
|
44
|
+
|
|
45
|
+
配置与环境:
|
|
46
|
+
config 查看和修改本地配置(AI profile / 平台 token / 受保护分支)
|
|
47
|
+
doctor 检查 Git/AI/平台配置健康状态
|
|
27
48
|
info 查看当前 git 与配置环境
|
|
28
49
|
completion 生成 bash/zsh tab 补全脚本
|
|
29
50
|
help 查看帮助(命令总览)
|
|
30
51
|
|
|
31
52
|
Global options:
|
|
53
|
+
-R, --remote <name> 指定使用的 remote(默认按 upstream / .icoderc / origin 自动选择)
|
|
32
54
|
-d, --debug 开启调试日志(输出更多细节)
|
|
33
55
|
-h, --help 查看帮助
|
|
34
56
|
|
|
35
57
|
Tips:
|
|
58
|
+
直接运行 icode 会显示常用操作菜单;多数命令不带参数时会给出可选项,方向键选择即可。
|
|
36
59
|
icode <command> -h 查看子命令完整参数说明
|
|
60
|
+
大多数命令支持 --json,便于脚本和 AI agent 读取;出错时也会输出 {"ok":false,...}
|
|
61
|
+
会改写历史或删除分支的操作都会自动创建恢复点,可用 icode rescue 找回。
|
|
62
|
+
仓库根目录的 .icoderc.json 可以共享 remote、主分支、受保护分支和提交扫描规则。
|
|
37
63
|
|
|
38
64
|
Examples:
|
|
39
|
-
icode checkout feature/login main --push-origin
|
|
40
65
|
icode ready
|
|
41
|
-
icode ready --json
|
|
42
66
|
icode save -m "feat: add login form"
|
|
43
|
-
icode save
|
|
44
|
-
icode
|
|
45
|
-
icode ai commit --apply -y
|
|
46
|
-
icode ai codereview
|
|
47
|
-
icode codereview --base origin/main --head HEAD
|
|
48
|
-
icode explain --base origin/main --head HEAD
|
|
67
|
+
icode save -m "fix: typo" src/login.vue
|
|
68
|
+
icode sw login
|
|
49
69
|
icode push release test -m "feat: batch publish" -y
|
|
50
|
-
icode push
|
|
51
|
-
icode
|
|
52
|
-
icode
|
|
53
|
-
icode
|
|
54
|
-
icode
|
|
55
|
-
icode
|
|
56
|
-
icode
|
|
57
|
-
icode undo --
|
|
58
|
-
icode
|
|
59
|
-
icode
|
|
60
|
-
icode
|
|
61
|
-
icode
|
|
62
|
-
icode
|
|
70
|
+
icode push --force -y
|
|
71
|
+
icode amend -a
|
|
72
|
+
icode fixup -a
|
|
73
|
+
icode log --unpushed
|
|
74
|
+
icode stash save -m "wip"
|
|
75
|
+
icode clean --gone -y
|
|
76
|
+
icode continue
|
|
77
|
+
icode undo --mode hard --ref HEAD~1
|
|
78
|
+
icode rescue
|
|
79
|
+
icode pr create --ai
|
|
80
|
+
icode open src/cli.js:42
|
|
81
|
+
icode release --dry-run
|
|
82
|
+
icode --remote upstream sync
|
|
83
|
+
icode config platform token set github <token>
|
|
63
84
|
source <(icode completion zsh)
|
|
64
|
-
icode info
|
|
65
85
|
`)
|
|
66
86
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { parseCommandArgs, printJson } from '../core/cli/output.js'
|
|
2
|
+
import { c, ui } from '../core/cli/render.js'
|
|
3
|
+
import { isInteractiveTerminal } from '../core/tools/interactive.js'
|
|
4
|
+
import { logger } from '../core/tools/logger.js'
|
|
5
|
+
import { runAmendWorkflow, runFixupWorkflow } from '../workflows/history-workflow.js'
|
|
6
|
+
|
|
7
|
+
const SHARED_OPTIONS = {
|
|
8
|
+
all: { type: 'boolean', short: 'a', default: false },
|
|
9
|
+
'allow-risky': { type: 'boolean', default: false },
|
|
10
|
+
'no-verify': { type: 'boolean', default: false }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function printAmendHelp() {
|
|
14
|
+
process.stdout.write(`
|
|
15
|
+
Usage:
|
|
16
|
+
icode amend [path...] [options]
|
|
17
|
+
|
|
18
|
+
Arguments:
|
|
19
|
+
[path...] 把这些路径的改动追加到上一个提交
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
-m, --message <msg> 修改上一个提交的信息
|
|
23
|
+
-a, --all 追加全部未提交改动
|
|
24
|
+
--allow-risky 检测到敏感文件/疑似密钥/大文件时仍继续
|
|
25
|
+
--no-verify 跳过 hook 校验
|
|
26
|
+
--json 输出机器可读 JSON
|
|
27
|
+
-y, --yes 已推送时也不再确认
|
|
28
|
+
--repo-mode <mode> 仓库模式: auto | strict
|
|
29
|
+
-h, --help 查看帮助
|
|
30
|
+
|
|
31
|
+
Notes:
|
|
32
|
+
不传 -a 或路径时只追加已暂存的改动。上一个提交已推送时会提示,之后需要 icode push --force。
|
|
33
|
+
执行前会自动创建恢复点。
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
icode amend -a
|
|
37
|
+
icode amend -m "feat: 更准确的描述"
|
|
38
|
+
icode amend src/login.js
|
|
39
|
+
`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function printFixupHelp() {
|
|
43
|
+
process.stdout.write(`
|
|
44
|
+
Usage:
|
|
45
|
+
icode fixup [commit] [options]
|
|
46
|
+
|
|
47
|
+
Arguments:
|
|
48
|
+
[commit] 要修正的提交;不传则从当前分支未合入主分支的提交里选择
|
|
49
|
+
|
|
50
|
+
Options:
|
|
51
|
+
-a, --all 先暂存全部未提交改动
|
|
52
|
+
--no-rebase 只生成 fixup! 提交,不自动整理历史
|
|
53
|
+
--allow-risky 检测到敏感文件/疑似密钥/大文件时仍继续
|
|
54
|
+
--no-verify 跳过 hook 校验
|
|
55
|
+
--json 输出机器可读 JSON
|
|
56
|
+
-y, --yes 已推送时也不再确认
|
|
57
|
+
--repo-mode <mode> 仓库模式: auto | strict
|
|
58
|
+
-h, --help 查看帮助
|
|
59
|
+
|
|
60
|
+
Notes:
|
|
61
|
+
等同于 git commit --fixup=<commit> && git rebase -i --autosquash(不会打开编辑器)。
|
|
62
|
+
发生冲突时用 icode continue / icode abort 处理。执行前会自动创建恢复点。
|
|
63
|
+
|
|
64
|
+
Examples:
|
|
65
|
+
icode fixup -a
|
|
66
|
+
icode fixup a1b2c3d
|
|
67
|
+
`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function printForceHint(result) {
|
|
71
|
+
if (result.needsForcePush) {
|
|
72
|
+
logger.warn('改写的提交之前已推送,更新远程请执行: icode push --force')
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function runAmendCommand(rawArgs) {
|
|
77
|
+
const { values, positionals } = parseCommandArgs(rawArgs, {
|
|
78
|
+
...SHARED_OPTIONS,
|
|
79
|
+
message: { type: 'string', short: 'm' }
|
|
80
|
+
})
|
|
81
|
+
if (values.help) {
|
|
82
|
+
printAmendHelp()
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result = await runAmendWorkflow({
|
|
87
|
+
message: values.message,
|
|
88
|
+
all: values.all,
|
|
89
|
+
paths: positionals,
|
|
90
|
+
interactive: !values.json && !values.yes && isInteractiveTerminal(),
|
|
91
|
+
allowRisky: values['allow-risky'],
|
|
92
|
+
noVerify: values['no-verify'],
|
|
93
|
+
yes: values.yes,
|
|
94
|
+
repoMode: values['repo-mode'],
|
|
95
|
+
silentLog: values.json
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
if (values.json) {
|
|
99
|
+
printJson(result)
|
|
100
|
+
return result
|
|
101
|
+
}
|
|
102
|
+
if (result.canceled) {
|
|
103
|
+
logger.warn('已取消。')
|
|
104
|
+
return result
|
|
105
|
+
}
|
|
106
|
+
logger.success(`已修改上一个提交 ${ui.hash(result.previousCommitId)} ${c.gray('→')} ${ui.hash(result.commitId)} ${ui.subject(result.message.split('\n')[0])}`)
|
|
107
|
+
printForceHint(result)
|
|
108
|
+
return result
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function runFixupCommand(rawArgs) {
|
|
112
|
+
const { values, positionals } = parseCommandArgs(rawArgs, {
|
|
113
|
+
...SHARED_OPTIONS,
|
|
114
|
+
'no-rebase': { type: 'boolean', default: false }
|
|
115
|
+
})
|
|
116
|
+
if (values.help) {
|
|
117
|
+
printFixupHelp()
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const result = await runFixupWorkflow({
|
|
122
|
+
commit: positionals[0],
|
|
123
|
+
all: values.all,
|
|
124
|
+
noRebase: values['no-rebase'],
|
|
125
|
+
allowRisky: values['allow-risky'],
|
|
126
|
+
noVerify: values['no-verify'],
|
|
127
|
+
yes: values.yes,
|
|
128
|
+
repoMode: values['repo-mode'],
|
|
129
|
+
silentLog: values.json
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
if (values.json) {
|
|
133
|
+
printJson(result)
|
|
134
|
+
return result
|
|
135
|
+
}
|
|
136
|
+
if (result.canceled) {
|
|
137
|
+
logger.warn('已取消。')
|
|
138
|
+
return result
|
|
139
|
+
}
|
|
140
|
+
if (result.rebased) {
|
|
141
|
+
logger.success(`已把改动合进提交 ${ui.hash(result.target)}`)
|
|
142
|
+
} else {
|
|
143
|
+
logger.success(`已创建 fixup 提交 ${result.fixupCommit}(目标 ${result.target}),之后可执行 git rebase -i --autosquash`)
|
|
144
|
+
}
|
|
145
|
+
printForceHint(result)
|
|
146
|
+
return result
|
|
147
|
+
}
|
package/src/commands/info.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util'
|
|
2
2
|
import { normalizeLegacyArgs } from '../core/cli/args.js'
|
|
3
|
-
import {
|
|
3
|
+
import { printJson } from '../core/cli/output.js'
|
|
4
|
+
import { c, keyValues, print, title, ui } from '../core/cli/render.js'
|
|
4
5
|
import { runInfoWorkflow } from '../workflows/info-workflow.js'
|
|
5
6
|
|
|
6
7
|
function printHelp() {
|
|
@@ -10,6 +11,7 @@ Usage:
|
|
|
10
11
|
|
|
11
12
|
Options:
|
|
12
13
|
--repo-mode <mode> 仓库模式: auto(自动继承父仓库) | strict(禁止继承)
|
|
14
|
+
--json 输出机器可读 JSON
|
|
13
15
|
-h, --help 查看帮助
|
|
14
16
|
|
|
15
17
|
Notes:
|
|
@@ -25,6 +27,7 @@ export async function runInfoCommand(rawArgs) {
|
|
|
25
27
|
allowPositionals: true,
|
|
26
28
|
options: {
|
|
27
29
|
'repo-mode': { type: 'string', default: 'auto' },
|
|
30
|
+
json: { type: 'boolean', default: false },
|
|
28
31
|
help: { type: 'boolean', short: 'h', default: false }
|
|
29
32
|
}
|
|
30
33
|
})
|
|
@@ -46,15 +49,47 @@ export async function runInfoCommand(rawArgs) {
|
|
|
46
49
|
inheritedFromParent: result.context.inheritedFromParent,
|
|
47
50
|
currentBranch: result.context.currentBranch,
|
|
48
51
|
defaultBranch: result.context.defaultBranch,
|
|
52
|
+
remote: result.context.remote,
|
|
53
|
+
remoteSource: result.context.remoteSource,
|
|
54
|
+
remotes: result.context.remotes,
|
|
55
|
+
repoConfigFile: result.context.repoConfig?.filePath || '',
|
|
49
56
|
hookPath: result.context.hookPath,
|
|
50
57
|
hasHookPath: result.context.hasHookPath,
|
|
51
58
|
hasHuskyFolder: result.context.hasHuskyFolder,
|
|
52
59
|
isSubmodule: result.context.isSubmodule,
|
|
53
60
|
superprojectPath: result.context.superprojectPath,
|
|
54
|
-
protectedBranches: result.repoPolicy.protectedBranches || [],
|
|
61
|
+
protectedBranches: [...new Set([...(result.repoPolicy.protectedBranches || []), ...(result.context.repoConfig?.protectedBranches || [])])],
|
|
55
62
|
ai: result.aiConfig
|
|
56
63
|
}
|
|
57
64
|
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
if (parsed.values.json) {
|
|
66
|
+
printJson(infoBlock)
|
|
67
|
+
return infoBlock
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const remoteSourceLabel = {
|
|
71
|
+
option: '命令行参数', env: '环境变量', 'repo-config': '.icoderc.json', config: '全局配置',
|
|
72
|
+
upstream: '分支 upstream', default: '默认', 'first-remote': '第一个 remote'
|
|
73
|
+
}[infoBlock.remoteSource] || infoBlock.remoteSource
|
|
74
|
+
|
|
75
|
+
print(title('仓库'))
|
|
76
|
+
print(keyValues([
|
|
77
|
+
['根目录', ui.path(infoBlock.repoRoot)],
|
|
78
|
+
['当前目录', infoBlock.inheritedFromParent ? `${ui.path(infoBlock.cwd)} ${c.yellow('(继承父级仓库)')}` : ''],
|
|
79
|
+
['当前分支', infoBlock.currentBranch ? ui.branch(infoBlock.currentBranch) : c.red('DETACHED')],
|
|
80
|
+
['主分支', ui.branch(infoBlock.defaultBranch)],
|
|
81
|
+
['remote', `${ui.remote(infoBlock.remote)} ${c.gray(`(${remoteSourceLabel})`)}${infoBlock.remotes?.length > 1 ? c.gray(` 可用: ${infoBlock.remotes.join(', ')}`) : ''}`],
|
|
82
|
+
['受保护分支', infoBlock.protectedBranches.length ? infoBlock.protectedBranches.map(ui.branch).join(', ') : c.gray('无')],
|
|
83
|
+
['子模块', infoBlock.isSubmodule ? `是,父仓库 ${ui.path(infoBlock.superprojectPath)}` : '']
|
|
84
|
+
]))
|
|
85
|
+
print('')
|
|
86
|
+
print(title('环境'))
|
|
87
|
+
print(keyValues([
|
|
88
|
+
['Git', infoBlock.gitVersion.replace(/^git version /, '')],
|
|
89
|
+
['Hooks', infoBlock.hasHuskyFolder ? c.green('husky') : infoBlock.hasHookPath ? `${c.green('已配置')} ${c.gray(infoBlock.hookPath)}` : c.gray('未检测到')],
|
|
90
|
+
['AI profile', infoBlock.ai.activeProfile ? `${c.green(infoBlock.ai.activeProfile)} ${c.gray(`(共 ${infoBlock.ai.profileCount} 个)`)}` : c.gray('未配置')],
|
|
91
|
+
['全局配置', ui.path(infoBlock.configPath)],
|
|
92
|
+
['仓库配置', infoBlock.repoConfigFile ? ui.path(infoBlock.repoConfigFile) : c.gray('无(icode config init 生成)')]
|
|
93
|
+
]))
|
|
94
|
+
return infoBlock
|
|
60
95
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { IcodeError } from '../core/errors.js'
|
|
2
|
+
import { parseCommandArgs, printJson } from '../core/cli/output.js'
|
|
3
|
+
import { renderCommits } from '../core/cli/render.js'
|
|
4
|
+
import { runCommand } from '../core/commands/shell.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
|
+
|
|
9
|
+
function printHelp() {
|
|
10
|
+
process.stdout.write(`
|
|
11
|
+
Usage:
|
|
12
|
+
icode log [range] [options]
|
|
13
|
+
|
|
14
|
+
Arguments:
|
|
15
|
+
[range] 提交范围或分支,例如 main..HEAD、origin/main、v1.2.0..
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
-n, --limit <n> 显示条数(默认 20)
|
|
19
|
+
--mine 只看自己的提交(按 git config user.email)
|
|
20
|
+
--author <name> 按作者过滤
|
|
21
|
+
--since <date> 起始时间,例如 "2 weeks ago"、2026-10-01
|
|
22
|
+
--grep <text> 按提交信息过滤(忽略大小写)
|
|
23
|
+
--file <path> 只看某个文件的历史(跟踪重命名)
|
|
24
|
+
--unpushed 只看还没推送到远程的提交
|
|
25
|
+
--no-merges 不显示 merge commit
|
|
26
|
+
--graph 显示分支图(调用 git log --graph)
|
|
27
|
+
--json 输出机器可读 JSON
|
|
28
|
+
--repo-mode <mode> 仓库模式: auto | strict
|
|
29
|
+
-h, --help 查看帮助
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
icode log
|
|
33
|
+
icode log --mine --since "1 week ago"
|
|
34
|
+
icode log --unpushed
|
|
35
|
+
icode log --file src/cli.js -n 5
|
|
36
|
+
icode log main..HEAD --json
|
|
37
|
+
`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseLimit(value) {
|
|
41
|
+
if (value == null) {
|
|
42
|
+
return 20
|
|
43
|
+
}
|
|
44
|
+
const number = Number(value)
|
|
45
|
+
if (!Number.isInteger(number) || number <= 0) {
|
|
46
|
+
throw new IcodeError('--limit 需要正整数', { code: 'LOG_LIMIT_INVALID', exitCode: 2 })
|
|
47
|
+
}
|
|
48
|
+
return number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 解析 --unpushed 对应的范围:有 upstream 用 @{u}..HEAD,否则用 <remote>/<默认分支>..HEAD。
|
|
53
|
+
*/
|
|
54
|
+
async function resolveUnpushedRange(git, context) {
|
|
55
|
+
const upstream = await git.getUpstreamRef()
|
|
56
|
+
if (upstream) {
|
|
57
|
+
return `${upstream}..HEAD`
|
|
58
|
+
}
|
|
59
|
+
const fallback = git.remoteRef(context.defaultBranch)
|
|
60
|
+
if (await git.refExists(fallback)) {
|
|
61
|
+
return `${fallback}..HEAD`
|
|
62
|
+
}
|
|
63
|
+
return 'HEAD'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function runLogCommand(rawArgs) {
|
|
67
|
+
const { values, positionals } = parseCommandArgs(rawArgs, {
|
|
68
|
+
limit: { type: 'string', short: 'n' },
|
|
69
|
+
mine: { type: 'boolean', default: false },
|
|
70
|
+
author: { type: 'string' },
|
|
71
|
+
since: { type: 'string' },
|
|
72
|
+
grep: { type: 'string' },
|
|
73
|
+
file: { type: 'string' },
|
|
74
|
+
unpushed: { type: 'boolean', default: false },
|
|
75
|
+
'no-merges': { type: 'boolean', default: false },
|
|
76
|
+
graph: { type: 'boolean', default: false }
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
if (values.help) {
|
|
80
|
+
printHelp()
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const context = await resolveGitContext({ repoMode: values['repo-mode'] })
|
|
85
|
+
const git = new GitService(context)
|
|
86
|
+
const limit = parseLimit(values.limit)
|
|
87
|
+
|
|
88
|
+
let author = values.author || ''
|
|
89
|
+
if (values.mine) {
|
|
90
|
+
author = (await git.getConfigValue('user.email')) || (await git.getConfigValue('user.name'))
|
|
91
|
+
if (!author) {
|
|
92
|
+
throw new IcodeError('未配置 git user.email,无法使用 --mine。', { code: 'LOG_USER_UNKNOWN', exitCode: 2 })
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const range = values.unpushed ? await resolveUnpushedRange(git, context) : positionals[0] || ''
|
|
97
|
+
|
|
98
|
+
if (values.graph && !values.json) {
|
|
99
|
+
// 分支图直接交给 git 渲染,颜色和对齐都更可靠。
|
|
100
|
+
const args = ['log', '--graph', '--color=always', '--format=%C(yellow)%h%C(reset) %C(cyan)%ar%C(reset) %C(green)%an%C(reset)%C(auto)%d%C(reset) %s', `-${limit}`]
|
|
101
|
+
if (author) {
|
|
102
|
+
args.push(`--author=${author}`)
|
|
103
|
+
}
|
|
104
|
+
if (values.since) {
|
|
105
|
+
args.push(`--since=${values.since}`)
|
|
106
|
+
}
|
|
107
|
+
if (range) {
|
|
108
|
+
args.push(range)
|
|
109
|
+
} else {
|
|
110
|
+
args.push('--all')
|
|
111
|
+
}
|
|
112
|
+
const result = await runCommand('git', args, { cwd: context.topLevelPath })
|
|
113
|
+
process.stdout.write(result.stdout)
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const entries = await git.logEntries({
|
|
118
|
+
range,
|
|
119
|
+
limit,
|
|
120
|
+
author,
|
|
121
|
+
since: values.since,
|
|
122
|
+
grep: values.grep,
|
|
123
|
+
file: values.file,
|
|
124
|
+
noMerges: values['no-merges']
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
if (values.json) {
|
|
128
|
+
printJson({ range, commits: entries })
|
|
129
|
+
return entries
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!entries.length) {
|
|
133
|
+
logger.info(values.unpushed ? '没有未推送的提交。' : '没有匹配的提交。')
|
|
134
|
+
return entries
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
renderCommits(entries)
|
|
138
|
+
if (entries.length === limit) {
|
|
139
|
+
logger.hint(`只显示最近 ${limit} 条,可用 -n 调整`)
|
|
140
|
+
}
|
|
141
|
+
return entries
|
|
142
|
+
}
|