Skip to content

Commit 69d64c3

Browse files
committed
Merge origin/feature/1.x.x → hyper-git-icon-design
同步 #31(Branches 视图多选 + 批量删除/复制/收藏)与 #32(门控 Marketplace 自动发布以消除 deployments 报错);解决 CHANGELOG.md [Unreleased] 区段冲突——按 Keep a Changelog 顺序保留双条目(### Added → ### Changed)。 🤖 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<threefish.ai@gmail.com>
2 parents 4b708f4 + 0554484 commit 69d64c3

15 files changed

Lines changed: 445 additions & 73 deletions

File tree

.github/workflows/ci.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ jobs:
9494
generate_release_notes: true
9595
fail_on_unmatched_files: true
9696

97-
# 发布:仅 tag 触发,双市场(Marketplace + OpenVSX),生产环境审批门
97+
# 发布:仅 tag 触发;OpenVSX 默认发布,VS Code Marketplace 由 ENABLE_MARKETPLACE_PUBLISH 变量门控(默认关闭);生产环境审批门
9898
publish:
9999
name: Publish
100100
needs: package
@@ -112,7 +112,11 @@ jobs:
112112
- uses: actions/download-artifact@v4
113113
with:
114114
name: vsix
115+
# VS Code Marketplace 发布:默认关闭(未配置 VSCE_PAT)。
116+
# 配好 VSCE_PAT 后,在仓库 Settings → Secrets and variables → Actions → Variables
117+
# 新增 ENABLE_MARKETPLACE_PUBLISH=true 即恢复,无需改代码或发 PR。
115118
- name: 发布到 VS Code Marketplace
119+
if: ${{ vars.ENABLE_MARKETPLACE_PUBLISH == 'true' }}
116120
run: |
117121
PRE_FLAG=$([[ "${GITHUB_REF_NAME}" == *rc* ]] && echo "--pre-release" || echo "")
118122
pnpm dlx @vscode/vsce publish --packagePath *.vsix $PRE_FLAG

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
## [Unreleased]
66

7+
### Added
8+
9+
- **Branches 视图多选 + 批量操作**`hyperGit.branches``registerTreeDataProvider` 改用 `createTreeView({ canSelectMany: true })`,支持 Ctrl/Cmd/Shift 框选多个分支/标签。批量命令作用于整个选区:**删除分支**(一次 `git branch --merged` 分类已合并/未合并,单条确认弹窗诚实分栏呈现强制删除风险,逐个删除并汇总成功/失败)、**删除标签****复制引用**(按行连接)、**收藏切换**。仅单目标语义的操作(检出/合并/变基/重命名/比较)经 `!listMultiSelection` 在多选时从右键菜单隐藏,且因仅读「右键点击项」而始终安全。新增纯逻辑 `engine/ref/selection.collectBranchRefs`(选区归一化 + 「点击在选区之外」手势优先)与 `engine/ref/cleanup``partitionByMerged`/`formatBranchDeleteConfirm`/`truncateNames`,单测全覆盖。
10+
711
### Changed
812

