Skip to content

fix(git): keep git status fresh when git metadata changes externally - #53

Merged
1lck merged 4 commits into
1lck:preview/0.2.0from
Yager-42:feature/git-watch-context
Aug 12, 2026
Merged

fix(git): keep git status fresh when git metadata changes externally #53
1lck merged 4 commits into
1lck:preview/0.2.0from
Yager-42:feature/git-watch-context

Conversation

@Yager-42

@Yager-42 Yager-42 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #22

问题与根本原因

报告的现象:在终端执行 git commit/push 后,Lithe 的 Git 面板停留在旧状态,直到手动刷新。issue
已指出直接原因:外部提交改变 .git/indexHEADrefs,而这些文件系统事件被过滤掉了。

深入排查后,这个现象背后有四个架构层面的根本原因,本 PR 全部解决:

1. 监听根不覆盖 Git 元数据。 原有 watcher 只监听工作区根目录,而 Git 元数据经常位于工作区之外:linked worktree
的 gitdir/common dir、separate git-dir、以及位于父仓库 .git/modules/ 下的 submodule git 目录。即使 .git
在工作区内,其事件也会被隐藏路径规则过滤。→ 新增共享 Rust 命令 git.watchContext,解析并规范化仓库根目录、绝对 git
目录和 git common 目录(覆盖普通仓库、linked worktree、submodule、separate
git-dir,以及从嵌套子目录打开的仓库);MacDirectoryWatcher 现在监听多个逻辑根,并自动去重物理根。

2. 事件分类是单一的"工作区变化"模型。 旧实现把一切外部变化都当作工作区变化处理:一次微小的 Git 元数据写入(如
index 更新)要么被过滤导致 Git 从不刷新,要么(若不过滤)触发不必要的整树重扫。→ 引入结构化 DirectoryChangeBatch
分类:Git 元数据路径只触发 Git 状态刷新;可见工作区路径触发快照刷新;仓库范围内的隐藏路径(如 dist/)只刷新
Git;恢复类事件(RootChangedMustScanSubDirs、事件 ID 回绕、逻辑根被删除或重命名)重建 watcher 并整树重扫。

3. Git 刷新没有独立的事件通道,刷新期间到达的请求会被丢弃。 高频 Git 活动(外部 commit 后紧跟 index
写入)会让新请求在刷新进行中丢失,Git 操作冻结期间到达的事件也没有补偿。→ WorkspaceFeatureModel 新增专用的 Git
刷新状态机:pending 标记合并突发事件,刷新进行中到达的请求随后重放(GitFeatureModel
同步支持);begin/endGitOperationFreeze 期间累积的事件在结束时一次性 flush,按"恢复 > 工作区路径 > 仅
Git"的优先级顺序处理。

4. 初始化与生命周期缺陷。 watcher
只监听打开时已知的根,因此项目打开后才初始化的仓库永远不会被发现,从后台恢复后监听也可能失效。→
启动顺序改为:先启动工作区监听并加载快照,再解析 Git context 并无条件刷新一次
Git(由此发现打开后创建的仓库);应用回到前台时,每个打开的项目都会重新解析 context、重建 watcher 并刷新 Git。

另修复一个实现层面的隐患:FSEventStream 回调直接持有 watcher 实例的引用,watcher 重建时存在
use-after-free(完整测试套件曾因此随机崩溃)——回调改为持有弱引用的 CallbackContext,其生命周期由 stream 的
retain/release 管理。

