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
9 changes: 9 additions & 0 deletions .agents/issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,12 @@
- **后续防范**:含 test-electron 的扩展,eslint ignores 必须含 `.vscode-test/**`(及 `out/**`、`dist/**`、`*.vsix`);CI 因不缓存该目录可能不暴露,但本地必现——本地与 CI 环境差异需警惕。
- **同类问题影响**:所有跑过 test-electron 的本地环境的 eslint/其他静态分析工具。

## #6 vscode.git 公开 API add() 须传绝对路径

- **表因**:调用 `Repository.add(['README.md'])`(相对路径)无效或误加文件;CommitService 初期也曾困惑路径语义。
- **根因**:`extensions/git/src/api/api1.ts` 的 `add(paths)` 实现为 `paths.map(p => Uri.file(p))`——`Uri.file()` 要求**绝对路径**;相对路径会被包装成畸形 Uri,内部 `path.relative(root, ...)` 计算错误。`revert`/`clean`/`restore` 同理。
- **处理方式**:CommitService 始终传 `ChangeItem.uri.fsPath`(绝对)。
- **后续防范**:消费 vscode.git 公开 API 的路径类方法(add/revert/clean/restore)一律传绝对 fsPath;已加集成测试 `tests/suite/commit-flow.test.js` 守护。
- **同类问题影响**:所有消费 vscode.git API 做 stage/revert 的扩展;git CLI 本身接受相对路径,但**公开 API 层不接受**,二者语义差异易踩。


9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@

## [Unreleased]

### Added — M2 Commit 提交窗口(0.3.0)

- Commit 提交窗口(WebviewView 自绘 IDEA 风格):活动 changelist 文件勾选 + 多行 Commit Message 编辑器 + Amend / Signed-off-by / 跳过 Git hooks 选项 + Commit / Commit and Push 按钮。
- Conventional Commits 实时校验:`engine/commit/conventional-linter` 纯函数 + webview 指示器(ok/warning/error)+ 内置 `ConventionalCommitCheck` Checkin hook(pipeline 内阻断不合规提交)。
- `CommitPipeline` 责任链接入提交流程(对齐 IDEA `CheckinHandler`:校验 → stage → hook 链 → commit → 可选 push)。
- AI 接缝 5 接口 + Null 实现注入 CommitService(`ILlmProvider` / `ICommitMessageProvider` / `IPreCommitInspector` / `IChangelistGrouper` / `IConflictResolver`),M5 替换为真实实现。
- 最近提交消息复用(`workspaceState` 持久化,webview 一键填入)。
- 真实 git 提交闭环集成测试(fixture 仓库 + `vscode.git` add/commit + git log 校验)。

### Added — M1 Git Adapter + 多 changelist Changes(0.2.0)