913
- **品牌图标统一**:将活动栏图标(`media/hyper-git-icon.svg`)、Marketplace 市场徽标(`media/icon.png`)与 README 头图统一为辨识度更高的「Git Pull Request」造型(字形改编自 Tabler Icons,MIT);市场徽标基于新字形重新栅格化为 256×256 RGBA 蓝→紫渐变图,设计源沉淀至 `media-src/icon.svg`;README 头部重构为居中布局(徽标 + 标题 + 一句话定位 + License 徽章)。

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ IntelliJ IDEA 的统一 Git 工具窗口(顶部 `Commit / Shelf / Stash` 标
2626
- **多 changelist Changes 视图**:active 列表、新建/删除/重命名/移动,`workspaceState` 持久化(重启恢复);文件状态色复用 `gitDecoration.*` 主题色。
2727
- **Commit 提交窗口**:多行编辑器 + Conventional Commits 实时校验 + Amend / Signed-off-by / 跳过 Git hooks + Commit / Commit and Push;勾选集为提交权威范围;最近消息复用。
2828
- **Log 提交历史**:按作者/路径过滤、清除过滤、复制 commit hash、显示文件历史。
29-
- **Branches**:本地/远程分组、活动分支高亮;新建/检出/删除/合并/变基(rebase)。
29+
- **Branches**:本地/远程分组、活动分支高亮;新建/检出/删除/合并/变基(rebase)**多选批量**(Ctrl/Cmd/Shift 框选后批量删除分支/标签、复制引用、收藏)
3030
- **Stash**:create / apply / pop / drop(作用于 stash@{0} 最新)。
3131
- **其他**:Discard 改动、Pull / Push / Fetch、Blame、Diff(HEAD ↔ Working)。
3232

docs/.agents/issue.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,12 @@
6262
- **后续防范**:该 job 与市场 `publish` **解耦**(不 `needs: publish`、不挂 `environment: production`),保证「Release 带 `.vsix`」不被市场审批门/密钥缺失阻塞;「仅出 Release、暂不发市场」时不审批 production 即可,无需改 publish job;最小权限仅本 job 提权 `contents: write`
6363
- **同类问题影响**:所有「CI 只上传 artifact + 发市场、却在 README 承诺 Release 手动下载」的 VS Code 扩展;artifact ≠ Release 资产,二者可见性/留存期差异易被忽视。
6464

65+
## #8 Branches 视图无法多选(批量删除等批量操作缺失)
66+
67+
- **表因**:用户截图反馈 Branches 视图中一组功能/工作分支无法框选多个、无法批量删除。
68+
- **根因**`hyperGit.branches``vscode.window.registerTreeDataProvider` 注册——该 API **不支持** `canSelectMany`,故视图天然单选;所有分支命令处理器亦只接收单个 `BranchNode`。多选能力(`canSelectMany: true`)仅 `createTreeView``TreeViewOptions` 支持。
69+
- **处理方式**:改用 `createTreeView('hyperGit.branches', { treeDataProvider, canSelectMany: true })`(句柄入 subscriptions)。批量命令处理器签名扩展为 `(clickedNode, selectedNodes[])`——VS Code 多选树的 `view/item/context` 命令第 2 实参即完整选区数组。新增纯逻辑 `engine/ref/selection.collectBranchRefs`(谓词过滤 + shortName 去重 + 「点击在选区之外则以点击项为准」)与 `engine/ref/cleanup.partitionByMerged`/`formatBranchDeleteConfirm`,使 `branchDelete`/`tagDelete`/`copyBranchRef`/`toggleFavorite` 批量化(删除仅一次 `git branch --merged` 分类、汇总成功/失败、末尾单次刷新)。`package.json` 对仅单目标命令(检出/合并/变基/重命名/比较等)追加 `&& !listMultiSelection` 在多选时隐藏。
70+
- **后续防范**:① 需要承载 `.badge``canSelectMany``TreeViewOptions` 能力的视图,一律用 `createTreeView` 而非 `registerTreeDataProvider`(本仓 `hyperGit.changes` 已有先例)。② 多选命令正确性**只依赖处理器读取实参**`clickedNode` + `selectedNodes[]`),不得依赖 `listMultiSelection` 上下文键——其对**自定义贡献视图**的可靠性无法确证,仅作菜单整洁的视觉优化;单目标命令因只读 `clickedNode` 即便该键失效仍安全。③ 「右键点击选区之外」须以点击项为准(手势目标优先),由归一化助手统一兜底。
71+
- **同类问题影响**:所有以 `registerTreeDataProvider` 注册却后续需要多选/角标的自定义 TreeView;以及误把单目标命令在多选下直接作用于「点击项」造成的隐性误操作。
72+
6573

docs/research/04-publishing-cicd.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,28 @@ jobs:
188188
- **最小权限**:仅 `github-release` 提权 `contents: write`,顶层 `permissions: contents: read` 不动;
189189
- **复用**:消费 `package` job 既有的 `vsix` artifact,零重复打包。
190190

191+
### 3.2.2 VS Code Marketplace 发布门控(特性开关,本仓 `ci.yml` 现状)
192+
193+
在尚未配置 `VSCE_PAT` 凭证期间,`publish` job 的「发布到 VS Code Marketplace」步骤(`vsce publish`)会因鉴权失败而中断整个 job;因该 job 挂 `environment: production`,会在仓库 `/deployments` 页持续产生 **error** 状态的 deployment 记录。为此,本仓用**仓库级特性开关变量** `ENABLE_MARKETPLACE_PUBLISH` 对该步骤做门控:
194+
195+
```yaml
196+
- name: 发布到 VS Code Marketplace
197+
if: ${{ vars.ENABLE_MARKETPLACE_PUBLISH == 'true' }} # 默认未设=跳过,零报错
198+
run: |
199+
PRE_FLAG=$([[ "${GITHUB_REF_NAME}" == *rc* ]] && echo "--pre-release" || echo "")
200+
pnpm dlx @vscode/vsce publish --packagePath *.vsix $PRE_FLAG
201+
env:
202+
VSCE_PAT: ${{ secrets.VSCE_PAT }}
203+
```
204+
205+
设计要点(契合 AGENTS.md「最小干预 / 演进式设计」):
206+
207+
- **默认关闭**:变量未设置时其值为空串,`'' == 'true'` 为假 → 步骤显示 **skipped**(非 failed),不致 job 失败;job 继续执行其后的 OpenVSX 步骤。
208+
- **OpenVSX 不受影响**:`ovsx publish` 步骤保持无条件发布,Cursor/Windsurf 等 AI IDE 用户的安装来源不中断(前提:`OVSX_PAT` 已配置)。
209+
- **零代码恢复**:取得 Azure DevOps PAT 后,在仓库 `Settings → Secrets and variables → Actions` 配置 `VSCE_PAT`(Secret)并新增 `ENABLE_MARKETPLACE_PUBLISH=true`(Variable)即恢复 Marketplace 自动发布,无需改动 workflow 或再发 PR——这是特性开关相较「注释/删除步骤」的核心优势。
210+
211+
> 注:本门控仅阻止「未来」的失败 deployment;`/deployments` 上已有的历史失败记录不会自动消失,如需清空可在 `Settings → Environments → production` 删除该环境(会一并清除其 deployment 历史,下次发布时按 `environment: production` 自动重建),或经 REST API 逐条删除。
212+
191213
### 3.3 关键 Action 版本
192214
- `actions/checkout@v4`、`actions/setup-node@v4`、`actions/upload-artifact@v4`、`actions/download-artifact@v4`;
193215
- `@vscode/test-electron`(或新版 VS Code Test CLI)驱动集成测试;

