☰
MooTool Next Compose Vault Git 提交/推送前刷新编辑器:`GitEditorFlushPolicy` 的 flush 语义与实现
2026/10/15 2:38:02 网站建设 项目流程
  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

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

导读

本文深入剖析 MooTool Next Compose 版本(next-compose模块)中 Vault 仓库的 Git 操作前编辑器刷新机制:在commit(提交)、push(推送)等写操作执行之前,VaultGitDialog会强制调用onFlush()将 JSON Vault / 随手记(QuickNote)编辑器的未保存缓冲区写盘,确保 Git 操作针对的是磁盘上的真实内容而非内存草稿。读完本文,你将掌握GitVaultFlushAction枚举与GitEditorFlushPolicy的判定策略、各 Git 动作的 flush/刷新矩阵、与 Electron 版prepareGitAction的对齐关系,以及相关的验证测试与演进历史。


一、背景:为什么 Git 操作前必须先"刷新"编辑器

MooTool 的 Vault(JSON 片段库与随手记)采用"编辑器缓冲区 + 磁盘文件"双轨模型。用户在编辑器里输入的内容默认保存在内存中(session.editor.text),仅在自动保存(250ms 空闲)、用户显式保存或执行特定操作时才落盘。而 Vault Git 面板(VaultGitDialog)通过GitEngine直接执行真实的gitCLI 命令,Git 本身只认磁盘文件:

  • git add暂存的是磁盘内容;
  • git diff、git status展示的是磁盘与 HEAD 的差异;
  • git commit打包的是暂存区(staging area)内容。

因此,如果用户在编辑器中修改了笔记但尚未落盘,此时直接提交,Git 看到的仍是被git add过的旧内容,提交产物与用户所见缓冲区不一致——这正是 DIFF-184 要解决的问题:在 commit / push 这类依赖工作区内容的操作执行前,先通过onFlush()把缓冲区刷新到磁盘,保证"所见即所提交"。

关联文档:DIFF-184:Vault Git 提交/推送前刷新编辑器


二、核心设计:GitVaultFlushAction+GitEditorFlushPolicy

整个 flush 策略被集中封装在两个类型中,位于领域层:

  • GitEditorFlushPolicy.kt

2.1GitVaultFlushAction:Vault Git 操作的动作枚举

enum class GitVaultFlushAction { Pull, Discard, AbortMerge, ResolveConflict, ContinueOperation, Commit, Push, }

这个枚举覆盖了 Vault Git 面板的全部工作区动作。值得注意的是它只包含会改变工作区或依赖工作区内容的操作,而fetch、init、配置 remote(setRemote)等操作不携带GitVaultFlushAction(调用时传入null),因此不触发额外 flush。

2.2shouldFlushEditor:执行前是否需要刷新缓冲区

fun shouldFlushEditor(action: GitVaultFlushAction?): Boolean = when (action) { GitVaultFlushAction.Discard, GitVaultFlushAction.AbortMerge, GitVaultFlushAction.ResolveConflict -> false GitVaultFlushAction.Pull, GitVaultFlushAction.ContinueOperation, GitVaultFlushAction.Commit, GitVaultFlushAction.Push -> true null -> false }

判定规则可以整理成一张清晰的决策表:

操作执行前 flush 编辑器?成功后台后刷新 Vault 树?
Commit(提交)✅ 是❌ 否
Push(推送)✅ 是❌ 否
Pull(拉取)✅ 是✅ 是
ContinueOperation(继续合并/变基)✅ 是✅ 是
Discard(丢弃更改)❌ 否✅ 是
AbortMerge(中止合并)❌ 否✅ 是
ResolveConflict(解决冲突)❌ 否✅ 是
null(fetch / init / setRemote / 面板打开)❌ 否❌ 否

2.3refreshesVaultAfterSuccess:操作成功后是否刷新 Vault 树

