The Unix philosophy
Provide “sharp” gadgets, each of which aims to do one thing well.
— “The Programmer’s Way: From Little Employee to Expert”
Writing in the front
If you use Git, you understand the appeal of plain text and love a scripting language like the shell.
Most of the time, I prefer tools that can be configured through a scripting language rather than tools that are installed directly into the editor. One is that scripts can be shared with more people in the project to keep the specification going. The second is that the operation automatically triggered by the script does not need to remember more shortcuts or click a mouse; The third is that scripting languages can do more flexible operations without the constraints of the software developer. This is probably why I’ve always preferred to use Git directives rather than the Git tools provided to me by compilers.
This article continues with Git hooks, introducing a tool that can help you better manage and utilize them. The tools you expect to find have the following functions:
-
You only need to provide the configuration file to automatically get the script from the central hooks repository
- If you have multiple projects, you do not need to copy hooks for each project
-
Local script repositories can be defined, allowing developers to customize scripts without modifying configuration files
- The developer will have some scripts to perform the custom actions
- This means that you can point directly to a directory and either execute any hooks in it or specify a local configuration file that does not need to be uploaded to git
-
Each phase allows you to define multiple scripts
- Multiple scripts allow functionality to be partitioned without having to be consolidated into a bloated file
- Scripts support multiple languages
The pre – commit profile
Don’t be fooled by the pre-commit name. This tool does not only run during the pre-commit phase, but you can set custom stages on any git-hooks stage, as described in the “Stages” configuration section. (This name probably comes from the fact that they only did the Pre-Commit phase at the beginning and then expanded to other phases).
Install the pre – commit
Install in the systempre-commit
Brew install pre-commit # or PIP install pre-commit # Pre-commit -- Version # Pre-commit 2.12.1 <- This is the version I am currently using
Install in the projectpre-commit
CD <git-repo> pre-commit install # uninstall pre-commit uninstall
Doing so will generate a pre-commit file (overwriting the original pre-commit file) under the project’s.git/hooks, which will execute the task according to the.pre-commit-config.yaml in the project root directory. If Vim.git /hooks/pre-commit can see the implementation of the code, the basic logic is to use the pre-commit file to extend more pre-commits, which is similar to the logic in my last article.
Install/uninstall hooks for other stages.
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 {the pre - commit, pre - merge - commit, pre - a push, prepare - commit - MSG, commit - MSG, post - checkout, post - commit, post - merge} #, such as the pre - commit install --hook-type prepare-commit-msg
Commonly used instructions
Mysql > apply hooks to all files manually, which can be used when adding hooks to the code. Executes this directive directly without having to wait until the pre-commit phase to trigger hooks pre-commit run --all-files # Executes specific hooks pre-commit run <hook_id> # to update all hooks to the latest version /tag The pre - commit autoupdate # specified update repo pre - commit autoupdate - repo https://github.com/DoneSpeak/gromithooks
For more instructions and instructions, please visit the Pre-Commit website.
Add third party hooks
cd <git-repo>
pre-commit install
touch .pre-commit-config.yaml
An example of a basic configuration is shown below.
.pre-commit-config.yaml
# This config file is a pre-commit configuration file for the project, which specifies the git hooks that the project is allowed to execute. # This is one of the global configurations for the pre-commit: fail_fast: false repos: # repo: https://github.com/pre-commit/pre-commit-hooks # Repository version, can use tag or branch directly, but branch is easy to change # If using branch, # with the 'pre-commit autoupdate' directive you can update the tag to the latest tag rev: v4.0.1 of the default branch -id: check-added-large-files # Remove tail space character -id: check-added-large-files Trailing -whitespace # will not handle makedown args: [--markdown-linebreak-ext=md] # check if there is a merge collision symbol -id: Check - the 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/pre Args: [--autofix, --indent, '2'] args: [--autofix, --indent, '2']
After the Run, the Pre-Commit downloads the specified repository code and installs the runtime environment required for the configuration. After the configuration is complete, you can run the added hooks via pre-commit run –all-files. The following table is the.pre-commit-hooks. YAML optional configuration items.
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. |
Develop the hooks repository
Using third-party hooks in your project has been explained above, but some of the functionality is required for customization and cannot be obtained from a third party. This is where we need to develop our own repertoire of 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.
As long as your Git repository is installable or exposed as executable, it can be used by the Pre-Commit. The project demonstrated here is a packable Python project. It was also the first time to write such a project, and it took a lot of effort. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Here is the basic directory structure for the project (for the complete project, see the source path at the end of this article) :
├ ─ ─ the README. Md ├ ─ ─ pre_commit_hooks │ ├ ─ ─ just set py │ ├ ─ ─ cm_tapd_autoconnect. # p y actual execution script │ ├ ─ ─ Pcm_issue_ref_prefix. Py # └── pcm_tapd_ref_prefix. Py # └ -- Exercise-Exercise-Exercise-Exercise-Exercise.yAML # * * * * * * * * * * * * * * * * * * * *
A Git repository that contains a pre-commit plug-in must contain a.pre-commit-hooks. YAML file that tells the pre-commit plug-in information. The configuration options for. Pre-commit-hooks. Yaml are the same as for. Pre-commit-config.yaml.
.pre-commit-hooks.yaml
# this project is a pre-commit hooks repository project that provides 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 # 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 # This is the language used for the implementation of HOOK. [prepare-commit-msg] # Enable intermediate log: # Verbose: true-id: cm-tapd-autoconnect name: # Verbose: true-id: cm-tapd-autoconnect name: # Verbose: true-id: cm-tapd-autoconnect name: # Verbose: true-id: cm-tapd-autoconnect name: # 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: Language: Python Stages: [commit-msg] : [commit-msg] :
Where entry is the instruction to execute and corresponds to the list of [options.entry_points] configured in setup.cfg.
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
Here is the Python script for PCM-issue-ref-prefix, which is used for a prepare-commit-msg hook that prefixes issue to the commit message based on the branch name.
pre_commit_hooks/pcm_issue_ref_prefix.py
# Auto-prefix commit message to associate issue and commit based on the branch name. # # # branch name | commit format -- - | -- - | # 1234, # issue - 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 = commit_msg) The list of parameters for some stages can be found in the Install instructions on the Pre-Commit website.
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())
Just follow the following configuration in the project you want to configure. Pre-commit-config.yaml for use.
Repos: - repo: https://github.com/DoneSpeak/gromithooks rev: v1.0.0 hooks: - id: PCM - issue - the ref - prefix verbose: [prepare-commit- MSG]
Local hooks
Pre-Commit also provides local hooks, which allow you to configure execution instructions in Entry or point to a local executable script file, similar to Husky.
- Scripts are tightly coupled to and distributed with the code repository.
- The states required by Hooks exist only in the build artifact of the code repository (such as the VirtualEnv of PyLint for the application).
- Linter’s official code repository does not provide pre-commit metadata.
Local hooks can be used in languages that support additional_dependencies or docker_image/fail/pygrep/script/system.
-- repo: local hooks: -id: pylint name: pylint entry: pylint language: system types: [python] - id: changelogs-rst name: changelogs must be rst entry: changelog filenames must end in .rst language: Fail # fail is a light language for disallowing files by filename files: 'changelog/.*(? <! \.rst)$'
Customize local scripts
As mentioned at the beginning of this article, I hope to provide a way for developers to create their own hooks, but submit them to a common code base. I have read the official documentation, but I can’t find the relevant feature points. But with the Local Repo functionality above we can develop functionality that meets this requirement.
Because the local repo allows Entry to execute local files, you only need to define an executable file for each stage. In the following configuration, a.git_hooks directory is created under the project directory to hold the developer’s own scripts. (Note that not all stages are defined here, only those supported by Pre-Commit Install -T).
- 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
Follow the principle of automating what can be automated. Scripts that automatically create all of the above phase files are provided (Fail if the script file specified by entry does not exist). Install-git-hooks. Sh installs the pre-commit and the stages supported by the pre-commit, and initializes the hooks in.git_hooks if you specify CUSTOMIZED=1. Add the customized local hooks to.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 "# Use pre-commit to manage hooks" >> $PRE_COMMIT_CONFIG echo "# in your Git project 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' ]; Return fi fi echo -e "\n# Imit. Git_hooks file to prevent script from being committed to code repository \n$CUSTOMIZED_GITHOOK_DIR\n! .git_hooks/.gitkeeper" >> $GITIGNORE_FILE } installPreCommit if [ "$CUSTOMIZED" == "1" ]; then touchCustomizedGitHook initPreCommitConfigYaml else touchPreCommitConfigYaml fi preCommitInstall ignoreCustomizedGitHook
Add a Makefile and provide the make install-git-hook installation directive. This directive automatically downloads the install-git-hooks. Sh file from the git repository and executes it. In addition, if you execute CUSTOMIZED=1 make install-git-hook, the hooks to the account will be initialized.
Makefile
install-git-hooks: install-git-hooks.sh chmod +x ./$< && ./$<install-git-hooks.sh: # if you have Failed to connect to port 443 raw.githubusercontent.com: Connection refused # for DNS pollution problem, can inquire on the https://www.ipaddress.com/ domain name, and then to see # in the hosts file: https://github.com/hawtim/blog/issues/10 wget https://raw.githubusercontent.com/DoneSpeak/gromithooks/v1.0.1/install-git-hooks.sh
Hook files in. Git_hooks can be written in the script that was originally written in. Git /hooks, or in the pre-commit hooks.
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"
If you need to know more about the functions (such as defining the Git Template), you can see the documentation on the website.
Related articles
recommended
- This article source Donespeak/Gromithooks
- Define global and custom Git Hooks
- Correlate TAPD and COMMIT via Git Hook
reference
- pre-commit | A framework for managing and maintaining multi-language pre-commit hooks. @pre-commit.com
- pre-commit | Supported hooks @pre-commit.com
- A worthy reference.pre-commit-config.yaml@github
- Git hooks: Customize your workflow by writing Git hooks in Python
- Packaging Python Projects @python.org provides an introduction to the process from creation to release