dsh-merging-stacked-prs
依赖 PR 栈(A ← B ← C)的官方落地法:为什么必须用 gh stack,而不是逐个 merge 再 retarget。
这是 .agents/skills/dsh-merging-stacked-prs/SKILL.md 的中文精读。它管的是「PR 栈」的落地:A ← B ← C 这样一串互相依赖的 PR,怎么合进 master 才不把历史搅乱。
什么时候触发
把一串依赖 PR 落到 master、合并一个 base 是另一个开放 PR 分支的 PR,或请求里出现「stacked PRs」「PR stack」「dependent PRs」「按顺序合几个相关 PR」时使用。核心要求:每条同仓库的依赖链,落地前必须使用 GitHub 官方的 stacked-PR 功能,让 GitHub 来管栈级规则、CI、顺序、retarget 和合并状态。
通过 GitHub 原生栈对象和 gh stack merge 落地依赖 PR。不要用 gh pr merge + gh pr edit 逐个合并再手动 retarget 去复刻栈语义。根 AGENTS.md 拥有允许的 merge-forward 与 rebase 历史规则;栈上 review 修复的传播另有 cookbook 指南管。
一、要求原生栈支持
改动任何 GitHub 状态之前先跑 gh stack --version。官方扩展或服务端栈功能不可用就硬停——不要退化成逐个手动合并加 retarget。GitHub 栈要求每个 head 分支都在同一个仓库,跨 fork 的链同样硬停。
用一个干净的专用 worktree。拉当前 PR 元数据和精确 head OID,而不是信任分支名或旧报告:
$ gh pr view <pr> --json number,author,baseRefName,baseRefOid,headRefName,headRefOid,isCrossRepository,state,isDraft,reviewDecision,mergeStateStatus,statusCheckRollup
对每条疑似链里的至少一个 PR 查 PullRequest.stack 和 stackEntry.position——这个官方对象(而不是 base 分支推断)才是栈成员资格的权威。size 超过返回页大小就分页 entries(原文给出完整 GraphQL 查询)。从 live PR base 建立自底向上的预期顺序:最底层指向主干,上面的每层指向下方的 head 分支。
二、把缺的成员链进栈
先把已有栈条目与预期链对比。一个已有栈可以包含所请求链的保序子集;出现多个栈号、意外条目或冲突顺序,先问用户再动。
有依赖 PR 还没进官方栈时:
- 逐个精确比对
author.login; - 作者全一致,就按自底向上顺序自动链接:
gh stack link --base <trunk> <bottom-pr> <next-pr> ... <top-pr>; - 作者不一致或查不到,先问用户是否链接,再动 GitHub 状态;
- 重新查 GraphQL,要求:一个栈号、预期主干、完整的 PR 集合、预期的位置与 base 链。
绝不自动解散、重排或重建已有栈;gh stack link 是只增的,已合并或已入队的条目无法出栈。
三、只在需要时刷新
不要因为「有刷新机制存在」就重写分支。live 合并状态或仓库规则确实要求跟上主干时,两种被允许的历史二选一:
- 原生级联 rebase:本地没跟踪就先
gh stack checkout <pr-or-stack>,然后gh stack sync。这个命令可能在本地验证之前就把每一层 rebase 并租约保护地 force-push 出去——所以之后要立刻检查重写范围、为每个受影响层跑相关检查,通过前不合并也不声称就绪。sync 遇到 rebase 冲突时用gh stack rebase,解决并验证后用gh stack push发布。checkout 或 sync 报本地与远端栈构成分歧时,取消并询问,不要自动删了重建远端栈。 - 增量 merge-forward:把主干合进最底层受影响分支,再自底向上把每个更新过的父层合进它的子层,正常 push。合并进行中 base 又前进了,按「增量 retarget」决策记录保住那个检查点,再合更新的顶端。
review 之后允许任何历史重写,但它会使 commit-OID 假设失效:push 后重新拉精确 heads,重新审计未解决的 review 线程、approval、可合并性和检查。永远不用裸 --force,永不覆盖并发推进过的远端 head。
四、合并前检飞
合并前一刻重新查官方栈。要求每个选中的 PR:open、非 draft、顺序符合预期、满足仓库的 review 与检查要求。每个 PR 的状态独立判断——顶层 ready 证明不了它的依赖 ready。
「落地整个栈」就是选整个栈。部分落地必须显式给出边界 PR,范围是从最底层到该边界(含)的每一层。
五、通过栈 API 合并
$ gh stack merge <stack-number> --yes --merge # 整栈 $ gh stack merge <boundary-pr> --yes --merge # 显式请求的部分落地
不要传 --delete-branch,不要手动 retarget 依赖方,不要发逐个 PR 的合并命令。GitHub 会自底向上合并选中范围,并 retarget/rebase 剩余上层。直接栈合并是 all-or-nothing;主干用 merge queue 时,GitHub 把选中范围一起入队,但可能分批落地。
不要绕过合并要求。原生合并报阻塞就通过归属的 PR 检查并解决,或停下汇报——绝不退回 gh pr merge。
六、验证落地后的状态
等每个选中的 PR 报 MERGED——已入队不是已落地:
$ gh pr view <pr> --json number,state,mergedAt,mergeCommit,baseRefName,headRefName $ gh pr list --state open --base <branch> --json number --jq length # 删分支前必须为 0
部分落地要重查官方栈:每个剩余 PR 仍按预期顺序链接,指向栈主干或下一层;重新检查当前 heads、review 状态和 CI——GitHub 可能已经 rebase 了剩余层。
删分支只在所有对应 PR 报 MERGED 之后、作为单独的收尾步骤进行;删除前要求 GitHub 报告没有任何开放 PR 还拿它当 base,任何非 0 结果都阻止删除。
清单
- 原生
gh stack支持可用;每个 PR 分支在同一仓库; - live PR base 与精确 head 确立了一条自底向上的依赖链;
- GraphQL 报出唯一官方栈,主干、条目、顺序符合预期;够格的同作者未链接链已自动链接;
- 任何被重写的层都通过了相关验证,之后重新审计了 review 线程、approval、可合并性与检查;
- 整栈(或显式划界的前缀)通过
gh stack merge --yes --merge提交; - 每个选中 PR 报
MERGED;剩余上层仍构成预期的官方栈; - 删分支只发生在「已合并 + 零依赖」验证之后。