Skip to content

Add action and condition shorthands to simplify the configuration #277

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions src/Hook/Condition/Config/CustomValueIsFalsy.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,31 @@
use SebastianFeldmann\Git\Repository;

/**
* Class CustomValueIsFalsy
* Condition CustomValueIsFalsy
*
* With this condition, you can check if a given custom value is falsy.
* The Action only is executed if the custom value is falsy.
* Values considered falsy are, 0, null, empty string, empty array and false.
*
* Example configuration:
*
* "action": "some-action"
* "conditions": [
* {"exec": "\\CaptainHook\\App\\Hook\\Condition\\Config\\CustomValueIsFalsy",
* "args": [
* "NAME_OF_CUSTOM_VALUE"
* ]}
* ]
* <code>
* {
* "action": "some-action"
* "conditions": [
* {
* "exec": "CaptainHook.Config.CustomValueIsFalsy",
* "args": ["NAME_OF_CUSTOM_VALUE"]
* }
* ]
* }
* </code>
*
* @package CaptainHook
* @author Sebastian Feldmann <[email protected]>
* @link https://github.com/captainhook-git/captainhook
* @since Class available since Release 5.17.2
* @short CaptainHook.Config.CustomValueIsFalsy
*/
class CustomValueIsFalsy extends Condition\Config
{
Expand Down
20 changes: 16 additions & 4 deletions src/Runner/Action/PHP.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use CaptainHook\App\Hook\Constrained;
use CaptainHook\App\Hook\EventSubscriber;
use CaptainHook\App\Runner\Action as ActionRunner;
use CaptainHook\App\Runner\Shorthand;
use Error;
use Exception;
use RuntimeException;
Expand All @@ -40,13 +41,13 @@ class PHP implements ActionRunner
*
* @var string
*/
private $hook;
private string $hook;

/**
*
* @var \CaptainHook\App\Event\Dispatcher
*/
private $dispatcher;
private Dispatcher $dispatcher;

/**
* PHP constructor.
Expand All @@ -71,7 +72,7 @@ public function __construct(string $hook, Dispatcher $dispatcher)
*/
public function execute(Config $config, IO $io, Repository $repository, Config\Action $action): void
{
$class = $action->getAction();
$class = $this->getActionClass($action->getAction());

try {
// if the configured action is a static php method display the captured output and exit
Expand All @@ -80,7 +81,7 @@ public function execute(Config $config, IO $io, Repository $repository, Config\A
return;
}

// if not static it has to be an 'Action' so let's instantiate
// if not static, it has to be an 'Action' so let's instantiate
$exe = $this->createAction($class);
// check for any given restrictions
if (!$this->isApplicable($exe)) {
Expand Down Expand Up @@ -170,4 +171,15 @@ private function isApplicable(Action $action)
}
return true;
}

/**
* Make sure action shorthands are translated before instantiating
*
* @param string $action
* @return string
*/
private function getActionClass(string $action): string
{
return Shorthand::isShorthand($action) ? Shorthand::getActionClass($action) : $action;
}
}
13 changes: 12 additions & 1 deletion src/Runner/Condition.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ private function createCondition(Config\Condition $config): ConditionInterface
}

/** @var class-string<\CaptainHook\App\Hook\Condition> $class */
$class = $config->getExec();
$class = $this->getConditionClass($config->getExec());
if (!class_exists($class)) {
throw new RuntimeException('could not find condition class: ' . $class);
}
Expand Down Expand Up @@ -165,4 +165,15 @@ private function isLogicCondition(Config\Condition $config): bool
{
return in_array(strtolower($config->getExec()), ['and', 'or']);
}

/**
* Returns the condition class
*
* @param string $exec
* @return string
*/
private function getConditionClass(string $exec): string
{
return Shorthand::isShorthand($exec) ? Shorthand::getConditionClass($exec) : $exec;
}
}
138 changes: 138 additions & 0 deletions src/Runner/Shorthand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<?php

