本文主要讲解如何使用 pre-commit 结合静态代码分析工具。

学习前端发现了 Husky 能在 git commit 的时候自动触发代码格式化和检查,于是我想 Python 也应该有此类工具,就搜索到了 pre-commit

# pre-commit 说明

pre-commit 预提交,是 git hooks 中的一个钩子,由 git commit 命令调用,可以通过 --no-verify 参数绕过调用 pre-commit。通常用于在提交代码前,进行代码规范检查。

为了更方便的管理 pre-commit 的设置,于是有了一个同名的工具项目 pre-commit,一个用于管理和维护多语言预提交挂钩的框架。官网地址:https://pre-commit.com/ (opens new window)

PS:由于工具项目名称和 git hooks 的钩子同名,容易混淆概念,主要注意一下。这里我们详细说明使用的则是同名的工具项目,而不是 git 自身的 pre-commit 脚本钩子

# pre-commit 框架使用

此框架是多语言的,这里以 Python 为例,下面内容均为 Python 相关的使用说明。版本为最新版 2.19.0

pre-commit 框架,随着发展,已经不单单只能用于 git hooks 的 pre-commit 阶段,而是能作用于所有 git hooks 的所有阶段,如上面说的 prepare-commit-msg, commit-msg, post-commi 等,具体有哪些阶段,参考 git 官方文档:https://git-scm.com/docs/githooks (opens new window)

也就是说,pre-commit 不单单可以用于规范代码检测,还可以在 git 的不同阶段,自定义做出各种不同的操作,灵活性很大,非常好用。

# 使用步骤

  1. 安装

    # 直接使用pip安装即可
    pip3 install pre-commit
    

    安装成功后,使用 pre-commit --version 可正常查看版本。

  2. 设置配置文件 pre-commit 依赖项目根目录配置文件 .pre-commit-config.yaml需要手动在根目录创建此文件。

    # 在根目录执行如下命令,生成一个默认的配置,python版本
    pre-commit sample-config > .pre-commit-config.yaml
    

    这里配置的详细设置,后面详细说,这里先使用官方示例。

  3. 设置 git hooks 钩子脚本 pre-commit 框架是通过 git hooks 本身的钩子来调用的,所以在设置好配置文件后,会根据配置,在 git 项目中将钩子脚本配置到 .git/hooks 路径下。

    因为 pre-commit 框架支持各种不同阶段的钩子,所以需要根据配置,配置中使用到什么阶段的钩子,就回自动配置对应的 hooks 脚本到 .git/hooks

    # 直接执行此命令,设置git hooks钩子脚本
    pre-commit install
    
  4. 对所有文件进行一次检查 (可选) 配置设置好后,默认是需要 git commit 命令来出发调用的,只会检查 git commit 中变更的文件。对所有文件进行检查:

    # 直接执行此命令,进行全文件检查,--all-files 可简写为 -a
    pre-commit run --all-files
    
  5. 常用命令

pre-commit gc # 清理未使用的缓存 repo
pre-commit clean # 清除缓存的 pre-commit 文件
pre-commit autoupdate # 将 pre-commit 的配置自动更新到最新的 repos 版本

# .pre-commit-config.yaml 配置详细描述

首先,是顶层的全局配置,配置项有如下这些:

字段名 描述
repos (必需)存储库映射列表
default_install_hook_types (可选:默认 [pre-commit])--hook-type 列表,在运行 pre-commit install 时默认使用。
default_language_version (可选:默认 {})从语言到应用于该语言的默认 language_version 的映射。这只会覆盖没有设置 language_version 的单个钩子。
default_stages (可选:默认 (all stages))钩子的 stages 属性的配置范围默认值。这只会覆盖没有设置 stages 的单个钩子。
files (可选:默认 "")全局文件包含模式,正则匹配。
exclude (可选:默认^$)全局文件排除模式,正则匹配。
fail_fast (可选:默认 false)设置为 true 时,预提交将在第一次失败后停止运行钩子。
minimum_pre_commit_version (可选:默认 '0')需要的最低 pre-commit 版本。

举个例子说明一下,一般常用的顶层配置大致如下:

# 设置默认的阶段为commit,只在提交时进行检查
default_stages:
- commit
# 设置默认的语言版本,你也可以在每个repos中单独设置language_version
default_language_version:
  python: python3.8
# 设置排除的文件,正则匹配,这个也可以在每个repos中的hooks脚本中单独配置,以灵活实现不同文件对不同规则的检测排除
exclude: "^$"
# 起码顶层配置,一般使用默认值即可。
# 这个repos配置下面的内容,就是最重要的核心配置,下面细说
repos:
- ......

# repos 配置