fun refreshesVaultAfterSuccess(action: GitVaultFlushAction?): Boolean = action in setOf( GitVaultFlushAction.Pull, GitVaultFlushAction.Discard, GitVaultFlushAction.AbortMerge, GitVaultFlushAction.ResolveConflict, GitVaultFlushAction.ContinueOperation, )

这条规则回答了一个对称的问题:哪些动作会改变磁盘上的 Vault 文件内容,从而需要刷新打开中的编辑器文件。

  • 会刷新:Pull(远端覆盖本地)、Discard(丢弃本地改动)、AbortMerge(中止合并)、ResolveConflict(写入 ours/theirs 解析结果)、ContinueOperation(合并/变基推进后工作区被 Git 改写)。这些动作都可能让当前打开文件的磁盘内容与缓冲区不一致,因此成功后需调用onVaultRefresh()重新加载。
  • 不会刷新:Commit与Push本身不改变文件内容(只是打快照、上传),提交成功后afterSuccess = { onVaultRefresh() }刷新的是变更列表而非重新加载文件内容。

从源码结构看,这种"刷新树/刷新文件"的双通道设计正是为了让 Git 面板状态(changes 列表、冲突数、ahead/behind)与 Vault 树、打开文件始终保持一致。


三、集成点:VaultGitDialog.runAction中的 flush 时序

flush 策略的真正落地在 Git 面板对话框 VaultGitDialog.kt 的runAction函数中:

fun runAction( workingTree: GitVaultFlushAction? = null, afterSuccess: (() -> Unit)? = null, remoteOverrideAfterReload: String? = null, block: () -> GitActionResult, ) { scope.launch { busy = true try { if (GitEditorFlushPolicy.shouldFlushEditor(workingTree)) { val flushed = onFlush() if (flushed != null) { notifyVaultGitFlushBlocked(container, flushed) return@launch } } val result = withContext(Dispatchers.IO) { block() } if (result.success) { container.toastSuccess(container.t("git.done")) afterSuccess?.invoke() if (GitEditorFlushPolicy.refreshesVaultAfterSuccess(workingTree)) { onVaultRefresh() } } else { notifyVaultGitActionFailure(container, result) if (workingTree == GitVaultFlushAction.Pull) { onVaultRefresh() } else { return@launch } } // ... 重新拉取 status / history,同步 remote 字段,触发 onGitStatusChanged() } catch (error: Exception) { notifyVaultGitPanelFailure(container, error) } finally { busy = false } } }

这里的关键时序(flush → 执行 → 结果处理)可以概括为:

  1. 前置 flush:若shouldFlushEditor(workingTree)为true,调用onFlush()。onFlush()返回String?:返回null表示刷新成功(或缓冲区本就干净,无需写盘),可继续执行 Git 操作;返回非null表示刷新被阻塞(如文件未命名、写盘失败),此时通过notifyVaultGitFlushBlocked弹出错误 toast 并中止 Git 操作(return@launch)。
  2. 后台执行:block()在Dispatchers.IO线程池中执行真实 Git 命令(GitEngine封装)。
  3. 成功处理:toast 成功提示 →afterSuccess?.invoke()→ 若refreshesVaultAfterSuccess则onVaultRefresh()。
  4. 失败处理:弹出失败 toast;Pull操作失败时仍刷新 Vault(因为 pull 可能已部分改变工作区),其余动作直接中止。
  5. 收尾:重新拉取GitEngine.status(root)与GitEngine.history(root),同步 remote 字段,并调用onGitStatusChanged()同步工具栏上的 Git 变更数角标(对齐 Electron 的refreshGitChangeCount)。

3.1 各按钮的挂载方式

面板中每个操作按钮都通过workingTree参数声明自己的动作类型(VaultGitDialog.kt):

按钮workingTree参数底层 Git 操作
提交(commit)GitVaultFlushAction.CommitGitEngine.commit(root, message, identity())
推送(push)GitVaultFlushAction.PushGitEngine.push(root, token = token())
拉取(pull)GitVaultFlushAction.PullGitEngine.pull(root, token = token())
继续(continue)GitVaultFlushAction.ContinueOperationGitEngine.continueOperation(root, identity())
丢弃(discard)GitVaultFlushAction.DiscardGitEngine.discard(root, discardPath)
中止(abort)GitVaultFlushAction.AbortMergeGitEngine.abortMerge(root)
解决冲突(ours/theirs)GitVaultFlushAction.ResolveConflictGitEngine.resolveConflict(root, path, "ours"/"theirs")
拉取远端(fetch)不传(null)GitEngine.fetch(root, token = token())
初始化(init)不传(null)GitEngine.init(root, identity())
保存/删除 remote不传(null)GitEngine.setRemote(root, committed)

fetch/init/setRemote不传workingTree,shouldFlushEditor(null)返回false,因此不会在操作前打扰正在编辑的缓冲区——这与"打开面板时load()不 flush"共同构成了"只在必要时刷新"的克制原则。


四、onFlush()的两个实际实现:JSON Vault 与随手记

VaultGitDialog本身不关心编辑器细节,onFlush是外部传入的回调(onFlush: () -> String?)。项目里两个调用方分别注入各自的刷新逻辑。

4.1 JSON Vault:jsonGitFlushBeforeAction

入口在 JsonScreen.kt,实现于 JsonVaultSession.kt:

internal fun jsonGitFlushBeforeAction( container: AppContainer, session: JsonSession, monitor: VaultRevisionMonitor?, onConflict: (VaultConflictState) -> Unit, ): String? { val editorHasUserDraft = session.editor.text.isNotBlank() && session.editor.text != JsonSession.SAMPLE_JSON JsonVaultFooterPresentation.gitUntitledBlockKey(session.currentFile, editorHasUserDraft)?.let { return container.t(it) } if (JsonVaultFooterPresentation.gitFlushSkipsWhenClean(session.currentFile, session.isVaultEditorDirty())) { return null } return saveJsonVault(container, session, monitor, showToast = false, onConflict = onConflict) .fold( onSuccess = { null }, onFailure = { it.message ?: container.t("json.notice.failed") }, ) }

它的三层守卫逻辑(对齐 ElectronprepareGitAction):

  1. 未命名文件守卫:若currentFile为空且编辑器里有非示例的用户草稿,返回未命名提示文案,阻止 Git 操作——因为一个未命名文档无法确定写盘目标。
  2. 干净文档跳过:已命名文件且editor.text == savedText(即缓冲区与磁盘一致,例如刚经过 250ms 自动保存),直接返回null,不写盘,避免多余 IO 与不必要的 toast 打断(对应 DIFF-332)。
  3. 脏内容才写盘:调用saveJsonVault(..., showToast = false),静默写盘避免 Git 操作前出现重复的"保存成功"提示;写盘失败则返回错误文案,Git 操作随之中止。

4.2 随手记(QuickNote):quickNoteGitFlushBeforeAction

入口在 QuickNoteScreen.kt,实现于 QuickNoteVaultContext.kt:

internal fun quickNoteGitFlushBeforeAction( container: AppContainer, session: QuickNoteSession, vault: NoteVault, monitor: VaultRevisionMonitor?, onConflict: (VaultConflictState) -> Unit, ): String? { QuickNoteVaultFooterPresentation.gitUntitledBlockKey( session.currentFile, session.editor.text, QuickNoteSession.SAMPLE, )?.let { return container.t(it) } if (QuickNoteVaultFooterPresentation.gitFlushSkipsWhenClean(session.currentFile, quickNoteDirty(session))) { return null } if (!quickNoteVaultSaveIfNeeded(container, session, vault, monitor, onConflict)) { return session.error.ifBlank { container.t("quickNote.saveFailed") } } return null }

与 JSON 版本结构一致,唯一差异在脏判定:随手记使用quickNoteDirty(session),它会同时考虑正文与 metadata(对应 DIFF-366),而 JSON 侧使用session.isVaultEditorDirty()。两者共同保证了 "Git flush 跳过干净文档" 的语义在两条产品线上的一致性。

4.3 与 Electron 版prepareGitAction的对齐

Compose 的这份策略并非凭空设计,而是与 Electron 版本逐点对齐。Electron 端 JSON Vault 面板的对照实现位于 JsonVaultPanel.tsx:

async function prepareGitAction(action: VaultGitAction): Promise<boolean> { if (!dirty) return true if (action === 'pull' || action === 'continue-operation') return saveSelected() return true }

对照结论:

  • 对齐点:pull/continue-operation都刷新编辑器;!dirty(缓冲区干净)时放行不写盘。
  • 增强点(Compose 独有):Compose 在commit/push前也刷新编辑器(Electron 只在 pull/continue 前保存)。这是 DIFF-184 的关键增强——Git 提交/推送前强制 flush,保证"staged files 与编辑器缓冲区一致"。GitEditorFlushPolicy源码注释明确记录了这一点:"Electron prepareGitAction saves before pull/continue only. Compose additionally flushes before commit/push so staged files match the editor buffer."
  • 共同边界:discard/abort-merge/resolve-conflict在两侧都不刷新编辑器(DIFF-110 对齐结论)。

五、被刷新之后:GitEngine的真实 Git 命令链

onFlush()保证磁盘内容就绪后,VaultGitDialog.runAction里的block()才真正执行。底层由 GitEngine.kt 封装(全部通过ProcessBuilder调用gitCLI,并以root.toAbsolutePath()为 key 加锁串行化,避免并发 Git 进程冲突)。

各操作对应的核心命令(有源码依据):

GitEngine 方法执行的核心命令前置校验
commit→commitLocked(L467-L485)git add --all后git commit -m <message>消息非空(截断 300 字符)、仓库已初始化、无进行中合并、存在变更
push(L255-L265)git push -u origin HEAD仓库已初始化、无进行中合并/冲突、存在 origin remote
pull(L242-L253)git pull --no-rebase origin仓库已初始化、非 merging、无冲突、配置了 remote
fetch(L238-L240)git fetch --prune origin需 origin remote
discard(L267-L287)未跟踪文件用git clean -f;已跟踪文件用git restore --staged --worktree --source=HEAD(失败回退git reset+git checkout)路径存在且在工作区变更中

其中 commit 的身份参数值得注意(identityArgs,L487-L491):

private fun identityArgs(identity: GitIdentity): List<String> = listOf( "-c", "user.name=${identity.name.ifBlank { "MooTool Next Compose" }}", "-c", "user.email=${identity.email.ifBlank { "next-compose@local" }}", "-c", "commit.gpgsign=false" )

即每个 commit 都以-c注入user.name/user.email(从设置中的vault.gitUsername派生,未配置时使用默认占位值),并显式关闭 GPG 签名(commit.gpgsign=false),避免本机全局 Git 配置干扰 Vault 提交的确定性。


六、演进历史:从"无条件 flush"到"策略化按需 flush"

这一节整理 flush 语义在next-compose/docs/diff系列文档中的演进脉络,理解它有助于把握当前设计的边界:

  1. DIFF-110「Git 面板 P5 与 flush 语义」:确立discard/abort-merge/resolve-conflict执行前不再调用onFlush(对齐 ElectronprepareGitAction);pull/continue-operation仍先 flush。参见 110-git-panel-p5-flush.md。
  2. DIFF-184「提交/推送前刷新编辑器」(本文主体):把 flush 面扩展至commit/push,并将全部规则收敛到GitEditorFlushPolicy+GitVaultFlushAction。原文档当时还提到"打开面板时load()仍会 flush 一次"。
  3. DIFF-198「Git 面板打开/刷新不再隐式 flush」:后续修订撤销了load()中的无条件onFlush()——Electron 的load仅拉取 status/history,不调用prepareGitAction;Compose 对齐后,打开面板、点刷新按钮都不再触发保存/toast,只在真正执行 pull / continue / commit / push 时才 flush。参见 198-git-panel-load-no-flush.md。
  4. DIFF-332「JSON Vault Git flush 跳过已保存文件」与DIFF-366「随手记 Vault Git flush 跳过干净文档」:进一步优化onFlush内部实现,缓冲区与磁盘一致时跳过写盘、脏时才静默保存,避免多余 IO 与重复 toast。参见 332-json-git-flush-skip-clean.md 与 366-quicknote-git-flush-skip-clean.md。

最终形成的设计原则可以概括为:flush 只发生在"即将执行依赖磁盘内容的写操作"之前,且只在"缓冲区确实脏"时落盘;不刷新编辑器的操作(discard/abort/resolve)与不依赖工作区内容的操作(fetch/init/setRemote/面板打开)一律不打扰用户。


七、验证方式:单元测试与全量桌面测试

DIFF-184 给出的验证手段有两层,均可在仓库中复现。

7.1GitEditorFlushPolicyTest

策略的单元测试位于 GitEditorFlushPolicyTest.kt,包含三个用例:

@Test fun flushBeforePullCommitPushNotBeforeDiscard() { assertTrue(GitEditorFlushPolicy.shouldFlushEditor(GitVaultFlushAction.Pull)) assertTrue(GitEditorFlushPolicy.shouldFlushEditor(GitVaultFlushAction.Commit)) assertTrue(GitEditorFlushPolicy.shouldFlushEditor(GitVaultFlushAction.Push)) assertFalse(GitEditorFlushPolicy.shouldFlushEditor(GitVaultFlushAction.Discard)) } @Test fun vaultRefreshAfterPullNotAfterCommit() { assertTrue(GitEditorFlushPolicy.refreshesVaultAfterSuccess(GitVaultFlushAction.Pull)) assertFalse(GitEditorFlushPolicy.refreshesVaultAfterSuccess(GitVaultFlushAction.Commit)) assertFalse(GitEditorFlushPolicy.refreshesVaultAfterSuccess(GitVaultFlushAction.Push)) } @Test fun fetchAndInitDoNotImplicitlyFlush() { assertFalse(GitEditorFlushPolicy.shouldFlushEditor(null)) }

三个用例恰好覆盖了策略决策表的三个关键分支:要 flush 的动作、成功后要刷新 Vault 的动作、不隐式 flush 的动作(null),构成对GitEditorFlushPolicy的完整行为契约。

7.2 全量桌面测试

./gradlew :composeApp:desktopTest --offline

在 DIFF-184 落地时全量桌面测试为342/342 全部通过(该数字为文档记录的当时结果)。此外,随手记侧的 flush 跳过逻辑还有独立的QuickNoteGitFlushTest(见 366-quicknote-git-flush-skip-clean.md)。


八、总结

GitEditorFlushPolicy+GitVaultFlushAction是 MooTool Next Compose 版本 Vault Git 能力中一条精密的"数据一致性防线"。它用一张 8 行决策表,解决了"编辑器缓冲区 vs 磁盘工作区"在不同 Git 操作前的同步问题:

  • 提交/推送前强制 flush(Compose 对 Electron 的增强),保证 staged files 与编辑器所见一致;
  • 拉取/继续操作前 flush,对齐 Electron 行为;
  • 丢弃/中止/解决冲突前不 flush,因为这些操作本身在改写工作区;
  • fetch/init/setRemote/打开面板不 flush,不打扰纯查看场景。

配合 JSON 与随手记两侧的"未命名守卫 + 干净跳过 + 静默写盘"实现,以及GitEngine的 CLI 封装与加锁串行化,这套机制在保证 Git 操作正确性的同时,把对编辑体验的打扰降到了最低。对希望理解 Vault 数据一致性设计或复刻类似"编辑器 + Git"集成方案的开发者,这份策略与源码就是一份可直接参考的完整范本。

  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

项目地址:https://gitcode.com/gh_mirrors/mo/MooTool
点击查看免费下载
上一篇:RedTeam-OffensiveSecurity深度解析:红队实战技术架构与攻防对抗艺术
下一篇:SO-VITS-SVC 5.0:3步实现专业级AI歌声转换的完整指南

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

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

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

立即咨询