no-mistakes 分支同步与推送安全机制深度解析:guarded sync、custody recovery 与 force-push 数据保全
【免费下载链接】no-mistakesgit push no-mistakes项目地址: https://gitcode.com/GitHub_Trending/no/no-mistakes
本篇技术指南聚焦 no-mistakes 项目内部的branch-sync-and-push-safety技能契约(见 .agents/skills/branch-sync-and-push-safety/SKILL.md),系统讲解其在 AI 驱动 pipeline 场景下的三条安全主线:本地分支与远程的受保护同步、Review 之后头部连续性与推送绑定、以及 rebase 基线与 force-push 的数据丢失防护。读完你将理解no-mistakes axi sync一族命令背后的状态机、--recover的精确归还语义,以及resolveForcePushDecision如何从根上避免静默丢弃他人代码。
一、整体架构:一个服务、三种入口
sync、axi sync与 TUI 的u动作共享同一个internal/branchsync服务,其核心契约在 internal/branchsync/sync.go 中实现:
- 唯一的普通 worktree 变更方式:一次"干净的、受保护的移动"——要么是 behind 分支上的严格快进(strict fast-forward),要么是"锚定后重置"(anchored reset)到内容等价的 diverged pipeline 头部(前提是本地独有提交已被 pipeline 头部完整承载)。
- 被动检查永不联网:
InspectCached(由 CLI/TUI/axi status 渲染)只读取本地 Git、持久化 provenance 与只读的 gate 祖先证据,绝不 fetch、绝不改动 refs/index/worktree。 - 阻塞状态永不破坏性操作:被阻塞的分支状态(如
blocked_pipeline_owned、blocked_diverged)下,服务绝不执行 reset、stash、merge、rebase、force、switch、delete 或对外部 remote 的 update。
Service结构体(sync.go)同时暴露了两个可替换的 remote 操作 seam(lsRemote/fetchRemote),使测试可以在不污染全局状态的前提下注入受控 fake。
二、核心状态机:状态、关系与安全等级
同步契约由三组常量描述,它们是 CLI、AXI、TUI 三套展示层的共同语言:
分支状态(State,见 sync.go)
| 状态 | 含义 |
|---|---|
pipeline_owned | pipeline 正在/曾持有分支,头部已移动但未成功推送,禁止本地后续提交 |
push_in_progress | push 步骤正在执行 |
behind | 本地落后于 pipeline 推送绑定 |
synchronized | 本地与推送绑定完全一致 |
local_ahead | 本地领先,建议走axi run而非同步 |
diverged | 双方分叉 |
dirty | worktree 不干净 |
remote_advanced/remote_rewritten | 远端包含绑定之外的提交 / 远端不再等于绑定 |
remote_missing | 远端分支不存在 |
merged_remote_retained/merged_remote_removed | PR 已合并,分支保留 / 已被清理 |
closed | PR 已关闭 |
offline | 网络操作失败,fail closed |
target_changed | 推送目标配置被改动 |
ambiguous_context | 上下文无法唯一判定 |
legacy_unbound | 无精确推送绑定 |
custody_returned | 滞留 run 的 custody 已归还 |
user_owned | run 终止于提交头未移动之前,分支本就属于操作者 |
关系(Relation,sync.go):equal、behind、ahead、diverged、unknown。
安全等级(Safety,sync.go):仅两种等级允许Apply落地——safe_fast_forward与safe_equivalent_advance(见CanApply)。
三、Refresh 与 Apply:只做"干净的受保护移动"
3.1 单操作网络超时预算:branch_sync_remote_timeout
Refresh中,每次网络 remote 操作都使用从调用方 ctx 派生的独立有界子上下文,而不是共享同一个 deadline(sync.go):一次"慢但成功"的git.LsRemote绝不能偷走随后git.FetchRemoteBranchToPrivateRef的预算,否则一个可达的远端会被误判为 offline。Apply的最终 live 检查也使用相同的单操作预算。
该预算即Service.RemoteTimeout(sync.go),只来自操作者全局配置branch_sync_remote_timeout,默认值为config.DefaultBranchSyncRemoteTimeout(60s,见 internal/config/config.go)。关键设计点:RepoConfig故意没有对应字段——一个被推送进仓库的.no-mistakes.yaml不能通过修改配置来放宽或收紧服务等待网络多久,从而防止仓库内容操纵超时预算(相关回归测试见TestLoadRepo_BranchSyncRemoteTimeoutIsNotARepoSetting)。注意Recover的本地 gate fetch 属于本地仓库间操作,不在此网络 deadline 契约内。
3.2 Refresh:先验证再移动
Refresh的流程(sync.go)可概括为:本地检视 → 数据库绑定复检(run、generation、pushed head、target fingerprint/kind/ref 全部一致,否则blocked_binding_changed)→ 带独立超时的ls-remote→ 带独立超时的 fetch 到私有 refrefs/no-mistakes/sync/<run>→ 校验 fetched == live → 校验 live == 持久化的 pushed head → 再按 PR 生命周期(merged/closed)或关系分类落定。它从不更新普通的 remote-tracking ref,只把精确的推送 ref 验证进 no-mistakes 私有 ref。
3.3 Apply:apply 前的多重复检 + fail-closed 的两种移动
Apply(sync.go)在真正移动之前要连续通过多层防线:
- gate 执行上下文拒绝检查(
gateContextRefusal,防止在 gate 自身的嵌套执行环境里误操作); Refresh重新出计划,且CanApply必须为真;- 数据库 push binding 复检(run、pushed head、generation、ref、target fingerprint/kind);
inspect复检本地分支/HEAD/cleanliness 未变;- 带超时的最终 live
ls-remote,要求 live == pushed head; - 最终前提复检(
blocked_assumptions_changed); - 关系证明复检:equivalent advance 重新执行
equivalentDivergence,普通快进重新验证祖先关系。
移动本身分两种:
- 严格快进:
git merge --ff-only --no-edit <pushed>,由 Git 自身保证不会覆盖未落地的本地工作; - 等价 diverged 推进:先把同步前本地 HEAD 锚定到
refs/no-mistakes/sync-anchor/<run>并校验,再reset --hard到验证过的 pipeline 头部。
成功后还会检查 post-apply 的 worktree 是否仍干净(防止 hook 留下脏状态),最终才置为synchronized。
成功推送的持久化绑定由 internal/pipeline/steps/push.go 写入:精确 SHA、免凭据的目标指纹(branchsync.TargetFingerprint对 URL 做规范化 sha256,见 sync.go)、ref 与 generation;历史遗留行的可空字段永远不允许从可变的head_sha反推 provenance。结构化的 PR 生命周期(merged/closed)会退役对应分支。
四、Terminal run 的 custody recovery:把"滞留"的分支精确归还
当一个 run 进入 TERMINAL 状态且存在未发布的 pipeline 提交(moved head)时,分支被 pipeline "托管"却再也不会被发布——必须有一个显式的受保护出口。这就是--recover。
4.1 决策矩阵
Recover的完整决策矩阵与 fail-safe 规则写在 sync.go 的 doc comment 中(P 指 preserved pipeline head,即 terminal 验证过的 run head,由私有 recovery ref 固定):
| relation | worktree | 默认行为 | --keep-local |
|---|---|---|---|
| equal | any | 本地锚定,归还 custody | 相同 |
| ahead | any | 本地锚定,归还 custody | 相同 |
| behind | clean | 严格快进到 P,归还 custody | custody 留在本地 head,gate 以 CAS 重置到它 |
| behind | dirty | 拒绝(先 commit/stash) | 同上 |
| diverged 且 P 含全部本地工作 | clean | 锚定 pre-recovery 本地 head 后 fail-closed 移动到 P,归还 custody | 同上 |
| diverged 且 P 含全部本地工作 | dirty | 拒绝(先 commit/stash) | 同上 |
| diverged 且 P 不含全部本地工作 | any | 拒绝(锚定并人工协调 / 受保护 rerun) | 同上 |
| P 缺失 | any | 拒绝 | 同上 |
4.2 三种基础场景
- 取消且 head 未移动(
user_owned):TERMINAL run 若从未改变提交头(head_sha == submitted_head_sha,无 push、无 custody 戳),选择逻辑会保留它可见,避免误报为blocked_wrong_branch,并分类为user_owned——无next_action、非阻塞退出、永不表述为可恢复的 custody,--recover在此是幂等 no-op(不修改任何文件、ref 或数据库行),新的axi run或单独授权的直接 push 永不被阻塞(见 sync.go)。 - 本地可达(equal/ahead):本地锚定即可,不需要 gate 访问;但若 gate 可用,其既有 recovery ref 必须与记录 head 一致。
- 本地不可达(behind/diverged):从 run 专属的 gate recovery ref 验证并 fetch preserved head;只有干净的 behind worktree 才会被快进。
terminalization 固化:pipeline 在移除托管 worktree 之前,会把每个已验证的未发布 head 固定到refs/no-mistakes/recover/<run>(见 internal/custody/refs.go)。恢复时读取该 run 专属 ref 而非要求 gate 分支匹配,因此 abort、rebase、pre-push 失败都保持可恢复,同时独立移动的 gate 分支不受影响。历史遗留的记录 head 若仍以 dangling gate 对象存在,会在恢复时被锚定。
4.3preservedContainsLocalWork:内容包含性证明,而非 patch-id
被取消的验证经常留下一个本地分支的 rebase作为 preserved head——同样的逻辑提交换了新 SHA,equality 与 ancestry 只能读出"分叉"。此时干净的 diverged worktree 只有在preservedContainsLocalWork证明"P 已包含本地全部变更"时才会被采纳(sync.go):
- 证明方式是可执行的
git merge-tree三方合并:以merge-base为唯一可靠锚点(绝不使用runs.base_sha——那是上一个 gate head,对重新推送过的分支会携带本地从未有过的 pipeline 提交),将本地分支合并进 preserved head,要求结果树恰好等于preserved head 的树; - 它刻意不使用 patch identity:patch ID 丢弃 hunk 位置与空白,无法区分"真正的重放"与"对另一个相同代码块的同形状编辑",基于它的包含性断言不构成证明;
- 一切无法判定的情况都升级处理,包括 rebase 的修复轮同时改写了操作者的行——此时没有任何东西能区分"有意的 pipeline 修复"与"被丢弃的改动",由操作者决定(fail-safe 优先于便利)。
4.4recoverAdoptPreserved:自带守卫的原子移动
采纳(adopt)路径先把 pre-recovery 本地 head 锚定到refs/no-mistakes/recover-local/<run>,然后用 Git 操作自带的失败语义移动分支(sync.go):
git update-ref <branch> <preserved> <observed>:原子 CAS,并发提交移动了分支则整体拒绝、零副作用;git read-tree -m -u <observed> <preserved>:拒绝覆盖被修改或未跟踪的工作树文件;若拒绝,分支通过反向 CAS 回滚。
这绝不是"先观察再reset --hard"的 check-then-act——后者会摧毁观察与执行之间的空隙里落地的一切。doc comment 还明确记录了一个已知的有界 Git 根本限制(两条命令之间的并发 checkout 窗口),但它不会造成数据丢失:包含性证明在移动前已完成、pre-recovery head 保持锚定、custody 永不盖章,操作 fail-closed 为报告失败而非虚假成功。
4.5--keep-local:显式丢弃未发布提交的唯一出口
当操作者选择保留 behind/diverged 的本地 head 而不是采纳 preserved head 时,--keep-local(sync.go):
- 绝不触碰 worktree;
- 以原子 CAS 把 gate 分支移动到保留的本地 head(并发 gate push 获胜则恢复拒绝);
- 通过gate 侧的 fetch暂存对象(
refs/no-mistakes/custody-return/<run>)——绝不是 push,因为 push 会触发 gate 的 receive hook 并启动一个新的 pipeline run; - 独立移动过的 gate head 先被固定到
refs/no-mistakes/recover-gate/<run>,CAS 永不丢弃它。
对于"已验证的 recorded head 同时不存在于 worktree 与可访问 gate"的场景,--recover --keep-local是操作者显式丢弃未发布 pipeline 提交的唯一路径;不可验证的 head、不可访问的 gate、冲突的 ref 一律保留人工协调,普通--recover仍拒绝。
4.6recovery_archives:追加式归档的窄路径
一个 diverged 的较新 head 还可以通过恰好一条追加式recovery_archives数据库绑定来证明被保存,但前提是它的 repository、run、branch、required/preserved heads、原始 archive ref 与 gate recovery ref全部重新验证通过(见 sync.go)。recoverySourceAvailable是唯一的发现/分类 owner,且只提供recover_custody+--recover --keep-local——归档 head永远不会被选中作为工作结果。任何类似的未绑定 ref、stale、moved、symbolic、malformed、跨库、歧义记录都 fail closed。
五、评审后头部连续性与推送绑定
5.1assertPipelineHeadContinuity:每个后评审步骤的入口守卫
固定 pipeline 顺序(Test、Document、Lint、Push、PR、CI)中 Review 之后的每一个步骤都在入口调用assertPipelineHeadContinuity(internal/pipeline/steps/common_fix.go),它是该语义的唯一 owner:
- 比较的是记录的 head(
sctx.Run.HeadSHA,由 daemon 进程内 Run 结构持有、仅由 no-mistakes 的 commit 代码推进)与实时的 worktree HEAD(git.HeadSHA),绝不从可变 worktree 反推锚点(否则就是循环且可被击败); - equal 或 descendant 的 live head 继续;backward、sibling(旁支)与不可验证的 head 在步骤做任何工作之前失败——例如并发的
git reset把共享 worktree 拨到丢失已评审提交的旁支,文档步骤就不会在 clobber 之上再提交并发布。
5.2review_approved_head_sha:可验证的不可变推送绑定
一次成功完成的完整评审会原子地记录runs.review_approved_head_sha;parked、failed、skipped 与历史遗留的评审不携带任何推断的授权。Push 步骤读取这份持久绑定,只允许推送"恰好该提交或其后代",并推送已验证的不可变 SHA而非可变的HEAD(internal/pipeline/steps/push.go)。任何情况下都不得从runs.head_sha、worktree、gate ref 或远端分支推断评审授权。若评审批准 head 与待推 head 之间失去连续性(非 equal/非 descendant),assertReviewApprovedPushHead直接拒绝发布——CI 修复路径的处理方式是重新验证,而不是绕过发布。
六、rebase 基线与 force-push 安全(数据丢失防护)
这个工具存在的全部意义就是不弄丢用户的代码:宁可拒绝推送并暴露 finding,也不用任何"聪明的恢复"来冒险(完整论证见 internal/pipeline/steps/forcepush.go 的注释)。
6.1 rebase 基线只来自新鲜权威 ref
rebase 基线只来自刚 fetch 的权威远端 ref,绝不使用本地或陈旧状态。同时,构建在未推送的本地默认分支提交之上的分支会以NeedsApproval+AutoFixable=false暂停(detectBundledLocalDefaultCommits,#283),而不是静默扩大 PR 范围。
6.2resolveForcePushDecision:唯一的 force-push 路由
每一次 force-push 都经过resolveForcePushDecision(forcepush.go),它重新读取live 远端 head,仅在以下情况放行:
- 远端分支不存在(
newBranch,普通 push); - 远端已等于要推的 head(
upToDate,无需推送); - 远端未变,仍是
lastSeenSHA; - 远端当前所有提交都已被(按 patch-id 内容等价)并入新 head,或属于 run 已知重写的历史(
^baseSHA)。
任何其他情况一律拒绝;ls-remote/fetch失败也 fail closed——绝不在没有显式锚点的情况下退化为裸--force/--force-with-lease。remoteCommitsNotIncorporated(forcepush.go)使用rev-list --cherry-pick:干净的 rebase 会重写 SHA 但保留 patch-id,因此 pipeline 真正重放过的提交会被识别为"已并入",而只在远端存在过的提交会被报告为"即将被丢弃"。
6.3lastSeenSHA:必须是被观察过的 head,而不是 live 远端 tip
lastSeenSHA必须保持为 run最后观察/产生的 head——来自当前 run 或先前 run 的推送 provenance,或 remote-tracking ref——而绝不是live 远端 tip:
- rebase 步骤只在普通push 时刷新
origin/<branch>,不在force push 时刷新; - CI 修复在本地提交并回到 Review 重新验证;后续 Push 步骤负责它们的远端更新与 force-push 安全。
两个必须永不回归的历史 bug:
- #281:把 lease 锚定到"推送前立刻读取"的 SHA——它总是通过,等于没有任何保护;
- 在 force push 时总是 fetch 该分支——这等于重现 #281。
6.4 回归测试矩阵
相关回归测试的分布与意图(可在对应文件与 e2e 中检索验证):
- 分支同步网络预算:
TestRefreshSlowSuccessfulLsRemoteDoesNotStealFetchBudget、TestRefreshSlowButSuccessfulLsRemoteAloneExceedsItsOwnBudgetReportsOffline、TestRefreshRaisedRemoteTimeoutAcceptsTheSameLegitimateSlowLsRemote、TestRefreshParentCancellationStopsFetchAfterLsRemoteSucceeds、TestServiceRemoteTimeoutDefaultsToConfigDefault、TestLoadGlobal_InvalidBranchSyncRemoteTimeout(internal/branchsync/sync_test.go); - 评审后连续性:
TestPostReviewStepsRefuseHeadClobberAtEntry、TestPushStep_RefusesPostReviewClobberWithoutLaterPipelineCommit、TestPushStep_BindsRemoteAndDatabaseToVerifiedCommitWhenHEADMovesDuringPush、TestExecutor_FullRereviewReplacesApprovalWithoutAuthorizingParkedRound; - force-push 安全:
TestPushStep_RefusesToClobberAdvancedUpstreamBranch(#305)、TestForcePushRun_RefusesToClobberOutOfBandBranchCommit、TestRebaseStep_DetectsUnpushedLocalDefaultBranchCommits(#283)、TestResolveForcePushDecision_*、TestExecutor_CIRestartRevalidatesBeforePush、TestPushStep_AllowsForcePushAfterMidRunRebaseOverPriorPushedGeneration(#837)、TestPushStep_AllowsForcePushOnRerunOverPriorRunPushedGeneration(#837); - 端到端旅程:
TestAxiBranchSyncJourney、TestAxiCustodyRecoveryJourney、TestAxiCustodyRecoveryAfterRebaseJourney、TestAxiPrePushAbortUnmovedHeadCustodyJourney。
公共操作指引由 internal/skill/skill.go 与 live AXI 字符串持有(见 internal/cli/axi_guidance.go),并通过make skill重新生成。
七、操作命令速查与适用边界
| 场景 | 命令 | 说明 |
|---|---|---|
| 查看分支同步状态(被动,不联网) | no-mistakes axi status/no-mistakes sync --check | 只读本地证据与缓存分类 |
| 执行受保护同步 | no-mistakes axi sync | 严格快进或等价 diverged 锚定推进 |
| 归还滞留 run 的分支 custody | no-mistakes axi sync --recover | 采纳 gate 保存的 preserved head |
| 保留本地 head、显式丢弃未发布提交 | no-mistakes axi sync --recover --keep-local | 不触碰 worktree,CAS 移动 gate 分支 |
| 绑定归档为 keep-local 恢复证据 | no-mistakes sync --bind-archive-ref <ref> | 仅限refs/heads/archive/*的精确提交,不修改任何 Git ref |
| 提交意图开始新一轮验证 | no-mistakes axi run --intent "<what the user set out to accomplish>" | user_owned/custody_returned之后的标准下一步 |
约束与边界:--check与--recover互斥;--keep-local必须与--recover同用;branch_sync_remote_timeout是全局唯一配置项(默认 60s),仓库级.no-mistakes.yaml无法覆盖;服务只同步发起调用的 worktree,并要求检出的是精确分支(detached HEAD 直接拒绝);Sync/Recover都会先做 gate 嵌套执行上下文拒绝检查。所有路径的共同底色是同一句话——不做任何可能丢失代码的"聪明"操作,无法判定时把决定权交还给操作者。
【免费下载链接】no-mistakesgit push no-mistakes项目地址: https://gitcode.com/GitHub_Trending/no/no-mistakes
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考