- 桌面应用
- 开发工具
【免费下载链接】MooTool
Handy tool set for developers
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) }逻辑很直白:
- 只有
merging == true且conflicts > 0时才触发选中; - 若用户当前已选中某个冲突路径,则不覆盖(尊重手动选择);
- 否则调用
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 == 0UI 动作层在 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:§B
preferredConflictSelectionPath与 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
相关推荐
MooTool Next Compose 的 Git Rebase 冲突解析与 Continue 集成:从 `resolveConflict` 到「继续合并 / Rebase」的完整链路
MooTool Next Compose 的 Git Rebase 冲突解析与 Continue 集成:从 resolveConflict 到「继续合并 / R
桌面应用开发工具doocs/leetcode 题解精讲:面试题 08.08「有重复字符串的排列组合」排序 + 回溯去重全解析
doocs/leetcode 题解精讲:面试题 08.08「有重复字符串的排列组合」排序 + 回溯去重全解析 本文基于开源仓库 doocs/leetcode 中
桌面应用开发工具MooTool 桌面端 Vault 外部冲突检测链路:VaultRevisionMonitor 监视器到冲突叠层的 wiring 实现与测试(DIFF-439)
MooTool 桌面端 Vault 外部冲突检测链路:VaultRevisionMonitor 监视器到冲突叠层的 wiring 实现与测试(DIFF 439)
桌面应用开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考