mcpField
G
gitkit

gitkit

github.com/mimukit开发2271 次安装2 天前更新
代码仓库

关于 gitkit

gitkit 是一项基础技能,为其他技能或直接使用者统一 Git worktree、分支基线和同步规则。其核心操作使用原生 Git,并按仓库声明的命令执行 worktree 设置。

主要功能

  1. 01创建、接管、查找和移除 worktree
  2. 02解析基线分支
  3. 03通过变基或合并同步分支
  4. 04执行仓库声明的 worktree 设置
  5. 05清理分支并恢复丢失的工作
  6. 06构建和重排分支链

适用场景

  1. 01为 Issue 分支创建 worktree
  2. 02将功能分支同步到最新基线
  3. 03准备或移除 worktree
  4. 04恢复重置或变基后丢失的工作

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/mimukit/skills --skill gitkit

安装后在 Claude Code 中输入/gitkit确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

clean.md
prepare.md
rescue.md
stacks.md

详细介绍

gitkit

把工具链中其他所有技能反复推导的 Git 信息集中在一处:工作树放在哪里、叫什么名字,如何创建、接管和移除,哪个分支是基准分支,以及何时变基、何时合并。

这里的核心并不复杂。所有操作都使用原生的 git worktree,不依赖任何厂商的 CLI 或 GUI,也不会在 Git 自身之外记录状态。这正是重点:装有桌面开发工具的笔记本电脑和无头 Linux 机器运行完全相同的命令,因此不必维护第二套代码路径,也无需考虑“降级模式”。

这项技能有两个分支不属于该核心,特此说明。堆叠分支会调用 GitHub 的 gh stack 扩展,因此只有这个分支需要 gh 和 GitHub 远程仓库。若不满足条件,它会退回到普通 Git 操作,而不是失败。prepare 会运行仓库为新工作树声明的命令,但不会自行选择任何命令:仓库没有声明时,只会执行其锁文件所对应的安装操作,不会做更多事情。本文档中的其他内容都不依赖这两个分支。

gitkit 是一项基础技能。它通常由另一项技能调用,而不是由人直接点名运行。

何时触发

  • worktree。“为这个分支创建一个工作树”、“为 #42 建一个工作树”、“issue-42-… 的工作树在哪里”、“拆除这个工作树”、“列出我的工作树”。
  • 基准引用。“这里的基准分支是什么”、“比较一下我和基准分支的差异”。
  • 同步。“这里应该变基还是合并”、“将这个分支与 main 同步”、“这个分支落后了,更新到最新状态”、“让这个 PR 基于最新的基准分支,方便审查”。
  • prepare。“设置这个工作树”、“安装并初始化这个工作树”、“为这个工作树创建开发数据库”、“重新准备一次”。
  • clean。“清理已合并的工作树”、“哪些分支可以删除”、“这个仓库里全是废弃分支”。
  • rescue。“我丢了一个提交”、“变基把我的工作弄没了”、“我重置错了分支”、“这个储藏里有什么”。
  • stack。“把这个分支堆叠在 #43 上”、“从 #43 的分支开始创建 #44”、“重新堆叠这条链”、“显示堆叠分支”。
  • **由另一项技能调用。**任何需要工作树、基准引用、分支名称或同步决策的技能,都会委托给这里并使用返回结果。

**不属于这项技能的内容:**提交、创建 PR、审查差异、评判代码。gitkit 只回答在哪里、如何操作,从不判断应该做什么更改。

模式

模式用途所在位置
worktree创建、接管、查找、列出和移除工作树下文
sync将功能分支更新到与其远程分支一致,使其基于最新的基准分支,并使用租约强制推送下文
prepare在工作树中运行仓库声明的设置命令,并在移除前运行清理命令prepare.md 和下文
clean清理已合并的工作树和分支,每项操作都单独确认clean.md
rescue查找疑似丢失的工作成果,并将其恢复到新分支rescue.md
stack通过 gh stack 构建并重新堆叠分支链stacks.md 和堆叠分支

worktree 和 sync 直接写在此处,因为几乎每次运行和每项调用技能都会用到它们。其他四种模式只会在触发时加载。

如果无法从请求中明确判断模式,就根据动词选择,并说明选择了哪种模式。 合并后说“整理一下”指的是 clean;在落后于基准分支的分支上说“整理一下”则指的是 sync。在第一句话中点明模式,能让用户在任何操作运行前纠正错误选择。

约定

路径
$WORKTREE_ROOT/<repo-basename>/<local-branch-name>

$WORKTREE_ROOT 默认值为 ~/worktrees;如果已设置该环境变量,则应遵循其值。<repo-basename> 是主检出目录的名称。

