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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)。
Expand Down
30 changes: 25 additions & 5 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.4.0",
"version": "0.5.0",
"publisher": "threefish-ai",
"license": "MIT",
"preview": true,
Expand Down Expand Up @@ -73,13 +73,22 @@
"id": "hyperGit.branches",
"name": "Branches",
"visibility": "visible"
},
{
"id": "hyperGit.stash",
"name": "Stash",
"visibility": "visible"
}
]
},
"viewsWelcome": [
{
"view": "hyperGit.changes",
"contents": "未检测到 Git 变更或仓库。\n在 Git 仓库中打开工作区,变更将按 Changelist 分组显示。\n[新建 Changelist](command:hyperGit.newChangelist)"
},
{
"view": "hyperGit.stash",
"contents": "暂无 Stash。\n[Stash 当前变更](command:hyperGit.stashCreate)"
}
],
"commands": [
Expand All @@ -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": [
Expand All @@ -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" },
Expand All @@ -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" },
Expand All @@ -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": {
Expand Down
80 changes: 80 additions & 0 deletions src/adapter/stash-commands.ts
Original file line number Diff line number Diff line change
@@ -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;
}
56 changes: 56 additions & 0 deletions src/adapter/tree/stash-tree.ts
Original file line number Diff line number Diff line change
@@ -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<StashNode> {
private readonly _onDidChange = new vscode.EventEmitter<StashNode | undefined>();
readonly onDidChangeTreeData = this._onDidChange.event;

constructor(private readonly service: GitRepositoryService) {}

refresh(): void {
this._onDidChange.fire(undefined);
}

async getChildren(element?: StashNode): Promise<StashNode[]> {
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;
}
}
6 changes: 6 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -55,6 +57,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
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');
};
Expand All @@ -67,8 +70,10 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
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),
);
Expand All @@ -78,6 +83,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
commitView.refresh();
logTree.refresh();
branchesTree.refresh();
stashTree.refresh();
};
context.subscriptions.push(
service.onDidChange(refreshAll),
Expand Down
6 changes: 5 additions & 1 deletion tests/suite/extension.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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} 未注册`);
}
Expand Down
Loading