Orca 跨执行主机的 Git 兼容性策略:以能力探测为核心的 Git 版本适配体系
【免费下载链接】orcaOrca is the ADE for working with a fleet of parallel agents. Run any coding agent with your own subscription. Available on desktop, mobile and remote runtime.项目地址: https://gitcode.com/GitHub_Trending/orca48/orca
Orca 作为面向多智能体并行工作的桌面端与远端运行时,会调用用户机器上已安装的 Git 二进制完成仓库操作,而这些 Git 可能运行在 native、WSL、SSH 三类差异极大的执行主机上。本文以仓库中的 Git Compatibility Policy 为骨架,结合 git-capability-cache.ts 等源码实现,完整讲解 Orca 如何以 Git 2.25 为兼容基线、通过“行为探测 + 精确回退 + 主机级缓存”来适配多版本 Git,并给出 CI 验证契约。读完本文,你将理解 Orca 的能力矩阵、GitCapabilityCache的运行机制、为何不能依赖git --version分支判断,以及如何为新增的 Git 特性安全地建立降级路径。
一、适用范围:三类执行主机与主机级能力状态
Orca 不会自己解析 Git 仓库的内部格式,而是执行用户的 Git 二进制。该二进制可能出现在三类执行主机上(git-compatibility.md 的Scope一节):
- native:Orca 直接在本机操作系统上调用的 Git;
- WSL:Windows 下经 WSL 发行版(distro)调用的 Git;
- SSH:通过 SSH 连接到的远端主机上的 Git。
每一台主机的 Git 版本都可能不同,因此“某个能力是否可用”这一兼容性状态必须按真正执行命令的那台主机来划分作用域,而不能是进程级的单一全局状态。这一点在源码中体现得很直接:git-capability-state.ts 里用
const localCapabilitiesByExecutionHost = new Map<string, GitCapabilityCache>()维护本地主机(含 WSL)各自的缓存,键由getLocalGitExecutionHostKey生成:存在 WSL distro 时返回wsl:${distro},否则返回'local';而 SSH 侧则使用WeakMap<object, GitCapabilityCache>(sshCapabilitiesByProvider)——因为重连会产生新的 provider 对象,而同一 SSH 连接的并发 IPC/runtime 调用方必须共享同一份远端能力结果。文件头注释还解释了为何不用普通 Map 存 SSH provider:避免 provider 销毁后内存无法回收。
2.25 核心工作流基线
文档明确约定:Git 2.25 是命令选型的核心工作流兼容基线。它是最老的、能覆盖 Orca 基线用法的版本线,涵盖以下能力:
- porcelain v2(
git status --porcelain=v2)输出解析; branch --show-current;restore命令;- sparse checkout(稀疏检出)。
仓库中确实同时维护了新旧两种输出通道的实现,例如 porcelain-v2-records.ts 与 porcelain-v1-records.ts 并存,以及 worktree-sparse-checkout.ts 等稀疏检出相关实现,这正印证了“永远保留基线可用的命令或解析器作为回退”的工程原则。
需要强调的是两条基线约束:
- Orca当前不在启动时阻止旧版 Git,即旧 Git 用户仍可使用;
- 但新命令的构造不得假设存在 2.25 之后才引入的特性——凡是依赖新特性的地方都必须走下面的能力规则。
二、能力规则:新增特性如何安全降级
当一个较新的 Git 特性确实能显著改善正确性或性能时,Orca 要求开发者遵循五步规则(文档Capability Rules一节):
- 保留基线兼容的命令或解析器作为 fallback;
- 用针对该选项/子命令的窄谓词(narrow predicate)识别“被拒绝”,而不是笼统地匹配任意报错;
- 让首选命令经由
GitCapabilityCache执行,这样一次拒绝会被“记住”,并归属到产生该拒绝的 native 主机、WSL 发行版或 SSH provider 名下; - 在缓存间隔之后重试,使原地升级 Git(in-place upgrade)无需重启 Orca 即可自愈;
- 测试:首个 fallback、后续跳过已拒绝探测的调用、并发探测合并(coalescing)、以及执行主机隔离。
为什么不能用git --version做分支
文档给出一条非常明确的工程纪律:不要只依据解析出的git --version字符串来分支。原因有二:
- 发行版厂商会 backport:某些发行版会把新特性移植回旧版本号;
- wrapper 会撒谎:外层包装程序报告的宿主版本,可能与实际在 WSL 或 SSH 内部真正执行的二进制版本不一致。
因此,最终裁决权属于行为探测 + 精确回退,版本号只能作为提示。这正是CapabilityProbeCache存在的意义。
缓存与探测的运行机制
能力缓存的核心实现在 capability-probe-cache.ts。值得注意,该文件头注释说明它是“从GitCapabilityCache中抽取出来”的通用基类,目标是为仓库里所有主机能力缓存统一三种行为:探测一次、只记住“确实缺失”的阳性信号、让并发调用等待正在进行的探测而不是重复发起。
它的内部状态由三个容器构成:
retryAfterByCapability: Map<TCapability, number>——记录某能力被判定为不支持后的重试时间点;probesByCapability: Map<TCapability, Promise<Outcome>>——正在进行的探测 promise,用于并发合并;supportedCapabilities: Set<TCapability>——已被证明支持的能力集合。
其公开 API 及语义如下(与源码对应):
| API | 行为 |
|---|---|
shouldTry(capability) | 能力从未失败或已过重试期则返回true;未到重试期返回false,调用方直接走 fallback |
isKnownSupported(capability) | 该能力是否已被证明支持 |
rememberSupported(capability) | 主动记录支持(清除失败时间并加入集合) |
rememberUnsupported(capability) | 记录“不支持”:从支持集合删除,并把重试时间设为now + retryIntervalMs(见 git-capability-cache.ts 中的GIT_CAPABILITY_RETRY_INTERVAL_MS = 30 * 60_000,即 30 分钟) |
runWithFallback(capability, runPreferred, runFallback, isUnsupportedError) | 探测并执行的首选入口 |
clear() | 测试与重置用 |
runWithFallback是整套机制的心脏,值得逐行拆解其状态机(capability-probe-cache.ts):
- 若能力已在
supportedCapabilities中,直接“乐观”地执行首选路径——注意注释强调“受支持的命令是真实工作而非一次性探测”,所以要让同仓库/同 SSH 的并发调用保持原有并发度,不再走探测锁; - 若
shouldTry返回false(仍在 30 分钟冷却期),直接执行 fallback,避免每个 poll/search 都重复一次已知失败、白白消耗子进程与 trace 空间; - 若已有 in-flight 探测,则等待其 outcome,再决定走首选还是 fallback——这就是“并发探测合并”;
- 否则由本调用充当探针:把 promise 放入
probesByCapability,执行runPreferredOrFallback,无论成功失败都会 settle 出'supported' | 'unsupported' | 'unknown'三种结果之一,并在finally中清理探测槽位(且有一个防挂起的兜底:即便isUnsupportedError抛异常,等待方也不会永久等待)。
在runPreferredOrFallback内部有一个精妙细节:首选回调成功返回后,并不会盲目标记为 supported,而是检查该能力是否已被回调自己在执行过程中通过更弱的“阳性信号”(例如旧 Git 对未知选项原样回显且 exit 0)标记为 unsupported——如果有更强的否定信号,就不覆盖它。只有真正确认支持时,才写入supportedCapabilities。
专门的并发探测合并实现见 coalesced-probe.ts 及其测试 coalesced-probe.test.ts,用于验证“同一能力并发请求只发起一次真实探测”。
三、当前能力矩阵
文档用一张表完整列出了当前登记在案的能力及其首选/兼容行为,这里完整继承并逐项结合源码展开:
| Capability | 首选行为 | 兼容行为 |
|---|---|---|
fetch-no-write-fetch-head | Fetch 私有 rebase ref 时不改写 worktree 本地的FETCH_HEAD | Git 2.29 之前,按 worktree Git 目录串行化 Orca 的所有 fetch/pull 操作 |
worktree-list-z | NUL 分隔的 worktree 路径并带prunable标记 | 对worktree list -z(2.36)之前的 Git 使用行块解析器;Git 2.31–2.35 上仍可解析prunable/locked注解;2.31 之前的 Git 用路径存在性探测恢复prunable检测 |
rev-parse-path-format | 返回绝对仓库元数据路径 | 针对被扫描仓库解析旧版相对路径输出 |
for-each-ref-exclude | 在输出上限之前先排除远端 HEAD | 多请求部分 refs,再由 Orca 内部过滤远端 HEAD |
merge-tree-write-tree | 推导真实合并冲突与 no-op tree 证明 | Git 2.38 之前省略冲突摘要并保持保守的分支清理行为 |
merge-tree-merge-base | 提供已解析的 merge base | 使用旧版两提交形式的merge-tree --write-tree |
所有这些能力标识构成一个受类型约束的联合,定义在 git-capability-cache.ts 的GitCapability类型中,编译期即可防止拼错能力名。
逐项源码印证
worktree-list-z/rev-parse-path-format:在 worktree-list-reader.ts 中可以看到真实调用模式——通过withLocalGitCapabilityCacheForExecution拿到主机对应的缓存后,以capabilities.runWithFallback(...)包裹首选与回退两种解析路径;当首选路径失败且被窄谓词判定为“不支持”时,还会显式调用capabilities.rememberUnsupported('rev-parse-path-format')(见第 63、114 行),并在第 203–207 行对worktree-list-z走同样的runWithFallback。对应地,判别函数isUnsupportedWorktreeListZError位于 git-worktree-command-capabilities.ts。
fetch-no-write-fetch-head:在 remote-rebase.ts 中,rebase 前的 fetch 通过withLocalGitCapabilityCacheForExecution包裹并探测fetch-no-write-fetch-head;其第 58 行注释点出了该问题的根源:并发 fetch 会在 fetch 与 rebase 之间改写FETCH_HEAD与 remote-tracking refs,这正是首选行为要规避的竞态,而兼容行为则牺牲并发换取正确性。
merge-tree-write-tree/merge-tree-merge-base:两个能力的窄谓词都在 git-merge-tree-capability.ts 中实现,它们不是宽泛地匹配任何报错文本,而是精确匹配 Git 的报错形态:
isUnsupportedMergeTreeWriteTreeError:匹配unknown/invalid/unrecognized option ... --write-tree、unknown rev '--write-tree',以及旧版git merge-tree <base-tree> <branch1> <branch2>的 usage 文本;isUnsupportedMergeTreeMergeBaseError:只匹配--merge-base选项不被识别的报错。
for-each-ref-exclude:判别函数isForEachRefExcludeUnsupportedError位于 git-ref-command-capabilities.ts,它把错误文本小写化后,要求同时包含unknown option与exclude才判定为不支持——这就是“窄谓词”的典型范例。
窄谓词为何必须“窄”
窄谓词直接决定能力状态机的正确性:若谓词过宽,会把真实的 Git 故障(如网络错误、仓库损坏、权限问题)误判为“该选项不受支持”,从而悄悄把用户导向功能缩水的兼容路径,掩盖真正的错误;若过窄,则探测会反复失败并不断触发 fallback。因此每个谓词都要精确模拟 Git 针对“该选项不存在”时的报错形态,而这类形态恰恰无法用单个真实二进制在 CI 中确定性地构造,这也是文档要求单元测试覆盖“error-stream shapes”的原因(见下文 CI 契约)。
四、占位符“Fail Open”问题:%(decorate:…)
文档特别辟出一节讨论一类无法被缓存记录的失败模式。GitCapabilityCache记录的是 Git明确拒绝的命令;但git log --format的占位符如果 Git 不认识,并不会报错——Git 会把它原样回显并 exit 0,因此“没有错误可记,也没有可缓存的探测”。
| Placeholder | 首选行为 | 兼容行为 |
|---|---|---|
%(decorate:…) | Git 2.43 起用\x1f分隔提交装饰信息,含逗号的 ref 名因此可以存活 | 同一条记录同时携带%D(Git 2.10 可用);解析时若发现%(decorate未被展开,则退回%D,代价是逗号会被拆开 |
解决思路是文档明确给出的:在一条记录里同时请求两种形式,在解析阶段再做选择。也就是说,输出的 log 记录同时携带%(decorate:...)与%D两个字段;解析器看到%(decorate仍保持原样(未被 Git 展开)就说明二进制太老,于是改用%D的语义。这是“探测 + 回退”哲学在“无法被拒绝、只能被静默透传”的场景下的变体:把兼容性判断从执行时推迟到解析时。
五、为什么不用simple-git
文档单列一节解释不采用simple-git库的架构决策。simple-git本质是已安装 Git 二进制外的进程包装器,其自定义选项与rawAPI 只是把参数透传给 Git,因此:
- 它无法让一个新 flag 在一个旧二进制上生效;
- 它也无法自动替 Orca 选择语义等价的自定义回退。
simple-git固然提供了版本报告与子进程队列,但这些恰恰不是 Orca 的瓶颈。Orca 自身已经需要实现 WSL/SSH 路由、取消、追踪、敏感信息脱敏、进程清理与有界输出处理(这些能力散见于 src/main/git 下的 runner、wsl-* 系列文件与 command-runner 子目录)。若引入simple-git替换 runner,能力问题只是从一个地方搬到另一个地方,并没有被消除,反而会引入一层无法接入 Orca 主机路由与能力缓存的间接层。
六、CI 契约:真实二进制矩阵 + 单元测试矩阵
能力策略是否成立,最终靠 CI 契约来兜底(文档CI Contract一节)。PR 检查会针对真实 Git 二进制运行能力契约,覆盖三个版本点:
- Git 2.25.5:验证 2.29 之前串行化
FETCH_HEAD的回退路径(最老的受支持版本线); - Git 2.38.1:验证
--merge-base出现之前、过渡期的merge-tree --write-tree行为; - Git 2.49.1:验证当前 Git 下的首选行为。
这三个版本横跨了能力矩阵中的关键分水岭。更重要的是,文档强调要让单元测试与该矩阵并行存在:真实二进制无法确定性地构造某些场景,因此单测必须覆盖:
- 并发探测合并(见 coalesced-probe.test.ts);
- native / WSL / SSH / relay 的执行主机隔离(见 git-capability-state.test.ts 与 git-capability-cache.test.ts,后者还覆盖缓存重试、同主机多调用跳过重复探测等行为);
- 错误流形态(error-stream shapes)——即各类窄谓词对真实 Git 报错文本的匹配,例如 git-merge-tree-capability.test.ts 会验证上面那些正则是否能命中、是否会误伤其他错误。
仓库中另有 worktree-git-capabilities.test.ts,将 worktree 场景与 Git 能力矩阵绑定做真实 Git 级别的验证,与文档描述的“真实二进制矩阵 + 单测矩阵并行”完全吻合。
七、给调用方与贡献者的工程要点
把整篇策略收敛为几条可直接落地的工程要点:
- 新命令一律按“最老的支持版本”假设,任何超出 Git 2.25 基线的特性都要走
runWithFallback(capability, runPreferred, runFallback, isUnsupportedError); - 不要把版本判断写死在命令构造处,一律通过能力缓存,且每个能力都要有独立的窄谓词;
- 新能力三步接入:在 git-capability-cache.ts 的
GitCapability联合类型里登记名称 → 用主机级缓存包装调用点(参考 worktree-list-reader.ts 的既有用法)→ 为该能力补窄谓词(放在 shared 下对应 capability 模块)与单测; - 区分“被拒绝”与“被静默透传”:对
git log --format这类会 exit 0 的占位符,采用“同记录双形式 + 解析时选择”而非“探测 + 缓存”; - 版本自愈依赖 30 分钟重试窗口:
rememberUnsupported写入的冷却期意味着用户原地升级 Git 后,最多 30 分钟内 Orca 会自动重新探测到新能力,无需重启。
这条策略的完整原始定义始终以仓库中的 git-compatibility.md 为权威来源,本文的所有源码引用均可在 src/shared 与 src/main/git 目录下复核。
【免费下载链接】orcaOrca is the ADE for working with a fleet of parallel agents. Run any coding agent with your own subscription. Available on desktop, mobile and remote runtime.项目地址: https://gitcode.com/GitHub_Trending/orca48/orca
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考