In this tutorial we are going to create a very basic git hook, using the awesome package pre-commit
, created by the one and only Anthony Sottile 👏👏.
The hook will be very basic. It won't have unit-tests nor will be very functional for a real project. Although it will show you the first steps you need to take in order to create something incredible.
I recommend you using a virtual environment to follow this tutorial.
Tutorial Index
- Create the structure of the project
- Create the functionality
- Turn it into a python package
- Turn it into a hook
- Test it
1. Create the structure of the project
Create the following project structure:
.
├── test-the-hook-in-this-folder
└── the-hook
├── print_arguments
│ ├── __init__.py
│ └── main.py
├── setup.cfg
└── setup.py
-
test-the-hook-in-this-folder
: Folder in which we will test our hook by the end of the tutorial (we won't use it until step 5). -
the-hook
: Folder that contains all the files required for the package. -
print_arguments
: This is our python package (and also a spoiler of what our hook will do). -
__init__.py
: Turn the folder that contains it into a package. -
main.py
: Here is where the logic of the hook will be. -
setup.py
: Necessary for building our package. -
setup.cfg
: Describe our package.
2. Create the functionality
We will work in the
the-hook
folder for steps 2 to 4.
For creating the functionality, we only need to edit the main.py
file:
# print_arguments/main.py
import argparse
def print_arguments(arguments: list[str]):
for argument in arguments:
print(argument)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("filenames", nargs="*")
args = parser.parse_args()
print_arguments(args.filenames)
if __name__ == "__main__":
main()
Yes, our hook will print the arguments we pass to it. We can test its functionality:
python print_arguments/main.py arg1 arg2 arg3
Output:
arg1
arg2
arg3
3. Turn it into a python package
To do this, we need to edit our 2 setup files.
setup.py:
# setup.py
from setuptools import setup
setup()
setup.cfg:
# setup.cfg
[metadata]
name = print-arguments
description = print the arguments you pass
version = 0.1.0
author = Jorge Alvarado
author_email = alvaradosegurajorge@gmail.com
license = MIT
url = https://jorgealvarado.me
[options]
packages = find:
[options.entry_points]
console_scripts =
print-arguments = print_arguments.main:main
Important points:
-
packages = find:
: It help us find ourprint_arguments
package when packaging. -
console_scripts
: This is our entry point, with this we kind of expose our function to the world. The name of our single entry point, in this caseprint-arguments
, will be used bypre-commit
.
You can choose other values for the rest of the points.
Install the package with:
pip install .
Make sure you are in
the-hook
folder when running that command.
4. Turn it into a hook
For our hook to work we need our project to be a git repository. So let's do that by running the following commands (make sure you are in the the-hook
folder):
Initialize the git repository:
git init
Add every file and commit them:
git add .; git commit -m "Create the package"
Now the hook part. First let's install pre-commit
:
pip install pre-commit
Create a .pre-commit-hooks.yaml
file inside the the-hook
folder and edit it:
# .pre-commit-hooks.yaml
- id: some-id
name: some-name
description: some description
entry: print-arguments
language: python
-
id
: The id of our hook. We will need it for testing and in the future if someone wants to use our hook, the id will be required. -
name
: The name of the hook, it is what is shown during hook execution. -
description
(optional): Description of the hook. -
entry
: The entry point, the executable to run. It has to match with the entry point name defined inside oursetup.cfg
. -
language
: The language of the hook, it tellspre-commit
how to install the hook.
👀 See a complete list of the options.
To complete this step, we need to do a commit of our hook stuff:
git add .; git commit -m "Create a hook"
5. Test it
Time to use the test-the-hook-in-this-folder
folder. Change directory to that folder, once there, let's create a very little git project:
Create some files
touch a.py b.py
Initialize the git repository:
git init
Commit the files:
git add .; git commit -m "Create the project"
Finally, let's test our hook:
pre-commit try-repo ../the-hook some-id --verbose --all-files
Output:
some-name................................................................Passed
- hook id: some-id
- duration: 0.08s
a.py
b.py
Notice:
- We got
some-name
in the output, because that's what we defined in our.pre-commit-hooks.yaml
file. - We used
some-id
(defined in the.pre-commit-hooks.yaml
file as well) in the command to refer to our hook (we could have multiple hooks). -
a.py
andb.py
were printed. That's because we used the flag--all-files
in our command (If we didn't, the hook execution would have been skipped, because there are no new/edited files for git).
Remember that you need to be in the
test-the-hook-in-this-folder
folder for the command just used to work.
If we now add a new file to our test folder:
touch c.py
We track it with git:
git add c.py
And then we run the command without the --all-files
flag:
pre-commit try-repo ../the-hook some-id --verbose
We get:
some-name................................................................Passed
- hook id: some-id
- duration: 0.07s
c.py
As you can see, only c.py
was printed this time, because is the only new/edited file for git, so our hook was run only against that file. This is the behavior you will normally want.
Now you know how to create a pre-commit hook 🪝! Go and make cool things with this knowledge 😁.
Let me know if you create a hook or something. This is one I created a few weeks ago.
Top comments (3)
Amazing post
Thank you for this post!
Super clear ! Thanks a lot! 👏