☰
MooTool Next Compose Vault 自动 pull 的“干净工作树“守卫:与 Electron 行为对齐的 GitStatus 前置条件剖析
2026/10/15 3:05:37 网站建设 项目流程
  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

项目地址:https://gitcode.com/gh_mirrors/mo/MooTool
点击查看免费下载

本篇技术指南聚焦 MooTool Next Compose(next-compose)在 Vault Git 自动 pull 流程中引入的干净工作树(clean working tree)前置条件。Compose 版本此前仅依赖"编辑器无未保存修改",在本地存在未提交变更时仍可能自动执行git pull,与 Electron 版pullJsonVault/pullQuickNoteVault的严格行为不一致。本文以 DIFF-407 文档为主体,结合VaultGitAutoPull、GitEngine、VaultGitPullScheduler的源码与测试,完整还原这一守卫的判定条件、调用链与验证方式。读完本文,你将理解"何时才允许对 Vault 自动 pull"的完整判定逻辑,以及该判定如何与编辑器脏检查、pull 后 UI 刷新、间隔重置等机制协同工作。

背景:Compose 自动 pull 与 Electron 的行为落差

MooTool 的多端产品中,Electron 版的 JSON Vault 与随手记(QuickNote)Vault 均通过pullJsonVault/pullQuickNoteVault实现自动拉取远端变更。Electron 侧的执行前置条件非常严格,除了要求编辑器无未保存修改之外,还同时要求:

  • Git 工作区status.changes为空(本地无任何未提交变更);
  • 无 merge / 冲突状态(merging为空、冲突数为 0);
  • 已配置 remote(origin存在且 URL 非空)。

而 Compose 版此前的pullVault实现仅跳过了三种情况:不可用的仓库、没有配置 remote、处于merging状态。这意味着当用户在本地编辑了 Vault 文件但尚未提交时,Compose 依然可能发起自动 pull,从而与 Electron 行为产生不一致——这正是 DIFF-407 文档要解决的核心问题。

需要说明的是,DIFF-407 本身属于一份简洁的功能差异变更说明(diff note),本文写作时同时阅读了仓库中的实际实现代码与测试用例,以保证对判定条件、数据结构与调用链的描述准确对应源码。

核心守卫:VaultGitAutoPull.mayPullCleanWorkingTree

DIFF-407 引入的核心构件是VaultGitAutoPull单例对象,其完整实现位于 VaultGitAutoPull.kt:

object VaultGitAutoPull { fun mayPullCleanWorkingTree(status: GitStatus): Boolean { if (!status.available || !status.repository || status.remote.isBlank()) return false if (status.merging || status.conflicts > 0) return false if (status.changes.isNotEmpty()) return false return true } }

这个函数把 DIFF-407 描述的判定条件落成了代码,可归纳为四组、五个具体门槛,任一不满足即返回false(不允许自动 pull):

判定条件含义
环境可用性status.available为true本机 Git 可执行文件可用、git --version执行成功
仓库存在性status.repository为trueVault 根目录本身就是 Git 仓库(顶层目录校验通过)
remote 配置status.remote非空存在originremote,且git remote get-url origin返回了 URL
操作状态非merging且conflicts == 0当前不在 merge / rebase 流程中,也没有未解决的冲突文件
工作区状态status.changes为空本地无任何未提交变更(含未跟踪文件)

只有这五个门槛全部通过,函数才返回true。注意它与"编辑器脏检查"是两层独立防线:mayPullCleanWorkingTree只关心 Git 工作区本身是否干净,而编辑器是否有未保存修改由调度器(VaultGitPullScheduler)的hasUnsavedEditorChanges回调单独拦截,两者缺一不可。

判定数据从哪来:GitEngine.status与GitStatus结构

mayPullCleanWorkingTree收到的GitStatus并非凭空构造,而是由 Git 引擎实时探测生成。GitStatus的定义位于 GitEngine.kt:

data class GitStatus( val available: Boolean, val repository: Boolean, val version: String = "", val branch: String = "", val remote: String = "", val ahead: Int = 0, val behind: Int = 0, val changes: List<GitChange> = emptyList(), val conflicts: Int = 0, val merging: Boolean = false, val operation: String = "none" )

