@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,228 @@
|
|
|
1
|
+
// 终端配色与排版工具。所有命令的输出都走这里,保证风格一致。
|
|
2
|
+
// 规则:FORCE_COLOR 强制开启;NO_COLOR 关闭;否则只在 TTY 中显示颜色。
|
|
3
|
+
|
|
4
|
+
const CODES = {
|
|
5
|
+
reset: 0,
|
|
6
|
+
bold: 1,
|
|
7
|
+
dim: 2,
|
|
8
|
+
italic: 3,
|
|
9
|
+
underline: 4,
|
|
10
|
+
red: 31,
|
|
11
|
+
green: 32,
|
|
12
|
+
yellow: 33,
|
|
13
|
+
blue: 34,
|
|
14
|
+
magenta: 35,
|
|
15
|
+
cyan: 36,
|
|
16
|
+
gray: 90,
|
|
17
|
+
brightRed: 91,
|
|
18
|
+
brightGreen: 92,
|
|
19
|
+
brightYellow: 93,
|
|
20
|
+
brightBlue: 94,
|
|
21
|
+
brightMagenta: 95,
|
|
22
|
+
brightCyan: 96
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const CLOSE = {
|
|
26
|
+
bold: 22,
|
|
27
|
+
dim: 22,
|
|
28
|
+
italic: 23,
|
|
29
|
+
underline: 24
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let forcedStream = null
|
|
33
|
+
|
|
34
|
+
// 把家目录前缀显示成 ~,路径更短。
|
|
35
|
+
export function shortenHome(value) {
|
|
36
|
+
const text = String(value ?? '')
|
|
37
|
+
const home = process.env.HOME || process.env.USERPROFILE || ''
|
|
38
|
+
return home && text.startsWith(home) ? `~${text.slice(home.length)}` : text
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// JSON 模式下普通日志走 stderr,颜色判断也要看 stderr。
|
|
42
|
+
export function setColorStream(stream) {
|
|
43
|
+
forcedStream = stream
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function colorEnabled(stream = forcedStream || process.stdout) {
|
|
47
|
+
const force = process.env.FORCE_COLOR
|
|
48
|
+
if (force != null && force !== '' && force !== '0' && force !== 'false') {
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
if (process.env.NO_COLOR != null && process.env.NO_COLOR !== '') {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
if (force === '0' || force === 'false') {
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
return Boolean(stream?.isTTY)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function paint(name, text) {
|
|
61
|
+
const value = String(text ?? '')
|
|
62
|
+
if (!colorEnabled() || !CODES[name]) {
|
|
63
|
+
return value
|
|
64
|
+
}
|
|
65
|
+
const close = CLOSE[name] || 39
|
|
66
|
+
return `\x1b[${CODES[name]}m${value}\x1b[${close}m`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 去掉 ANSI 颜色,用来计算显示宽度。
|
|
70
|
+
export function stripAnsi(text) {
|
|
71
|
+
return String(text ?? '').replace(/\x1b\[[0-9;]*m/g, '')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 估算显示宽度:中日韩全角字符按 2 计算。
|
|
75
|
+
export function displayWidth(text) {
|
|
76
|
+
let width = 0
|
|
77
|
+
for (const char of stripAnsi(text)) {
|
|
78
|
+
const code = char.codePointAt(0)
|
|
79
|
+
width += (code >= 0x1100 && (code <= 0x115f || (code >= 0x2e80 && code <= 0xa4cf) || (code >= 0xac00 && code <= 0xd7a3) ||
|
|
80
|
+
(code >= 0xf900 && code <= 0xfaff) || (code >= 0xfe30 && code <= 0xfe4f) || (code >= 0xff00 && code <= 0xff60) ||
|
|
81
|
+
(code >= 0xffe0 && code <= 0xffe6) || (code >= 0x1f300 && code <= 0x1faff))) ? 2 : 1
|
|
82
|
+
}
|
|
83
|
+
return width
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function pad(text, width) {
|
|
87
|
+
const value = String(text ?? '')
|
|
88
|
+
const gap = width - displayWidth(value)
|
|
89
|
+
return gap > 0 ? `${value}${' '.repeat(gap)}` : value
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 超出宽度时截断并加省略号。
|
|
93
|
+
export function truncate(text, width) {
|
|
94
|
+
const value = String(text ?? '')
|
|
95
|
+
if (displayWidth(value) <= width) {
|
|
96
|
+
return value
|
|
97
|
+
}
|
|
98
|
+
let result = ''
|
|
99
|
+
for (const char of stripAnsi(value)) {
|
|
100
|
+
if (displayWidth(`${result}${char}…`) > width) {
|
|
101
|
+
break
|
|
102
|
+
}
|
|
103
|
+
result += char
|
|
104
|
+
}
|
|
105
|
+
return `${result}…`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const c = Object.fromEntries(Object.keys(CODES).filter((key) => key !== 'reset').map((key) => [key, (text) => paint(key, text)]))
|
|
109
|
+
|
|
110
|
+
export const icons = {
|
|
111
|
+
get success() { return c.green('✔') },
|
|
112
|
+
get error() { return c.red('✖') },
|
|
113
|
+
get warn() { return c.yellow('▲') },
|
|
114
|
+
get info() { return c.cyan('●') },
|
|
115
|
+
get arrow() { return c.cyan('›') },
|
|
116
|
+
get pointer() { return c.cyan('❯') },
|
|
117
|
+
get dot() { return c.gray('·') }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 语义化着色:同一类信息在所有命令里颜色一致。
|
|
121
|
+
export const ui = {
|
|
122
|
+
branch: (name) => c.magenta(name),
|
|
123
|
+
remote: (name) => c.blue(name),
|
|
124
|
+
hash: (sha) => c.yellow(String(sha || '').slice(0, 8)),
|
|
125
|
+
tag: (name) => c.brightYellow(name),
|
|
126
|
+
path: (value) => c.cyan(shortenHome(value)),
|
|
127
|
+
url: (value) => c.underline(c.blue(value)),
|
|
128
|
+
time: (value) => c.gray(value),
|
|
129
|
+
author: (value) => c.green(value),
|
|
130
|
+
dim: (value) => c.gray(value),
|
|
131
|
+
strong: (value) => c.bold(value),
|
|
132
|
+
ok: (value) => c.green(value),
|
|
133
|
+
bad: (value) => c.red(value),
|
|
134
|
+
warn: (value) => c.yellow(value),
|
|
135
|
+
key: (value) => c.gray(value),
|
|
136
|
+
code: (value) => c.cyan(value),
|
|
137
|
+
|
|
138
|
+
// ahead/behind 显示为 ↑2 ↓1。
|
|
139
|
+
sync(ahead = 0, behind = 0) {
|
|
140
|
+
const parts = []
|
|
141
|
+
if (ahead) {
|
|
142
|
+
parts.push(c.green(`↑${ahead}`))
|
|
143
|
+
}
|
|
144
|
+
if (behind) {
|
|
145
|
+
parts.push(c.red(`↓${behind}`))
|
|
146
|
+
}
|
|
147
|
+
return parts.join(' ')
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
// 检查项状态徽标。
|
|
151
|
+
status(status) {
|
|
152
|
+
if (status === 'pass' || status === 'ok') {
|
|
153
|
+
return c.green('✔ 通过')
|
|
154
|
+
}
|
|
155
|
+
if (status === 'warn') {
|
|
156
|
+
return c.yellow('▲ 注意')
|
|
157
|
+
}
|
|
158
|
+
if (status === 'fail' || status === 'error') {
|
|
159
|
+
return c.red('✖ 失败')
|
|
160
|
+
}
|
|
161
|
+
return c.gray(status)
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
// Conventional Commit 标题着色:类型和 scope 高亮。
|
|
165
|
+
subject(subject) {
|
|
166
|
+
const match = String(subject || '').match(/^(\w+)(\([^)]*\))?(!)?:\s*(.*)$/)
|
|
167
|
+
if (!match) {
|
|
168
|
+
return String(subject || '')
|
|
169
|
+
}
|
|
170
|
+
const typeColors = { feat: 'green', fix: 'red', perf: 'magenta', refactor: 'blue', docs: 'cyan', test: 'yellow', chore: 'gray', ci: 'gray', build: 'gray', style: 'gray', revert: 'brightRed' }
|
|
171
|
+
const color = typeColors[match[1].toLowerCase()] || 'cyan'
|
|
172
|
+
return `${c[color](match[1])}${match[2] ? c.gray(match[2]) : ''}${match[3] ? c.red('!') : ''}: ${match[4]}`
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 渲染标题行:`━━ 标题 ━━━━`。
|
|
178
|
+
*/
|
|
179
|
+
export function title(text) {
|
|
180
|
+
const label = ` ${text} `
|
|
181
|
+
const width = Math.max(10, Math.min(60, (process.stdout.columns || 80) - 2) - displayWidth(label) - 2)
|
|
182
|
+
return `${c.cyan('━━')}${c.bold(label)}${c.cyan('━'.repeat(width))}`
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 渲染键值对,key 列对齐。rows: [[key, value], ...],value 为空的行会跳过。
|
|
187
|
+
*/
|
|
188
|
+
export function keyValues(rows, options = {}) {
|
|
189
|
+
const visible = rows.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
|
190
|
+
const keyWidth = Math.max(0, ...visible.map(([key]) => displayWidth(key)))
|
|
191
|
+
const indent = ' '.repeat(options.indent ?? 2)
|
|
192
|
+
return visible.map(([key, value]) => `${indent}${c.gray(pad(key, keyWidth))} ${value}`).join('\n')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 渲染表格。columns: [{ key, label, width?, align?, format? }],rows 为对象数组。
|
|
197
|
+
* 列宽自动取内容最大宽度(受 maxWidth 限制)。
|
|
198
|
+
*/
|
|
199
|
+
export function table(rows, columns, options = {}) {
|
|
200
|
+
const indent = ' '.repeat(options.indent ?? 2)
|
|
201
|
+
const cells = rows.map((row) => columns.map((column) => {
|
|
202
|
+
// 先截断原始值再上色,避免截断时丢失颜色。
|
|
203
|
+
let value = row[column.key]
|
|
204
|
+
if (column.maxWidth && typeof value === 'string') {
|
|
205
|
+
value = truncate(value, column.maxWidth)
|
|
206
|
+
}
|
|
207
|
+
const raw = column.format ? column.format(value, row) : value
|
|
208
|
+
return String(raw ?? '')
|
|
209
|
+
}))
|
|
210
|
+
const widths = columns.map((column, index) => Math.max(
|
|
211
|
+
options.header === false ? 0 : displayWidth(column.label || ''),
|
|
212
|
+
...cells.map((line) => displayWidth(line[index]))
|
|
213
|
+
))
|
|
214
|
+
|
|
215
|
+
const lines = []
|
|
216
|
+
if (options.header !== false) {
|
|
217
|
+
lines.push(indent + columns.map((column, index) => c.gray(pad(column.label || '', widths[index]))).join(' ').trimEnd())
|
|
218
|
+
}
|
|
219
|
+
for (const line of cells) {
|
|
220
|
+
lines.push(indent + line.map((cell, index) => (index === line.length - 1 ? cell : pad(cell, widths[index]))).join(' ').trimEnd())
|
|
221
|
+
}
|
|
222
|
+
return lines.join('\n')
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 打印到 stdout(JSON 模式下 logger 会改走 stderr,这里统一跟随)。
|
|
226
|
+
export function print(text = '') {
|
|
227
|
+
;(forcedStream || process.stdout).write(`${text}\n`)
|
|
228
|
+
}
|
|
@@ -4,7 +4,8 @@ import { buildLimitedDiffPrompt, formatDiffCoverage, summarizeDiffCoverage } fro
|
|
|
4
4
|
import { resolveGitContext } from '../core/git/context.js'
|
|
5
5
|
import { scanCommitConventions } from '../core/git/commit-conventions.js'
|
|
6
6
|
import { GitService } from '../core/git/service.js'
|
|
7
|
-
import {
|
|
7
|
+
import { chooseOne, input, isInteractiveTerminal } from '../core/tools/interactive.js'
|
|
8
|
+
import { c, print, ui, shortenHome } from '../core/tools/theme.js'
|
|
8
9
|
import { logger } from '../core/tools/logger.js'
|
|
9
10
|
|
|
10
11
|
function normalizeCommitType(value) {
|
|
@@ -51,11 +52,14 @@ export async function runAiCommitWorkflow(options) {
|
|
|
51
52
|
const git = new GitService(context)
|
|
52
53
|
|
|
53
54
|
if (!options.silentContextLog) {
|
|
54
|
-
logger.
|
|
55
|
+
logger.hint(`仓库 ${shortenHome(context.topLevelPath)}`)
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
const
|
|
58
|
+
// scope 控制 AI 看到的改动范围:stagedOnly 只看暂存区;paths 只看指定路径。
|
|
59
|
+
const scopePaths = Array.isArray(options.paths) ? options.paths.filter(Boolean) : []
|
|
60
|
+
const stagedOnly = options.stagedOnly === true
|
|
61
|
+
const stagedDiff = await git.diffStaged(scopePaths)
|
|
62
|
+
const workingDiff = stagedOnly ? '' : await git.diffWorkingTree(scopePaths)
|
|
59
63
|
const hasStagedDiff = Boolean(stagedDiff.trim())
|
|
60
64
|
const hasWorkingDiff = Boolean(workingDiff.trim())
|
|
61
65
|
const diff = joinDiffSections([
|
|
@@ -89,19 +93,23 @@ export async function runAiCommitWorkflow(options) {
|
|
|
89
93
|
logger.info(`检测到提交规范配置,AI 将优先参考: ${conventionContext.sources.join(', ')}`)
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
const
|
|
93
|
-
{
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
const generate = async () => {
|
|
97
|
+
const { parsed, text } = await askAiJson(
|
|
98
|
+
{
|
|
99
|
+
systemPrompt: `You are a senior software engineer. Generate a concise Conventional Commit message. Output JSON only. Language: ${language}.`,
|
|
100
|
+
userPrompt: `${conventionPrompt}Based on the following git diff, return JSON with fields:\n{\"type\":\"feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert\",\"scope\":\"optional\",\"subject\":\"required one-line summary\",\"body\":\"optional details\"}\n\nDiff Source: ${diffSource}\n\n${formatDiffCoverage(limitedDiff.coverage)}\n\nDiff:\n${limitedDiff.diff}`
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
profile: options.profile
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
return { commitMessage: buildCommitMessage(parsed), text }
|
|
107
|
+
}
|
|
101
108
|
|
|
102
|
-
|
|
109
|
+
let { commitMessage, text } = await generate()
|
|
110
|
+
const interactiveReview = options.apply && !options.yes && isInteractiveTerminal()
|
|
103
111
|
|
|
104
|
-
if (!options.silentResultLog) {
|
|
112
|
+
if (!options.silentResultLog && !interactiveReview) {
|
|
105
113
|
logger.success(`AI 建议提交信息:\n${commitMessage}`)
|
|
106
114
|
}
|
|
107
115
|
|
|
@@ -114,9 +122,24 @@ export async function runAiCommitWorkflow(options) {
|
|
|
114
122
|
}
|
|
115
123
|
}
|
|
116
124
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
// 交互终端里让用户选择:直接用 / 改一下标题 / 重新生成 / 取消,不需要重写整条信息。
|
|
126
|
+
while (interactiveReview) {
|
|
127
|
+
const [subjectLine, ...bodyLines] = commitMessage.split('\n')
|
|
128
|
+
print('')
|
|
129
|
+
print(` ${c.magenta('AI 建议')} ${c.bold(ui.subject(subjectLine))}`)
|
|
130
|
+
bodyLines.filter((line) => line.trim()).forEach((line) => print(` ${c.gray(line)}`))
|
|
131
|
+
print('')
|
|
132
|
+
const action = await chooseOne('使用这条提交信息?', [
|
|
133
|
+
{ value: 'use', label: '使用并提交' },
|
|
134
|
+
{ value: 'edit', label: '修改标题后提交', hint: '在原标题基础上改' },
|
|
135
|
+
{ value: 'regenerate', label: '重新生成' },
|
|
136
|
+
{ value: 'cancel', label: '取消' }
|
|
137
|
+
], 0, { filter: false })
|
|
138
|
+
|
|
139
|
+
if (action === 'use') {
|
|
140
|
+
break
|
|
141
|
+
}
|
|
142
|
+
if (action === 'cancel') {
|
|
120
143
|
return {
|
|
121
144
|
applied: false,
|
|
122
145
|
commitMessage,
|
|
@@ -125,15 +148,24 @@ export async function runAiCommitWorkflow(options) {
|
|
|
125
148
|
canceled: true
|
|
126
149
|
}
|
|
127
150
|
}
|
|
151
|
+
if (action === 'edit') {
|
|
152
|
+
const edited = await input('标题', subjectLine)
|
|
153
|
+
commitMessage = [edited, ...bodyLines].join('\n')
|
|
154
|
+
break
|
|
155
|
+
}
|
|
156
|
+
;({ commitMessage, text } = await generate())
|
|
128
157
|
}
|
|
129
158
|
|
|
130
|
-
if (
|
|
159
|
+
if (scopePaths.length) {
|
|
160
|
+
await git.stagePaths(scopePaths)
|
|
161
|
+
} else if (hasWorkingDiff) {
|
|
131
162
|
// 统一暂存工作区改动,确保未暂存修改和新文件与 AI 看到的 diff 保持一致。
|
|
132
163
|
await git.stageAll()
|
|
133
164
|
}
|
|
134
165
|
|
|
135
166
|
await git.commit(commitMessage, {
|
|
136
|
-
noVerify: options.noVerify
|
|
167
|
+
noVerify: options.noVerify,
|
|
168
|
+
paths: scopePaths
|
|
137
169
|
})
|
|
138
170
|
|
|
139
171
|
const commitId = await git.revParseShort('HEAD')
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { IcodeError } from '../core/errors.js'
|
|
2
|
+
import { ui, shortenHome } from '../core/tools/theme.js'
|
|
3
|
+
import { withAutoStash } from '../core/git/autostash.js'
|
|
2
4
|
import { resolveGitContext } from '../core/git/context.js'
|
|
3
5
|
import {
|
|
4
6
|
assertCleanWorktree,
|
|
@@ -23,7 +25,8 @@ export async function runCheckoutWorkflow(input) {
|
|
|
23
25
|
|
|
24
26
|
const context = await resolveGitContext({
|
|
25
27
|
cwd: input.cwd,
|
|
26
|
-
repoMode: input.repoMode
|
|
28
|
+
repoMode: input.repoMode,
|
|
29
|
+
remote: input.remote
|
|
27
30
|
})
|
|
28
31
|
|
|
29
32
|
const git = new GitService(context)
|
|
@@ -32,9 +35,18 @@ export async function runCheckoutWorkflow(input) {
|
|
|
32
35
|
const safetyState = await readGitSafetyState(git)
|
|
33
36
|
assertNoPendingOperation(safetyState.operation, 'checkout', 'CHECKOUT_GIT_OPERATION_IN_PROGRESS')
|
|
34
37
|
assertNoConflictedFiles(safetyState.changes, 'checkout', 'CHECKOUT_CONFLICTED_FILES')
|
|
35
|
-
|
|
38
|
+
if (!input.autostash) {
|
|
39
|
+
assertCleanWorktree(safetyState.changes, 'checkout', 'CHECKOUT_DIRTY_WORKTREE')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return withAutoStash(git, input.autostash === true, `checkout ${branchName}`, () =>
|
|
43
|
+
switchToBranch(git, context, input, branchName, baseBranchName)
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function switchToBranch(git, context, input, branchName, baseBranchName) {
|
|
36
48
|
|
|
37
|
-
logger.
|
|
49
|
+
logger.hint(`仓库 ${shortenHome(context.topLevelPath)}`)
|
|
38
50
|
if (context.inheritedFromParent) {
|
|
39
51
|
logger.warn('当前目录继承了父级 Git 仓库,命令将基于父仓库根目录执行。')
|
|
40
52
|
}
|
|
@@ -45,10 +57,10 @@ export async function runCheckoutWorkflow(input) {
|
|
|
45
57
|
const remoteExists = await git.branchExistsRemote(branchName)
|
|
46
58
|
|
|
47
59
|
if (localExists) {
|
|
48
|
-
logger.info(`切换本地分支: ${branchName}`)
|
|
60
|
+
logger.info(`切换本地分支: ${ui.branch(branchName)}`)
|
|
49
61
|
await git.checkout(branchName)
|
|
50
62
|
} else if (remoteExists) {
|
|
51
|
-
logger.info(`创建并跟踪远程分支: ${branchName}`)
|
|
63
|
+
logger.info(`创建并跟踪远程分支: ${ui.branch(branchName)}`)
|
|
52
64
|
await git.checkoutTracking(branchName)
|
|
53
65
|
} else {
|
|
54
66
|
const baseLocalExists = await git.branchExistsLocal(baseBranchName)
|
|
@@ -61,27 +73,27 @@ export async function runCheckoutWorkflow(input) {
|
|
|
61
73
|
})
|
|
62
74
|
}
|
|
63
75
|
|
|
64
|
-
const fromRef = baseLocalExists ? baseBranchName :
|
|
65
|
-
logger.info(`从 ${fromRef} 新建分支: ${branchName}`)
|
|
76
|
+
const fromRef = baseLocalExists ? baseBranchName : git.remoteRef(baseBranchName)
|
|
77
|
+
logger.info(`从 ${fromRef} 新建分支: ${ui.branch(branchName)}`)
|
|
66
78
|
await git.checkoutNewBranch(branchName, fromRef)
|
|
67
79
|
}
|
|
68
80
|
|
|
69
81
|
if (input.pullMain && context.defaultBranch !== branchName) {
|
|
70
|
-
logger.info(`同步主分支到当前分支: ${context.defaultBranch}`)
|
|
82
|
+
logger.info(`同步主分支到当前分支: ${ui.branch(context.defaultBranch)}`)
|
|
71
83
|
await git.pull(context.defaultBranch, {
|
|
72
84
|
noRebase: true
|
|
73
85
|
})
|
|
74
86
|
}
|
|
75
87
|
|
|
76
88
|
if (remoteExists) {
|
|
77
|
-
logger.info(`拉取远程分支: ${branchName}`)
|
|
89
|
+
logger.info(`拉取远程分支: ${ui.branch(branchName)}`)
|
|
78
90
|
await git.pull(branchName, {
|
|
79
91
|
noRebase: true
|
|
80
92
|
})
|
|
81
93
|
}
|
|
82
94
|
|
|
83
95
|
if (input.pushOrigin && !remoteExists) {
|
|
84
|
-
logger.info(`推送新分支到远程: ${branchName}`)
|
|
96
|
+
logger.info(`推送新分支到远程: ${ui.branch(branchName)}`)
|
|
85
97
|
await git.push(branchName, {
|
|
86
98
|
setUpstream: true,
|
|
87
99
|
noVerify: input.noVerify
|
|
@@ -91,6 +103,8 @@ export async function runCheckoutWorkflow(input) {
|
|
|
91
103
|
return {
|
|
92
104
|
branchName,
|
|
93
105
|
baseBranchName,
|
|
106
|
+
mode: localExists ? 'local' : remoteExists ? 'tracking' : 'created',
|
|
107
|
+
remote: git.remote,
|
|
94
108
|
remoteExists,
|
|
95
109
|
repoRoot: context.topLevelPath
|
|
96
110
|
}
|