分支名称中的斜杠会转换为连字符。 分支 mimukit/6-marketing-site 对应的目录是 mimukit-6-marketing-site,而不是嵌套的 mimukit/ 父目录。这样,每个仓库都只有一层目录,因此 ls "$WORKTREE_ROOT/<repo>" 就能完整列出所有内容,无需遍历目录树。代价是目录名称无法再反向还原为分支名称,但这毫无影响,因为始终通过 git 查找,而不会通过读取路径来查找。极少数情况下,如果分支 a/b 和 a-b 都被转换为 a-b,git worktree add 会因路径已存在而失败:报告此情况,让人来选择名称,不要悄悄追加后缀。

工作树位于仓库之外,绝不放在仓库内部的 .worktrees/ 目录中。仓库内的工作树会被纳入 Docker 构建上下文、绑定挂载、文件监视器和测试 glob 中,而这些问题都会在远离根因的地方显现。

不变量:一个分支,一个工作树

一个分支最多对应一个工作树,目录以分支命名。 工作树与分支关联,而非与工作流阶段关联;因此,实现某项功能和之后审查其拉取请求会使用同一个工作树,因为它们属于同一个分支。

这不是风格偏好。Git 会强制执行此规则:

$ git worktree add ../wt-b feature-x
fatal: 'feature-x' is already used by worktree at '.../wt-a'

任何按阶段命名工作树的方案(review-…、qa-…)一旦两个阶段操作同一个分支就会彻底失败,而这很常见,并非边缘情况。

后果:

  • 始终按分支查找,通过 git 查找,绝不读取或猜测目录路径。这使得上述扁平化方式毫无代价,也让位于旧根目录中的工作树可以继续正常使用,无需改动。
  • 工作树的“类型”会自动随之而来,因为分支名称已经体现了它。issue-42-auth-capability-module 之所以这样命名,是因为工作开始时,分支就是这个名称。
分支命名
  • issue-<n>-<slug> 用于从跟踪器问题开始的工作。slug 取自问题标题:如果标题符合常见的 type(scope): summary 格式,则去掉前缀;将其余部分转换为 kebab-case,在单词边界处截断至约 50 个字符,并去掉末尾的连字符。slug 为空时,退化为单独的 issue-<n>。数字可保证唯一性,因此无需任何平局处理。
  • hotfix-<slug> 用于直接修补基准分支的紧急修复。slug 取自问题症状,转换为 kebab-case,限制在约 40 个字符。此名称不带数字,因为 hotfix 是从问题症状出发,而非从跟踪器问题出发;如果确实能追溯到某个问题,分支仍使用这种格式,而拉取请求会带有 Closes #<n> 链接。如果与现有分支冲突,则添加一个简短的区分词,而不是数字。分隔符是连字符:写作 hotfix-<slug>,绝不能写成 hotfix/<slug>。 斜杠会让 git 将 hotfix 视为引用命名空间中的目录,因此 hotfix-restore-session 和 hotfix/restore-session 不能同时存在;而所有根据分支名称识别类型的技能(commitkit、prkit)都只匹配连字符格式。
  • pr-<n>-<slug> 仅用于复刻仓库的拉取请求,此时本地还没有分支,必须创建一个分支来保存获取到的 head。slug 取自 head 分支名称,转换为 kebab-case,限制在约 40 个字符。
  • 其他情况使用人提供的分支名称或仓库约定的名称。gitkit 不会重命名分支。

Agent 自行创建分支时,必须使用上述三种格式之一。 它绝不会另创其他前缀(feature/…、fix-…、claude/…)。除此之外,只能使用人输入的名称或仓库文档中规定的名称。

对于同一仓库中的拉取请求,本地已经有分支名称:直接使用它。为已存在的分支另造一个 pr-* 名称,正是导致上述“两个工作树对应一个分支”问题的原因。

worktree

这四项操作都使用原生 git,且全部都具有幂等性,因此运行两次很正常,绝不能报错或破坏工作。

查找分支的 worktree

其他所有操作都基于这一项查找原语:

git -C "$REPO" worktree list --porcelain

每条记录都包含一行 worktree <path>;当某个分支已检出时,还会包含一行 branch refs/heads/<name>。匹配分支即可获取路径。如果 porcelain 的输出格式发生变化,请查看 git worktree list --help,不要猜测;但应将 worktree/branch 配对视为稳定格式,这正是 porcelain 输出的用途。

创建或接管

始终先查找。**如果该分支已经有 worktree,就接管它:报告路径并停止。**绝不创建第二个,也绝不报错。

如果没有:

WT="${WORKTREE_ROOT:-$HOME/worktrees}/$(basename "$REPO")/$(printf '%s' "$BRANCH" | tr / -)"   # slashes flatten to dashes
git -C "$REPO" fetch origin --prune
git -C "$REPO" worktree add -b "$BRANCH" "$WT" "$BASE"
  • 始终先获取更新。基于过时的基准分支进行分支创建不会立即报错,只会在之后产生冲突。
  • $BASE 来自基准引用,绝不能硬编码为 origin/main,也不能使用同级功能分支。唯一例外是堆叠分支,其基准分支就是下方的分支,由调用者明确指定。
  • 如果本地分支已存在(例如你已获取的同仓库 PR 分支,或之前创建的分支),则去掉 -b;git worktree add <path> <branch> 会检出该分支。
  • 对于 fork 拉取请求,先用 git fetch origin "pull/<n>/head:pr-<n>-<slug>" 将头部分支获取到本地分支,再基于该分支添加 worktree,不要使用 -b。

然后记录 gitkit 创建的内容,因为后续会话没有其他办法区分自己的 worktree 和别人的 worktree:

touch "$(git -C "$WT" rev-parse --absolute-git-dir)/gitkit-created"   # gitkit made this worktree
git -C "$REPO" config "branch.$BRANCH.gitkitCreated" true             # only after -b: gitkit made the branch too
  • 两条记录都存储在 git 自身的存储中,因此不需要状态文件。worktree 标记位于 .git/worktrees/ 下的 worktree 管理目录中;git worktree remove 会在移除 worktree 时将其删除。分支键位于分支的配置节中;git branch -d 会在删除分支时将其删除。
  • **仅当本次运行使用了 -b 时才写入分支键。**已存在的分支属于创建它的人;被接管的 worktree 不会获得标记。
  • **没有记录就表示不是 gitkit 创建的。**在此规则生效前创建的 worktree 或分支,以及手动创建的 worktree 或分支,都会被视为已接管;移除时会保留它们。

当调用者将 worktree 交给某个人或 Agent 使用时,无论 worktree 是新建还是已接管,都要接着运行 prepare。仅运行一条命令的临时 worktree 则跳过此步骤。

报告路径和分支。当用户直接向 gitkit 请求 worktree 时,回复末尾要附上工作区块。创建 worktree 并不意味着要在其中执行任何操作。到 $WT 路径下的 $BRANCH 分支已有 worktree 且记录已写入时,操作即告完成。

工作区块

将 worktree 交给人类使用的技能,回复末尾都要附上此区块。标签固定不变;填写对应的值。

Workspace ready
- repo: <absolute path of the main checkout>
- worktree: <absolute worktree path>
- branch: <branch>
- base: <base ref the branch was cut from or tracks>
- state: created | adopted
- for: issue #<n> | PR #<n> | none
  • **此区块是与宿主的约定,而不是与工具的约定。**它不指明任何运行环境或应用。能够打开绑定到 worktree 的新会话的宿主会读取此区块;其他宿主则显示这六行纯文本。
  • **它不会启动任何操作。**其中不包含提示词,也不包含要在 worktree 中运行的命令。人类自行决定如何开始工作。
  • **仅面向人类,只输出一次,并放在最后。**当另一个技能调用 gitkit 时,将路径和分支返回给调用方,由调用方在自己的交接内容中输出此区块。无人值守运行不输出区块,因为它会在自己的会话中通过路径操作 worktree。
移除
test -e "$(git -C "$WT" rev-parse --absolute-git-dir)/gitkit-created"   # gitkit made this worktree?
git -C "$REPO" config --get "branch.$BRANCH.gitkitCreated"              # gitkit made this branch?
git -C "$REPO" worktree remove "$WT"
git -C "$REPO" branch -d "$BRANCH"

这是整个集合中唯一的移除规则。 每个会移除工作树或删除分支的技能,无论是这里的清理操作、工作区工具的清理,还是问题关闭流程,都遵循并引用此规则,而不是重述自己的规则。以下四条规则各自防止一种真实的工作丢失方式:

  • 工作树有未提交内容时,停止拆除。 明确展示将会丢失的内容(未提交的更改、未跟踪的文件、未推送的提交),并让人类决定。绝不擅自使用 --force。
  • 只移除标记表明由 gitkit 创建的工作树。 没有标记的工作树是被接管的:那是其他人的工作上下文,你不知道其中打开了什么。
  • 只删除设置了 gitkitCreated 键的分支,并使用 -d 删除。 这样 Git 会拒绝删除未合并的分支,因此错误判断会明确失败,而不会删除工作成果。
  • 每次移除都要单独确认。 每个工作树和每个分支都是独立的决定,因此一份列表中的每一行都需要单独确认,不能一次确认整组。
  • 已准备的工作树要先执行拆除流程。 当 gitkit-prepared 标记存在且工作树声明了 teardown: 时,在检查未提交内容并获得确认后、执行 git worktree remove 前运行该流程。拆除流程失败时,保留工作树并报告失败。拆除流程中载有具体步骤。

