MooTool Next Compose DIFF-515 深度解析:Vault Git merge/rebase 状态 UI、push 引擎守卫与 HTTP/翻译代理运行时规范化
MooTool Next Compose DIFF-515 深度解析:Vault Git merge/rebase 状态 UI、push 引擎守卫与 HTTP/翻译代理运行时规范化
导读
本篇技术指南以 DIFF-515 变更说明 为骨架,解析 MooTool 新一代 Compose 桌面端在 Vault Git 工作流与网络代理两条链路上的运行时收口:一方面把 push 从"仅依赖 UI 禁用"升级为与 pull 同级的引擎层守卫,并在 VaultGitDialog 中呈现 merge/rebase 状态胶囊与变更计数;另一方面将 HTTP(F09)与翻译(F20,含单词本重译)的发送逻辑统一收敛到 toHttpProxyConfig() 映射,在发送前完成 host/凭据 trim 与端口合法性校验。读完你将对这两个切片的底层调用链、守卫策略与对应测试验证有完整、可复用的技术认知。
一、变更背景:从 parity-gap 到运行时收口
DIFF-515 承接 DIFF-514 的成果。DIFF-514 已为设置页的网络代理做了"失焦提交"语义与加载规范化(SettingsNetworkNormalize),但彼时 F09 HTTP 与 F20 翻译发送时仍直接读取 NetworkSettings 的原始字段,规范化只在设置保存/加载阶段生效,运行时未收口。
与此同时,Electron 版 parity-gap 仍将 Vault Git pull/push/rebase 工作流可感知性列为差距项:GitEngine.pull 在 merge/rebase 进行中会从引擎层拒绝,而 push 此前仅依赖 UI 禁用,缺少引擎层兜底。
DIFF-515 的定位正是补齐这两处运行时收口,可概括为:
| 切片 | 此前状态 | DIFF-515 目标 |
|---|---|---|
Vault Git push |
仅 UI 禁用 | 引擎层与 pull 同级拒绝 |
| Vault Git 状态展示 | 进行中无明确提示 | 状态胶囊 + 上下文计数 |
| HTTP/翻译发送代理 | 直接读原始字段 | 统一经 toHttpProxyConfig() 规范化 |
二、A03 Vault Git:push 引擎级守卫与状态呈现
2.1 GitEngine.push 与 pull 对齐的守卫策略
核心实现位于 GitEngine.kt。push 与 pull 采用了几乎对称的双层拒绝逻辑:
fun push(root: Path, isolateConfig: Boolean = false, token: String = ""): GitActionResult = locked(root) {
val current = status(root, isolateConfig)
if (!current.repository) return@locked GitActionResult(false, "Git repository is not initialized")
if (current.merging) {
return@locked GitActionResult(false, "Finish or abort the current merge/rebase before pushing")
}
if (current.conflicts > 0) {
return@locked GitActionResult(false, "Resolve all conflicts before pushing")
}
requireRemote(root, isolateConfig) { run(listOf("push", "-u", "origin", "HEAD"), root, isolateConfig, token, REMOTE_TIMEOUT_MS) }
}
- merge/rebase 进行中(
current.merging):直接返回失败,文案含 "merge/rebase" 与 "pushing" 关键字,这正是文档要求对齐的拒绝语义; - 存在未解决冲突(
current.conflicts > 0):同样拒绝,即使merging为 false(例如异常残留的冲突状态); - remote 缺失:
requireRemote兜底返回 "No origin remote is configured"; - 满足条件后才真正执行
git push -u origin HEAD,走REMOTE_TIMEOUT_MS = 120_000L远程超时。
pull 的守卫与之对齐(GitEngine.kt pull 方法):同样拒绝 merging 与冲突状态,并校验 remote 后执行 git pull --no-rebase origin。
merging / operation 的来源在 status() 中可追溯到 Git 的底层状态判定:
val merge = run(listOf("rev-parse", "-q", "--verify", "MERGE_HEAD"), root, isolateConfig)
val rebaseMerge = gitPathExists(root, "rebase-merge", isolateConfig)
val rebaseApply = gitPathExists(root, "rebase-apply", isolateConfig)
val operation = when {
rebaseMerge || rebaseApply -> "rebase"
merge.exitCode == 0 -> "merge"
else -> "none"
}
即通过 MERGE_HEAD 与 .git 目录下的 rebase-merge / rebase-apply 哨兵文件识别进行中的操作,写入 GitStatus.operation 与 GitStatus.merging。
2.2 GitOperationPresentation:操作状态胶囊与上下文计数
GitOperationPresentation.kt 是纯函数式的展示层判断中心,全部方法对齐 Electron VaultGitDialog 语义:
fun inProgressMessageKey(operation: String, merging: Boolean): String? {
if (!merging) return null
return when (operation) {
"merge" -> "git.operationMerge"
"rebase" -> "git.operationRebase"
else -> "git.operationInProgress"
}
}
/** 对齐 DIFF-515:仅在 merge/rebase/冲突上下文显示变更计数。 */
fun showChangeCounts(merging: Boolean, conflicts: Int): Boolean =
merging || conflicts > 0
关键点在于 showChangeCounts 的上下文限定:文档明确指出"仅 merge/rebase/冲突上下文,不恢复 DIFF-315 移除的常显 counts"。也就是说,git.counts(changes / conflicts 数量)不再是常态展示,只有当 merging 为 true 或存在未解决冲突时才出现,避免回归 DIFF-315 已删除的常显方案。
在 UI 侧,VaultGitDialog.kt 中按操作类型呈现:
GitOperationPresentation.inProgressMessageKey(...)命中时渲染MooStatusPill状态胶囊,冲突时取 Error 色、无冲突时取 Valid 色(第 323-328 行);showChangeCounts命中时渲染git.counts文本,冲突数大于 0 时用colors.danger高亮(第 329-341 行);- 中止按钮文案按操作类型区分:
git.abortRebase/git.abortMergeOnly/git.abortMerge,确认弹窗文案对应git.confirmAbortRebase/git.confirmAbortMerge/git.confirmAbort。
此外展示层还明确了 pull/push 的启用条件(remotePresent && !merging):未解决冲突但非 merging 时面板按钮仍可点击,实际由引擎层拒绝——这正是 DIFF-515 强调"push 不再仅依赖 UI 禁用"的落地方式:UI 与引擎形成双层防线。
2.3 测试验证矩阵
DIFF-515 为 Vault Git 切片指定了两组测试:
- GitRebaseGuardTest.kt 中的
pushBlockedWhileRebaseInProgress:构造 rebase 冲突夹具(base/feature/main 三分支制造冲突),断言GitEngine.push失败且文案同时包含 "merge" 与 "pushing"; - GitOperationPresentationTest.kt:覆盖
inProgressMessageKey在 none/merge/rebase/unknown 与 merging 组合下的文案键映射,以及 abort 文案、pull/push/commit/continue/abort 的启用与禁用边界。
补充证据:未解决冲突场景的 merge 变体由 GitPushGuardTest.kt 的 pushBlockedWhileMergeConflictsRemain 覆盖。所有 Git 引擎测试都通过 isolateConfig = true 隔离全局/系统 Git 配置,保证不受开发者本机配置干扰。
三、F09 HTTP / F20 翻译:代理运行时统一映射
3.1 NetworkSettings.toHttpProxyConfig() 规范化规则
DIFF-515 引入的运行时映射位于 NetworkRuntime.kt:
fun NetworkSettings.toHttpProxyConfig(): HttpProxyConfig {
if (!proxyEnabled) return HttpProxyConfig()
return HttpProxyConfig(
enabled = true,
host = proxyHost.trim(),
port = SettingsNetworkNormalize.sanitizeProxyPort(proxyPort),
username = proxyUsername.trim(),
password = proxyPassword.trim(),
)
}
规则明确:
proxyEnabled关闭时直接返回未启用的空HttpProxyConfig,不做任何 trim 副作用;- host / username / password 发送前统一
trim(); - port 必须经过 SettingsNetworkNormalize.kt 的
sanitizeProxyPort校验:空串保留为空,非整数或超出1..65_535均被清空,而非抛错或透传。
fun sanitizeProxyPort(value: String): String {
val trimmed = value.trim()
if (trimmed.isEmpty()) return ""
return trimmed.toIntOrNull()?.takeIf { it in 1..65_535 }?.toString() ?: ""
}
这一约束与 DIFF-514 中 SettingsNetworkNormalize.apply 在加载/迁入时使用的端口规则完全一致(SettingsNetworkNormalize.kt 还限定了 host ≤256、用户名 ≤128、密码 ≤512 字符),使设置保存、配置加载、运行时发送三条路径共享同一套规范化语义。目标结构 HttpProxyConfig(enabled / host / port / username / password)定义在 HttpEngine.kt,默认均为空值。
3.2 消费链路:HttpScreen 与 TranslationScreen
映射的消费点统一替换了原先"直接读原始字段"的写法:
- F09 HTTP:HttpScreen.kt 第 323 行在发送入口读取
val proxy = container.settings.value.network.toHttpProxyConfig(),随后把proxy与 draft、requestId、timeout 一并交给HttpRequestPresentation.runSend(...)在Dispatchers.IO上执行; - F20 翻译:TranslationScreen.kt 有两个消费点:
- 常规翻译发送(第 147 行):
val proxy = settings.network.toHttpProxyConfig()进入TranslationWiringPresentation.runTranslate(...); - 单词本重译(第 391 行):
WordBookPane的onRetranslate回调同样走toHttpProxyConfig(),确保重译与首次翻译使用一致的规范化代理配置。
- 常规翻译发送(第 147 行):
由此,DIFF-515 达成"HttpScreen / TranslationScreen(含单词本重译)统一走该映射"的验收条件。数据模型源在 Settings.kt 的 NetworkSettings:proxyEnabled / proxyHost / proxyPort / proxyUsername / proxyPassword,以及超时默认值 requestTimeoutMs = 30_000、translationTimeoutMs = 15_000。
3.3 验证:NetworkRuntimeTest
NetworkRuntimeTest.kt 用三个用例锁定映射行为:
| 用例 | 输入 | 期望 |
|---|---|---|
httpProxyConfigTrimsAndSanitizesPort |
host/port/user/password 均带空格 | 全部 trim,port " 7890 " → "7890" |
httpProxyConfigClearsInvalidPortButKeepsEnabledFlag |
port "70000"(越界) |
enabled 仍为 true,port 清空为空串 |
httpProxyConfigDisabledWhenToggleOff |
proxyEnabled = false |
proxy.enabled = false |
其中"非法端口清空但保留 enabled 标记"的语义值得注意:它避免了发送时因端口非法整体禁用代理,而是在运行时用清空端口的方式降级,端口合法性判断完全交给 sanitizeProxyPort 单一入口。
四、验证方式与运行前提
DIFF-515 指定的验证命令为:
./gradlew :composeApp:desktopTest --offline
运行前提:
- JDK 21:仓库说明中明确验证环境为 JDK 21(见 next-compose/README.md 与文档);
- --offline:使用本地 Gradle 依赖缓存离线执行,避免网络抖动影响结果;
- Git 相关测试(
GitRebaseGuardTest、GitPushGuardTest)通过Assume.assumeTrue("git CLI not installed", ...)在未安装 git CLI 的机器上自动跳过,属于有前置条件的集成型单测;NetworkRuntimeTest为纯 JVM 单测,无外部依赖。
测试范围集中在 composeApp/src/desktopTest 下的三个类:GitOperationPresentationTest、NetworkRuntimeTest、GitRebaseGuardTest,分别覆盖展示层判断、代理映射、push/pull/commit/continue/abort 在 rebase 冲突下的引擎行为。
五、明确未做(Roadmap 边界)
DIFF-515 文档如实划定了本次切片的边界,避免对能力做出超范围承诺:
- 产品主窗冲突 / Git merge / IME 界面截图证据;
- 六套 CSS 皮肤切换;
- P7 Windows / Linux 安装包验证;
runDistributable烟雾测试;- 设置页 TextField 失焦提交链路延伸;
- 其余 F-tool 大切片迁移。
这些属于后续迭代范围,未在本条变更中覆盖。对于同类迁移工作,acceptance.md、architecture.md 以及 diff 目录 下的其余切片文档可提供完整的产品一致性基线。
六、实现要点小结
- 双层防线设计:Vault Git 的 push/pull 在 UI 启用判定(
GitOperationPresentation.pushEnabled)之外,必须存在引擎层守卫(GitEngine.push的 merging/conflicts 拒绝),防止绕过 UI 的调用路径直接污染仓库状态; - 操作状态判定:
operation由MERGE_HEAD、rebase-merge、rebase-apply哨兵推导,UI 状态胶囊与 abort/continue 文案键随操作类型分派; - 计数上下文限定:
git.counts仅在 merge/rebase 或存在冲突时展示,避免回归常显方案; - 运行时规范化:代理配置在发送前统一 trim 与端口校验(1–65535),与设置加载路径共享同一
SettingsNetworkNormalize语义;关闭开关则返回空配置,不产生副作用; - 测试锚点:展示层与引擎行为均有独立测试锁定,端口越界被"清空而非报错",rebase/merge 冲突下 push 文案必须同时含 merge 与 pushing 关键字。
以上实现均可回溯至 GitEngine.kt、GitOperationPresentation.kt、NetworkRuntime.kt 与对应的测试文件,可作为后续维护与横向迁移(如 Tauri/Flutter/原生版本)的参照基线。