Worktrunk Hook 系统实践:Git Worktree 生命周期钩子的配置、模板引擎与执行原理
2026/9/16 19:30:30 网站建设 项目流程

Worktrunk Hook 系统实践:Git Worktree 生命周期钩子的配置、模板引擎与执行原理

【免费下载链接】worktrunkWorktrunk is a CLI for Git worktree management, designed for parallel AI agent workflows项目地址: https://gitcode.com/GitHub_Trending/wo/worktrunk

Worktrunk 的 Hook 是在 worktree 生命周期关键点(切换、创建、提交、合并、删除)自动执行的 shell 命令集合,是它支撑并行 AI agent 工作流的核心机制之一。本文基于仓库内的 hook 参考文档 完整展开:从 10 种 hook 类型与 pre/post 执行模型讲起,覆盖安全审批、项目/用户双层配置、三种 TOML 配置形式、模板变量与过滤器、JSON 上下文,并结合 src/cli/hook.rs、src/commands/hooks.rs、src/config/expansion.rs 等源码印证底层调用链,读完你可以为任意仓库写出一套可运行、可审批、可调试的 worktree 自动化钩子。

一、Hook 类型与执行模型

Hook 是 shell 命令,在 worktree 生命周期的关键节点运行——既会在wt switchwt mergewt remove等命令执行过程中自动触发,也可以通过wt hook <type>按需手动执行。用户级(user)与项目级(project)两层 hook 均受支持。

10 种 hook 类型按「操作 × 阶段」组织,pre-*为阻塞式,post-*为后台式:

事件pre-— 阻塞post-— 后台
switch(切换)pre-switchpost-switch
create(创建)pre-startpost-start
commit(提交)pre-commitpost-commit
merge(合并)pre-mergepost-merge
remove(删除)pre-removepost-remove

两种阶段的行为差异是设计核心:

  • pre-*hook阻塞主流程——命令失败会中止整个操作;
  • post-*hook 在后台运行,输出写入日志(可用wt config state logs查找和管理日志文件);
  • wt hook <type> --foreground可以让本应后台执行的 hook 内联运行,输出直接出现在终端;
  • -v可看到后台 hook 渲染后的模板变量;wt hook <type> --dry-run预览将要执行的命令。

最常用的创建类 hook 是post-start——它在后台运行耗时任务(dev server、文件拷贝、构建),不阻塞 worktree 创建。文档明确建议:除非后续步骤依赖前序工作完成,否则优先用post-start而非pre-start

各 hook 的语义与适用场景(完整继承自参考文档):

Hook用途
pre-switch在源 worktree 中、切换动作之前运行——无论是创建、切到已有 worktree,还是停留在当前
post-switch所有切换结果均触发:创建、切到已有、或停留在当前
pre-start新 worktree 创建时运行一次,阻塞post-start/--execute直至完成:依赖安装、env 文件生成
post-start新 worktree 创建时运行一次,后台执行:dev server、长构建、文件监视、缓存拷贝
pre-commit格式化、lint、类型检查——在任何 Worktrunk 提交(wt step commitwt step squash、以及wt merge产生的提交)之前运行
post-commit触发 CI、发送通知、后台 lint
pre-merge测试、安全扫描、构建验证——在 rebase 之后、合并到目标之前运行
post-merge部署、通知、安装更新后的二进制。若目标分支存在 worktree 则在其内运行,否则在主 worktree
pre-remove删除前的清理:保存测试产物、备份状态。在被删除的 worktree 中运行
post-remove停止 dev server、删除容器、通知外部系统。模板变量引用的是被删除的 worktree

wt merge中的 hook 编排

wt merge期间,阻塞式 hook 按此顺序执行:pre-commit → pre-merge → pre-remove。合并完成后,所有post-*hook同时启动,各自锚定在它所属的 worktree 中——post-mergepost-switchpost-remove锚定在目标端,post-commit锚定在产生提交的那个 worktree。

一个值得注意的边界:如果这次合并会删除post-commit所锚定的 worktree,由于 hook 启动时目录已不存在,post-commit会被报告为skipped而非执行。需要在该 worktree 内完成的工作应放进pre-remove,或用--no-remove保留 worktree。完整合并流水线见 merge 文档。