repos 配置是一个存储库映射列表,用来告诉 pre-commit 从哪里获取钩子的代码。其有三个下层配置,都是必填,如下:

字段名 描述
repo git clone 的存储库 url
rev git clone 要拉取的 tag 或者 revision
hooks 钩子映射列表,也就是要进行检测的钩子列表

第二层配置很简单,就是指定 repo 库的地址和版本 tag,拉取代码,然后指定使用这个库中具体的哪些 hooks 脚本。

其中,hooks 配置为核心配置。 有哪些 hooks 脚本可选,都是从 git 库中的配置获得。需要具体看这个 git 库的使用方式。因此允许自定义库,自定义 hooks 脚本,然后通过 pre-commit 拉取使用。

hooks 配置下层又有如下配置项:

字段名 描述
id (必须)要使用存储库中的哪个钩子,钩子的 id
alias (可选)允许钩子在使用 pre-commit 运行 时使用额外的 id 来引用,一般不常用。
name (可选)重写钩子的名称 - 在钩子执行时显示
language_version (可选)覆盖钩子的语言版本
files (可选)覆盖默认的文件包含,正则匹配模版
exclude (可选)覆盖默认的文件排除,正则匹配模版
types (可选)要运行的文件类型列表 (AND)
types_or (可选)要在 (OR) 上运行的文件类型列表
exclude_types (可选)要排除的文件的类型列表
args (可选)要传递给钩子的附加参数列表
stages (可选)将钩子限制在 commit, merge-commit, push, prepare-commit-msg, commit-msg, post-checkout, post-commit, post-merge, post-rewrite, or manual 阶段。这也就是 git 自身支持的不同阶段的钩子
additional_dependencies (可选)将被安装在该钩子运行的环境中的依赖项列表。
always_run (可选)如果为 true,即使没有匹配的文件,这个钩子也会运行
verbose (可选)如果为 true,则强制打印该钩子的输出,即使该钩子通过
log_file (可选)日志文件路径,如果存在,当钩子失败或 verbose 为 true 时,钩子输出将额外写入此文件

# 实用配置

举例说明,下面就列出一份 Python 版本的配置,看看一般常见的配置是怎样的。

exclude: (migrations/|test/)
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v3.2.0
    hooks:
    -   id: trailing-whitespace
        args: [--markdown-linebreak-ext=md]
    -   id: end-of-file-fixer
    -   id: check-toml
    -   id: check-yaml
    -   id: check-added-large-files
        args: [--maxkb=51200]
    -   id: requirements-txt-fixer
    -   id: check-merge-conflict
-   repo: https://github.com/psf/black
    rev: 22.3.0
    hooks:
    -   id: black
        args: [--skip-string-normalization]
        exclude: migrations/
  - repo: https://github.com/charliermarsh/ruff-pre-commit
    rev: "v0.0.277"
    hooks:
      - id: ruff
        args: [--fix, --exit-non-zero-on-fix]

如上,则是一个常用的检测配置,使用了 ruff (包含 flake8、pylint、isort、pyupgrade 等的规则)进行静态代码检查,black 进行代码格式化,同时还添加了 pre-commit 框架项目提供的一些公共 hooks,具体的使用说明(更多 args 和自定义配置),可以直接访问各 repo 地址,查看文档说明。

# 自动启用 pre-commit

看上面的介绍和使用,会发现有个问题:从 git 库中 clone 一个项目到本地后,即使项目中有 .pre-commit-config.yaml 配置文件,但是 .git/hooks 中还是没有设置钩子,需要运行 pre-commit install 安装后,才能正常使用。

那如果要让他根据 .per-commit-config.yaml 配置自动安装 git 钩子呢?让我们直接 clone 项目后,便自动启用了 pre-commit。

官方给了一个解决方式: 使用 pre-commit init-templatedir 命令,自动设置 git 项目的初始化模版,在版本中自动执行 pre-commit install 以启用 pre-commit。

设置步骤如下:

# 设置git全局变量,制定初始化模版目录
git config --global init.templateDir ~/.git-template
# 自动设置初始化模版
pre-commit init-templatedir ~/.git-template

ok,到此就可以了,然后可以测试一下,clone 一个带 .per-commit-config.yaml 配置的库,然后进行提交,则会自动进行 pre-commit 的检测。如果没有 .per-commit-config.yaml 配置,则会跳过,普通正常使用。

pre-commit 还有很多的命令和用法,这里只是列举了常见的使用,如果要进行更高级的自定义和使用,详细参考官方文档:https://pre-commit.com/ (opens new window)

官方文档比较简单,看起来也不难,以上大多都是官方文档中的内容,后续更新和使用也以官方文档为准

参考文章:

1、Git 项目管理,代码规范 pre-commit 使用详解 (opens new window)