diff --git a/src/Hook/Condition/Config/CustomValueIsFalsy.php b/src/Hook/Condition/Config/CustomValueIsFalsy.php index 83e26616..01df6313 100644 --- a/src/Hook/Condition/Config/CustomValueIsFalsy.php +++ b/src/Hook/Condition/Config/CustomValueIsFalsy.php @@ -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" - * ]} - * ] + * + * { + * "action": "some-action" + * "conditions": [ + * { + * "exec": "CaptainHook.Config.CustomValueIsFalsy", + * "args": ["NAME_OF_CUSTOM_VALUE"] + * } + * ] + * } + * * * @package CaptainHook * @author Sebastian Feldmann * @link https://github.com/captainhook-git/captainhook * @since Class available since Release 5.17.2 + * @short CaptainHook.Config.CustomValueIsFalsy */ class CustomValueIsFalsy extends Condition\Config { diff --git a/src/Runner/Action/PHP.php b/src/Runner/Action/PHP.php index 138e0011..ab54b11b 100644 --- a/src/Runner/Action/PHP.php +++ b/src/Runner/Action/PHP.php @@ -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; @@ -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. @@ -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 @@ -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)) { @@ -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; + } } diff --git a/src/Runner/Condition.php b/src/Runner/Condition.php index 3363db89..a43edc47 100644 --- a/src/Runner/Condition.php +++ b/src/Runner/Condition.php @@ -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); } @@ -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; + } } diff --git a/src/Runner/Shorthand.php b/src/Runner/Shorthand.php new file mode 100644 index 00000000..a3baa8a0 --- /dev/null +++ b/src/Runner/Shorthand.php @@ -0,0 +1,138 @@ + + * + * 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 + * @link https://github.com/captainhook-git/captainhook + * @since Class available since Release 5.26.0 + */ +class Shorthand +{ + /** + * Shorthand to action mapping + * + * @var array>> + */ + 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]; + } +} diff --git a/src/Runner/Util.php b/src/Runner/Util.php index 7dbf138f..3e7d69ff 100644 --- a/src/Runner/Util.php +++ b/src/Runner/Util.php @@ -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); } } diff --git a/tests/unit/Runner/Action/PHPTest.php b/tests/unit/Runner/Action/PHPTest.php index da46e112..e9aeb125 100644 --- a/tests/unit/Runner/Action/PHPTest.php +++ b/tests/unit/Runner/Action/PHPTest.php @@ -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 * diff --git a/tests/unit/Runner/ShorthandTest.php b/tests/unit/Runner/ShorthandTest.php new file mode 100644 index 00000000..66464754 --- /dev/null +++ b/tests/unit/Runner/ShorthandTest.php @@ -0,0 +1,88 @@ + + * + * 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')); + } +}