From f78f55cc5970c648aa2f8d199ee75c4dee5bb63c Mon Sep 17 00:00:00 2001 From: ThreeFish Date: Sat, 27 Jun 2026 20:29:11 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix(Review):=20=E4=BF=AE=E5=A4=8D=20M0/M1/M?= =?UTF-8?q?2=20=E5=AE=A1=E6=9F=A5=E5=8F=91=E7=8E=B0=E7=9A=84=E6=AD=A3?= =?UTF-8?q?=E7=A1=AE=E6=80=A7=E9=97=AE=E9=A2=98(=E8=AE=A2=E9=98=85?= =?UTF-8?q?=E6=B3=84=E6=BC=8F/=E4=BB=93=E5=BA=93=E9=80=89=E5=8F=96/indexCh?= =?UTF-8?q?anges/commit=20=E8=AF=AD=E4=B9=89/push=20=E8=AD=A6=E5=91=8A/lin?= =?UTF-8?q?ter=20=E7=AD=89);?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://github.com/claude), [CodeX](https://openai.com), [Gemini](https://github.com/apps/gemini-code-assist) Co-Authored-By: Aurelius Huang --- src/adapter/commands.ts | 7 ++-- src/adapter/commit/commit-service.ts | 22 +++++++--- src/adapter/git-repository-service.ts | 51 +++++++++++++++--------- src/adapter/tree/changes-tree.ts | 3 +- src/adapter/webview/commit-webview.ts | 22 +++++----- src/engine/commit/conventional-linter.ts | 5 ++- src/engine/model/index.ts | 16 ++++++++ src/extension.ts | 8 ++-- src/shared/protocol.ts | 2 - 9 files changed, 90 insertions(+), 46 deletions(-) diff --git a/src/adapter/commands.ts b/src/adapter/commands.ts index 3d9238d..5ce3bc0 100644 --- a/src/adapter/commands.ts +++ b/src/adapter/commands.ts @@ -13,8 +13,8 @@ export function registerChangesCommands( const subs: vscode.Disposable[] = []; subs.push( - vscode.commands.registerCommand('hyperGit.refresh', () => { - void service.repo?.status(); + vscode.commands.registerCommand('hyperGit.refresh', async () => { + await service.repo?.status(); tree.refresh(); }), ); @@ -87,7 +87,8 @@ export function registerChangesCommands( if (!repo) { return; } - const left = service.toGitUri(change.uri, 'HEAD'); + // 空仓库(无 HEAD)时用 originalUri 兜底,避免 git scheme 解析失败 + const left = repo.state.HEAD ? service.toGitUri(change.uri, 'HEAD') : change.originalUri; const right = change.uri; const title = `${path.basename(change.relativePath)} (HEAD ↔ Working)`; await vscode.commands.executeCommand('vscode.diff', left, right, title); diff --git a/src/adapter/commit/commit-service.ts b/src/adapter/commit/commit-service.ts index 9b12b64..eaa2ce1 100644 --- a/src/adapter/commit/commit-service.ts +++ b/src/adapter/commit/commit-service.ts @@ -27,6 +27,8 @@ export interface CommitRequest { export interface CommitOutcome { readonly ok: boolean; readonly error?: string; + /** 提交成功但后续操作(如 push)失败时的提示。 */ + readonly warning?: string; } /** Commit 流水线依赖的 AI 接缝集合(Null 实现注入,M5 替换为真实实现)。 */ @@ -118,25 +120,35 @@ export class CommitService implements vscode.Disposable { // 解析选中文件的绝对路径 const changes = this.service.getChanges(); + const checkedSet = new Set(req.selectedPaths); 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 }); + // Checkin hook 责任链(传绝对路径,供未来 AI hook 读取文件内容) + const hookResult = await this.pipeline.run({ message, filePaths: absPaths }); if (hookResult === CheckinResult.Cancel) { return { ok: false, error: '提交被检查拦截(Checkin hook)' }; } try { + // 让勾选集成为提交的权威范围:未勾选的已暂存文件先 unstage(对齐 IDEA「提交该集合」语义) + const toUnstage = changes.filter((c) => c.staged && !checkedSet.has(c.relativePath)).map((c) => c.uri.fsPath); + if (toUnstage.length > 0) { + await repo.restore(toUnstage, { staged: true }); + } 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(); + if (req.push) { + try { + await repo.push(); + } catch (e) { + return { ok: true, warning: `提交已成功,但推送失败:${this.normalizeError(e)}` }; + } + } return { ok: true }; } catch (e) { return { ok: false, error: this.normalizeError(e) }; diff --git a/src/adapter/git-repository-service.ts b/src/adapter/git-repository-service.ts index 21fe824..b7b339d 100644 --- a/src/adapter/git-repository-service.ts +++ b/src/adapter/git-repository-service.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import * as vscode from 'vscode'; -import type { API, Repository } from '../types/git'; +import type { API, Change, Repository } from '../types/git'; import { FileStatus } from '../engine/model'; import { mapGitStatus } from './git-status-map'; @@ -24,6 +24,7 @@ export class GitRepositoryService implements vscode.Disposable { private readonly _onDidChange = new vscode.EventEmitter(); readonly onDidChange: vscode.Event = this._onDidChange.event; private readonly disposables: vscode.Disposable[] = []; + private repoSub?: vscode.Disposable; constructor(private readonly api: API) { this.disposables.push(api.onDidOpenRepository(() => this.pickRepository())); @@ -39,46 +40,59 @@ export class GitRepositoryService implements vscode.Disposable { return this._repo?.rootUri.fsPath ?? null; } - /** 选取活跃仓库:优先匹配工作区根,否则首个。 */ + /** 选取活跃仓库:优先匹配工作区根(用 API.getRepository,路径段精确匹配),否则首个。 */ private pickRepository(): void { - const folders = vscode.workspace.workspaceFolders; let repo: Repository | null = null; - if (folders && folders.length > 0) { - const wsRoot = folders[0].uri.fsPath; - repo = this.api.repositories.find((r) => wsRoot.startsWith(r.rootUri.fsPath)) ?? null; + const folder = vscode.workspace.workspaceFolders?.[0]; + if (folder) { + repo = this.api.getRepository(folder.uri) ?? null; } if (!repo) { repo = this.api.repositories[0] ?? null; } - const changed = repo !== this._repo; - if (changed) { + if (repo !== this._repo) { + this.repoSub?.dispose(); + this.repoSub = undefined; this._repo = repo; if (repo) { - this.disposables.push(repo.state.onDidChange(() => this._onDidChange.fire())); + this.repoSub = repo.state.onDidChange(() => this._onDidChange.fire()); } + this._onDidChange.fire(); } - this._onDidChange.fire(); } - /** 读取本地变更(工作区 + 未跟踪),映射为 ChangeItem。 */ + /** 读取本地变更(已暂存 + 工作区 + 未跟踪,按相对路径去重,index 优先),映射为 ChangeItem。 */ getChanges(): ChangeItem[] { const repo = this._repo; if (!repo) { return []; } const root = repo.rootUri.fsPath; - const items: ChangeItem[] = []; - const fromChange = (uri: vscode.Uri, originalUri: vscode.Uri, renameUri: vscode.Uri | undefined, status: number, staged: boolean): ChangeItem => { - const rel = path.relative(root, uri.fsPath).split(path.sep).join('/'); - return { relativePath: rel, uri, originalUri, renameUri, status: mapGitStatus(status), staged }; + const map = new Map(); + const add = (c: Change, staged: boolean): void => { + const rel = path.relative(root, c.uri.fsPath).split(path.sep).join('/'); + if (map.has(rel)) { + return; + } + map.set(rel, { + relativePath: rel, + uri: c.uri, + originalUri: c.originalUri, + renameUri: c.renameUri ?? undefined, + status: mapGitStatus(c.status), + staged, + }); }; + for (const c of repo.state.indexChanges) { + add(c, true); + } for (const c of repo.state.workingTreeChanges) { - items.push(fromChange(c.uri, c.originalUri, c.renameUri ?? undefined, c.status, false)); + add(c, false); } for (const c of repo.state.untrackedChanges) { - items.push(fromChange(c.uri, c.originalUri, c.renameUri ?? undefined, c.status, false)); + add(c, false); } - return items; + return [...map.values()]; } /** 构造任意 ref 版本的资源 Uri(diff 原始端,复用 vscode.git 的 git scheme)。 */ @@ -87,6 +101,7 @@ export class GitRepositoryService implements vscode.Disposable { } dispose(): void { + this.repoSub?.dispose(); this.disposables.forEach((d) => d.dispose()); this._onDidChange.dispose(); } diff --git a/src/adapter/tree/changes-tree.ts b/src/adapter/tree/changes-tree.ts index 4f1f5a9..ed2ac41 100644 --- a/src/adapter/tree/changes-tree.ts +++ b/src/adapter/tree/changes-tree.ts @@ -1,5 +1,6 @@ import * as path from 'path'; import * as vscode from 'vscode'; +import { fileStatusLabel } from '../../engine/model'; import { getDecoration } from '../../engine/scm-mapping/status-decoration'; import type { ChangelistRegistry } from '../changelist-registry'; import type { ChangeItem, GitRepositoryService } from '../git-repository-service'; @@ -87,7 +88,7 @@ export class ChangesTreeProvider implements vscode.TreeDataProvider treeItem.id = `file:${change.relativePath}`; treeItem.resourceUri = change.uri; treeItem.description = `${decoration.letter}${dir && dir !== '.' ? ' · ' + dir : ''}`; - treeItem.tooltip = `${change.relativePath}\n状态:${change.status}${change.staged ? '(已暂存)' : ''}`; + treeItem.tooltip = `${change.relativePath}\n状态:${fileStatusLabel(change.status)}${change.staged ? '(已暂存)' : ''}`; treeItem.iconPath = new vscode.ThemeIcon('circle-filled', new vscode.ThemeColor(decoration.themeColor)); treeItem.command = { command: 'hyperGit.openDiff', title: '打开 Diff', arguments: [change] }; return treeItem; diff --git a/src/adapter/webview/commit-webview.ts b/src/adapter/webview/commit-webview.ts index 3077cf3..45f3f9b 100644 --- a/src/adapter/webview/commit-webview.ts +++ b/src/adapter/webview/commit-webview.ts @@ -1,3 +1,4 @@ +import * as crypto from 'crypto'; import * as path from 'path'; import * as vscode from 'vscode'; import { getDecoration } from '../../engine/scm-mapping/status-decoration'; @@ -28,7 +29,8 @@ export class CommitWebviewProvider implements vscode.WebviewViewProvider { this.view = view; view.webview.options = { enableScripts: true, localResourceRoots: [] }; view.webview.html = this.renderHtml(); - view.webview.onDidReceiveMessage((msg) => this.onMessage(msg as WebviewToHostMessage)); + const msgSub = view.webview.onDidReceiveMessage((msg) => this.onMessage(msg as WebviewToHostMessage)); + view.onDidDispose(() => msgSub.dispose()); this.pushState(); } @@ -81,8 +83,6 @@ export class CommitWebviewProvider implements vscode.WebviewViewProvider { path: c.relativePath, label: path.basename(c.relativePath), dir: path.dirname(c.relativePath), - status: decoration.letter, - statusName: c.status, themeColor: decoration.themeColor, }; } @@ -161,7 +161,9 @@ button:disabled { opacity: 0.5; cursor: default; }