ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MooTool 的 Git Rebase 冲突产品走查与 pull 恢复链路:DIFF-617 切片源码级解析

MooTool 的 Git Rebase 冲突产品走查与 pull 恢复链路:DIFF-617 切片源码级解析
  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

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

MooTool 的 next-compose(Compose Multiplatform 桌面版)在 JSON Vault 的 Git 面板中实现了完整的 merge/rebase 冲突处理走查体系。本文以 DIFF-617 切片为核心,结合GitRebaseProductEvidenceFlowTest、GitMergeProductEvidenceFlowTest与GitEngine源码,完整拆解「pull --rebase 分叉 → 冲突文件自动选中 → resolve 提示 → continue 收尾 → push/pull 恢复」这条产品级验收链路,并给出可复现的测试命令与证据脚本,适合想要理解该产品走查设计或在其上做二次开发的读者。

一、DIFF-617 在做什么:从一条切片看 Vault Git 面板的验收闭环

DIFF-617 是 next-compose 项目中 A03(Vault Git)验收链上的一张切片,直接基线是 DIFF-616(工作区)。它的核心使命可以用一句话概括:锁定pull --rebase造成分叉并产生冲突后,产品主窗从"自动选中冲突文件"到"展示 resolve 走查提示",再到"rebase 完成、pull 与 push 一并恢复"的完整行为。

与之配套的是 merge 侧的镜像链路(对齐 DIFF-609 / 616),以及一个证据脚本mootool_evidence_print_git_rebase_product_hint的文案对齐。而 DIFF-616 中已经锁定过的「push/pull 禁用 Compose 帧211/ 捕获测试链」,在 DIFF-617 中被明确不重复,因此本文不会把它当作新内容,而是作为禁用态的背景前提来展开。

从项目目录看,该切片对应的实现与测试集中在:

  • 测试:GitRebaseProductEvidenceFlowTest.kt
  • 测试:GitMergeProductEvidenceFlowTest.kt
  • 呈现逻辑:GitMergeProductFlowPresentation.kt
  • 证据脚本:prepare-git-rebase-conflict-evidence.sh

二、冲突如何产生:pull 分叉的三种操作路径

产品走查的第一步是构造一个"真实的"冲突现场。MooTool 通过conflict.json这一个证据文件,在隔离的临时 Git 仓库里制造出"本地与远端分叉"的场景。下面是 rebase 与 merge 两条链路的构造对比(源码见 prepare-git-rebase-conflict-evidence.sh 与 prepare-git-merge-conflict-evidence.sh):

环节rebase 链路(§C)merge 链路(§B)
仓库初始化git init --bare建远端,再在 JSON Vault 目录git init -b main同左
基线提交{"side":"base"}提交并 push同左
远端新提交clone 副本改{"side":"remote"}提交并 push同左
本地新提交本地改{"side":"local"}提交同左
制造冲突git pull origin <branch> --rebase(预期失败)git pull origin <branch> --no-rebase(预期失败)
断言.git/rebase-merge或.git/rebase-apply存在.git/MERGE_HEAD存在

关键点在脚本末尾的校验函数(定义于 product-evidence-common.sh):

  • mootool_evidence_assert_git_merge_conflict:检查$vault_json/.git/MERGE_HEAD存在,并校验git diff --name-only --diff-filter=U(unmerged)非空;
  • mootool_evidence_assert_git_rebase_conflict:检查.git/rebase-merge或.git/rebase-apply目录存在,并同样校验 unmerged 路径非空。

这两条断言直接对应GitEngine.status对operation的判定逻辑(见下文第六节),形成"脚本造现场 + 引擎认现场"的双向锁定。

三、§C 自动选中:conflict.json 为何总是被优先选中

冲突发生后,Git 面板的变更列表里有冲突文件,但产品走查要求自动选中conflict.json,而不是让用户手工挑选。这一行为的核心函数是GitMergeProductFlowPresentation.autoSelectConflictPath:

fun autoSelectConflictPath( merging: Boolean, conflicts: Int, changes: List<Pair<String, Boolean>>, currentSelected: String, ): String? { if (!merging || conflicts <= 0) return null val conflictPaths = changes.filter { it.second }.map { it.first } if (currentSelected.isNotBlank() && conflictPaths.contains(currentSelected)) return null return preferredConflictSelectionPath(conflictPaths) }

