How Git Hooks Support Local Development Blog

Ahoy, CaptainHook

Sebastian Feldmann
Jan 30, 2023

No more commits with messages like "Fixed typo" or "Best commit ever". No more commits with syntax errors or failing unit tests. No more error messages because Composer packages were not updated after a pull. Sounds too good to be true? Not with the help of a pirate!

CaptainHook is a Git hook manager that handles the management of Git hooks and can be extended with its own functions (Fig. 1). To understand the benefits of using Captain, first, we need to know what Git Hooks are, how they work, and what their limitations are.

Fig 1: CaptainHook – the Git Hook Library for PHP Developers

Git Hooks

Git Hooks are user-defined statements that are automatically executed before or after a Git command. They work similar to WebHooks that can be used in applications to execute HTTP requests at specific times. Git hooks can be used to validate commit messages or check for syntax errors or style guide violations before committing. If you want to activate a git hook, you only need to put a script in the .git/hooks directory with the name of the hook. In the default configuration, this directory already contains some files with the extension .sample, which serve as hook templates.

IPC NEWSLETTER

All news about PHP and web development

 

Normally, Git hooks are shell scripts, but any executable can be used. Whether binary or script does not matter, only the name is important. The most important thing about Git Hooks is that they are local. They are not part of the repository, i.e. they cannot be committed and cannot be installed via a simple git clone for security reasons. Imagine the opposite: One could deposit a script in a repository that, without asking, would be launched when running a git command with local user privileges. Yeah, exactly, that sounds scary.

If a team wants to share the same Git hooks, the scripts can be stored in the repository. But each team member still needs to activate them on their own – either via symlinks or by copying them to the local .git/hooks directory. This is where the Captain comes in. CaptainHook allows you to do three things: first, configure all Git hooks in a JSON configuration file. Second, automatically install and activate the hooks in combination with Composer. And third, use hooks already included, or existing CaptainHook plug-ins. But first things first, let’s start with the installation.

Install CaptainHook

There are two ways to install CaptainHook. The first and recommended way is to use a PHAR file, which can either be downloaded manually or installed via PHIVE. The second way is to use Composer and deposit CaptainHook as DEV dependency. For this article, we use the second option because it is the easiest way for most developers to try the Captain. However, I recommend that all readers who don’t know PHIVE yet take a look at it. To install CaptainHook via Composer, the following command can be used:

composer require --dev captainhook/captainhook

After successful installation, there are still two steps missing. In step one, we create a *captainhook.json configuration file. The file can either be created manually by copying a sample file from the documentation, or you can use the configuration wizard.

vendor/bin/captainhook configure

The wizard asks some questions, which when in doubt, can be answered simply with no (n). Once you have answered the questions, CaptainHook creates a corresponding configuration file. But before we take a look at the configuration file, the last step is still missing: hook activation. As already described, the hooks still have to be stored under .git/hooks. To do this, we simply run the Captain installer.

vendor/bin/captainhook install

Again, some questions have to be answered with yes (y) or no (n). Once this is done, the captain is ready for action.

YOU LOVE PHP?

Explore the PHP Core Track

 

Not without the Captain

In a team, we rely on all team members to activate the Captain locally by calling the install command on themselves. Thanks to Composer, however, there is a way around this. Composer also allows you to run your own commands after certain actions. All you have to do is to add the corresponding command to composer.json. This way, you can automatically activate the hooks after each run of composer install. Then no team member has to worry about activating the hooks anymore.

"scripts": {
  "post-install-cmd": "vendor/bin/captainhook install --skip-existing"
}

Easy configuration

Now let’s take a closer look at the captainhook.json configuration file. Inside the file there is a section for all supported Git hooks. Currently, there are eight of them: prepare-commit-msg, commit-msg, pre-commit, post-commit, post-merge, post-checkout, post-rewrite, and pre-push. We’ll talk more about what the individual hooks can be used for later. Each section of a hook consists of an on-off switch enabled and a list of actions to be executed during a hook. Via enabled you can decide with true or false if the configured actions should be executed or not. The actions can be two different things: one is PHP classes that implement a CaptainHook interface; the other is any executable that communicates via its exit code whether it was successful or not (this is the only way the Captain can decide whether the Git command may be executed). In our configuration, PHPUnit is executed as an action within the pre-commit hook. PHPUnit communicates via its exit code whether the unit tests were successful or not. If not, the commit is aborted with an error message. The user must first ensure that all unit tests have been run successfully before committing. The other action in the example performs a PHP syntax check on all PHP files included in the commit. This ensures that no PHP files with syntax errors enter the repository (Listing 1).

{
  ...
    "pre-commit": {
      "enabled": true,
      "actions": [
        {"action": "\\CaptainHook\\App\\Hook\\PHP\\Action\\Linting"},
        {"action": "vendor/bin/phpunit"}
      ]
    },
  ...
}

All hands on deck

But what are Git hooks for? Hooks are tools that make the development process more comfortable and help detect problems early. Of course, they are not a substitute for CI builds, and you should never run tests that take longer than a few seconds. After all, you want to keep committing, changing branches, pushing changes, and not constantly waiting for the hooks to finally run through. Nevertheless, it makes sense to verify certain things already on the client side – especially things that can be checked quickly. Small automations like running composer install when the composer.lock file changes due to a pull or merge make your life a little bit easier. Table 1 lists the hooks currently supported by Captain and some use cases.