边界(明确不包含)

  • 多仓库工作区:一个工作区同时管理多个独立 Git 仓库不在本设计内(与 feat(git): complete conflict handling and shelve workflows #9 的 D 项一致)。本 PR
    覆盖包含打开路径的单一仓库,包括其 worktree/submodule 元数据联动。
  • Windows:FSEvents 是 macOS 平台能力;本 PR 只交付 macOS 监听器。Windows 在共享 Rust 侧复用 git.watchContext
    契约(共享层已就绪),平台 watcher 按分层架构在 windows/ 下另行实现。
  • 不做 Git 钩子或轮询:刷新完全由文件系统事件驱动,不安装 git 钩子,也不定时轮询 Git。
  • 不做 .git 内部逐文件映射.git 下的任何变化都被视为"Git 状态可能已改变",通过 git status
    快照重算,不做增量 diff(git status 自带缓存,成本可控)。
  • 不支持无 FSEvents 的卷(网络挂载/NFS):此类挂载不产生事件,RootChanged
    标志会触发整树重扫作为兜底,但不保证实时。
  • Git 状态数据源与 UI 不变GitFeatureModel 仍消费 git.status 快照;本 PR 只改变"何时刷新"。

验证

  • swift test --disable-sandbox(133 个测试,含 linked worktree / submodule / separate git-dir / git init 发现 /
    仅 Git 事件路由的真实仓库观察测试)
  • swift build
  • cargo test --manifest-path rust/lithe-core/Cargo.toml(36 个单元 + 5 个 git.watchContext 集成测试)
  • scripts/verify-rust-core.sh
  • cargo fmt --manifest-path rust/lithe-core/Cargo.toml --check
  • git diff --check
  • scripts/verify-git-graph.sh / verify-shared-contracts.sh / verify-windows-boundaries.sh

已知基线问题

scripts/verify-core.sh 引用了已被移除的 Sources/Lithe/Core/Terminal/TerminalBuffer.swift(SwiftTerm
迁移时删除),且 scripts/verify-service-boundaries.sh 报告 Sources/Lithe/Core/RustWorkspaceOperations.swift
中既有的 FileManager 用法。两者在干净的 preview/0.2.0 上均可复现,与本 PR 无关。

Result

以下四组截图按机制各取一个代表场景,覆盖本次修改的四种核心机制;其余同机制场景(外部 checkout/stash/merge、separate git-dir、直接打开 submodule、打开仓库子目录)由自动化测试覆盖(见测试清单,全部通过):

  1. Git 元数据事件监听(普通仓库):外部 commit/push 改变 .git/indexHEADrefs(即 issue [Bug] 在终端执行 git commit/push 后,Lithe 没有感知 .git 状态变化 #22
    原文)。旧实现中这些路径被隐藏规则过滤,现在监听解析出的 Git 目录,元数据变化只触发 Git 状态刷新;
  2. 工作区外 gitdir 监听(worktree 等):linked worktree 的 index/HEAD
    位于打开的工作区之外(.git/worktrees/<name>/),旧实现监听范围不覆盖。现在用 git rev-parse 解析出绝对 git-dir 与
    common-dir 作为额外监听根,外部 git add 后面板自动更新;
  3. 事件分类路由(仓库范围内变化只刷 Git):父仓库感知 submodule HEAD 前进(gitlink
    状态)时不重扫工作区。DirectoryChangeBatch 把事件分为 Git 元数据/仓库内隐藏路径/可见工作区/恢复类四类路由;
  4. 初始化与生命周期发现:项目打开后外部执行 git init,面板自动从 No Git
    切换为仓库状态。打开时先监听工作区、加载快照后无条件刷新一次 Git,.git 创建事件触发重建 watcher。

Issue #22 场景:外部 commit 后 Git 面板自动刷新

在外部终端执行 git add + git commit 前后,Changes 面板的变化(未手动刷新):

截图 1(外部提交前,面板显示 README.md 已修改):

issue-22 before

截图 2(外部提交后,面板自动变为无改动):

issue-22 after

截图 3:

issue-22 extra

linked worktree 场景:index 在外部,git add 后面板自动从 Modified 变 Staged

工作区打开在 worktree 目录,git rev-parse --git-path index 显示 index 位于工作区之外;执行 git add 写入外部
index 后面板自动刷新:

截图 1(git add 前,面板显示 Modified):

worktree before

截图 2(git add 后,面板自动变为 Staged,附 rev-parse 输出证据):

worktree after

父仓库 + submodule 场景:外部 commit 使 submodule HEAD 前进,父仓库面板自动刷新

外部在 submodule 内执行 git commit 使其 HEAD 前进,父仓库的 Changes 面板自动刷新并显示 submodule
已更新,无需手动刷新:

截图 1(提交前,父仓库面板无变化):

submodule before

截图 2(外部提交后,面板显示 sub 更新):

submodule after

截图 3:

submodule extra

打开项目后 git init 场景:外部初始化仓库后,面板自动发现

工作区打开的是一个普通目录(显示 No Git);外部终端执行 git initgit addgit commit
后,面板自动发现新仓库并切换为正常 Git 状态,无需重开项目:

截图 1(git init 前,面板显示 No Git):

git-init before

截图 2(git init 并提交后,面板显示正常仓库状态):

git-init after

截图 3-5:

git-init extra 1

git-init extra 2

git-init extra 3

@Yager-42
Yager-42 requested a review from 1lck as a code owner August 11, 2026 04:49
@Yager-42
Yager-42 marked this pull request as draft August 11, 2026 04:57
@Yager-42
Yager-42 force-pushed the feature/git-watch-context branch from 78837e4 to 260c0f1 Compare August 11, 2026 05:16
@Yager-42
Yager-42 marked this pull request as ready for review August 11, 2026 05:33
@xiaoyumuxi

Copy link
Copy Markdown
Collaborator

这边个人文档不建议合入分支呢

@Yager-42
Yager-42 force-pushed the feature/git-watch-context branch from 260c0f1 to b7e0f98 Compare August 12, 2026 00:51
Comment thread Sources/Lithe/Application/WorkspaceFeatureModel.swift
@1lck

1lck commented Aug 12, 2026

Copy link
Copy Markdown
Owner

本 PR 新增了公开命令 git.watchContext,但共享 Rust Core 契约尚未同步。请在 shared/contracts/rust-core-api.md 记录 { "root": string } 请求、非仓库返回 null,以及 repositoryRootgitDirectorygitCommonDirectory 三个绝对路径字段。

Add git.watchContext command to the Rust core that resolves the
repository root, git directory, and git common directory as
canonical absolute paths. The Swift bridge exposes these through
GitService and the directory watcher now observes the repository and
Git metadata roots in addition to the workspace root.

DirectoryChangeBatch routes filesystem events to workspace snapshot
refreshes or Git status refreshes so worktree changes refresh Git
without rescanning the whole tree, and Git metadata changes do not
reload the workspace. Recovery events rebuild the watcher and rescan
when watched roots change, and Git refreshes are coalesced through a
pending-flag state machine that keeps the final request when a
refresh is already running.

Projects now start observing the workspace immediately, resolve the
Git watch context afterward, and refresh Git unconditionally once
loaded, which also picks up a repository initialized after the
project was opened. The app re-resolves the watch context for every
open project when it becomes active again.
Add real-repository observation tests covering linked worktrees,
submodules, separate git directories, git init discovery, and git-only
event routing. Unit tests cover watch-root deduplication, event
classification, recovery state machines, and refresh coalescing.
Rust integration tests exercise the git.watchContext command contract
across ordinary, separate-dir, worktree, and submodule repositories.
watchRootsChanged/requiresFullRescan batches returned early and dropped their workspacePaths, so a merged FSEvents batch could skip document and snapshot refresh after recovery. Merge batch paths first, process them after a non-full recovery, and cover the watchRootsChanged + workspacePaths combination with a regression test.
git.watchContext is public surface shared by both products. Document its { root } request, the null response outside a repository, and the absolute repositoryRoot, gitDirectory, and gitCommonDirectory fields.
@Yager-42
Yager-42 force-pushed the feature/git-watch-context branch from 7fa4439 to d22ffca Compare August 12, 2026 11:43

@1lck 1lck left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed. The remaining full-rescan document refresh edge case is accepted as non-blocking for this PR.

@1lck
1lck merged commit bffea54 into 1lck:preview/0.2.0 Aug 12, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants