Abbildung logo git

How to GitHooks ?

Hey there, if you are reading this you probably don’t know what githooks are.

TL;DR: git hooks are a way to execute scripts and programs in your git workflow and perhaps to react to their result.


Why I’m writing this

I’m writing this because we started to build a generic git hooks solution for our projects which is also extensible. It can be found on GitHub.
The problem is that we are constantly changing our setups and so are our needs for different git hooks.
So we would have to constantly adjust this generic git hooks script which would take way more time than to just write separate git hooks for every project.
As you will see in this article it doesn’t take much to do so.


Introduction

To get an overview of the avaiable githooks you can go into a project directory where you are using git or initialize a new one (git init).
To see some of the available githooks you could just do ls -l .git/hooks/ in your directory. A complete list can be found in the official git documentation.

You will notice that all of them have the extension .sample, if you remove said extension the hook will be ‚activated‘.
When you open one of the existing files you will see that they are just simple shell scripts but you can use any other interpreter or put a compiled program there of course.

The arguments and return values are described in the sample file, the pre-commit hook could abort a commit if its return value is non zero for example .
There are also server-side hooks which are able to let the server reject a push if something is wrong or do something after the push has succeeded,
maybe like deploying the application, sending notifications or something similar.


Use cases

So, what are good use cases?
The pre-commit can be used to lint your source files or to run tests before every commit and is the most important hook in my opinion.
How would such a script look like? Imagine you are using yarn and composer to lint your source files then it would be a simple script like this:

.git/pre-commit

Bash
!/usr/bin/env bash

composer lint && yarn lint

In case of linting errors the linter will return a non-zero exit value and so will the script, which means the commit will be aborted like stated in the documentation:


Exiting with a non-zero status from this script causes the git commit command to abort before creating a commit.
Official git documentation on pre-commit hooks

Testing in front of every commit could be added as simple as the linting command:

.git/pre-commit

Bash
#!/usr/bin/env bash

composer lint && composer test && yarn lint && yarn test

Or Imagine a pre-commit hook which will generate and add a npm-shrinkwrap.json automatically every time your package.json has changed:

.git/pre-commit

Bash
#!/usr/bin/env bash

# If package.json is modified this command will not return an empty result
CHANGED_PACKAGE_JSON=$(git diff --cached --name-only -- '*package.json')

# -z tests if the variable is empty
if [ ! -z "$CHANGED_PACKAGE_JSON" ]; then
    echo "Seems like you've changed a 'pacakge.json' file, creating/updating a 'npm-shrinkwrap.json' for consistent dependencies..."

    # --dev is set to include devDependencies.
    npm shrinkwrap --dev
    generateResults=$?
        
    # Add the generated file to the commit index.
    git add npm-shrinkwrap.json

    #
    # Exit with an error code if the npm shrinkwrap command aborted.
    #
    if [ $generateResults -ne 0 ]; then
        echo "Seems like something went wrong while creating the npm-shrinkwrap file.

        exit 1
    fi
fi

Of course you could include the linting step there also.

Another really nice use case is the post-checkout hook. This one can be used to update dependencies when checking out another branch after updating the files:


This hook is invoked when a git checkout is run after having updated the worktree.
Official git documentation on post-checkout hooks

A simple script can look like this:

.git/post-checkout

Bash
#!/usr/bin/env bash

composer install && yarn install && yarn build

You could manipulate your commit message with information from your current branch name within the prepare-commit-msg hook, validate the given commit message against certain guidelines within the commit-msg hook or anything else which makes sense for your project and workflow.
The only thing you should not forget is to make your git hook executable(chmod +x <path/to/your/git/>/hook).


Efficient Usage

A minor problem with git hooks is that you can’t put your .git/hooks folder under version control. So what we are doing is we create a Build/GitHooksfolder in our project where our git hooks live. In order to make sure everyone in your project is using them you have to make sure that everyone has copied or linked the scripts from the Build/GitHooks folder to .git/hooks.

One solution would be to write a setup script called setup.sh for example:

setup.sh

Bash
#!/usr/bin/env bash

# copying
composer install && yarn install && yarn build && cp -f Build/GitHooks/* .git/hooks/

#linking
composer install && yarn install && yarn build && cd .git/hooks/ && ln -sf ../../Build/GitHooks/* .

But you have to make sure that everyone is executing this script when setting up the project. This can be critical especially if the git hooks are added later in the project.

The way of linking is recommended because if you change the contents of your git hooks it will be automatically applied, if you copy them you have to manually (or automatically) copy them again.

But you can also – and this is my current recommended way – execute commands after an composer install or yarn install for example, depending on your project.
I’m sure the package manager and programming language of your choice have this ability too.

For composer it could be done this way:

composer.json

JSON
{
    "name": "sitegeist/sample-projcet",
    "authors": [
        {
            "name": "Max Strübing",
            "email": "struebing@sitegeist.de"
        }
    ],
    "require": {}
    "scripts": {
        "post-install-cmd": [
            "some other post-install-cmd",
            "./Build/Scripts/setup.sh"
        ]
    }
}

And for node this way:

package.json:

JSON
{
  "name": "sample-project",
  "version": "1.0.0",
  "main": "index.js",
  "author": "Max Strübing <struebing@sitegeist.de>",
  "license": "MIT"
  "scripts": {
      "postinstall": "./Build/Scripts/setup.sh"
  }
}

The setup script should contain a script like mentioned above.

That’s all for now, good bye and good luck! ;)

Copyright Notice:

Git Logo by Jason Long is licensed under the Creative Commons Attribution 3.0 Unported License.

This license lets others distribute, remix, tweak, and build upon your work, even commercially, as long as they credit you for the original creation. This is the most accommodating of the CC licenses offered. Recommended for maximum dissemination and use of licensed materials.