压缩合并例外是 gitkit 唯一会运行 -D 的情况。 压缩合并的分支无法通过 -d 删除,因为压缩提交没有指向该分支的祖先关系。只有当 gh 报告该分支对应的拉取请求已合并,且该拉取请求的 headRefOid 与本地分支尖端一致时,才使用 -D:

gh pr list --head "$BRANCH" --state merged --json number,headRefOid --jq '.[0]'
git -C "$REPO" rev-parse "$BRANCH"     # must equal headRefOid, or -D is off the table

SHA 一致,证明该分支上的每个提交都包含在已合并的拉取请求中。合并后移动过的尖端包含合并时未出现的提交,因此保留该分支,并在报告中注明。确认该行时,要写明拉取请求编号。

已合并 PR 例外会针对某个 PR 自己的工作区,解除标记规则和确认规则。 当人类要求调用方合并或关闭某个拉取请求,而调用方随后看到它已合并时,该指令即为移除该 PR 头部分支对应工作树和本地头部分支的确认。无论它们是否由 gitkit 创建,都会被移除,无需再次询问。调用方传入 PR 编号,此例外仅适用于该分支及其工作树。以下保护措施仍然有效,每项都会保留对应对象并在报告中注明:

  • 人类要求保留。 “合并它,但保留工作树”表示保留工作树;“保留分支”表示保留分支。
  • 工作树有未提交内容。 上述未提交内容规则仍会阻止移除。
  • 拆除流程失败。 上述拆除规则仍会保留工作树。在此例外下,拆除流程无需询问即可运行。
  • 分支尖端已移动。 使用 -d 删除;只有尖端与已合并 PR 的 headRefOid 一致时,才按压缩合并例外的规定使用 -D。
  • 有其他分支依赖此分支。 以此分支为基础的开放 PR 会使分支保留,但工作树仍会移除。

如果要移除的工作树就是调用方当前所在的工作目录,请在主检出目录中使用 git -C "$REPO" 运行所有命令。移除后,说明会话的工作目录已不存在,并指出可在主检出目录中继续操作。

已经不存在?报告“已不存在”并视为成功。与接管操作一样,拆除流程也是幂等的。每个已确认的工作树和分支都被移除,或其无法移除的原因已报告,流程即告结束。

列出
git -C "$REPO" worktree list

纯粹的清单。用户要求整理而非查看时,应使用 clean 模式;该模式会先对每一行进行分类,再提供可执行的操作。

如果移动或还原后路径看起来不对,git worktree repair <path> 会重写反向指针。Git 会在 .git/worktrees/<name>/gitdir 和每个工作树的 .git 文件中存储绝对路径,因此手动移动工作树后总是需要修复。

基准分支

绝不要假设基准分支是 main。实际使用中,仓库的默认分支可能是 develop、trunk 或 master,选错会悄无声息地生成空差异或巨大差异。按以下顺序逐级确认:

  1. gh repo view --json defaultBranchRef --jq .defaultBranchRef.name:如果已安装并通过身份验证的 GitHub CLI 可用,这是权威来源。
  2. git symbolic-ref --short refs/remotes/origin/HEAD:如果已设置,这是本地记录。
  3. 运行 git remote set-head origin --auto 修复未设置的 origin/HEAD,然后重试上一步。大多数实现都会跳过这一步,而正是这一步能将失败转化为答案。
  4. 选择实际存在的 origin/main 或 origin/master。
  5. 询问用户。到这一步为止,不要再猜。

后续所有操作都使用这个答案:git diff <base>...HEAD、领先/落后提交数,以及工作树分支的切出来源。

堆栈层是唯一有意设置的例外,但仍要执行上述逐级确认。 堆栈层的基准分支是它下方的分支,而不是仓库默认分支,因此由调用方指定该分支,不对它执行上述确认。仍需通过上述步骤确定堆栈最底层所基于的主干。其他场景仍严格禁止以同级分支为基准,因为这条规则要防范的是意外情况:从碰巧处于检出状态的分支切出工作树。层的基准分支之所以合法,是因为有人明确说明了分支及其原因。请参阅堆叠分支。

变基还是合并

将功能分支与其基准分支同步时,这里只有一个默认做法,而且与是否已创建拉取请求无关:

将功能分支变基到基准分支上来完成同步,无论该分支是否已发布。合入基准分支是例外,必须说明理由并征得用户同意。

