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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

## [Unreleased]

### Added

- **Branches 视图本地分支行显示 PR 状态徽标**:Local Branches(含 Favorites 本地条目与活动分支)行右侧以与远程行一致的形态显示关联 GitHub PR(`#N Merged/Open/Closed/Draft`),非活动分支图标按状态着色(Merged 紫 / Closed 红 / Open 绿 / Draft 弱化;活动蓝、收藏黄优先,活动分支呈 `active #N …`),悬停 Tooltip 与远程行同构(PR 号 / 标题 / Open on GitHub)。匹配语义:本地**裸分支名**走同仓 Tier b → 唯一兜底 Tier c(与远程行同口径,宁缺勿错),**不剥 remote 前缀、不做 Tier a fork 身份匹配**(本地名不携带 remote 身份,本地名 `origin/feature` 只匹配 headRefName 字面同名 PR);不用 `ref.upstream` 参与匹配(upstream 仅 CLI 兜底路径填充且 v0.0.22 起树节点 upstream 数据不受信,裸名匹配消除双数据路径下的徽标闪烁)。取数链路零改动(复用同一 GraphQL 索引,`RemotePrIndex` 更名 `BranchPrIndex` 并新增 `lookupLocal` 入口),engine 增量全量单测覆盖。详见 [功能文档](./docs/features/branches-pr-status.md)。

## [0.0.22] - 2026-10-24 — Graph 过滤持久化与激活指示 · Branches 就地更新 · 开发依赖升级

### Added
Expand Down
2 changes: 1 addition & 1 deletion docs/.agents/knowledge-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
- [文档中心](../docs/README.md) — 文档与调研资产总索引。
- [README(简体中文)](../docs/i18n/zh-CN/README.md) — 根 README 的中文版本(根路径为英文版,i18n 目录沉淀中文)。
- [Log 视图 CI 状态](../docs/features/log-ci-status.md) — 按提交显示 GitHub CI 最终状态(绿勾/红叉 + Tooltip 明细):认证、限流、懒加载、边界与配置。
- [Branches 视图远程分支 PR 状态徽标](../docs/features/branches-pr-status.md) — 远程分支行右侧显示关联 GitHub PR(`#138 Merged` + 图标按态着色):三级匹配、TTL 缓存与无循环刷新、静默边界与配置。
- [Branches 视图分支 PR 状态徽标](../docs/features/branches-pr-status.md) — 本地与远程分支行右侧显示关联 GitHub PR(`#138 Merged` + 图标按态着色):远程短名三级匹配 + 本地裸名 Tier b/c、TTL 缓存与无循环刷新、静默边界与配置。
- [Commit 视图整合](../docs/features/commit-view-consolidation.md) — 移除 CHANGES 视图,其文件操作/changelist 管理/Git 工具栏/未提交角标零回归平移入 Commit 视图。
- [变更文件目录树](../docs/features/file-list-group-by-directory.md) — Commit / Log 文件列表「平铺 ⇄ 按目录分组」切换:host 侧 `buildFileTree` 构树下发、compact 折叠、目录三态。
- [分支前缀分组树](../docs/features/branch-tree-group-by-prefix.md) — Branches 视图 Local/Remote/Tags「平铺 ⇄ 按 `/` 前缀分组」切换:`buildRefTree` 构树、分支感知排序、compact 折叠、隐藏 `origin/HEAD`、按仓库持久化。
Expand Down
27 changes: 19 additions & 8 deletions docs/features/branches-pr-status.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Branches 视图远程分支 PR 状态徽标
# Branches 视图分支 PR 状态徽标

> 在 Branches 视图(`hyperGit.branches`)**远程分支**行右侧显示关联 GitHub Pull Request 状态与
> PR 号(如 `#138 Merged`),云图标按状态着色(Merged 紫 / Closed 红 / Open 绿 / Draft 弱化);
> 悬停行以 Tooltip 展示 PR 号、标题与经主机白名单校验的 Open on GitHub 链接。数据源为 GitHub
> GraphQL(最近更新的全态 PR 列表),按 origin 远程主机自动判定 github.com / GitHub Enterprise。
> 在 Branches 视图(`hyperGit.branches`)**本地与远程分支**行右侧显示关联 GitHub Pull Request
> 状态与 PR 号(如 `#138 Merged`),图标按状态着色(Merged 紫 / Closed 红 / Open 绿 / Draft 弱化;
> 活动分支蓝、收藏黄优先);悬停行以 Tooltip 展示 PR 号、标题与经主机白名单校验的 Open on GitHub
> 链接。数据源为 GitHub GraphQL(最近更新的全态 PR 列表),按 origin 远程主机自动判定
> github.com / GitHub Enterprise。

