Ahoy! Captain Hook – git hooks ARRR!

A while ago I had a look at our git commit messages and found myself in some kind of a whatthecommit mess. I remember clearly that we agreed on doing commit messages the git, or as I call it, the Beams way, don’t worry, I’ll explain what that means a bit later. But agreeing obviously wasn’t enough, to actually follow the rules.

I’m a big fan of automation, so I wanted to validate our future commit messages with some git hook magic. I started looking for a tool or a library that would help me with this allegedly simple looking task. I came up with a short list of hard requirements.

  • Run on Linux, OSX and Windows (for us that meant: PHP, Java, or bash)
  • Easy to configure
  • Easy to extend with custom logic

This ruled out nearly everything I found immediately and the ones I had a closer look at mostly failed in requirement number three and yes, you got me I wasn’t really looking because I just wanted to actually code something again, so I decided to write something myself.

Captain my Captain

Let me introduce you to  CaptainHook, the result of last weeks coding nights and a PHP package to manage your git hooks. It focuses mainly on the three requirements on my list. As a result, CaptainHook uses a simple json file for its configuration and integrating your own code is super easy. Here is how you can give it a try.

Installation

The easiest way to install CaptainHook is to use Composer.

$ composer require --dev sebastianfeldmann/captainhook:~1.0

After installing the package you have to create a captainhook.json configuration file. You can use the captainhook console application to create one.

$ vendor/bin/captainhook configure

The command guides you step by step through the configuration process and asks if you would like to enable a certain hook if you decide to enable the hook you can configure so-called Actions, that get executed if the git hook is triggered.
Actions can be a cli command or a PHP class that implements the sebastianfeldmann\CaptainHook\Hook\Action interface. You can also skip adding Actions and edit the configuration in an editor of your choice later.

Alter running the configuration you have to install the hooks to your local git repository. On certain git events git is triggering scripts located in .git/hooks/{event-name}. So to create those scripts and to finally activate the hooks, once again, use the captainhook application and run the following command to create the corresponding scripts.

$ vendor/bin/captainhook install

This will install the hooks of your choice, so if git is triggering any hooks, CaptainHook gets executed.
You can see a demo installation and how to test a hook execution in this short video.

Configuration

If you watched the video above you may have seen, that CaptainHook comes with some built-in Actions to validate commit messages out of the box. The two main ones are:

  • \sebastianfeldmann\CaptainHook\Hook\Message\Action\Regex
  • \sebastianfeldmann\CaptainHook\Hook\Message\Action\Beams

You can use them to validate you commit message. And they do exactly what you’d expect. Regex is validating your commit message by matching it against a regular expression and Beams is enforcing six rules, Chris Beams talks about in his blog post “How to Write a Git Commit Message”.

So let’s see how to use those. Open the captainhook.json configuration file in a text editor, and set up the commit-msg actions like this.

  "commit-msg": {
    "enabled": true,
    "actions": [
      {
        "action": "\\sebastianfeldmann\\CaptainHook\\Hook\\Message\\Action\\Beams",
        "options": {
          "subjectLength": 70,
          "bodyLineLength": 80
        }
      },
      {
        "action": "\\sebastianfeldmann\\CaptainHook\\Hook\\Message\\Action\\Regex",
        "options": {
          "regex": "#.*#"
        }
      }
    ]
  }

Just use the respective PHP class as action. Additionally, I added some options, for the allowed subject length and the max line length for the message body, because I felt that Beams recommendation, of 50 characters for the subject, is a bit harsh. You can add multiple Actions that get executed sequentially. In this example, I added a basic, but rather useless, regular expression check.

Besides PHP classes you can execute every cli command, that uses exit codes. Have a look at the following example.

  "pre-commit": {
    "enabled": true,
    "actions": [
      {
        "action": "phpunit"
      },
      {
        "action": "phpcs --standard=psr2 src"
      }
    ]
  }

Here I configured CaptainHook to run two pre-commit Actions. So before actually committing anything PHPUnit and PHPCodeSniffer get executed and if one of them fails, the commit will fail as well. Be aware, if you are using it, as shown in the example above, the phpcs and phpunit binaries have to be in your path and a PHPUnit configuration has to be available. If you install PHPUnit via Composer you should use the following Action configuration to execute it.

"action": "bin/vendor/phpunit"

Run your own git hooks

Using your own hooks is super easy. Just create a cli command or a PHP class that implements the CaptainHook Action interface. Let’s assume you want to verify Ticket-IDs mentioned in your commit messages. Therefore we create a class TicketIDValidator as follows.

<?php
namespace MyName\GitHooks;

use sebastianfeldmann\CaptainHook\Config;
use sebastianfeldmann\CaptainHook\Console\IO;
use sebastianfeldmann\CaptainHook\Git\Repository;
use sebastianfeldmann\CaptainHook\Hook\Action;

class TicketIDValidator implements Action
{
    /**
     * Execute the action.
     *
     * @param  \sebastianfeldmann\CaptainHook\Config         $config
     * @param  \sebastianfeldmann\CaptainHook\Console\IO     $io
     * @param  \sebastianfeldmann\CaptainHook\Git\Repository $repository
     * @param  \sebastianfeldmann\CaptainHook\Config\Action  $action
     * @throws \Exception
     */
    public function execute(Config $config, IO $io, Repository $repository, Config\Action $action)
    {
        $message = $repository->getCommitMsg();
        foreach ($this->findTicketIdsIn($message->getContent()) as $id) {
            if (!$this->isValidTicketId($id)) {
                throw new \Exception('invalid ticket ID: ' . $id);
            }
        }
    }

    /**
     * @param  string $text
     * @return array
     */
    private function findTicketIdsIn($text)
    {
        // put some logic here
    }

    /**
     * @param  string $id
     * @return bool
     */
    private function isValidTicketId($id)
    {
        // put some logic here
    }
}

To actually use our TicketIDValidator, all you have to do is, configure an Action like this.

  "commit-msg": {
    "enabled": true,
    "actions": [
      {
        "action": "\\MyName\\GitHooks\\TicketIDValidator",
        "options": []
      }
    ]
  }

From now on you can be sure, that all ticket IDs mentioned in a commit message exist.

All on Board!

So now I want to make sure everybody in our team is using CaptainHook and executes the configured Actions. But the “problem” with git hooks is, they are activated locally. Luckily Composer offers a handy script feature. All you have to do is, add a captainhook.json configuration to your project repository and add the following line to your composer.json file.

  "post-install-cmd": "\\sebastianfeldmann\\CaptainHook\\Composer\\Cmd::install"

This will execute the CaptainHook install command every time someone calls composer install. Off course you can still bypass the hooks if you execute git with –no-verify, but at least eveybody should have installed the hooks and has to activly bypass them if he doesn’t want to execute them. That’s good enough for me 😉

For a more detailed look into CaptainHook you can check out the source code or the documentation on github.

I hope you find this useful and I could encourage you to use git hooks to enforce some style of commit message or to ensure your code quality as soon as possible. If you have any questions or any ideas how to improve CaptainHook, feel free to use the comments below.

Leave a Reply

Your email address will not be published. Required fields are marked *