DEV Community

Cover image for Create your own pre-commit hook
Jorge Alvarado
Jorge Alvarado

Posted on • Edited on

Create your own pre-commit hook

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

  1. Create the structure of the project
  2. Create the functionality
  3. Turn it into a python package
  4. Turn it into a hook
  5. 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
Enter fullscreen mode Exit fullscreen mode
  • 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()
Enter fullscreen mode Exit fullscreen mode

Yes, our hook will print the arguments we pass to it. We can test its functionality:

python print_arguments/main.py arg1 arg2 arg3
Enter fullscreen mode Exit fullscreen mode

Output:

arg1
arg2
arg3
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Important points:

  • packages = find:: It help us find our print_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 case print-arguments, will be used by pre-commit.

You can choose other values for the rest of the points.

Install the package with:

pip install .
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Add every file and commit them:

git add .; git commit -m "Create the package"
Enter fullscreen mode Exit fullscreen mode

Now the hook part. First let's install pre-commit:

pip install pre-commit
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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 our setup.cfg.
  • language: The language of the hook, it tells pre-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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Initialize the git repository:

git init
Enter fullscreen mode Exit fullscreen mode

Commit the files:

git add .; git commit -m "Create the project"
Enter fullscreen mode Exit fullscreen mode

Finally, let's test our hook:

pre-commit try-repo ../the-hook some-id --verbose --all-files
Enter fullscreen mode Exit fullscreen mode

Output:

some-name................................................................Passed
- hook id: some-id
- duration: 0.08s

a.py
b.py
Enter fullscreen mode Exit fullscreen mode

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 and b.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
Enter fullscreen mode Exit fullscreen mode

We track it with git:

git add c.py
Enter fullscreen mode Exit fullscreen mode

And then we run the command without the --all-files flag:

pre-commit try-repo ../the-hook some-id --verbose
Enter fullscreen mode Exit fullscreen mode

We get:

some-name................................................................Passed
- hook id: some-id
- duration: 0.07s

c.py
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
libialany profile image
LibiaLany

Amazing post

Collapse
 
oguzhan-yilmaz profile image
Oguzhan Yilmaz

Thank you for this post!

Collapse
 
mvargas33 profile image
Maximiliano Vargas

Super clear ! Thanks a lot! 👏