dsh-git-ui 0.0.2 → 0.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/README.md +27 -20
- package/README.zh.md +28 -21
- package/cordis.patch.yml +1 -1
- package/lib/client.js +36 -35
- package/lib/client.js.map +4 -4
- package/lib/host/actions.d.ts +22 -2
- package/lib/host/core.d.ts +7 -1
- package/lib/host/index.d.ts +12 -10
- package/lib/host/index.js +366 -21
- package/lib/host/index.js.map +4 -4
- package/lib/host/parser.d.ts +37 -1
- package/lib/host/queries.d.ts +17 -0
- package/lib/host/types.d.ts +131 -1
- package/package.json +1 -1
- package/src/client/GitCenter.tsx +1411 -148
- package/src/client/GitPill.tsx +280 -55
- package/src/client/changes-diff.ts +63 -0
- package/src/client/controller.ts +36 -1
- package/src/client/error-text.ts +21 -0
- package/src/client/file-tree.ts +101 -0
- package/src/client/git-graph.ts +188 -0
- package/src/client/icons.tsx +292 -0
- package/src/client/index.ts +2 -1
- package/src/client/locales.ts +124 -0
- package/src/client/popup-close.ts +19 -0
- package/src/client/remote.ts +83 -1
- package/src/client/select-menu.tsx +113 -0
- package/src/client/side-by-side.ts +150 -0
- package/src/client/styles.ts +1375 -86
- package/src/client/time-format.ts +32 -0
- package/src/host/actions.ts +66 -15
- package/src/host/core.ts +9 -2
- package/src/host/index.ts +21 -10
- package/src/host/parser.ts +155 -12
- package/src/host/queries.ts +289 -0
- package/src/host/types.ts +104 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-free read-only query runner (history / diff / show / branches).
|
|
3
|
+
*
|
|
4
|
+
* Same layering as `core.ts`/`actions.ts`: structural injection, testable
|
|
5
|
+
* against real temporary repositories without a cordis runtime. Every query
|
|
6
|
+
* resolves the workspace once, then runs one or two read-only git commands
|
|
7
|
+
* against the repository root.
|
|
8
|
+
*/
|
|
9
|
+
import { resolveWorkspace, runCommand, type GitStatusConfig, type SnapshotDeps } from './core.ts'
|
|
10
|
+
import { parseBranchOutput, parseGraphLogOutput, parseNameStatusOutput, parseShowMeta } from './parser.ts'
|
|
11
|
+
import { isSafePath, operationError } from './actions.ts'
|
|
12
|
+
import type { GitBranch, GitQueryRequest, GitQueryResponse } from './types.ts'
|
|
13
|
+
|
|
14
|
+
/** Machine-readable log format for show queries (no parents). */
|
|
15
|
+
const LOG_FORMAT = '%H%x1f%h%x1f%s%x1f%an%x1f%aI'
|
|
16
|
+
/** 带图的 log 格式(%P = 父提交,%D = ref 装饰)。 */
|
|
17
|
+
const GRAPH_FORMAT = '%H%x1f%h%x1f%s%x1f%an%x1f%aI%x1f%P%x1f%D'
|
|
18
|
+
|
|
19
|
+
/** History page size cap (and default). 千条级 + 无限滚动。 */
|
|
20
|
+
const MAX_HISTORY_LIMIT = 1000
|
|
21
|
+
|
|
22
|
+
/** A ref is acceptable when non-empty and free of whitespace. */
|
|
23
|
+
function isValidRef(ref: string): boolean {
|
|
24
|
+
return ref !== '' && !/\s/.test(ref)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 执行一条只读查询。结果均为 JSON 纯数据且有界
|
|
29
|
+
* (history 分页;diff 文本受 runner 的 spill/截断约束)。
|
|
30
|
+
* `config` 当前未用:保留以与 runAction 共享 runner 签名契约
|
|
31
|
+
* (deps, config, request),后续查询限流等调优可直接启用。
|
|
32
|
+
*/
|
|
33
|
+
export async function runQuery(
|
|
34
|
+
deps: SnapshotDeps,
|
|
35
|
+
config: GitStatusConfig,
|
|
36
|
+
request: GitQueryRequest,
|
|
37
|
+
): Promise<GitQueryResponse> {
|
|
38
|
+
void config
|
|
39
|
+
const workspace = await resolveWorkspace(deps, request.sessionId)
|
|
40
|
+
if (!workspace.ok) return { ok: false, error: operationError(workspace.error).error }
|
|
41
|
+
const root = workspace.root
|
|
42
|
+
const query = request.query
|
|
43
|
+
|
|
44
|
+
switch (query.kind) {
|
|
45
|
+
case 'history':
|
|
46
|
+
return historyQuery(deps, root, query)
|
|
47
|
+
case 'diff':
|
|
48
|
+
return diffQuery(deps, root, query.path, query.base)
|
|
49
|
+
case 'show':
|
|
50
|
+
return showQuery(deps, root, query.ref)
|
|
51
|
+
case 'branches':
|
|
52
|
+
return branchesQuery(deps, root)
|
|
53
|
+
case 'tags':
|
|
54
|
+
return tagsQuery(deps, root)
|
|
55
|
+
case 'authors':
|
|
56
|
+
return authorsQuery(deps, root)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function historyQuery(
|
|
61
|
+
deps: SnapshotDeps,
|
|
62
|
+
root: string,
|
|
63
|
+
query: Extract<GitQueryRequest['query'], { kind: 'history' }>,
|
|
64
|
+
): Promise<GitQueryResponse> {
|
|
65
|
+
const safeLimit = Math.min(Math.max(Math.floor(query.limit), 0), MAX_HISTORY_LIMIT)
|
|
66
|
+
const safeSkip = Math.max(Math.floor(query.skip), 0)
|
|
67
|
+
if (query.ref !== undefined && !isValidRef(query.ref)) {
|
|
68
|
+
return { ok: false, error: { code: 'invalid-name', message: `invalid ref: ${query.ref}` } }
|
|
69
|
+
}
|
|
70
|
+
const search = query.search?.trim() ?? ''
|
|
71
|
+
const hexLike = /^[0-9a-f]{7,40}$/i.test(search)
|
|
72
|
+
// 哈希精准检索:仅定位目标提交自身(--no-walk 不遍历祖先)→ 单条目,
|
|
73
|
+
// 不再列出该提交的全部祖先;文本搜索走 --grep(-i -E,跨引用匹配)。
|
|
74
|
+
const scope = hexLike ? [] : query.ref === undefined ? ['--all'] : [query.ref]
|
|
75
|
+
const noWalk = hexLike ? ['--no-walk', search] : []
|
|
76
|
+
const filters: string[] = []
|
|
77
|
+
if (search !== '' && !hexLike) filters.push('--regexp-ignore-case', '--extended-regexp', `--grep=${search}`)
|
|
78
|
+
const author = query.author?.trim() ?? ''
|
|
79
|
+
if (author !== '') filters.push(`--author=${author}`)
|
|
80
|
+
const since = query.since?.trim() ?? ''
|
|
81
|
+
if (since !== '') filters.push(`--since=${since}`)
|
|
82
|
+
|
|
83
|
+
const log = await runCommand(
|
|
84
|
+
deps.run,
|
|
85
|
+
// -n/--skip 前置:git 的 `-n N` 出现在 `--no-walk` 之后会重置 no-walk
|
|
86
|
+
// (hexLike 会错误列出全部祖先),前置则 `-n 1000 --no-walk x` 恒返回单条。
|
|
87
|
+
['git', 'log', ...filters, `--skip=${String(safeSkip)}`, '-n', String(safeLimit), ...noWalk, ...scope, `--format=${GRAPH_FORMAT}`],
|
|
88
|
+
root,
|
|
89
|
+
'log',
|
|
90
|
+
deps.signal,
|
|
91
|
+
)
|
|
92
|
+
if ('failure' in log) return { ok: false, error: operationError(log.failure).error }
|
|
93
|
+
if (log.run.timedOut) return { ok: false, error: { code: 'timeout' } }
|
|
94
|
+
if (log.run.exitCode !== 0) {
|
|
95
|
+
// 未出生仓库无提交:git log 以 128 此信息退出——稳定空历史,非错误。
|
|
96
|
+
if (log.run.stderr.includes('does not have any commits')) {
|
|
97
|
+
return { ok: true, value: { kind: 'history', commits: [], total: 0 } }
|
|
98
|
+
}
|
|
99
|
+
// 哈希无解析(未命中)或前缀不唯一(ambiguous):稳定空结果(让用户输入更长前缀)。
|
|
100
|
+
if (hexLike && /unknown revision|bad revision|ambiguous/i.test(log.run.stderr)) {
|
|
101
|
+
return { ok: true, value: { kind: 'history', commits: [], total: 0 } }
|
|
102
|
+
}
|
|
103
|
+
return gitError('log', log.run.stderr, log.run.stdout)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 过滤范围内的提交总数(best-effort)。
|
|
107
|
+
let total = 0
|
|
108
|
+
const count = await runCommand(deps.run, ['git', 'rev-list', '--count', ...noWalk, ...scope, ...filters], root, 'rev-list', deps.signal)
|
|
109
|
+
if ('run' in count && count.run.exitCode === 0) {
|
|
110
|
+
const parsed = Number(count.run.stdout.trim())
|
|
111
|
+
if (Number.isFinite(parsed) && parsed >= 0) total = parsed
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 远程名用于 %D 装饰的远程分支分类;失败时降级为空列表(其余按本地分支处理)。
|
|
115
|
+
let remotes: readonly string[] = []
|
|
116
|
+
const remoteRun = await runCommand(deps.run, ['git', 'remote'], root, 'remote', deps.signal)
|
|
117
|
+
if ('run' in remoteRun && remoteRun.run.exitCode === 0) {
|
|
118
|
+
remotes = remoteRun.run.stdout.split('\n').map((s) => s.trim()).filter((s) => s !== '')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { ok: true, value: { kind: 'history', commits: parseGraphLogOutput(log.run.stdout, remotes), total } }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 单文件差异(变更界面对照查看用)。
|
|
126
|
+
* staged = --cached;worktree = 工作区对索引;
|
|
127
|
+
* 未版本管理文件 worktree 差异为空 → 回退 --no-index 与 /dev/null 对比(退出码 1 视为有差异的成功)。
|
|
128
|
+
*/
|
|
129
|
+
async function diffQuery(
|
|
130
|
+
deps: SnapshotDeps,
|
|
131
|
+
root: string,
|
|
132
|
+
path: string,
|
|
133
|
+
base: 'worktree' | 'staged',
|
|
134
|
+
): Promise<GitQueryResponse> {
|
|
135
|
+
if (!isSafePath(path, root)) return { ok: false, error: { code: 'invalid-path', message: `unsafe path: ${path}` } }
|
|
136
|
+
// 使用 -U999999 显示完整文档上下文(而非仅变更 hunk),支持文档浏览体验。
|
|
137
|
+
const argv = base === 'staged'
|
|
138
|
+
? ['git', 'diff', '--cached', '-U999999', '--', path]
|
|
139
|
+
: ['git', 'diff', '-U999999', '--', path]
|
|
140
|
+
const run = await runCommand(deps.run, argv, root, 'diff', deps.signal)
|
|
141
|
+
if ('failure' in run) return { ok: false, error: operationError(run.failure).error }
|
|
142
|
+
if (run.run.timedOut) return { ok: false, error: { code: 'timeout' } }
|
|
143
|
+
if (run.run.exitCode !== 0) return gitError('diff', run.run.stderr, run.run.stdout)
|
|
144
|
+
if (run.run.stdout !== '' || base === 'staged') {
|
|
145
|
+
return { ok: true, value: { kind: 'diff', path, text: run.run.stdout } }
|
|
146
|
+
}
|
|
147
|
+
// 空差异:可能是未版本管理文件——与 /dev/null 对比生成全增差异。
|
|
148
|
+
const ni = await runCommand(deps.run, ['git', 'diff', '--no-index', '-U999999', '--', '/dev/null', path], root, 'diff --no-index', deps.signal)
|
|
149
|
+
if ('failure' in ni) return { ok: false, error: operationError(ni.failure).error }
|
|
150
|
+
if (ni.run.timedOut) return { ok: false, error: { code: 'timeout' } }
|
|
151
|
+
if (ni.run.exitCode !== 0 && ni.run.exitCode !== 1) return gitError('diff', ni.run.stderr, ni.run.stdout)
|
|
152
|
+
return { ok: true, value: { kind: 'diff', path, text: ni.run.stdout } }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function showQuery(deps: SnapshotDeps, root: string, ref: string): Promise<GitQueryResponse> {
|
|
156
|
+
if (!isValidRef(ref)) return { ok: false, error: { code: 'invalid-name', message: `invalid ref: ${ref}` } }
|
|
157
|
+
// -s 仅输出格式块:%b 为排除首段落后的正文,独立调用避免解析歧义。
|
|
158
|
+
const meta = await runCommand(
|
|
159
|
+
deps.run,
|
|
160
|
+
['git', 'show', '-s', `--format=${LOG_FORMAT}%x1f%b`, ref],
|
|
161
|
+
root,
|
|
162
|
+
'show',
|
|
163
|
+
deps.signal,
|
|
164
|
+
)
|
|
165
|
+
if ('failure' in meta) return { ok: false, error: operationError(meta.failure).error }
|
|
166
|
+
if (meta.run.timedOut) return { ok: false, error: { code: 'timeout' } }
|
|
167
|
+
if (meta.run.exitCode !== 0) return gitError('show', meta.run.stderr, meta.run.stdout)
|
|
168
|
+
|
|
169
|
+
const stat = await runCommand(
|
|
170
|
+
deps.run,
|
|
171
|
+
['git', '-c', 'core.quotePath=false', 'show', '--format=', '--name-status', '-z', ref],
|
|
172
|
+
root,
|
|
173
|
+
'show --name-status',
|
|
174
|
+
deps.signal,
|
|
175
|
+
)
|
|
176
|
+
if ('failure' in stat) return { ok: false, error: operationError(stat.failure).error }
|
|
177
|
+
if (stat.run.timedOut) return { ok: false, error: { code: 'timeout' } }
|
|
178
|
+
if (stat.run.exitCode !== 0) return gitError('show', stat.run.stderr, stat.run.stdout)
|
|
179
|
+
|
|
180
|
+
const parsed = parseShowMeta(meta.run.stdout)
|
|
181
|
+
return {
|
|
182
|
+
ok: true,
|
|
183
|
+
value: {
|
|
184
|
+
kind: 'show',
|
|
185
|
+
ref,
|
|
186
|
+
commit: parsed?.commit ?? null,
|
|
187
|
+
body: parsed?.body ?? '',
|
|
188
|
+
stats: parseNameStatusOutput(stat.run.stdout),
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** 作者列表(工具栏用户选择用),去重排序截断 100。 */
|
|
194
|
+
async function authorsQuery(deps: SnapshotDeps, root: string): Promise<GitQueryResponse> {
|
|
195
|
+
const run = await runCommand(deps.run, ['git', 'log', '--all', '-n', '1000', '--format=%an'], root, 'log authors', deps.signal)
|
|
196
|
+
if ('failure' in run) return { ok: false, error: operationError(run.failure).error }
|
|
197
|
+
if (run.run.timedOut) return { ok: false, error: { code: 'timeout' } }
|
|
198
|
+
if (run.run.exitCode !== 0) return { ok: true, value: { kind: 'authors', authors: [] } }
|
|
199
|
+
const authors = [...new Set(run.run.stdout.split('\n').map((s) => s.trim()).filter((s) => s !== ''))].sort().slice(0, 100)
|
|
200
|
+
return { ok: true, value: { kind: 'authors', authors } }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** 标签列表(左栏过滤树用),复用 tab 分隔解析。 */
|
|
204
|
+
async function tagsQuery(deps: SnapshotDeps, root: string): Promise<GitQueryResponse> {
|
|
205
|
+
const FORMAT = '--format=%(refname:short)%09%(objectname:short)'
|
|
206
|
+
const run = await runCommand(deps.run, ['git', 'tag', FORMAT], root, 'tag', deps.signal)
|
|
207
|
+
if ('failure' in run) return { ok: false, error: operationError(run.failure).error }
|
|
208
|
+
if (run.run.timedOut) return { ok: false, error: { code: 'timeout' } }
|
|
209
|
+
if (run.run.exitCode !== 0) return gitError('tag', run.run.stderr, run.run.stdout)
|
|
210
|
+
return { ok: true, value: { kind: 'tags', tags: parseBranchList(run.run.stdout) } }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function branchesQuery(deps: SnapshotDeps, root: string): Promise<GitQueryResponse> {
|
|
214
|
+
// 本地分支格式:name\thash\tupstream\ttrack(track 如 [ahead 2, behind 1])。
|
|
215
|
+
// 远程分支无上游 → upstream/track 为空。
|
|
216
|
+
const LOCAL_FORMAT = '--format=%(refname:short)%09%(objectname:short)%09%(upstream:short)%09%(upstream:track)'
|
|
217
|
+
const REMOTE_FORMAT = '--format=%(refname:short)%09%(objectname:short)'
|
|
218
|
+
const local = await runCommand(deps.run, ['git', 'branch', LOCAL_FORMAT], root, 'branch', deps.signal)
|
|
219
|
+
if ('failure' in local) return { ok: false, error: operationError(local.failure).error }
|
|
220
|
+
if (local.run.timedOut) return { ok: false, error: { code: 'timeout' } }
|
|
221
|
+
if (local.run.exitCode !== 0) return gitError('branch', local.run.stderr, local.run.stdout)
|
|
222
|
+
|
|
223
|
+
const remote = await runCommand(deps.run, ['git', 'branch', '-r', REMOTE_FORMAT], root, 'branch -r', deps.signal)
|
|
224
|
+
if ('failure' in remote) return { ok: false, error: operationError(remote.failure).error }
|
|
225
|
+
if (remote.run.timedOut) return { ok: false, error: { code: 'timeout' } }
|
|
226
|
+
if (remote.run.exitCode !== 0) return gitError('branch -r', remote.run.stderr, remote.run.stdout)
|
|
227
|
+
|
|
228
|
+
const current = await runCommand(deps.run, ['git', 'branch', '--show-current'], root, 'branch', deps.signal)
|
|
229
|
+
const currentName = 'run' in current && current.run.exitCode === 0 ? parseBranchOutput(current.run.stdout) : null
|
|
230
|
+
|
|
231
|
+
// 默认分支:origin/HEAD 符号引用(如 origin/main);失败降级 null。
|
|
232
|
+
let defaultBranch: string | null = null
|
|
233
|
+
const def = await runCommand(deps.run, ['git', 'symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], root, 'symbolic-ref', deps.signal)
|
|
234
|
+
if ('run' in def && def.run.exitCode === 0) {
|
|
235
|
+
const value = def.run.stdout.trim()
|
|
236
|
+
const slash = value.indexOf('/')
|
|
237
|
+
defaultBranch = value === '' ? null : slash === -1 ? value : value.slice(slash + 1)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
ok: true,
|
|
242
|
+
value: {
|
|
243
|
+
kind: 'branches',
|
|
244
|
+
current: currentName,
|
|
245
|
+
defaultBranch,
|
|
246
|
+
local: parseBranchList(local.run.stdout),
|
|
247
|
+
remote: parseBranchList(remote.run.stdout).filter((branch) => !branch.name.endsWith('/HEAD')),
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 解析 `%(refname:short)%09%(objectname:short)[%09%(upstream:short)%09%(upstream:track)]` 行。
|
|
254
|
+
* 本地分支含 4 字段(upstream + track),远程分支仅 2 字段(无上游)。
|
|
255
|
+
* track 格式:`[ahead N]`、`[behind N]`、`[ahead N, behind N]` 或空(无上游/已同步)。
|
|
256
|
+
*/
|
|
257
|
+
function parseBranchList(output: string): readonly GitBranch[] {
|
|
258
|
+
const branches: GitBranch[] = []
|
|
259
|
+
for (const line of output.split('\n')) {
|
|
260
|
+
if (line === '') continue
|
|
261
|
+
const parts = line.split('\t')
|
|
262
|
+
const name = parts[0]
|
|
263
|
+
const hash = parts[1]
|
|
264
|
+
if (name === undefined || name === '') continue
|
|
265
|
+
const track = parts[3] ?? ''
|
|
266
|
+
const aheadMatch = /ahead (\d+)/.exec(track)
|
|
267
|
+
const behindMatch = /behind (\d+)/.exec(track)
|
|
268
|
+
const ahead = aheadMatch ? Number(aheadMatch[1]) : 0
|
|
269
|
+
const behind = behindMatch ? Number(behindMatch[1]) : 0
|
|
270
|
+
branches.push({
|
|
271
|
+
name,
|
|
272
|
+
shortHash: hash === undefined || hash === '' ? null : hash,
|
|
273
|
+
...(ahead > 0 ? { ahead } : {}),
|
|
274
|
+
...(behind > 0 ? { behind } : {}),
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
return branches
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function gitError(label: string, stderr: string, stdout: string): GitQueryResponse {
|
|
281
|
+
const message = stderr.trim() || stdout.trim()
|
|
282
|
+
return {
|
|
283
|
+
ok: false,
|
|
284
|
+
error: {
|
|
285
|
+
code: 'git-error',
|
|
286
|
+
message: message !== '' ? message : `git ${label} failed`,
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
}
|
package/src/host/types.ts
CHANGED
|
@@ -58,10 +58,40 @@ export interface GitCommit {
|
|
|
58
58
|
readonly dateIso: string
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* 带父引用的提交(图渲染用)。
|
|
63
|
+
* `parents` 为完整 SHA(线上格式以空格分隔);根提交为空数组。
|
|
64
|
+
* `refs` 为 `%D` 装饰(分支 / 远程 / 标签)。
|
|
65
|
+
*/
|
|
66
|
+
export interface GraphCommit extends GitCommit {
|
|
67
|
+
readonly parents: readonly string[]
|
|
68
|
+
readonly refs: readonly GitRef[]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 提交上挂载的一个 ref 装饰(分支 / 远程 / 标签)。 */
|
|
72
|
+
export interface GitRef {
|
|
73
|
+
readonly kind: 'branch' | 'remote' | 'tag'
|
|
74
|
+
readonly name: string
|
|
75
|
+
/** `HEAD -> name` 的当前分支为 true。 */
|
|
76
|
+
readonly head: boolean
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 一条变更文件条目。混合状态(porcelain XY 双列均非空,如 MM/AM)会被
|
|
81
|
+
* 拆为两条:`staged: true` 一侧(状态取 X 列)与 `staged: false` 一侧
|
|
82
|
+
* (状态取 Y 列),UI 据此分列「已暂存更改 / 更改」两组(IDEA 式)。
|
|
83
|
+
* 真实冲突(UU/AA/DD 等)保持单条 `conflicted`。
|
|
84
|
+
*/
|
|
61
85
|
export interface GitChange {
|
|
62
86
|
readonly path: string
|
|
63
87
|
readonly status: GitChangeStatus
|
|
64
88
|
readonly staged: boolean
|
|
89
|
+
/**
|
|
90
|
+
* 目录条目(git status 对未跟踪目录输出 `dir/`,host 解析时权威标记)。
|
|
91
|
+
* 展示层必须以此字段判断目录而非字符串派生——任何路径规范化剥离尾斜杠
|
|
92
|
+
* 都不会丢失目录性(`.agent/` 曾被当文件展示的根因防护)。
|
|
93
|
+
*/
|
|
94
|
+
readonly isDirectory: boolean
|
|
65
95
|
}
|
|
66
96
|
|
|
67
97
|
export type GitChangeStatus =
|
|
@@ -92,6 +122,10 @@ export type GitAction =
|
|
|
92
122
|
* empty commits everything already staged. */
|
|
93
123
|
readonly paths?: readonly string[]
|
|
94
124
|
}
|
|
125
|
+
| { readonly kind: 'branch-create'; readonly name: string; readonly from?: string }
|
|
126
|
+
| { readonly kind: 'branch-checkout'; readonly name: string }
|
|
127
|
+
| { readonly kind: 'branch-delete'; readonly name: string; readonly force?: boolean }
|
|
128
|
+
| { readonly kind: 'fetch' }
|
|
95
129
|
|
|
96
130
|
export type GitOperationErrorCode =
|
|
97
131
|
| 'session-not-found'
|
|
@@ -99,8 +133,13 @@ export type GitOperationErrorCode =
|
|
|
99
133
|
| 'path-not-found'
|
|
100
134
|
| 'not-a-git-repo'
|
|
101
135
|
| 'invalid-path'
|
|
136
|
+
| 'invalid-name'
|
|
102
137
|
| 'git-error'
|
|
103
138
|
| 'timeout'
|
|
139
|
+
/** 切分支被工作区未提交变更阻止(git: "would be overwritten by checkout")。
|
|
140
|
+
* host 归一化为业务错误:client 用友好文案 +「处理变更」引导,不再直接
|
|
141
|
+
* 抛原始多行 git stderr。原始信息保留在 message。 */
|
|
142
|
+
| 'local-changes-block'
|
|
104
143
|
|
|
105
144
|
export type GitActionResult =
|
|
106
145
|
| { readonly ok: true; readonly snapshot: GitSnapshot; readonly output?: string }
|
|
@@ -111,3 +150,68 @@ export interface GitActionRequest {
|
|
|
111
150
|
readonly sessionId: string
|
|
112
151
|
readonly action: GitAction
|
|
113
152
|
}
|
|
153
|
+
|
|
154
|
+
// ── Query endpoint (read-only inspections: history / diff / show / branches) ──
|
|
155
|
+
|
|
156
|
+
/** 一条只读查询,对应 `gitInfo/query` 端点。 */
|
|
157
|
+
export type GitQuery =
|
|
158
|
+
| {
|
|
159
|
+
readonly kind: 'history'
|
|
160
|
+
readonly limit: number
|
|
161
|
+
readonly skip: number
|
|
162
|
+
/** 可选 ref 过滤(分支/远程/标签);缺省为 --all 全分支。 */
|
|
163
|
+
readonly ref?: string
|
|
164
|
+
/** 文本搜索:7+ 位十六进制视为哈希前缀跳转;否则提交信息正则搜索(-i -E)。 */
|
|
165
|
+
readonly search?: string
|
|
166
|
+
/** 作者过滤(--author)。 */
|
|
167
|
+
readonly author?: string
|
|
168
|
+
/** 日期下限(--since,如 '7 days ago')。 */
|
|
169
|
+
readonly since?: string
|
|
170
|
+
}
|
|
171
|
+
| { readonly kind: 'diff'; readonly path: string; readonly base: 'worktree' | 'staged' }
|
|
172
|
+
| { readonly kind: 'show'; readonly ref: string }
|
|
173
|
+
| { readonly kind: 'branches' }
|
|
174
|
+
| { readonly kind: 'tags' }
|
|
175
|
+
| { readonly kind: 'authors' }
|
|
176
|
+
|
|
177
|
+
/** 提交变更文件行(`--name-status` 源:状态 + 路径,不再携带 +/- 行数)。 */
|
|
178
|
+
export interface GitFileStat {
|
|
179
|
+
readonly path: string
|
|
180
|
+
readonly status: GitChangeStatus
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** One branch row from `git branch --format`. Local branches may carry
|
|
184
|
+
* ahead/behind counts relative to their upstream (from `%(upstream:track)`). */
|
|
185
|
+
export interface GitBranch {
|
|
186
|
+
readonly name: string
|
|
187
|
+
readonly shortHash: string | null
|
|
188
|
+
/** 本地分支领先上游的提交数(仅本地有上游时存在)。 */
|
|
189
|
+
readonly ahead?: number
|
|
190
|
+
/** 本地分支落后上游的提交数(仅本地有上游时存在)。 */
|
|
191
|
+
readonly behind?: number
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export type GitQueryResult =
|
|
195
|
+
| { readonly kind: 'history'; readonly commits: readonly GraphCommit[]; readonly total: number }
|
|
196
|
+
| { readonly kind: 'diff'; readonly path: string; readonly text: string }
|
|
197
|
+
| {
|
|
198
|
+
readonly kind: 'show'
|
|
199
|
+
readonly ref: string
|
|
200
|
+
readonly commit: GitCommit | null
|
|
201
|
+
/** 提交完整正文(不含 subject 行);IDEA 式右栏展示用。 */
|
|
202
|
+
readonly body: string
|
|
203
|
+
readonly stats: readonly GitFileStat[]
|
|
204
|
+
}
|
|
205
|
+
| { readonly kind: 'branches'; readonly current: string | null; readonly defaultBranch: string | null; readonly local: readonly GitBranch[]; readonly remote: readonly GitBranch[] }
|
|
206
|
+
| { readonly kind: 'tags'; readonly tags: readonly GitBranch[] }
|
|
207
|
+
| { readonly kind: 'authors'; readonly authors: readonly string[] }
|
|
208
|
+
|
|
209
|
+
export type GitQueryResponse =
|
|
210
|
+
| { readonly ok: true; readonly value: GitQueryResult }
|
|
211
|
+
| { readonly ok: false; readonly error: { readonly code: GitOperationErrorCode; readonly message?: string } }
|
|
212
|
+
|
|
213
|
+
/** Wire request of the `query` endpoint. */
|
|
214
|
+
export interface GitQueryRequest {
|
|
215
|
+
readonly sessionId: string
|
|
216
|
+
readonly query: GitQuery
|
|
217
|
+
}
|