feat: Git 面板真正的 diff 显示 + VSCode 式历史与右键高级操作,v0.3.0
- 真正的统一 diff 显示:hunk 头 + 双行号 + 红绿着色,长行换行无横向滚动,untracked 全新增视图 - 点击变更/历史自动打开独立 diff tab(首次在下半栏新增分栏,同变更去重聚焦,临时页签刷新不恢复) - 历史行:分支徽章 + 作者/相对时间,懒加载分页(20 条/页 + 加载更多) - 右键菜单:文件行(打开/暂存/放弃更改/复制路径)、历史行(复制哈希/信息/还原/捡取),破坏性操作确认弹窗 - 修复暂存/未暂存分类错误(' M' 被误标为已暂存导致 diff 恒空) - 修复 git 相对路径解析(会话在仓库子目录时 diff/discard/show/read 可用) - host 新增 git.commit-diff / git.discard / git.revert / git.cherry-pick 路由与 log 分页
This commit is contained in:
+50
-6
@@ -27,10 +27,16 @@ export interface GitStatusResult {
|
||||
|
||||
/** One `git log` row. */
|
||||
export interface GitLogEntry {
|
||||
/** Short hash (7+ chars, display). */
|
||||
hash: string
|
||||
/** Full 40-char hash (advanced operations: revert / cherry-pick). */
|
||||
hashFull: string
|
||||
subject: string
|
||||
author: string
|
||||
/** ISO 8601 author date (`%ai`), e.g. `2024-01-01 10:00:00 +0800`. */
|
||||
date: string
|
||||
/** Ref decorations (`%D` with --decorate=short), e.g. `HEAD -> main, origin/main`; '' when none. */
|
||||
refs: string
|
||||
}
|
||||
|
||||
/** One git failure (stderr text as the message). */
|
||||
@@ -65,14 +71,21 @@ export function parsePorcelainZ(output: string): GitStatusEntry[] {
|
||||
return entries
|
||||
}
|
||||
|
||||
/** Parse `git log --pretty=format:%h%x1f%s%x1f%an%x1f%ai` rows. */
|
||||
/** Parse `git log --pretty=format:%h%x1f%s%x1f%an%x1f%ai%x1f%H%x1f%D` rows. */
|
||||
export function parseLogLines(output: string): GitLogEntry[] {
|
||||
const rows: GitLogEntry[] = []
|
||||
for (const line of output.split('\n')) {
|
||||
if (line === '') continue
|
||||
const [hash, subject, author, date] = line.split('\x1f')
|
||||
const [hash, subject, author, date, hashFull, refs] = line.split('\x1f')
|
||||
if (hash === undefined || subject === undefined) continue
|
||||
rows.push({ hash, subject, author: author ?? '', date: date ?? '' })
|
||||
rows.push({
|
||||
hash,
|
||||
subject,
|
||||
author: author ?? '',
|
||||
date: date ?? '',
|
||||
hashFull: hashFull ?? hash,
|
||||
refs: refs ?? '',
|
||||
})
|
||||
}
|
||||
return rows
|
||||
}
|
||||
@@ -118,6 +131,12 @@ export async function isGitRepo(cwd: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
/** The repository top level containing `cwd` (`git rev-parse --show-toplevel`). */
|
||||
export async function repoRoot(cwd: string): Promise<string> {
|
||||
const out = await runGit(cwd, ['rev-parse', '--show-toplevel'])
|
||||
return out.trim()
|
||||
}
|
||||
|
||||
/** The current branch name (`git rev-parse --abbrev-ref HEAD`; 'HEAD' when detached). */
|
||||
export async function currentBranch(cwd: string): Promise<string> {
|
||||
const out = await runGit(cwd, ['rev-parse', '--abbrev-ref', 'HEAD'])
|
||||
@@ -173,9 +192,12 @@ export async function checkout(cwd: string, branch: string): Promise<void> {
|
||||
await runGit(cwd, ['checkout', branch])
|
||||
}
|
||||
|
||||
/** Recent commit history (newest first). */
|
||||
export async function log(cwd: string, count = 30): Promise<GitLogEntry[]> {
|
||||
const raw = await runGit(cwd, ['log', `-n ${count}`, '--pretty=format:%h%x1f%s%x1f%an%x1f%ai'])
|
||||
/** Recent commit history (newest first), lazily pageable via skip/count. */
|
||||
export async function log(cwd: string, count = 30, skip = 0): Promise<GitLogEntry[]> {
|
||||
const raw = await runGit(cwd, [
|
||||
'log', '-n', String(count), '--skip', String(skip), '--decorate=short',
|
||||
'--pretty=format:%h%x1f%s%x1f%an%x1f%ai%x1f%H%x1f%D',
|
||||
])
|
||||
return parseLogLines(raw)
|
||||
}
|
||||
|
||||
@@ -190,3 +212,25 @@ export async function show(cwd: string, rev: string, path: string): Promise<stri
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** Full patch text of one commit (`git show` with the commit header suppressed).
|
||||
* Merge commits show their diff against the first parent (`-m --first-parent`
|
||||
* is a no-op for regular commits), so a history click always has content. */
|
||||
export async function commitDiff(cwd: string, hash: string): Promise<string> {
|
||||
return runGit(cwd, ['show', '--no-ext-diff', '--no-color', '--format=', '-m', '--first-parent', hash])
|
||||
}
|
||||
|
||||
/** Discard the worktree changes of one path (`git checkout -- <path>`; the index is untouched). */
|
||||
export async function discard(cwd: string, path: string): Promise<void> {
|
||||
await runGit(cwd, ['checkout', '--', path])
|
||||
}
|
||||
|
||||
/** Revert one commit onto the current branch with an auto-generated message. */
|
||||
export async function revert(cwd: string, hash: string): Promise<void> {
|
||||
await runGit(cwd, ['revert', '--no-edit', hash])
|
||||
}
|
||||
|
||||
/** Cherry-pick one commit onto the current branch. */
|
||||
export async function cherryPick(cwd: string, hash: string): Promise<void> {
|
||||
await runGit(cwd, ['cherry-pick', hash])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user