- Git Adapter:`GitRepositoryService` 封装内置 vscode.git 稳定 `Repository` API(读取 workingTreeChanges/untrackedChanges、状态变更事件、diff/toGitUri 委托)。
Expand Down
12 changes: 10 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "hyper-git",
"displayName": "Hyper Git",
"description": "在 VS Code 上完整复刻 IntelliJ IDEA 的 Git 工具窗口与 Commit 提交窗口,并为未来 AI Agent 自主代理预留架构接缝。",
"version": "0.2.0",
"version": "0.3.0",
"publisher": "threefish-ai",
"license": "MIT",
"preview": true,
Expand Down Expand Up @@ -57,6 +57,12 @@
"id": "hyperGit.changes",
"name": "Changes",
"visibility": "visible"
},
{
"id": "hyperGit.commit",
"name": "Commit",
"type": "webview",
"visibility": "visible"
}
]
},
Expand All @@ -74,7 +80,9 @@
{ "command": "hyperGit.renameChangelist", "title": "重命名 Changelist", "category": "Hyper Git" },
{ "command": "hyperGit.deleteChangelist", "title": "删除 Changelist", "category": "Hyper Git" },
{ "command": "hyperGit.moveChangelist", "title": "移至 Changelist…", "category": "Hyper Git" },
{ "command": "hyperGit.openDiff", "title": "打开 Diff", "category": "Hyper Git" }
{ "command": "hyperGit.openDiff", "title": "打开 Diff", "category": "Hyper Git" },
{ "command": "hyperGit.commit", "title": "提交", "category": "Hyper Git", "icon": "$(check)" },
{ "command": "hyperGit.commitAndPush", "title": "提交并推送", "category": "Hyper Git", "icon": "$(cloud-upload)" }
],
"menus": {
"view/title": [
Expand Down
168 changes: 168 additions & 0 deletions src/adapter/commit/commit-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import * as vscode from 'vscode';
import { CommitPipeline } from '../../engine/commit/pipeline';
import { CheckinResult } from '../../engine/commit/pipeline';
import type { CheckinHook } from '../../engine/commit/pipeline';
import { validateConventional } from '../../engine/commit/conventional-linter';
import type { ConventionalValidation } from '../../engine/commit/conventional-linter';
import type { Repository } from '../../types/git';
import type { ChangeItem, GitRepositoryService } from '../git-repository-service';
import type {
IChangelistGrouper,
ICommitMessageProvider,
IConflictResolver,
ILlmProvider,
IPreCommitInspector,
} from '../../agent';
import { ConventionalCommitCheck } from './conventional-check';

export interface CommitRequest {
readonly message: string;
readonly selectedPaths: readonly string[];
readonly amend: boolean;
readonly signoff: boolean;
readonly skipHooks: boolean;
readonly push: boolean;
}

export interface CommitOutcome {
readonly ok: boolean;
readonly error?: string;
}

/** Commit 流水线依赖的 AI 接缝集合(Null 实现注入,M5 替换为真实实现)。 */
export interface AiSeams {
readonly llm: ILlmProvider;
readonly commitMessage: ICommitMessageProvider;
readonly preCommit: IPreCommitInspector;
readonly grouper: IChangelistGrouper;
readonly conflict: IConflictResolver;
}

const RECENT_KEY = 'hyperGit.recentCommitMessages';
const RECENT_MAX = 10;

/**
* CommitService:编排提交流水线(对齐 IDEA checkin 流程)。
*
* 流程:校验信息 → 解析选中文件 → stage → CommitPipeline 责任链(Checkin hook)→ commit → 可选 push。
* 注入 5 个 AI 接缝(Null 实现),为 M5 即插即用预留。
*/
export class CommitService implements vscode.Disposable {
private readonly pipeline: CommitPipeline;
private readonly _onDidChange = new vscode.EventEmitter<void>();
readonly onDidChange: vscode.Event<void> = this._onDidChange.event;

constructor(
context: vscode.ExtensionContext,
private readonly service: GitRepositoryService,
private readonly workspaceState: vscode.Memento,
readonly ai: AiSeams,
) {
const hooks: readonly CheckinHook[] = [
new ConventionalCommitCheck(() => this.conventionalEnabled()),
ai.preCommit,
];
this.pipeline = new CommitPipeline(hooks);
context.subscriptions.push(this._onDidChange);
}

get repo(): Repository | null {
return this.service.repo;
}

validateMessage(message: string): ConventionalValidation {
return validateConventional(message);
}

conventionalEnabled(): boolean {
return vscode.workspace.getConfiguration('hyperGit.commit').get('conventional', true);
}

getTemplate(): string {
return vscode.workspace.getConfiguration('hyperGit.commit').get('template', '') ?? '';
}

getRecentMessages(): readonly string[] {
const raw = this.workspaceState.get<string>(RECENT_KEY);
try {
return raw ? (JSON.parse(raw) as string[]) : [];
} catch {
return [];
}
}

private pushRecent(message: string): void {
const trimmed = message.trim();
const list = [trimmed, ...this.getRecentMessages().filter((m) => m !== trimmed)].slice(0, RECENT_MAX);
void this.workspaceState.update(RECENT_KEY, JSON.stringify(list));
}

/** 提交:stage 选中文件 → Checkin hook 链 → commit → 可选 push。 */
async executeCommit(req: CommitRequest): Promise<CommitOutcome> {
const repo = this.service.repo;
if (!repo) {
return { ok: false, error: '未找到 Git 仓库' };
}
const message = req.message.trim();
if (!message) {
return { ok: false, error: '提交信息不能为空' };
}

// CC 即时校验(pipeline 内的 hook 亦会拦截,此处给出明确原因)
if (this.conventionalEnabled()) {
const v = validateConventional(message);
if (v.severity === 'error') {
return { ok: false, error: v.reason ?? '提交信息不符合 Conventional Commits 规范' };
}
}

// 解析选中文件的绝对路径
const changes = this.service.getChanges();
const absPaths = this.resolveAbsolute(req.selectedPaths, changes);
if (absPaths.length === 0) {
return { ok: false, error: '未选择任何待提交文件' };
}

// Checkin hook 责任链
const hookResult = await this.pipeline.run({ message, filePaths: req.selectedPaths });
if (hookResult === CheckinResult.Cancel) {
return { ok: false, error: '提交被检查拦截(Checkin hook)' };
}

try {
await repo.add(absPaths);
await repo.commit(message, { amend: req.amend, signoff: req.signoff, noVerify: req.skipHooks });
if (req.push) {
await repo.push();
}
this.pushRecent(message);
this._onDidChange.fire();
return { ok: true };
} catch (e) {
return { ok: false, error: this.normalizeError(e) };
}
}

private resolveAbsolute(selectedPaths: readonly string[], changes: readonly ChangeItem[]): string[] {
const map = new Map(changes.map((c) => [c.relativePath, c.uri.fsPath]));
const out: string[] = [];
for (const p of selectedPaths) {
const abs = map.get(p);
if (abs) {
out.push(abs);
}
}
return out;
}

private normalizeError(e: unknown): string {
if (e instanceof Error) {
return e.message;
}
return String(e);
}

dispose(): void {
this._onDidChange.dispose();
}
}
24 changes: 24 additions & 0 deletions src/adapter/commit/conventional-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { CheckinResult } from '../../engine/commit/pipeline';
import type { CheckinHook, CommitInfo } from '../../engine/commit/pipeline';
import { validateConventional } from '../../engine/commit/conventional-linter';

/**
* 内置非 AI 提交检查:Conventional Commits 校验(对齐 IDEA CheckinHandler 闸门语义)。
*
* 当配置 `hyperGit.commit.conventional` 开启时,校验失败(severity=error)阻断提交,
* 用以证明 CommitPipeline 责任链可注入并阻断(M5 的 AI 审查 hook 将以同样方式接入)。
*/
export class ConventionalCommitCheck implements CheckinHook {
readonly name = 'conventional-commit-check';
readonly executionOrder = 10;

constructor(private readonly isEnabled: () => boolean) {}

async beforeCheckin(info: CommitInfo): Promise<CheckinResult> {
if (!this.isEnabled()) {
return CheckinResult.Commit;
}
const result = validateConventional(info.message);
return result.severity === 'error' ? CheckinResult.Cancel : CheckinResult.Commit;
}
}
Loading
Loading