Next.js 仓库中的 gh-stack 命令行为详解:从 init 到 merge 的前置条件、副作用与失败模式
【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js
在 Next.js 仓库的.agents/skills/目录中,gh-stack是一个面向 Agent 与开发者协作的技能(skill),其参考文件 commands.md 专门记录gh stack各子命令的前置条件、副作用、原子性与失败模式——即--help不解释的那部分行为。读完本文,你能掌握:gh stack每个命令在什么条件下失败、失败时以什么退出码退出、命令之间哪些操作是原子的哪些不是,以及如何把 Next.js 仓库这种多分支协作场景下的分支栈(stacked branches / 依赖式 PR 链)稳定地驱动起来,包括在脚本、CI 和 Agent 会话中安全使用它的规则。
1. 技能定位:文档明确划定自身的"解释边界"
gh stack是 GitHub CLI 的扩展,用于管理堆叠式分支与堆叠式 PR:一个栈是一根以 trunk(主干分支)为根的有序分支链,每个分支有一个基于其下方分支的 PR,评审者因此只看到本层的 diff。栈的展示方式是 trunk 在左、自左向右向上:
(main) <- auth <- api <- frontend左侧是底层(先合入),右侧是顶层(最后合入);up朝栈顶、远离 trunk,down朝 trunk。基础层(如认证、模型)放底部,依赖它的代码放其上。
commands.md 开篇就声明了自己的定位,这是使用整套文档的前提:
gh stack <command> --helpis authoritative for flags and arguments.(gh stack help <command>only prints the top-level help.)This file only covers behavior--helpdoes not explain: preconditions, side effects, atomicity, and failure modes.
即:参数和标志以<command> --help为权威;注意gh stack help <command>是不起作用的——它只打印顶层帮助(这一点在 SKILL.md 的 "More detail" 一节再次强调)。commands.md只补充--help覆盖不到的行为面:前置条件、副作用、原子性、失败模式。这也决定了本文的组织方式——逐命令拆解这四类信息。
2. 环境准备与非交互使用规则(来自技能主文档)
在进入逐命令语义之前,先给出 SKILL.md 中定义的三行安装配置,这是所有命令的适用前提:
gh extension install github/gh-stack git config rerere.enabled true # remember conflict resolutions git config remote.pushDefault origin # required if the repo has more than one remote其中后两条配置与commands.md的内容直接相关:
rerere.enabled:init命令会启用git rerere(见第 4 节);预先配置可跳过首次运行在 TTY 下的确认提示。remote.pushDefault:当仓库存在多个 remote 时,push、submit、sync、rebase、link都必须显式带--remote <name>;checkout和trunk没有--remote标志,只能依赖该配置。
Next.js 仓库将gh stack明确按**"stdout 是否为 TTY"** 来分流行为:管道场景下多数命令干净报错或打印静态文本;而 PTY 下同样命令会打开提示或全屏 TUI 并永久阻塞。Agent 会话的 harness 各不相同,因此技能要求始终显式传标志。SKILL.md 给出的强制规则表如下(左列"总是这样跑"、中列"永远不要裸跑"、右列原因):
| 总是运行 | 不要裸跑 | 原因 |
|---|---|---|
gh stack view --json | gh stack view | PTY 下会打开 TUI |
gh stack submit --auto | gh stack submit | 每个新 PR 会提示输入标题 |
gh stack merge <target> --yes | gh pr merge | gh pr merge无法合并栈 |
gh stack init <branch>... | gh stack init | 会提示输入分支名 |
gh stack add <branch> | gh stack add | 提示输入名字,且管道下也会失败 |
gh stack checkout <target> | gh stack checkout | 打开选择菜单 |
gh stack up/down/top/bottom | gh stack switch | switch仅菜单操作 |
| — | gh stack modify | 仅 TUI,没有非交互路径 |
补充两条边界说明:view --short在两种模式下都安全,但它是面向人类排版的,机器解析请用--json;当本地已存在一个覆盖相同分支但构成不同的栈时,checkout <pr>无法强制覆盖——先gh stack unstack --local(保留 GitHub 上的栈),再重试。
一个典型的核心工作流(同样来自 SKILL.md):
gh stack init auth # create the stack and check out its branch git add ... && git commit -m "Add auth middleware" gh stack add api # next layer, branched from the current one git add ... && git commit -m "Add API routes" gh stack submit --auto # push every branch and open draft PRs gh stack view --json # confirmsubmit加--open可让 PR 直接进入可评审状态而非草稿。分支名按字面使用:gh stack add refactor/foo创建的分支名字就是refactor/foo,不做任何前缀或转换。
3.init:自底向上建立整条链,并顺手启用rerere
commands.md对init的记录要点:
- 一次
init铺整条链,并检出最后一个分支:gh stack init auth api frontend会依次创建(或接管)auth、api、frontend,最后停在frontend(栈顶)上。 - 参数按从底到顶的顺序处理:已存在的分支被接管(adopt),而不是重建;第一个分支若不存在则从 trunk 创建,其后每个新分支都从它前一个分支创建。没有单独的 "adopt 模式"——分支是否存在决定行为。
--base用于选择非默认的 trunk。init会启用git rerere。在 TTY 下,仓库内的首次运行会要求确认;提前git config rerere.enabled true可跳过。
"存在即接管"这条设计在重组栈时非常有用:troubleshooting.md 的 "Restructuring a stack" 一节正是利用它——先unstack拆掉本地追踪与 GitHub 分组,改写 Git 祖先关系后重新init,已有分支会被复用,已有 PR 也会保留。
4.add:只能在栈顶新增,退出码 5 是主要失败信号
commands.md中add的行为约束是四条,每一条都对应一个具体的失败模式:
- 必须从栈的顶层分支运行(栈还空着时,从 trunk 运行)。在其他任何分支运行都会以退出码 5退出,并打印
can only add branches on top of the stack。正确做法是先gh stack top。 - 未提交改动会被带走:不带
-Am时add不触碰工作区,已暂存与未暂存的改动会跟着你进入新分支。想要干净起点就先提交或 stash。 add -Am在当前分支还没有任何提交时是就地提交,而不是建分支——典型场景就是init之后立刻执行。这是刻意设计:栈的第一层通常要先有内容,第二层才有意义。-A与-u互斥,且两者都要求-m。
对照 SKILL.md 的退出码表,退出码 5 的语义是 "Invalid arguments",恢复方式是"修正调用方式,参见<command> --help"。而 stack-design.md 进一步给出为什么要刻意管理暂存区:建议直接用git add/git commit而非add -Am快捷方式,以便精确控制哪些改动落在哪个分支——例如先在模型分支暂存并提交模型文件,再gh stack add api-routes,然后在新分支上暂存 API 文件:
git add internal/models/user.go internal/models/session.go git commit -m "Add user and session models" gh stack add api-routes git add internal/api/routes.go internal/api/handlers.go git commit -m "Add user API routes"每个分支允许多次提交;关键是同一分支的每个提交服务同一关注点,属于另一关注点的改动必须去另一个分支。
5.push与submit:两种推送语义,原子性完全不同
这是commands.md中"原子性"信息最集中的部分。
push:一次 multi-ref 推送,但不原子
- 一次性以 multi-ref 推送所有活动分支(未合并、未入队的),每个分支带
--force-with-lease。 - 非原子:可能出现"部分分支已更新、另一分支被拒绝"的状态。被拒绝意味着该分支在远端被改动过——修好那个分支后重跑即可,重跑是安全的,已经落地部分会被跳过。
push从不创建或更新 PR;需要 PR 语义时用submit。
submit:推送 + 建 PR + 链接成 Stack
submit依次完成:推送每个活动分支 → 为没有 PR 的分支创建 PR(基于其第一个未合并的祖先)→ 在 GitHub 上把它们链接成 Stack。四个关键行为:
- 非原子。分支按序推送,每个分支
--force-with-lease;若后面某个推送被拒绝,前面的推送和 PR 更新保留。修复拒绝后重跑同一条命令。 - 完全合并的栈无法被延长。当当前栈的所有 PR 都已合并时,
submit会把剩余未合并分支分叉成一个新栈,以 trunk 为根在 GitHub 上创建,原合并栈保持不动。 --auto的标题生成规则:单提交分支用该提交的 subject 作标题、body 作 PR 正文;多提交分支则把分支名"人性化"(连字符与下划线变空格)。没有自定义标题/正文的标志——之后用gh pr edit修改。--open让新 PR和已有 PR 都进入可评审状态;不带则新 PR 为草稿。- 前置条件:仓库必须启用 Stacked PRs。未启用时,非交互模式下
submit以退出码 9退出(TTY 下会提供创建普通未堆叠 PR 的替代路径)。
对照 SKILL.md 的退出码表,退出码 9 正是 "Stacked PRs unavailable —— 仓库未启用;告知用户"。
6.link:无本地追踪状态的建栈路径
commands.md将link定位为不写任何本地追踪状态(不碰.git/gh-stack文件)的建栈/更新栈路径,专为"分支由其他工具管理、或位于另一个 worktree"的场景设计(详见 troubleshooting.md 的 "Driving stacks from another tool or worktree" 一节,其中明确提到 jj、Sapling、git-town 等工作流)。行为要点:
- 参数自底向上。每个参数是分支名或 PR 号;数字参数先尝试按 PR 号解析,失败再回退为分支名。
- 首参为数字时,仅当存在该编号的栈才按栈号处理。此时剩余参数被追加到该栈顶部,无需重列栈内现有 PR:
gh stack link 7 feature-c。已在栈内的参数跳过;属于其他栈的参数被拒绝。 - 分支参数会被自动推送(非 force、原子);缺失的 PR 用自动生成标题、正确链式 base 创建;base 错误的已有 PR 会被纠正。
- 栈成员关系只增不减——
link从不把 PR 移出栈。
注意一个后果:因为link不写本地状态,基于本地状态的导航命令(up、down、top、bottom)对其结果无效;之后需要本地追踪时用gh stack checkout <stack-number>。
7.sync:八步例行动作,冲突时"全体回滚"
sync是日常最常用的命令,commands.md按顺序记录了它的八步,值得完整理解:
- Fetch:从远端拉取。
- 与 GitHub 上的栈对账:在 github.com 上加入栈的 PR 会被拉取并本地追加;发现分歧(divergence)时,非交互模式下中止(恢复方案见 troubleshooting.md)。
- 快进 trunk:已最新则跳过;分歧则告警。
- 需要时级联 rebase:触发条件是——trunk 前进了、某个栈分支从其远端被快进了、或某分支不再包含其预期父分支。已合并 PR 会被自动处理(squash 合并场景用
--onto规避伪冲突)。发生冲突时,所有分支被恢复到 rebase 前状态,命令以退出码 3 退出。 - 推送所有活动分支,原子地。
- 刷新 PR 状态:从 GitHub 拉取最新。
- 同步栈对象:把 open PR 增量地链接成栈;仅当存在两个及以上 PR 时执行。
sync从不打开 PR——那是submit的职责。 - 清理:删除已合并 PR 对应的本地分支;仅当非交互环境下传入
--prune时发生。
几个值得强调的边界:
- 退出 0 不代表成功同步:SKILL.md 特别指出,当本地与远端栈分歧时,
sync会打印两条链、不做任何改动、以退出码 0 加Sync aborted消息结束。脚本必须检查该消息,或重跑gh stack view --json比对。 - 退出码 3 的恢复:
sync失败时栈已被恢复,直接gh stack rebase重新触发 rebase,然后解冲突--continue(详见第 8 节)。 - 分歧的两条恢复路径(保留远端版:
unstack --local+checkout <stack-number>;保留本地版:unstack+submit --auto)都不删除任何 PR 或分支,但远端 unstack 会保留处于 auto-merge 或 merge queue 中的 PR 的堆叠状态,必要时先清理该状态。
8.rebase:级联重放、squash 合并感知、退出码 7 保护
rebase从远端拉取并做级联 rebase。commands.md的要点:
适用时机:
sync报告冲突后,或只需要 rebase 栈的一部分时。--upstack:从当前分支向栈顶 rebase——编辑底层之后要跑的就是它。SKILL.md 的标准片段是:gh stack down # or: gh stack checkout api git add ... && git commit -m "Add get-user endpoint" gh stack rebase --upstack # replay every branch above onto the change gh stack top # return to where you were gh stack push--downstack:从 trunk rebase 到当前分支。--no-trunk:跳过 fetch 与 trunk rebase,只做栈分支之间的相互对齐。--continue:解决完暂存冲突后续跑;--abort:恢复每一个分支(不只是当前分支)。squash 合并自动感知:已合并 PR 会被自动检测,并用
--onto针对正确目标重放,因此 squash 合并的父分支不会产生伪冲突(troubleshooting.md 的 "After a squash merge" 一节印证:sync检测后以--onto重放并跳过已合并分支,view --json中该分支报告"isMerged": true, "state": "MERGED",无需手工操作)。rebase 进行中的再入保护:此时启动新的 rebase 以退出码 7退出(对照 SKILL.md 退出码表:恢复方式为
gh stack rebase --continue或--abort)。
退出码 3 的标准恢复流程(rebase与sync都以此退出,但状态不同:失败的sync已把栈恢复原状,失败的rebase则停在半途等待处理):
gh stack rebase # exit 3 — conflicted paths are listed on stderr git add <resolved paths> gh stack rebase --continue # repeat if the next branch also conflicts由于init启用了rerere,同一冲突解决过一次后会自动重放——这在栈中很常见,因为栈底部的改动会被 rebase 穿过其上方的每一个分支;没有rerere时,重复冲突可能在每一层都要手工解决。
9.view与checkout:机器可读状态与解析优先级
view
--json把机器可读负载写到stdout,状态消息走stderr——脚本不要解析 stderr,应按退出码分支。--json的 schema(记录在 SKILL.md):trunk string currentBranch string branches[] name, head, base, isCurrent, isMerged, isQueued, needsRebase branches[].pr number, url, state ("OPEN" | "MERGED" | "QUEUED"); absent when no PR exists其中
base是该分支最后已知包含的父分支的保存 SHA,可能比父分支当前 tip 旧;当当前父分支 tip 不再是该分支的祖先时needsRebase为真。裸
view在 stdout 为 TTY 时打开全屏 TUI,管道时打印静态文本。--short打印每分支一行的紧凑摘要、从不打开 TUI,但它是人类排版,解析请改用--json。view会尽力而为地从 GitHub 刷新 PR 状态作为副作用——API 不可达时不失败。
checkout
接受栈号、PR 号、PR URL 或分支名,解析优先级与副作用如下:
- 裸数字的解析顺序:先栈号,再 PR 号,最后分支名。
- 栈号、PR 号、PR URL 都会从 GitHub fetch、拉取分支并在本地建立栈。
- 分支名只对本地已追踪的栈解析,从不接触 GitHub;要拉取本地未追踪的栈,必须用栈号或 PR 号。
- 若本地已存在覆盖这些分支但构成不同的栈,
checkout无法强制越过——先gh stack unstack --local再重试。 checkout没有任何标志,多 remote 时依赖remote.pushDefault。
10.unstack与merge:拆组不删 PR,合并全有或全无
unstack
只移除栈的分组,从不删除 PR 或分支:
- 无参数时作用于活动栈(包含当前分支的那个),同时移除 GitHub 分组与本地追踪。
- 带栈号时,从仓库任意位置、无论是否本地追踪,都通过 API 工作;存在本地追踪时一并移除。
--local只删本地追踪,从不接触 GitHub;对一个本地未追踪的栈号组合--local是错误。- 未知栈号以退出码 2 退出。
merge
commands.md中merge是最强调"整体性"的命令:
- 作用域由参数决定:传 PR 号 → 合并该 PR 及其下方所有未合并 PR;传栈号 → 合并该栈内所有未合并 PR。
- 全有或全无(all-or-nothing):该合并集合中任何一个 PR 无法合并,则一个都不合并,并报告原因。
- 合并方式来自
--squash、--rebase、--merge或--merge-method <method>;都不传则复用上次使用的方法。 - 合并前只检查基础 PR 状态:open 且非草稿。栈不支持绕过合并要求(merge requirements)。
- base 分支上的 merge queue 压倒一切:栈会被加入队列而非直接合并;队列选择合并方式,你传入的方式标志会被忽略并告警。入队 PR 是一起提交的,但随队列处理可能分组落地,而非一次性全部合入。
gh pr merge无法合并栈,必须始终用gh stack merge(SKILL.md 的非交互表亦强制:裸gh stack merge会提示确认,脚本中应写gh stack merge <target> --yes)。
示例:
gh stack merge 42 --yes # PR #42 plus every unmerged PR below it gh stack merge 7 --yes # every unmerged PR in stack #7 gh stack merge 42 --yes --squash # or --merge, --rebase, --merge-method <method>11. 导航命令:永远非交互,边界钳制与合并分支跳过
commands.md的 "Navigation" 一节很短但信息密度高:
up、down、top、bottom、trunk永远是非交互的;up/down接受步长(gh stack up 3)。- 移动在栈边界处钳制(clamp);从活动分支导航时跳过已合并分支,因此
bottom落在最低的未合并分支上。 gh stack switch是纯选择菜单,没有非交互路径——脚本中应使用上述命令替代(SKILL.md 的非交互表也把switch列入"永远不要裸跑")。
12. 退出码全景:Agent 与脚本如何分支处理
commands.md在命令叙述中散落出现的退出码(2、3、5、7、9),与 SKILL.md 中完整的退出码表拼在一起,构成完整的机器可判定语义:
| 退出码 | 含义 | 恢复方式 |
|---|---|---|
| 0 | 成功 | — |
| 1 | 通用错误 | 读 stderr |
| 2 | 不在栈中(含unstack遇到未知栈号) | gh stack init,或gh stack checkout <target> |
| 3 | rebase 冲突 | 解冲突后rebase --continue;sync场景栈已恢复,重跑rebase |
| 4 | GitHub API 失败 | 检查gh auth status,重试 |
| 5 | 参数错误(含"非栈顶执行add") | 修正调用,参见<command> --help |
| 6 | 需要消歧(分支同属多个栈) | 检出该栈内不共享的分支后重试 |
| 7 | rebase 已在进行 | gh stack rebase --continue或--abort |
| 8 | 栈文件被锁 | 另一gh stack进程在写入;约 5 秒后重试 |
| 9 | 仓库未启用 Stacked PRs | 告知用户;非交互下submit以此退出 |
| 10 | 需要 modify 恢复 | gh stack modify --abort |
两条与锁和 TUI 相关的补充事实(来自 troubleshooting.md):退出码 8 对应另一进程持有.git/gh-stack.lock的排他锁,锁约 5 秒超时,持续出现 8 说明仍有进程持锁,先定位并结束该进程;退出码 10 对应被中断的modify会话(modify是 TUI-only,Agent 不应调用),恢复方式为gh stack modify --abort,且submit也会检测 pending modify 状态并在 TTY 下询问是否用本地状态覆盖 GitHub 上的栈。
退出码 6 的典型场景是当前分支同时是多个栈的 trunk,没有任何标志可以消歧:gh stack checkout <a-branch-unique-to-the-intended-stack>后重试;而接受显式栈号的命令(merge 7、unstack 7)因为不从当前分支推断栈,天然绕开该问题。
13. 三个参考文件的分工:按需加载而非预载
gh-stack技能目录是 Next.js 仓库技能体系 中"hub + 细节文件"模式的实例:SKILL.md 是入口(setup、非交互规则、核心循环、退出码、JSON schema、约束),三个 references 各自对应一类触发条件:
- stack-design.md —— 建栈之前读:定层数、定每层内容、判断工作是否该进新栈。核心观点包括:栈是依赖链,依赖必须与代码同分支或在更底层,且规划期满足远比事后重组便宜(因为没有非交互的原地重排,改顺序意味着
unstack+init);分支命名推荐<topic>/<concern>(如billing/schema、billing/api),但用户与仓库的分支命名规范优先;"一个栈讲一个故事"——同一功能用单栈,无关工作开新栈。 - commands.md ——本文主体:命令失败得意外、或需要其前置条件/副作用/原子性/顺序保证时读。
- troubleshooting.md —— 出 rebase 冲突、squash 合并之后、本地远端分歧、重组栈、或其他工具驱动栈时读。
SKILL.md 明确:"Open the reference whose trigger matches the task; no need to preload all three."(打开触发条件匹配的那份参考即可,无需三份全读)。这个技能也在 create-pr 技能 中被交叉引用为"管理堆叠分支与依赖式 PR"的入口,说明它是 Next.js 仓库 PR 工作流技能图谱中的一环。
14. 关键约束速查
汇总 SKILL.md 的 "Constraints" 与commands.md的行为事实,以下是日常协作中最容易踩中的硬约束:
- 栈是严格线性的:一个父、至多一个子;并行工作用独立栈。
- 没有非交互的重排或移除:报错信息可能提示
gh stack modify,但它只有 TUI——重组请用unstack然后init重建(init会接管已存在分支,PR 不丢失;Git 祖先关系正确后submit会更新 base 并重新链接栈)。 - 改元数据不等于改祖先关系:想调层顺序,先按 troubleshooting.md 的三步
git rebase --onto序列重写祖先,再重建栈;移动分支前先保存旧的边界 SHA。 - PR 标题与正文自动生成,之后用
gh pr edit修改。 checkout <branch-name>只解析本地栈;从 GitHub 拉栈必须用栈号或 PR 号。
15. 小结
commands.md 的价值在于把gh stack从"看起来是普通 git 封装"提升为行为可预测的工具:push/submit非原子但可安全重跑,sync冲突时全体回滚并以退出码 3 报告,rebase有退出码 7 的再入保护,merge全有或全无且受 merge queue 覆盖,link/unstack --local提供不碰 GitHub 或纯 API 的对称路径,checkout的解析优先级决定了何时接触 GitHub。把这些与 SKILL.md 的退出码表和 troubleshooting.md 的恢复剧本合起来,Agent 与 CI 脚本都能在 Next.js 仓库这类大团队协作场景中,可靠地把多分支工作拆成可评审的 PR 层,并在任何失败状态下按退出码走确定的恢复路径。
【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考