从源码看,这套「锚定」语义有专门的安全设计:src/commands/hooks.rs 的模块注释将 hook 分为两类执行模型——

  • Plan-backed(防 TOCTOU)pre-mergepost-mergepre-removepost-removepost-switchpre-startpost-start。这些 hook 在审批门与执行之间存在状态变更(rebase 甚至可能改写.config/wt.toml本身),因此审批时会把选定命令冻结成ApprovedHookPlan,执行器只运行这份冻结值,杜绝「审批 A 命令、执行 B 命令」的时间差攻击面;
  • Invocation-resolvedpre-commitpost-commitpre-switchwt hook <type>与 alias。它们依赖「审批与执行之间不会改写配置」加「执行器与审批器共用同一Repository实例的OnceCell配置缓存」两个不变量保证安全。

二、安全:项目 hook 的首次运行审批

项目级命令首次运行需要审批:

▲ repo needs approval to execute 3 commands: ○ pre-start install: npm ci ○ pre-start build: cargo build --release ○ pre-start env: echo 'PORT={{ branch | hash_port }}' > .env.local ❯ Allow and remember? [y/N]

审批规则:

  • 审批结果保存到~/.config/worktrunk/approvals.toml(见 src/cli/config.rs 中wt config approvals子命令的帮助文本);
  • 命令一旦变更,需要重新审批
  • 拒绝会跳过该操作的所有项目命令(包括已审批的),主流程继续执行,且不影响已保存的审批;
  • --yes可绕过提示,适用于 CI 与自动化场景;
  • --no-hooks可整体跳过 hook——注意它被执行 hook 的命令接受(wt switchwt mergewt removewt step commitwt step squash),而不被wt hook本身接受。

审批的增删通过wt config approvals addwt config approvals clear管理。

三、配置:位置、形式与 user/project 分层

配置位置

Hook 可定义在项目配置.config/wt.toml,通常入库共享)或用户配置~/.config/worktrunk/config.toml)中,两者格式相同。项目配置读取自命令实际运行的那个 worktree——src/commands/hooks.rs 强调:无论 hook「关于」哪个 worktree,命令都来自发起命令的 worktree.config/wt.toml(与wt config show读的是同一文件)。仓库内 dev/wt.example.toml 给出了带注释的项目配置样例(wt config create --project可生成同款模板)。

维度项目 hook用户 hook
位置.config/wt.toml~/.config/worktrunk/config.toml
作用范围单仓库所有仓库(或 按项目细分)
审批需要不需要
执行顺序pre-*:在用户 hook 之后;post-*:并行pre-*:最先;post-*:与项目 hook 并行

三种配置形式

Hook 按 TOML 形状分为三种形式:

字符串 = 单条命令:

pre-start = "npm install"

表 = 并发命令:

[post-start] server = "npm run dev" watch = "npm run watch"

流水线 = 有序[[hook]]块。每个块是一个步骤,块内多个 key 并发执行;某一步失败会中止流水线剩余部分:

[[post-start]] install = "npm ci" [[post-start]] build = "npm run build" server = "npm run dev"

此处install先执行,然后buildserver并发。文档建议:大多数 hook 用不到[[hook]]块,只有存在依赖链(典型如「装完依赖才能并发跑构建和 dev server」)时才用它。

流水线中一个精细行为:模板在流水线开始前做语法检查,在每步执行时渲染,因此某一步可以写入 per-branch 变量,供后续步骤经{{ vars.<key> }}读取。由于前序步骤仍可能改变这些值,wt hook <type> --dry-runwt hook show --expanded的预览不解析这类引用,而是原样保留:{{ vars.thing | default('none') }}会预览为{{ vars.thing }}(引用已定义,default不触发),其余变量正常展开;对输入做变换的过滤器仍会运行,只是作用于占位文本,且输出像其他值一样做 shell 转义——{{ vars.thing | upper }}预览为'{{ VARS.THING }}'

user 与 project 的执行合并语义

  • pre-*阻塞主流程,两个来源合并为一条流水线:用户命令先跑,其中失败会跳过项目侧;
  • post-*后台运行时,每个来源是独立的分离流水线——同时启动、互不等待,一侧失败不影响另一侧继续运行;
  • 来源内部顺序仍由[[hook]]块控制,但跨post-*来源不存在顺序;
  • 两条post-*若写同一文件、或在同一 worktree 里跑git,会产生竞态——依赖关系强的命令应放进同一来源。

当 user 与 project 定义了同名 hook 时,可用user:name/project:name语法指定执行哪一个。

四、模板变量

Hook 模板在运行时展开模板变量。完整变量表如下:

类别变量说明
active{{ branch }}分支名;detached worktree 中未定义
{{ worktree_path }}worktree 路径
{{ worktree_name }}worktree 目录名
{{ commit }}分支 HEAD SHA
{{ short_commit }}core.abbrev缩略的 HEAD SHA
{{ upstream }}分支上游(若跟踪远程)
operation{{ base }}基础分支名(仅 switch/create)
{{ base_worktree_path }}基础 worktree 路径
{{ target }}目标分支名
{{ target_worktree_path }}目标 worktree 路径(目标有 worktree 时)
{{ pr_number }}PR/MR 编号(switch/create hook;经pr:N/mr:N切换时)
{{ pr_url }}PR/MR 网页 URL(switch/create hook;经pr:N/mr:N切换时)
repo{{ repo }}仓库目录名
{{ repo_path }}仓库根绝对路径
{{ owner }}主 remote 的 owner 路径(可含 subgroup)
{{ remote_repo }}主 remote URL 中的仓库名,不含.git
{{ primary_worktree_path }}主 worktree 路径
{{ default_branch }}默认分支名
{{ remote }}主 remote 名
{{ remote_url }}remote URL
exec{{ cwd }}hook 命令运行的目录
{{ hook_type }}正在运行的 hook 类型(如pre-startpre-merge
{{ hook_name }}hook 命令名(若已命名)
{{ args }}从 CLI 转发的 token——见 手动运行 Hook
user{{ vars.<key> }}来自wt config state vars的 per-branch 变量

repo类变量在整个仓库内恒定(default_branch在每个 worktree 中相同);active类变量逐 worktree 变化。

裸变量指操作作用对象:switch/create 是目标端,merge/remove 是源端;basetarget给的是另一端:

操作裸变量basetarget
switch/create目标端出发处= 裸变量
commit(merge/squash 期间)被压缩的 worktree= 裸变量集成目标
merge被合并的 feature= 裸变量合并目标
remove被删除的分支= 裸变量最终落点

所有 hook 共享同一视角——{{ branch | hash_port }}post-startpost-remove中产生相同的端口。

cwd的三种例外(平时cwd等于worktree_path):

  • pre-switch:hook 在源 worktree 运行;若目标 worktree 已存在,worktree_path指向目标,而cwd仍是源——创建型切换尚无目标目录,worktree_path也停在源(要在新 worktree 里做事请用pre-start);
  • post-remove:当前 worktree 已删除,hook 改在主 worktree 运行;
  • post-merge:hook 在目标分支的 worktree 运行(目标没有 worktree 时为主 worktree),--no-remove下被合并的 worktree 仍留在磁盘上。

未定义变量会报错——这正是实现选择:src/config/expansion.rs 中 minijinja 环境设置UndefinedBehavior::SemiStrict,对未定义变量的打印/迭代报错,但允许{% if var %}真值判断,既抓住拼写错误又支持可选变量。因此可选行为要写成条件或默认值:

[pre-start] # 若跟踪远程分支则 rebase(如 wt switch --create feature --base origin/feature) sync = "{% if upstream %}git fetch && git rebase {{ upstream }}{% endif %}"

detached worktree 不在任何分支上,branch在其中未定义——由它派生的base/target名同样未定义(无论是手动wt hook,还是源/目标 worktree 处于 detached 的操作:pre-switch中源自 detached 时的base,落入 detached 时的 remove 中的target),适用同样的{% if branch %}守卫。这与wt list --format=json对该 worktree 报告branch: null的行为一致。

调试手段:任一触发 hook 的命令加-v,每个 hook 会打印template variables:块,列出全部在作用域内变量及其值(条件变量未填充时显示(unset),如wt switch -期间的target_worktree_path)。alias 在-v下同样打印自身作用域变量:wt -v <alias>在流水线运行前输出。

变量支持点访问与default过滤器处理缺失键。JSON 对象/数组值会被自动解析,值形如{"port": 3000}{{ vars.config.port }}可直接工作:

[post-start] dev = "ENV={{ vars.env | default('development') }} npm start -- --port {{ vars.config.port | default('3000') }}"

五、Worktrunk 过滤器与函数

模板支持一组自定义 Jinja2 过滤器,实现注册于 src/config/expansion.rs 的template_environment

过滤器示例说明
sanitize{{ branch \| sanitize }}/\替换为-
sanitize_db{{ branch \| sanitize_db }}数据库安全标识符,带哈希后缀([a-z0-9_],最长 48 字符)
sanitize_hash{{ branch \| sanitize_hash }}文件系统安全名,为唯一性附加哈希后缀
hash{{ branch \| hash }}输入的 3 字符 base36 摘要
hash_port{{ branch \| hash_port }}哈希映射到 10000-19999 端口
dirname{{ repo_path \| dirname }}去掉最后一段路径(/a/b/c/a/b
basename{{ repo_path \| basename }}只保留最后一段(/a/b/cc
codename(n){{ branch \| codename(2) }}确定性友好词

细节补充:sanitize_db产出小写字母数字加下划线、不以数字开头、带 3 字符哈希后缀(防撞名与保留字)的标识符;sanitize_hash在净化改变了输入时才追加 3 字符哈希后缀——不同原名永不冲突,已安全的名字原样通过;codename(n)确定性生成友好名,codename(1)为名词、codename(2)为「形容词-名词」,更高数量级继续加形容词,词池约 126 万组合,通常可单独作为 worktree 叶子名(worktree-path 配方 展示了单独使用与放在分支名父目录下的两种用法)。

hash是裸 3 字符 base36 摘要,适合在输出预算紧张(如 Unix socket 路径 107 字节上限)时自组「截断 + 防撞」方案:

# 截断分支 slug + 哈希:前缀相同时冲突仍由哈希区分 worktree-path = "/tmp/{{ (branch | sanitize)[:20] }}_{{ branch | sanitize | hash }}"

dirname/basename适合「裸仓库放在隐藏目录」的场景,如myproject/.git(此时{{ repo }}解析为.git):

# 把 worktree 放在裸仓库同级,命名 <wrapper>.<branch> worktree-path = "{{ repo_path }}/../{{ repo_path | dirname | basename }}.{{ branch | sanitize }}"

hash_port让每个 worktree 的 dev server 使用唯一端口:

[post-start] dev = "npm run dev -- --host {{ branch }}.localhost --port {{ branch | hash_port }}"

任何字符串(含拼接)都可哈希:

# 每个 repo+branch 组合一个唯一端口 dev = "npm run dev --port {{ (repo ~ '-' ~ branch) | hash_port }}"

变量自动做 shell 转义——{{ ... }}外不需要加引号,加引号反而可能在特殊字符上出麻烦。

模板还支持动态查找函数:

函数示例说明
worktree_path_of_branch(branch){{ worktree_path_of_branch("main") }}查某分支 worktree 的路径

该函数返回给定分支 worktree 的文件系统路径,无 worktree 时返回空串(源码实现在 src/config/expansion.rs 的env.add_function中,经Repository::worktree_for_branch查询)。典型用途是引用其他 worktree 中的文件:

[pre-start] # 从主 worktree 拷贝配置 setup = "cp {{ worktree_path_of_branch('main') }}/config.local {{ worktree_path }}"

JSON 上下文:模板之外的复杂逻辑

Hook 还会把全部模板变量作为 JSON 从 stdin 传入,可做模板表达不了的复杂逻辑。模板中未设置的变量在 JSON 中同样缺席,所以可选变量要带默认读取——detached worktree 的branch就没有:

[pre-start] setup = "python3 scripts/pre-start-setup.py"
import json, sys, subprocess ctx = json.load(sys.stdin) if ctx.get('branch', '').startswith('feature/') and 'backend' in ctx['repo']: subprocess.run(['make', 'seed-db'])

拷贝未跟踪文件

一个值得单独点名的命令:wt step copy-ignored。Git worktree 共享仓库对象但不共享未跟踪文件,该命令在 worktree 之间拷贝 gitignore 的文件:

[post-start] copy = "wt step copy-ignored"

六、手动运行 Hook:wt hook <type>

wt hook <type>按需运行 hook——适合开发期测试、CI 流水线、或失败后重跑。完整命令参考(来自参考文档):

wt hook - Run configured hooks Usage: wt hook [OPTIONS] <COMMAND> Commands: show Show configured hooks pre-switch Run pre-switch hooks post-switch Run post-switch hooks pre-start Run pre-start hooks post-start Run post-start hooks pre-commit Run pre-commit hooks post-commit Run post-commit hooks pre-merge Run pre-merge hooks post-merge Run post-merge hooks pre-remove Run pre-remove hooks post-remove Run post-remove hooks Options: -h, --help Print help (see a summary with '-h') Global Options: -C <path> Working directory for this command --config <path> User config file path --config-set <toml> Override config with inline TOML, e.g. --config-set list.full=true (repeatable) -v, --verbose... Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to apply the same level everywhere — including shell completion, which no flag can reach -y, --yes Skip approval prompts

用法示例与过滤语法:

$ wt hook pre-merge # 运行所有 pre-merge hooks $ wt hook pre-merge test # 只运行两个来源中名为 "test" 的 hook $ wt hook pre-merge test build # 运行名为 "test" 和 "build" 的 hook $ wt hook pre-merge user: # 运行所有用户 hook $ wt hook pre-merge project: # 运行所有项目 hook $ wt hook pre-merge user:test # 只运行用户侧的 "test" $ wt hook pre-merge --yes # 跳过审批提示(CI 用) $ wt hook pre-start --branch=feature/test # 覆盖一个模板变量 $ wt hook pre-merge -- --extra args # 转发 token 进 {{ args }}

user:/project:前缀按来源过滤;单独使用表示该来源全部,user:name/project:name指定具体命令。运行输出形如:

$ wt hook pre-merge ◎ Running pre-merge project:test cargo test Finished test [unoptimized + debuginfo] target(s) in 0.12s Running unittests src/lib.rs (target/debug/deps/worktrunk-abc123) running 18 tests test auth::tests::test_jwt_decode ... ok test auth::tests::test_jwt_encode ... ok test auth::tests::test_token_refresh ... ok test auth::tests::test_token_validation ... ok test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s ◎ Running pre-merge project:lint cargo clippy Checking worktrunk v0.1.0 Finished dev [unoptimized + debuginfo] target(s) in 1.23s
$ wt hook post-start ◎ Running post-start: project @ ~/acme

从 CLI 传值

  • --KEY=VALUE{{ KEY }}被任一 hook 命令模板引用时绑定KEY——与wt <alias>相同的智能路由规则。内置变量可被覆盖:--branch=foo设置 hook 模板中的{{ branch }}(不会移动 worktree 实际的分支)。key 中的连字符变下划线:--my-var=x设置{{ my_var }}
  • 未被 hook 模板引用的--KEY=VALUE会作为字面 token 转发进{{ args }}--之后的 token 也原样转发进{{ args }}{{ args }}渲染为空格连接、shell 转义的字符串,可用{{ args[0] }}取索引、{% for a in args %}…{% endfor %}循环、{{ args | length }}计数;
  • 长形式--var KEY=VALUE已弃用但仍支持,它会无条件强制绑定,即使没有模板引用该 key——适用于模板只条件性引用 key 的场景(如{% if override %}…{% endif %})。

这套参数的解析语法在 src/cli/hook.rs 的HookOptions::parse中完整实现:wt hook <type>后的 argv 由一个external_subcommand兜底捕获(特意保住--分隔符),随后按「--yes/-y--dry-run--foreground--var--KEY=VALUE简写绑定、--之后字面转发、其余视为过滤名」的语法左到右扫描。另有两处源码级细节值得注意:

  • 类型名校验带纠错parse_hook_type对未知类型给出 did-you-mean 提示(wt hook pre-mrege会建议pre-merge);HOOK_TYPE_NAMES常量是 help、补全与校验共享的唯一事实来源,防止三处漂移(由测试兜底);
  • 静默别名pre-create/post-createpre-start/post-start的隐藏别名——不出现在 help 与补全中,但解析、执行、wt hook show均接受,保证使用旧名的脚本继续可用。

七、常用配方

参考文档将以下高频场景指向 tips-patterns 文档:

  • 消除冷启动:post-start中跑wt step copy-ignored共享构建缓存与依赖;若后续 hook 依赖这次拷贝,改用[[post-start]]流水线;
  • 每个 worktree 一个 dev server:post-start中跑wt step tether,启动 dev server 并在 worktree 删除时杀掉其整个进程组,支持可选子域名路由;
  • 每个 worktree 一个数据库:post-start流水线把容器名、端口、连接串存为 per-branch 变量,供后续 hook 引用;
  • 渐进式验证:pre-commit跑快速 lint/类型检查,pre-merge跑昂贵的测试与构建;
  • 按目标分发的 hook:在post-merge中按{{ target }}分支做分环境部署。

小结

Worktrunk 的 hook 体系把「worktree 生命周期自动化」拆成了三个正交层次:类型(10 种 pre/post 钩子点,pre 阻塞、post 后台)、配置(字符串/并发表/有序流水线三种形式,user 与 project 双层来源按明确规则合并,项目侧有审批门与防 TOCTOU 的计划冻结)、模板引擎(全仓库恒定的 repo 变量、逐 worktree 变化的 active 变量、Jinja2 过滤器与函数、stdin JSON 上下文)。配合-v查看渲染变量、--dry-run预览命令、wt hook show检视配置,可以完整闭环地调试任何一套 worktree 自动化钩子;实现细节可继续深入 src/cli/hook.rs、src/commands/hooks.rs、src/commands/hook_plan.rs 与 src/config/expansion.rs。

【免费下载链接】worktrunkWorktrunk is a CLI for Git worktree management, designed for parallel AI agent workflows项目地址: https://gitcode.com/GitHub_Trending/wo/worktrunk

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询