/**
* This file is part of CaptainHook.
*
* (c) Sebastian Feldmann <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace CaptainHook\App\Runner;

use CaptainHook\App\Hook;
use RuntimeException;

/**
* Class Shorthand
*
* Defines some shorthands that can be used in the configuration file to not
* clutter the configuration with the full classnames.
*
* @package CaptainHook
* @author Sebastian Feldmann <[email protected]>
* @link https://github.com/captainhook-git/captainhook
* @since Class available since Release 5.26.0
*/
class Shorthand
{
/**
* Shorthand to action mapping
*
* @var array<string, array<string, array<string, string>>>
*/
private static array $map = [
'action' => [
'branch' => [
'ensurenaming' => Hook\Branch\Action\EnsureNaming::class,
'preventpushoffixupandsquashcommits' => Hook\Branch\Action\BlockFixupAndSquashCommits::class,
],
'debug' => [
'fail' => Hook\Debug\Failure::class,
'ok' => Hook\Debug\Success::class,
],
'file' => [
'blocksecrets' => Hook\Diff\Action\BlockSecrets::class,
'doesnotcontainregex' => Hook\File\Action\DoesNotContainRegex::class,
'isnotempty' => Hook\File\Action\IsNotEmpty::class,
'maxsize' => Hook\File\Action\MaxSize::class,
],
'message' => [
'injectissuekeyfrombranch' => Hook\Message\Action\InjectIssueKeyFromBranch::class,
'cacheonfail ' => Hook\Message\Action\CacheOnFail::class,
'mustfollowbeamsrules' => Hook\Message\Action\Beams::class,
'mustcontainsregex' => Hook\Message\Action\Regex::class,
'preparefromfile' => Hook\Message\Action\PrepareFromFile::class,
'prepare' => Hook\Message\Action\Prepare::class,
],
'notify' => [
'gitnotify' => Hook\Notify\Action\Notify::class,
],
],
'condition' => [
'inconfig' => [
'customvalueistruthy' => Hook\Condition\Config\CustomValueIsTruthy::class,
'customvalueisfalsy' => Hook\Condition\Config\CustomValueIsFalsy::class,
],
'filechanged' => [
'any' => Hook\Condition\FileChanged\Any::class,
'all' => Hook\Condition\FileChanged\All::class,
],
'filestaged' => [
'all' => Hook\Condition\FileStaged\All::class,
'any' => Hook\Condition\FileStaged\Any::class,
'thatis' => Hook\Condition\FileStaged\ThatIs::class,
],
'status' => [
'onbranch' => Hook\Condition\Branch\On::class,
]
]
];

/**
* Check if a configured action value is actually shorthand for an internal action
*
* @param string $action
* @return bool
*/
public static function isShorthand(string $action): bool
{
return (bool) preg_match('#^captainhook\.[a-z]+#i', $action);
}

/**
* Return the matching action class for given action shorthand
*
* @param string $shorthand
* @return string
*/
public static function getActionClass(string $shorthand): string
{
return Shorthand::getClass('action', $shorthand);
}

/**
* Return the matching condition class for given condition shorthand
*
* @param string $shorthand
* @return string
*/
public static function getConditionClass(string $shorthand): string
{
return Shorthand::getClass('condition', $shorthand);
}

/**
* Returns the matching class for shorthand
*
* @param string $type
* @param string $shorthand
* @return string
*/
private static function getClass(string $type, string $shorthand): string
{
$path = explode('.', strtolower($shorthand));
if (count($path) !== 3) {
throw new RuntimeException('Invalid ' . $type . ' shorthand: ' . $shorthand);
}
[$trigger, $group, $name] = $path;
if (!isset(self::$map[$type][$group])) {
throw new RuntimeException('Invalid ' . $type . ' group: ' . $group);
}
if (!isset(self::$map[$type][$group][$name])) {
throw new RuntimeException('Invalid ' . $type . ' => ' . $name);
}
return self::$map[$type][$group][$name];
}
}
13 changes: 12 additions & 1 deletion src/Runner/Util.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ public static function isTypeValid(string $type): bool
*/
public static function getExecType(string $action): string
{
return substr($action, 0, 1) === '\\' ? 'php' : 'cli';
return self::isPHPType($action) ? 'php' : 'cli';
}