## 数据流

Expand All @@ -24,14 +25,20 @@

## 分支 → PR 匹配(三级优先,宁缺勿错)

剥 remote 前缀复用 `resolveRemoteBranch` 最长前缀匹配(remote 名可含 `/`):
**远程短名**(`origin/feature` 形态)剥 remote 前缀复用 `resolveRemoteBranch` 最长前缀匹配(remote 名可含 `/`):

| Tier | 规则 | 场景 |
|---|---|---|
| a | `shortName === ${headOwner}/${headRefName}`,且首段 remote 的 URL host/owner/repo 与本仓 host 及 head owner/head repo 一致(owner/repo 大小写不敏感;fork 与 base 同实例、保留 base 仓库名) | remote 本身以 fork owner 命名(如 `mylogin/feature`);撞名劫持(login 与 remote 名撞名、含 `/` remote 名与键同形、remote 指向同 owner 他仓或异 forge 实例)被身份校验排除 |
| b | 剥离后 `branch === headRefName && headOwner === repoOwner` | dependabot / 仓内功能分支(主场景) |
| c | 剥离后 `branch === headRefName` 且候选**唯一** | 专属 fork remote;歧义(多 fork 同名)→ 不显示 |

**本地裸名**(Local Branches 行,含 Favorites 本地条目):**不剥前缀、无 Tier a**——本地分支名不携带
remote 身份(无 URL 可作 fork 身份校验,本地名形如 `owner/branch` 不得被当作 fork 全名),按完整裸名
走 Tier b 同仓 → Tier c 唯一兜底;本地名 `origin/feature` 只匹配 headRefName 字面同名的 PR。不用
`ref.upstream` 参与匹配:upstream 仅 CLI 兜底路径填充(API 路径刻意不填,v0.0.22 起树节点 upstream
数据不受信),参与匹配会造成首帧有徽标、刷新后消失的闪烁;裸名匹配与 refs 数据来源路径解耦。

同一 headRefName 多条 PR(复用分支名)取 `updatedAt` 最近一条。

## 认证与安全
Expand All @@ -57,7 +64,10 @@
| 配置关闭 | 零徽标、零请求 |
| 多 fork 同名分支(匹配歧义) | 不显示徽标(宁缺勿错) |
| 掉出最近 100 条 PR 的旧分支 | 静默无徽标 |
| Favorites 中的远程分支 | 同样显示徽标(图标收藏黄优先于 PR 状态色) |
| 本地分支(含活动分支) | 同样显示徽标(活动蓝 / 收藏黄图标优先于 PR 状态色,状态由徽标文本承载,形如 `active #20 Open`) |
| 本地名形如 `owner/branch`(fork checkout 形态) | 本地行无徽标(无 Tier a,不做 fork 身份匹配);对应远程行可经 Tier a 有徽标,属预期分歧 |
| 本地裸名与唯一 fork PR 撞名 | 显示该 PR(Tier c 唯一兜底)——属同名信息展示,非本分支已合并的凭证 |
| Favorites 中的分支(本地 / 远程) | 同样显示徽标(图标收藏黄优先于 PR 状态色) |
| GitHub Enterprise | 按 origin 主机自动判定,使用 `github-enterprise` provider(需已配置 `github-enterprise.uri`) |

