diff --git a/docs/.agents/issue.md b/docs/.agents/issue.md index 48f959b..4cadad8 100644 --- a/docs/.agents/issue.md +++ b/docs/.agents/issue.md @@ -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 过滤」混为一谈的实现均可能漏过游离提交。 + diff --git a/src/engine/log/log-query.ts b/src/engine/log/log-query.ts index 6134437..e77ddfa 100644 --- a/src/engine/log/log-query.ts +++ b/src/engine/log/log-query.ts @@ -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'; @@ -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) { diff --git a/tests/unit/log-query.test.ts b/tests/unit/log-query.test.ts index 5a8407b..3c3be5b 100644 --- a/tests/unit/log-query.test.ts +++ b/tests/unit/log-query.test.ts @@ -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', () => {