package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -252,18 +252,18 @@
252252
{ "command": "hyperGit.showHistory", "when": "view == hyperGit.changes && viewItem == hyperGit.fileChange", "group": "1_file@2" },
253253
{ "command": "hyperGit.discardChanges", "when": "view == hyperGit.changes && viewItem == hyperGit.fileChange", "group": "1_file@3" },
254254
{ "command": "hyperGit.copyCommitHash", "when": "view == hyperGit.log && viewItem == hyperGit.commit", "group": "1_commit@1" },
255-
{ "command": "hyperGit.branchCheckout", "when": "view == hyperGit.branches && viewItem =~ /hyperGit.branch|hyperGit.remoteBranch/", "group": "1_branch@1" },
255+
{ "command": "hyperGit.branchCheckout", "when": "view == hyperGit.branches && viewItem =~ /hyperGit.branch|hyperGit.remoteBranch/ && !listMultiSelection", "group": "1_branch@1" },
256256
{ "command": "hyperGit.branchDelete", "when": "view == hyperGit.branches && viewItem == hyperGit.branch", "group": "1_branch@2" },
257-
{ "command": "hyperGit.mergeBranch", "when": "view == hyperGit.branches && viewItem =~ /hyperGit.branch|hyperGit.remoteBranch/", "group": "1_branch@3" },
258-
{ "command": "hyperGit.rebaseBranch", "when": "view == hyperGit.branches && viewItem =~ /hyperGit.branch|hyperGit.remoteBranch/", "group": "1_branch@4" },
259-
{ "command": "hyperGit.branchRename", "when": "view == hyperGit.branches && viewItem == hyperGit.branch", "group": "1_branch@5" },
260-
{ "command": "hyperGit.compareBranches", "when": "view == hyperGit.branches && viewItem =~ /hyperGit.branch|hyperGit.remoteBranch/", "group": "1_branch@6" },
257+
{ "command": "hyperGit.mergeBranch", "when": "view == hyperGit.branches && viewItem =~ /hyperGit.branch|hyperGit.remoteBranch/ && !listMultiSelection", "group": "1_branch@3" },
258+
{ "command": "hyperGit.rebaseBranch", "when": "view == hyperGit.branches && viewItem =~ /hyperGit.branch|hyperGit.remoteBranch/ && !listMultiSelection", "group": "1_branch@4" },
259+
{ "command": "hyperGit.branchRename", "when": "view == hyperGit.branches && viewItem == hyperGit.branch && !listMultiSelection", "group": "1_branch@5" },
260+
{ "command": "hyperGit.compareBranches", "when": "view == hyperGit.branches && viewItem =~ /hyperGit.branch|hyperGit.remoteBranch/ && !listMultiSelection", "group": "1_branch@6" },
261261
{ "command": "hyperGit.copyBranchRef", "when": "view == hyperGit.branches && viewItem =~ /hyperGit.branch|hyperGit.remoteBranch/", "group": "1_branch@7" },
262262
{ "command": "hyperGit.toggleFavorite", "when": "view == hyperGit.branches && viewItem =~ /hyperGit.branch|hyperGit.remoteBranch/", "group": "inline" },
263263
{ "command": "hyperGit.toggleFavorite", "when": "view == hyperGit.branches && viewItem =~ /hyperGit.branch|hyperGit.remoteBranch/", "group": "1_branch@0" },
264-
{ "command": "hyperGit.checkoutAsNew", "when": "view == hyperGit.branches && viewItem =~ /hyperGit.branch|hyperGit.remoteBranch/", "group": "1_branch@1.5" },
265-
{ "command": "hyperGit.compareWithCurrent", "when": "view == hyperGit.branches && viewItem =~ /hyperGit.branch|hyperGit.remoteBranch|hyperGit.tag/", "group": "1_branch@8" },
266-
{ "command": "hyperGit.tagCheckout", "when": "view == hyperGit.branches && viewItem == hyperGit.tag", "group": "1_tag@1" },
264+
{ "command": "hyperGit.checkoutAsNew", "when": "view == hyperGit.branches && viewItem =~ /hyperGit.branch|hyperGit.remoteBranch/ && !listMultiSelection", "group": "1_branch@1.5" },
265+
{ "command": "hyperGit.compareWithCurrent", "when": "view == hyperGit.branches && viewItem =~ /hyperGit.branch|hyperGit.remoteBranch|hyperGit.tag/ && !listMultiSelection", "group": "1_branch@8" },
266+
{ "command": "hyperGit.tagCheckout", "when": "view == hyperGit.branches && viewItem == hyperGit.tag && !listMultiSelection", "group": "1_tag@1" },
267267
{ "command": "hyperGit.tagDelete", "when": "view == hyperGit.branches && viewItem == hyperGit.tag", "group": "1_tag@2" },
268268
{ "command": "hyperGit.cherryPick", "when": "view == hyperGit.log && viewItem == hyperGit.commit", "group": "1_commit@1" },
269269
{ "command": "hyperGit.revertCommit", "when": "view == hyperGit.log && viewItem == hyperGit.commit", "group": "1_commit@2" },