Hook Description
pre-commit Executed before the commit. It can be used to decide if the commit is allowed at all. A good hook for syntax or style guide checking.
prepare-commit-msg Can be used to preformulate a commit message, for example, to include ticket IDs, branch name, or co-authors in the commit message.
commit-message Here, the commit message can be validated. For example, it can check whether a ticket ID is included, whether the commit message is too short, or whether other rules agreed upon by the team are followed.
pre-push Is executed before the push is performed. Here it can be checked whether the test coverage has a certain value or the static code analysis still finds errors.
post-merge Can be used to call composer install when git pull has been called.
post-checkout Automatic migrations can be triggered here, for example, database changes.
post-rewrite Executed after a git rebase or git commit –amend. Use cases are for post-merge and post-checkout.

Table 1: Supported Git Hooks

IPC NEWSLETTER

All news about PHP and web development

 

Checking Commit Messages

One of the use cases for Git Hooks is reviewing commit messages. It’s never wrong to agree as a team on a few rules for creating commit messages. I recommend taking a look at Chris Beam’s blog article “How to Write a Git Commit Message”. In summary, he describes seven rules for good Git commit messages.

  1. Separate subject and description with a blank line.
  2. Limit the subject to 50 characters.
  3. Start the subject line with a capital letter.
  4. Do not end the subject line with a period.
  5. Use the imperative for the subject.
  6. Description lines should be no longer than 72 characters.
  7. Use the description to clarify the “what” and the “why,” not the “how.”

Let’s assume that the team has agreed to follow these rules. To check the commit messages for this, the code from Listing 2 must be inserted into captainhook.json. This will abort all commits with commit messages that do not follow the rules.

{
  "commit-msg": {
    "enabled": true,
    "actions": [
      {"action": "\\CaptainHook\\App\\Hook\\Message\\Action\\Beams"}
    ]
  }
}

The Beams action is basically just a simplified notation for the Rules action. If you don’t think all seven rules make sense, you can use the Rules action to decide for yourself which rules should be followed. Here you can use the rules contained in CaptainHook or define your own rules. Self-written rules are also the easiest way to extend the functionality of CaptainHook. In our example, we add a rule called DoNotYell to the list. To create this new commit message rule, we need to implement CaptainHook’s rule interface. In the example of the DoNotYell class, we see the methods to be implemented are getHint, which is used to communicate a hint to the user in case of an error about what exactly they did wrong, and pass, which is responsible for the actual verification (Listing 3).

{
  {
    "action": "\\CaptainHook\\App\\Hook\\Message\\Action\\Rules",
    "options": [
      "\\CaptainHook\\App\\Hook\\Message\\Rule\\MsgNotEmpty",
      "\\CaptainHook\\App\\Hook\\Message\\Rule\\CapitalizeSubject",
      "\\My\\Hooks\\DoNotYell"
    ],
  }
}
 
 
<?php declare(strict_types=1);
 
namespace My\Hooks;
 
use CaptainHook\App\Hooks\Message\Rule;
use SebastianFeldmann\Git\CommitMessage;
 
class DoNotYell implements Rule
{
  public function getHint(): string
  {
    return "DO NOT YELL AT ME!!!";
  }
 
  public function pass(CommitMessage $message): bool
  {
    return $message->getContent() !== mb_strtoupper($message->getContent());
  }
}
´´´

But please only if

Another useful feature from the Captain are Conditions. They ensure that an action is only executed if certain conditions are met. This is useful when working with placeholders that have no value in certain situations and would generate an invalid command, as in the example from Listing 4.

{
  "pre-commit": {
    "enabled": true,
    "actions": [
      {
        "action": „vendor/bin/phpcs --colors --standard=psr12 {$STAGED_FILES|of-type:php|separated-by: }",
        "conditions": [
          {
            "exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\OfType",
            "args": ["php"]
          }
        ]
      }
    ]
  }
}

Here we use PHP_Codesniffer to make sure there are no code style violations before committing. To avoid always having to check the entire codebase, we want to only check the files for violations that are in the Git index for the current commit. This is achieved by using the placeholder {$STAGED_FILES|of-type:php|separated-by: }, which ensures that all files in the Git index with the .php file extension are included in this command separated by spaces. This way we only check the files that we intend to commit at that moment. However, if we execute a commit that does not include PHP files, the command will be invalid and PHP_Codesniffer will throw an error. To prevent this, we have to make sure that the action is only executed if PHP files are actually included in the commit. This is exactly what the FileStaged\OfType condition used here does. Conditions can also be used to check if files have changed after a merge, rebase, or checkout, such as the composer.lock file, and then – and only then – automatically trigger composer install.

 

My name is CaptainHook and I am a mighty pirate

Captain has many more features, like linking configuration files in a configuration file. This makes it possible to implement things like composer hook packages that contain a captainhook.lib.json, which then only needs to be linked in its own captainhook.json. In addition to custom commit message rules, custom actions or conditions can also be created. This requires nothing more than the implementation of a manageable interface. If actions are not written in PHP, the return of an exit code is sufficient. Captain also works without locally installed PHP and it is possible to move the execution of CaptainHook into a Docker container. Examples of this and a detailed description of all features can be found in the documentation.

Finally, I would like to thank everyone who has contributed to CaptainHook over the years – whether by contributing code, lending an ear, or providing feedback. Thanks a lot! And to all readers: Commit like a pirate! ARRRR!

Top Articles About Blog

Stay tuned!

Register for our newsletter

Behind the Tracks of IPC

PHP Core
Best practices & applications

General Web Development
Broader web development topics

Test & Performance
Software testing and performance improvements

Agile & People
Getting agile right is so important

Software Architecture
All about PHP frameworks, concepts &
environments

DevOps & Deployment
Learn about DevOps and transform your development pipeline

Content Management Systems
Sessions on content management systems

#slideless (pure coding)
See how technology really works

Web Security
All about
web security