规范编码之利用pre-commit给项目添加提交前检查
# 前言
日常开发过程中,不论是哪个语言,一定都会有相对应的语法检测工具或者手段来辅助我们检查出编码过程中的一些遗漏或疏忽。但有时候会有一个尴尬的情况就是,把检测的方式配置上去了,却没有运行,最后成了摆设。
今天来介绍一个工具,能够完成在代码提交之前运行指定检测,从而实现代码的自检。
- 项目:pre-commit (opens new window)
- 官网: https://pre-commit.com/ (opens new window)
- 开箱即用:pre-commit-hooks (opens new window)
pre-commit 的运行机制借助于 git hook 来完成提交之前的一些预定义指令的运行,来达到提交前检测的目的。
# 关于git hook
Git 能在特定的重要动作发生时触发自定义脚本钩子。钩子分为两组:
- 客户端钩子:
pre-commit,prepare-commit-msg,commit-msg,post-commit等,主要在服务端接收提交对象时、推送到服务器之前调用。 - 服务器钩子:
pre-receive,post-receive,update等,主要在服务端接收提交对象时、推送到服务器之前调用。
git hooks 位置位于每个 git 项目下的 .git/hooks 目录里,进去后会看到这些钩子的官方示例,都是以 .sample 结尾的文件,这些示例脚本是不会执行的,去掉 .sample 后缀可激活该钩子脚本。
PS:GIt hooks 的每个钩子的作用和说明,详细的以官方文档为准: https://git-scm.com/docs/githooks