src/adapter/advanced-commands.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { BranchNode, BranchesTreeProvider } from './tree/branches-tree';
33
import type { GitRepositoryService } from './git-repository-service';
44
import type { LogNode } from './tree/log-tree';
55
import { filterMergeable } from '../engine/ref/cleanup';
6+
import { selectedBranchRefs } from './branch-selection';
67

78
const errMsg = (e: unknown): string => (e instanceof Error ? e.message : String(e));
89

@@ -135,15 +136,16 @@ export function registerAdvancedCommands(service: GitRepositoryService, branches
135136
);
136137

137138
subs.push(
138-
vscode.commands.registerCommand('hyperGit.copyBranchRef', async (node?: BranchNode) => {
139-
if (node?.kind !== 'branch') {
139+
vscode.commands.registerCommand('hyperGit.copyBranchRef', async (node?: BranchNode, nodes?: BranchNode[]) => {
140+
// 支持多选:复制全部选中引用(按行连接)。
141+
const names = selectedBranchRefs(node, nodes, () => true)
142+
.map((r) => r.shortName)
143+
.filter((n) => n.length > 0);
144+
if (names.length === 0) {
140145
return;
141146
}
142-
const ref = node.ref.shortName;
143-
if (ref) {
144-
await vscode.env.clipboard.writeText(ref);
145-
void vscode.window.showInformationMessage(`已复制 ${ref}`);
146-
}
147+
await vscode.env.clipboard.writeText(names.join('\n'));
148+
void vscode.window.showInformationMessage(names.length === 1 ? `已复制 ${names[0]}` : `已复制 ${names.length} 个引用`);
147149
}),
148150
);
149151

src/adapter/branch-selection.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Branches 多选命令的适配层归一化(薄包装)。
3+
*
4+
* 把 VS Code `view/item/context` 命令的 `(clickedNode, selectedNodes[])` 实参抽取为 RawRef,
5+
* 再委托纯逻辑 {@link collectBranchRefs} 完成谓词过滤 / 去重 / 「点击在选区之外」归一化。
6+
* 维持「UI → Adapter → Engine」单向依赖:BranchNode 类型仅在本适配层出现。
7+
*/
8+
9+
import type { RawRef } from '../engine/ref/for-each-ref';
10+
import { collectBranchRefs } from '../engine/ref/selection';
11+
import type { BranchNode } from './tree/branches-tree';
12+
13+
/**
14+
* 解析多选命令的作用目标 ref 列表。
15+
* @param clicked 右键点击节点(第 1 实参)
16+
* @param selection VS Code 传入的选区节点(第 2 实参,仅多选时存在)
17+
* @param predicate ref 谓词(如本地可删 / 标签)
18+
*/
19+
export function selectedBranchRefs(
20+
clicked: BranchNode | undefined,
21+
selection: BranchNode[] | undefined,
22+
predicate: (ref: RawRef) => boolean,
23+
): RawRef[] {
24+
const clickedRef = clicked?.kind === 'branch' ? clicked.ref : undefined;
25+
const selRefs = (selection ?? [])
26+
.filter((n): n is Extract<BranchNode, { kind: 'branch' }> => n.kind === 'branch')
27+
.map((n) => n.ref);
28+
return collectBranchRefs(clickedRef, selRefs, predicate);
29+
}

0 commit comments

Comments
 (0)