默认采用变基,是因为这样的提交历史更清晰:功能分支呈现为一连串线性的自身提交,基准分支上的工作则位于其下,而不是交织其中。每次同步都把基准分支合进来的仓库,会为每次同步累积一个合并提交;在功能分支合入之前,它自身的开发历程就会变得难以理解。

变基确实有实际代价,而且这是操作层面的,不是审美层面的:它会改写提交 SHA。一旦拉取请求已创建,其评审讨论串就会锚定到这些提交,因此强制推送会将所有讨论标记为过时,包括人工和机器人留下的发现。这项代价应该如实说明,而不是作为阻碍,因此同步完成推送后应报告受影响的数量。这也是用户通常会提出合并例外的原因。

实际操作如下:

git fetch origin
git rev-list --left-right --count "origin/$BASE"...HEAD   # "<behind>\t<ahead>"; left > 0 means behind
git rebase "origin/$BASE"
  • 除非有人针对这个分支另有说明,否则应选择变基。 无论没有拉取请求、已有未关闭的拉取请求,还是共享分支,建议都一样。
  • 同步应直接执行到底,无论分支是否已发布。 用户要求同步,就等于同意对该分支进行变基并执行 --force-with-lease 推送,因为这是针对同一个分支的一项决定,而用户的请求已经包含了这项同意。只有遇到问题时才暂停并询问:工作区不干净、出现需要裁定的冲突、门禁检查失败,或租约被拒绝。
  • 推送后报告代价。 对于有未关闭 PR 的分支,请在变基前查询 GraphQL reviewThreads 连接中的未解决讨论串,并在交接说明中写明数量。“3 个评审讨论串现已过时”能提醒用户重新请求评审;“某些评论可能已过时”则没有提供任何有用信息。
  • 只有用户提出请求时才采用合并例外。 未解决的讨论串不是暂停并提出合并方案的理由。用户要求合并时,请执行合并例外。
  • 使用 --force-with-lease,绝不使用裸 --force。 租约可以防止你覆盖他人在你变基期间推送的提交。
  • 发生冲突时,按冲突处理。
冲突

Agent 会解决每个冲突并写入每项解决方案。人类绝不会编辑发生冲突的文件。每个冲突唯一需要决定的问题是:是否需要先由人类裁定;而裁定只是从 Agent 已经写出的解决方案中选一个。

无需询问的情况:冲突属于机械性冲突,也就是说,双方意图都能完整保留:

  • 双方各自添加独立条目(导入、导出、路由、列表项、变更日志行)。两者都保留,并按文件中的顺序排列。
  • 双方做出相同的更改。 保留一份即可。
  • 一方只更改格式。 保留另一方的内容,然后运行仓库的格式化工具处理该文件。
  • 锁文件或生成文件。 使用基线版本,然后用仓库自己的命令(pnpm install --lockfile-only、代码生成脚本)重新生成。
  • 冲突仅发生在上下文中。 基线修改或移动了周围的行,而分支的更改可以原样应用到新代码上。

先询问的情况:解决方案属于决策,而非合并:

  • 双方以无法同时成立的方式更改了相同的行为。
  • 修改/删除冲突、重命名/重命名冲突,或二进制文件冲突。
  • 解决方案会丢弃任一方添加或保留的代码。
  • 无法用一句话说明双方各自的意图。此检查用于发现你不理解的解决方案,因此每个冲突都要检查,包括机械性冲突。

询问时应说明文件、用一句话分别说明双方的意图、你推荐的解决方案和另一种方案,并分别展示它们会生成的代码。一次变基暂停时产生的所有询问,都应合并为一轮提问。由你亲自写入选定的解决方案。

每项解决方案都必须遵守以下要求:

  • 在首次变基前保存起点: git update-ref "refs/gitkit/pre-sync/$BRANCH" HEAD。每次同步都会覆盖它。ORIG_HEAD 无法在第二次变基后保留;此引用可通过 git reset --hard "refs/gitkit/pre-sync/$BRANCH" 撤销整个同步。
  • 根据双方内容构建每项解决方案。 整体采用某一方的策略(-X ours、-X theirs、对源文件运行 git checkout --ours)会在不留记录的情况下丢弃另一方,因此只能用于解决锁文件和生成文件冲突。
  • 每次运行 git rebase --continue 前都运行 git diff --check。 它会标出遗留的冲突标记。
  • 使用 rerere 记录解决方案。 运行 git -c rerere.enabled=true rebase …,这样下次同步或重新整理堆栈时,就会重放已解决的冲突。
  • 由门禁检查结果。 解决冲突属于代码更改,因此之后会运行仓库的测试与构建门禁。已解决文件中的门禁失败由你负责修复:重新处理解决方案并再次运行门禁。只有在修复需要做出上述某项决策时才询问。
  • 无人值守的调用方无法回答。 无人可问时,运行 git rebase --abort,并将每项尚未解决的询问及其推荐方案返回给调用方以便升级处理。收到裁定后恢复运行时,重新执行同步并写入相应解决方案。