## 配置
Expand All @@ -70,11 +80,12 @@ provider 与取数仓判定复用 `hyperGit.log.ci.provider` / `hyperGit.log.ci.

## 实现

- 引擎层(纯逻辑,Vitest 可测):[`engine/pr/`](../../src/engine/pr) — `types.ts`(PullRequestVM)、`graphql-query.ts`($variables 防注入列表查询)、`model.ts`(宽容解析 + prBadge + prsEqual)、`match.ts`(三级匹配索引)。
- 引擎层(纯逻辑,Vitest 可测):[`engine/pr/`](../../src/engine/pr) — `types.ts`(PullRequestVM)、`graphql-query.ts`($variables 防注入列表查询)、`model.ts`(宽容解析 + prBadge + prsEqual)、`match.ts`(`BranchPrIndex`:远程短名三级匹配 + 本地裸名 `lookupLocal` Tier b/c)。
- 适配层(唯一触碰 vscode/网络):[`adapter/pr/github-pr-service.ts`](../../src/adapter/pr/github-pr-service.ts)(缓存/分页/限流/safePrUrl);[`adapter/ci/github-ci-service.ts`](../../src/adapter/ci/github-ci-service.ts) 增 `getGitHubContext()`(remote/provider 单一事实源);[`adapter/ci/github-auth.ts`](../../src/adapter/ci/github-auth.ts) 增 `onDidChange` 透传。
- 渲染:[`adapter/tree/branches-tree.ts`](../../src/adapter/tree/branches-tree.ts) — `ensurePrData` 异步 kick、`describe` 追加 `#N 状态`、`prThemeColor` 图标着色(收藏黄/活动蓝优先);[`adapter/tree/tree-tooltip.ts`](../../src/adapter/tree/tree-tooltip.ts) — `mdTooltip` 增 `links` 链接行(仅含链接实例放开 isTrusted)。

## 验证

1. `pnpm run check-types` + `pnpm run lint` + `pnpm run test:unit`(含 `tests/unit/pr-*.test.ts`)全绿。
2. Extension Development Host(F5)在 GitHub 仓库:远程分支行右侧出现 `#N Open/Merged/Closed/Draft` + 图标着色;悬停见 PR 号/标题/可点击 Open on GitHub;收藏远程条目同样生效(黄标优先);未登录/非 GitHub/配置关闭 → 零徽标零请求;PR 状态变化 ≤60s(下次树刷新 + TTL 到期)自然更新;多根切换无串仓残留。
3. 本地分支行:已通过 PR 合并的本地分支右侧出现 `#N Merged`(非活动分支图标按状态着色);活动分支显示 `active #N …` 且图标保持蓝;收藏本地分支黄标优先、徽标文本仍在;同一逻辑分支的本地行与远程行显示同一 PR。
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1566,7 +1566,7 @@
"hyperGit.branches.pr.enabled": {
"type": "boolean",
"default": true,
"markdownDescription": "Show the associated GitHub pull request (e.g. `#138 Merged`) on the right of each remote branch in the Branches view, with the branch icon tinted by PR state. Uses the most recently updated 100 pull requests; hover a branch for the title and an Open on GitHub link. Requires a GitHub remote — if nothing appears, sign in via the Graph view's GitHub sign-in action (shown when CI status is enabled, on by default).",
"markdownDescription": "Show the associated GitHub pull request (e.g. `#138 Merged`) on the right of each local and remote branch in the Branches view, with the branch icon tinted by PR state. Uses the most recently updated 100 pull requests; hover a branch for the title and an Open on GitHub link. Requires a GitHub remote — if nothing appears, sign in via the Graph view's GitHub sign-in action (shown when CI status is enabled, on by default).",
"scope": "resource"
}
}
Expand Down
12 changes: 6 additions & 6 deletions src/adapter/pr/github-pr-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { extractRateLimit, parseRetryAfterMs } from '../../engine/ci/model';
import { fetchWithTimeout } from '../ci/graphql-http';
import { buildPrQuery } from '../../engine/pr/graphql-query';
import { parsePullRequestsResponse, prsEqual } from '../../engine/pr/model';
import { buildPrIndex, type PrRemoteInfo, type RemotePrIndex } from '../../engine/pr/match';
import { buildPrIndex, type BranchPrIndex, type PrRemoteInfo } from '../../engine/pr/match';
import type { PullRequestVM } from '../../engine/pr/types';

