diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 04f4c02..be27a44 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,6 +5,11 @@ updates: schedule: interval: weekly open-pull-requests-limit: 5 + ignore: + # @types/vscode 须与 package.json 的 engines.vscode 手动联动(vsce package 强制要求 + # @types/vscode 的 major.minor ≤ engines.vscode)。Dependabot 单方面 bump 会破坏该约束 + # 并令 CI 的 vsce package 失败,故忽略其升级——抬高最低 VS Code 版本属产品决策,需人工执行。 + - dependency-name: '@types/vscode' - package-ecosystem: github-actions directory: / schedule: diff --git a/package.json b/package.json index a47f1e3..c97e874 100644 --- a/package.json +++ b/package.json @@ -1237,7 +1237,7 @@ "@eslint/js": "^10.0.1", "@stylistic/eslint-plugin": "^2.9.0", "@types/node": "^26.0.1", - "@types/vscode": "~1.125.0", + "@types/vscode": "~1.85.0", "@vscode/test-electron": "^2.4.1", "@vscode/vsce": "^3.2.1", "esbuild": "^0.28.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ba3f02..d7da77e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^26.0.1 version: 26.0.1 '@types/vscode': - specifier: ~1.125.0 - version: 1.125.0 + specifier: ~1.85.0 + version: 1.85.0 '@vscode/test-electron': specifier: ^2.4.1 version: 2.5.2(supports-color@8.1.1) @@ -713,8 +713,8 @@ packages: '@types/sarif@2.1.7': resolution: {integrity: sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==} - '@types/vscode@1.125.0': - resolution: {integrity: sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA==} + '@types/vscode@1.85.0': + resolution: {integrity: sha512-CF/RBon/GXwdfmnjZj0WTUMZN5H6YITOfBCP4iEZlOtVQXuzw6t7Le7+cR+7JzdMrnlm7Mfp49Oj2TuSXIWo3g==} '@typescript-eslint/eslint-plugin@8.62.0': resolution: {integrity: sha512-o+mpz7EYiMzXoySXiKmzlabIvTVqUuK5yLrAedRPRDA0IpPFMUV1IXt6OqljIxX/kumN6EjUYp41Hqelh6p/Dw==} @@ -2906,7 +2906,7 @@ snapshots: '@types/sarif@2.1.7': {} - '@types/vscode@1.125.0': {} + '@types/vscode@1.85.0': {} '@typescript-eslint/eslint-plugin@8.62.0(@typescript-eslint/parser@8.62.0(eslint@10.6.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3))(eslint@10.6.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3)': dependencies: diff --git a/src/adapter/ci/github-ci-service.ts b/src/adapter/ci/github-ci-service.ts index 7222bf3..989562d 100644 --- a/src/adapter/ci/github-ci-service.ts +++ b/src/adapter/ci/github-ci-service.ts @@ -246,6 +246,11 @@ export class GitHubCiService implements vscode.Disposable { return authProviderId(remote); } + /** 对外暴露当前仓库的 GitHub 坐标(复用内部 repoRoot 缓存),供 Commit 详情面板生成提交页 URL。 */ + getGitHubRemote(): GitHubRemote | null { + return this.resolveRemote(); + } + /** 解析当前仓库远程为 GitHub 坐标(按 repoRoot 缓存;优先配置名 → origin → 首个可解析)。 */ private resolveRemote(): GitHubRemote | null { const repoRoot = this.service.repoRoot ?? ''; diff --git a/src/adapter/webview/commit-detail-panel.ts b/src/adapter/webview/commit-detail-panel.ts new file mode 100644 index 0000000..37db196 --- /dev/null +++ b/src/adapter/webview/commit-detail-panel.ts @@ -0,0 +1,192 @@ +import * as crypto from 'crypto'; +import * as vscode from 'vscode'; +import type { GitRepositoryService } from '../git-repository-service'; +import type { GitHubCiService } from '../ci/github-ci-service'; +import type { CommitDetailVM, CommitDetailWebviewToHostMessage } from '../../shared/protocol'; +import { parseShortStat } from '../../engine/log/commit-files'; +import { commitWebUrl } from '../../engine/ci/remote-parser'; +import { formatRelative, formatAbsolute } from '../../engine/log/format-time'; +import { getBaseStyles } from './shared-styles'; + +/** git show 单条元信息的字段分隔符(NUL,与 log-line LOG_GRAPH_FORMAT 同范式)。 */ +const SHOW_FORMAT = '%H%x00%s%x00%b%x00%an%x00%ae%x00%aI%x00%cn%x00%cI%x00%P'; +const BODY_CAP = 4000; + +/** + * Commit 详情面板(编辑器区 WebviewPanel,对齐官方 Source Control Graph 的提交详情)。 + * + * 交互:GRAPH 侧栏悬停提交 → host 组装 {@link CommitDetailVM} → 本面板在编辑器区(Beside)显示, + * `preserveFocus` 不抢焦点;悬停切换只 `postMessage` 换数据 + `reveal`,不反复 create/dispose。 + * + * 分层:数据取用 host 侧 git(adapter),格式化/解析复用 engine 纯函数;面板 webview 仅负责渲染。 + * 单例:跨悬停复用同一 panel(`current`),用户手动关闭 → `onDidDispose` 置空 → 下次悬停重建。 + */ +export class CommitDetailPanel { + private static current: CommitDetailPanel | undefined; + private readonly panel: vscode.WebviewPanel; + private disposed = false; + private reqSeq = 0; + private lastHash: string | undefined; + + private constructor( + private readonly service: GitRepositoryService, + private readonly ciService: GitHubCiService, + ) { + this.panel = vscode.window.createWebviewPanel( + 'hyperGit.commitDetail', + 'Commit', + { viewColumn: vscode.ViewColumn.Beside, preserveFocus: true }, + { enableScripts: true, retainContextWhenHidden: true, localResourceRoots: [] }, + ); + this.panel.webview.html = CommitDetailPanel.renderShellHtml(); + this.panel.webview.onDidReceiveMessage((m: CommitDetailWebviewToHostMessage) => { + if (m?.type === 'commitDetail/openExternal') { + void this.ciService.openExternal(m.payload.url); + } + }); + this.panel.onDidDispose(() => { + this.disposed = true; + if (CommitDetailPanel.current === this) { + CommitDetailPanel.current = undefined; + } + }); + } + + /** 显示(或更新)指定提交的详情面板。单例复用,不抢焦点。 */ + static async show(service: GitRepositoryService, ciService: GitHubCiService, hash: string): Promise { + if (!CommitDetailPanel.current) { + CommitDetailPanel.current = new CommitDetailPanel(service, ciService); + } + await CommitDetailPanel.current.update(hash); + } + + private async update(hash: string): Promise { + if (this.lastHash === hash && this.panel.visible) { + return; // 已在显示同一提交(连续悬停/方向键去重)。 + } + const seq = ++this.reqSeq; + const vm = await this.buildDetailVM(hash); + if (this.disposed || seq !== this.reqSeq) { + return; // 面板已关闭,或已被更晚的悬停请求超越 → 丢弃过期响应。 + } + this.lastHash = vm ? hash : undefined; + this.panel.title = vm ? `${vm.shortHash} · Commit` : 'Commit'; + void this.panel.webview.postMessage({ type: 'commitDetail/data', payload: vm }); + this.panel.reveal(vscode.ViewColumn.Beside, /* preserveFocus */ true); + } + + /** host 侧一次取齐:基础字段(git show)+ 预格式化时间 + 变更统计 + 可选 GitHub 提交页 URL。 */ + private async buildDetailVM(hash: string): Promise { + if (!this.service.repo) { + return null; + } + try { + const raw = await this.service.execGit(['show', '-s', `--format=${SHOW_FORMAT}`, hash]); + const f = raw.split('\0'); + if (f.length < 9 || !f[0]) { + return null; + } + const [fullHash, subject, body, authorName, authorEmail, authorDate, committerName, committerDate, parentsRaw] = f; + const stat = parseShortStat( + await this.service.execGit(['diff-tree', '--no-commit-id', '--shortstat', '-r', '--root', hash]), + ); + const remote = this.ciService.getGitHubRemote(); + const cappedBody = body.length > BODY_CAP ? `${body.slice(0, BODY_CAP)}…` : body.replace(/\s+$/, ''); + return { + hash: fullHash, + shortHash: fullHash.slice(0, 7), + subject, + body: cappedBody, + authorName, + authorEmail, + authorDate, + authorDateRel: formatRelative(authorDate), + authorDateAbs: formatAbsolute(authorDate), + committerName, + committerDate, + parents: parentsRaw ? parentsRaw.trim().split(/\s+/).filter(Boolean) : [], + stat, + githubUrl: remote ? commitWebUrl(remote, fullHash) : undefined, + }; + } catch { + return null; // 坏 hash / git 失败 → 面板空态,不弹模态(对齐 sendCommitFiles 静默降级)。 + } + } + + private static renderShellHtml(): string { + const nonce = crypto.randomBytes(16).toString('base64'); + const csp = ['default-src \'none\'', 'style-src \'unsafe-inline\'', `script-src 'nonce-${nonce}'`].join('; '); + return ` + + + + + + + +
Hover a commit in the Graph to see its details.
+
+ + +`; + } +} diff --git a/src/adapter/webview/log-webview.ts b/src/adapter/webview/log-webview.ts index 42772ae..89cbf82 100644 --- a/src/adapter/webview/log-webview.ts +++ b/src/adapter/webview/log-webview.ts @@ -10,6 +10,7 @@ import { parseLogLines } from '../../engine/log/log-line'; import { buildLogArgs, type LogScope } from '../../engine/log/log-query'; import { buildFileTree } from '../../engine/tree/file-tree'; import type { GitHubCiService } from '../ci/github-ci-service'; +import { CommitDetailPanel } from './commit-detail-panel'; import type { CiMetaVM, CiStatusVM, @@ -186,6 +187,9 @@ export class LogWebviewProvider implements vscode.WebviewViewProvider, LogFilter case 'log/ciSignIn': void this.handleCiSignIn(); break; + case 'log/showCommitDetail': + void CommitDetailPanel.show(this.service, this.ciService, msg.payload.hash); + break; } } @@ -495,18 +499,6 @@ body { margin: 0; font-family: var(--vscode-font-family); font-size: var(--vscod #details .tree-dir:hover { background: var(--vscode-list-hoverBackground); } #details .tree-dir .tree-twist { flex: 0 0 12px; text-align: center; font-size: 10px; opacity: 0.8; } #details .tree-dir .tree-name { color: var(--vscode-descriptionForeground); overflow: hidden; text-overflow: ellipsis; } -/* ── 提交悬浮详情(自定义浮层,置于 #rows 之外,虚拟滚动重写不销毁;仿 CI Tooltip)── */ -#commit-tip { position: fixed; z-index: 50; display: none; max-width: 420px; min-width: 260px; max-height: 360px; overflow: hidden; background: var(--vscode-editorHoverWidget-background, var(--vscode-editorWidget-background)); color: var(--vscode-editorHoverWidget-foreground, var(--vscode-foreground)); border: 1px solid var(--vscode-editorHoverWidget-border, var(--vscode-editorWidget-border, rgba(128,128,128,.3))); border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,.35); font-size: 12px; } -#commit-tip.show { display: flex; flex-direction: column; } -#commit-tip .ct-scroll { overflow-y: auto; max-height: 360px; padding: 4px 0; } -#commit-tip .ct-sec { padding: 4px 10px; display: flex; gap: 8px; align-items: baseline; } -#commit-tip .ct-sec .ct-k { flex: 0 0 62px; color: var(--vscode-descriptionForeground); font-size: 10px; text-transform: uppercase; letter-spacing: .3px; } -#commit-tip .ct-sec .ct-v { flex: 1 1 auto; min-width: 0; word-break: break-word; } -#commit-tip .ct-msg .ct-subj { font-weight: 600; display: block; margin-bottom: 3px; } -#commit-tip .ct-msg .ct-body { white-space: pre-wrap; word-break: break-word; opacity: 0.92; } -#commit-tip .ct-refs { display: flex; flex-wrap: wrap; gap: 3px; } -#commit-tip .ct-div { border-top: 1px solid var(--vscode-editorHoverWidget-border, rgba(128,128,128,.2)); margin: 2px 0; } -#commit-tip .ct-sha { font-family: var(--vscode-editor-font-family, monospace); font-size: 11px; opacity: 0.85; word-break: break-all; } @@ -527,7 +519,6 @@ body { margin: 0; font-family: var(--vscode-font-family); font-size: var(--vscod
-