Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

## [Unreleased]

### Added — Parity Batch 2(UI 丰富度,0.0.3)

- **Git 提交图(WebviewPanel)**:`git log --graph --oneline --decorate --all`(CLI)获取拓扑,语义着色渲染(graph 连线 / refs / hash)——补齐 IDEA Log 提交图的可视化拓扑。命令面板 + Log 视图标题按钮。
- **Console**:Hyper Git Console(OutputChannel)记录所有 `execGit` 命令与输出(对齐 IDEA Console 标签页)。

### Added — Parity Batch 1(CLI 功能补齐,0.0.2)

> 关键转向:引入 `GitRepositoryService.execGit`(复用 vscode.git 的同一 git 二进制 `api.git.path`),补齐稳定 API 未暴露的操作——修正此前"API 限制延后"的过度自我设限。
Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "Hyper Git",
"icon": "media/icon.png",
"description": "在 VS Code 上完整复刻 IntelliJ IDEA 的 Git 工具窗口与 Commit 提交窗口,并为未来 AI Agent 自主代理预留架构接缝。",
"version": "0.0.2",
"version": "0.0.3",
"publisher": "threefish-ai",
"license": "MIT",
"preview": true,
Expand Down Expand Up @@ -130,13 +130,16 @@
{ "command": "hyperGit.branchRename", "title": "重命名分支", "category": "Hyper Git" },
{ "command": "hyperGit.ignorePath", "title": "添加到 .gitignore", "category": "Hyper Git" },
{ "command": "hyperGit.compareBranches", "title": "比较分支", "category": "Hyper Git" },
{ "command": "hyperGit.rewordCommit", "title": "改写最新提交信息", "category": "Hyper Git" }
{ "command": "hyperGit.rewordCommit", "title": "改写最新提交信息", "category": "Hyper Git" },
{ "command": "hyperGit.showGraph", "title": "查看提交图(Graph)", "category": "Hyper Git", "icon": "$(git-commit)" },
{ "command": "hyperGit.showConsole", "title": "打开 Console", "category": "Hyper Git" }
],
"menus": {
"view/title": [
{ "command": "hyperGit.refresh", "when": "view == hyperGit.changes", "group": "navigation" },
{ "command": "hyperGit.newChangelist", "when": "view == hyperGit.changes", "group": "navigation" },
{ "command": "hyperGit.refreshLog", "when": "view == hyperGit.log", "group": "navigation" },
{ "command": "hyperGit.showGraph", "when": "view == hyperGit.log", "group": "navigation" },
{ "command": "hyperGit.logFilterAuthor", "when": "view == hyperGit.log" },
{ "command": "hyperGit.logFilterPath", "when": "view == hyperGit.log" },
{ "command": "hyperGit.logClearFilter", "when": "view == hyperGit.log" },
Expand Down
3 changes: 3 additions & 0 deletions src/adapter/git-repository-service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { execFile } from 'child_process';
import * as path from 'path';
import * as vscode from 'vscode';
import { logGit } from '../infra/git-console';
import type { API, Change, Repository } from '../types/git';
import { FileStatus } from '../engine/model';
import { mapGitStatus } from './git-status-map';
Expand Down Expand Up @@ -114,8 +115,10 @@ export class GitRepositoryService implements vscode.Disposable {
return new Promise((resolve, reject) => {
execFile(this.api.git.path, args, { cwd: repo.rootUri.fsPath, maxBuffer: 20 * 1024 * 1024, encoding: 'utf8' }, (err, stdout) => {
if (err) {
logGit(args, undefined, err.message);
reject(err);
} else {
logGit(args, stdout);
resolve(stdout);
}
});
Expand Down
75 changes: 75 additions & 0 deletions src/adapter/webview/graph-webview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import * as vscode from 'vscode';
import type { GitRepositoryService } from '../git-repository-service';

/**
* Git 提交图(WebviewPanel)。
*
* 用 `git log --graph --oneline --decorate --all`(受控 CLI 通道)获取拓扑文本,在 Webview 内以
* 等宽字体 + 语义着色渲染(graph 连线、refs、hash)——补齐 IDEA Log 提交图的可视化拓扑。
* 完整像素级 lane-SVG 渲染作为后续增强(batch 2.x)。
*/
export class GraphWebview {
private static readonly viewType = 'hyperGit.graph';

static async open(service: GitRepositoryService): Promise<void> {
const repo = service.repo;
if (!repo) {
void vscode.window.showWarningMessage('未找到 Git 仓库');
return;
}
let graph = '';
try {
graph = await service.execGit(['log', '--graph', '--oneline', '--decorate', '--all', '-n', '300']);
} catch (e) {
void vscode.window.showErrorMessage(`获取提交图失败:${e instanceof Error ? e.message : String(e)}`);
return;
}

const panel = vscode.window.createWebviewPanel(GraphWebview.viewType, 'Git Graph — Hyper Git', vscode.ViewColumn.Active, {
enableScripts: false,
retainContextWhenHidden: false,
});
panel.webview.html = GraphWebview.renderHtml(graph, repo.rootUri.fsPath);
}

private static renderHtml(graph: string, repoRoot: string): string {
const lines = graph.split('\n').map(GraphWebview.renderLine).join('\n');
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'">
<style>
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); }
h3 { margin: 0 0 8px; font-weight: 600; font-size: 13px; opacity: 0.85; }
.repo { opacity: 0.6; font-size: 11px; margin-bottom: 10px; word-break: break-all; }
pre { margin: 0; white-space: pre; line-height: 1.5; overflow-x: auto; }
.graph { color: var(--vscode-gitDecoration-addedResourceForeground, #3fb950); }
.hash { color: var(--vscode-editorWarning-foreground, #d29922); }
.ref { color: var(--vscode-textLink-foreground, #4dabf7); font-weight: 600; }
</style>
</head>
<body>
<h3>Git 提交图(最近 300 条)</h3>
<div class="repo">${escapeHtml(repoRoot)}</div>
<pre>${lines}</pre>
</body>
</html>`;
}

private static renderLine(line: string): string {
const m = line.match(/^([*|/\\_. ]+)(.*)$/);
if (!m) {
return escapeHtml(line);
}
const graphPart = escapeHtml(m[1]);
let rest = escapeHtml(m[2]);
rest = rest.replace(/(\([^)]*\))/g, '<span class="ref">$1</span>');
rest = rest.replace(/\b([0-9a-f]{7,40})\b/g, '<span class="hash">$1</span>');
return `<span class="graph">${graphPart}</span>${rest}`;
}
}

function escapeHtml(s: string): string {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
4 changes: 4 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { registerStashCommands } from './adapter/stash-commands';
import { registerGitCliCommands } from './adapter/git-cli-commands';
import { StashTreeProvider } from './adapter/tree/stash-tree';
import { CommitWebviewProvider } from './adapter/webview/commit-webview';
import { GraphWebview } from './adapter/webview/graph-webview';
import { showGitConsole } from './infra/git-console';
import { getGitApi } from './adapter/git-api';
import { GitRepositoryService } from './adapter/git-repository-service';
import { createLogger } from './infra/logger';
Expand Down Expand Up @@ -81,6 +83,8 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
...registerStashCommands(service, stashTree),
vscode.commands.registerCommand('hyperGit.commit', focusCommitView),
vscode.commands.registerCommand('hyperGit.commitAndPush', focusCommitView),
vscode.commands.registerCommand('hyperGit.showGraph', () => GraphWebview.open(service)),
vscode.commands.registerCommand('hyperGit.showConsole', () => showGitConsole()),
);

// git 状态变化频繁(add/checkout/diff 缓存失效均触发),防抖合并避免 log/stash 高频重拉。
Expand Down
31 changes: 31 additions & 0 deletions src/infra/git-console.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as vscode from 'vscode';

/**
* Hyper Git Console:对齐 IDEA Console 标签页,记录所有经 execGit 执行的 git 命令及其输出。
* 复用单一 OutputChannel(懒构造)。
*/
let channel: vscode.OutputChannel | undefined;

function getChannel(): vscode.OutputChannel {
if (!channel) {
channel = vscode.window.createOutputChannel('Hyper Git Console');
}
return channel;
}

/** 记录一条 git 命令(及其输出/错误)到 Console。 */
export function logGit(args: readonly string[], output?: string, error?: string): void {
const c = getChannel();
c.appendLine(`$ git ${args.join(' ')}`);
if (output) {
c.appendLine(output);
}
if (error) {
c.appendLine(`[error] ${error}`);
}
}

/** 显示 Console 面板。 */
export function showGitConsole(): void {
getChannel().show(true);
}
2 changes: 2 additions & 0 deletions tests/suite/extension.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ suite('扩展冒烟测试', function () {
'hyperGit.ignorePath',
'hyperGit.compareBranches',
'hyperGit.rewordCommit',
'hyperGit.showGraph',
'hyperGit.showConsole',
]) {
assert.ok(commands.includes(cmd), `命令 ${cmd} 未注册`);
}
Expand Down
Loading