From 2039d8bb826798c89cba78a6fceff7442f98444b Mon Sep 17 00:00:00 2001 From: ThreeFish Date: Sat, 27 Jun 2026 20:47:23 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(M4):=20Stash=20=E8=A7=86=E5=9B=BE?= =?UTF-8?q?=E4=B8=8E=E6=93=8D=E4=BD=9C(create/apply/pop/drop)+=20stash-bac?= =?UTF-8?q?ked=20Shelf=20MVP;?= 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/stash-commands.ts | 80 ++++++++++++++++++++++++++++++++++ src/adapter/tree/stash-tree.ts | 56 ++++++++++++++++++++++++ src/extension.ts | 6 +++ 3 files changed, 142 insertions(+) create mode 100644 src/adapter/stash-commands.ts create mode 100644 src/adapter/tree/stash-tree.ts diff --git a/src/adapter/stash-commands.ts b/src/adapter/stash-commands.ts new file mode 100644 index 0000000..8f2aa6b --- /dev/null +++ b/src/adapter/stash-commands.ts @@ -0,0 +1,80 @@ +import * as vscode from 'vscode'; +import type { GitRepositoryService } from './git-repository-service'; +import type { StashNode, StashTreeProvider } from './tree/stash-tree'; + +/** + * 注册 Stash 相关命令(M4)。 + * + * 经 vscode.git 稳定 API:createStash / applyStash / popStash / dropStash。 + * 「Shelve Changes」以 stash 近似(MVP,见工程方案 §4 P2);忠实 patch Shelf 受 API 限制延后。 + */ +export function registerStashCommands(service: GitRepositoryService, stashTree: StashTreeProvider): vscode.Disposable[] { + const subs: vscode.Disposable[] = []; + const errMsg = (e: unknown): string => (e instanceof Error ? e.message : String(e)); + + subs.push( + vscode.commands.registerCommand('hyperGit.stashCreate', async () => { + const repo = service.repo; + if (!repo) { + return; + } + const message = await vscode.window.showInputBox({ prompt: 'Stash 信息(可空)', placeHolder: 'WIP' }); + try { + await repo.createStash({ message: message && message.trim() ? message.trim() : undefined, includeUntracked: true }); + stashTree.refresh(); + } catch (e) { + void vscode.window.showErrorMessage(`Stash 创建失败:${errMsg(e)}`); + } + }), + ); + + subs.push( + vscode.commands.registerCommand('hyperGit.stashApply', async (node?: StashNode) => { + const repo = service.repo; + if (!repo) { + return; + } + try { + await repo.applyStash(node?.kind === 'stash' ? node.index : 0); + stashTree.refresh(); + } catch (e) { + void vscode.window.showErrorMessage(`Stash 应用失败:${errMsg(e)}`); + } + }), + ); + + subs.push( + vscode.commands.registerCommand('hyperGit.stashPop', async (node?: StashNode) => { + const repo = service.repo; + if (!repo) { + return; + } + try { + await repo.popStash(node?.kind === 'stash' ? node.index : 0); + stashTree.refresh(); + } catch (e) { + void vscode.window.showErrorMessage(`Stash pop 失败:${errMsg(e)}`); + } + }), + ); + + subs.push( + vscode.commands.registerCommand('hyperGit.stashDrop', async (node?: StashNode) => { + const repo = service.repo; + if (!repo || node?.kind !== 'stash') { + return; + } + const choice = await vscode.window.showWarningMessage(`删除 stash@{${node.index}}?`, { modal: true }, '删除'); + if (choice === '删除') { + try { + await repo.dropStash(node.index); + stashTree.refresh(); + } catch (e) { + void vscode.window.showErrorMessage(`Stash 删除失败:${errMsg(e)}`); + } + } + }), + ); + + return subs; +} diff --git a/src/adapter/tree/stash-tree.ts b/src/adapter/tree/stash-tree.ts new file mode 100644 index 0000000..4f2d47e --- /dev/null +++ b/src/adapter/tree/stash-tree.ts @@ -0,0 +1,56 @@ +import * as vscode from 'vscode'; +import type { Commit } from '../../types/git'; +import type { GitRepositoryService } from '../git-repository-service'; + +export interface StashEntryNode { + readonly kind: 'stash'; + readonly index: number; + readonly commit: Commit; +} + +export type StashNode = StashEntryNode; + +/** + * Stash 视图 TreeDataProvider。 + * + * vscode.git 稳定 API 不暴露 `git stash list`,故用 `Repository.log({ refNames: ['stash'] })` + * 枚举 stash 提交(stash@{0} 对应最新 = 列表首项,index 即 apply/pop/drop 的索引)。 + * 行级 partial commit / 忠实 patch Shelf 受 API 限制,文档化延后(见 CHANGELOG)。 + */ +export class StashTreeProvider implements vscode.TreeDataProvider { + private readonly _onDidChange = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this._onDidChange.event; + + constructor(private readonly service: GitRepositoryService) {} + + refresh(): void { + this._onDidChange.fire(undefined); + } + + async getChildren(element?: StashNode): Promise { + if (element) { + return []; + } + const repo = this.service.repo; + if (!repo) { + return []; + } + try { + const commits = await repo.log({ refNames: ['stash'], maxEntries: 50 }); + return commits.map((c, i): StashEntryNode => ({ kind: 'stash', index: i, commit: c })); + } catch { + return []; + } + } + + getTreeItem(node: StashNode): vscode.TreeItem { + const subject = (node.commit.message.split('\n', 1)[0] ?? node.commit.message).slice(0, 60); + const item = new vscode.TreeItem(subject, vscode.TreeItemCollapsibleState.None); + item.id = `stash:${node.index}:${node.commit.hash}`; + item.description = `stash@{${node.index}} · ${node.commit.authorName ?? ''}`; + item.tooltip = `stash@{${node.index}}\n${node.commit.message}`; + item.contextValue = 'hyperGit.stash'; + item.iconPath = new vscode.ThemeIcon('archive'); + return item; + } +} diff --git a/src/extension.ts b/src/extension.ts index 0b7ca5b..abaee9b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -11,6 +11,8 @@ import { BranchesTreeProvider } from './adapter/tree/branches-tree'; import { ChangesTreeProvider, EmptyChangesProvider } from './adapter/tree/changes-tree'; import { LogTreeProvider } from './adapter/tree/log-tree'; import { registerHistoryCommands } from './adapter/history-commands'; +import { registerStashCommands } from './adapter/stash-commands'; +import { StashTreeProvider } from './adapter/tree/stash-tree'; import { CommitWebviewProvider } from './adapter/webview/commit-webview'; import { getGitApi } from './adapter/git-api'; import { GitRepositoryService } from './adapter/git-repository-service'; @@ -55,6 +57,7 @@ export async function activate(context: vscode.ExtensionContext): Promise const commitView = new CommitWebviewProvider(service, registry, commit); const logTree = new LogTreeProvider(service); const branchesTree = new BranchesTreeProvider(service); + const stashTree = new StashTreeProvider(service); const focusCommitView = (): void => { void vscode.commands.executeCommand('hyperGit.commit.focus'); }; @@ -67,8 +70,10 @@ export async function activate(context: vscode.ExtensionContext): Promise vscode.window.registerWebviewViewProvider(CommitWebviewProvider.viewType, commitView), vscode.window.registerTreeDataProvider('hyperGit.log', logTree), vscode.window.registerTreeDataProvider('hyperGit.branches', branchesTree), + vscode.window.registerTreeDataProvider('hyperGit.stash', stashTree), ...registerChangesCommands(service, registry, tree), ...registerHistoryCommands(service, logTree, branchesTree), + ...registerStashCommands(service, stashTree), vscode.commands.registerCommand('hyperGit.commit', focusCommitView), vscode.commands.registerCommand('hyperGit.commitAndPush', focusCommitView), ); @@ -78,6 +83,7 @@ export async function activate(context: vscode.ExtensionContext): Promise commitView.refresh(); logTree.refresh(); branchesTree.refresh(); + stashTree.refresh(); }; context.subscriptions.push( service.onDidChange(refreshAll), From 1df083090f3911e1e77e4f85d9fc52da818eb45d Mon Sep 17 00:00:00 2001 From: ThreeFish Date: Sat, 27 Jun 2026 20:47:23 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat(Manifest):=20M4=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=20stash=20=E8=A7=86=E5=9B=BE=E4=B8=8E=204=20=E4=B8=AA=20stash?= =?UTF-8?q?=20=E5=91=BD=E4=BB=A4=E5=8F=8A=E8=8F=9C=E5=8D=95=E5=B9=B6?= =?UTF-8?q?=E5=8D=87=E7=BA=A7=E8=87=B3=200.5.0;?= 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 --- package.json | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 76ac52b..ae67dc0 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "hyper-git", "displayName": "Hyper Git", "description": "在 VS Code 上完整复刻 IntelliJ IDEA 的 Git 工具窗口与 Commit 提交窗口,并为未来 AI Agent 自主代理预留架构接缝。", - "version": "0.4.0", + "version": "0.5.0", "publisher": "threefish-ai", "license": "MIT", "preview": true, @@ -73,6 +73,11 @@ "id": "hyperGit.branches", "name": "Branches", "visibility": "visible" + }, + { + "id": "hyperGit.stash", + "name": "Stash", + "visibility": "visible" } ] }, @@ -80,6 +85,10 @@ { "view": "hyperGit.changes", "contents": "未检测到 Git 变更或仓库。\n在 Git 仓库中打开工作区,变更将按 Changelist 分组显示。\n[新建 Changelist](command:hyperGit.newChangelist)" + }, + { + "view": "hyperGit.stash", + "contents": "暂无 Stash。\n[Stash 当前变更](command:hyperGit.stashCreate)" } ], "commands": [ @@ -105,7 +114,11 @@ { "command": "hyperGit.branchDelete", "title": "删除分支", "category": "Hyper Git" }, { "command": "hyperGit.mergeBranch", "title": "合并到当前分支", "category": "Hyper Git" }, { "command": "hyperGit.rebaseBranch", "title": "变基当前分支到…", "category": "Hyper Git" }, - { "command": "hyperGit.showBlame", "title": "显示 Blame", "category": "Hyper Git" } + { "command": "hyperGit.showBlame", "title": "显示 Blame", "category": "Hyper Git" }, + { "command": "hyperGit.stashCreate", "title": "Stash 变更", "category": "Hyper Git", "icon": "$(archive)" }, + { "command": "hyperGit.stashApply", "title": "应用 Stash", "category": "Hyper Git" }, + { "command": "hyperGit.stashPop", "title": "Pop Stash", "category": "Hyper Git" }, + { "command": "hyperGit.stashDrop", "title": "删除 Stash", "category": "Hyper Git" } ], "menus": { "view/title": [ @@ -116,7 +129,8 @@ { "command": "hyperGit.logFilterPath", "when": "view == hyperGit.log" }, { "command": "hyperGit.logClearFilter", "when": "view == hyperGit.log" }, { "command": "hyperGit.refreshBranches", "when": "view == hyperGit.branches", "group": "navigation" }, - { "command": "hyperGit.branchCreate", "when": "view == hyperGit.branches", "group": "navigation" } + { "command": "hyperGit.branchCreate", "when": "view == hyperGit.branches", "group": "navigation" }, + { "command": "hyperGit.stashCreate", "when": "view == hyperGit.stash", "group": "navigation" } ], "view/item/context": [ { "command": "hyperGit.setActiveChangelist", "when": "view == hyperGit.changes && viewItem == hyperGit.changelist", "group": "1_changelist@1" }, @@ -129,7 +143,10 @@ { "command": "hyperGit.branchCheckout", "when": "view == hyperGit.branches && viewItem =~ /hyperGit.branch|hyperGit.remoteBranch/", "group": "1_branch@1" }, { "command": "hyperGit.branchDelete", "when": "view == hyperGit.branches && viewItem == hyperGit.branch", "group": "1_branch@2" }, { "command": "hyperGit.mergeBranch", "when": "view == hyperGit.branches && viewItem =~ /hyperGit.branch|hyperGit.remoteBranch/", "group": "1_branch@3" }, - { "command": "hyperGit.rebaseBranch", "when": "view == hyperGit.branches && viewItem =~ /hyperGit.branch|hyperGit.remoteBranch/", "group": "1_branch@4" } + { "command": "hyperGit.rebaseBranch", "when": "view == hyperGit.branches && viewItem =~ /hyperGit.branch|hyperGit.remoteBranch/", "group": "1_branch@4" }, + { "command": "hyperGit.stashApply", "when": "view == hyperGit.stash && viewItem == hyperGit.stash", "group": "1_stash@1" }, + { "command": "hyperGit.stashPop", "when": "view == hyperGit.stash && viewItem == hyperGit.stash", "group": "1_stash@2" }, + { "command": "hyperGit.stashDrop", "when": "view == hyperGit.stash && viewItem == hyperGit.stash", "group": "1_stash@3" } ], "commandPalette": [ { "command": "hyperGit.openDiff", "when": "false" }, @@ -142,7 +159,10 @@ { "command": "hyperGit.branchCheckout", "when": "false" }, { "command": "hyperGit.branchDelete", "when": "false" }, { "command": "hyperGit.mergeBranch", "when": "false" }, - { "command": "hyperGit.rebaseBranch", "when": "false" } + { "command": "hyperGit.rebaseBranch", "when": "false" }, + { "command": "hyperGit.stashApply", "when": "false" }, + { "command": "hyperGit.stashPop", "when": "false" }, + { "command": "hyperGit.stashDrop", "when": "false" } ] }, "configuration": { From cde9bcef7268ca73caed0bcf8541e21af1f83aab Mon Sep 17 00:00:00 2001 From: ThreeFish Date: Sat, 27 Jun 2026 20:47:23 +0800 Subject: [PATCH 3/3] =?UTF-8?q?test/docs(M4):=20=E9=9B=86=E6=88=90?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E8=A6=86=E7=9B=96=20M4=20=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E5=B9=B6=E8=AE=B0=E5=BD=95=20CHANGELOG=20+=20API=20=E9=99=90?= =?UTF-8?q?=E5=88=B6=E8=AF=B4=E6=98=8E;?= 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 --- CHANGELOG.md | 7 +++++++ tests/suite/extension.test.js | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31ef204..0ba6a45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ ## [Unreleased] +### Added — M4 Stash/Shelf(0.5.0) + +- **Stash 视图**:`StashTreeProvider` 经 `Repository.log({ refNames: ['stash'] })` 枚举 stash(API 不暴露 `git stash list`,以此近似)。 +- **Stash 操作**:`createStash` / `applyStash` / `popStash` / `dropStash`(经 vscode.git 稳定 API),配视图标题按钮与右键菜单 + viewsWelcome。 +- **Shelf(MVP)**:以 stash 近似 IDEA shelve(工程方案 §4 P2 约定);忠实 patch Shelf 受 API 限制延后。 +- **API 限制(文档化延后)**:行级 partial commit(vscode.git `add` 仅整文件,无 hunk 暂存)、忠实 patch Shelf、Staging Area 模式开关、cherry-pick/revert/reset/分支重命名——均无稳定 API 对应,未来可经 git CLI 兜底或 proposed API 评估。 + ### Added — M3 Log/Branches/Diff·Blame(0.4.0) - **Log TreeView**:消费 `Repository.log()`,按 author/path 过滤(清除过滤)、复制 commit hash、显示文件历史。完整提交图(SVG 拓扑连线)作为后续增强(M3.x)。 diff --git a/tests/suite/extension.test.js b/tests/suite/extension.test.js index 270ad5e..e56ce06 100644 --- a/tests/suite/extension.test.js +++ b/tests/suite/extension.test.js @@ -6,7 +6,7 @@ const EXT_ID = 'threefish-ai.hyper-git'; suite('扩展冒烟测试', function () { this.timeout(30000); - test('扩展可激活并注册全部 M1+M2+M3 命令', async () => { + test('扩展可激活并注册全部 M1+M2+M3+M4 命令', async () => { const ext = vscode.extensions.getExtension(EXT_ID); assert.ok(ext, `扩展 ${EXT_ID} 未找到`); if (!ext.isActive) { @@ -37,6 +37,10 @@ suite('扩展冒烟测试', function () { 'hyperGit.mergeBranch', 'hyperGit.rebaseBranch', 'hyperGit.showBlame', + 'hyperGit.stashCreate', + 'hyperGit.stashApply', + 'hyperGit.stashPop', + 'hyperGit.stashDrop', ]) { assert.ok(commands.includes(cmd), `命令 ${cmd} 未注册`); }