/** 单页 PR 数(GitHub 单页上限 100,取 50 平衡时延与点数)。 */
Expand All @@ -41,7 +41,7 @@ const RATE_FLOOR = 100;
interface CacheEntry {
readonly repoRoot: string;
readonly prs: readonly PullRequestVM[];
readonly index: RemotePrIndex;
readonly index: BranchPrIndex;
/** 构建索引时的输入快照(remote 增删/改名/改址影响三级匹配),续期须连同比较。 */
readonly init: { readonly repoOwner: string; readonly repoHost: string; readonly repo: string; readonly remotes: readonly PrRemoteInfo[] };
readonly expires: number;
Expand All @@ -62,7 +62,7 @@ export class GitHubPrService implements vscode.Disposable {
readonly onDidInvalidate = this._onDidInvalidate.event;

private cache: CacheEntry | null = null;
private inflight: { readonly key: string; readonly promise: Promise<RemotePrIndex | null> } | undefined;
private inflight: { readonly key: string; readonly promise: Promise<BranchPrIndex | null> } | undefined;
/** 取数代次:登录态或取数源配置变化即推进并清缓存,在途取数完成时据此作废(防写回复活旧会话/旧取数仓数据)。 */
private fetchGeneration = 0;
private cooldownUntil = 0;
Expand Down Expand Up @@ -94,7 +94,7 @@ export class GitHubPrService implements vscode.Disposable {
* 拉取(或复用)当前仓库的 PR 匹配索引。不可用(非 GitHub / 未登录 / 限流 / 关闭 / 失败)→
* null,绝不抛错。数据未变时返回缓存的同一索引实例(树侧引用相等即免 fire)。
*/
async getPullRequests(): Promise<RemotePrIndex | null> {
async getPullRequests(): Promise<BranchPrIndex | null> {
if (!this.enabled()) {
return null;
}
Expand Down Expand Up @@ -195,7 +195,7 @@ export class GitHubPrService implements vscode.Disposable {
}

/** 全量取数(分页 ≤ MAX_PAGES)→ 建索引 + 写缓存;任一页失败 → null(不缓存,留待重试)。 */
private async fetchAll(remote: GitHubRemote, provider: GitHubAuthProviderId, repoRoot: string): Promise<RemotePrIndex | null> {
private async fetchAll(remote: GitHubRemote, provider: GitHubAuthProviderId, repoRoot: string): Promise<BranchPrIndex | null> {
const generation = this.fetchGeneration; // 取数期间的代次(登录态/取数源配置变化后作废)
// remotes 与 repoRoot 同时采样(首个 await 之前):完成后才读会取到切库后他仓的列表,错配写入缓存。
// name + URL 解析 host/owner/repo:供 Tier a 身份校验(不可解析 → null;可解析但 host 与本仓
Expand All @@ -221,7 +221,7 @@ export class GitHubPrService implements vscode.Disposable {
// 网络已尝试后的失败统一走 fail():失败走**上下文一致的**旧缓存(cacheMatches 单一判定,
// 旧索引续显而非闪烁隐现),无缓存才无徽标;失败退避只在代次未变时武装(invalidate 刚复位
// 的退避不被在途旧代次的失败重新盖住——登录后/取数源切换后立即可重取)。
const fail = (): RemotePrIndex | null => {
const fail = (): BranchPrIndex | null => {
if (generation === this.fetchGeneration) {
this.failureCooldownUntil = Date.now() + FAILURE_BACKOFF_MS;
}
Expand Down
22 changes: 14 additions & 8 deletions src/adapter/tree/branches-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { GitHubPrService } from '../pr/github-pr-service';
import { FOR_EACH_REF_FORMAT, type RawRef, parseForEachRef } from '../../engine/ref/for-each-ref';
import { buildRefTree, type RefTreeNode } from '../../engine/ref/ref-tree';
import { prBadge, prStateLabel } from '../../engine/pr/model';
import type { RemotePrIndex } from '../../engine/pr/match';
import type { BranchPrIndex } from '../../engine/pr/match';
import type { PullRequestVM } from '../../engine/pr/types';
import { mdTooltip } from './tree-tooltip';

Expand Down Expand Up @@ -73,7 +73,8 @@ function prThemeColor(pr: PullRequestVM): vscode.ThemeColor {
* 四段分组:Favorites(收藏置顶,参考 JetBrains 分组设计)/ Local / Remote / Tags。
* 构造函数自订阅 service.onDidChange + favorites.onDidChange 做即时刷新。
*
* PR 徽标:远程分支行右侧 description 显示关联 PR(`#138 Merged`,GitHub 图标着色)。
* PR 徽标:本地与远程分支行右侧 description 显示关联 PR(`#138 Merged`,GitHub 图标着色)。
* 远程短名走索引三级匹配,本地裸名走 Tier b/c(不剥前缀、无 fork 身份校验,语义见 engine/pr/match)。
* 数据经 {@link GitHubPrService} 异步拉取(TTL 缓存):首轮无徽标先行渲染,索引就绪且与快照
* 不同才 fire 一次二次渲染(引用相等守卫 + 服务端同实例返回 → 无刷新循环)。
*/
Expand All @@ -87,7 +88,7 @@ export class BranchesTreeProvider implements vscode.TreeDataProvider<BranchNode>
/** 「按前缀分组为文件夹树」开关(工具栏切换、按仓库持久化,默认开启)。 */
private groupingEnabled: boolean;
/** PR 匹配索引快照(null = 无数据:未登录/非 GitHub/未就绪 → 行为与无徽标一致)。 */
private prIndex: RemotePrIndex | null = null;
private prIndex: BranchPrIndex | null = null;
private prEnsureInFlight: Promise<void> | undefined;
/** PR 取数代次:切库/登录态/配置任一变化即 +1;在途取数完成时代次已变则弃结果并对当前态补拉。 */
private prKickEpoch = 0;
Expand Down Expand Up @@ -378,16 +379,21 @@ export class BranchesTreeProvider implements vscode.TreeDataProvider<BranchNode>
const active = this.isActive(ref);
const fav = this.favorites.isFavorite(ref.shortName);
const isTag = ref.isTag;
// PR 徽标仅远程分支(Favorites 里的远程条目同路径自动生效)。
const pr = !isTag && element.remote ? this.prIndex?.lookup(ref.shortName) : undefined;
// PR 徽标:远程短名三级匹配 / 本地裸名 Tier b/c(含活动分支与 Favorites 条目,同路径自动生效)。
const pr = !isTag
? element.remote
? this.prIndex?.lookup(ref.shortName)
: this.prIndex?.lookupLocal(ref.shortName)
: undefined;
// 分组形态下展示后缀(element.label);平铺/收藏回退完整短名。命令仍以 ref.shortName 定位。
const item = new vscode.TreeItem(element.label ?? ref.shortName, vscode.TreeItemCollapsibleState.None);
item.description = this.describe(ref, active, pr);
item.contextValue = isTag ? 'hyperGit.tag' : element.remote ? 'hyperGit.remoteBranch' : 'hyperGit.branch';
item.tooltip = this.tooltip(ref, active, fav, pr);
const icon = isTag ? 'tag' : element.remote ? 'cloud' : 'git-branch';
// 活动分支 = charts.blue(与 Log 本地分支 chip 同语义,全局一致);收藏(非活动)= charts.yellow;
// 其余远程分支带 PR → 按状态着色(远程行 active 恒 false,实际即收藏优先于 PR 色)。
// 活动分支 = charts.blue(与 Log 本地分支 chip 同语义,全局一致;本地活动行的 PR 状态色此时
// 由 description 徽标文本承载);收藏(非活动)= charts.yellow;其余分支带 PR → 按状态着色
// (远程行 active 恒 false,实际即收藏优先于 PR 色)。
const color = active
? new vscode.ThemeColor('charts.blue')
: fav
Expand All @@ -412,7 +418,7 @@ export class BranchesTreeProvider implements vscode.TreeDataProvider<BranchNode>
}
}
if (pr) {
parts.push(prBadge(pr)); // `#138 Merged` —— 远程行右侧 PR 徽标
parts.push(prBadge(pr)); // `#138 Merged` —— 分支行右侧 PR 徽标
}
return parts.join(' ');
}
Expand Down
Loading
Loading