共计 13342 个字符,预计需要花费 34 分钟才能阅读完成。
Unix 哲学
提供”尖锐“的小工具、其中每一把都意在把一件事件做好。
–《程序员修炼之道 – 从小工到专家》
写在后面
如果你应用 Git,那你肯定懂得纯文本的魅力并青睐上 shell 这样的脚本语言。
在很多时候,我更喜爱可能通过脚本语言进行配置的工具,而不是间接装置到编辑器的工具。一是因为脚本能够放在我的项目中与更多的人共享,以放弃标准始终;二是脚本主动触发的操作无须要记更多的快捷键或者点击一点鼠标;再来则是脚本语言能够做更多灵便的操作,而不受软件开发者的束缚。这大略也是我始终喜爱用 Git 指令,而不是编译器提供给我的 Git 工具。
本文将持续解说 git hooks,介绍一款可能帮忙咱们更好地治理和利用 git hooks 的工具。冀望找到的工具有如下的性能:
-
只须要提供配置文件,主动从地方 hooks 仓库获取脚本
- 如果有多个我的项目,就不须要再每个我的项目都拷贝一份 hooks 了
-
能够定义本地脚本仓库,容许开发人员自定义脚本,且无需批改配置文件
- 开发人员会有一些脚本以实现的自定义操作
- 无需批改配置文件是指能够间接指向一个目录,并执行外面的所有 hooks 或者指定一个无需上传到 git 的本地配置文件
-
每个阶段容许定义多个脚本
- 多个脚本能够使得性能划分而无需整合到一个臃肿的文件中
- 脚本反对多种语言
pre-commit 概要
不要被这个 pre-commit 的名字蛊惑,这个工具不仅仅能够在 pre-commit 阶段执行,其实能够在 git-hooks 的任意阶段,设置自定义阶段执行,见的 stages
配置的解说。(这个名字大略是因为他们开始只做了 pre-commit 阶段的,后续才拓展了其余的阶段)。
装置 pre-commit
在零碎中装置pre-commit
brew install pre-commit
# 或者
pip install pre-commit
# 查看版本
pre-commit --version
# pre-commit 2.12.1 <- 这是我以后应用的版本
在我的项目中装置pre-commit
cd <git-repo>
pre-commit install
# 卸载
pre-commit uninstall
依照操作将会在我的项目的 .git/hooks
下生成一个 pre-commit
文件(笼罩原 pre-commit 文件),该 hook 会依据我的项目根目录下的 .pre-commit-config.yaml
执行工作。如果 vim .git/hooks/pre-commit
能够看到代码的实现,根本逻辑是利用 pre-commit
文件去拓展更多的 pre-commit,这个和我上一篇文章的逻辑是相似的。
装置 / 卸载其余阶段的 hook。
pre-commit install
pre-commit uninstall
-t {pre-commit,pre-merge-commit,pre-push,prepare-commit-msg,commit-msg,post-checkout,post-commit,post-merge}
--hook-type {pre-commit,pre-merge-commit,pre-push,prepare-commit-msg,commit-msg,post-checkout,post-commit,post-merge}
# 如 pre-commit install --hook-type prepare-commit-msg
罕用指令
# 手动对所有的文件执行 hooks,新增 hook 的时候能够执行,使得代码均符合规范。间接执行该指令则无需等到 pre-commit 阶段再触发 hooks
pre-commit run --all-files
# 执行特定 hooks
pre-commit run <hook_id>
# 将所有的 hook 更新到最新的版本 /tag
pre-commit autoupdate
# 指定更新 repo
pre-commit autoupdate --repo https://github.com/DoneSpeak/gromithooks
更多指令及指令参数请间接拜访 pre-commit 官方网站。
增加第三方 hooks
cd <git-repo>
pre-commit install
touch .pre-commit-config.yaml
如下为一个根本的配置样例。
.pre-commit-config.yaml
# 该 config 文件为该项目标 pre-commit 的配置文件,用于指定该我的项目能够执行的 git hooks
# 这是 pre-commit 的全局配置之一
fail_fast: false
repos:
# hook 所在的仓库
- repo: https://github.com/pre-commit/pre-commit-hooks
# 仓库的版本,能够间接用 tag 或者分支,但分支是容易发生变化的
# 如果应用分支,则会在第一次装置之后不自动更新
# 通过 `pre-commit autoupdate` 指令能够将 tag 更新到默认分支的最新 tag
rev: v4.0.1
# 仓库中的 hook id
hooks:
# 定义的 hook 脚本,在 repo 的.pre-commit-hooks.yaml 中定义
- id: check-added-large-files
# 移除尾部空格符
- id: trailing-whitespace
# 传入参数,不解决 makedown
args: [--markdown-linebreak-ext=md]
# 查看是否含有合并抵触符号
- id: check-merge-conflict
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
rev: v2.0.0
hooks:
- id: pretty-format-yaml
# https://github.com/macisamuele/language-formatters-pre-commit-hooks/blob/v2.0.0/language_formatters_pre_commit_hooks/pretty_format_yaml.py
# hook 脚本须要的参数,能够在该 hook 脚本文件中看到
args: [--autofix, --indent, '2']
在 run
之后,pre-commit 会下载指定仓库代码,并装置配置所须要的运行环境。配置实现之后能够通过 pre-commit run --all-files
运行一下增加的 hooks。下表为 .pre-commit-hooks.yaml
可选配置项。
key word | description |
---|---|
id |
the id of the hook – used in pre-commit-config.yaml. |
name |
the name of the hook – shown during hook execution. |
entry |
the entry point – the executable to run. entry can also contain arguments that will not be overridden such as entry: autopep8 -i . |
language |
the language of the hook – tells pre-commit how to install the hook. |
files |
(optional: default '' ) the pattern of files to run on. |
exclude |
(optional: default ^$ ) exclude files that were matched by files |
types |
(optional: default [file] ) list of file types to run on (AND). See Filtering files with types. |
types_or |
(optional: default [] ) list of file types to run on (OR). See Filtering files with types. new in 2.9.0. |
exclude_types |
(optional: default [] ) the pattern of files to exclude. |
always_run |
(optional: default false ) if true this hook will run even if there are no matching files. |
verbose |
(optional) if true , forces the output of the hook to be printed even when the hook passes. new in 1.6.0. |
pass_filenames |
(optional: default true ) if false no filenames will be passed to the hook. |
require_serial |
(optional: default false ) if true this hook will execute using a single process instead of in parallel. new in 1.13.0. |
description |
(optional: default '' ) description of the hook. used for metadata purposes only. |
language_version |
(optional: default default ) see Overriding language version. |
minimum_pre_commit_version |
(optional: default '0' ) allows one to indicate a minimum compatible pre-commit version. |
args |
(optional: default [] ) list of additional parameters to pass to the hook. |
stages |
(optional: default (all stages)) confines the hook to the commit , merge-commit , push , prepare-commit-msg , commit-msg , post-checkout , post-commit , post-merge , or manual stage. See Confining hooks to run at certain stages. |
开发 hooks 仓库
下面曾经解说了在我的项目中应用第三方的 hooks,但有局部性能是定制化须要的,无奈从第三方取得。这时候就须要咱们本人开发本人的 hooks 仓库。
As long as your git repo is an installable package (gem, npm, pypi, etc.) or exposes an executable, it can be used with pre-commit.
只有你的 git 仓库是可装置的或者裸露为可执行的,它就能够被 pre-commit 应用。这里演示的我的项目为可打包的 Python 我的项目。也是第一次写这样的我的项目,花了不少力量。如果是不怎么接触的 Python 的,能够跟着文末的Packaging Python Projects,也能够模拟第三方 hooks 仓库来写。
如下为我的项目的目录根本构造(残缺我的项目见文末的源码门路):
├── README.md
├── pre_commit_hooks
│ ├── __init__.py
│ ├── cm_tapd_autoconnect.py # 理论执行的脚本
│ ├── pcm_issue_ref_prefix.py # 理论执行的脚本
│ └── pcm_tapd_ref_prefix.py # 理论执行的脚本
├── .pre-commit-hooks.yaml # 配置 pre-commit hooks entry
├── pyproject.toml
├── setup.cfg # 我的项目信息,配置 hook entry point 执行的脚本
└── setup.py
一个含有 pre-commit 插件的 git 仓库,必须含有一个 .pre-commit-hooks.yaml
文件,告知 pre-commit
插件信息。.pre-commit-hooks.yaml
的配置可选项和 .pre-commit-config.yaml
是一样的。
.pre-commit-hooks.yaml
# 该我的项目为一个 pre-commit hooks 仓库我的项目,对外提供 hooks
- id: pcm-issue-ref-prefix
name: Add issue reference prefix for commit msg
description: Add issue reference prefix for commit msg to link commit and issue
entry: pcm-issue-ref-prefix
# 实现 hook 所应用的语言
language: python
stages: [prepare-commit-msg]
- id: pcm-tapd-ref-prefix
name: Add tapd reference prefix for commit msg
description: Add tapd reference prefix for commit msg
entry: pcm-tapd-ref-prefix
# 实现 hook 所应用的语言
language: python
stages: [prepare-commit-msg]
# 强制输入两头日志,这里不做配置,由用户在 .pre-commit-config.yaml 中指定
# verbose: true
- id: cm-tapd-autoconnect
name: Add tapd reference for commit msg
description: Add tapd reference for commit msg to connect tapd and commit
entry: cm-tapd-autoconnect
# 实现 hook 所应用的语言
language: python
stages: [commit-msg]
其中中的 entry 为执行的指令,对应在 setup.cfg
中的 [options.entry_points]
配置的列表。
setup.cfg
...
[options.entry_points]
console_scripts =
cm-tapd-autoconnect = pre_commit_hooks.cm_tapd_autoconnect:main
pcm-tapd-ref-prefix = pre_commit_hooks.pcm_tapd_ref_prefix:main
pcm-issue-ref-prefix = pre_commit_hooks.pcm_issue_ref_prefix:main
以下是 pcm-issue-ref-prefix
对应的 python 脚本,该脚本用于依据 branch name 为 commit message 增加 issue 前缀的一个prepare-commit-msg
hook。
pre_commit_hooks/pcm_issue_ref_prefix.py
# 依据分支名,主动增加 commit message 前缀以关联 issue 和 commit。#
# 分支名 | commit 格局
# --- | ---
# issue-1234 | #1234, message
# issue-1234-fix-bug | #1234, message
import sys, os, re
from subprocess import check_output
from typing import Optional
from typing import Sequence
def main(argv: Optional[Sequence[str]] = None) -> int:
commit_msg_filepath = sys.argv[1]
# 检测咱们所在的分支
branch = check_output(['git', 'symbolic-ref', '--short', 'HEAD']).strip().decode('utf-8')
# 匹配如:issue-123, issue-1234-fix
result = re.match('^issue-(\d+)((-.*)+)?$', branch)
if not result:
# 分支名不合乎
warning = "WARN: Unable to add issue prefix since the format of the branch name dismatch."
warning += "\nThe branch should look like issue-<number> or issue-<number>-<other>, for example: issue-100012 or issue-10012-fix-bug)"
print(warning)
return
issue_number = result.group(1)
with open(commit_msg_filepath, 'r+') as f:
content = f.read()
if re.search('^#[0-9]+(.*)', content):
# print('There is already issue prefix in commit message.')
return
issue_prefix = '#' + issue_number
f.seek(0, 0)
f.write("%s, %s" % (issue_prefix, content))
# print('Add issue prefix %s to commit message.' % issue_prefix)
if __name__ == '__main__':
exit(main())
这里用 commit_msg_filepath = sys.argv[1]
获取 commit_msg 文件的门路,当然,你也能够用 argparse
获取到。局部阶段的参数列表能够在 pre-commit 官网的 install 命令解说中看到。
import argparsefrom typing import Optionalfrom typing import Sequencedef main(argv: Optional[Sequence[str]] = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filename', nargs='*', help='Filenames to check.') args = parser.parse_args(argv) # .git/COMMIT_EDITMSG print("commit_msg file is" + args.filename[0])if __name__ == '__main__': exit(main())
只有在须要配置的我的项目中依照如下配置 .pre-commit-config.yaml
即可应用。
repos:- repo: https://github.com/DoneSpeak/gromithooks rev: v1.0.0 hooks: - id: pcm-issue-ref-prefix verbose: true # 指定 hook 执行的阶段 stages: [prepare-commit-msg]
本地 hooks
pre-commit 也提供了 local
的 hook,容许在 entry
中配置执行指令或指向本地一个可执行的脚本文件,应用起来和 husky
相似。
- 脚本与代码仓库严密耦合,并且与代码仓库一起散发。
- Hooks 须要的状态只存在于代码仓库的 build artifact 中(比方应用程序的 pylint 的 virtualenv)。
- linter 的官网代码仓库没有提供 pre-commit metadata.
local hooks 能够应用反对additional_dependencies
的语言或者 docker_image
/ fail
/ pygrep
/ script
/ system
。
# 定义 repo 为 local,示意该 repo 为本地仓库 - repo: local hooks: - id: pylint name: pylint entry: pylint language: system types: - id: changelogs-rst name: changelogs must be rst entry: changelog filenames must end in .rst language: fail # fail 是一种用于通过文件名禁止文件的轻语言 files: 'changelog/.*(?<!\.rst)$'
自定义本地脚本
在文章开篇也有说到,心愿能够提供一个办法让开发人员创立本人的 hooks,但提交到公共代码库中。我看完了官网的文档,没有找到相干的性能点。但通过下面的 local repo
性能咱们能够开发合乎该需要的性能。
因为 local repo
容许 entry 执行本地文件,所以只有为每个阶段定义一个可执行的文件即可。上面的配置中,在我的项目更目录下创立了一个 .git_hooks
目录,用来寄存开发人员本人的脚本。(能够留神到这里并没有定义出全副的 stage,仅仅定义了 pre-commit install -t
反对的 stage)。
- repo: local
hooks:
- id: commit-msg
name: commit-msg (local)
entry: .git_hooks/commit-msg
language: script
stages: [commit-msg]
# verbose: true
- id: post-checkout
name: post-checkout (local)
entry: .git_hooks/post-checkout
language: script
stages: [post-checkout]
# verbose: true
- id: post-commit
name: post-commit (local)
entry: .git_hooks/post-commit
language: script
stages: [post-commit]
# verbose: true
- id: post-merge
name: post-merge (local)
entry: .git_hooks/post-merge
language: script
stages: [post-merge]
# verbose: true
- id: pre-commit
name: pre-commit (local)
entry: .git_hooks/pre-commit
language: script
stages: [commit]
# verbose: true
- id: pre-merge-commit
name: pre-merge-commit (local)
entry: .git_hooks/pre-merge-commit
language: script
stages: [merge-commit]
# verbose: true
- id: pre-push
name: pre-push (local)
entry: .git_hooks/pre-push
language: script
stages: [push]
# verbose: true
- id: prepare-commit-msg
name: prepare-commit-msg (local)
entry: .git_hooks/prepare-commit-msg
language: script
stages: [prepare-commit-msg]
# verbose: true
遵循可能自动化的就自动化的准则。这里提供了主动创立以上所有阶段文件的脚本(如果 entry 指定的脚本文件不存在会 Fail)。install-git-hooks.sh
会装置 pre-commit
和 pre-commit 反对的 stage,如果指定 CUSTOMIZED=1
则初始化 .git_hooks
中的 hooks,并增加 customized local hooks 到.pre-commit-config.yaml
。
install-git-hooks.sh
#!/bin/bash
:<<'COMMENT'
chmod +x install-git-hooks.sh
./install-git-hooks.sh
# intall with initializing customized hooks
CUSTOMIZED=1 ./install-git-hooks.sh
COMMENT
STAGES="pre-commit pre-merge-commit pre-push prepare-commit-msg commit-msg post-checkout post-commit post-merge"
installPreCommit() {HAS_PRE_COMMIT=$(which pre-commit)
if [-n "$HAS_PRE_COMMIT"]; then
return
fi
HAS_PIP=$(which pip)
if [-z "$HAS_PIP"]; then
echo "ERROR:pip is required, please install it mantually."
exit 1
fi
pip install pre-commit
}
touchCustomizedGitHook() {
mkdir .git_hooks
for stage in $STAGES
do
STAGE_HOOK=".git_hooks/$stage"
if [-f "$STAGE_HOOK"]; then
echo "WARN:Fail to touch $STAGE_HOOK because it exists."
continue
fi
echo -e "#!/bin/bash\n\n# general git hooks is available." > "$STAGE_HOOK"
chmod +x "$STAGE_HOOK"
done
}
preCommitInstall() {
for stage in $STAGES
do
STAGE_HOOK=".git/hooks/$stage"
if [-f "$STAGE_HOOK"]; then
echo "WARN:Fail to install $STAGE_HOOK because it exists."
continue
fi
pre-commit install -t "$stage"
done
}
touchPreCommitConfigYaml() {
PRE_COMMIT_CONFIG=".pre-commit-config.yaml"
if [-f "$PRE_COMMIT_CONFIG"]; then
echo "WARN: abort to init .pre-commit-config.yaml for it's existence."
return 1
fi
touch $PRE_COMMIT_CONFIG
echo "# 在 Git 我的项目中应用 pre-commit 对立治理 hooks" >> $PRE_COMMIT_CONFIG
echo "# https://donespeak.gitlab.io/posts/210525-using-pre-commit-for-git-hooks/" >> $PRE_COMMIT_CONFIG
}
initPreCommitConfigYaml() {
touchPreCommitConfigYaml
if ["$?" == "1"]; then
return 1
fi
echo "" >> $PRE_COMMIT_CONFIG
echo "repos:" >> $PRE_COMMIT_CONFIG
echo "- repo: local" >> $PRE_COMMIT_CONFIG
echo "hooks:" >> $PRE_COMMIT_CONFIG
for stage in $STAGES
do
echo "- id: $stage" >> $PRE_COMMIT_CONFIG
echo "name: $stage (local)" >> $PRE_COMMIT_CONFIG
echo "entry: .git_hooks/$stage" >> $PRE_COMMIT_CONFIG
echo "language: script" >> $PRE_COMMIT_CONFIG
if [[$stage == pre-*]]; then
stage=${stage#pre-}
fi
echo "stages: [$stage]" >> $PRE_COMMIT_CONFIG
echo "# verbose: true" >> $PRE_COMMIT_CONFIG
done
}
ignoreCustomizedGitHook() {
CUSTOMIZED_GITHOOK_DIR=".git_hooks/"
GITIGNORE_FILE=".gitignore"
if [-f "$GITIGNORE_FILE"]; then
if ["$(grep -c"$CUSTOMIZED_GITHOOK_DIR"$GITIGNORE_FILE)" -ne '0' ]; then
# 判断文件中曾经有配置
return
fi
fi
echo -e "\n# 疏忽.git_hooks 中文件,使得其中的脚本不提交到代码仓库 \n$CUSTOMIZED_GITHOOK_DIR\n!.git_hooks/.gitkeeper" >> $GITIGNORE_FILE
}
installPreCommit
if ["$CUSTOMIZED" == "1"]; then
touchCustomizedGitHook
initPreCommitConfigYaml
else
touchPreCommitConfigYaml
fi
preCommitInstall
ignoreCustomizedGitHook
增加 Makefile,提供 make install-git-hook
装置指令。该指令会主动下载 git 仓库中的 install-git-hooks.sh
文件,并执行。此外,如果执行 CUSTOMIZED=1 make install-git-hook
则会初始化 customized 的 hooks。
Makefile
install-git-hooks: install-git-hooks.sh chmod +x ./$< && ./$<install-git-hooks.sh: # 如果遇到 Failed to connect to raw.githubusercontent.com port 443: Connection refused # 为 DNS 净化问题,可在 https://www.ipaddress.com/ 查问域名,而后写入 hosts 文件中 # 见:https://github.com/hawtim/blog/issues/10 wget https://raw.githubusercontent.com/DoneSpeak/gromithooks/v1.0.1/install-git-hooks.sh
在.git_hooks 中的 hook 文件能够依照本来在.git/hooks 中的脚本写,也能够依照 pre-commit 的 hook 写。
prepare-commit-msg
#!/usr/bin/env python
import argparse
from typing import Optional
from typing import Sequence
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('filename', nargs='*', help='Filenames to check.')
args = parser.parse_args(argv)
# .git/COMMIT_EDITMSG
print("commit_msg file is" + args.filename[0])
if __name__ == '__main__':
exit(main())
prepare-commit-msg
#!/bin/bash
echo "commit_msg file is $1"
到这里 pre-commit
的次要性能就解说实现了,如果须要理解更多的性能(如定义 git template),能够看官网文档。
相干文章
举荐
- 本文章源码 Donespeak/Gromithooks
- 定义全局 Git Hooks 和自定义 Git Hooks
- 通过 Git Hook 关联 Tapd 和 Commit
参考
- pre-commit | A framework for managing and maintaining multi-language pre-commit hooks. @pre-commit.com
- pre-commit | Supported hooks @pre-commit.com
- 一个值得参考的.pre-commit-config.yaml @github
- Git 钩子:自定义你的工作流 用 python 写 git hooks
- Packaging Python Projects @python.org 会给出一个从创立到公布的流程介绍