lefthook run 命令实战指南:手动触发 Git Hook、按 Job/Tag 筛选与文件模板覆盖
【免费下载链接】lefthookFast and powerful Git hooks manager for any type of projects.项目地址: https://gitcode.com/GitHub_Trending/le/lefthook
lefthook run是 lefthook 中最核心的执行命令:它负责加载 lefthook 配置,取出某个 hook 名下配置的全部命令与脚本并执行。安装到.git/hooks/下的 Git hooks 在触发时(如git commit、git push)也是隐式调用lefthook run <hook-name>来完成工作的。读完本文,你将掌握lefthook run的全部用法——从最基本的"手动跑一遍某个 hook",到用--job/--tag精准筛选任务、用--all-files/--file覆盖文件模板,并理解其底层执行链路(配置加载、命令替换、并行/串行调度)。
lefthook run是什么
官方文档的定义非常精炼:执行某个给定 hook 名下配置的命令和脚本(Executes the commands and scripts configured for a given hook)。安装后的 Git hooks 会隐式调用lefthook run,因此它既是手动调试的入口,也是 Git 触发场景下真正干活的那个进程。
典型配置与使用流程
假设项目根目录下有一份 lefthook.yml 风格的配置:
# lefthook.yml pre-commit: jobs: - name: lint run: yarn lint --fix {staged_files} test: jobs: - name: test run: yarn test先安装 hook:
$ lefthook install然后即可用lefthook run手动触发,或直接通过 Git 命令触发:
$ lefthook run test # 运行 'yarn test' $ git commit # 触发 pre-commit hook(运行 'yarn lint --fix') $ lefthook run pre-commit # 运行 pre-commit hook(同样执行 'yarn lint --fix')值得注意的是,配置中test并非标准的 Git hook 名称,但它依然可以被lefthook run test手动触发——这说明lefthook run接受任意 hook 名,只要它在配置中被定义即可。而标准的 Git hooks(如pre-commit)既会被 Git 隐式调用,也可以手动运行。
从源码看,这一判断位于 internal/command/run.go 的resolveHook中:若传入的 hook 名不在配置里,且该名字属于 lefthook 已知的 Git hooks 列表(config.KnownHook),则打印 debug 日志并静默跳过;否则直接返回错误hook <name> doesn't exist in the config。
为什么需要手动运行
手动运行的价值在于:
- 快速调试:改完
lefthook.yml不必真的去git commit,直接lefthook run pre-commit即可验证; - 非 Git 场景触发:自定义 hook 名(如上面的
test)不会由 Git 自动触发,只能靠手动lefthook run test; - 带参数验证:可以在命令行追加 Git 参数,模拟真实的 commit/push 场景。
只运行特定的 Job(--job与--tag)
当某个 hook 名下配置了多个 job 时,默认全部执行;你可以用--job或--tag精确挑选:
$ lefthook run pre-commit --job lints --job pretty --tag checks--job <name>:只运行指定名称的 job(可重复传入多个);--tag <tag>:只运行带有指定 tag 的 job(可重复传入多个)。
两者可以组合使用,筛选逻辑是"取并集":只要 job 命中了任一--job名称或任一--tag,就会被执行。
源码中的筛选逻辑
在 internal/run/controller/job.go 的runJob中可以看到精确的实现:
if len(scope.opts.RunOnlyJobs) != 0 && !slices.Contains(scope.opts.RunOnlyJobs, job.Name) { return result.Skip(job.PrintableName(id)) } if len(scope.opts.RunOnlyTags) != 0 && (!utils.Intersect(scope.opts.RunOnlyTags, job.Tags) && !utils.Intersect(scope.opts.RunOnlyTags, scope.tags)) { return result.Skip(job.PrintableName(id)) }即:job 名称必须严格命中--job列表;而 tag 命中比较宽松——job 自身的tags或 hook 级继承下来的scope.tags中任何一个与--tag列表相交即可。被筛掉的 job 会以 Skip 结果记录,并不会执行。
对 group 的传播行为
从 internal/run/controller/scope.go 的scope.extend可见一个细节:当--job指定的是某个 group 的名字时,lefthook 会清空RunOnlyJobs,转而运行该 group 下的全部子 job——也就是说,用 group 名筛选会"展开"这个组,而不是只挑组内同名子任务。集成测试 tests/integration/cli_run_only.txt 完整验证了--job a --job c --job db --job lint与--tag red的筛选结果,包括命令(commands:)也会被--job命中的行为。
指定文件(--all-files与--file)
命令模板中的文件占位符(如{staged_files})默认由 lefthook 根据 Git 状态自动填充。你可以用以下两个参数强制覆盖这些模板:
$ lefthook run pre-commit --all-files $ lefthook run pre-commit --file file1.js --file file2.js--all-files:把所有文件模板替换为{all_files}(即仓库中的全部文件);--file <path>:把文件模板替换为指定的文件列表(可重复传入多个)。
一个容易踩坑的点(原文档特别强调):如果两者同时指定,--all-files会被忽略,以--file列表为准。
源码中的实现
在 internal/command/run.go 的getFiles中可以看到覆盖逻辑:
if args.FilesFromStdin { // 从 STDIN 读取文件列表 } else if args.AllFiles { files, err := repo.AllFiles() return append(args.Files, files...) } return args.Files--file传入的值直接进入args.Files;只有未使用--file且传了--all-files时才调用repo.AllFiles()获取全部文件。两者并存时由于args.Files已非空,--all-files分支的追加结果被跳过,印证了"忽略--all-files"的文档描述。
文件列表最终会进入命令构建器(internal/run/controller/command/build_command.go 的buildReplacer):当opts.ForceFiles非空时,会用replacer.NewMocked将{staged_files}、{push_files}、{all_files}、{files}全部替换为强制指定的文件列表,并经过 shellescape 转义(internal/run/controller/command/replacer/replacer.go),避免含空格或特殊字符的文件名破坏命令。集成测试 tests/integration/files_override.txt 验证了--all-files输出仓库全部文件(包括带逗号的文件名c,file.rb)、--file则精确输出给定列表(甚至允许传入ghost.file这类不存在的文件)。
空文件列表的处理
若强制指定的文件列表为空(或文件模板替换后为空),lefthook 默认会跳过该 job,除非加上--force。相关逻辑在 internal/run/controller/command/build_command.go:replacer.HasEmpty()为真且未启用--force时,返回SkipError{"no files for inspection"},job 以 Skip 结果呈现,不执行命令。
完整命令行参数参考
lefthook run的完整用法为:
lefthook run <hook-name> [args...] [options]其全部 flag 定义于 cmd/run.go,汇总如下:
| 参数 | 别名 | 说明 | 对应源码字段 |
|---|---|---|---|
--verbose | -v | 开启 debug 日志 | args.Verbose |
--colors <on\|off\|auto> | 颜色输出开关,默认auto | colors | |
--job <name> | 只运行指定名称的 job,可重复 | args.RunOnlyJobs | |
--tag <tag> | 只运行带指定 tag 的 job,可重复 | args.RunOnlyTags | |
--command <name> | 只运行指定的命令,可重复 | args.RunOnlyCommands | |
--exclude <pattern> | 从所有文件模板中排除指定文件 | args.Exclude | |
--file <path> | 用指定文件覆盖文件模板,可重复 | args.Files | |
--force | -f | 即使没有文件变更也不跳过 | args.Force |
--all-files | 将文件模板替换为{all_files} | args.AllFiles | |
--no-auto-install | 不隐式同步/安装 hooks | args.NoAutoInstall | |
--no-stage-fixed | 忽略配置中的stage_fixed: true | args.NoStageFixed | |
--no-tty | 视为没有连接 TTY | args.NoTTY | |
--skip-lfs | 不运行 LFS hooks | args.SkipLFS | |
--fail-on-changes | 若有文件被改动则以退出码 1 结束 | args.FailOnChanges | |
--fail-on-changes-diff | 因文件改动失败时输出 diff | args.FailOnChangesDiff | |
--files-from-stdin | 从 STDIN 解析文件列表(支持\0分隔) | args.FilesFromStdin |
容易被忽略的几个参数
--files-from-stdin:从标准输入读取文件列表并合并进args.Files。解析逻辑见 internal/command/run.go 的parseFilesFromString,它同时兼容换行与\0(NUL)分隔的输入——后者正是git diff --name-only -z这类命令的输出格式,可与管道配合使用。--fail-on-changes/--fail-on-changes-diff:用于"检查文件是否被改动"的守护场景。其取值优先级为"命令行参数 > hook 配置的fail_on_changes> 默认行为",且fail_on_changes支持never/always/ci/non-ci等取值,逻辑见 internal/command/run.go。同时 hook 名后追加的[args...]会以{0}、{1}……形式注入命令模板,模拟真实 Git 传给 hook 的参数。--no-tty/--no-stage-fixed/--skip-lfs:分别用于在 CI 等无交互终端场景禁用 spinner、临时关闭某 job 的stage_fixed: true自动暂存、以及跳过 Git LFS hook 的执行。
底层执行链路:一次lefthook run内部发生了什么
从 internal/command/run.go 的Lefthook.Run方法可以看出完整流程:
- 环境开关检查:若环境变量
LEFTHOOK为"0"或"false",直接返回不执行(用于 CI 中一键禁用所有 hooks); - 预取 Git 状态:
repo.CacheGitCommands()并行预计算 staged/push 文件等,减少后续 IO 等待; - 加载配置:
LoadConfig()读取 lefthook 配置文件;文件不存在时打印警告并正常返回; - 版本校验:若配置中声明了
min_version,会校验当前 lefthook 版本,不满足则报错(逻辑见 internal/command/run.go); - 隐式同步 hooks:除非显式传
--no-auto-install或配置关闭了no_auto_install,否则会检查并更新已安装的 Git hooks 以匹配最新配置——这正是"改完配置自动生效"机制的来源; - 解析 hook:
resolveHook确认 hook 存在,并检测parallel与piped不能同时为true的冲突; - 收集文件:
getFiles处理--files-from-stdin、--all-files、--file的优先级; - 归一化任务:
config.CommandsToJobs与config.ScriptsToJobs把旧的commands:/scripts:配置统一转换成jobs,--command追加进RunOnlyJobs; - 调度执行:
runHook通过 internal/run/run.go 进入 controller,按 hook 的parallel设置走并发(concurrently)或串行(sequentially,支持piped断管跳过)执行,见 internal/run/controller/controller.go; - 结果汇总:执行结束后打印 summary,若有任一 job 失败则以非零退出码结束(internal/command/run.go)。
值得一提的是,controller 在启动时会用utils.NewCachedReader(os.Stdin)缓存标准输入,保证多个 job(如多个use_stdin: true的脚本)都能读到同一次 Git 通过 STDIN 传入的数据。
实战建议
- 调试配置时用
--job精准定位:一个 hook 下有多个 job 时,先lefthook run <hook> --job <name>单独验证,避免其他 job 干扰输出; - CI 中禁用全部 hooks:设置环境变量
LEFTHOOK=0即可让所有lefthook run调用直接返回,无需改配置; - 配合
-v观察模板替换:lefthook run <hook> -v会输出每条最终命令的 debug 日志([lefthook] job: ...),是排查文件模板、转义问题的利器; - 利用
--force跳过空文件跳过逻辑:当文件列表为空但确实希望执行命令(例如只想验证脚本本身)时,加-f强制运行; - 让
--all-files与--file二选一:两者语义互斥,混用时结果以--file为准,保持命令可读性建议只传一种。
如果需要在安装、卸载、校验配置等其他环节做进一步排查,可参考 docs/usage/commands/install.md、docs/usage/commands/uninstall.md 与 docs/usage/commands/validate.md;lefthook run本身的全部命令行定义可随时在 cmd/run.go 中查阅。
【免费下载链接】lefthookFast and powerful Git hooks manager for any type of projects.项目地址: https://gitcode.com/GitHub_Trending/le/lefthook
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考