GitEngine.status(root)生成该结构的过程(GitEngine.kt)对应了守卫依赖的每一个字段:

  • 先执行git --version,失败则返回available = false,即守卫中的第一道闸门;
  • 执行git rev-parse --show-toplevel,并校验vaultRoot == topLevel,确保 Vault 根目录本身就是仓库根,否则repository = false;
  • 执行git status --porcelain=v1 -z --branch --untracked-files=all解析工作区变更列表(changes),其中--untracked-files=all保证了未跟踪文件也被计入——这正是"本地有未提交变更"判定不漏判的关键;
  • 执行git remote get-url origin得到remote字段;
  • 通过git rev-parse -q --verify MERGE_HEAD与rebase-merge/rebase-apply目录探测,确定operation(merge/rebase/none)与merging标志;
  • conflicts由changes.count { it.conflict }计算,其中GitChange.conflict通过 porcelain 状态码判定(U、AA、DD等冲突码)。

GitChange的定义(GitEngine.kt)包含path、originalPath、status(porcelain 码)、conflict四个字段,为后续冲突计数和文件级 diff 都提供了数据基础。从源码结构可以推断,只要任一被跟踪文件被修改、被新增(??)或处于冲突状态,changes就会非空,从而直接触发守卫短路、跳过 pull。

调用链:VaultGitPullScheduler→runScheduledVaultPull

DIFF-407 明确指出AppContainer.pullVault同时服务定时调度器与 JSON / 随手记两个 Vault。对应的调度器是 VaultGitCheckpointScheduler.kt 中定义的VaultGitPullScheduler:

class VaultGitPullScheduler( private val enabled: () -> Boolean, private val hasUnsavedEditorChanges: () -> Boolean, private val intervalMilliseconds: () -> Long, private val pull: () -> GitActionResult, private val now: () -> Long = { System.currentTimeMillis() } ) { private var lastPullAt = 0L private var running = false fun evaluate(): Boolean { val interval = intervalMilliseconds() if (running || !enabled() || interval <= 0L || hasUnsavedEditorChanges()) return false val current = now() if (lastPullAt != 0L && current - lastPullAt < interval) return false running = true return try { val result = pull() lastPullAt = current result.success } catch (_: Exception) { lastPullAt = current false } finally { running = false } } }

scheduler.evaluate()在工作流中承担"第一道闸门"的角色:开关未开启、间隔非法、或编辑器存在未保存修改(hasUnsavedEditorChanges()返回true)时直接跳过,连 Git 状态都不会探测;随后以lastPullAt做时间节流,满足间隔才真正调用pull回调。

在 AppContainer.kt 中,两个调度器被实例化并接上配置:

private val notePullScheduler = VaultGitPullScheduler( enabled = { _settings.value.vault.autoPullMinutes > 0 }, hasUnsavedEditorChanges = { sessionManager.quickNoteSession().isVaultEditorDirty() }, intervalMilliseconds = { _settings.value.vault.autoPullMinutes.coerceIn(0, 1440) * 60_000L }, pull = { runScheduledVaultPull(noteVault().root(), forJson = false) } ) private val jsonPullScheduler = VaultGitPullScheduler( enabled = { _settings.value.vault.autoPullMinutes > 0 }, hasUnsavedEditorChanges = { sessionManager.jsonSession().isVaultEditorDirty() }, intervalMilliseconds = { _settings.value.vault.autoPullMinutes.coerceIn(0, 1440) * 60_000L }, pull = { runScheduledVaultPull(jsonVault().root(), forJson = true) } )

从中可以看到与自动 pull 直接相关的用户配置项settings.vault.autoPullMinutes:

  • 单位与语义:分钟;0表示关闭自动 pull(调度器enabled直接返回false);
  • 取值范围:经coerceIn(0, 1440)约束在 0~1440 分钟(即最长一天一次);
  • 换算:在intervalMilliseconds中乘60_000L转为毫秒。同一配置也出现在 ElectronNextSettingsImport.kt 中(跨端导入时同样按 0~1440 收敛),并在设置界面 SettingsScreen.kt 中展示,i18n 文案为"自动拉取间隔(分钟,0 关闭)"。

runScheduledVaultPull是 DIFF-407 守卫的最终落地处(AppContainer.kt):