sync

上述规则的可执行形式。一次同步包含 两个阶段,顺序如下:先让本地分支与其在 origin 上的对应分支保持一致,然后再更新到基分支。跳过第一阶段会导致之后的 --force-with-lease 被拒绝,或悄悄丢弃从另一台机器推送的提交。在该分支的工作树中运行。每一步都说明了结束条件。

  1. 确认起点。 使用基分支引用查找顺序解析基分支。运行 git -C "$WT" fetch origin --prune。确认工作树位于功能分支上,并且 git status --porcelain 为空。工作树有未提交更改时应停止同步:报告相关文件,让人工执行 stash 或提交。使用 git update-ref "refs/gitkit/pre-sync/$BRANCH" HEAD 保存起始位置。结束条件:已确认基分支名称、分支名称,且工作树干净。
  2. 与 origin/$BRANCH 同步。 当远程分支存在时,运行 git rev-list --left-right --count "origin/$BRANCH"...HEAD。落后计数为 0 表示无需获取提交。否则运行 git pull --rebase origin "$BRANCH",将本地提交重放到远程提交之上,并按照冲突处理所有冲突。没有对应远程分支时跳过此步骤。结束条件:本地分支包含 origin/$BRANCH 上的所有提交。
  3. 计算与基分支的差距。 运行 git rev-list --left-right --count "origin/$BASE"...HEAD。落后计数为 0 表示分支已基于最新的基分支:如果上一步获取了提交,则运行下方的刷新已准备好的工作树步骤,推送获取的提交,报告结果,然后停止。结束条件:得到落后计数和领先计数。
  4. 统计评审讨论串。 当拉取请求处于打开状态时,通过 GraphQL 的 reviewThreads 连接统计未解决的讨论串。此步骤不发起任何询问;统计结果会写入交接报告。结束条件:得到讨论串数量,或确认没有打开的拉取请求。
  5. 变基到基分支。 运行 git -c rerere.enabled=true rebase "origin/$BASE"。结束条件:变基成功,或因冲突而停止。
  6. 解决所有冲突。 每次停止时,使用 git diff --name-only --diff-filter=U 列出文件。按照冲突对每个冲突分类,编写机械性解决方案,并针对其余冲突进行一轮提问。编写按规则确定的解决方案,运行 git diff --check,暂存文件,然后运行 git rebase --continue。对所有剩余的冲突重复此操作。结束条件:没有剩余冲突标记且变基完成;如果是无人值守的调用方,则以中止变基并返回未解决问题结束。
  7. 刷新已准备好的工作树。 当 gitkit-prepared 标记存在且此次同步获取了提交时,按照 prepare.md 执行 prepare 的更改检查及其选定的操作块。此步骤在门禁之前运行,因为如果门禁在过时的安装或过时的架构上运行,就会因错误原因而失败。操作块失败时,应在推送前停止同步。结束条件:操作块返回退出代码、没有受监控的更改,或标记不存在。
  8. 验证分支仍可正常工作。 运行仓库自己的测试和构建门禁。失败时报告输出,并在推送前停止。结束条件:通过或停止。
  9. 使用租约推送。 运行 git push --force-with-lease origin "$BRANCH"。租约被拒绝表示有人在你变基期间推送了提交:执行 fetch,展示新提交,并在重试前征求同意。结束条件:远程分支与本地分支一致。
交接

报告从 origin/$BRANCH 获取的提交、基分支、落后和领先计数、刷新的内容、门禁结果、推送的分支,以及此次推送导致过时的评审讨论串。逐项列出已解决的文件、所属类别和一句话理由,并标明由人工裁定的文件,同时将 refs/gitkit/pre-sync/$BRANCH 作为撤销点。说明分支已是最新且没有任何更改的情况。接下来,基于新的基分支审查拉取请求差异;如果有讨论串过时,则重新请求评审。

合并例外

当用户选择合并,或明确要求合并时,它仍然是一次同步,并且会明确说明这一点:

