Lefthook 的script配置详解:在 Git 钩子中执行自定义脚本的完整指南
【免费下载链接】lefthookFast and powerful Git hooks manager for any type of projects.项目地址: https://gitcode.com/GitHub_Trending/le/lefthook
script是 Lefthook 钩子配置中用来指定“要执行的自定义脚本文件名”的核心选项,与run(内联命令)并列。本文以 docs/configuration/script.md 为主体,结合 Lefthook 源码,系统讲解脚本的存放目录、runner执行器、args参数模板、可执行权限处理以及底层调用链,帮助你写出可直接落地的钩子脚本配置。
一、script是什么:与scripts与run的关系
在lefthook.yml中,每个 hook(如pre-commit、commit-msg)下既可以配置commands,也可以配置scripts,还可以通过jobs混用两者。script正是jobs中用来指向一个脚本文件的键:
# lefthook.yml pre-commit: jobs: - script: linter.sh # 指定要执行的脚本文件名 runner: bash # 指定执行器 args: "{staged_files}" # 追加参数script的值是一个脚本文件名(而非内联命令),其规则与顶层scripts配置完全一致:脚本存放在<source_dir>/<hook-name>/目录下,由项目根目录执行。
从源码看,script与run在Job结构中是互斥的“二选一”字段(见 internal/config/job.go):
Run string `json:"run,omitempty" jsonschema:"oneof_required=Run a command" ...` Script string `json:"script,omitempty" jsonschema:"oneof_required=Run a script" ...`也就是说,一个 job 要么通过run执行内联命令,要么通过script执行外部脚本,二者不可同时作为主体。而scripts顶层键则是把“脚本名 → 配置”作为一个 map 批量声明的语法糖——Lefthook 内部会把它们统一转换成 Job 列表(见ScriptsToJobs,后文详述)。
二、脚本存放规则:<source_dir>/<hook-name>/
脚本必须放在与钩子同名的子目录中,目录根由source_dir决定,默认值为.lefthook/(见 internal/config/config.go 中SourceDir字段的默认值注释)。典型布局:
.lefthook/ ├── pre-commit/ │ ├── linter.sh │ └── check-python.py ├── commit-msg/ │ └── template_checker └── pre-push/ └── check-files.rb对应的配置写法:
# lefthook.yml pre-commit: scripts: "linter.sh": runner: bash用lefthook add快速创建脚本骨架
官方推荐的实操流程(详见 Scripts.md):
- 运行
lefthook add -d pre-commit:-d(即CreateDirs)会在.lefthook/pre-commit/与.lefthook-local/pre-commit/下创建目录(对应实现见 internal/command/add.go); - 编辑
.lefthook/pre-commit/my-script.sh,写入你的检查逻辑; - 在
lefthook.yml中注册脚本:
# lefthook.yml pre-commit: scripts: "my-script.sh": runner: bash一个完整示例:校验 commit message
在.lefthook/commit-msg/template_checker中放置以下 bash 脚本(不要求.sh后缀,文件名即标识):
INPUT_FILE=$1 START_LINE=`head -n1 $INPUT_FILE` PATTERN="^(TICKET)-[[:digit:]]+: " if ! [[ "$START_LINE" =~ $PATTERN ]]; then echo "Bad commit message, see example: TICKET-123: some text" exit 1 fi然后在lefthook.yml中注册并保留 Git 传入的第一个参数(commit message 文件路径):
# lefthook.yml commit-msg: scripts: "template_checker": runner: bash args: "{0}"此后执行git commit -m "bad commit text"时,template_checker会被运行;由于提交信息不匹配^(TICKET)-\d+:模式,脚本以非零状态退出,提交过程被中断。
三、args:向脚本追加参数(并“接管” Git 参数)
args(自 Lefthook 2.0.5 起引入,详见 args.md)用于向脚本追加参数,支持与run完全相同的模板语法:
| 模板 | 含义 |
|---|---|
{files} | 自定义files命令的结果 |
{staged_files} | 暂存区中即将提交的文件 |
{push_files} | 已提交但尚未推送的文件 |
{all_files} | Git 跟踪的所有文件 |
{cmd} | 配置文件中的命令简写(用于在 Docker 等 wrapper 中复用) |
{0} | Git 钩子全部参数拼接成的单字符串 |
{1}、{2}… | 第 1、2… 个 Git 钩子参数 |
{lefthook_job_name} | 当前 job/command/script 的名称 |
关键行为:一旦在配置中指定了args,Git 自动传入钩子的参数就会被省略;只有在args中显式包含{0}模板时,Git 参数才会被保留。换句话说:
- 不配置
args⇔ 配置args: "{0}":脚本能拿到 Git 传入的完整参数; - 配置
args: "{staged_files}":脚本收到的是暂存文件列表,而不是 Git 参数。
# lefthook.yml pre-commit: jobs: - script: check-python-files.sh runner: bash args: "{staged_files}" glob: "*.py" - run: yarn lint args: "{staged_files}" glob: - "*.ts" - "*.js"在源码层面,args的替换逻辑位于 internal/run/controller/command/replacer/replacer.go:Replacer.Discover会扫描args字符串中出现的模板并批量缓存结果;AddGitArgs(见 replacer.go)负责把{0}、{1}… 映射为实际的 Git 参数。
四、源码级解析:脚本是如何被构建与执行的
1. 从scriptsmap 到 Job 的转换与排序
顶层scripts配置在加载时会被ScriptsToJobs转成 Job(见 internal/config/script.go):每个脚本的runner、args、fail_text、timeout、tags、env、interactive、use_stdin、stage_fixed、skip、only都会被原样搬运到对应 Job 上。同时,脚本会按以下顺序排序执行:
- 配置了
priority的脚本按 priority 升序,未配置 priority 的脚本排最后; - 都没有 priority 时,按脚本名的数字前缀升序(如
1-lint.sh先于2-test.sh),否则按字典序(parseNum实现见 script.go)。
Script结构体支持的完整选项(见 internal/config/script.go):
| 选项 | 说明 |
|---|---|
runner | 执行脚本的解释器/命令,如bash、node、python |
args | 追加到脚本后的参数(支持模板) |
skip/only | 条件跳过或仅在某些条件(git 状态/分支)下运行 |
tags | 标签,配合LEFTHOOK_EXCLUDE/exclude_tags过滤 |
env | 注入环境变量 |
priority | 执行顺序优先级 |
fail_text | 失败时输出的自定义提示 |
timeout | 超时时间(如15s) |
interactive | 是否以交互式终端运行 |
use_stdin | 是否把 stdin 传给脚本 |
stage_fixed | 是否在脚本修复文件后自动git add |
2. 脚本执行的关键流程:查找、赋予可执行权限、拼接命令
当 job 的script生效时,实际构建命令的逻辑在 internal/run/controller/command/build_script.go 的buildScript中:
- 校验脚本路径:遍历所有
SourceDirs(全局 + 本地),拼接<sourceDir>/<hookName>/<script名>并检查文件是否存在; - 跳过空文件模板:如果
args中包含{staged_files}等文件模板,但模板解析结果为空(如没有可检查的文件),默认会返回SkipError{"no files for inspection"}直接跳过该脚本(除非--force); - 自动赋予可执行权限:若脚本文件缺少可执行位,Lefthook 会将其
chmod为0o751(executableFileMode),见 build_script.go 与Chmod调用; - 拼接命令:若配置了
runner,先拼上 runner(如bash),随后用shellescape.Quote对脚本路径做 shell 转义(防止路径含空格/特殊字符时出错),最后追加args; - 未配置 args 时:直接追加 Git 钩子参数(
b.opts.GitArgs); - 处理命令行长度上限:当文件列表很长时,
Replacer.ReplaceAndSplit会根据系统MaxCmdLen把命令拆分为多条顺序执行的命令(见 replacer.go 与run中的说明)。
3. 文件名的引号转义
ReplaceAndSplit在替换文件模板时会对每个文件名做 shell 转义(escapeFiles,见 replacer.go),并在命令行长度允许时对带引号的模板("{staged_files}"、'{staged_files}')做对应引号包裹。这意味着文件名中的空格、引号等特殊字符都能被安全传递。
五、实战组合:把脚本与其它配置项配合使用
在 Docker 中运行脚本
利用{cmd}模板(见run),可以把本地脚本包进容器执行:
# lefthook-local.yml pre-commit: scripts: "good_job.js": runner: docker run -it --rm <container_id_or_name> {cmd}多脚本 + 优先级 + 失败提示
# lefthook.yml pre-commit: scripts: "1-lint.sh": runner: bash priority: 1 glob: "*.{js,ts}" "2-test.sh": runner: bash priority: 2 fail_text: "测试未通过,请修复后再提交" "format-fixer.py": runner: python stage_fixed: true # 脚本修复文件后自动 git add仓库中自带的可运行示例见 examples/with_scripts/lefthook.yml。
六、小结
script是 Lefthook 把“文件型检查器”接入 Git 钩子生命周期的主通道:脚本按<source_dir>/<hook-name>/规则存放,配合runner选择解释器,通过args模板精确控制传给脚本的参数(并决定是否保留 Git 参数)。底层实现上,Lefthook 会自动补齐可执行位、按 priority/数字前缀排序、对文件列表做 shell 转义并按命令行长度自动拆分,这些细节共同保证了脚本在各类系统上的稳定执行。掌握这些规则后,你可以把 lint、测试、格式修复等任意检查逻辑组织成独立脚本,交给 Lefthook 统一调度。
【免费下载链接】lefthookFast and powerful Git hooks manager for any type of projects.项目地址: https://gitcode.com/GitHub_Trending/le/lefthook
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考