private fun runScheduledVaultPull(root: Path, forJson: Boolean): GitActionResult { val status = GitEngine.status(root) if (!VaultGitAutoPull.mayPullCleanWorkingTree(status)) { return GitActionResult(true, "skipped") } val result = GitEngine.pull(root, token = _settings.value.vault.gitToken) val after = GitEngine.status(root) if (VaultGitAutoPull.shouldNotifyVaultAfterPull(result, after)) { if (forJson) notifyJsonVaultTreeChanged() else notifyQuickNoteVaultTreeChanged() } return result }

完整时序如下:

  1. 调度器判定开关、间隔、编辑器脏状态,通过后进入runScheduledVaultPull;
  2. GitEngine.status(root)实时探测工作区状态;
  3. mayPullCleanWorkingTree执行干净工作树判定,不满足则返回GitActionResult(true, "skipped")——跳过不算失败,避免调度器将"条件不满足"误报为拉取错误;
  4. 满足条件才执行GitEngine.pull(root, token = ...),token 来自 Vault Git 设置;
  5. pull 完成后再次探测状态,由shouldNotifyVaultAfterPull决定是否通知 UI 刷新。

值得注意的是,GitEngine.pull本身也保留了与守卫一致的二次防御(GitEngine.kt):非仓库返回 "Git repository is not initialized",merging时返回 "Finish or abort the current merge/rebase before pulling",conflicts > 0时返回 "Resolve all conflicts before pulling",无 remote 时返回 "No origin remote is configured";实际执行的是git pull --no-rebase origin,远程操作走REMOTE_TIMEOUT_MS(120 秒)超时保护。由此可以推断,mayPullCleanWorkingTree在调度层提前过滤掉大部分无效场景,GitEngine.pull则兜底保证即使被绕过也不会在冲突/合并态下盲拉。

自动检查点与自动 pull 的分工

Vault 的自动 Git 行为由两个调度器并行协作,二者职责清晰:

  • VaultGitCheckpointScheduler(VaultGitCheckpointScheduler.kt)负责自动提交检查点:在用户活动空闲(idleMilliseconds)或窗口失活(inactiveMilliseconds)满足阈值时执行 commit(可能附带 push),其enabled对应vault.autoCommit,空闲/失活秒数均被coerceIn(5, 3600)约束;
  • VaultGitPullScheduler负责自动 pull,由本文所述的干净工作树守卫把关。

两者都以sessionManager.jsonSession().isVaultEditorDirty()/quickNoteSession().isVaultEditorDirty()作为hasUnsavedEditorChanges的来源,即编辑器脏检查统一由会话层提供。与之配套的还有 DIFF-201:该文档指出 Compose 早期实现仅比较随手记正文(editor.text != savedText),不包含 metadata(标题/字体/语法等)变更,会在仅改元数据时误判"不脏";结合本文可看出,isVaultEditorDirty的判定范围直接影响hasUnsavedEditorChanges的拦截效果,进而影响自动 pull 与自动提交的触发时机。

pull 之后:shouldNotifyVaultAfterPull与 UI 刷新

DIFF-407 引入的第二个函数shouldNotifyVaultAfterPull负责决定 pull 后是否通知 Vault UI 刷新(VaultGitAutoPull.kt):

/** Electron `pull*Vault`:`broadcast('*-vault:changed')` after success or merge/conflict state. */ fun shouldNotifyVaultAfterPull(pullResult: GitActionResult, afterStatus: GitStatus): Boolean = pullResult.success || afterStatus.merging || afterStatus.conflicts > 0

其语义与 Electron 的broadcast('*-vault:changed')对齐:pull 成功、或 pull 后进入merge/冲突态(例如远端改动与本地产物冲突导致 pull 失败但仓库进入合并流程),都应通知界面刷新;仅当 pull 失败且仓库仍干净时才不通知。

这一步由配套的 DIFF-408 补全了 UI 侧行为:AppContainer.notifyJsonVaultTreeChanged()/notifyQuickNoteVaultTreeChanged()各自递增一个 tick(AppContainer.kt),JSON / 随手记屏的LaunchedEffect监听该 tick,重新抓取 Vault 树 snapshot 并执行persistFilter()(含干净状态下重载当前文件、Git 角标 rev 刷新)。从源码结构看,该 tick 被自动 pull 与手动 Git 面板刷新共用,保证了两种触发路径下的 UI 行为一致。

间隔修改:resetIntervalClock让新周期立即生效

