Git Hooks (Part II) – Implementing Git hooks using Python

This post describes how to implement Git hooks using Python. If you have some commit hooks in Python or any other languages please do share them in the comments as Gists or repositories.

General Information

The Templates Directories

We have two directories that interest us:

  • The '/usr/share/git-core/templates/' directory on Linux and 'C:/Program Files (x86)/Git/share/git-core/templates/' directory on Windows (Note that on 32bit machines msysGit is installed by default on 'C:/Program Files/…') in which the default hooks are being copied from. If you installed Git using another configuration the installation might reside in a different folder. Adjust the path accordingly.
  • The '.git/hooks/' directory is the directory in which the hooks templates are being copied to.

The hooked are being copied from the '[...]/share/git-core/templates/'  directory.  There are other types of templates but they are out of scope for this post.

Note:  If you change the templates directory the hooks directory  must be a subdirectory of the templates directory. Do not set the templates directory to the desired hooks directory instead.

Changing the global templates directory

If you'd like to change the global templates directory open the terminal and type:

You can also set the $GIT_TEMPLATE_DIR environment variable:

The global templates directory is exclusively for hook templates. Just as we usually don’t like to use site-packages for Python and therefore we set up a virtualenv for containment of each project/deployment/test suite we don’t want global business logic for our hooks, just framework code.

The .git/hooks/ directory should contain the business logic of the hooks.

Changing the repository's templates directory

You can change the template directory when you git init or git clone using the --template option to the desired templates directory specifically for the newly created repository.

For new repositories open the terminal and type:

For cloned repositories open the terminal and type: 

Order of precedence

The hooks templates folder will be chosen by the following precedence:

  • The argument given with the --template option.
  • The value of the $GIT_TEMPLATE_DIR.
  • The init.templatedir configuration variable.
  • The default templates directory.

Starting Fresh

The default templates directory contains some examples. Each example script ends with the *.example extension. If you remove the extension they become executable git hooks.

Since we are developing using Python lets create a new templates folder. Copy the templates into the python-templates folder.

Erase the contents of the hooks folder, download this gist and extract it into the empty hooks folder.

Architecture

Hooks are simple CLI programs. That's how git works. Every git command or sub system is a CLI program. 

Parsing Arguments

The Python standard library provides us with two libraries for parsing command line arguments: 

  • Optparse - Simple but deprecated.
  • Argparse - Has support for everything we need including accepting STDIN as an argument which is used by git's server side hooks

You can also use the wonderful docopt , a library that automagically parses your docstrings and generates a CLI for you. It has ports to different languages so if you are not using python, you can still use it.

Communicating with Git

Sometimes you need to access your git repository to fetch information or even modify it. Python has a library that do just that. It's called DulwitchA pure Python implementation of Git.

Testing Your Hooks

Testing your hooks is not hard but here are a few guidelines that might help you in doing so:

Unit Testing

Since we know git works we should mock out the dependencies that git hooks use. For example commit hooks provide the name of the file that contains the commit message as an argument.

You can do so by mocking open() as explained here.

If you're using a tool like pep8 in a commit hook mock it as well and ensure it is called.

Functional Testing

Fake the process object and invoke the commit hook with the correct arguments.

See how we did it with nose2. It's not complicated. 

Integration Testing

In order to verify that your hooks work exactly as expected you need to invoke git directly.

You're integration tests should git init when you initialize the suite and remove the .git folder when the suite is done running.

I prefer using what instead of the standard library to run sub processes because it has some assertions built into it.

That's it you're good to go. Your commits will be safer now that you write hooks that protect you from making silly mistakes.