Worktrunk Hook 系统实践:Git Worktree 生命周期钩子的配置、模板引擎与执行原理
Worktrunk Hook 系统实践Git Worktree 生命周期钩子的配置、模板引擎与执行原理【免费下载链接】worktrunkWorktrunk is a CLI for Git worktree management, designed for parallel AI agent workflows项目地址: https://gitcode.com/GitHub_Trending/wo/worktrunkWorktrunk 的 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 switch、wt merge、wt remove等命令执行过程中自动触发也可以通过wt hook type按需手动执行。用户级user与项目级project两层 hook 均受支持。10 种 hook 类型按「操作 × 阶段」组织pre-*为阻塞式post-*为后台式事件pre-— 阻塞post-— 后台switch切换pre-switchpost-switchcreate创建pre-startpost-startcommit提交pre-commitpost-commitmerge合并pre-mergepost-mergeremove删除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 commit、wt step squash、以及wt merge产生的提交之前运行post-commit触发 CI、发送通知、后台 lintpre-merge测试、安全扫描、构建验证——在 rebase 之后、合并到目标之前运行post-merge部署、通知、安装更新后的二进制。若目标分支存在 worktree 则在其内运行否则在主 worktreepre-remove删除前的清理保存测试产物、备份状态。在被删除的 worktree 中运行post-remove停止 dev server、删除容器、通知外部系统。模板变量引用的是被删除的 worktreewt merge中的 hook 编排wt merge期间阻塞式 hook 按此顺序执行pre-commit → pre-merge → pre-remove。合并完成后所有post-*hook同时启动各自锚定在它所属的 worktree 中——post-merge、post-switch、post-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防 TOCTOUpre-merge、post-merge、pre-remove、post-remove、post-switch、pre-start、post-start。这些 hook 在审批门与执行之间存在状态变更rebase 甚至可能改写.config/wt.toml本身因此审批时会把选定命令冻结成ApprovedHookPlan执行器只运行这份冻结值杜绝「审批 A 命令、执行 B 命令」的时间差攻击面Invocation-resolvedpre-commit、post-commit、pre-switch、wt 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 switch、wt merge、wt remove、wt step commit、wt step squash而不被wt hook本身接受。审批的增删通过wt config approvals add与wt 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先执行然后build与server并发。文档建议大多数 hook 用不到[[hook]]块只有存在依赖链典型如「装完依赖才能并发跑构建和 dev server」时才用它。流水线中一个精细行为模板在流水线开始前做语法检查在每步执行时渲染因此某一步可以写入 per-branch 变量供后续步骤经{{ vars.key }}读取。由于前序步骤仍可能改变这些值wt hook type --dry-run和wt 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 网页 URLswitch/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 URLexec{{ cwd }}hook 命令运行的目录{{ hook_type }}正在运行的 hook 类型如pre-start、pre-merge{{ hook_name }}hook 命令名若已命名{{ args }}从 CLI 转发的 token——见 手动运行 Hookuser{{ vars.key }}来自wt config state vars的 per-branch 变量repo类变量在整个仓库内恒定default_branch在每个 worktree 中相同active类变量逐 worktree 变化。裸变量指操作作用对象switch/create 是目标端merge/remove 是源端base与target给的是另一端操作裸变量basetargetswitch/create目标端出发处 裸变量commitmerge/squash 期间被压缩的 worktree 裸变量集成目标merge被合并的 feature 裸变量合并目标remove被删除的分支 裸变量最终落点所有 hook 共享同一视角——{{ branch | hash_port }}在post-start与post-remove中产生相同的端口。cwd的三种例外平时cwd等于worktree_pathpre-switchhook 在源 worktree 运行若目标 worktree 已存在worktree_path指向目标而cwd仍是源——创建型切换尚无目标目录worktree_path也停在源要在新 worktree 里做事请用pre-startpost-remove当前 worktree 已删除hook 改在主 worktree 运行post-mergehook 在目标分支的 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 --formatjson对该 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/bbasename{{ repo_path \| basename }}只保留最后一段/a/b/c→ccodename(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 }}任何字符串含拼接都可哈希# 每个 repobranch 组合一个唯一端口 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.pyimport 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六、手动运行 Hookwt hook typewt 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.fulltrue (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_VERBOSE0|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 --branchfeature/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 传值--KEYVALUE在{{ KEY }}被任一 hook 命令模板引用时绑定KEY——与wt alias相同的智能路由规则。内置变量可被覆盖--branchfoo设置 hook 模板中的{{ branch }}不会移动 worktree 实际的分支。key 中的连字符变下划线--my-varx设置{{ my_var }}未被 hook 模板引用的--KEYVALUE会作为字面 token 转发进{{ args }}--之后的 token 也原样转发进{{ args }}。{{ args }}渲染为空格连接、shell 转义的字符串可用{{ args[0] }}取索引、{% for a in args %}…{% endfor %}循环、{{ args | length }}计数长形式--var KEYVALUE已弃用但仍支持它会无条件强制绑定即使没有模板引用该 key——适用于模板只条件性引用 key 的场景如{% if override %}…{% endif %}。这套参数的解析语法在 src/cli/hook.rs 的HookOptions::parse中完整实现wt hook type后的 argv 由一个external_subcommand兜底捕获特意保住--分隔符随后按「--yes/-y、--dry-run、--foreground、--var、--KEYVALUE简写绑定、--之后字面转发、其余视为过滤名」的语法左到右扫描。另有两处源码级细节值得注意类型名校验带纠错parse_hook_type对未知类型给出 did-you-mean 提示wt hook pre-mrege会建议pre-mergeHOOK_TYPE_NAMES常量是 help、补全与校验共享的唯一事实来源防止三处漂移由测试兜底静默别名pre-create/post-create是pre-start/post-start的隐藏别名——不出现在 help 与补全中但解析、执行、wt hook show均接受保证使用旧名的脚本继续可用。七、常用配方参考文档将以下高频场景指向 tips-patterns 文档消除冷启动post-start中跑wt step copy-ignored共享构建缓存与依赖若后续 hook 依赖这次拷贝改用[[post-start]]流水线每个 worktree 一个 dev serverpost-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),仅供参考