/**
* Check if the action type is PHP
*
* @param string $action
* @return bool
*/
private static function isPHPType(string $action): bool
{
return str_starts_with($action, '\\') || Shorthand::isShorthand($action);
}
}
21 changes: 21 additions & 0 deletions tests/unit/Runner/Action/PHPTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,27 @@ public function testExecuteError(): void
$php->execute($config, $io, $repo, $action);
}

/**
* Check if the action shorthand works
*/
public function testExecuteByShorthand(): void
{
$this->expectException(Exception::class);
$this->expectExceptionMessageMatches('/debugging/i');

$config = $this->createConfigMock();
$io = $this->createIOMock();
$repo = $this->createRepositoryMock();
$action = $this->createActionConfigMock();
$events = new Dispatcher($io, $config, $repo);
$class = 'CaptainHook.Debug.fail';

$action->expects($this->once())->method('getAction')->willReturn($class);

$php = new PHP('pre-commit', $events);
$php->execute($config, $io, $repo, $action);
}

/**
* Tests PHP::execute
*
Expand Down
88 changes: 88 additions & 0 deletions tests/unit/Runner/ShorthandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

/**
* This file is part of CaptainHook.
*
* (c) Sebastian Feldmann <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace CaptainHook\App\Runner;

use Exception;
use PHPUnit\Framework\TestCase;

class ShorthandTest extends TestCase
{
/**
* Checks if shorthands are identified correctly
*/
public function testCanIdentifyShorthand()
{
// negative
$this->assertFalse(Shorthand::isShorthand('foo'));
$this->assertFalse(Shorthand::isShorthand('\\CaptainHook\\App'));
$this->assertFalse(Shorthand::isShorthand('CaptainHook.'));

// positive
$this->assertTrue(Shorthand::isShorthand('CaptainHook.foo'));
$this->assertTrue(Shorthand::isShorthand('captainhook.bar'));
$this->assertTrue(Shorthand::isShorthand('CAPTAINHOOK.baz'));
}

/**
* Check if invalid shorthand detection works
*/
public function testDetectsInvalidActionShortHand(): void
{
$this->expectException(Exception::class);
Shorthand::getActionClass('Captainhook.foo.bar.baz');
}

/**
* Check if an invalid shorthand group is detected
*/
public function testDetectsInvalidActionShorthandGroup(): void
{
$this->expectException(Exception::class);
Shorthand::getActionClass('Captainhook.foo.bar');
}

/**
* Check if an invalid action shorthand name is detected
*/
public function testDetectsInvalidActionShorthandName(): void
{
$this->expectException(Exception::class);
Shorthand::getActionClass('Captainhook.File.bar');
}

/**
* Check if an invalid condition shorthand name is detected
*/
public function testDetectsInvalidConditionShorthandName(): void
{
$this->expectException(Exception::class);
Shorthand::getConditionClass('Captainhook.FileStaged.bar');
}

/**
* Check if a valid action shorthand is mapped correctly
*/
public function testFindsActionClassByShorthand(): void
{
$class = Shorthand::getActionClass('Captainhook.Branch.EnsureNaming');
$this->assertTrue(str_contains($class, 'CaptainHook\App\Hook\Branch\Action\EnsureNaming'));
}

/**
* Check if a valid condition shorthand is mapped correctly
*/
public function testFindsConditionClassByShorthand(): void
{
$class = Shorthand::getConditionClass('Captainhook.Status.OnBranch');
$this->assertTrue(str_contains($class, 'CaptainHook\App\Hook\Condition\Branch\On'));
}
}