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, jet not so powerful one.

If you want to leverage other peoples coding skills you can check the list of available plugins.

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"]
            ]
          }
        ]
      }
    ]
  }
}

Hook Constrained

It is possible that your Actions or Conditions are only applicable for certain hooks. For example CaptainHooks commit message validation hooks are only applicable for the commit-message hook. If you want to constrain your Actions or Conditions 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;
    }
}

Plugins

Plugins are a composer installable packages of curated CaptainHook Actions, Rules or Conditions. After installing a plugin via composer you should be able to reference those in your CaptainHook configuration.

This is the list of know plugins in alphabetical order. If you want to add your plugin 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 plugin maintainers!