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
8 changes: 8 additions & 0 deletions docs/.agents/issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,12 @@
- **后续防范**:① 需要承载 `.badge` 或 `canSelectMany` 等 `TreeViewOptions` 能力的视图,一律用 `createTreeView` 而非 `registerTreeDataProvider`(本仓 `hyperGit.changes` 已有先例)。② 多选命令正确性**只依赖处理器读取实参**(`clickedNode` + `selectedNodes[]`),不得依赖 `listMultiSelection` 上下文键——其对**自定义贡献视图**的可靠性无法确证,仅作菜单整洁的视觉优化;单目标命令因只读 `clickedNode` 即便该键失效仍安全。③ 「右键点击选区之外」须以点击项为准(手势目标优先),由归一化助手统一兜底。
- **同类问题影响**:所有以 `registerTreeDataProvider` 注册却后续需要多选/角标的自定义 TreeView;以及误把单目标命令在多选下直接作用于「点击项」造成的隐性误操作。

## #9 LOG 视图残留「已删分支」提交(实为工具注入的内部引用污染 `git log --all`)

- **表因**:用户截图反馈 LOG 的 All 范围下,一批本应随分支删除而消失的提交仍以游离泳道残留;运行「清理已删远程分支」(#44,`git fetch --prune`)后**依旧存在**。
- **根因**:`engine/log/log-query.ts` 的 `buildLogArgs` 对 `all`/`checkpointer` 范围下 `git log --all`。`--all` 遍历 `refs/` 下**全部**引用,不止 heads/remotes/tags——还包括宿主工具(如 Conductor)注入的 `refs/conductor-checkpoints/*`(会话快照)、`refs/conductor-archive-heads/*`(已删/被取代分支头的归档)。这些归档头让**真实的游离提交**(被 amend/rebase 取代、或分支删除后仅靠归档存活者)仍可达,画成游离泳道。而既有的客户端 `CHECKPOINT_SUBJECT_RE=/^checkpoint:/i` 过滤**只能拦住 checkpoint 元数据提交本身**,拦不住作为其祖先的游离业务提交——故泄漏。`git fetch --prune` 仅清理 `refs/remotes/*`,对上述非远端跟踪引用**完全无效**,这正是「prune 后依旧存在」的根因。实证:本仓 `--all` 取 241 提交、`--branches --tags --remotes` 仅 70;refs 命名空间 135 conductor-checkpoints + 17 conductor-archive-heads,远多于 3 heads/3 remotes/2 tags。
- **处理方式**:`all` 范围由 `--all` 改为 `--branches --tags --remotes`(仅三大标准命名空间,排除一切工具注入的内部引用),根治游离泳道;`checkpointer` 范围**保留 `--all`**——该 Tab 的职责即「原始完整视图,含内部 checkpoint 快照」,需触达 `refs/conductor-checkpoints/*`。客户端 `keepCheckpoint` 过滤作为双保险保留。更新 `tests/unit/log-query.test.ts` 断言(`all` 含三件套、不含 `--all`;`checkpointer` 含 `--all`、不叠三件套)作回归护栏。
- **后续防范**:① 「全分支视图」语义应映射到 `--branches --tags --remotes` 而非 `--all`——`--all` 是「全部引用」而非「全部分支」,二者差异恰是工具注入引用的污染面。② 客户端按提交 message 正则过滤是**漏的抽象**(拦不住作为祖先被带入的游离提交);根治应在 ref 选取层(服务端参数)而非 subject 过滤层。③ **诊断 git 引用类问题时务必先 `git for-each-ref` 列出全部命名空间**——本案最初误判为「远端已删、本地未 prune」(#44 与一度推进的 prune-on-fetch 方案均为此误判),直到列出 refs 才发现真凶是 conductor-* 引用;「prune 无效」本身就是关键反证,应据其反向收敛而非强行加 prune。④ 修正「错漏逻辑」前先用 `git log --all` vs `--branches --tags --remotes` 的差集实证根因,避免再次基于关键字匹配机械式修改。
- **同类问题影响**:所有在带「工具注入内部引用」环境(IDE/Agent checkpoint、`refs/stash`、`refs/replace/*`、`refs/notes/*` 等)下展示 `git log --all` 图的 Git GUI;凡把「范围 = 引用集合」与「范围 = message 过滤」混为一谈的实现均可能漏过游离提交。


17 changes: 13 additions & 4 deletions src/engine/log/log-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@ import { LOG_GRAPH_FORMAT } from './log-line';

/**
* 提交范围(工具栏互斥单选):
* - `all` = 全分支(`--all`),剔除 checkpoint 自动提交(干净的人类历史,默认);
* - `all` = 仅 heads/tags/remotes(`--branches --tags --remotes`),呈现干净的人类历史(默认)。
* 关键:不使用 `--all`——`--all` 会遍历 `refs/` 下全部引用,把工具注入的内部引用(如
* `refs/conductor-checkpoints/*`、`refs/conductor-archive-heads/*`)所携带的游离/陈旧提交也画成
* 游离泳道污染视图,且 `git fetch --prune` 对这类非 `refs/remotes/*` 引用无效。显式收敛到三大
* 标准命名空间即可根治。客户端仍据 {@link LogClientFilter.keepCheckpoint} 剔除 checkpoint 自动提交(双保险)。
* - `current` = 当前分支(HEAD),剔除 checkpoint 自动提交;
* - `checkpointer` = 全分支(`--all`),保留 checkpoint 自动提交(原始完整视图)。
* - `checkpointer` = 全分支(`--all`),保留 checkpoint 自动提交(原始完整视图,含内部 checkpoint 快照)。
* checkpoint 的保留/剔除由 adapter 层据 scope 注入 `LogClientFilter.keepCheckpoint`,
* engine query 层仅据本类型决定 `--all` 分支范围,对 checkpoint 概念无感知(职责单一)。
* engine query 层仅据本类型决定分支范围,对 checkpoint 概念无感知(职责单一)。
*/
export type LogScope = 'all' | 'current' | 'checkpointer';

Expand All @@ -33,8 +37,13 @@ export interface LogQueryOptions {
*/
export function buildLogArgs(filter: LogFilter | undefined, scope: LogScope, opts: LogQueryOptions): string[] {
const args: string[] = ['--topo-order'];
if (scope === 'all' || scope === 'checkpointer') {
if (scope === 'checkpointer') {
// 原始完整视图:遍历 refs/ 全部引用,含 conductor 等工具的 checkpoint 快照与归档头。
args.push('--all');
} else if (scope === 'all') {
// 干净视图:仅 heads/tags/remotes,排除 refs/conductor-* 等工具注入的内部引用,
// 以免其携带的游离/陈旧提交以游离泳道污染人类历史(`--all` 无法排除,prune 亦触不及)。
args.push('--branches', '--tags', '--remotes');
}
args.push(`--max-count=${opts.maxCount}`);
if (opts.skip && opts.skip > 0) {
Expand Down
15 changes: 11 additions & 4 deletions tests/unit/log-query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,20 @@ describe('buildLogArgs — 顺序与必选项', () => {
expect(buildLogArgs(noFilter, 'all', { maxCount: 300 })[0]).toBe('--topo-order');
});

it('scope=all 含 --all;scope=current 不含', () => {
expect(buildLogArgs(noFilter, 'all', { maxCount: 300 })).toContain('--all');
it('scope=all 用 --branches --tags --remotes(排除工具注入的内部引用);不含 --all', () => {
const allArgs = buildLogArgs(noFilter, 'all', { maxCount: 300 });
expect(allArgs).toContain('--branches');
expect(allArgs).toContain('--tags');
expect(allArgs).toContain('--remotes');
// 关键回归护栏:不得回退到 --all(否则 refs/conductor-* 等内部引用会污染视图)
expect(allArgs).not.toContain('--all');
expect(buildLogArgs(noFilter, 'current', { maxCount: 300 })).not.toContain('--all');
});

it('scope=checkpointer 与 all 同为全分支(含 --all)', () => {
expect(buildLogArgs(noFilter, 'checkpointer', { maxCount: 300 })).toContain('--all');
it('scope=checkpointer 用 --all(原始完整视图,含内部 checkpoint 引用)且不叠三件套', () => {
const cpArgs = buildLogArgs(noFilter, 'checkpointer', { maxCount: 300 });
expect(cpArgs).toContain('--all');
expect(cpArgs).not.toContain('--branches');
});

it('max-count 与 skip', () => {
Expand Down