逻辑很直白:

  1. 只有merging == true且conflicts > 0时才触发选中;
  2. 若用户当前已选中某个冲突路径,则不覆盖(尊重手动选择);
  3. 否则调用preferredConflictSelectionPath在多冲突场景里"钦定"conflict.json:
fun preferredConflictSelectionPath(conflictPaths: List<String>): String? { if (conflictPaths.isEmpty()) return null if (conflictPaths.contains(EVIDENCE_CONFLICT_FILE)) return EVIDENCE_CONFLICT_FILE return conflictPaths.first() }

EVIDENCE_CONFLICT_FILE = "conflict.json"是与证据脚本共用的常量。这意味着无论冲突文件有几个,只要证据文件在场,面板就会稳定地选中它,从而保证 §B/§C 产品走查的截图与验收结果可复现。测试中对应的断言是:

assertEquals(GitMergeProductFlowPresentation.EVIDENCE_CONFLICT_FILE, auto)

四、走查提示的切换:Resolve 与 Select 的语义边界

选中冲突文件后,状态区要给出正确的走查提示。DIFF-617 的核心点之一就是:rebase 冲突自动选中后提示必须是git.rebaseProductFlowResolve(而非未选中时的git.rebaseProductFlowSelect)。

判定函数productFlowHintKey完整实现了这个语义:

fun productFlowHintKey( merging: Boolean, conflicts: Int, selectedConflict: Boolean, operation: String = "merge", ): String? { if (!merging || conflicts <= 0) return null return if (operation == "rebase") { if (selectedConflict) "git.rebaseProductFlowResolve" else "git.rebaseProductFlowSelect" } else { if (selectedConflict) "git.mergeProductFlowResolve" else "git.mergeProductFlowSelect" } }

四个组合键对应四句 UI 文案(Translator.kt 中的中英文表):

i18n key中文文案英文文案
git.mergeProductFlowSelect产品走查 §B:在变更列表选中带「冲突」标记的文件(如 conflict.json)。Product walkthrough §B: select a conflicted file in the change list (e.g. conflict.json).
git.mergeProductFlowResolve产品走查 §B:对该文件使用「保留本地/远端」,冲突清零后点「继续合并」。Product walkthrough §B: resolve with ours/theirs, then Continue merge when conflicts reach zero.
git.rebaseProductFlowSelect产品走查 §C:在变更列表选中带「冲突」标记的文件(如 conflict.json)。Product walkthrough §C: select a conflicted file in the change list (e.g. conflict.json).
git.rebaseProductFlowResolve产品走查 §C:对该文件使用「保留本地/远端」,冲突清零后点「继续合并 / Rebase」。Product walkthrough §C: resolve with ours/theirs, then Continue merge / rebase when conflicts reach zero.

测试中锁定的是 resolve 分支:

assertEquals( "git.rebaseProductFlowResolve", GitMergeProductFlowPresentation.productFlowHintKey( merging = true, conflicts = 1, selectedConflict = true, operation = "rebase", ), "§C auto-select conflict.json should show resolve walkthrough hint", )

同时,操作区是否展示 ours/theirs 按钮由showResolveActions(merging, selectedConflict)决定,即merging && selectedConflict——只有既处于 merge/rebase 又选中了冲突文件时才可见,与提示键逻辑严格一致。

五、冲突期间的禁用矩阵:push / pull / commit / fetch 各有各的状态

这是 DIFF-616 已锁定、DIFF-617 在此基础上补齐 pull 的部分。冲突未清零前,四个动作的"可用性"是完全不同的(证据帧见 211/213/214/217)。

底层判定在 GitOperationPresentation.kt:

fun pullEnabled(remotePresent: Boolean, merging: Boolean): Boolean = remotePresent && !merging fun pushEnabled(remotePresent: Boolean, merging: Boolean): Boolean = remotePresent && !merging fun fetchEnabled(remotePresent: Boolean): Boolean = remotePresent fun commitEnabled(merging: Boolean, conflicts: Int, hasChanges: Boolean, messageTrimmed: String): Boolean = !merging && conflicts == 0 && hasChanges && messageTrimmed.isNotEmpty() fun continueOperationEnabled(merging: Boolean, conflicts: Int): Boolean = merging && conflicts == 0

UI 动作层在 GitVaultRemotePresentation.kt:

fun pullActionEnabled(persistedRemote: String, merging: Boolean, busy: Boolean, conflicts: Int = 0): Boolean = !busy && pullEnabled(persistedRemote, merging, conflicts) fun fetchActionEnabled(persistedRemote: String, busy: Boolean): Boolean = !busy && fetchEnabled(persistedRemote)

注意pullActionEnabled比pullEnabled多一个conflicts == 0约束:GitOperationPresentation.pullEnabled只要求"远端存在且非 merge/rebase 进行中",而 Vault 层进一步要求"未解决冲突为 0"。也就是说 rebase 进行中即使理论上满足!merging之外的其它条件,只要冲突仍在,pull 就保持禁用;fetch 则始终可用(对齐 ElectronVaultGitDialog顶栏行为)。

测试中的断言组合如下:

断言rebase 测试merge 测试说明
mergeContinueActionEnabled= false✓✓冲突未清时 continue 禁用
pushActionEnabled= false✓✓冲突期 push 禁用
pullActionEnabled= false✓✓冲突期 pull 禁用(DIFF-617 新增锁定)
commitActionEnabled= false✓✓冲突期 commit 禁用
fetchActionEnabled= true✓✓冲突期 fetch 仍可用
冲突清零后 continue = true✓✓resolve 完成后可继续

六、resolve → continue 的底层调用链:GitEngine 做了什么

UI 层的开关背后,是 GitEngine.kt 的一整套 git CLI 封装。走查链路对应的三个关键方法:

1.status判定 operation(L99-L136)

通过git rev-parse -q --verify MERGE_HEAD和.git/rebase-merge/.git/rebase-apply目录的存在性区分三种操作状态:

val operation = when { rebaseMerge || rebaseApply -> "rebase" merge.exitCode == 0 -> "merge" else -> "none" }

这解释了为什么证据脚本必须保证rebase-merge/rebase-apply(而非 MERGE_HEAD)存在——引擎正是靠它识别 rebase 状态。

2.resolveConflict执行 ours/theirs(L384-L396)

if (strategy != "ours" && strategy != "theirs") return@locked GitActionResult(false, "Invalid conflict strategy") ... val checkout = run(listOf("checkout", "--$strategy", "--", normalized), root, isolateConfig) ... val add = run(listOf("add", "--", normalized), root, isolateConfig)

即git checkout --ours/--theirs -- <path>+git add。一个关键细节:rebase 冲突与 merge 冲突的 ours/theirs 语义相反。rebase 测试注释明确写道:

Rebase 冲突:ours为 upstream(remote),theirs为 replay 的本地提交(对齐 git checkout --ours)。

这是因为 rebase 会把你的本地提交"重放到"远端之上,ours指向当前检出的远端基座,theirs指向被重放失败的本地提交。因此测试中 rebase 链路用"ours"解析出远端内容、merge 链路用"theirs"解析出远端内容,最后都断言文件内容contains("remote")。

3.continueOperation收尾(L426-L436)

val result = if (current.operation == "rebase") { run(identityArgs(identity) + listOf("-c", "core.editor=true", "rebase", "--continue"), root, isolateConfig) } else { run(identityArgs(identity) + listOf("commit", "--no-edit"), root, isolateConfig) }

rebase 走git -c core.editor=true rebase --continue(绕过编辑器),merge 走git commit --no-edit。入口前会再次校验conflicts > 0时拒绝继续,与continueOperationEnabled(merging && conflicts == 0)双保险。

七、完成后恢复:pull 与 push 同步回弹

DIFF-617 标题的"pull 恢复"落在测试的最后一段断言上。continueOperation成功后,GitEngine.status返回operation == "none"且merging == false,此时两个动作必须同时恢复:

assertTrue( GitVaultRemotePresentation.pushActionEnabled(done.remote, done.merging, busy = false, done.conflicts), "push re-enabled after rebase completes", ) assertTrue( GitVaultRemotePresentation.pullActionEnabled(done.remote, done.merging, busy = false, done.conflicts), "pull re-enabled after rebase completes", )

merge 测试尾部有完全相同的两段断言。这构成一个"进入冲突 → 按钮禁用 → 解析 → 继续 → 按钮恢复"的完整闭环,确保用户不会被滞留在半禁用状态。

八、证据脚本对齐:mootool_evidence_print_git_rebase_product_hint

product-evidence-common.sh中mootool_evidence_print_git_rebase_product_hint输出的手工走查步骤,与测试断言逐条对齐(§C):

# 1) F04 JSON Vault 打开 Git 面板 → 变更列表应自动选中 conflict.json # 2) 状态区应显示变基进行中(git.operationRebase)与 §C 走查提示 # (自动选中 conflict.json 时为 git.rebaseProductFlowResolve) # 3) 对 conflict.json 使用 ours/theirs → 冲突计数归零 # 4) rebase 进行中 push/pull 应禁用、fetch 仍可用 # (GitVaultRemotePresentation;Compose 帧 211/214)、 # 冲突未清时「提交」应禁用(帧 213 / commitEnabled) # 5) 「继续合并 / Rebase」完成 rebase(Compose 帧 207;仍须 runDistributable 手工 PNG)

状态区的"变基进行中"文案由GitOperationPresentation.inProgressMessageKey提供:rebase -> "git.operationRebase"(中文文案"Rebase 进行中",见 Translator.kt)。脚本输出中还附带mootool_evidence_assert_git_rebase_conflict校验与MOOTOOL_COMPOSE_DATA_DIR数据目录指引,方便直接在runDistributable中复现手工截图。

九、验证与复现:跑通这条验收链

DIFF-617 官方验证命令为:

./gradlew :composeApp:desktopTest --offline --no-daemon

前提条件:JDK 21,且机器上安装了 git CLI(两个测试都用Assume.assumeTrue("git CLI not installed", GitEngine.detect(isolateConfig = true).available)做环境跳过)。测试运行于 next-compose 目录下,等价于:

cd next-compose ./gradlew :composeApp:desktopTest --offline --no-daemon

如果想跑单一测试:

./gradlew :composeApp:desktopTest \ --tests com.rememberber.mootool.next.compose.domain.GitRebaseProductEvidenceFlowTest \ --offline --no-daemon

证据脚本复现(同样要求 git 可用):

cd next-compose && ./scripts/prepare-git-rebase-conflict-evidence.sh cd next-compose && ./scripts/prepare-git-merge-conflict-evidence.sh

脚本执行后会在数据目录data/vaults/json下留下带冲突的仓库,输出会提示MOOTOOL_COMPOSE_DATA_DIR环境变量、runDistributable运行命令与预期 unmerged 文件(git diff --name-only --diff-filter=U应为conflict.json)。

十、周边切片与下一步

DIFF-617 处于一条清晰的产品走查切片链中,读者可沿以下文档追溯演进脉络(均在 next-compose/docs/diff 下):

  • 604-git-merge-product-evidence-flow-slice.md:§BpreferredConflictSelectionPath与 merge 证据链的起点;
  • 609-git-merge-push-disabled-evidence-slice.md:merge 冲突期 push/pull 禁用锁定;
  • 612-git-rebase-product-evidence-slice.md:productFlowHintKey(operation)引入 rebase 分支;
  • 616-git-rebase-push-pull-disabled-slice.md:rebase 冲突期 pull 禁用 + 帧 211,DIFF-617 的基线;
  • 617-git-rebase-product-flow-resolve-pull-slice.md:本文主体,补齐 resolve 提示与完成后 pull/push 恢复;
  • 619-git-fetch-during-conflict-slice.md 与 633-git-continue-action-f05-history-java-python-slice.md:冲突期 fetch 与 continue 的进一步演进。

小结

DIFF-617 虽是一张小切片,但它把「自动选中冲突文件 → resolve 提示切换 → 冲突期按钮禁用矩阵 → resolve/continue → push 与 pull 恢复」这条产品链路完整锁定。读懂它,也就读懂了 MooTool next-compose 的 Vault Git 面板在处理 merge/rebase 冲突时"引擎层(GitEngine)与呈现层(Git*Presentation)如何分工、证据脚本与测试如何双向校验"的整套工程方法。对想复现验收、扩展该面板行为或移植到其它桌面框架的开发者,这条切片连同它的测试与脚本就是最直接的参考蓝本。

  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

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

相关推荐

上一篇:deeplearning-models CORN算法:基于条件概率的序数回归终极指南
下一篇:ttkbootstrap自定义主题创建教程:打造专属UI设计系统

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进