# 安装体验
# 安装
pre-commit 是一个 Python 语言写的工具,通过如下命令能够轻松安装该工具:
$ pip3 install pre-commit
查看版本:
$ pre-commit --version
pre-commit 3.1.0
2
如果这条命令运行没有问题,则说明安装成功。
# 配置
现在我拿 learn-github (opens new window) 项目来作为示例进行体验。
pre-commit 以 .pre-commit-config.yaml 文件作为默认的配置文件,在项目根目录执行如下命令生成简单的配置内容:
$ pre-commit sample-config > .pre-commit-config.yaml
查看一下配置文件的内容信息:
$ cat .pre-commit-config.yaml
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
2
3
4
5
6
7
8
9
10
11
- repos:表示一系列仓库的映射。
- repo:表示接下来使用的 hooks 脚本从哪个仓库进行拉取。
- rev:指定将要拉取的 tag 。
- hooks:钩子脚本列表,这些脚本来自于 repo 定义的仓库中。
- id:指定将要应用的钩子的名称,就是对应的文件名。
其中 hooks 还有更加丰富的配置信息,这里暂不展开,随后再进行完整介绍。
简单解释下如上配置文件的意思:在代码提交之前,会运行 hooks 列表中的这些检查,这些脚本来自于 https://github.com/pre-commit/pre-commit-hooks 这个仓库的 v4.4.0。四个检查脚本的含义如下:
trailing-whitespace:检查修建行尾的空格end-of-file-fixer:确保文件以换行符结尾且仅以换行符结尾。check-yaml:检查 yaml 文件的语法。check-added-large-files:防止提交大文件。(默认检测阈值为 500KB)
# 运行
执行 pre-commit run --all-files 命令,可以手动运行 pre-commit 的检查:
$ pre-commit run --all-files
[INFO] Initializing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Installing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Failed
- hook id: end-of-file-fixer
- exit code: 1
- files were modified by this hook
Fixing workflows-tested/rss.yml
Check Yaml...............................................................Passed
Check for added large files..............................................Passed
2
3
4
5
6
7
8
9
10
11
12
13
14
15
📢注意: 钩子添加完毕之后,默认情况下,pre-commit 只会检测当次变更了的文件,因此一般建议在添加之后,运行一次针对项目的全面检测。
如上内容表示运行 Fix End of Files 这个脚本的时候发现有不符合检测规范的内容,然后自动 fix 掉了,这些检测脚本都是根据个人需求按需加载,因此这里我就把这个脚本去掉了。
# 安装
上边是手动运行的,我们还应该运行一下安装命令,把 pre-commit 的配置文件加载到 git hooks 当中:
$ pre-commit install
pre-commit installed at .git/hooks/pre-commit
2
这个时候,再次运行常规的提交步骤就会触发检测了:
$ gcmsg '添加pre commit'
[INFO] Initializing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Installing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
trim trailing whitespace.................................................Passed
check yaml...............................................................Passed
check for added large files..............................................Passed
[main aeb4728] 添加pre commit
1 file changed, 9 insertions(+)
create mode 100644 .pre-commit-config.yaml
2
3
4
5
6
7
8
9
10
11
# Go 项目实践
以上内容介绍了 pre-commit 的简单配置以及使用,接下来我们测试一个 go 项目的实践。
然后在项目根目录添加如下配置文件 .pre-commit-config.yaml :
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: check-yaml
- id: trailing-whitespace
- id: check-added-large-files
- repo: https://github.com/golangci/golangci-lint # golangci-lint hook repo
rev: v1.47.3 # golangci-lint hook repo revision
hooks:
- id: golangci-lint
name: golangci-lint
description: Fast linters runner for Go.
entry: golangci-lint run --fix
types: [go]
language: golang
pass_filenames: false
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
然后运行如下命令将 hooks 载入到 git 配置文件中:
$ pre-commit install
pre-commit installed at .git/hooks/pre-commit
2
然后将代码某处的 err 错误忽略不做处理,此时提交代码看看是否会检查:
$ gcmsg 'test pre check'
Check Yaml...............................................................Passed
Trim Trailing Whitespace.................................................Passed
Check for added large files..............................................Passed
golangci-lint............................................................Failed
- hook id: golangci-lint
- exit code: 1
config/config.go:30:11: ineffectual assignment to err (ineffassign)
workDir, err := os.Getwd()
^
2
3
4
5
6
7
8
9
10
11
如此就实现了一个简单的提交前的 lint 检查,一些简单的语法问题就能在这里抛出来了。
其他语言同理,pre-commit 官方提供了大量检测脚本集成,各语言都有,大家可按需进行了解使用。
# 补充
# hooks 配置文件
内容摘自官方文档:
id (opens new window) | 钩子的 id - 在 pre-commit-config.yaml 中使用。 |
|---|---|
name (opens new window) | 挂钩的名称 - 在挂钩执行期间显示。 |
entry (opens new window) | 入口点 - 要运行的可执行文件。 entry 还可以包含不会被覆盖的参数,例如 entry: autopep8 -i. |
language (opens new window) | 钩子的语言 - 告诉预提交如何安装钩子。 |
files (opens new window) | (可选:默认 '')要运行的文件模式。 |
exclude (opens new window) | (可选:默认 ^$)排除匹配的文件 files (opens new window)。 |
types (opens new window) | (可选:默认 [file])要运行的文件类型列表(AND)。请参阅 使用类型过滤文件 (opens new window)。 |
types_or (opens new window) | (可选:默认 [])要运行的文件类型列表(或)。请参阅 使用类型过滤文件 (opens new window)。 2.9.0 中的新功能。 |
exclude_types (opens new window) | (可选:默认 [])要排除的文件模式。 |
always_run (opens new window) | (可选:默认 false)即使 true 没有匹配的文件,这个钩子也会运行。 |
fail_fast (opens new window) | (可选:默认 false)如果 true 此挂钩失败,预提交将停止运行挂钩。 2.16.0 中的新功能。 |
verbose (opens new window) | (可选:默认 false)如果 true,即使挂钩通过,也强制打印挂钩的输出。 |
pass_filenames (opens new window) | (可选:默认 true)如果 false 没有文件名将传递给挂钩。 |
require_serial (opens new window) | (可选:默认 false)如果 true 这个钩子将使用单个进程而不是并行执行。 |
description (opens new window) | (可选:默认 '')钩子的描述。仅用于元数据目的。 |
language_version (opens new window) | (可选:默认 default)请参阅 覆盖语言版本 (opens new window)。 |
minimum_pre_commit_version (opens new window) | (可选:默认 '0')允许一个人指示最低兼容的预提交版本。 |
args (opens new window) | (可选:默认 [])要传递给挂钩的附加参数列表。 |
stages (opens new window) | (可选:默认(所有阶段))将挂钩限制在 commit、merge-commit、 push、prepare-commit-msg、commit-msg、post-checkout、post-commit、 post-merge、post-rewrite 和/或 manual 阶段。请参阅 限制挂钩在特定阶段运行 (opens new window)。 |
# 其他内容快链
大部分内容官方文档已经介绍的很好,这里不再重复介绍,把相关的内容快链如下:
- 如何设置默认启用 (opens new window)
- 支持的语言 (opens new window)
- 按类型过滤文件 (opens new window)
- 使用徽标标记你的存储库 (opens new window)
- 与 GitHub Action 的集成 (opens new window)
# 一些可用的检查概览
https://github.com/Lucas-C/pre-commit-hooks-nodejs
htmlhint:html语法检测
markdown-toc:自动给Markdown添加TOC
dockerfile_lint:检查 dockerfile 的语法
repos: - repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs rev: v1.1.2 hooks: - id: htmlhint # optional custom config: args: [--config, .htmlhintrc] - id: htmllint - id: markdown-toc # optional custom config: args: [--indent, " ", -i] - id: dockerfile_lint # optional custom config: args: [--json, --verbose, --dockerfile]1
2
3
4
5
6
7
8
9
10
11
12
13
14
https://github.com/dnephin/pre-commit-golang
go-fmt- 运行gofmt,需要 golanggo-vet- 运行go vet,需要 golanggo-lint- 运行golint,需要https://github.com/golang/lint但未维护且已弃用,有利于golangci-lint(opens new window)go-imports- 运行goimports,需要 golang.org/x/tools/cmd/goimportsgo-cyclo- 运行gocyclo,需要https://github.com/fzipp/gocyclovalidate-toml- 运行tomlv,需要 https://github.com/BurntSushi/toml/tree/master/cmd/tomlvno-go-testing- 检查没有文件正在使用testing.T,如果您希望开发人员使用不同的测试框架golangci-lint- 运行golangci-lint run ./...,需要 golangci-lint (opens new window)go-critic- 运行gocritic check ./...,需要go-critic (opens new window)go-unit-tests- 跑步go test -tags=unit -timeout 30s -short -vgo-build-运行go build,需要golanggo-mod-tidy-运行go mod tidy -v,需要golanggo-mod-vendor-运行go mod vendor,需要golang- repo: https://github.com/dnephin/pre-commit-golang rev: master hooks: - id: go-fmt - id: go-vet - id: go-lint - id: go-imports - id: go-cyclo args: [-over=15] - id: validate-toml - id: no-go-testing - id: golangci-lint - id: go-critic - id: go-unit-tests - id: go-build - id: go-mod-tidy1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
https://github.com/detailyang/pre-commit-shell
shell-lint:包装shellcheck来检查 shell 脚本
- repo: git://github.com/detailyang/pre-commit-shell rev: v1.0.6 hooks: - id: shell-lint args: [--format=json]1
2
3
4
5
https://github.com/ansible/ansible-lint
- ansible-lint:运行ansible语法检测
https://github.com/fortman/pre-commit-prometheus
check-config- 检查普罗米修斯配置文件check-rules- 检查普罗米修斯规则文件test-rules- 单元测试普罗米修斯规则文件
https://github.com/syntaqx/git-hooks
circleci-config-validate- 测试 CircleCI 配置是否正确。go-fmt- 运行 go fmt 并断言不需要任何更改。go-test- 运行 go test 并断言没有测试失败。go-mod-tidy- 运行 go mod tidy 以确保 go.mod 与项目源匹配。go-generate- 针对项目 go 文件运行 go generate 。forbid-binary- 禁止提交二进制文件shellcheck- Shell 脚本符合 shellcheckshfmt- 使用 shfmt 检查 shell 样式
https://github.com/TekWizely/pre-commit-golang
my-cmd- 为每个暂存的 .go 文件运行 '$ARGS[0] [$ARGS[1:]] $FILE'my-cmd-mod- 运行 'cd $(mod_root $FILE); $ARGS[0] [$ARGS[1:]] ./...' 对于每个暂存的 .go 文件my-cmd-pkg- 为每个暂存的 .go 文件运行 '$ARGS[0] [$ARGS[1:]] ./$(dirname $FILE)'my-cmd-repo- 在 repo 根文件夹中运行“$ARGS[0] [$ARGS[1:]]”my-cmd-repo-mod- 运行'cd $(mod_root); $ARGS[0] [$ARGS[1:]] /...' 用于 repo 中的每个模块my-cmd-repo-pkg- 在 repo 根文件夹中运行“$ARGS[0] [$ARGS[1:]] ./...”go-build-mod- 运行 'cd $(mod_root $FILE); go build -o /dev/null [$ARGS] ./...' 对于每个暂存的 .go 文件go-build-pkg- 为每个暂存的 .go 文件运行“go build -o /dev/null [$ARGS] ./$(dirname $FILE)”go-build-repo-mod- 运行'cd $(mod_root); go build -o /dev/null [$ARGS] ./...' 为 repo 中的每个模块go-build-repo-pkg- 在 repo 根文件夹中运行“go build -o /dev/null [$ARGS] ./...”go-critic- 为每个暂存的 .go 文件运行“gocritic check [$ARGS] $FILE”go-fmt- 为每个暂存的 .go 文件运行 'gofmt -l -d [$ARGS] $FILE'go-fmt-repo- 运行“gofmt -l -d [$ARGS]”。在回购根文件夹中go-fumpt- 为每个暂存的 .go 文件运行 'fumpt -l -d [$ARGS] $FILE'go-fumpt-repo- 运行“fumpt -l -d [$ARGS]”。在回购根文件夹中go-imports- 为每个暂存的 .go 文件运行“goimports -l -d [$ARGS] $FILE”go-imports-repo- 运行“goimports -l -d [$ARGS]”。在回购根文件夹中go-lint- 为每个暂存的 .go 文件运行“golint -set_exit_status [$ARGS] $FILE”go-mod-tidy- 运行 'cd $(mod_root $FILE); go mod tidy [$ARGS]' 为每个暂存的 .go 文件go-mod-tidy-repo- 运行'cd $(mod_root); go mod tidy [$ARGS]' 为 repo 中的每个模块go-returns- 为每个暂存的 .go 文件运行 'goreturns -l -d [$ARGS] $FILE'go-returns-repo- 运行“goreturns -l -d [$ARGS]”。在回购根文件夹中go-revive- 为每个暂存的 .go 文件运行'revive [$ARGS] $FILE'go-revive-mod- 运行 'cd $(mod_root $FILE); 为每个暂存的 .go 文件恢复 [$ARGS] ./...'go-revive-repo-mod- 运行'cd $(mod_root); 为 repo 中的每个模块恢复 [$ARGS] ./...'go-sec-mod- 运行 'cd $(mod_root $FILE); gosec [$ARGS] ./...' 用于每个暂存的 .go 文件go-sec-pkg- 为每个暂存的 .go 文件运行 'gosec [$ARGS] ./$(dirname $FILE)'go-sec-repo-mod- 运行'cd $(mod_root); gosec [$ARGS] ./...' 用于 repo 中的每个模块go-sec-repo-pkg- 在 repo 根文件夹中运行“gosec [$ARGS] ./...”go-staticcheck-mod- 运行 'cd $(mod_root $FILE); staticcheck [$ARGS] ./...' 用于每个暂存的 .go 文件go-staticcheck-pkg- 为每个暂存的 .go 文件运行 'staticcheck [$ARGS] ./$(dirname $FILE)'go-staticcheck-repo-mod- 运行'cd $(mod_root); staticcheck [$ARGS] ./...' 用于 repo 中的每个模块go-staticcheck-repo-pkg- 在 repo 根文件夹中运行“staticcheck [$ARGS] ./...”go-structslop-mod- 运行 'cd $(mod_root $FILE); structslop [$ARGS] ./...' 用于每个暂存的 .go 文件go-structslop-pkg- 为每个暂存的 .go 文件运行“structslop [$ARGS] ./$(dirname $FILE)”go-structslop-repo-mod- 运行'cd $(mod_root); structslop [$ARGS] ./...' 用于 repo 中的每个模块go-structslop-repo-pkg- 在 repo 根文件夹中运行“structslop [$ARGS] ./...”go-test-mod- 运行 'cd $(mod_root $FILE); go test [$ARGS] ./...' 为每个暂存的 .go 文件go-test-pkg- 为每个暂存的 .go 文件运行“go test [$ARGS] ./$(dirname $FILE)”go-test-repo-mod- 运行'cd $(mod_root); 去测试 [$ARGS] ./...' 为 repo 中的每个模块go-test-repo-pkg- 在 repo 根文件夹中运行“go test [$ARGS] ./...”go-vet-mod- 运行 'cd $(mod_root $FILE); go vet [$ARGS] ./...' 为每个上演的 .go 文件go-vet-pkg- 为每个暂存的 .go 文件运行“go vet [$ARGS] ./$(dirname $FILE)”go-vet-repo-mod- 运行'cd $(mod_root); go vet [$ARGS] ./...' 对于 repo 中的每个模块go-vet-repo-pkg- 在 repo 根文件夹中运行“go vet [$ARGS] ./...”go-vet- 为每个暂存的 .go 文件运行“go vet [$ARGS] $FILE”golangci-lint-mod- 运行 'cd $(mod_root $FILE); golangci-lint 为每个暂存的 .go 文件运行 [$ARGS] ./...'golangci-lint-pkg- 为每个暂存的 .go 文件运行“golangci-lint run [$ARGS] ./$(dirname $FILE)”golangci-lint-repo-mod- 运行'cd $(mod_root); golangci-lint 为 repo 中的每个模块运行 [$ARGS] ./...'golangci-lint-repo-pkg- 在 repo 根文件夹中运行“golangci-lint run [$ARGS] ./...”golangci-lint- 为每个暂存的 .go 文件运行“golangci-lint run [$ARGS] $FILE”
https://github.com/PeterMosmans/jenkinslint
jenkinslint- 使用 Jenkins 服务器验证 Jenkinsfiles
https://github.com/mrtazz/checkmake
checkmake- Makefile linter/分析器