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'));
+ }
+}