Extending

You can extend CaptainHook with your own PHP classes, this requires you to configure your PHP classes with their fully qualified class names and you have to be sure that composer can autoload your classes. There are two ways of hooking your own PHP code in. Let's start with the simpler, yet not so powerful one.

If you want to leverage other people's coding skills you can check the list of available extensions.

Custom Rules

CaptainHook offers a thing called RuleBook to validate commit messages. All of CaptainHooks message validation is done using a RuleBook. For example the regular expression validation is using a RuleBook with a single rule called MatchesRegularExpression. But let's just look at an example.

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

The Rules action accepts a list of Rule classes and creates a RuleBook with all the given rules. You can write your own rules by implementing the CaptainHook\App\Hook\Message\Rule interface. Here is an example.

<?php
namespace MyName\GitHook;

use CaptainHook\App\Hooks\Message\Rule;
use SebastianFeldmann\Git\CommitMessage;

class DoNotYell implements Rule
{
    /**
     * Make sure nobody yells in commit messages.
     *
     * @param  \SebastianFeldmann\Git\CommitMessage $message
     * @return bool
     */
    public function pass(CommitMessage $message) : bool
    {
        return $message->getContent() !== strtoupper($message->getContent());
    }
}

With rules you can only validate your commit message. If you want to hook into other events like pre-commit or pre-push or you need more flexibility, then custom Actions are your thing.

Custom Actions

Custom Actions allow you to execute your own logic on any git hook you want. Just like the built-in actions, you can define a list or map of options you want to pass to your class.

You can either use a static method, or a custom Action class implementing the Action interface.

Static Method Call

{
  "pre-commit": {
    "enabled": true,
    "actions": [
      {
        "action": "\\MyName\\GitHook\\MyAction::execute"
      }
    ]
  }
}

This is most likely the easiest way to execute custom php code via git hook. But if you need access to the original hook arguments or some git status information I would recommend implementing a custom Action.

Custom Action Class

{
  "commit-msg": {
    "enabled": true,
    "actions": [
      {
        "action": "\\MyName\\GitHook\\MyValidator",
        "options": {
          "key": "value",
          "use": "what you need"
        }
      }
    ]
  }
}

The whole process is best explained with a "real-world" example. So let's assume you want to check if all ticket numbers in a commit message do exist in your issue tracker. All you have to do, is to create a PHP class like this.

<?php
namespace MyName\GitHook;

use CaptainHook\App\Config;
use CaptainHook\App\Console\IO;
use CaptainHook\App\Hook\Action;
use SebastianFeldmann\Git\Repository;

class TicketIDValidator implements Action
{
    /**
     * Execute the action.
     *
     * @param  \CaptainHook\App\Config           $config
     * @param  \CaptainHook\App\Console\IO       $io
     * @param  \SebastianFeldmann\Git\Repository $repository
     * @param  \CaptainHook\App\Config\Action    $action
       @return void
     * @throws \Exception
     */
    public function execute(Config $config, IO $io, Repository $repository, Config\Action $action) : void
    {
        $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) : array
    {
        // put some logic here
    }

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

Conditions

The only thing you have to do to write your own Condition is that you have to implement the \CaptainHook\App\Hook\Condition interface. That means that you have to implement one single method called "isTrue"

Here is a minimal example.

<?php

namespace MyName\HookConditions;

use CaptainHook\App\Console\IO;
use CaptainHook\App\Hook\Condition;
use SebastianFeldmann\Git\Repository;

class NotOnMonday implements Condition
{
    /**
     * Make sure the action is not executed on mondays
     *
     * @param  \CaptainHook\App\Console\IO       $io
     * @param  \SebastianFeldmann\Git\Repository $repository
     * @return bool
     */
    public function isTrue(IO $io, Repository $repository) : bool
    {
        return date('D') !== 'Mon';
    }
}

To use this you can add this to any action configuration you want.

{
  "pre-commit": {
    "enabled": true,
    "actions": [
      {
        "action": "\\MyName\\GitHook\\MyAction::execute",
        "conditions": [
          {
            "exec": "\\MyName\\HookConditions\\NotOnMonday"
          }
        ]
      }
    ]
  }
}

If you want to make the days of the week configurable, all you have to do is to add an constructor and configure the arguments you want to pass to the constructor.

<?php

namespace MyName\HookConditions;

use CaptainHook\App\Console\IO;
use CaptainHook\App\Hook\Condition;
use SebastianFeldmann\Git\Repository;

class NotOnAnyWeekday implements Condition
{
    /**
     * List of days where to not execute the action e.g. ['SAT', 'SUN']
     *
     * @var array
     */
    private $weekdays;

    /**
     * NotOnAnyWeekday constructor
     *
     * @param array $weekdays List of weekdays to skip e.g. ['MON', 'SAT']
     */
    public function __construct(array $weekdays)
    {
        $this->weekdays = $weekdays;
    }

