|
| 1 | +import * as vscode from 'vscode'; |
| 2 | +import type { GitRepositoryService } from '../git-repository-service'; |
| 3 | + |
| 4 | +/** |
| 5 | + * Git 提交图(WebviewPanel)。 |
| 6 | + * |
| 7 | + * 用 `git log --graph --oneline --decorate --all`(受控 CLI 通道)获取拓扑文本,在 Webview 内以 |
| 8 | + * 等宽字体 + 语义着色渲染(graph 连线、refs、hash)——补齐 IDEA Log 提交图的可视化拓扑。 |
| 9 | + * 完整像素级 lane-SVG 渲染作为后续增强(batch 2.x)。 |
| 10 | + */ |
| 11 | +export class GraphWebview { |
| 12 | + private static readonly viewType = 'hyperGit.graph'; |
| 13 | + |
| 14 | + static async open(service: GitRepositoryService): Promise<void> { |
| 15 | + const repo = service.repo; |
| 16 | + if (!repo) { |
| 17 | + void vscode.window.showWarningMessage('未找到 Git 仓库'); |
| 18 | + return; |
| 19 | + } |
| 20 | + let graph = ''; |
| 21 | + try { |
| 22 | + graph = await service.execGit(['log', '--graph', '--oneline', '--decorate', '--all', '-n', '300']); |
| 23 | + } catch (e) { |
| 24 | + void vscode.window.showErrorMessage(`获取提交图失败:${e instanceof Error ? e.message : String(e)}`); |
| 25 | + return; |
| 26 | + } |
| 27 | + |
| 28 | + const panel = vscode.window.createWebviewPanel(GraphWebview.viewType, 'Git Graph — Hyper Git', vscode.ViewColumn.Active, { |
| 29 | + enableScripts: false, |
| 30 | + retainContextWhenHidden: false, |
| 31 | + }); |
| 32 | + panel.webview.html = GraphWebview.renderHtml(graph, repo.rootUri.fsPath); |
| 33 | + } |
| 34 | + |
| 35 | + private static renderHtml(graph: string, repoRoot: string): string { |
| 36 | + const lines = graph.split('\n').map(GraphWebview.renderLine).join('\n'); |
| 37 | + return `<!DOCTYPE html> |
| 38 | +<html lang="zh-CN"> |
| 39 | +<head> |
| 40 | +<meta charset="UTF-8"> |
| 41 | +<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'"> |
| 42 | +<style> |
| 43 | +body { margin: 0; padding: 12px 16px; font-family: var(--vscode-editor-font-family), ui-monospace, Menlo, Consolas, monospace; font-size: var(--vscode-editor-font-size); color: var(--vscode-foreground); background: var(--vscode-editor-background); } |
| 44 | +h3 { margin: 0 0 8px; font-weight: 600; font-size: 13px; opacity: 0.85; } |
| 45 | +.repo { opacity: 0.6; font-size: 11px; margin-bottom: 10px; word-break: break-all; } |
| 46 | +pre { margin: 0; white-space: pre; line-height: 1.5; overflow-x: auto; } |
| 47 | +.graph { color: var(--vscode-gitDecoration-addedResourceForeground, #3fb950); } |
| 48 | +.hash { color: var(--vscode-editorWarning-foreground, #d29922); } |
| 49 | +.ref { color: var(--vscode-textLink-foreground, #4dabf7); font-weight: 600; } |
| 50 | +</style> |
| 51 | +</head> |
| 52 | +<body> |
| 53 | +<h3>Git 提交图(最近 300 条)</h3> |
| 54 | +<div class="repo">${escapeHtml(repoRoot)}</div> |
| 55 | +<pre>${lines}</pre> |
| 56 | +</body> |
| 57 | +</html>`; |
| 58 | + } |
| 59 | + |
| 60 | + private static renderLine(line: string): string { |
| 61 | + const m = line.match(/^([*|/\\_. ]+)(.*)$/); |
| 62 | + if (!m) { |
| 63 | + return escapeHtml(line); |
| 64 | + } |
| 65 | + const graphPart = escapeHtml(m[1]); |
| 66 | + let rest = escapeHtml(m[2]); |
| 67 | + rest = rest.replace(/(\([^)]*\))/g, '<span class="ref">$1</span>'); |
| 68 | + rest = rest.replace(/\b([0-9a-f]{7,40})\b/g, '<span class="hash">$1</span>'); |
| 69 | + return `<span class="graph">${graphPart}</span>${rest}`; |
| 70 | + } |
| 71 | +} |
| 72 | + |
| 73 | +function escapeHtml(s: string): string { |
| 74 | + return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); |
| 75 | +} |
0 commit comments