DIFF-407 的自动 pull 判定链还依赖调度器的间隔节流逻辑,而 DIFF-413 修正了一个相关细节:Electron 在设置变更时会clearInterval并重建定时器,使新间隔立即生效;Compose 的VaultGitPullScheduler以lastPullAt节流,若用户修改autoPullMinutes后仍沿用旧lastPullAt,则必须等完旧间隔的剩余时间才能执行下一次 pull。

解决方式是resetIntervalClock()将lastPullAt清零(VaultGitCheckpointScheduler.kt),并在AppContainer.updateSettings中当vault.autoPullMinutes、数据目录或 Vault 自定义路径发生变化时对两个 pull 调度器同时调用(AppContainer.kt)。配套测试 VaultGitPullSchedulerTest.kt 用假时钟验证了"重置后立即可再次 pull"的行为。

测试与验收

DIFF-407 指定了验收路径:A03 / F01-F04 Vault Git 相关功能、VaultGitAutoPullTest,以及离线运行全部桌面测试:

./gradlew :composeApp:desktopTest --offline

核心单元测试 VaultGitAutoPullTest.kt 用三个用例覆盖了守卫的全部分支:

@Test fun allowsPullWhenRepositoryClean() { assertTrue(VaultGitAutoPull.mayPullCleanWorkingTree(status())) } @Test fun skipsWhenLocalChangesOrConflictOrMerge() { assertFalse(VaultGitAutoPull.mayPullCleanWorkingTree(status(changes = 1))) assertFalse(VaultGitAutoPull.mayPullCleanWorkingTree(status(conflicts = 1))) assertFalse(VaultGitAutoPull.mayPullCleanWorkingTree(status(merging = true))) assertFalse(VaultGitAutoPull.mayPullCleanWorkingTree(status(remote = ""))) } @Test fun notifiesVaultAfterPullOnSuccessOrMergeState() { val clean = status() assertTrue(VaultGitAutoPull.shouldNotifyVaultAfterPull(GitActionResult(true, "ok"), clean)) assertTrue( VaultGitAutoPull.shouldNotifyVaultAfterPull( GitActionResult(false, "failed"), status(merging = true), ), ) assertFalse( VaultGitAutoPull.shouldNotifyVaultAfterPull( GitActionResult(false, "failed"), clean, ), ) }

测试内部的status()辅助函数展示了GitStatus各字段的默认构造方式(available = true、repository = true、remote 默认https://example.com/r.git),并以List(changes) { GitChange(...) }模拟本地变更。从中可以清晰看到测试矩阵:干净仓库放行、本地变更 / 冲突 / merge 中 / 无 remote 四种情形均拒绝,以及"pull 成功或处于 merge/冲突态时通知"的完整覆盖。

小结

DIFF-407 通过一个仅十余行的VaultGitAutoPull.mayPullCleanWorkingTree与调度链整合,消除了 Compose 与 Electron 在 Vault 自动 pull 上的行为差异。其设计要点可总结为:

  • 五重前置条件:Git 可用、是仓库、已配 remote、非 merge/冲突态、工作区无变更,全部满足才允许自动 pull;
  • 两层防线:调度器拦截编辑器脏(hasUnsavedEditorChanges),守卫拦截 Git 工作区脏,GitEngine.pull内部再兜底防御;
  • 跳过即成功:条件不满足时返回GitActionResult(true, "skipped"),不污染调度器的成功统计;
  • 通知时机明确:pull 成功或进入 merge/冲突态才通知 Vault UI 刷新(shouldNotifyVaultAfterPull),并由 DIFF-408 的 tick 机制落地;
  • 可测可回归:纯函数设计使判定逻辑可在无 Git 环境的 JVM 单测中完整验证,配合离线desktopTest保证回归安全。

对于希望深入 MooTool Next Compose Vault Git 机制的读者,建议按以下顺序继续阅读仓库代码:先看 VaultGitAutoPull.kt(守卫本体),再读 GitEngine.kt 的status()/pull()(状态探测与执行),随后对照 AppContainer.kt 的调度器装配,最后用两份单元测试验证行为边界。

  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

项目地址:https://gitcode.com/gh_mirrors/mo/MooTool
点击查看免费下载
上一篇:Get Shit Done:彻底终结AI编程上下文衰退的终极解决方案
下一篇:PDFPatcher完全指南:5个实战技巧快速解决PDF处理难题

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询