From 2cb460de78f24bf6537b8d1892305d7f6c7bf3d6 Mon Sep 17 00:00:00 2001 From: ThreeFish Date: Sun, 5 Jul 2026 23:49:14 +0800 Subject: [PATCH] =?UTF-8?q?fix(Graph):=20=E6=8F=90=E4=BA=A4=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E6=8C=89=E4=BD=9C=E8=80=85=E6=97=A5=E6=9C=9F=E5=80=92?= =?UTF-8?q?=E5=BA=8F=E2=80=94=E2=80=94=E5=8F=96=E6=95=B0=20--topo-order=20?= =?UTF-8?q?=E6=94=B9=20--author-date-order=EF=BC=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=97=81=E6=94=AF=E6=8F=90=E4=BA=A4=E6=88=90=E5=9D=97=E8=87=B4?= =?UTF-8?q?=E6=97=A5=E6=9C=9F=E5=88=97=E5=9B=9E=E8=B7=B3;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 现象:GRAPH 视图(All 范围)提交记录前段日期正常递减,中段日期回跳到最新(旁支提交被整块挤到末尾),严重不符合「按时间倒序」预期; - 根因:buildLogArgs 取数首参 --topo-order,其「分支成块」语义把不同分支线提交不相邻混排,致从较旧提交分叉但日期较新的旁支提交整块下放;管道各层(engine 解析 / adapter 装配 / webview 渲染)均无二次排序,git 输出序即最终显示序; - 修复:log-query.ts:39 单行 --topo-order → --author-date-order。选 author-date(非 committer date)以与视图日期列 row.authorDate 对齐(单一事实源);lane 算法仅依赖「子在父之上」不变量,--author-date-order 同样保证,泳道不断裂; - 测试:log-query.test.ts 断言改 --author-date-order + 补「不含 --topo-order」回归护栏;顺带把 startsWith('--author') 改为 startsWith('--author='),消除与 --author-date-order 的前缀碰撞隐患; - 注释:同步 log-query/graph-types/graph-layout/log-line/log-webview 共 8 处 topo-order 措辞,澄清「不变量真实要求 = 子在父之上」(非「分支成块」); - 沉淀:CHANGELOG [Unreleased]/Fixed 记录;docs/.agents/issue.md 追加 #14(表因 / 根因 / 处理 / 防范 / 同类影响); - 验证:pnpm run test:unit(349 全过)/ check-types / lint 全绿;git log 实测 --author-date-order 序列严格单调递减; 🤖 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 --- CHANGELOG.md | 3 +++ docs/.agents/issue.md | 8 ++++++++ src/adapter/webview/log-webview.ts | 2 +- src/engine/log/graph-layout.ts | 2 +- src/engine/log/graph-types.ts | 6 ++++-- src/engine/log/log-line.ts | 5 +++-- src/engine/log/log-query.ts | 10 +++++++--- tests/unit/log-query.test.ts | 12 ++++++++---- 8 files changed, 35 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53f9df5..8c217ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ ## [Unreleased] +### Fixed +- **GRAPH 视图提交记录日期回跳**:取数参数 `--topo-order` → `--author-date-order`。`--topo-order` 会把不同分支的提交「成块」输出,导致从较旧提交分叉但日期较新的旁支提交被整块挤到列表末尾,出现「前几行日期递减、中途回跳到最新日期」的反直觉乱序;`--author-date-order` 同样保证 lane 算法依赖的「子在父之上」不变量,且按作者日期倒序跨支正确交错,并与视图日期列(`row.authorDate`)对齐。详见 [issue #14](./docs/.agents/issue.md)。 + ## [0.0.13] - 2026-07-05 — 视图容器由活动栏迁移至底部面板(Terminal 之后) 将 Hyper Git 视图容器从活动栏(Activity Bar / Primary Side Bar)迁移至底部面板(Panel),默认排在 Terminal 页签之后,提供与官方 Terminal / Output / Problems 一致的底部停靠体验。完整用户视角叙述见 [Release Note v0.0.13](./docs/releases/v0.0.13.md)。 diff --git a/docs/.agents/issue.md b/docs/.agents/issue.md index d6d1839..f918195 100644 --- a/docs/.agents/issue.md +++ b/docs/.agents/issue.md @@ -114,4 +114,12 @@ 4. **dock 迁移属用户可见布局变更**:VS Code 会记忆旧 dock 位置,老用户升级后需「View: Reset View Locations」或右键容器页签「Reset Location」才采用新默认(与 #12 同款平台行为,实机验证须用干净 profile)。 - **同类问题影响**:所有在 activitybar/panel 间迁移自定义视图容器的扩展;凡依赖容器图标 badge「始终可见」语义的扩展;以及把「dock 迁移」误当作纯内部重构而忽视 badge 可见性回退的实现。 +## #14 GRAPH 视图提交记录日期回跳(`--topo-order` 旁支成块 vs `--author-date-order`) + +- **表因**:用户截图反馈 GRAPH 视图(All 范围)提交记录**未按时间倒序**——前 10 行作者日期由 `2026-07-05` 正确递减到 `2026-05-25`,但第 11 行起日期**回跳**回 `2026-07-05`(一批远端旁支提交:`fix-github-security` / `msgpack` / `pydantic-settings` / `ts-deepmerge` / `perceive daegu-v2` / `docs wiki` ×2 / `PDF→MD`),理应排在最顶部的新提交却落在列表底部。 +- **根因**:`engine/log/log-query.ts` 的 `buildLogArgs` 取数首参为 `--topo-order`,且管道各层(engine 解析 / adapter 装配 / webview 渲染)**均无任何二次排序**——git 输出序即最终显示序。`--topo-order` 语义为「子在父之上」**且额外约束「不同分支线历史不相邻混排」**:它把一条分支的提交整块输出后再切下一条。当仓库存在从较旧提交分叉、但提交日期较新的旁支(本案 `--branches --tags --remotes` 命中的远端跟踪分支),这些旁支提交会被整块挤到列表末尾,造成日期列回跳。`--topo-order` **不是**按日期排序,分支成块是其设计特性而非 bug,但对「按时间倒序浏览」的人类预期是错的。实证(本仓 `git log`,前 18 条作者日期):`--topo-order` 序列在 `22:07:16`(旁支提交)处错位到第 15 位(晚于 `20:32:35`);`--author-date-order` 同位置归位到第 13 位,整体严格单调递减。 +- **处理方式**:`log-query.ts:39` 单行 `'--topo-order'` → `'--author-date-order'`。选 `--author-date-order` 而非 `--date-order` 的依据——**单一事实源**:视图日期列渲染的是 `row.authorDate`(`log-webview.ts:771` `fmtDate(row.authorDate)`),排序键须与显示键对齐,否则 rebase / cherry-pick 提交(committer date ≠ author date)仍会与显示列错位。lane 算法安全性:通读 `graph-layout.ts:32-145`,算法仅依赖「处理 commit 时窗口内其全部子已处理」这一不变量(在父 hash 上开 / 闭 lane 槽),**不依赖** topo-order 的「分支成块」特性;`git log --author-date-order` 契约原文 "Show no parents before all of their children are shown" 同样保证「子在父之上」,故 lane 不会断裂。同步更新 8 处 `topo-order` 注释(`log-query.ts` / `graph-types.ts` / `graph-layout.ts` / `log-line.ts` / `log-webview.ts`)澄清「不变量真实要求 = 子在父之上」,更新 `log-query.test.ts` 断言并补「不含 `--topo-order`」回归护栏;顺带加固既有的 `--author` 前缀测试(`startsWith('--author')` → `startsWith('--author=')`),消除与本 flag 的前缀碰撞隐患。 +- **后续防范**:① 自计算 DAG lane 布局的算法只需「子在父之上」拓扑约束,**不要求**「分支成块」——`--topo-order` / `--date-order` / `--author-date-order` 三者均满足前者,区别仅在无父子约束提交间的次序;选 flag 时应据「人类预期次序」而非默认 topo。② **排序键须与显示键对齐**:UI 显示哪一列(author date / committer date),git 取数就应用对应的 `--author-date-order` / `--date-order`,否则会出现「列内日期看似乱序」的二次 bug。③ 排查「显示乱序」类问题先确认管道**是否存在二次排序**——本案各层均无,根因在 git 取数参数层;若上层曾 re-sort,还须检查是否破坏拓扑约束。④ 测试断言里用 `--author` / `--grep` 这类**短前缀**判「无 flag」时,须警惕与同前缀的排序 / 过滤 flag(`--author-date-order`、`--author-date`)碰撞,宜用带 `=` 的精确前缀(`--author=`)。⑤ 与 #9 同源教训:`log-query.ts` 的 git 取数参数是 GRAPH 视图多项语义(范围 / 排序)的单一事实源,改 git 参数 + 注释 + 测试断言三件套应一并完成。 +- **同类问题影响**:所有用 `git log --topo-order` 取数、自计算或直接渲染提交图、且 UI 暴露「按时间浏览」预期的 Git GUI;凡把「lane 算法要求」误读为「必须 `--topo-order`」(实为「子在父之上」即可)的实现均会复现日期回跳;以及排序键与显示日期列不一致(author date vs committer date)导致的「列内看似乱序」类二次 bug。 + diff --git a/src/adapter/webview/log-webview.ts b/src/adapter/webview/log-webview.ts index 31fe8d3..9b702bf 100644 --- a/src/adapter/webview/log-webview.ts +++ b/src/adapter/webview/log-webview.ts @@ -85,7 +85,7 @@ interface GraphPage { /** * Log 视图(WebviewView):可视化提交图(DAG)。 * - * 自计算 lane 布局(engine/log/graph-layout)→ 渲染彩色泳道;host 侧单次 `git log --topo-order` + * 自计算 lane 布局(engine/log/graph-layout)→ 渲染彩色泳道;host 侧单次 `git log --author-date-order` * 取数 + `for-each-ref` 取引用标签;webview 端虚拟化 SVG 行 + 文本列。保留全部既有交互: * 7 个过滤命令(经 {@link LogFilterControl})、9 个 per-commit 操作(右键 → host 重调用)、 * 选中提交查看变更文件、All/Current 范围切换、滚动增量加载、实时刷新。 diff --git a/src/engine/log/graph-layout.ts b/src/engine/log/graph-layout.ts index e61976d..16b75ea 100644 --- a/src/engine/log/graph-layout.ts +++ b/src/engine/log/graph-layout.ts @@ -26,7 +26,7 @@ function firstNull(arr: ReadonlyArray): number { } /** - * 计算提交图布局。`commits` 须按 `--topo-order`(newest-first)排序。 + * 计算提交图布局。`commits` 须按「newest-first 且子在父之上」排序(取数用 `--author-date-order`)。 * 复杂度 O(n·L),L = 瞬时并发 lane 数(现实 <10),1000 commit < 5ms。 */ export function computeGraphLayout(commits: readonly GraphCommit[]): readonly GraphLayoutRow[] { diff --git a/src/engine/log/graph-types.ts b/src/engine/log/graph-types.ts index d4030ab..3748850 100644 --- a/src/engine/log/graph-types.ts +++ b/src/engine/log/graph-types.ts @@ -6,8 +6,10 @@ * Graph 效果——不再依赖 `git log --graph` 的粗糙 ASCII(lane 由 git 分配、 * 不可控、随列号抖动着色)。算法参考 gitamine 的 nil-slot 复用 [1] 与 git-graph 的分支区间装箱 [2]。 * - * 调用方须保证 commits 按 `--topo-order`(newest-first 且子在父之上)排序——lane 增量算法依赖 - * 「处理 commit 时其全部在窗口内的子已处理」这一不变量,否则 lane 会断裂。 + * 调用方须保证 commits 按「newest-first 且子在父之上」排序(取数用 `--author-date-order`,见 + * {@link ./log-query})——lane 增量算法依赖「处理 commit 时其全部在窗口内的子已处理」这一不变量, + * 否则 lane 会断裂。注意:不变量真实要求仅为「子在父之上」(`--topo-order` / `--author-date-order` + * 均满足),非要求 topo-order 的「分支成块」特性——后者会把旁支提交整块挤到末尾,致日期列回跳。 * * [1] P. Vigier, "Commit graph drawing algorithms," pvigier's blog, 2019. * [2] M. Lange, "git-graph: branch assignment & lane layout," 2025. diff --git a/src/engine/log/log-line.ts b/src/engine/log/log-line.ts index ecf1091..4f76629 100644 --- a/src/engine/log/log-line.ts +++ b/src/engine/log/log-line.ts @@ -5,10 +5,11 @@ * 替代旧 `git log --graph` ASCII 路径。配套 CLI(字段以 NUL `%x00` 分隔、记录以 RS `%x1e` 终止, * 二者均不会出现在 git 文本输出中,规避多行 subject 歧义): * - * git log --topo-order [--all] --max-count= [--skip=] [--author --grep -- ] + * git log --author-date-order [--all] --max-count= [--skip=] [--author --grep -- ] * --format= * - * `--topo-order` 为硬性要求:lane 增量算法依赖「子在父之上」严格成立。 + * `--author-date-order` 为硬性要求:lane 增量算法依赖「子在父之上」严格成立(本 flag 与 `--topo-order` + * 均满足该不变量,但另按作者日期倒序排列,避免 `--topo-order` 把旁支成块挤到末尾致日期列回跳)。 */ /** diff --git a/src/engine/log/log-query.ts b/src/engine/log/log-query.ts index e77ddfa..6965532 100644 --- a/src/engine/log/log-query.ts +++ b/src/engine/log/log-query.ts @@ -4,7 +4,9 @@ * 把 {@link LogFilter} + 范围 + 分页翻译为 `git log` 参数向量(含 {@link LOG_GRAPH_FORMAT}), * 供 host 侧 `service.execGit(['log', ...buildLogArgs(...)])` 单次取数。服务端维度(author/grep/path) * 走 git 参数;客户端维度(mergeMode/date/regex)由 host 经 {@link toClientFilter} + {@link applyClientFilters} - * 处理。`--topo-order` 为硬性要求(lane 算法依赖拓扑序);pathspec `-- ` 必须置于末尾。 + * 处理。排序用 `--author-date-order`:① lane 算法依赖「子在父之上」不变量,本 flag 同样保证(与 `--topo-order` 一致); + * ② 额外按作者日期倒序,跨支提交正确交错(`--topo-order` 会把旁支成块挤到末尾致日期回跳);③ 排序键与视图日期列 + * (`row.authorDate`)对齐,肉眼所见与排列顺序永远一致。pathspec `-- ` 必须置于末尾。 */ import type { LogFilter } from './log-filter'; @@ -33,10 +35,12 @@ export interface LogQueryOptions { /** * 构造 `git log` 参数向量(不含 `log` 字面量,host 拼接)。 - * 顺序:`--topo-order` → 范围 → 分页 → 服务端过滤 → `--format` → pathspec(末尾)。 + * 顺序:`--author-date-order` → 范围 → 分页 → 服务端过滤 → `--format` → pathspec(末尾)。 */ export function buildLogArgs(filter: LogFilter | undefined, scope: LogScope, opts: LogQueryOptions): string[] { - const args: string[] = ['--topo-order']; + // 排序键须与视图日期列(row.authorDate)对齐;lane 算法仅依赖「子在父之上」不变量, + // --author-date-order 与 --topo-order 均满足,且额外按作者日期倒序、跨支正确交错(详见文件头)。 + const args: string[] = ['--author-date-order']; if (scope === 'checkpointer') { // 原始完整视图:遍历 refs/ 全部引用,含 conductor 等工具的 checkpoint 快照与归档头。 args.push('--all'); diff --git a/tests/unit/log-query.test.ts b/tests/unit/log-query.test.ts index 2c95b28..9627896 100644 --- a/tests/unit/log-query.test.ts +++ b/tests/unit/log-query.test.ts @@ -5,8 +5,11 @@ import type { LogFilter } from '../../src/engine/log/log-filter'; const noFilter: LogFilter = {}; describe('buildLogArgs — 顺序与必选项', () => { - it('--topo-order 置首(lane 算法依赖拓扑序)', () => { - expect(buildLogArgs(noFilter, 'all', { maxCount: 300 })[0]).toBe('--topo-order'); + it('--author-date-order 置首(lane 算法依赖「子在父之上」;作者日期倒序与视图日期列对齐、跨支正确交错)', () => { + const args = buildLogArgs(noFilter, 'all', { maxCount: 300 }); + expect(args[0]).toBe('--author-date-order'); + // 回归护栏:不得回退到 --topo-order(否则旁支提交会被成块挤到末尾,致日期列回跳) + expect(args).not.toContain('--topo-order'); }); it('scope=all 用 --branches --tags --remotes(排除工具注入的内部引用);不含 --all', () => { @@ -41,8 +44,9 @@ describe('buildLogArgs — 服务端过滤翻译', () => { it('空 author/grep 不产生 flag', () => { const args = buildLogArgs({ author: ' ', grep: '' }, 'all', { maxCount: 300 }); - expect(args.some((a) => a.startsWith('--author'))).toBe(false); - expect(args.some((a) => a.startsWith('--grep'))).toBe(false); + // 用 `--author=` / `--grep=` 精确前缀,避免与既有的 `--author-date-order` 排序 flag 误判 + expect(args.some((a) => a.startsWith('--author='))).toBe(false); + expect(args.some((a) => a.startsWith('--grep='))).toBe(false); }); it('path 以 pathspec 形式置于参数末尾(--format 之后)', () => {