git merge "origin/$BASE" -m "chore(repo): sync with origin $BASE"
  • 绝不使用 git 的默认主题。 如果没人选择消息,Merge branch 'main' into issue-42-tailwind-4-foundation 就是 git 写入的内容,并且它会永远以这种形式出现在 git log 中。固定主题就是这条消息。
  • 插入基准分支,不要硬编码 main。 基准引用 的优先级列表之所以存在,是因为 develop 和 trunk 仓库确实存在;在这类仓库中,主题写着 main 就是错误的。
  • 不要使用 --no-ff。 如果同步可以快进,说明分支没有自己的内容需要保留,git 根本不会写入提交,因此也就没有主题需要修正;强行创建合并提交只会凭空制造一个空提交。
  • 在提出请求时同时说明原因。 “由于这个 PR 有 3 个未解决的评审讨论,因此选择合并而不是变基”就是原因。“为了安全而合并”不算;如果你说不出变基会破坏什么,就进行变基。
绝不直接使用 git pull

当分支落后于远程分支时,git pull 默认会执行合并,并且会在未经任何人询问的情况下写入 Merge branch 'main' of github.com:owner/repo。请使用 git pull --rebase;如果希望同步失败时明确报错,而不是自动解决,则使用 git pull --ff-only。调用 git pull 来刷新基准分支时也一样:请进行配置或传入标志,绝不要让它按默认行为运行。

prepare

根据仓库在 Agent 指南文件的 ## Worktree setup 部分声明的命令,让工作树做好运行准备:首次运行时执行 setup:,监视路径发生变化时执行 update:,以及在 Remove 删除工作树前执行 teardown:。工作树管理目录中的 gitkit-prepared 标记会记录每次运行所基于的提交。只有存在该标记时才会执行 teardown,因此 gitkit 绝不会移除它未设置过的资源。

相关流程、声明格式、模板和锁文件表格都在 prepare.md 中。当运行准备工作树、同步对已准备的工作树应用了提交,以及移除操作涉及已准备的工作树时,请阅读该文件。

clean

清理已完成的工作树和分支,包括本机和 origin 上的内容。它会将每个工作树、本地分支和远程分支归入一个类别:active、adopted、dirty、reapable、orphan,并且每次只移除一个已确认的 reapable 项目。删除远程分支需要单独确认,并且要单独证明该分支的工作已落地。

有两点让它不只是一个 git branch --merged 循环,这两点都详见 clean.md:squash 合并的分支在提交祖先关系中不可见,因此“已合并”需要三种检测方式,而不是一种;逐项确认是有意为之,因为清理列表中的项目并非都同样适合删除。当运行实际执行清理时,请阅读该文件。

清理所做的每项移除操作都遵循 Remove 规则。它会逐项单独确认,仅在 squash 例外情况下使用 -D,绝不触碰没有 gitkit 标记的工作树,并且遇到有未提交更改的工作树时会停止。在 origin 上,它绝不删除基准分支,也绝不删除开放中的拉取请求的头部分支。

rescue

找回看似丢失的工作并恢复它。错误的变基、硬重置、已删除的分支、找不到的 stash:相关提交通常仍然存在,git 自己的日志也会指出它们去了哪里。

相关流程详见 rescue.md。有两条规则决定了整个流程:在恢复之前,它是只读的;恢复操作会新增分支,而不是移动现有分支,因为正是重置操作让工作丢失。该文件也会明确说明限制,而不会暗示操作绝对安全:不可达的提交会一直存在,直到 git 清理它。

救援期间,绝不要以任何模式运行 git gc、git prune 或 git reflog expire。

堆叠分支

堆叠是同一仓库中的一串分支:每个分支都从它下方的分支切出,最底层则基于 trunk。适用于当前工作依赖尚未合并的工作成果:无需等待前置工作的拉取请求合并,就可以从它切出分支并继续推进。

GitHub 原生支持这一功能,而 gh stack 扩展则负责驱动它。**这是 gitkit 中唯一需要 gh 和 GitHub 远程仓库的部分。**完整的命令接口、预检和降级路径都在 stacks.md 中;只有当一次运行实际涉及堆叠时,才需要阅读。以下内容则是每次运行都需要了解的部分。

每一层各有一个工作树

每一层都保留自己的工作树,并从下方那一层切出。一个分支对应一个工作树这一不变规则没有改变:一层就是一个分支,因此会有一个以该分支命名的目录;仍按分支查找工作树。

另一种做法是让整个堆叠共用一个工作树,再用 gh stack up 和 down 在其中的各层之间移动。这是该扩展自身导航功能所采用的方式。但本项目集合并不这样做,因为这里的每个调用方都将工作树关联到正在处理的问题,而一个堆叠包含多个问题。因此,gh stack 的导航命令不在适用范围内;up、down、switch、top 和 bottom 也不由 gitkit 执行:它们会移动一个已被另一个工作树占用的检出目录。

gitkit 在此负责什么,不负责什么

