公元 4202 年,终于配置上了 pre-commit 提交检查


提要信息

整理照片发现2022年6月5日的截图,当时看到博主安利 pre-commit 也想用下,截图完就忘在脑后,推都变成了 X,两年后迟来的尝试,博主没骗人,真的「早该这么做了」

pre-commit 是什么?

Git 提供 4 种 Hooks,可以在时间点自定义执行一些操作,此处暂时只关注其中之一的 pre-commit Hook

The pre-commit hook is run first, before you even type in a commit message. It’s used to inspect the snapshot that’s about to be committed, to see if you’ve forgotten something, to make sure tests run, or to examine whatever you need to inspect in the code. Exiting non-zero from this hook aborts the commit, although you can bypass it with git commit --no-verify. You can do things like check for code style (run lint or something equivalent), check for trailing whitespace (the default hook does exactly this), or check for appropriate documentation on new methods.

设置 pre-commit 后,每次执行 git commit -m "..." 前 Git 会自动运行 pre-commit Hook,可以是一些命令或脚本,如果配置了 Lint 检查,不符合规范的代码在检测不通过时就会自动中断 Git 提交过程

同时,有一个工具,也叫 pre-commit

pre-commit: A framework for managing and maintaining multi-language pre-commit hooks.

使用这个工具,可以很方便的配置 Git pre-commit Hooks

pre-commit 工具安装

通过 pip 在项目中使用

$ python3 -m venv myvenv
$ source myvenv/bin/activate
$ pip3 install pre-commit

通过 brew 全局安装(macOS 系统)

$ brew install pre-commit

查看 pre-commit 版本

$ pre-commit --version
pre-commit 3.8.0

添加 pre-commit 配置

在项目的根目录创建名为 .pre-commit-config.yaml 的配置文件

添加以下内容

repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
    -   id: check-yaml
    -   id: end-of-file-fixer
    -   id: trailing-whitespace
-   repo: https://github.com/psf/black
    rev: 24.8.0
    hooks:
    -   id: black

以上是一个示例配置,意为使用 pre-commit/pre-commit-hookspsf/black 仓库提供的脚本对项目中的文件、代码做检查

  • check-yaml 用来检查 yaml 配置文件语法是否正确
  • end-of-file-fixer 可以确保每个文件的末尾都有一个空行
  • trailing-whitespace 去除文件行末的多余空白字符
  • black 用于严格地确保相同的代码风格

其中 pre-commit/pre-commit-hooks 提供了一些通用的 Hooks,而 psf/black 提供的 Hook 仅针对于 Python 语言

https://pre-commit.com/hooks.html 可以找到不同语言的专属 Hooks 仓库,另外在 Github 上也能找到一些 Hooks

安装 pre-commit 钩子

安装完 pre-commit 工具,也添加了配置文件,接下来将配置文件应用到 Git

在项目根目录运行(即 .pre-commit-config.yaml 所在目录)

$ pre-commit install
pre-commit installed at .git/hooks/pre-commit

pre-commit 写入的 .git/hooks/pre-commit 内容如下,仅作为了解即可,它是个 Bash 脚本,将配置文件和 Git Hook 进行了绑定

#!/usr/bin/env bash
# File generated by pre-commit: https://pre-commit.com
# ID: 138fd403232d2ddd5efb44317e38bf03

# start templated
INSTALL_PYTHON=/Users/projects/fr-compare/myvenv/bin/python3.12
ARGS=(hook-impl --config=.pre-commit-config.yaml --hook-type=pre-commit)
# end templated

HERE="$(cd "$(dirname "$0")" && pwd)"
ARGS+=(--hook-dir "$HERE" -- "$@")

if [ -x "$INSTALL_PYTHON" ]; then
    exec "$INSTALL_PYTHON" -mpre_commit "${ARGS[@]}"
elif command -v pre-commit > /dev/null; then
    exec pre-commit "${ARGS[@]}"
else
    echo '`pre-commit` not found.  Did you forget to activate your virtualenv?' 1>&2
    exit 1
fi

手动触发 pre-commit 工具检查(可选)

可以使用以下命令手动触发检查所有文件,这有助于确保现有代码符合 isort、black 和 flake8 的规范

pre-commit run --all-files

首次使用,会自动修复一些问题、同时抛出不符合规范的错误提示

自动修复的内容从可视化 Diff 视角看,单引号被调整为了双引号、移除了多余的空格、文件尾行保留一行等

其它不能自动修复的问题如行过长,未使用到的代码、更好的实现方式,错误被覆盖等等,需要手动处理,修复完成后显示如下

适用于 Python 项目的配置文件

使用前建议到仓库获取最新 Tag 标签进行替换

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: check-yaml
      - id: debug-statements
      - id: end-of-file-fixer
      - id: trailing-whitespace

  - repo: https://github.com/pycqa/isort
    rev: 5.13.2
    hooks:
      - id: isort
        name: isort (python)
        args: ["--profile", "black", "--filter-files"]

  - repo: https://github.com/psf/black-pre-commit-mirror
    rev: 24.8.0
    hooks:
      - id: black
        args: [--line-length=79]
        language_version: python3.12

  - repo: https://github.com/PyCQA/flake8
    rev: 7.1.1
    hooks:
      - id: flake8

适用于 Golang 的配置文件

使用前建议到仓库获取最新 Tag 标签进行替换

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files
  - repo: https://github.com/dnephin/pre-commit-golang
    rev: v0.5.1
    hooks:
      - id: go-fmt
      - id: go-imports
      - id: no-go-testing
      - id: golangci-lint
        args:
          - --disable=unused # 或者使用 // nolint:gosimple 进行标注,哪个 Hook 报错 nolint 哪个 Hook 名
      - id: go-unit-tests
      # - id: validate-toml
  - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
    rev: v9.16.0
    hooks:
      - id: commitlint
        stages: [commit-msg]
        additional_dependencies: ['@commitlint/config-conventional']

运行检查后的输出效果

提示:使用到的 golangci-lint 和 goimports 工具需要先安装

# macOS 安装 golangci-lint
$ brew install golangci-lint

# 通过 go install 安装 goimports,需要确认 go bin 目录已在环境变量中
$ go install golang.org/x/tools/cmd/goimports@latest

例如 Golang 项目,它帮我发现了无效的 if (true) { } 空内容分支代码,项目中 _, r := range []rune(s) 去掉了多此一举的 []rune(),可以直接 range s,使用 time.Since() 替换time.Now().Sub()

再比如以下的代码,其实不需要判断 Key 是否存在,直接使用 delete(r.Headers, key) 即可,因为 delete() 再 m 为 nil 或者 m[key] 不存在时什么都不会做

The delete built-in function deletes the element with the specified key (m[key]) from the map. If m is nil or there is no such element, delete is a no-op.

if _, exist := r.Headers[key]; exist {
    delete(r.Headers, key)
}

等等其它的提示,实打实的提升代码的质量、统一风格、看到各项检查都是 “Passed”,提交代码也更安心了...

参考