From 960a50cda4a309c9bcdbb63cc859f34fefc4b081 Mon Sep 17 00:00:00 2001 From: Sebastian Feldmann Date: Fri, 25 Jul 2025 16:31:54 +0200 Subject: [PATCH 01/10] Define shorthands for actions and conditions --- src/Runner/Shorthand.php | 138 ++++++++++++++++++++++++++++ tests/unit/Runner/ShorthandTest.php | 88 ++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 src/Runner/Shorthand.php create mode 100644 tests/unit/Runner/ShorthandTest.php 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/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')); + } +} From 91f9d23973b0a1152b8fa57c42bb70d9909ebfd7 Mon Sep 17 00:00:00 2001 From: Sebastian Feldmann Date: Fri, 25 Jul 2025 16:32:20 +0200 Subject: [PATCH 02/10] Allow shorthand usage in configuration files --- src/Runner/Action/PHP.php | 20 ++++++++++++++++---- src/Runner/Condition.php | 13 ++++++++++++- src/Runner/Util.php | 13 ++++++++++++- tests/unit/Runner/Action/PHPTest.php | 18 ++++++++++++++++++ 4 files changed, 58 insertions(+), 6 deletions(-) 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/Util.php b/src/Runner/Util.php index 3b3b6e45..7a182303 100644 --- a/src/Runner/Util.php +++ b/src/Runner/Util.php @@ -45,7 +45,18 @@ 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 d53c8b85..edd82fa1 100644 --- a/tests/unit/Runner/Action/PHPTest.php +++ b/tests/unit/Runner/Action/PHPTest.php @@ -118,6 +118,24 @@ public function testExecuteError(): void $php->execute($config, $io, $repo, $action); } + 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); + } + public function testExecuteNoAction(): void { $this->expectException(Exception::class); From d8d995f205c8c914a2e361413e4a9c1897ec3016 Mon Sep 17 00:00:00 2001 From: Sebastian Feldmann Date: Fri, 25 Jul 2025 21:12:58 +0200 Subject: [PATCH 03/10] Shorthand example documentation Adding an example configuration to the class DocBlock and a 'short' DocBlock-Tag. --- .../Condition/Config/CustomValueIsFalsy.php | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) 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 { From 54422a7985490eb851c08374e45cb033ae9e9dab Mon Sep 17 00:00:00 2001 From: Sebastian Feldmann Date: Fri, 17 Oct 2025 21:58:01 +0200 Subject: [PATCH 04/10] Add branch condition shorthands --- src/Hook/Condition/Branch/NotOn.php | 15 ++++++++++++++- src/Hook/Condition/Branch/NotOnMatching.php | 15 ++++++++++++++- src/Hook/Condition/Branch/On.php | 15 ++++++++++++++- src/Hook/Condition/Branch/OnMatching.php | 15 ++++++++++++++- src/Runner/Shorthand.php | 5 ++++- 5 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/Hook/Condition/Branch/NotOn.php b/src/Hook/Condition/Branch/NotOn.php index d30d0762..978bc215 100644 --- a/src/Hook/Condition/Branch/NotOn.php +++ b/src/Hook/Condition/Branch/NotOn.php @@ -18,7 +18,20 @@ use SebastianFeldmann\Git\Repository; /** - * NotOn condition + * NotOn Branch condition + * + * Example configuration: + * + * { + * "action": "some-action", + * "conditions": [ + * { + * "exec": "CaptainHook.Status.NotOnBranch", + * "args": ["not-on-this-branch"] + * } + * ] + * } + * * * @package CaptainHook * @author Sebastian Feldmann diff --git a/src/Hook/Condition/Branch/NotOnMatching.php b/src/Hook/Condition/Branch/NotOnMatching.php index 59ec917c..dc3ac546 100644 --- a/src/Hook/Condition/Branch/NotOnMatching.php +++ b/src/Hook/Condition/Branch/NotOnMatching.php @@ -18,7 +18,20 @@ use SebastianFeldmann\Git\Repository; /** - * NotOnMatching condition + * NotOnMatching Branch condition + * + * Example configuration: + * + * { + * "action": "some-action", + * "conditions": [ + * { + * "exec": "CaptainHook.Status.NotOnMatchingBranch", + * "args": ["#^branches-names/not-matching[0-9]+-this-regex$#i"] + * } + * ] + * } + * * * @package CaptainHook * @author Sebastian Feldmann diff --git a/src/Hook/Condition/Branch/On.php b/src/Hook/Condition/Branch/On.php index cf35f8b6..919d3e26 100644 --- a/src/Hook/Condition/Branch/On.php +++ b/src/Hook/Condition/Branch/On.php @@ -18,7 +18,20 @@ use SebastianFeldmann\Git\Repository; /** - * On condition + * On Branch condition + * + * Example configuration: + * + * { + * "action": "some-action", + * "conditions": [ + * { + * "exec": "CaptainHook.Status.OnBranch", + * "args": ["only-on-this-branch"] + * } + * ] + * } + * * * @package CaptainHook * @author Sebastian Feldmann diff --git a/src/Hook/Condition/Branch/OnMatching.php b/src/Hook/Condition/Branch/OnMatching.php index 53264108..e14e958c 100644 --- a/src/Hook/Condition/Branch/OnMatching.php +++ b/src/Hook/Condition/Branch/OnMatching.php @@ -18,7 +18,20 @@ use SebastianFeldmann\Git\Repository; /** - * OnMatching condition + * OnMatching Branch condition + * + * Example configuration: + * + * { + * "action": "some-action", + * "conditions": [ + * { + * "exec": "CaptainHook.Status.OnMatchingBranch", + * "args": ["#^branches-names/matching[0-9]+-this-regex$#i"] + * } + * ] + * } + * * * @package CaptainHook * @author Sebastian Feldmann diff --git a/src/Runner/Shorthand.php b/src/Runner/Shorthand.php index a3baa8a0..bd183c85 100644 --- a/src/Runner/Shorthand.php +++ b/src/Runner/Shorthand.php @@ -75,7 +75,10 @@ class Shorthand 'thatis' => Hook\Condition\FileStaged\ThatIs::class, ], 'status' => [ - 'onbranch' => Hook\Condition\Branch\On::class, + 'onbranch' => Hook\Condition\Branch\On::class, + 'onmatchingbranch' => Hook\Condition\Branch\OnMatching::class, + 'notonbranch' => Hook\Condition\Branch\NotOn::class, + 'notonmatchingbranch' => Hook\Condition\Branch\NotOnMatching::class, ] ] ]; From 78871dd70d7d471769259448d2a651a6f409438d Mon Sep 17 00:00:00 2001 From: Sebastian Feldmann Date: Fri, 17 Oct 2025 22:19:05 +0200 Subject: [PATCH 05/10] Add example configuration to class PHPDoc --- .../Action/BlockFixupAndSquashCommits.php | 20 +++++++++---------- src/Hook/Branch/Action/EnsureNaming.php | 14 +++++++++++++ src/Hook/Debug/Failure.php | 12 ++++++++++- src/Hook/Debug/Success.php | 12 ++++++++++- 4 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/Hook/Branch/Action/BlockFixupAndSquashCommits.php b/src/Hook/Branch/Action/BlockFixupAndSquashCommits.php index c8f854fe..a09a9440 100644 --- a/src/Hook/Branch/Action/BlockFixupAndSquashCommits.php +++ b/src/Hook/Branch/Action/BlockFixupAndSquashCommits.php @@ -27,17 +27,17 @@ * This action blocks pushes that contain fixup! or squash! commits. * Just as a security layer, so you are not pushing stuff you wanted to autosquash. * - * Configure like this: - * + * Example configuration: + * * { - * "action": "\\CaptainHook\\App\\Hook\\Branch\\Action\\BlockFixupAndSquashCommits", - * "options": { - * "blockSquashCommits": true, - * "blockFixupCommits": true, - * "protectedBranches": ["main", "master", "integration"] - * }, - * "conditions": [] - * } + * "action": "CaptainHook.Branch.PreventPushOfFixupAndSquashCommits", + * "options": { + * "blockSquashCommits": true, + * "blockFixupCommits": true, + * "protectedBranches": ["main", "master", "integration"] + * } + * } + * * * @package CaptainHook * @author Sebastian Feldmann diff --git a/src/Hook/Branch/Action/EnsureNaming.php b/src/Hook/Branch/Action/EnsureNaming.php index 4a80da81..9665110f 100644 --- a/src/Hook/Branch/Action/EnsureNaming.php +++ b/src/Hook/Branch/Action/EnsureNaming.php @@ -22,6 +22,20 @@ /** * Class EnsureNaming * + * This Action makes sure you are on a branch that follows your naming conventions. + * + * Example configuration: + * + * { + * "action": "CaptainHook.Branch.EnsureNaming", + * "options": { + * "regex": "#feature/issue[0-9]+-.*#i", + * "error": "Arr matey! Ye be on the wrong branch!", + * "success": "All clear, Captain! Full speed ahead!", + * } + * } + * + * * @package CaptainHook * @author Felix Edelmann * @link https://github.com/captainhook-git/captainhook diff --git a/src/Hook/Debug/Failure.php b/src/Hook/Debug/Failure.php index e3dd9827..4dad62ca 100644 --- a/src/Hook/Debug/Failure.php +++ b/src/Hook/Debug/Failure.php @@ -18,7 +18,17 @@ use SebastianFeldmann\Git\Repository; /** - * Debug hook to test hook triggering that fails the hook execution + * Debug Failure + * + * Debug hook to test hook triggering, arguments, stdIn ect. + * Fails the hook execution and stops current the git operation. + * + * Example configuration: + * + * { + * "action": "CaptainHook.Debug.Fail" + * } + * * * @package CaptainHook * @author Sebastian Feldmann diff --git a/src/Hook/Debug/Success.php b/src/Hook/Debug/Success.php index 7a9d47b0..097f9b78 100644 --- a/src/Hook/Debug/Success.php +++ b/src/Hook/Debug/Success.php @@ -17,7 +17,17 @@ use SebastianFeldmann\Git\Repository; /** - * Debug hook to test hook triggering that allows the hook to pass + * Debug Success + * + * Debug hook action to test hook triggering, arguments, stdIn etc. + * Does not crash the hook execution. The git operation will succeed. + * + * Example configuration: + * + * { + * "action": "CaptainHook.Debug.Ok" + * } + * * * @package CaptainHook * @author Sebastian Feldmann From 1615043deab7287108e3469b31ce140dfbdea32b Mon Sep 17 00:00:00 2001 From: Sebastian Feldmann Date: Fri, 17 Oct 2025 22:54:13 +0200 Subject: [PATCH 06/10] Make the shorthand detection more restrictive In order to allow people to use scripts like `captainhook.sh` or `captainhook.php` the regex and the test examples got improved. --- src/Runner/Shorthand.php | 9 +++++---- tests/unit/Runner/ShorthandTest.php | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Runner/Shorthand.php b/src/Runner/Shorthand.php index bd183c85..a9a708cf 100644 --- a/src/Runner/Shorthand.php +++ b/src/Runner/Shorthand.php @@ -12,6 +12,7 @@ namespace CaptainHook\App\Runner; use CaptainHook\App\Hook; +use PHPUnit\Framework\MockObject\MockObject; use RuntimeException; /** @@ -84,14 +85,14 @@ class Shorthand ]; /** - * Check if a configured action value is actually shorthand for an internal action + * Check if a configured action or condition value is actually a shorthand for an internal action * - * @param string $action + * @param string $shorthand * @return bool */ - public static function isShorthand(string $action): bool + public static function isShorthand(string $shorthand): bool { - return (bool) preg_match('#^captainhook\.[a-z]+#i', $action); + return preg_match('#^captainhook\.[a-z]+\.[a-z]+$#i', $shorthand); } /** diff --git a/tests/unit/Runner/ShorthandTest.php b/tests/unit/Runner/ShorthandTest.php index 66464754..e2d26c88 100644 --- a/tests/unit/Runner/ShorthandTest.php +++ b/tests/unit/Runner/ShorthandTest.php @@ -25,11 +25,12 @@ public function testCanIdentifyShorthand() $this->assertFalse(Shorthand::isShorthand('foo')); $this->assertFalse(Shorthand::isShorthand('\\CaptainHook\\App')); $this->assertFalse(Shorthand::isShorthand('CaptainHook.')); + $this->assertFalse(Shorthand::isShorthand('captainhook.sh')); // positive - $this->assertTrue(Shorthand::isShorthand('CaptainHook.foo')); - $this->assertTrue(Shorthand::isShorthand('captainhook.bar')); - $this->assertTrue(Shorthand::isShorthand('CAPTAINHOOK.baz')); + $this->assertTrue(Shorthand::isShorthand('CaptainHook.foo.fiz')); + $this->assertTrue(Shorthand::isShorthand('captainhook.Bar.Baz')); + $this->assertTrue(Shorthand::isShorthand('CAPTAINHOOK.FOO.BAR')); } /** From 12973d9321658642f46402a1014bfcc4122322a7 Mon Sep 17 00:00:00 2001 From: Sebastian Feldmann Date: Mon, 17 Nov 2025 23:24:13 +0100 Subject: [PATCH 07/10] Switch url to https --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 6b67f08b..74de0301 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "type": "library", "description": "PHP git hook manager", "keywords": ["git", "hooks", "pre-commit", "pre-push", "commit-msg", "prepare-commit-msg", "post-merge"], - "homepage": "http://php.captainhook.info/", + "homepage": "https://php.captainhook.info/", "license": "MIT", "authors": [ { From 051ef2470f81e4f74c1c55fe5172ef28f3adcd86 Mon Sep 17 00:00:00 2001 From: Sebastian Feldmann Date: Mon, 17 Nov 2025 23:25:19 +0100 Subject: [PATCH 08/10] Add example configuration to class PHPDoc --- .../Condition/Config/CustomValueIsTruthy.php | 18 ++++++++----- src/Hook/Condition/FileChanged/All.php | 16 +++++++++++ src/Hook/Condition/FileChanged/Any.php | 27 ++++++++++--------- src/Hook/Condition/FileChanged/OfType.php | 18 ++++++++----- src/Hook/Condition/FileStaged/All.php | 20 +++++++++----- src/Hook/Condition/FileStaged/Any.php | 16 +++++++---- src/Hook/Condition/FileStaged/InDirectory.php | 18 ++++++++----- src/Hook/Condition/FileStaged/OfType.php | 26 +++++++++++------- src/Hook/Condition/FileStaged/ThatIs.php | 20 +++++++++----- src/Runner/Shorthand.php | 11 ++++---- 10 files changed, 123 insertions(+), 67 deletions(-) diff --git a/src/Hook/Condition/Config/CustomValueIsTruthy.php b/src/Hook/Condition/Config/CustomValueIsTruthy.php index 2815aa3e..280f10a7 100644 --- a/src/Hook/Condition/Config/CustomValueIsTruthy.php +++ b/src/Hook/Condition/Config/CustomValueIsTruthy.php @@ -21,13 +21,17 @@ * * Example configuration: * - * "action": "some-action" - * "conditions": [ - * {"exec": "\\CaptainHook\\App\\Hook\\Condition\\Config\\CustomValueIsTruthy", - * "args": [ - * "NAME_OF_CUSTOM_VALUE" - * ]} - * ] + * + * { + * "action": "some-action" + * "conditions": [ + * { + * "exec": "CaptainHook.Config.CustomValueIsTruthy", + * "args": ["NAME_OF_CUSTOM_VALUE"] + * } + * ] + * } + * * * @package CaptainHook * @author Sebastian Feldmann diff --git a/src/Hook/Condition/FileChanged/All.php b/src/Hook/Condition/FileChanged/All.php index e6dcc0d4..9f663b36 100644 --- a/src/Hook/Condition/FileChanged/All.php +++ b/src/Hook/Condition/FileChanged/All.php @@ -21,6 +21,22 @@ * The FileChange condition is applicable for `post-merge` and `post-checkout` hooks. * It checks if all configured files are updated within the last change set. * + * Example configuration: + * + * + * { + * "action": "some-action" + * "conditions": [ + * { + * "exec": "CaptainHook.FileChanged.All", + * "args": [ + * ["list", "of", "files"] + * ] + * } + * ] + * } + * + * * @package CaptainHook * @author Sebastian Feldmann * @link https://github.com/captainhook-git/captainhook diff --git a/src/Hook/Condition/FileChanged/Any.php b/src/Hook/Condition/FileChanged/Any.php index d40f6dd4..8db27500 100644 --- a/src/Hook/Condition/FileChanged/Any.php +++ b/src/Hook/Condition/FileChanged/Any.php @@ -19,21 +19,24 @@ * Class Any * * The FileChange condition is applicable for `post-merge` and `post-checkout` hooks. - * For example it can be used to trigger an automatic composer install if the composer.json - * or composer.lock file is changed during a checkout or merge. + * For example, it can be used to trigger an automatic composer install if the composer.json + * or `composer.lock` file is changed during a checkout or merge. * * Example configuration: * - * "action": "composer install" - * "conditions": [ - * {"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileChange\\Any", - * "args": [ - * [ - * "composer.json", - * "composer.lock" - * ] - * ]} - * ] + * + * { + * "action": "some-action" + * "conditions": [ + * { + * "exec": "CaptainHook.FileChanged.Any", + * "args": [ + * ["list", "of", "files"] + * ] + * } + * ] + * } + * * * @package CaptainHook * @author Sebastian Feldmann diff --git a/src/Hook/Condition/FileChanged/OfType.php b/src/Hook/Condition/FileChanged/OfType.php index b13b5a8a..7b03e395 100644 --- a/src/Hook/Condition/FileChanged/OfType.php +++ b/src/Hook/Condition/FileChanged/OfType.php @@ -25,13 +25,17 @@ * * Example configuration: * - * "action": "some-action" - * "conditions": [ - * {"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileChanged\\OfType", - * "args": [ - * "php" - * ]} - * ] + * + * { + * "action": "some-action" + * "conditions": [ + * { + * "exec": "CaptainHook.FileChanged.OfType", + * "args": ["php"] + * } + * ] + * } + * * * @package CaptainHook * @author Sebastian Feldmann diff --git a/src/Hook/Condition/FileStaged/All.php b/src/Hook/Condition/FileStaged/All.php index 2c7e61a8..efb7aac8 100644 --- a/src/Hook/Condition/FileStaged/All.php +++ b/src/Hook/Condition/FileStaged/All.php @@ -23,13 +23,19 @@ * * Example configuration: * - * "action": "some-action" - * "conditions": [ - * {"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\All", - * "args": [ - * ["file1", "file2", "file3"] - * ]} - * ] + * + * { + * "action": "some-action" + * "conditions": [ + * { + * "exec": "CaptainHook.FileStaged.All", + * "args": [ + * ["list", "of", "files"] + * ] + * } + * ] + * } + * * * The file list can also be defined as comma seperated string "file1,file2,file3" * diff --git a/src/Hook/Condition/FileStaged/Any.php b/src/Hook/Condition/FileStaged/Any.php index fe65b340..8e43be19 100644 --- a/src/Hook/Condition/FileStaged/Any.php +++ b/src/Hook/Condition/FileStaged/Any.php @@ -20,15 +20,21 @@ * * The FileStaged condition is applicable for `pre-commit hooks. * - * Example configuration: + * Example configuration: * + * + * { * "action": "some-action" * "conditions": [ - * {"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\Any", - * "args": [ - * ["file1", "file2", "file3"] - * ]} + * { + * "exec": "CaptainHook.FileStaged.Any", + * "args": [ + * ["list", "of", "files"] + * ] + * } * ] + * } + * * * The file list can also be defined as comma seperated string "file1,file2,file3" * diff --git a/src/Hook/Condition/FileStaged/InDirectory.php b/src/Hook/Condition/FileStaged/InDirectory.php index 7ddd0562..b69d2485 100644 --- a/src/Hook/Condition/FileStaged/InDirectory.php +++ b/src/Hook/Condition/FileStaged/InDirectory.php @@ -26,13 +26,17 @@ * * Example configuration: * - * "action": "some-action" - * "conditions": [ - * {"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\InDirectory", - * "args": [ - * "src/" - * ]} - * ] + * + * { + * "action": "some-action" + * "conditions": [ + * { + * "exec": "CaptainHook.FileStaged.InDirectory", + * "args": ["src"] + * } + * ] + * } + * * * @package CaptainHook * @author Sebastian Feldmann diff --git a/src/Hook/Condition/FileStaged/OfType.php b/src/Hook/Condition/FileStaged/OfType.php index e56bf5c4..eaf5e6b7 100644 --- a/src/Hook/Condition/FileStaged/OfType.php +++ b/src/Hook/Condition/FileStaged/OfType.php @@ -27,18 +27,24 @@ * * Example configuration: * - * "action": "some-action" - * "conditions": [ - * {"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\OfType", - * "args": [ - * "php", - * ["A", "C"] - * ]} - * ] + * + * { + * "action": "some-action" + * "conditions": [ + * { + * "exec": "CaptainHook.FileStaged.OfType", + * "args": [ + * "php", + * ["A", "C"] + * ] + * } + * ] + * } + * * * Multiple types can be configured using a comma separated string or an array - * "php,html,xml" - * ["php", "html", "xml"] + * - "php,html,xml" + * - ["php", "html", "xml"] * * @package CaptainHook * @author Sebastian Feldmann diff --git a/src/Hook/Condition/FileStaged/ThatIs.php b/src/Hook/Condition/FileStaged/ThatIs.php index 6e749a38..65bccec5 100644 --- a/src/Hook/Condition/FileStaged/ThatIs.php +++ b/src/Hook/Condition/FileStaged/ThatIs.php @@ -26,13 +26,19 @@ * * Example configuration: * - * "action": "some-action" - * "conditions": [ - * {"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\ThatIs", - * "args": [ - * {"ofType": "php", "inDirectory": "foo/", "diff-filter": ["A", "C"]} - * ]} - * ] + * + * { + * "action": "some-action" + * "conditions": [ + * { + * "exec": "CaptainHook.FileStaged.ThatIs", + * "args": [ + * {"ofType": "php", "inDirectory": "foo/", "diff-filter": ["A", "C"]} + * ] + * } + * ] + * } + * * * @package CaptainHook * @author Sebastian Feldmann diff --git a/src/Runner/Shorthand.php b/src/Runner/Shorthand.php index a9a708cf..bdf256ab 100644 --- a/src/Runner/Shorthand.php +++ b/src/Runner/Shorthand.php @@ -12,7 +12,6 @@ namespace CaptainHook\App\Runner; use CaptainHook\App\Hook; -use PHPUnit\Framework\MockObject\MockObject; use RuntimeException; /** @@ -62,7 +61,7 @@ class Shorthand ], ], 'condition' => [ - 'inconfig' => [ + 'config' => [ 'customvalueistruthy' => Hook\Condition\Config\CustomValueIsTruthy::class, 'customvalueisfalsy' => Hook\Condition\Config\CustomValueIsFalsy::class, ], @@ -71,9 +70,11 @@ class Shorthand '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, + 'all' => Hook\Condition\FileStaged\All::class, + 'any' => Hook\Condition\FileStaged\Any::class, + 'indirectory' => Hook\Condition\FileStaged\InDirectory::class, + 'oftype' => Hook\Condition\FileStaged\OfType::class, + 'thatis' => Hook\Condition\FileStaged\ThatIs::class, ], 'status' => [ 'onbranch' => Hook\Condition\Branch\On::class, From e244b095d54fd584acd7be92cd4403bea3eeaa6a Mon Sep 17 00:00:00 2001 From: Sebastian Feldmann Date: Mon, 17 Nov 2025 23:27:38 +0100 Subject: [PATCH 09/10] Fix return type --- src/Runner/Shorthand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Runner/Shorthand.php b/src/Runner/Shorthand.php index bdf256ab..a17ef5c4 100644 --- a/src/Runner/Shorthand.php +++ b/src/Runner/Shorthand.php @@ -93,7 +93,7 @@ class Shorthand */ public static function isShorthand(string $shorthand): bool { - return preg_match('#^captainhook\.[a-z]+\.[a-z]+$#i', $shorthand); + return (bool) preg_match('#^captainhook\.[a-z]+\.[a-z]+$#i', $shorthand); } /** From e36155630c0b85f8bfe9480d74cdb3cf950aaddf Mon Sep 17 00:00:00 2001 From: Sebastian Feldmann Date: Wed, 19 Nov 2025 00:36:07 +0100 Subject: [PATCH 10/10] Add demo config to PHPDoc --- src/Hook/Message/Action/InjectIssueKeyFromBranch.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Hook/Message/Action/InjectIssueKeyFromBranch.php b/src/Hook/Message/Action/InjectIssueKeyFromBranch.php index 3ff4f505..408dba8c 100644 --- a/src/Hook/Message/Action/InjectIssueKeyFromBranch.php +++ b/src/Hook/Message/Action/InjectIssueKeyFromBranch.php @@ -28,16 +28,20 @@ * Class PrepareFromFile * * Example configuration: + * + * * { - * "action": "\\CaptainHook\\App\\Hook\\Message\\Action\\InjectIssueKeyFromBranch", + * "action": "CaptainHook.Message.InjectIssueKeyFromBranch", * "options": { * "regex": "#([A-Z]+\\-[0-9]+)#i", - * "into": "body", - * "mode": "append", - * "prefix": "\nissue: ", + * "into": "body|subject", + * "mode": "append|prepend", + * "prefix": "", + * "suffix": "", * "force": true * } * } + * * * The regex option needs group $1 (...) to be the issue key *