gitkit 只负责底层操作:创建堆叠、添加一层、重新堆叠、同步、检查,以及将现有分支纳入堆叠。以下两条命令则有意排除在外,因为它们越过了此技能在其他方面遵守的职责边界:

  • **gh stack submit 会创建拉取请求。**gitkit 从不创建任何内容。这属于负责编写拉取请求的技能。
  • **gh stack merge 会合并拉取请求。**gitkit 从不执行合并;而堆叠合并会进行级联,一次合并多个拉取请求,因此需要人工确认并逐一列出这些拉取请求。这属于负责合并的技能。

这里特意指出责任方,而非命令本身:gitkit 负责做好准备,发布则由其他人负责,无论分支是否属于堆叠。

堆叠深度的代价

堆叠每增加一层,就多一个需要审查的拉取请求,以及一个每当下方内容发生变化时都需要变基的分支。重新堆叠会级联进行,因此底层发生变化时,上方每一层都会被重写。

**这里有意不设定具体数量。**堆叠应有多深,取决于每一层的规模和审查速度,应由人来决定。他们询问时,再说明具体机制,让他们自行确定合适的深度。

容器和远程环境

Git worktree 会存储绝对路径。如果仓库在容器中的绑定挂载路径与主机上的路径不同,其中的每个 worktree 都会损坏。

  • 挂载 $WORKTREE_ROOT 和主检出目录两者。worktree 的实际 git 目录位于主仓库的 .git/worktrees/ 下;只挂载 worktree 会导致检出损坏。
  • 在容器内外都挂载到相同的绝对路径。这是最稳妥的方案。
  • 另一道保险措施,适用于 git 2.48 及更高版本:git config worktree.useRelativePaths true 和 git worktree repair --relative-paths。只要仓库和 worktree 根目录的相对位置不变,相对路径指针就能在路径映射变化后继续有效;因此最好让两者的相对位置保持固定,例如 ~/code/<repo> 和 ~/worktrees/<repo>。

欢迎使用能管理 worktree 的 GUI 开发工具来发现这些 worktree。Orca 及同类工具会读取 git worktree list,通常也提供设置,以显示外部创建的 worktree。这只需在运行 GUI 的机器上进行一次 UI 配置,也是机器之间唯一的不对称之处。

任何技能都不得通过厂商 CLI 创建或移除 worktree:一旦这样做,两台机器就会出现分歧。厂商对 worktree 自有的元数据,例如卡片关联的问题、状态或评论,则属于不同层面,可以使用,因为这些内容只存在于该工具中,没有对应的 git 信息会产生分歧。只要 worktree 本身仍在这里创建和销毁,配套技能就可以协调这一层(orcakit 会为 Orca 这样做)。gitkit 绝不会调用这类工具:它必须能在未安装此类工具的机器上继续工作。

注意事项

  • gitkit 是其所管理内容的唯一事实来源。 调用它的技能可以制定策略,决定何时发起请求,例如 PR 是否值得拉取,或分支是否已准备好发布,但绝不能规定 git 操作如何执行。
  • 可以重述结论;重述推导过程才是问题。 每个公开技能都是独立安装到仓库中的,而这些仓库可能没有 gitkit。因此,调用方需要 git 信息时,必须携带足够的信息才能继续工作,而这项要求优先于整洁性。两者的界限在于:调用方可以陈述 gitkit 的答案以及降级备用方案(“gitkit 负责同步规则,结论是 rebase;没有 gitkit 时,就 rebase 到基准分支上”),但不能复述得出该答案的推理过程,即完整的基准引用逐级回退规则、worktree 路径公式,或 rebase 与 merge 的比较论证。答案只有一行,重命名不会悄然使它失效;推导过程才是会逐渐偏离的部分。值得参考的合规示例:prkit 和 mergekit 都会说明 rebase 这一结论及其一次确认规则,但不会重新推导;wikikit 和 designkit 都将 gh repo view --json defaultBranchRef 指定为基准引用的备用方案,但不会附带五级回退规则。
  • gitkit 从不实现、提交或打开任何内容。 它只负责准备环境并清理环境。prepare 执行的命令属于仓库,而非 gitkit 自身。在 worktree 中编写代码、提交和发布都属于其他技能。
  • 破坏性步骤必须先预览并确认。 移除 worktree 和删除分支时,都要展示即将执行的操作并等待确认。清理工作依附于移除操作的确认。创建、接管和同步则直接执行。同步会重写已发布的分支,但请求同步的用户已经做出了决定,租约会保护远程端,而 refs/gitkit/pre-sync/$BRANCH 可以撤销此操作。
  • 没有可用的 Shell(例如基于浏览器的 Agent)?那就无法运行 git。根据用户提供的信息进行判断,并以代码块形式输出准确的命令供其运行;绝不要声称创建或移除了你无法实际操作的 worktree。

评论

支持 Markdown