-
Notifications
You must be signed in to change notification settings - Fork 153
Git Workspace State
本页集中记录 Git、工作区、数据库和运行会话中最容易被异步时序放大的问题。共同原则是:状态必须有作用域、有版本、有事实来源;刷新不是一个无语义的“重新做一遍”。
#53 — git status 停留在旧快照
症状:用户在外部终端执行 commit/push 后,Lithe 的 Git 面板不变,直到手动刷新;linked worktree、submodule、separate git-dir 和项目打开后执行 git init 的场景更明显。
根因:旧 watcher 只监听工作区根目录;Git 的 index、HEAD、refs 可能位于工作区外,且 .git 相关路径又被隐藏规则过滤。所有文件事件都被当成一种“工作区变化”,导致要么 Git 元数据事件被丢弃,要么一次很小的 index 写入触发整树扫描。刷新进行中到达的事件也没有独立队列。FSEventStream 回调直接持有 watcher,在 watcher 重建时还存在 use-after-free 风险。
方案:
- 在共享 Core 增加
git.watchContext,解析仓库根、绝对 git-dir 和 common-dir,覆盖普通仓库、worktree、submodule 和 separate git-dir。 - 用
DirectoryChangeBatch把事件分为 Git 元数据、可见工作区、仓库隐藏路径和恢复类事件;只刷 Git 的事件不再触发整树扫描。 - 用 Git 专用刷新状态机合并 pending 事件;刷新或 Git 操作 freeze 期间到达的事件在结束时按优先级重放。
- watcher 由弱引用
CallbackContext接收回调,stream 管理上下文生命周期,避免重建时回调悬空。
可迁移经验:文件监听不是“监听一个目录然后刷新”;首先要确定真实数据所在的物理根和事件语义。事件被合并时必须说明哪些事件可丢弃、哪些事件必须 replay。
验证:PR 报告覆盖普通仓库、linked worktree、submodule、separate git-dir、外部 checkout/stash/merge、git init 以及事件路由测试;同时明确网络挂载和多仓库工作区不在该 PR 的范围内。
#546 — 工作区只显示第一个仓库的变化
症状:工作区根目录没有 .git,但下方有多个独立仓库时,Changes 面板可能只显示第一个仓库;同名文件的 diff、暂存或回滚还有落到错误仓库的风险。
根因:仓库发现逻辑分散在平台层,并存在深度/扫描范围假设;聚合后的文件只保留路径,没有保留它所属的仓库。多工作区根目录的旧扫描晚完成时,还可能覆盖强制刷新后得到的新缓存。
方案:新增共享 workspace.repositories,在 Rust Core 递归发现 .git 目录和文件,跳过 .git 元数据、不跟随目录符号链接、检查取消并稳定排序。前端聚合时为每个文件携带 repositoryPath 和仓库相对路径,所有 stage、rollback、diff、open 和索引操作先按仓库分组再执行;当前仓库的 branch/commit/conflict 也不再被其他仓库污染。旧扫描结果通过刷新版本/身份检查隔离。
可迁移经验:当一个集合元素需要在后续执行写操作时,展示模型不能只存“文件路径”;必须保留资源的 owner/context。发现、聚合、操作三层应共享同一主键。
#625 — 状态能读,history/branch 却报 Workspace does not exist
症状:#618 之后 getGitStatus 看似成功,但 history、branches、stashes、references 和 watch context 仍使用发现出来的仓库根并失败。
根因:Windows Path::canonicalize 返回 \\?\\C:\\... 形式;Core 序列化为 //?/C:/... 后,Windows 前端的 normalizer 又把它变成 /?/C:/...,路径身份因此失真。status 恰好使用了 git rev-parse 返回的普通路径,所以造成“一个 API 好、另一个 API 坏”的假象。
方案:在 Core 增加 simplified_canonical_path,在 root validation、repository discovery、containing-repository、worktree、watch context 和 status 等边界统一去掉 verbatim 前缀;Windows 前端的 normalizePath 保留防御性处理,并在契约中明确 repository root 不带该前缀。相对路径恢复时同时保护 drive-root 分隔符。
可迁移经验:路径规范化要在跨语言边界集中完成,并且所有产生/消费路径的 API 使用同一个函数。一个调用路径能工作不能证明路径契约正确。
#618 — Windows 打开项目时 Git 先于 workspace 就绪
症状:打开本地 Git 项目后,面板显示 无法加载 Git 数据 或 Workspace does not exist,但在项目目录执行 Git 命令正常。
根因:Git controller 在工作区 runtime 还没有完成路径/快照准备时就开始加载,queued refresh 也没有被 readiness 保护。
方案:Git loading 以 workspace readiness 为门禁;runtime 状态变化通知 scoped controller;queued refresh 只有在 ready 后才运行,并补充 ARM64 target/toolchain 的构建固定。这个修复解决了启动顺序问题,但随后 #625 证明仍需修复 canonical path 身份。
可迁移经验:启动门禁只能保证时序,不会修复数据格式。排查时要把“尚未可用”和“拿到的身份错误”拆成两个独立假设。
#606 — Git 查询失败把已有状态清空
症状:Windows Git 查询返回空值或失败时,原实现覆盖已有数据并显示“不是 Git 仓库”;暂存、提交和 history 入口随之不可用。
根因:空仓库列表、查询失败和真正的“没有仓库”没有结构化区分;不同范围的刷新也互相清掉错误提示或草稿。每次刷新都替换数组/对象引用,即使 observable 数据没有变化也会触发完整加载。
方案:区分 empty 与 failed,失败时保留 last-known-good;初始加载和刷新失败展示可重试入口,显式重试才清理 discovery cache。history 失败只更新 history 作用域的错误,不覆盖 working-tree 数据;相同查询复用数组/对象引用,只有可观察数据真正变化才通知。
回归重点:空值、异常、多仓库、恢复、history-only 失败、缓存保留和 no-op refresh。PR 描述还注明 Windows 原生测试/实机复现受环境限制,因此这些验证边界不应被二次总结成“全部在 Windows 实机通过”。
#597 — 单文件 stage/unstage 的反馈与刷新竞态
症状:Windows 端缺少单文件快速暂存/取消暂存;批量操作跨仓库时可能提前释放 pending、刷新覆盖新状态或在一个仓库失败后阻断其他仓库。
根因:操作状态以全局或单次请求为粒度,无法表达同一文件重叠操作、跨仓库独立完成和“读取期间又发生写入”。普通暂存还会触发全量 Git reload。
方案:按解析后的 repository path 分组,同仓库写入串行、不同仓库独立;pending 按路径保留到该路径所有操作完成。暂存后只刷新 working-tree 范围,并把读取期间到达的新写入合并成下一次刷新。初始/全量/工作区刷新在真实读取开始时分配版本,旧的纯工作区结果不能覆盖新状态,但仍可补齐 history/branch/stash。
验证边界:PR 的原始提交有测试与 Windows GUI 验收记录,但后续审查提交明确写着新增回归用例尚未运行。本条目保留这个事实,说明“设计正确”和“这一次代码已验证”是两件事。
#575 — “丢弃更改”确认后什么也没发生
症状:macOS 用户确认丢弃更改后,操作偶尔不执行。
根因:确认框关闭回调先把 pending discard 清空,随后启动的异步任务再读取该状态,拿到 nil 后提前返回。
方案:在确认按钮闭包中同步捕获已确认的文件或 diff hunk,通过 AppModel 显式传入 Git feature model;异步任务只使用这份不可变输入,完成后按成功/失败刷新并提示。
可迁移经验:UI 状态是“待确认请求”,不是异步操作的输入参数。所有 destructive action 都应在确认边界复制值,不能让后续任务依赖会被视图销毁清理的 @State。
#593 — revert 后 Git Log 不刷新
症状:clean 仓库执行 revert 后,Git Log 仍显示旧列表,且不会自动选中新 HEAD。
根因:原 refreshGit() 只在 branch、working-tree、stash 或 operation snapshot 变化时刷新 history;clean 仓库创建 revert commit 时这些字段可能完全不变。即使数据重新读取,旧 selection 也不会自动指向新提交。
方案:在 merge/rebase/cherry-pick/revert 前记录历史版本;成功后调用带 since 的 metadata refresh,必要时显式刷新可见 history、切回当前检出分支并选择新 HEAD。失败或冲突保留用户当前范围和选择,避免成功路径的自动定位污染失败路径。
#85 — 全选只是装饰,暂存反馈还会跳动
症状:Changes 节头的“全选”图标并未执行暂存;文件勾选需要等 Git 命令和刷新结束才更新,未跟踪文件暂存后还从原分组消失。
根因:节头是一个折叠按钮,勾选只是装饰;命令完成前没有 pending state,异步返回的旧 status 会覆盖用户刚点下的状态;分组按 isUntracked 而不是 added 状态构造。
方案:将折叠、全选和标题拆成独立语义控件;按文件保存 optimistic staging state,后台 stage/unstage 完成后与 status 快照 reconcile,失败只回滚对应路径;刷新期间的 stale snapshot 不清除 pending;已暂存未跟踪文件按 added 语义留在正确分组。
#114 — tag checkout 失败却显示成功
症状:Windows tag checkout 总是报告成功,并可能触发 Git changed 事件。
根因:共享 Core 返回 { output, exitCode },Windows adapter 却无条件生成 success: true,把失败路径改写成成功路径。
方案:adapter 根据 exitCode 生成 success,保留 trimmed output 作为消息;只有适配后的成功结果才触发后续 Git refresh。这个案例很小,但说明 adapter 不是格式转换器,而是跨边界契约的完整实现者。
#101 — branch checkout/delete 的引用和参数不匹配
根因:前端只传短分支名,而 Core 要求 refs/heads/<branch>;缺少 referenceKind: local;adapter 也没有把原始 { output, exitCode } 变成带 success/hasChanges 的前端结果。
方案:在 platform adapter 集中补全 local ref、传递 reference kind、翻译 preflight 和退出码,并让 checkout 前置检查复用现有 stash-retry 流程。
#62 — 多模块项目打开后崩溃
症状:嵌套 .worktree/.worktrees 检出目录被重复扫描,生成重复运行配置并最终崩溃。
根因:扫描器没有把嵌套 worktree 视为派生/忽略目录;同名主类和配置 ID 又没有跨模块的稳定区分。
方案:Swift、Rust Core、Windows 统一忽略嵌套 worktree;Core 保留同名主类在不同模块的独立配置,Swift 侧再做防御性去重。该 PR 同时记录了现有大小写规则和配置 ID 稳定性的未解决边界。
#39 — 刷新后表、Redis、Nacos 仍显示旧数据
根因:刷新连接只更新了连接级数据,没有重新加载当前打开的 table/Redis key/Nacos 列表;并发请求返回顺序不受保护,失败时又保留了旧数据。
方案:刷新连接时重载当前 workspace;写操作后刷新对应列表/详情;给并发刷新加 stale-response guard,失败清空不可再信任的数据并保留可操作的错误状态。
- 每个异步结果都带有 workspace/session/request/version identity。
- 取消只是不再等待,不能代替回写前的 stale 检查。
-
empty、failed、not-ready、ready、stale是不同状态,不能只用nil。 - 写操作的乐观状态必须按资源粒度保存,并能和真实快照 reconcile 或局部 rollback。
- UI 关闭、切换、销毁前,把用户已确认的值和需要继续执行的输入复制给任务。
- refresh 应说明范围:working-tree、history、branch、stash、discovery,而不是无差别全量重载。