    /**
     * Make sure this action is not executed on and configured weekdays
     *
     * @param  \CaptainHook\App\Console\IO       $io
     * @param  \SebastianFeldmann\Git\Repository $repository
     * @return bool
     */
    public function isTrue(IO $io, Repository $repository) : bool
    {
        return !in_array(date('D'), $this->weekdays);
    }
}

To setup the weekdays you would configure it like this. If you want to use multiple constructor arguments, just add them to the args array.

{
  "pre-commit": {
    "enabled": true,
    "actions": [
      {
        "action": "\\MyName\\GitHook\\MyAction::execute",
        "conditions": [
          {
            "exec:"\\MyName\\HookConditions\\NotOnWeekday",
            "args": [
              ["Mon", "Sat"]
            ]
          }
        ]
      }
    ]
  }
}

Plugins

Actions and Conditions are suitable for most needs, but there may be times you need to add more functionality to CaptainHook, such as performing tasks before or after a hook runs. CaptainHook's plugin system provides this flexibility!

All CaptainHook plugins implement the CaptainHook\App\Plugin\CaptainHook interface. To use a plugin, add it to the plugins array in your config. If a plugin requires options, you may also provide those.

{
  "config": {
    "plugins": [
      {
        "plugin": "\\MyName\\GitHook\\MyPlugin",
        "options": {
          "myOption": true,
          "stuff": "cool things"
        }
      }
    ]
  }
}

Hook Plugins

Hook plugins are so-called because they execute during the hook run stage of CaptainHook, allowing you to do things before and after the hook runs, as well as before and after each action in the hook.

To create a hook plugin, implement the CaptainHook\App\Plugin\Hook interface. Optionally, you may extend the CaptainHook\App\Plugin\Hook\Base abstract class, which provides some basic functionality.

To see an example hook plugin, check out CaptainHook\App\Plugin\Hook\PreserveWorkingTree.

<?php

namespace MyName\GitHook;

use CaptainHook\App\Config;
use CaptainHook\App\Plugin;
use CaptainHook\App\Runner\Hook as RunnerHook;

class MyPlugin extends Plugin\Hook\Base implements Plugin\Hook
{
    /**
     * Runs before the hook.
     *
     * @param RunnerHook $hook This is the current hook that's running.
     */
    public function beforeHook(RunnerHook $hook): void
    {
        $stuff = $this->plugin->getOptions()->get('stuff');
        $this->io->write("Do {$stuff} before {$hook->getName()} runs");
    }

    /**
     * Runs before each action.
     *
     * @param RunnerHook $hook This is the current hook that's running.
     * @param Config\Action $action This is the configuration for action that will
     *                              run immediately following this method.
     */
    public function beforeAction(RunnerHook $hook, Config\Action $action): void
    {
        $this->io->write("Do stuff before action {$action->getAction()} runs");
    }

    /**
     * Runs after each action.
     *
     * @param RunnerHook $hook This is the current hook that's running.
     * @param Config\Action $action This is the configuration for action that just
     *                              ran immediately before this method.
     */
    public function afterAction(RunnerHook $hook, Config\Action $action): void
    {
        $this->io->write("Do stuff after action {$action->getAction()} runs");
    }

    /**
     * Runs after the hook.
     *
     * @param RunnerHook $hook This is the current hook that's running.
     */
    public function afterHook(RunnerHook $hook): void
    {
        $this->io->write("Do stuff after {$hook->getName()} runs");
    }
}

Hook Constrained

It is possible that your Actions, Conditions, or Plugins are only applicable for certain hooks. For example CaptainHook's commit message validation hooks are only applicable for the commit-message hook. If you want to constrain your Actions, Conditions, or Plugins to specific hooks, you can implement the Constrained interface and implement the required getRestrictions method.

In the following example the condition will only be executed during pre-commit hooks.

<?php

namespace MyName\HookConditions;

use CaptainHook\App\Console\IO;
use CaptainHook\App\Hook\Condition;
use CaptainHook\App\Hook\Constrained;
use CaptainHook\App\Hook\Restriction;
use SebastianFeldmann\Git\Repository;

class RandomFail implements Condition, Constrained
{
    /**
     * Return the hook restriction information
     *
     * @return \CaptainHook\App\Hook\Restriction
     */
    public static function getRestriction(): Restriction
    {
        return Restriction::fromArray([Hooks::PRE_COMMIT]);
    }

    /**
     * Fail 50% of the time randomly ;)
     *
     * @param  \CaptainHook\App\Console\IO       $io
     * @param  \SebastianFeldmann\Git\Repository $repository
     * @return bool
     */
    public function isTrue(IO $io, Repository $repository) : bool
    {
        return rand(0, 10)%2 === 0;
    }
}

Third-party Extensions

Third-party extensions are composer installable packages of curated CaptainHook Actions, Rules, Conditions, or Plugins. After installing an extension via composer you should be able to reference those in your CaptainHook configuration.

This is the list of known extensions in alphabetical order. If you want to add your extension just open a pull request on github or drop me a line via twitter. I will happily add it to the list.

Thanks to all the extension maintainers!