diff --git a/.gitignore b/.gitignore
index df991db82c..3424594092 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,6 +58,10 @@ tests/*/vendor/*
/tests/php-unit-tests/phpunit.xml
/tests/php-unit-tests/postbuild_integration.xml
+# CaptainHook: For Git hooks management
+# - Never version local config file
+/captainhook.config.json
+
# Jetbrains
/.idea/**
diff --git a/.make/git-hooks/README.md b/.make/git-hooks/README.md
index 3e14d1deda..fc310be965 100644
--- a/.make/git-hooks/README.md
+++ b/.make/git-hooks/README.md
@@ -1,13 +1,16 @@
# Git hooks for iTop
+> [!WARNING]
+> This read me and the `install.php` / `pre-commit.php` files are outdated. If we were to keep using what it is supposed to do, we should migrate it to the proper CaptainHook Git hooks manager.\
+
## ❓ Goal
-Those [git hooks](https://git-scm.com/docs/githooks) aims to ease developing on [iTop](https://github.com/Combodo/iTop).
+~~Those [git hooks](https://git-scm.com/docs/githooks) aims to ease developing on [iTop](https://github.com/Combodo/iTop).~~
## ☑ Available hooks
-* pre-commit : rejects commit if you have at least one SCSS file staged, and no CSS file
+* ~~pre-commit : rejects commit if you have at least one SCSS file staged, and no CSS file~~
## ⚙ Install
-Just run install.php !
+~~Just run install.php !~~
diff --git a/.make/git-hooks/pre-commit-php-code-style-fixer.php b/.make/git-hooks/pre-commit-php-code-style-fixer.php
new file mode 100644
index 0000000000..a5aef94658
--- /dev/null
+++ b/.make/git-hooks/pre-commit-php-code-style-fixer.php
@@ -0,0 +1,87 @@
+ $aChunk) {
+ $iChunkNumber = $iIdx + 1;
+
+ $sChunkFilesAsArgs = implode(' ', array_map('escapeshellarg', $aChunk));
+ $sPhpCsFixerCmd = escapeshellcmd(PHP_BINARY)
+ .' '.escapeshellarg($sPhpCsFixerBinaryAbsPath)
+ .' fix --using-cache=no --config='.escapeshellarg($sPhpCsFixerConfigFileAbsPath)
+ .' --verbose '.$sChunkFilesAsArgs;
+
+ echo "Executing chunk {$iChunkNumber}/{$iNbChunks} : {$sPhpCsFixerCmd}\n\n";
+ passthru($sPhpCsFixerCmd, $iExitCode);
+ if ($iExitCode !== 0) {
+ echo "Failed to fix chunk #{$iChunkNumber} Aborting.\n";
+ exit($iExitCode);
+ }
+}
+
+// Find which files have been fixed and re-stage them
+$sFixedFilesCmd = 'git diff --name-only --diff-filter=M';
+exec($sFixedFilesCmd, $aFixedFiles);
+$aFixedFilesToRestage = array_intersect($aFixedFiles, $aStagedFiles);
+
+// Re-stage fixed files to include them in the commit
+if (count($aFixedFilesToRestage) === 0) {
+ echo "No file needed PHP code style fixing, it was already ok.\n";
+ exit(0);
+}
+
+echo "Re-staging fixed files:\n";
+foreach ($aFixedFilesToRestage as $sFixedFileToRestage) {
+ $sGitAddCmd = 'git add '.escapeshellarg($sFixedFileToRestage);
+
+ echo " - {$sFixedFileToRestage}\n";
+ passthru($sGitAddCmd, $iRetCode);
+ if ($iRetCode !== 0) {
+ echo " Failed to re-stage fixed file '{$sFixedFileToRestage}'. Continuing anyway.\n";
+ }
+}
+
+echo "All done, file(s) PHP code style fixed and added to commit.\n";
+exit($iExitCode);
diff --git a/captainhook.config.json.dist b/captainhook.config.json.dist
new file mode 100644
index 0000000000..8b85646714
--- /dev/null
+++ b/captainhook.config.json.dist
@@ -0,0 +1,9 @@
+// * Duplicate file into `captainhook.config.json`
+// * Remove all comments (`// ...`)
+// * Keep only 1 `php-path` line and adjust it to your environment
+{
+ // Example of Windows path to PHP binary
+ "php-path": "C:/wamp64/bin/php/php8.2.26/php.exe"
+ // Example of Unix path tp PHP binary
+ "php-path": "/usr/bin/php"
+}
\ No newline at end of file
diff --git a/captainhook.json b/captainhook.json
new file mode 100644
index 0000000000..250257fc46
--- /dev/null
+++ b/captainhook.json
@@ -0,0 +1,21 @@
+{
+ "config" : {
+ "bootstrap" : "lib/autoload.php"
+ },
+ "commit-msg" : {
+ "enabled" : true,
+ "actions" : []
+ },
+ "pre-commit" : {
+ "enabled" : true,
+ "actions" : [
+ {
+ "action" : "{$CONFIG|value-of:php-path} .make/git-hooks/pre-commit-php-code-style-fixer.php",
+ "config" : {
+ "label" : "PHP code style fixer",
+ "allow-failure": false
+ }
+ }
+ ]
+ }
+}
diff --git a/composer.json b/composer.json
index ec1d512445..0ccb5f363f 100644
--- a/composer.json
+++ b/composer.json
@@ -36,6 +36,7 @@
"soundasleep/html2text": "~2.1"
},
"require-dev": {
+ "captainhook/captainhook": "^5.25",
"symfony/debug-bundle": "~6.4.0",
"symfony/stopwatch": "~6.4.0",
"symfony/web-profiler-bundle": "~6.4.0"
@@ -98,8 +99,9 @@
}
},
"scripts": {
- "post-install-cmd": ["@rmUnnecessaryFolders", "@tcpdfCustomFonts"],
- "post-update-cmd": ["@rmUnnecessaryFolders", "@tcpdfCustomFonts"],
+ "post-install-cmd": ["@installGitHooks","@rmUnnecessaryFolders", "@tcpdfCustomFonts"],
+ "post-update-cmd": ["@installGitHooks","@rmUnnecessaryFolders", "@tcpdfCustomFonts"],
+ "installGitHooks": "@php lib/bin/captainhook install --force",
"rmUnnecessaryFolders": "@php .make/dependencies/rmUnnecessaryFolders.php --manager composer",
"tcpdfCustomFonts": "@php .make/dependencies/composer/tcpdf/tcpdfUpdateFonts.php"
}
diff --git a/composer.lock b/composer.lock
index d2d5c710ba..1e2dc2881c 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "0da5da3b165955f386268e6dd8db2a8d",
+ "content-hash": "9de096942ebd728704bc74b51221a5df",
"packages": [
{
"name": "apereo/phpcas",
@@ -4880,6 +4880,322 @@
}
],
"packages-dev": [
+ {
+ "name": "captainhook/captainhook",
+ "version": "5.25.11",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/captainhook-git/captainhook.git",
+ "reference": "f2278edde4b45af353861aae413fc3840515bb80"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/captainhook-git/captainhook/zipball/f2278edde4b45af353861aae413fc3840515bb80",
+ "reference": "f2278edde4b45af353861aae413fc3840515bb80",
+ "shasum": ""
+ },
+ "require": {
+ "captainhook/secrets": "^0.9.4",
+ "ext-json": "*",
+ "ext-spl": "*",
+ "ext-xml": "*",
+ "php": ">=8.0",
+ "sebastianfeldmann/camino": "^0.9.2",
+ "sebastianfeldmann/cli": "^3.3",
+ "sebastianfeldmann/git": "^3.14",
+ "symfony/console": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0",
+ "symfony/filesystem": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0",
+ "symfony/process": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0"
+ },
+ "replace": {
+ "sebastianfeldmann/captainhook": "*"
+ },
+ "require-dev": {
+ "composer/composer": "~1 || ^2.0",
+ "mikey179/vfsstream": "~1"
+ },
+ "bin": [
+ "bin/captainhook"
+ ],
+ "type": "library",
+ "extra": {
+ "captainhook": {
+ "config": "captainhook.json"
+ },
+ "branch-alias": {
+ "dev-main": "6.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "CaptainHook\\App\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Feldmann",
+ "email": "sf@sebastian-feldmann.info"
+ }
+ ],
+ "description": "PHP git hook manager",
+ "homepage": "http://php.captainhook.info/",
+ "keywords": [
+ "commit-msg",
+ "git",
+ "hooks",
+ "post-merge",
+ "pre-commit",
+ "pre-push",
+ "prepare-commit-msg"
+ ],
+ "support": {
+ "issues": "https://github.com/captainhook-git/captainhook/issues",
+ "source": "https://github.com/captainhook-git/captainhook/tree/5.25.11"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sponsors/sebastianfeldmann",
+ "type": "github"
+ }
+ ],
+ "time": "2025-08-12T12:14:57+00:00"
+ },
+ {
+ "name": "captainhook/secrets",
+ "version": "0.9.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/captainhook-git/secrets.git",
+ "reference": "d62c97f75f81ac98e22f1c282482bd35fa82f631"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/captainhook-git/secrets/zipball/d62c97f75f81ac98e22f1c282482bd35fa82f631",
+ "reference": "d62c97f75f81ac98e22f1c282482bd35fa82f631",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": ">=8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "CaptainHook\\Secrets\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Feldmann",
+ "email": "sf@sebastian-feldmann.info"
+ }
+ ],
+ "description": "Utility classes to detect secrets",
+ "keywords": [
+ "commit-msg",
+ "keys",
+ "passwords",
+ "post-merge",
+ "prepare-commit-msg",
+ "secrets",
+ "tokens"
+ ],
+ "support": {
+ "issues": "https://github.com/captainhook-git/secrets/issues",
+ "source": "https://github.com/captainhook-git/secrets/tree/0.9.7"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sponsors/sebastianfeldmann",
+ "type": "github"
+ }
+ ],
+ "time": "2025-04-08T07:10:48+00:00"
+ },
+ {
+ "name": "sebastianfeldmann/camino",
+ "version": "0.9.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianfeldmann/camino.git",
+ "reference": "bf2e4c8b2a029e9eade43666132b61331e3e8184"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianfeldmann/camino/zipball/bf2e4c8b2a029e9eade43666132b61331e3e8184",
+ "reference": "bf2e4c8b2a029e9eade43666132b61331e3e8184",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "SebastianFeldmann\\Camino\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Feldmann",
+ "email": "sf@sebastian-feldmann.info"
+ }
+ ],
+ "description": "Path management the OO way",
+ "homepage": "https://github.com/sebastianfeldmann/camino",
+ "keywords": [
+ "file system",
+ "path"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianfeldmann/camino/issues",
+ "source": "https://github.com/sebastianfeldmann/camino/tree/0.9.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianfeldmann",
+ "type": "github"
+ }
+ ],
+ "time": "2022-01-03T13:15:10+00:00"
+ },
+ {
+ "name": "sebastianfeldmann/cli",
+ "version": "3.4.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianfeldmann/cli.git",
+ "reference": "6fa122afd528dae7d7ec988a604aa6c600f5d9b5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianfeldmann/cli/zipball/6fa122afd528dae7d7ec988a604aa6c600f5d9b5",
+ "reference": "6fa122afd528dae7d7ec988a604aa6c600f5d9b5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "require-dev": {
+ "symfony/process": "^4.3 | ^5.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "SebastianFeldmann\\Cli\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Feldmann",
+ "email": "sf@sebastian-feldmann.info"
+ }
+ ],
+ "description": "PHP cli helper classes",
+ "homepage": "https://github.com/sebastianfeldmann/cli",
+ "keywords": [
+ "cli"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianfeldmann/cli/issues",
+ "source": "https://github.com/sebastianfeldmann/cli/tree/3.4.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianfeldmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-11-26T10:19:01+00:00"
+ },
+ {
+ "name": "sebastianfeldmann/git",
+ "version": "3.15.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianfeldmann/git.git",
+ "reference": "90cb5a32f54dbb0d7dcd87d02e664ec2b50c0c96"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianfeldmann/git/zipball/90cb5a32f54dbb0d7dcd87d02e664ec2b50c0c96",
+ "reference": "90cb5a32f54dbb0d7dcd87d02e664ec2b50c0c96",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-simplexml": "*",
+ "php": ">=8.0",
+ "sebastianfeldmann/cli": "^3.0"
+ },
+ "require-dev": {
+ "mikey179/vfsstream": "^1.6"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "SebastianFeldmann\\Git\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Feldmann",
+ "email": "sf@sebastian-feldmann.info"
+ }
+ ],
+ "description": "PHP git wrapper",
+ "homepage": "https://github.com/sebastianfeldmann/git",
+ "keywords": [
+ "git"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianfeldmann/git/issues",
+ "source": "https://github.com/sebastianfeldmann/git/tree/3.15.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianfeldmann",
+ "type": "github"
+ }
+ ],
+ "time": "2025-09-05T08:07:09+00:00"
+ },
{
"name": "symfony/debug-bundle",
"version": "v6.4.0",
@@ -4954,6 +5270,71 @@
],
"time": "2023-11-01T12:07:38+00:00"
},
+ {
+ "name": "symfony/process",
+ "version": "v6.4.26",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/process.git",
+ "reference": "48bad913268c8cafabbf7034b39c8bb24fbc5ab8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/process/zipball/48bad913268c8cafabbf7034b39c8bb24fbc5ab8",
+ "reference": "48bad913268c8cafabbf7034b39c8bb24fbc5ab8",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Process\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Executes commands in sub-processes",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/process/tree/v6.4.26"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-09-11T09:57:09+00:00"
+ },
{
"name": "symfony/stopwatch",
"version": "v6.4.0",
diff --git a/lib/autoload.php b/lib/autoload.php
index 9ee03077e4..db2890200f 100644
--- a/lib/autoload.php
+++ b/lib/autoload.php
@@ -3,20 +3,23 @@
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
- if (!headers_sent()) {
- header('HTTP/1.1 500 Internal Server Error');
- }
- $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
- if (!ini_get('display_errors')) {
- if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
- fwrite(STDERR, $err);
- } elseif (!headers_sent()) {
- echo $err;
- }
- }
- throw new RuntimeException($err);
+ if (!headers_sent()) {
+ header('HTTP/1.1 500 Internal Server Error');
+ }
+ $err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
+ if (!ini_get('display_errors')) {
+ if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
+ fwrite(STDERR, $err);
+ } elseif (!headers_sent()) {
+ echo $err;
+ }
+ }
+ trigger_error(
+ $err,
+ E_USER_ERROR
+ );
}
-require_once __DIR__ . '/composer/autoload_real.php';
+require_once __DIR__.'/composer/autoload_real.php';
return ComposerAutoloaderInit7f81b4a2a468a061c306af5e447a9a9f::getLoader();
diff --git a/lib/bin/captainhook b/lib/bin/captainhook
new file mode 100644
index 0000000000..353a266dfd
--- /dev/null
+++ b/lib/bin/captainhook
@@ -0,0 +1,119 @@
+#!/usr/bin/env php
+realpath = realpath($opened_path) ?: $opened_path;
+ $opened_path = $this->realpath;
+ $this->handle = fopen($this->realpath, $mode);
+ $this->position = 0;
+
+ return (bool) $this->handle;
+ }
+
+ public function stream_read($count)
+ {
+ $data = fread($this->handle, $count);
+
+ if ($this->position === 0) {
+ $data = preg_replace('{^#!.*\r?\n}', '', $data);
+ }
+
+ $this->position += strlen($data);
+
+ return $data;
+ }
+
+ public function stream_cast($castAs)
+ {
+ return $this->handle;
+ }
+
+ public function stream_close()
+ {
+ fclose($this->handle);
+ }
+
+ public function stream_lock($operation)
+ {
+ return $operation ? flock($this->handle, $operation) : true;
+ }
+
+ public function stream_seek($offset, $whence)
+ {
+ if (0 === fseek($this->handle, $offset, $whence)) {
+ $this->position = ftell($this->handle);
+ return true;
+ }
+
+ return false;
+ }
+
+ public function stream_tell()
+ {
+ return $this->position;
+ }
+
+ public function stream_eof()
+ {
+ return feof($this->handle);
+ }
+
+ public function stream_stat()
+ {
+ return array();
+ }
+
+ public function stream_set_option($option, $arg1, $arg2)
+ {
+ return true;
+ }
+
+ public function url_stat($path, $flags)
+ {
+ $path = substr($path, 17);
+ if (file_exists($path)) {
+ return stat($path);
+ }
+
+ return false;
+ }
+ }
+ }
+
+ if (
+ (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
+ || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
+ ) {
+ return include("phpvfscomposer://" . __DIR__ . '/..'.'/captainhook/captainhook/bin/captainhook');
+ }
+}
+
+return include __DIR__ . '/..'.'/captainhook/captainhook/bin/captainhook';
diff --git a/lib/bin/captainhook.bat b/lib/bin/captainhook.bat
new file mode 100644
index 0000000000..d2b9b59998
--- /dev/null
+++ b/lib/bin/captainhook.bat
@@ -0,0 +1,5 @@
+@ECHO OFF
+setlocal DISABLEDELAYEDEXPANSION
+SET BIN_TARGET=%~dp0/captainhook
+SET COMPOSER_RUNTIME_BIN_DIR=%~dp0
+php "%BIN_TARGET%" %*
diff --git a/lib/captainhook/captainhook/LICENSE b/lib/captainhook/captainhook/LICENSE
new file mode 100644
index 0000000000..3c01e7bb05
--- /dev/null
+++ b/lib/captainhook/captainhook/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2016 Sebastian Feldmann
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/lib/captainhook/captainhook/bin/captainhook b/lib/captainhook/captainhook/bin/captainhook
new file mode 100644
index 0000000000..b8d6ce01c8
--- /dev/null
+++ b/lib/captainhook/captainhook/bin/captainhook
@@ -0,0 +1,88 @@
+#!/usr/bin/env php
+
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+use CaptainHook\App\Console\Application as CaptainHook;
+use Symfony\Component\Console\Input\ArgvInput;
+
+(static function($argv)
+{
+ define('__CAPTAINHOOK_RUNNING__', true);
+
+ // check installation type [composer bin dir, git clone / phar, composer package dir]
+ $composerAutoloadLocations = [
+ __DIR__ . '/../autoload.php',
+ __DIR__ . '/../vendor/autoload.php',
+ __DIR__ . '/../../../autoload.php'
+ ];
+
+ foreach ($composerAutoloadLocations as $file) {
+ if (file_exists($file)) {
+ define('CAPTAINHOOK_COMPOSER_AUTOLOAD', $file);
+ break;
+ }
+ }
+ unset($file);
+
+ if (!defined('CAPTAINHOOK_COMPOSER_AUTOLOAD')) {
+ fwrite(STDERR,
+ 'Autoloader could not be found:' . PHP_EOL .
+ ' Please run `composer install` to generate the autoloader' . PHP_EOL
+ );
+ exit(1);
+ }
+
+ $GLOBALS['__composer_autoload_files'] = [];
+
+ try {
+ require CAPTAINHOOK_COMPOSER_AUTOLOAD;
+ } catch (Throwable $exception) {
+ fwrite(STDERR,
+ 'Composer autoloader crashed:' . PHP_EOL .
+ ' Please update your autoloader by running `composer install`' . PHP_EOL .
+ ' You can re-run the hook by executing `' . implode(' ', $argv) . '`' . PHP_EOL
+ );
+ exit(1);
+ }
+
+ $captainHook = new CaptainHook($argv[0]);
+ $captainHook->run(new ArgvInput($argv));
+}
+)($argv);
+
+
diff --git a/lib/captainhook/captainhook/composer.json b/lib/captainhook/captainhook/composer.json
new file mode 100644
index 0000000000..6b67f08be0
--- /dev/null
+++ b/lib/captainhook/captainhook/composer.json
@@ -0,0 +1,77 @@
+{
+ "name": "captainhook/captainhook",
+ "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/",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Sebastian Feldmann",
+ "email": "sf@sebastian-feldmann.info"
+ }
+ ],
+ "support": {
+ "issues": "https://github.com/captainhook-git/captainhook/issues"
+ },
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sebastianfeldmann"
+ }
+ ],
+ "autoload": {
+ "psr-4": {
+ "CaptainHook\\App\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "CaptainHook\\App\\": "tests/unit/",
+ "CaptainHook\\App\\Integration\\": "tests/integration/"
+ }
+ },
+ "require": {
+ "php": ">=8.0",
+ "ext-json": "*",
+ "ext-spl": "*",
+ "ext-xml": "*",
+ "captainhook/secrets": "^0.9.4",
+ "sebastianfeldmann/camino": "^0.9.2",
+ "sebastianfeldmann/cli": "^3.3",
+ "sebastianfeldmann/git": "^3.14",
+ "symfony/console": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0",
+ "symfony/filesystem": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0",
+ "symfony/process": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0"
+ },
+ "require-dev": {
+ "composer/composer": "~1 || ^2.0",
+ "mikey179/vfsstream": "~1"
+ },
+ "bin": [
+ "bin/captainhook"
+ ],
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.0.x-dev"
+ },
+ "captainhook": {
+ "config": "captainhook.json"
+ }
+ },
+ "replace" : {
+ "sebastianfeldmann/captainhook": "*"
+ },
+ "config": {
+ "sort-packages": true
+ },
+ "scripts": {
+ "post-install-cmd": "tools/phive install --force-accept-unsigned",
+ "tools": "tools/phive install --force-accept-unsigned",
+ "compile": "tools/box compile",
+ "test": "tools/phpunit --testsuite UnitTests",
+ "test:integration": "tools/phpunit --testsuite IntegrationTests --no-coverage",
+ "static": "tools/phpstan analyse",
+ "style": "tools/phpcs --standard=psr12 src tests"
+ }
+}
diff --git a/lib/captainhook/captainhook/phive.xml b/lib/captainhook/captainhook/phive.xml
new file mode 100644
index 0000000000..0b19e449c1
--- /dev/null
+++ b/lib/captainhook/captainhook/phive.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/lib/captainhook/captainhook/phpcs.xml b/lib/captainhook/captainhook/phpcs.xml
new file mode 100644
index 0000000000..8499fa39ed
--- /dev/null
+++ b/lib/captainhook/captainhook/phpcs.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+ src
+ tests
+
diff --git a/lib/captainhook/captainhook/src/CH.php b/lib/captainhook/captainhook/src/CH.php
new file mode 100644
index 0000000000..126c40870b
--- /dev/null
+++ b/lib/captainhook/captainhook/src/CH.php
@@ -0,0 +1,45 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace CaptainHook\App;
+
+/**
+ * Class CH
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 0.9.0
+ */
+final class CH
+{
+ /**
+ * Current CaptainHook version
+ */
+ public const VERSION = '5.25.11';
+
+ /**
+ * Release date of the current version
+ */
+ public const RELEASE_DATE = '2025-08-12';
+
+ /**
+ * Default configuration file
+ */
+ public const CONFIG = 'captainhook.json';
+
+ /**
+ * Minimal required version for the installer
+ */
+ public const MIN_REQ_INSTALLER = '5.22.0';
+}
diff --git a/lib/captainhook/captainhook/src/Config.php b/lib/captainhook/captainhook/src/Config.php
new file mode 100644
index 0000000000..88843eaa62
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Config.php
@@ -0,0 +1,449 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App;
+
+use CaptainHook\App\Config\Run;
+use InvalidArgumentException;
+use SebastianFeldmann\Camino\Check;
+
+/**
+ * Class Config
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 0.9.0
+ * @internal
+ */
+class Config
+{
+ public const SETTING_ALLOW_FAILURE = 'allow-failure';
+ public const SETTING_BOOTSTRAP = 'bootstrap';
+ public const SETTING_COLORS = 'ansi-colors';
+ public const SETTING_CUSTOM = 'custom';
+ public const SETTING_GIT_DIR = 'git-directory';
+ public const SETTING_INCLUDES = 'includes';
+ public const SETTING_INCLUDES_LEVEL = 'includes-level';
+ public const SETTING_LABEL = 'label';
+ public const SETTING_RUN_EXEC = 'run-exec';
+ public const SETTING_RUN_MODE = 'run-mode';
+ public const SETTING_RUN_PATH = 'run-path';
+ public const SETTING_RUN_GIT = 'run-git';
+ public const SETTING_PHP_PATH = 'php-path';
+ public const SETTING_VERBOSITY = 'verbosity';
+ public const SETTING_FAIL_ON_FIRST_ERROR = 'fail-on-first-error';
+
+ /**
+ * Path to the config file
+ *
+ * @var string
+ */
+ private string $path;
+
+ /**
+ * Does the config file exist
+ *
+ * @var bool
+ */
+ private bool $fileExists;
+
+ /**
+ * CaptainHook settings
+ *
+ * @var array
+ */
+ private array $settings;
+
+ /**
+ * All options related to running CaptainHook
+ *
+ * @var \CaptainHook\App\Config\Run
+ */
+ private Run $runConfig;
+
+ /**
+ * List of users custom settings
+ *
+ * @var array
+ */
+ private array $custom = [];
+
+ /**
+ * List of plugins
+ *
+ * @var array
+ */
+ private array $plugins = [];
+
+ /**
+ * List of hook configs
+ *
+ * @var array
+ */
+ private array $hooks = [];
+
+ /**
+ * Config constructor
+ *
+ * @param string $path
+ * @param bool $fileExists
+ * @param array $settings
+ */
+ public function __construct(string $path, bool $fileExists = false, array $settings = [])
+ {
+ $settings = $this->setupPlugins($settings);
+ $settings = $this->setupCustom($settings);
+ $settings = $this->setupRunConfig($settings);
+
+
+ $this->path = $path;
+ $this->fileExists = $fileExists;
+ $this->settings = $settings;
+
+ foreach (Hooks::getValidHooks() as $hook => $value) {
+ $this->hooks[$hook] = new Config\Hook($hook);
+ }
+ }
+
+ /**
+ * Extract custom settings from Captain Hook ones
+ *
+ * @param array $settings
+ * @return array
+ */
+ private function setupCustom(array $settings): array
+ {
+ /* @var array $custom */
+ $this->custom = $settings['custom'] ?? [];
+ unset($settings['custom']);
+
+ return $settings;
+ }
+
+ /**
+ * Setup all configured plugins
+ *
+ * @param array $settings
+ * @return array
+ */
+ private function setupPlugins(array $settings): array
+ {
+ /* @var array> $pluginSettings */
+ $pluginSettings = $settings['plugins'] ?? [];
+ unset($settings['plugins']);
+
+ foreach ($pluginSettings as $plugin) {
+ $name = (string) $plugin['plugin'];
+ $options = isset($plugin['options']) && is_array($plugin['options'])
+ ? $plugin['options']
+ : [];
+ $this->plugins[$name] = new Config\Plugin($name, $options);
+ }
+ return $settings;
+ }
+
+ /**
+ * Extract all running related settings into a run configuration
+ *
+ * @param array $settings
+ * @return array
+ */
+ private function setupRunConfig(array $settings): array
+ {
+ // extract the legacy settings
+ $settingsToMove = [
+ self::SETTING_RUN_MODE,
+ self::SETTING_RUN_EXEC,
+ self::SETTING_RUN_PATH,
+ self::SETTING_RUN_GIT
+ ];
+ $config = [];
+ foreach ($settingsToMove as $setting) {
+ if (!empty($settings[$setting])) {
+ $config[substr($setting, 4)] = $settings[$setting];
+ }
+ unset($settings[$setting]);
+ }
+ // make sure the new run configuration supersedes the legacy settings
+ if (isset($settings['run']) && is_array($settings['run'])) {
+ $config = array_merge($config, $settings['run']);
+ unset($settings['run']);
+ }
+ $this->runConfig = new Run($config);
+ return $settings;
+ }
+
+ /**
+ * Is configuration loaded from file
+ *
+ * @return bool
+ */
+ public function isLoadedFromFile(): bool
+ {
+ return $this->fileExists;
+ }
+
+ /**
+ * Are actions allowed to fail without stopping the git operation
+ *
+ * @return bool
+ */
+ public function isFailureAllowed(): bool
+ {
+ return (bool) ($this->settings[self::SETTING_ALLOW_FAILURE] ?? false);
+ }
+
+ /**
+ * @param string $hook
+ * @param bool $withVirtual if true, also check if hook is enabled through any enabled virtual hook
+ * @return bool
+ */
+ public function isHookEnabled(string $hook, bool $withVirtual = true): bool
+ {
+ // either this hook is explicitly enabled
+ $hookConfig = $this->getHookConfig($hook);
+ if ($hookConfig->isEnabled()) {
+ return true;
+ }
+
+ // or any virtual hook that triggers it is enabled
+ if ($withVirtual && Hooks::triggersVirtualHook($hookConfig->getName())) {
+ $virtualHookConfig = $this->getHookConfig(Hooks::getVirtualHook($hookConfig->getName()));
+ if ($virtualHookConfig->isEnabled()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Path getter
+ *
+ * @return string
+ */
+ public function getPath(): string
+ {
+ return $this->path;
+ }
+
+ /**
+ * Return git directory path if configured, CWD/.git if not
+ *
+ * @return string
+ */
+ public function getGitDirectory(): string
+ {
+ if (empty($this->settings[self::SETTING_GIT_DIR])) {
+ return getcwd() . '/.git';
+ }
+
+ // if repo path is absolute use it otherwise create an absolute path relative to the configuration file
+ return Check::isAbsolutePath($this->settings[self::SETTING_GIT_DIR])
+ ? $this->settings[self::SETTING_GIT_DIR]
+ : dirname($this->path) . '/' . $this->settings[self::SETTING_GIT_DIR];
+ }
+
+ /**
+ * Return bootstrap file if configured, CWD/vendor/autoload.php by default
+ *
+ * @param string $default
+ * @return string
+ */
+ public function getBootstrap(string $default = 'vendor/autoload.php'): string
+ {
+ return !empty($this->settings[self::SETTING_BOOTSTRAP])
+ ? $this->settings[self::SETTING_BOOTSTRAP]
+ : $default;
+ }
+
+ /**
+ * Return the configured verbosity
+ *
+ * @return string
+ */
+ public function getVerbosity(): string
+ {
+ return !empty($this->settings[self::SETTING_VERBOSITY])
+ ? $this->settings[self::SETTING_VERBOSITY]
+ : 'normal';
+ }
+
+ /**
+ * Should the output use ansi colors
+ *
+ * @return bool
+ */
+ public function useAnsiColors(): bool
+ {
+ return (bool) ($this->settings[self::SETTING_COLORS] ?? true);
+ }
+
+ /**
+ * Get configured php-path
+ *
+ * @return string
+ */
+ public function getPhpPath(): string
+ {
+ return (string) ($this->settings[self::SETTING_PHP_PATH] ?? '');
+ }
+
+ /**
+ * Get run configuration
+ *
+ * @return \CaptainHook\App\Config\Run
+ */
+ public function getRunConfig(): Run
+ {
+ return $this->runConfig;
+ }
+
+ /**
+ * Returns the users custom config values
+ *
+ * @return array
+ */
+ public function getCustomSettings(): array
+ {
+ return $this->custom;
+ }
+
+ /**
+ * Whether to abort the hook as soon as a any action has errored. Default is true.
+ * Otherwise, all actions get executed (even if some of them have failed) and
+ * finally, a non-zero exit code is returned if any action has errored.
+ *
+ * @return bool
+ */
+ public function failOnFirstError(): bool
+ {
+ return (bool) ($this->settings[self::SETTING_FAIL_ON_FIRST_ERROR] ?? true);
+ }
+
+ /**
+ * Return config for given hook
+ *
+ * @param string $hook
+ * @return \CaptainHook\App\Config\Hook
+ * @throws \InvalidArgumentException
+ */
+ public function getHookConfig(string $hook): Config\Hook
+ {
+ if (!Hook\Util::isValid($hook)) {
+ throw new InvalidArgumentException('Invalid hook name: ' . $hook);
+ }
+ return $this->hooks[$hook];
+ }
+
+ /**
+ * Return hook configs
+ *
+ * @return array
+ */
+ public function getHookConfigs(): array
+ {
+ return $this->hooks;
+ }
+
+ /**
+ * Returns a hook config containing all the actions to execute
+ *
+ * Returns all actions from the triggered hook but also any actions of virtual hooks that might be triggered.
+ * E.g. 'post-rewrite' or 'post-checkout' trigger the virtual/artificial 'post-change' hook.
+ * Virtual hooks are special hooks to simplify configuration.
+ *
+ * @param string $hook
+ * @return \CaptainHook\App\Config\Hook
+ */
+ public function getHookConfigToExecute(string $hook): Config\Hook
+ {
+ $config = new Config\Hook($hook, true);
+ $hookConfig = $this->getHookConfig($hook);
+ $config->addAction(...$hookConfig->getActions());
+ if (Hooks::triggersVirtualHook($hookConfig->getName())) {
+ $vHookConfig = $this->getHookConfig(Hooks::getVirtualHook($hookConfig->getName()));
+ if ($vHookConfig->isEnabled()) {
+ $config->addAction(...$vHookConfig->getActions());
+ }
+ }
+ return $config;
+ }
+
+ /**
+ * Return plugins
+ *
+ * @return Config\Plugin[]
+ */
+ public function getPlugins(): array
+ {
+ return $this->plugins;
+ }
+
+ /**
+ * Return config array to write to disc
+ *
+ * @return array
+ */
+ public function getJsonData(): array
+ {
+ $data = [];
+ $config = $this->getConfigJsonData();
+
+ if (!empty($config)) {
+ $data['config'] = $config;
+ }
+
+ foreach (Hooks::getValidHooks() as $hook => $value) {
+ if ($this->hooks[$hook]->isEnabled() || $this->hooks[$hook]->hasActions()) {
+ $data[$hook] = $this->hooks[$hook]->getJsonData();
+ }
+ }
+ return $data;
+ }
+
+ /**
+ * Build the "config" JSON section of the configuration file
+ *
+ * @return array
+ */
+ private function getConfigJsonData(): array
+ {
+ $config = !empty($this->settings) ? $this->settings : [];
+
+ $runConfigData = $this->runConfig->getJsonData();
+ if (!empty($runConfigData)) {
+ $config['run'] = $runConfigData;
+ }
+ if (!empty($this->plugins)) {
+ $config['plugins'] = $this->getPluginsJsonData();
+ }
+ if (!empty($this->custom)) {
+ $config['custom'] = $this->custom;
+ }
+ return $config;
+ }
+
+ /**
+ * Collect and return plugin json data for all plugins
+ *
+ * @return array
+ */
+ private function getPluginsJsonData(): array
+ {
+ $plugins = [];
+ foreach ($this->plugins as $plugin) {
+ $plugins[] = $plugin->getJsonData();
+ }
+ return $plugins;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Config/Action.php b/lib/captainhook/captainhook/src/Config/Action.php
new file mode 100644
index 0000000000..f04e577ef8
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Config/Action.php
@@ -0,0 +1,236 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Config;
+
+use CaptainHook\App\Config;
+
+/**
+ * Class Action
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 0.9.0
+ */
+class Action
+{
+ /**
+ * Action php class, php static method, or cli script
+ *
+ * @var string
+ */
+ private string $action;
+
+ /**
+ * Map of options name => value
+ *
+ * @var \CaptainHook\App\Config\Options
+ */
+ private Options $options;
+
+ /**
+ * List of action conditions
+ *
+ * @var \CaptainHook\App\Config\Condition[]
+ */
+ private array $conditions = [];
+
+ /**
+ * Action settings
+ *
+ * @var array
+ */
+ private array $settings = [];
+
+ /**
+ * List of available settings
+ *
+ * @var string[]
+ */
+ private static array $availableSettings = [
+ Config::SETTING_ALLOW_FAILURE,
+ Config::SETTING_LABEL
+ ];
+
+ /**
+ * Indicates if an action config was included from another file
+ *
+ * @var bool
+ */
+ private bool $isIncluded = false;
+
+ /**
+ * Action constructor
+ *
+ * @param string $action
+ * @param array $options
+ * @param array $conditions
+ * @param array $settings
+ */
+ public function __construct(string $action, array $options = [], array $conditions = [], array $settings = [])
+ {
+ $this->action = $action;
+ $this->setupOptions($options);
+ $this->setupConditions($conditions);
+ $this->setupSettings($settings);
+ }
+
+ /**
+ * Setup options
+ *
+ * @param array $options
+ */
+ private function setupOptions(array $options): void
+ {
+ $this->options = new Options($options);
+ }
+
+ /**
+ * Setup action conditions
+ *
+ * @param array> $conditions
+ */
+ private function setupConditions(array $conditions): void
+ {
+ foreach ($conditions as $condition) {
+ $this->conditions[] = new Condition($condition['exec'], $condition['args'] ?? []);
+ }
+ }
+
+ /**
+ * Setting up the action settings
+ *
+ * @param array $settings
+ * @return void
+ */
+ private function setupSettings(array $settings): void
+ {
+ foreach (self::$availableSettings as $setting) {
+ if (isset($settings[$setting])) {
+ $this->settings[$setting] = $settings[$setting];
+ }
+ }
+ }
+
+ /**
+ * Marks a action config as included
+ *
+ * @return void
+ */
+ public function markIncluded(): void
+ {
+ $this->isIncluded = true;
+ }
+
+ /**
+ * Check if an action config was included
+ *
+ * @return bool
+ */
+ public function isIncluded(): bool
+ {
+ return $this->isIncluded;
+ }
+
+ /**
+ * Indicates if the action can fail without stopping the git operation
+ *
+ * @param bool $default
+ * @return bool
+ */
+ public function isFailureAllowed(bool $default = false): bool
+ {
+ return (bool) ($this->settings[Config::SETTING_ALLOW_FAILURE] ?? $default);
+ }
+
+ /**
+ * Return the label or the action if no label is set
+ *
+ * @return string
+ */
+ public function getLabel(): string
+ {
+ return (string) ($this->settings[Config::SETTING_LABEL] ?? $this->getAction());
+ }
+
+ /**
+ * Action getter
+ *
+ * @return string
+ */
+ public function getAction(): string
+ {
+ return $this->action;
+ }
+
+ /**
+ * Return option map
+ *
+ * @return \CaptainHook\App\Config\Options
+ */
+ public function getOptions(): Options
+ {
+ return $this->options;
+ }
+
+ /**
+ * Return condition configurations
+ *
+ * @return \CaptainHook\App\Config\Condition[]
+ */
+ public function getConditions(): array
+ {
+ return $this->conditions;
+ }
+
+ /**
+ * Return config data
+ *
+ * @return array
+ */
+ public function getJsonData(): array
+ {
+ $data = [
+ 'action' => $this->action
+ ];
+
+ $options = $this->options->getAll();
+ if (!empty($options)) {
+ $data['options'] = $options;
+ }
+
+ $conditions = $this->getConditionJsonData();
+ if (!empty($conditions)) {
+ $data['conditions'] = $conditions;
+ }
+
+ if (!empty($this->settings)) {
+ $data['config'] = $this->settings;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Return conditions json data
+ *
+ * @return array
+ */
+ private function getConditionJsonData(): array
+ {
+ $json = [];
+ foreach ($this->conditions as $condition) {
+ $json[] = $condition->getJsonData();
+ }
+ return $json;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Config/Condition.php b/lib/captainhook/captainhook/src/Config/Condition.php
new file mode 100644
index 0000000000..910817a811
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Config/Condition.php
@@ -0,0 +1,83 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Config;
+
+/**
+ * Class Action
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 4.2.0
+ * @internal
+ */
+class Condition
+{
+ /**
+ * Condition executable
+ *
+ * @var string
+ */
+ private string $exec;
+
+ /**
+ * Condition arguments
+ *
+ * @var array
+ */
+ private array $args;
+
+ /**
+ * Condition constructor
+ *
+ * @param string $exec
+ * @param array $args
+ */
+ public function __construct(string $exec, array $args = [])
+ {
+ $this->exec = $exec;
+ $this->args = $args;
+ }
+
+ /**
+ * Exec getter
+ *
+ * @return string
+ */
+ public function getExec(): string
+ {
+ return $this->exec;
+ }
+
+ /**
+ * Args getter
+ *
+ * @return array
+ */
+ public function getArgs(): array
+ {
+ return $this->args;
+ }
+
+ /**
+ * Return config data
+ *
+ * @return array
+ */
+ public function getJsonData(): array
+ {
+ return [
+ 'exec' => $this->exec,
+ 'args' => $this->args,
+ ];
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Config/Factory.php b/lib/captainhook/captainhook/src/Config/Factory.php
new file mode 100644
index 0000000000..a452829885
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Config/Factory.php
@@ -0,0 +1,347 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Config;
+
+use CaptainHook\App\CH;
+use CaptainHook\App\Config;
+use CaptainHook\App\Hook\Util as HookUtil;
+use CaptainHook\App\Storage\File\Json;
+use RuntimeException;
+
+/**
+ * Class Factory
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 0.9.0
+ * @internal
+ */
+final class Factory
+{
+ /**
+ * Maximal level in including config files
+ *
+ * @var int
+ */
+ private int $maxIncludeLevel = 1;
+
+ /**
+ * Current level of inclusion
+ *
+ * @var int
+ */
+ private int $includeLevel = 0;
+
+ /**
+ * Create a CaptainHook configuration
+ *
+ * @param string $path Path to the configuration file
+ * @param array $settings Settings passed as options on the command line
+ * @return \CaptainHook\App\Config
+ * @throws \Exception
+ */
+ public function createConfig(string $path = '', array $settings = []): Config
+ {
+ $path = $path ?: getcwd() . DIRECTORY_SEPARATOR . CH::CONFIG;
+ $file = new Json($path);
+ $settings = $this->combineArgumentsAndSettingFile($file, $settings);
+
+ return $this->setupConfig($file, $settings);
+ }
+
+ /**
+ * Read settings from a local 'config' file
+ *
+ * If you prefer a different verbosity or use a different run mode locally then your teammates do.
+ * You can create a 'captainhook.config.json' in the same directory as your captainhook
+ * configuration file and use it to overwrite the 'config' settings of that configuration file.
+ * Exclude the 'captainhook.config.json' from version control, and you don't have to edit the
+ * version controlled configuration for your local specifics anymore.
+ *
+ * Settings provided as arguments still overrule config file settings:
+ *
+ * ARGUMENTS > SETTINGS_FILE > CONFIGURATION
+ *
+ * @param \CaptainHook\App\Storage\File\Json $file
+ * @param array $settings
+ * @return array
+ */
+ private function combineArgumentsAndSettingFile(Json $file, array $settings): array
+ {
+ $settingsFile = new Json(dirname($file->getPath()) . '/captainhook.config.json');
+ if ($settingsFile->exists()) {
+ $fileSettings = $settingsFile->readAssoc();
+ $settings = array_merge($fileSettings, $settings);
+ }
+ return $settings;
+ }
+
+ /**
+ * Includes an external captainhook configuration
+ *
+ * @param string $path
+ * @return \CaptainHook\App\Config
+ * @throws \Exception
+ */
+ private function includeConfig(string $path): Config
+ {
+ $file = new Json($path);
+ if (!$file->exists()) {
+ throw new RuntimeException('Config to include not found: ' . $path);
+ }
+ return $this->setupConfig($file);
+ }
+
+ /**
+ * Return a configuration with data loaded from json file if it exists
+ *
+ * @param \CaptainHook\App\Storage\File\Json $file
+ * @param array $settings
+ * @return \CaptainHook\App\Config
+ * @throws \Exception
+ */
+ private function setupConfig(Json $file, array $settings = []): Config
+ {
+ return $file->exists()
+ ? $this->loadConfigFromFile($file, $settings)
+ : new Config($file->getPath(), false, $settings);
+ }
+
+ /**
+ * Loads a given file into given the configuration
+ *
+ * @param \CaptainHook\App\Storage\File\Json $file
+ * @param array $settings
+ * @return \CaptainHook\App\Config
+ * @throws \Exception
+ */
+ private function loadConfigFromFile(Json $file, array $settings): Config
+ {
+ $json = $file->readAssoc();
+ Util::validateJsonConfiguration($json);
+
+ $settings = Util::mergeSettings($this->extractSettings($json), $settings);
+ $config = new Config($file->getPath(), true, $settings);
+ if (!empty($settings)) {
+ $json['config'] = $settings;
+ }
+
+ $this->appendIncludedConfigurations($config, $json);
+
+ foreach (HookUtil::getValidHooks() as $hook => $class) {
+ if (isset($json[$hook])) {
+ $this->configureHook($config->getHookConfig($hook), $json[$hook]);
+ }
+ }
+
+ $this->validatePhpPath($config);
+ return $config;
+ }
+
+ /**
+ * Return the `config` section of some json
+ *
+ * @param array $json
+ * @return array
+ */
+ private function extractSettings(array $json): array
+ {
+ return Util::extractListFromJson($json, 'config');
+ }
+
+ /**
+ * Returns the `conditions` section of an actionJson
+ *
+ * @param array $json
+ * @return array
+ */
+ private function extractConditions(mixed $json): array
+ {
+ return Util::extractListFromJson($json, 'conditions');
+ }
+
+ /**
+ * Returns the `options` section af some json
+ *
+ * @param array $json
+ * @return array
+ */
+ private function extractOptions(mixed $json): array
+ {
+ return Util::extractListFromJson($json, 'options');
+ }
+
+ /**
+ * Set up a hook configuration by json data
+ *
+ * @param \CaptainHook\App\Config\Hook $config
+ * @param array $json
+ * @return void
+ * @throws \Exception
+ */
+ private function configureHook(Config\Hook $config, array $json): void
+ {
+ $config->setEnabled($json['enabled'] ?? true);
+ foreach ($json['actions'] as $actionJson) {
+ $options = $this->extractOptions($actionJson);
+ $conditions = $this->extractConditions($actionJson);
+ $settings = $this->extractSettings($actionJson);
+ $config->addAction(new Config\Action($actionJson['action'], $options, $conditions, $settings));
+ }
+ }
+
+ /**
+ * Makes sure the configured PHP executable exists
+ *
+ * @param \CaptainHook\App\Config $config
+ * @return void
+ */
+ private function validatePhpPath(Config $config): void
+ {
+ if (empty($config->getPhpPath())) {
+ return;
+ }
+ $pathToCheck = [$config->getPhpPath()];
+ $parts = explode(' ', $config->getPhpPath());
+ // if there are spaces in the php-path and they are not escaped
+ // it looks like an executable is used to find the PHP binary
+ // so at least check if the executable exists
+ if ($this->usesPathResolver($parts)) {
+ $pathToCheck[] = $parts[0];
+ }
+
+ foreach ($pathToCheck as $path) {
+ if (file_exists($path)) {
+ return;
+ }
+ }
+ throw new RuntimeException('The configured php-path is wrong: ' . $config->getPhpPath());
+ }
+
+ /**
+ * Is a binary used to resolve the php path
+ *
+ * @param array $parts
+ * @return bool
+ */
+ private function usesPathResolver(array $parts): bool
+ {
+ return count($parts) > 1 && !str_ends_with($parts[0], '\\');
+ }
+
+ /**
+ * Append all included configuration to the current configuration
+ *
+ * @param \CaptainHook\App\Config $config
+ * @param array $json
+ * @throws \Exception
+ */
+ private function appendIncludedConfigurations(Config $config, array $json): void
+ {
+ $this->readMaxIncludeLevel($json);
+
+ if ($this->includeLevel < $this->maxIncludeLevel) {
+ $this->includeLevel++;
+ $includes = $this->loadIncludedConfigs($json, $config->getPath());
+ foreach (HookUtil::getValidHooks() as $hook => $class) {
+ $this->mergeHookConfigFromIncludes($config->getHookConfig($hook), $includes);
+ }
+ $this->includeLevel--;
+ }
+ }
+
+ /**
+ * Check config section for 'includes-level' setting
+ *
+ * @param array $json
+ */
+ private function readMaxIncludeLevel(array $json): void
+ {
+ // read the include-level setting only for the actual configuration
+ if ($this->includeLevel === 0 && isset($json['config'][Config::SETTING_INCLUDES_LEVEL])) {
+ $this->maxIncludeLevel = (int) $json['config'][Config::SETTING_INCLUDES_LEVEL];
+ }
+ }
+
+ /**
+ * Merge a given hook config with the corresponding hook configs from a list of included configurations
+ *
+ * @param \CaptainHook\App\Config\Hook $hook
+ * @param \CaptainHook\App\Config[] $includes
+ * @return void
+ */
+ private function mergeHookConfigFromIncludes(Hook $hook, array $includes): void
+ {
+ foreach ($includes as $includedConfig) {
+ $includedHook = $includedConfig->getHookConfig($hook->getName());
+ if ($includedHook->isEnabled()) {
+ $hook->setEnabled(true);
+ // This `setEnable` is solely to overwrite the main configuration in the special case that the hook
+ // is not configured at all. In this case the empty config is disabled by default, and adding an
+ // empty hook config just to enable the included actions feels a bit dull.
+ // Since the main hook is processed last (if one is configured) the enabled flag will be overwritten
+ // once again by the main config value. This is to make sure that if somebody disables a hook in its
+ // main configuration, no actions will get executed, even if we have enabled hooks in any include file.
+ $this->copyActionsFromTo($includedHook, $hook);
+ }
+ }
+ }
+
+ /**
+ * Return list of included configurations to add them to the main configuration afterwards
+ *
+ * @param array $json
+ * @param string $path
+ * @return \CaptainHook\App\Config[]
+ * @throws \Exception
+ */
+ protected function loadIncludedConfigs(array $json, string $path): array
+ {
+ $includes = [];
+ $directory = dirname($path);
+ $files = Util::extractListFromJson(Util::extractListFromJson($json, 'config'), Config::SETTING_INCLUDES);
+
+ foreach ($files as $file) {
+ $includes[] = $this->includeConfig($directory . DIRECTORY_SEPARATOR . $file);
+ }
+ return $includes;
+ }
+
+ /**
+ * Copy action from a given configuration to the second given configuration
+ *
+ * @param \CaptainHook\App\Config\Hook $sourceConfig
+ * @param \CaptainHook\App\Config\Hook $targetConfig
+ */
+ private function copyActionsFromTo(Hook $sourceConfig, Hook $targetConfig): void
+ {
+ foreach ($sourceConfig->getActions() as $action) {
+ $action->markIncluded();
+ $targetConfig->addAction($action);
+ }
+ }
+
+ /**
+ * Config factory method
+ *
+ * @param string $path
+ * @param array $settings
+ * @return \CaptainHook\App\Config
+ * @throws \Exception
+ */
+ public static function create(string $path = '', array $settings = []): Config
+ {
+ $factory = new static();
+ return $factory->createConfig($path, $settings);
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Config/Hook.php b/lib/captainhook/captainhook/src/Config/Hook.php
new file mode 100644
index 0000000000..8c515512cf
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Config/Hook.php
@@ -0,0 +1,137 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Config;
+
+/**
+ * Class Hook
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 0.9.0
+ * @internal
+ */
+class Hook
+{
+ /**
+ * Hook name e.g. pre-commit
+ *
+ * @var string
+ */
+ private string $name;
+
+ /**
+ * Is hook enabled
+ *
+ * @var bool
+ */
+ private bool $isEnabled;
+
+ /**
+ * List of Actions
+ *
+ * @var \CaptainHook\App\Config\Action[]
+ */
+ private $actions = [];
+
+ /**
+ * Hook constructor
+ *
+ * @param string $name
+ * @param bool $enabled
+ */
+ public function __construct(string $name, bool $enabled = false)
+ {
+ $this->name = $name;
+ $this->isEnabled = $enabled;
+ }
+
+ /**
+ * Name getter
+ *
+ * @return string
+ */
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * Enable or disable the hook
+ *
+ * @param bool $enabled
+ * @return void
+ */
+ public function setEnabled(bool $enabled): void
+ {
+ $this->isEnabled = $enabled;
+ }
+
+ /**
+ * Is this hook enabled
+ *
+ * @return bool
+ */
+ public function isEnabled(): bool
+ {
+ return $this->isEnabled;
+ }
+
+ /**
+ * Check if a hook config has actions
+ *
+ * @return bool
+ */
+ public function hasActions(): bool
+ {
+ return !empty($this->actions);
+ }
+
+ /**
+ * Add an action to the list
+ *
+ * @param \CaptainHook\App\Config\Action ...$actions
+ * @return void
+ */
+ public function addAction(Action ...$actions): void
+ {
+ foreach ($actions as $action) {
+ $this->actions[] = $action;
+ }
+ }
+
+ /**
+ * Return the action list
+ *
+ * @return \CaptainHook\App\Config\Action[]
+ */
+ public function getActions(): array
+ {
+ return $this->actions;
+ }
+
+ /**
+ * Return config data
+ *
+ * @return array
+ */
+ public function getJsonData(): array
+ {
+ $config = ['enabled' => $this->isEnabled, 'actions' => []];
+ foreach ($this->actions as $action) {
+ if (!$action->isIncluded()) {
+ $config['actions'][] = $action->getJsonData();
+ }
+ }
+ return $config;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Config/Options.php b/lib/captainhook/captainhook/src/Config/Options.php
new file mode 100644
index 0000000000..f23dd9f981
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Config/Options.php
@@ -0,0 +1,63 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Config;
+
+/**
+ * Class Options
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 1.0.0
+ */
+class Options
+{
+ /**
+ * Map of options
+ *
+ * @var array
+ */
+ private array $options;
+
+ /**
+ * Options constructor
+ *
+ * @param array $options
+ */
+ public function __construct(array $options)
+ {
+ $this->options = $options;
+ }
+
+ /**
+ * Return a option value
+ *
+ * @template ProvidedDefault
+ * @param string $name
+ * @param ProvidedDefault $default
+ * @return ProvidedDefault|mixed
+ */
+ public function get(string $name, $default = null)
+ {
+ return $this->options[$name] ?? $default;
+ }
+
+ /**
+ * Return all options
+ *
+ * @return array
+ */
+ public function getAll(): array
+ {
+ return $this->options;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Config/Plugin.php b/lib/captainhook/captainhook/src/Config/Plugin.php
new file mode 100644
index 0000000000..b1322a2609
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Config/Plugin.php
@@ -0,0 +1,99 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Config;
+
+use CaptainHook\App\Exception\InvalidPlugin;
+use CaptainHook\App\Plugin\CaptainHook;
+
+/**
+ * Class Plugin
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.9.0
+ */
+class Plugin
+{
+ /**
+ * Plugin class
+ *
+ * @var string
+ */
+ private string $plugin;
+
+ /**
+ * Map of options name => value
+ *
+ * @var Options
+ */
+ private Options $options;
+
+ /**
+ * Plugin constructor
+ *
+ * @param string $plugin
+ * @param array $options
+ */
+ public function __construct(string $plugin, array $options = [])
+ {
+ if (!is_a($plugin, CaptainHook::class, true)) {
+ throw new InvalidPlugin("{$plugin} is not a valid CaptainHook plugin.");
+ }
+
+ $this->plugin = $plugin;
+ $this->setupOptions($options);
+ }
+
+ /**
+ * Setup options
+ *
+ * @param array $options
+ */
+ private function setupOptions(array $options): void
+ {
+ $this->options = new Options($options);
+ }
+
+ /**
+ * Plugin class name getter
+ *
+ * @return string
+ */
+ public function getPlugin(): string
+ {
+ return $this->plugin;
+ }
+
+ /**
+ * Return option map
+ *
+ * @return Options
+ */
+ public function getOptions(): Options
+ {
+ return $this->options;
+ }
+
+ /**
+ * Return config data
+ *
+ * @return array
+ */
+ public function getJsonData(): array
+ {
+ return [
+ 'plugin' => $this->plugin,
+ 'options' => $this->options->getAll(),
+ ];
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Config/Run.php b/lib/captainhook/captainhook/src/Config/Run.php
new file mode 100644
index 0000000000..32175bac41
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Config/Run.php
@@ -0,0 +1,108 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Config;
+
+/**
+ * Run Config
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.18.0
+ */
+class Run
+{
+ private const MODE = 'mode';
+ private const PATH = 'path';
+ private const EXEC = 'exec';
+ private const GIT = 'git';
+
+ /**
+ * Map of options name => value
+ *
+ * @var \CaptainHook\App\Config\Options
+ */
+ private Options $options;
+
+ /**
+ * Run constructor
+ *
+ * @param array $options
+ */
+ public function __construct(array $options = [])
+ {
+ $this->setupOptions($options);
+ }
+
+ /**
+ * Setup options
+ *
+ * @param array $options
+ */
+ private function setupOptions(array $options): void
+ {
+ $this->options = new Options($options);
+ }
+
+ /**
+ * Return the run mode shell|docker|php|local|wsl
+ *
+ * @return string
+ */
+ public function getMode(): string
+ {
+ return $this->options->get(self::MODE, 'shell');
+ }
+
+ /**
+ * Return the path to the captain from within the container or to overwrite symlink resolution
+ *
+ * Since realpath() returns the real absolute path and not the absolute symlink path this
+ * setting could be used to overwrite this behaviour.
+ *
+ * @return string
+ */
+ public function getCaptainsPath(): string
+ {
+ return $this->options->get(self::PATH, '');
+ }
+
+ /**
+ * Return the docker command to use to execute the captain
+ *
+ * @return string
+ */
+ public function getDockerCommand(): string
+ {
+ return $this->options->get(self::EXEC, '');
+ }
+
+ /**
+ * Return the path mapping setting
+ *
+ * @return string
+ */
+ public function getGitPath(): string
+ {
+ return $this->options->get(self::GIT, '');
+ }
+
+ /**
+ * Return config data
+ *
+ * @return array
+ */
+ public function getJsonData(): array
+ {
+ return $this->options->getAll();
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Config/Util.php b/lib/captainhook/captainhook/src/Config/Util.php
new file mode 100644
index 0000000000..0553113582
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Config/Util.php
@@ -0,0 +1,193 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Config;
+
+use CaptainHook\App\Hook\Util as HookUtil;
+use CaptainHook\App\Config;
+use CaptainHook\App\Storage\File\Json;
+use RuntimeException;
+
+/**
+ * Class Util
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 1.0.3
+ * @internal
+ */
+abstract class Util
+{
+ /**
+ * Validate a configuration
+ *
+ * @param array $json
+ * @return void
+ * @throws \RuntimeException
+ */
+ public static function validateJsonConfiguration(array $json): void
+ {
+ self::validatePluginConfig($json);
+
+ foreach (HookUtil::getValidHooks() as $hook => $class) {
+ if (isset($json[$hook])) {
+ self::validateHookConfig($json[$hook]);
+ }
+ }
+ }
+
+ /**
+ * Validate a hook configuration
+ *
+ * @param array $json
+ * @return void
+ * @throws \RuntimeException
+ */
+ public static function validateHookConfig(array $json): void
+ {
+ if (!self::keysExist(['enabled', 'actions'], $json)) {
+ throw new RuntimeException('Config error: invalid hook configuration');
+ }
+ if (!is_array($json['actions'])) {
+ throw new RuntimeException('Config error: \'actions\' must be an array');
+ }
+ self::validateActionsConfig($json['actions']);
+ }
+
+ /**
+ * Validate a plugin configuration
+ *
+ * @param array $json
+ * @return void
+ * @throws \RuntimeException
+ */
+ public static function validatePluginConfig(array $json): void
+ {
+ if (!isset($json['config']['plugins'])) {
+ return;
+ }
+ if (!is_array($json['config']['plugins'])) {
+ throw new RuntimeException('Config error: \'plugins\' must be an array');
+ }
+ foreach ($json['config']['plugins'] as $plugin) {
+ if (!self::keysExist(['plugin'], $plugin)) {
+ throw new RuntimeException('Config error: \'plugin\' missing');
+ }
+ if (empty($plugin['plugin'])) {
+ throw new RuntimeException('Config error: \'plugin\' can\'t be empty');
+ }
+ }
+ }
+
+ /**
+ * Validate a list of action configurations
+ *
+ * @param array $json
+ * @return void
+ * @throws \RuntimeException
+ */
+ public static function validateActionsConfig(array $json): void
+ {
+ foreach ($json as $action) {
+ if (!self::keysExist(['action'], $action)) {
+ throw new RuntimeException('Config error: \'action\' missing');
+ }
+ if (empty($action['action'])) {
+ throw new RuntimeException('Config error: \'action\' can\'t be empty');
+ }
+ if (!empty($action['conditions'])) {
+ self::validateConditionsConfig($action['conditions']);
+ }
+ }
+ }
+
+ /**
+ * Validate a list of condition configurations
+ *
+ * @param array> $json
+ * @throws \RuntimeException
+ */
+ public static function validateConditionsConfig(array $json): void
+ {
+ foreach ($json as $condition) {
+ if (!self::keysExist(['exec'], $condition) || empty($condition['exec'])) {
+ throw new RuntimeException('Config error: \'exec\' is required for conditions');
+ }
+ if (!empty($condition['args']) && !is_array($condition['args'])) {
+ throw new RuntimeException('Config error: invalid \'args\' configuration');
+ }
+ }
+ }
+
+ /**
+ * Extracts a list from a json data struct with the necessary safeguards
+ *
+ * @param array $json
+ * @param string $value
+ * @return array
+ */
+ public static function extractListFromJson(array $json, string $value): array
+ {
+ return isset($json[$value]) && is_array($json[$value]) ? $json[$value] : [];
+ }
+
+ /**
+ * Write the config to disk
+ *
+ * @param \CaptainHook\App\Config $config
+ * @return void
+ */
+ public static function writeToDisk(Config $config): void
+ {
+ $filePath = $config->getPath();
+ $file = new Json($filePath);
+ $file->write($config->getJsonData());
+ }
+
+ /**
+ * Merges a various list of settings arrays
+ *
+ * @param array $settings
+ * @return array
+ */
+ public static function mergeSettings(array ...$settings): array
+ {
+ $includes = array_column($settings, Config::SETTING_INCLUDES);
+ $custom = array_column($settings, Config::SETTING_CUSTOM);
+ $mergedSettings = array_merge(...$settings);
+ if (!empty($includes)) {
+ $mergedSettings[Config::SETTING_INCLUDES] = array_merge(...$includes);
+ }
+ if (!empty($custom)) {
+ $mergedSettings[Config::SETTING_CUSTOM] = array_merge(...$custom);
+ }
+
+ return $mergedSettings;
+ }
+
+ /**
+ * Does an array have the expected keys
+ *
+ * @param array $keys
+ * @param array $subject
+ * @return bool
+ */
+ private static function keysExist(array $keys, array $subject): bool
+ {
+ foreach ($keys as $key) {
+ if (!isset($subject[$key])) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/Application.php b/lib/captainhook/captainhook/src/Console/Application.php
new file mode 100644
index 0000000000..71076e369d
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/Application.php
@@ -0,0 +1,143 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console;
+
+use CaptainHook\App\CH;
+use CaptainHook\App\Console\Command as Cmd;
+use CaptainHook\App\Console\Runtime\Resolver;
+use Symfony\Component\Console\Application as SymfonyApplication;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Class Application
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 0.9.0
+ */
+class Application extends SymfonyApplication
+{
+ /**
+ * Path to captainhook binary
+ *
+ * @var string
+ */
+ protected string $executable;
+
+ /**
+ * Cli constructor.
+ *
+ * @param string $executable
+ */
+ public function __construct(string $executable)
+ {
+ $this->executable = $executable;
+
+ parent::__construct('CaptainHook', CH::VERSION);
+
+ $this->setDefaultCommand('list');
+ $this->silenceXDebug();
+ }
+
+ /**
+ * Make sure the list command is run on default `-h|--help` executions
+ *
+ * @param \Symfony\Component\Console\Input\InputInterface $input
+ * @param \Symfony\Component\Console\Output\OutputInterface $output
+ * @return int
+ * @throws \Symfony\Component\Console\Exception\ExceptionInterface
+ * @throws \Throwable
+ */
+ public function doRun(InputInterface $input, OutputInterface $output): int
+ {
+ if ($this->isHelpWithoutCommand($input)) {
+ // Run the `list` command not `list --help`
+ return $this->find('list')->run($input, $output);
+ }
+ return parent::doRun($input, $output);
+ }
+
+ /**
+ * Initializes all the CaptainHook commands
+ *
+ * @return \Symfony\Component\Console\Command\Command[]
+ */
+ public function getDefaultCommands(): array
+ {
+ $resolver = new Resolver($this->executable);
+ $symfonyDefaults = parent::getDefaultCommands();
+
+ return array_merge(
+ array_slice($symfonyDefaults, 0, 2),
+ [
+ new Cmd\Install($resolver),
+ new Cmd\Uninstall($resolver),
+ new Cmd\Configuration($resolver),
+ new Cmd\Info($resolver),
+ new Cmd\Add($resolver),
+ new Cmd\Disable($resolver),
+ new Cmd\Enable($resolver),
+ new Cmd\Hook\CommitMsg($resolver),
+ new Cmd\Hook\PostCheckout($resolver),
+ new Cmd\Hook\PostCommit($resolver),
+ new Cmd\Hook\PostMerge($resolver),
+ new Cmd\Hook\PostRewrite($resolver),
+ new Cmd\Hook\PreCommit($resolver),
+ new Cmd\Hook\PrepareCommitMsg($resolver),
+ new Cmd\Hook\PrePush($resolver),
+ ]
+ );
+ }
+
+ /**
+ * Append release date to version output
+ *
+ * @return string
+ */
+ public function getLongVersion(): string
+ {
+ return sprintf(
+ '%s version %s %s #StandWith>Ukraine>',
+ $this->getName(),
+ $this->getVersion(),
+ CH::RELEASE_DATE
+ );
+ }
+
+ /**
+ * Make sure X-Debug does not interfere with the exception handling
+ *
+ * @return void
+ *
+ * @codeCoverageIgnore
+ */
+ private function silenceXDebug(): void
+ {
+ if (function_exists('ini_set') && extension_loaded('xdebug')) {
+ ini_set('xdebug.show_exception_trace', '0');
+ ini_set('xdebug.scream', '0');
+ }
+ }
+
+ /**
+ * Checks if the --help is called without any sub command
+ *
+ * @param \Symfony\Component\Console\Input\InputInterface $input
+ * @return bool
+ */
+ private function isHelpWithoutCommand(InputInterface $input): bool
+ {
+ return $input->hasParameterOption(['--help', '-h'], true) && !$input->getFirstArgument();
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/Command.php b/lib/captainhook/captainhook/src/Console/Command.php
new file mode 100644
index 0000000000..795e11f192
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/Command.php
@@ -0,0 +1,103 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console;
+
+use CaptainHook\App\Console\Runtime\Resolver;
+use Symfony\Component\Console\Command\Command as SymfonyCommand;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Throwable;
+
+/**
+ * Class Command
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.0.0
+ */
+abstract class Command extends SymfonyCommand
+{
+ /**
+ * Input output handler
+ *
+ * @var \CaptainHook\App\Console\IO|null
+ */
+ private ?IO $io = null;
+
+ /**
+ * Runtime resolver
+ *
+ * @var \CaptainHook\App\Console\Runtime\Resolver
+ */
+ protected Resolver $resolver;
+
+ /**
+ * Command constructor
+ *
+ * @param \CaptainHook\App\Console\Runtime\Resolver $resolver
+ */
+ public function __construct(Resolver $resolver)
+ {
+ $this->resolver = $resolver;
+ parent::__construct();
+ }
+
+ /**
+ * IO setter
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ */
+ public function setIO(IO $io): void
+ {
+ $this->io = $io;
+ }
+
+ /**
+ * IO interface getter
+ *
+ * @param \Symfony\Component\Console\Input\InputInterface $input
+ * @param \Symfony\Component\Console\Output\OutputInterface $output
+ * @return \CaptainHook\App\Console\IO
+ */
+ public function getIO(InputInterface $input, OutputInterface $output): IO
+ {
+ if (null === $this->io) {
+ $this->io = new IO\DefaultIO($input, $output, $this->getHelperSet());
+ }
+ return $this->io;
+ }
+
+ /**
+ * Write a final error message
+ *
+ * @param \Symfony\Component\Console\Output\OutputInterface $out
+ * @param \Throwable $t
+ * @return int
+ * @throws \Throwable
+ */
+ public function crash(OutputInterface $out, Throwable $t): int
+ {
+ if ($out->isDebug()) {
+ throw $t;
+ }
+
+ $out->writeln('' . $t->getMessage() . '>');
+ if ($out->isVerbose()) {
+ $out->writeln(
+ 'Error triggered in file: ' . $t->getFile() .
+ ' in line: ' . $t->getLine()
+ );
+ }
+ return 1;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/Command/Add.php b/lib/captainhook/captainhook/src/Console/Command/Add.php
new file mode 100644
index 0000000000..bc01c1c4a7
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/Command/Add.php
@@ -0,0 +1,73 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console\Command;
+
+use CaptainHook\App\Console\IOUtil;
+use CaptainHook\App\Runner\Config\Editor;
+use Exception;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Class Add
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 4.2.0
+ */
+class Add extends ConfigAware
+{
+ /**
+ * Configure the command
+ *
+ * @return void
+ */
+ protected function configure(): void
+ {
+ parent::configure();
+ $this->setName('config:add')
+ ->setAliases(['add'])
+ ->setDescription('Add an action to your hook configuration')
+ ->setHelp('Add an action to your hook configuration')
+ ->addArgument('hook', InputArgument::REQUIRED, 'Hook you want to add the action to');
+ }
+
+ /**
+ * Execute the command
+ *
+ * @param \Symfony\Component\Console\Input\InputInterface $input
+ * @param \Symfony\Component\Console\Output\OutputInterface $output
+ * @return int
+ * @throws \CaptainHook\App\Exception\InvalidHookName
+ * @throws \Exception
+ */
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ try {
+ $io = $this->getIO($input, $output);
+ $config = $this->createConfig($input, true);
+
+ $this->determineVerbosity($output, $config);
+
+ $editor = new Editor($io, $config);
+ $editor->setHook(IOUtil::argToString($input->getArgument('hook')))
+ ->setChange('AddAction')
+ ->run();
+
+ return 0;
+ } catch (Exception $e) {
+ return $this->crash($output, $e);
+ }
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/Command/ConfigAware.php b/lib/captainhook/captainhook/src/Console/Command/ConfigAware.php
new file mode 100644
index 0000000000..507760ffe0
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/Command/ConfigAware.php
@@ -0,0 +1,129 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console\Command;
+
+use CaptainHook\App\CH;
+use CaptainHook\App\Config;
+use CaptainHook\App\Console\Command;
+use CaptainHook\App\Console\IOUtil;
+use RuntimeException;
+use SebastianFeldmann\Camino\Check;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Class ConfigAware
+ *
+ * Base class for all commands that need to be aware of the CaptainHook configuration.
+ *
+ * @package CaptainHook\App
+ */
+abstract class ConfigAware extends Command
+{
+ /**
+ * Set up the configuration command option
+ *
+ * @return void
+ */
+ protected function configure(): void
+ {
+ parent::configure();
+ $this->addOption(
+ 'configuration',
+ 'c',
+ InputOption::VALUE_OPTIONAL,
+ 'Path to your captainhook.json configuration',
+ './' . CH::CONFIG
+ );
+ }
+
+ /**
+ * Create a new Config object
+ *
+ * @param \Symfony\Component\Console\Input\InputInterface $input
+ * @param bool $failIfNotFound
+ * @param array $settings
+ * @return \CaptainHook\App\Config
+ * @throws \Exception
+ */
+ protected function createConfig(InputInterface $input, bool $failIfNotFound = false, array $settings = []): Config
+ {
+ $config = Config\Factory::create($this->getConfigPath($input), $this->fetchConfigSettings($input, $settings));
+ if ($failIfNotFound && !$config->isLoadedFromFile()) {
+ throw new RuntimeException(
+ 'Please create a captainhook configuration first' . PHP_EOL .
+ 'Run \'captainhook configure\'' . PHP_EOL .
+ 'If you have a configuration located elsewhere use the --configuration option'
+ );
+ }
+ return $config;
+ }
+
+ /**
+ * Return the given config path option
+ *
+ * @param \Symfony\Component\Console\Input\InputInterface $input
+ * @return string
+ */
+ private function getConfigPath(InputInterface $input): string
+ {
+ $path = IOUtil::argToString($input->getOption('configuration'));
+
+ // if path not absolute
+ if (!Check::isAbsolutePath($path)) {
+ // try to guess the config location and
+ // transform relative path to absolute path
+ if (substr($path, 0, 2) === './') {
+ return getcwd() . substr($path, 1);
+ }
+ return getcwd() . '/' . $path;
+ }
+ return $path;
+ }
+
+ /**
+ * Return list of available options to overwrite the configuration settings
+ *
+ * @param \Symfony\Component\Console\Input\InputInterface $input
+ * @param array $settingNames
+ * @return array
+ */
+ private function fetchConfigSettings(InputInterface $input, array $settingNames): array
+ {
+ $settings = [];
+ foreach ($settingNames as $setting) {
+ $value = IOUtil::argToString($input->getOption($setting));
+ if (!empty($value)) {
+ $settings[$setting] = $value;
+ }
+ }
+ return $settings;
+ }
+
+ /**
+ * Check which verbosity to use, either the cli option or the config file setting
+ *
+ * @param \Symfony\Component\Console\Output\OutputInterface $out
+ * @param \CaptainHook\App\Config $config
+ * @return void
+ */
+ protected function determineVerbosity(OutputInterface $out, Config $config): void
+ {
+ $verbosity = IOUtil::mapConfigVerbosity($config->getVerbosity());
+ $cliVerbosity = $out->getVerbosity();
+ if ($cliVerbosity !== OutputInterface::VERBOSITY_NORMAL) {
+ $verbosity = $cliVerbosity;
+ }
+ $out->setVerbosity($verbosity);
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/Command/Configuration.php b/lib/captainhook/captainhook/src/Console/Command/Configuration.php
new file mode 100644
index 0000000000..8dd0867341
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/Command/Configuration.php
@@ -0,0 +1,82 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console\Command;
+
+use CaptainHook\App\Console\IOUtil;
+use CaptainHook\App\Console\Runtime\Resolver;
+use CaptainHook\App\Runner\Config\Creator;
+use Exception;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Class Config
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 0.9.0
+ */
+class Configuration extends ConfigAware
+{
+ /**
+ * Configure the command
+ *
+ * @return void
+ */
+ protected function configure(): void
+ {
+ parent::configure();
+ $this->setName('configure')
+ ->setDescription('Create or update a captainhook.json configuration')
+ ->setHelp('Create or update a captainhook.json configuration')
+ ->addOption('extend', 'e', InputOption::VALUE_NONE, 'Extend existing configuration file')
+ ->addOption('force', 'f', InputOption::VALUE_NONE, 'Overwrite existing configuration file')
+ ->addOption('advanced', 'a', InputOption::VALUE_NONE, 'More options, but more to type')
+ ->addOption(
+ 'bootstrap',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ 'Path to composers vendor/autoload.php'
+ );
+ }
+
+ /**
+ * Execute the command
+ *
+ * @param \Symfony\Component\Console\Input\InputInterface $input
+ * @param \Symfony\Component\Console\Output\OutputInterface $output
+ * @return int
+ * @throws \Exception
+ */
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ try {
+ $io = $this->getIO($input, $output);
+ $config = $this->createConfig($input, false, ['bootstrap']);
+
+ $this->determineVerbosity($output, $config);
+
+ $configurator = new Creator($io, $config);
+ $configurator->force(IOUtil::argToBool($input->getOption('force')))
+ ->extend(IOUtil::argToBool($input->getOption('extend')))
+ ->advanced(IOUtil::argToBool($input->getOption('advanced')))
+ ->setExecutable($this->resolver->getExecutable())
+ ->run();
+
+ return 0;
+ } catch (Exception $e) {
+ return $this->crash($output, $e);
+ }
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/Command/Disable.php b/lib/captainhook/captainhook/src/Console/Command/Disable.php
new file mode 100644
index 0000000000..6cf38f624f
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/Command/Disable.php
@@ -0,0 +1,73 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console\Command;
+
+use CaptainHook\App\Console\IOUtil;
+use CaptainHook\App\Runner\Config\Editor;
+use Exception;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Class Add
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 4.2.0
+ */
+class Disable extends ConfigAware
+{
+ /**
+ * Configure the command
+ *
+ * @return void
+ */
+ protected function configure(): void
+ {
+ parent::configure();
+ $this->setName('config:disable')
+ ->setAliases(['disable'])
+ ->setDescription('Disable the handling for a hook in your configuration')
+ ->setHelp('Disable the handling for a hook in your configuration')
+ ->addArgument('hook', InputArgument::REQUIRED, 'Hook you want to disable');
+ }
+
+ /**
+ * Execute the command
+ *
+ * @param \Symfony\Component\Console\Input\InputInterface $input
+ * @param \Symfony\Component\Console\Output\OutputInterface $output
+ * @return int
+ * @throws \CaptainHook\App\Exception\InvalidHookName
+ * @throws \Exception
+ */
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ try {
+ $io = $this->getIO($input, $output);
+ $config = $this->createConfig($input, true);
+
+ $this->determineVerbosity($output, $config);
+
+ $editor = new Editor($io, $config);
+ $editor->setHook(IOUtil::argToString($input->getArgument('hook')))
+ ->setChange('DisableHook')
+ ->run();
+
+ return 0;
+ } catch (Exception $e) {
+ return $this->crash($output, $e);
+ }
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/Command/Enable.php b/lib/captainhook/captainhook/src/Console/Command/Enable.php
new file mode 100644
index 0000000000..6c7e63b786
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/Command/Enable.php
@@ -0,0 +1,73 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console\Command;
+
+use CaptainHook\App\Console\IOUtil;
+use CaptainHook\App\Runner\Config\Editor;
+use Exception;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Class Add
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 4.2.0
+ */
+class Enable extends ConfigAware
+{
+ /**
+ * Configure the command
+ *
+ * @return void
+ */
+ protected function configure(): void
+ {
+ parent::configure();
+ $this->setName('config:enable')
+ ->setAliases(['enable'])
+ ->setDescription('Enable the handling for a hook in your configuration')
+ ->setHelp('Enable the handling for a hook in your configuration')
+ ->addArgument('hook', InputArgument::REQUIRED, 'Hook you want to enable');
+ }
+
+ /**
+ * Execute the command
+ *
+ * @param \Symfony\Component\Console\Input\InputInterface $input
+ * @param \Symfony\Component\Console\Output\OutputInterface $output
+ * @return int
+ * @throws \CaptainHook\App\Exception\InvalidHookName
+ * @throws \Exception
+ */
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ try {
+ $io = $this->getIO($input, $output);
+ $config = $this->createConfig($input, true);
+
+ $this->determineVerbosity($output, $config);
+
+ $editor = new Editor($io, $config);
+ $editor->setHook(IOUtil::argToString($input->getArgument('hook')))
+ ->setChange('EnableHook')
+ ->run();
+
+ return 0;
+ } catch (Exception $e) {
+ return $this->crash($output, $e);
+ }
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/Command/Hook.php b/lib/captainhook/captainhook/src/Console/Command/Hook.php
new file mode 100644
index 0000000000..6c6f3ff827
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/Command/Hook.php
@@ -0,0 +1,159 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console\Command;
+
+use CaptainHook\App\Config;
+use CaptainHook\App\Hook\Util as HookUtil;
+use CaptainHook\App\Runner\Bootstrap\Util as BootstrapUtil;
+use CaptainHook\App\Runner\Util as RunnerUtil;
+use RuntimeException;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Throwable;
+
+/**
+ * Class Hook
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 0.9.0
+ */
+abstract class Hook extends RepositoryAware
+{
+ /**
+ * Name of the hook to execute
+ *
+ * @var string
+ */
+ protected string $hookName;
+
+ /**
+ * Configure the command
+ *
+ * @return void
+ */
+ protected function configure(): void
+ {
+ parent::configure();
+ $this->setName('hook:' . $this->hookName)
+ ->setAliases([$this->hookName])
+ ->setDescription('Run git ' . $this->hookName . ' hook')
+ ->setHelp('This command executes the ' . $this->hookName . ' hook');
+
+ $this->addOption(
+ 'bootstrap',
+ 'b',
+ InputOption::VALUE_OPTIONAL,
+ 'Relative path from your config file to your bootstrap file'
+ );
+ $this->addOption(
+ 'input',
+ 'i',
+ InputOption::VALUE_OPTIONAL,
+ 'Original hook stdIn'
+ );
+ $this->addOption(
+ 'no-plugins',
+ null,
+ InputOption::VALUE_NONE,
+ 'Disable all hook plugins'
+ );
+ }
+
+ /**
+ * Execute the command
+ *
+ * @param \Symfony\Component\Console\Input\InputInterface $input
+ * @param \Symfony\Component\Console\Output\OutputInterface $output
+ * @return int
+ * @throws \Throwable
+ */
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ if ($this->shouldHooksBeSkipped()) {
+ $output->writeLn('all hooks were skipped because of the environment variable CAPTAINHOOK_SKIP_HOOKS or CI');
+ return 0;
+ }
+
+ $io = $this->getIO($input, $output);
+ $config = $this->createConfig($input, true, ['git-directory', 'bootstrap']);
+ $repository = $this->createRepository(dirname($config->getGitDirectory()));
+
+ // use ansi coloring if available and not disabled in captainhook.json
+ $output->setDecorated($output->isDecorated() && $config->useAnsiColors());
+ // use the configured verbosity to manage general output verbosity
+ $this->determineVerbosity($output, $config);
+
+ try {
+ $this->handleBootstrap($config);
+
+ $class = '\\CaptainHook\\App\\Runner\\Hook\\' . HookUtil::getHookCommand($this->hookName);
+ /** @var \CaptainHook\App\Runner\Hook $hook */
+ $hook = new $class($io, $config, $repository);
+ $hook->setPluginsDisabled($input->getOption('no-plugins'));
+ $hook->run();
+
+ return 0;
+ } catch (Throwable $t) {
+ return $this->crash($output, $t);
+ }
+ }
+
+ /**
+ * If CaptainHook is executed via PHAR this handles the bootstrap file inclusion
+ *
+ * @param \CaptainHook\App\Config $config
+ */
+ private function handleBootstrap(Config $config): void
+ {
+ // we only have to care about bootstrapping PHAR builds because for
+ // Composer installations the bootstrapping is already done in the bin script
+ if ($this->resolver->isPharRelease()) {
+ // check the custom and default autoloader
+ $bootstrapFile = BootstrapUtil::validateBootstrapPath($this->resolver->isPharRelease(), $config);
+ // since the phar has its own autoloader, we don't need to do anything
+ // if the bootstrap file is not actively set
+ if (empty($bootstrapFile)) {
+ return;
+ }
+ // the bootstrap file exists, so let's load it
+ try {
+ require $bootstrapFile;
+ } catch (Throwable $t) {
+ throw new RuntimeException(
+ 'Loading bootstrap file failed: ' . $bootstrapFile . PHP_EOL .
+ $t->getMessage() . PHP_EOL
+ );
+ }
+ }
+ }
+
+ /**
+ * Indicates if hooks should be skipped
+ *
+ * Either because of CI environment or the SKIP environment variable is set.
+ *
+ * @return bool
+ */
+ private function shouldHooksBeSkipped(): bool
+ {
+ foreach (['CAPTAINHOOK_SKIP_HOOKS', 'CI'] as $envVar) {
+ $skip = (int) RunnerUtil::getEnv($envVar, "0");
+ if ($skip === 1) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/Command/Hook/CommitMsg.php b/lib/captainhook/captainhook/src/Console/Command/Hook/CommitMsg.php
new file mode 100644
index 0000000000..3e5a20efbc
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/Command/Hook/CommitMsg.php
@@ -0,0 +1,45 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console\Command\Hook;
+
+use CaptainHook\App\Console\Command\Hook;
+use CaptainHook\App\Hooks;
+use Symfony\Component\Console\Input\InputArgument;
+
+/**
+ * Class CommitMessage
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 0.9.0
+ */
+class CommitMsg extends Hook
+{
+ /**
+ * Hook to execute
+ *
+ * @var string
+ */
+ protected string $hookName = Hooks::COMMIT_MSG;
+
+ /**
+ * Configure the command
+ *
+ * @return void
+ */
+ protected function configure(): void
+ {
+ parent::configure();
+ $this->addArgument(Hooks::ARG_MESSAGE_FILE, InputArgument::REQUIRED, 'File containing the commit message.');
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/Command/Hook/PostCheckout.php b/lib/captainhook/captainhook/src/Console/Command/Hook/PostCheckout.php
new file mode 100644
index 0000000000..78675300e6
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/Command/Hook/PostCheckout.php
@@ -0,0 +1,47 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console\Command\Hook;
+
+use CaptainHook\App\Console\Command\Hook;
+use CaptainHook\App\Hooks;
+use Symfony\Component\Console\Input\InputArgument;
+
+/**
+ * Class PostCheckout
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 4.1.0
+ */
+class PostCheckout extends Hook
+{
+ /**
+ * Hook to execute.
+ *
+ * @var string
+ */
+ protected string $hookName = Hooks::POST_CHECKOUT;
+
+ /**
+ * Configure the command
+ *
+ * @return void
+ */
+ protected function configure(): void
+ {
+ parent::configure();
+ $this->addArgument(Hooks::ARG_PREVIOUS_HEAD, InputArgument::OPTIONAL, 'Previous HEAD');
+ $this->addArgument(Hooks::ARG_NEW_HEAD, InputArgument::OPTIONAL, 'New HEAD');
+ $this->addArgument(Hooks::ARG_MODE, InputArgument::OPTIONAL, 'Checkout mode 1 branch 0 file');
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/Command/Hook/PostCommit.php b/lib/captainhook/captainhook/src/Console/Command/Hook/PostCommit.php
new file mode 100644
index 0000000000..5e61168bc2
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/Command/Hook/PostCommit.php
@@ -0,0 +1,33 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console\Command\Hook;
+
+use CaptainHook\App\Console\Command\Hook;
+use CaptainHook\App\Hooks;
+
+/**
+ * Class PostCommit
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 0.9.0
+ */
+class PostCommit extends Hook
+{
+ /**
+ * Hook to execute.
+ *
+ * @var string
+ */
+ protected string $hookName = Hooks::POST_COMMIT;
+}
diff --git a/lib/captainhook/captainhook/src/Console/Command/Hook/PostMerge.php b/lib/captainhook/captainhook/src/Console/Command/Hook/PostMerge.php
new file mode 100644
index 0000000000..ef3891b2da
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/Command/Hook/PostMerge.php
@@ -0,0 +1,45 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console\Command\Hook;
+
+use CaptainHook\App\Console\Command\Hook;
+use CaptainHook\App\Hooks;
+use Symfony\Component\Console\Input\InputArgument;
+
+/**
+ * Class PostMerge
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 4.0.1
+ */
+class PostMerge extends Hook
+{
+ /**
+ * Hook to execute.
+ *
+ * @var string
+ */
+ protected string $hookName = Hooks::POST_MERGE;
+
+ /**
+ * Configure the command
+ *
+ * @return void
+ */
+ protected function configure(): void
+ {
+ parent::configure();
+ $this->addArgument(Hooks::ARG_SQUASH, InputArgument::OPTIONAL, 'Merge was done with a squash merge.');
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/Command/Hook/PostRewrite.php b/lib/captainhook/captainhook/src/Console/Command/Hook/PostRewrite.php
new file mode 100644
index 0000000000..b29afadc77
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/Command/Hook/PostRewrite.php
@@ -0,0 +1,45 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console\Command\Hook;
+
+use CaptainHook\App\Console\Command\Hook;
+use CaptainHook\App\Hooks;
+use Symfony\Component\Console\Input\InputArgument;
+
+/**
+ * Class PostRewrite
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.4.0
+ */
+class PostRewrite extends Hook
+{
+ /**
+ * Hook to execute.
+ *
+ * @var string
+ */
+ protected string $hookName = Hooks::POST_REWRITE;
+
+ /**
+ * Configure the command
+ *
+ * @return void
+ */
+ protected function configure(): void
+ {
+ parent::configure();
+ $this->addArgument(Hooks::ARG_GIT_COMMAND, InputArgument::OPTIONAL, 'Executed command');
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/Command/Hook/PreCommit.php b/lib/captainhook/captainhook/src/Console/Command/Hook/PreCommit.php
new file mode 100644
index 0000000000..af1ea5c813
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/Command/Hook/PreCommit.php
@@ -0,0 +1,33 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console\Command\Hook;
+
+use CaptainHook\App\Console\Command\Hook;
+use CaptainHook\App\Hooks;
+
+/**
+ * Class PreCommit
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 0.9.0
+ */
+class PreCommit extends Hook
+{
+ /**
+ * Hook to execute.
+ *
+ * @var string
+ */
+ protected string $hookName = Hooks::PRE_COMMIT;
+}
diff --git a/lib/captainhook/captainhook/src/Console/Command/Hook/PrePush.php b/lib/captainhook/captainhook/src/Console/Command/Hook/PrePush.php
new file mode 100644
index 0000000000..1170ba4404
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/Command/Hook/PrePush.php
@@ -0,0 +1,46 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console\Command\Hook;
+
+use CaptainHook\App\Console\Command\Hook;
+use CaptainHook\App\Hooks;
+use Symfony\Component\Console\Input\InputArgument;
+
+/**
+ * Class PrePush
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 0.9.0
+ */
+class PrePush extends Hook
+{
+ /**
+ * Hook to execute.
+ *
+ * @var string
+ */
+ protected string $hookName = Hooks::PRE_PUSH;
+
+ /**
+ * Configure the command
+ *
+ * @return void
+ */
+ protected function configure(): void
+ {
+ parent::configure();
+ $this->addArgument(Hooks::ARG_TARGET, InputArgument::OPTIONAL, 'Target repository name');
+ $this->addArgument(Hooks::ARG_URL, InputArgument::OPTIONAL, 'Target repository url');
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/Command/Hook/PrepareCommitMsg.php b/lib/captainhook/captainhook/src/Console/Command/Hook/PrepareCommitMsg.php
new file mode 100644
index 0000000000..28464eb30b
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/Command/Hook/PrepareCommitMsg.php
@@ -0,0 +1,47 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console\Command\Hook;
+
+use CaptainHook\App\Console\Command\Hook;
+use CaptainHook\App\Hooks;
+use Symfony\Component\Console\Input\InputArgument;
+
+/**
+ * Class PrepareCommitMessage
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 3.1.0
+ */
+class PrepareCommitMsg extends Hook
+{
+ /**
+ * Hook to execute
+ *
+ * @var string
+ */
+ protected string $hookName = Hooks::PREPARE_COMMIT_MSG;
+
+ /**
+ * Configure the command
+ *
+ * @return void
+ */
+ protected function configure(): void
+ {
+ parent::configure();
+ $this->addArgument(Hooks::ARG_MESSAGE_FILE, InputArgument::REQUIRED, 'File containing the commit log message');
+ $this->addArgument(Hooks::ARG_MODE, InputArgument::OPTIONAL, 'Current commit mode');
+ $this->addArgument(Hooks::ARG_HASH, InputArgument::OPTIONAL, 'Given commit hash');
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/Command/Info.php b/lib/captainhook/captainhook/src/Console/Command/Info.php
new file mode 100644
index 0000000000..b147c1c8ab
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/Command/Info.php
@@ -0,0 +1,108 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console\Command;
+
+use CaptainHook\App\Console\IOUtil;
+use CaptainHook\App\Runner\Config\Reader;
+use Exception;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Command to display configuration information
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.24.0
+ */
+class Info extends RepositoryAware
+{
+ /**
+ * Configure the command
+ *
+ * @return void
+ */
+ protected function configure(): void
+ {
+ parent::configure();
+ $this->setName('config:info')
+ ->setAliases(['info'])
+ ->setDescription('Displays information about the configuration')
+ ->setHelp('Displays information about the configuration')
+ ->addArgument('hook', InputArgument::OPTIONAL, 'Hook you want to investigate')
+ ->addOption(
+ 'list-actions',
+ 'a',
+ InputOption::VALUE_NONE,
+ 'List all actions'
+ )
+ ->addOption(
+ 'list-conditions',
+ 'p',
+ InputOption::VALUE_NONE,
+ 'List all conditions'
+ )
+ ->addOption(
+ 'list-options',
+ 'o',
+ InputOption::VALUE_NONE,
+ 'List all options'
+ )
+ ->addOption(
+ 'list-config',
+ 's',
+ InputOption::VALUE_NONE,
+ 'List all config settings'
+ )
+ ->addOption(
+ 'extensive',
+ 'e',
+ InputOption::VALUE_NONE,
+ 'Show more detailed information'
+ );
+ }
+
+ /**
+ * Execute the command
+ *
+ * @param \Symfony\Component\Console\Input\InputInterface $input
+ * @param \Symfony\Component\Console\Output\OutputInterface $output
+ * @return int
+ * @throws \CaptainHook\App\Exception\InvalidHookName
+ * @throws \Exception
+ */
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ try {
+ $io = $this->getIO($input, $output);
+ $config = $this->createConfig($input, true, ['git-directory']);
+ $repo = $this->createRepository(dirname($config->getGitDirectory()));
+
+ $this->determineVerbosity($output, $config);
+
+ $editor = new Reader($io, $config, $repo);
+ $editor->setHook(IOUtil::argToString($input->getArgument('hook')))
+ ->display(Reader::OPT_ACTIONS, $input->getOption('list-actions'))
+ ->display(Reader::OPT_CONDITIONS, $input->getOption('list-conditions'))
+ ->display(Reader::OPT_OPTIONS, $input->getOption('list-options'))
+ ->extensive($input->getOption('extensive'))
+ ->run();
+
+ return 0;
+ } catch (Exception $e) {
+ return $this->crash($output, $e);
+ }
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/Command/Install.php b/lib/captainhook/captainhook/src/Console/Command/Install.php
new file mode 100644
index 0000000000..5bce00c61e
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/Command/Install.php
@@ -0,0 +1,156 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console\Command;
+
+use CaptainHook\App\Config;
+use CaptainHook\App\Console\IOUtil;
+use CaptainHook\App\Hook\Template;
+use CaptainHook\App\Runner\Installer;
+use Exception;
+use RuntimeException;
+use SebastianFeldmann\Git\Repository;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Class Install
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 0.9.0
+ */
+class Install extends RepositoryAware
+{
+ /**
+ * Configure the command
+ *
+ * @return void
+ */
+ protected function configure(): void
+ {
+ parent::configure();
+ $this->setName('install')
+ ->setDescription('Install hooks to your .git/hooks directory')
+ ->setHelp('Install git hooks to your .git/hooks directory')
+ ->addArgument(
+ 'hook',
+ InputArgument::OPTIONAL,
+ 'Limit the hooks you want to install. ' .
+ 'You can specify multiple hooks with comma as delimiter. ' .
+ 'By default all hooks get installed'
+ )
+ ->addOption(
+ 'only-enabled',
+ null,
+ InputOption::VALUE_NONE,
+ 'Limit the hooks you want to install to those enabled in your conf. ' .
+ 'By default all hooks get installed'
+ )
+ ->addOption(
+ 'force',
+ 'f',
+ InputOption::VALUE_NONE,
+ 'Force install without confirmation'
+ )
+ ->addOption(
+ 'skip-existing',
+ 's',
+ InputOption::VALUE_NONE,
+ 'Do not overwrite existing hooks'
+ )
+ ->addOption(
+ 'move-existing-to',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ 'Move existing hooks to given directory'
+ )
+ ->addOption(
+ 'bootstrap',
+ 'b',
+ InputOption::VALUE_OPTIONAL,
+ 'Path to composers vendor/autoload.php'
+ )
+ ->addOption(
+ 'run-mode',
+ 'm',
+ InputOption::VALUE_OPTIONAL,
+ 'Git hook run mode [php|shell|docker]'
+ )
+ ->addOption(
+ 'run-exec',
+ 'e',
+ InputOption::VALUE_OPTIONAL,
+ 'The Docker command to start your container e.g. \'docker exec CONTAINER\''
+ )
+ ->addOption(
+ 'run-path',
+ 'p',
+ InputOption::VALUE_OPTIONAL,
+ 'The path to the CaptainHook executable \'/usr/bin/captainhook\''
+ );
+ }
+
+ /**
+ * Execute the command
+ *
+ * @param \Symfony\Component\Console\Input\InputInterface $input
+ * @param \Symfony\Component\Console\Output\OutputInterface $output
+ * @return int
+ * @throws \Exception
+ */
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ try {
+ $args = ['git-directory', 'run-mode', 'run-exec', 'run-path', 'bootstrap'];
+ $io = $this->getIO($input, $output);
+ $config = $this->createConfig($input, true, $args);
+ $repo = $this->createRepository(dirname($config->getGitDirectory()));
+ $template = $this->createTemplate($config, $repo);
+
+ $this->determineVerbosity($output, $config);
+
+ $installer = new Installer($io, $config, $repo, $template);
+ $installer->setHook(IOUtil::argToString($input->getArgument('hook')))
+ ->setForce(IOUtil::argToBool($input->getOption('force')))
+ ->setSkipExisting(IOUtil::argToBool($input->getOption('skip-existing')))
+ ->setMoveExistingTo(IOUtil::argToString($input->getOption('move-existing-to')))
+ ->setOnlyEnabled(IOUtil::argToBool($input->getOption('only-enabled')))
+ ->run();
+
+ return 0;
+ } catch (Exception $e) {
+ return $this->crash($output, $e);
+ }
+ }
+
+ /**
+ * Create the template to generate the hook source code
+ *
+ * @param \CaptainHook\App\Config $config
+ * @param \SebastianFeldmann\Git\Repository $repo
+ * @return \CaptainHook\App\Hook\Template
+ */
+ private function createTemplate(Config $config, Repository $repo): Template
+ {
+ if (
+ $config->getRunConfig()->getMode() === Template::DOCKER
+ && empty($config->getRunConfig()->getDockerCommand())
+ ) {
+ throw new RuntimeException('Run "exec" option missing for run-mode docker.');
+ }
+
+ return Template\Builder::build($config, $repo, $this->resolver);
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/Command/RepositoryAware.php b/lib/captainhook/captainhook/src/Console/Command/RepositoryAware.php
new file mode 100644
index 0000000000..af11cf740b
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/Command/RepositoryAware.php
@@ -0,0 +1,54 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console\Command;
+
+use CaptainHook\App\Console\Runtime\Resolver;
+use SebastianFeldmann\Git\Repository;
+use Symfony\Component\Console\Input\InputOption;
+
+/**
+ * Trait RepositoryAware
+ *
+ * Trait for all commands that needs to be aware of the git repository.
+ *
+ * @package CaptainHook\App\Console\Command
+ */
+class RepositoryAware extends ConfigAware
+{
+ /**
+ * Configure method to set up the git-directory command option
+ *
+ * @return void
+ */
+ protected function configure(): void
+ {
+ parent::configure();
+
+ $this->addOption(
+ 'git-directory',
+ 'g',
+ InputOption::VALUE_OPTIONAL,
+ 'Path to your .git directory'
+ );
+ }
+
+ /**
+ * Create a new git repository representation
+ *
+ * @param string $path
+ * @return \SebastianFeldmann\Git\Repository
+ */
+ protected function createRepository(string $path): Repository
+ {
+ return Repository::createVerified($path);
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/Command/Uninstall.php b/lib/captainhook/captainhook/src/Console/Command/Uninstall.php
new file mode 100644
index 0000000000..b6a421ad07
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/Command/Uninstall.php
@@ -0,0 +1,100 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console\Command;
+
+use CaptainHook\App\Console\IOUtil;
+use CaptainHook\App\Runner\Uninstaller;
+use Exception;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Class Uninstall
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.17.0
+ */
+class Uninstall extends RepositoryAware
+{
+ /**
+ * Configure the command
+ *
+ * @return void
+ */
+ protected function configure(): void
+ {
+ parent::configure();
+ $this->setName('uninstall')
+ ->setDescription('Remove all git hooks from your .git/hooks directory')
+ ->setHelp('Remove all git hooks from your .git/hooks directory')
+ ->addArgument(
+ 'hook',
+ InputArgument::OPTIONAL,
+ 'Remove only this one hook. By default all hooks get uninstalled'
+ )
+ ->addOption(
+ 'force',
+ 'f',
+ InputOption::VALUE_NONE,
+ 'Force install without confirmation'
+ )
+ ->addOption(
+ 'only-disabled',
+ null,
+ InputOption::VALUE_NONE,
+ 'Limit the hooks you want to remove to those that are not enabled in your conf. ' .
+ 'By default all hooks get uninstalled'
+ )
+ ->addOption(
+ 'move-existing-to',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ 'Move existing hooks to this directory'
+ );
+ }
+
+ /**
+ * Execute the command
+ *
+ * @param \Symfony\Component\Console\Input\InputInterface $input
+ * @param \Symfony\Component\Console\Output\OutputInterface $output
+ * @return int
+ * @throws \Exception
+ */
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $io = $this->getIO($input, $output);
+
+ try {
+ $config = $this->createConfig($input, true, ['git-directory']);
+ $repo = $this->createRepository(dirname($config->getGitDirectory()));
+
+ $this->determineVerbosity($output, $config);
+
+ $uninstaller = new Uninstaller($io, $config, $repo);
+ $uninstaller->setHook(IOUtil::argToString($input->getArgument('hook')))
+ ->setForce(IOUtil::argToBool($input->getOption('force')))
+ ->setOnlyDisabled(IOUtil::argToBool($input->getOption('only-disabled')))
+ ->setMoveExistingTo(IOUtil::argToString($input->getOption('move-existing-to')))
+ ->run();
+
+ return 0;
+ } catch (Exception $e) {
+ $this->crash($output, $e);
+ return 1;
+ }
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/IO.php b/lib/captainhook/captainhook/src/Console/IO.php
new file mode 100644
index 0000000000..7ce7b13782
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/IO.php
@@ -0,0 +1,139 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console;
+
+/**
+ * Interface IO
+ *
+ * @package CaptainHook
+ * @author Nils Adermann
+ * @author Jordi Boggiano
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 0.9.0
+ */
+interface IO
+{
+ public const QUIET = 1;
+ public const NORMAL = 2;
+ public const VERBOSE = 4;
+ public const VERY_VERBOSE = 8;
+ public const DEBUG = 16;
+
+ /**
+ * Return the original cli arguments
+ *
+ * @return array
+ */
+ public function getArguments(): array;
+
+ /**
+ * Return the original cli argument or a given default
+ *
+ * @param string $name
+ * @param string $default
+ * @return string
+ */
+ public function getArgument(string $name, string $default = ''): string;
+
+ /**
+ * Returns the piped in standard input
+ *
+ * @return string[]
+ */
+ public function getStandardInput(): array;
+
+ /**
+ * Is this input interactive?
+ *
+ * @return bool
+ */
+ public function isInteractive();
+
+ /**
+ * Is this output verbose?
+ *
+ * @return bool
+ */
+ public function isVerbose();
+
+ /**
+ * Is the output very verbose?
+ *
+ * @return bool
+ */
+ public function isVeryVerbose();
+
+ /**
+ * Is the output in debug verbosity?
+ *
+ * @return bool
+ */
+ public function isDebug();
+
+ /**
+ * Writes a message to the output
+ *
+ * @param string|array $messages The message as an array of lines or a single string
+ * @param bool $newline Whether to add a newline or not
+ * @param int $verbosity Verbosity level from the VERBOSITY_* constants
+ * @return void
+ */
+ public function write($messages, $newline = true, $verbosity = self::NORMAL);
+
+ /**
+ * Writes a message to the error output
+ *
+ * @param string|array $messages The message as an array of lines or a single string
+ * @param bool $newline Whether to add a newline or not
+ * @param int $verbosity Verbosity level from the VERBOSITY_* constants
+ * @return void
+ */
+ public function writeError($messages, $newline = true, $verbosity = self::NORMAL);
+
+ /**
+ * Asks a question to the user
+ *
+ * @param string $question The question to ask
+ * @param string $default The default answer if none is given by the user
+ * @throws \RuntimeException If there is no data to read in the input stream
+ * @return string The user answer
+ */
+ public function ask($question, $default = null);
+
+ /**
+ * Asks a confirmation to the user
+ *
+ * The question will be asked until the user answers by nothing, yes, or no.
+ *
+ * @param string $question The question to ask
+ * @param bool $default The default answer if the user enters nothing
+ * @return bool true if the user has confirmed, false otherwise
+ */
+ public function askConfirmation($question, $default = true);
+
+ /**
+ * Asks for a value and validates the response
+ *
+ * The validator receives the data to validate. It must return the
+ * validated data when the data is valid and throw an exception
+ * otherwise.
+ *
+ * @param string $question The question to ask
+ * @param callable $validator A PHP callback
+ * @param int $attempts Max number of times to ask before giving up (default of null means infinite)
+ * @param mixed $default The default answer if none is given by the user
+ * @throws \Exception When any of the validators return an error
+ * @return mixed
+ */
+ public function askAndValidate($question, $validator, $attempts = null, $default = null);
+}
diff --git a/lib/captainhook/captainhook/src/Console/IO/Base.php b/lib/captainhook/captainhook/src/Console/IO/Base.php
new file mode 100644
index 0000000000..75b74149ed
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/IO/Base.php
@@ -0,0 +1,57 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console\IO;
+
+use CaptainHook\App\Console\IO;
+
+/**
+ * Class Base
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 0.9.0
+ */
+abstract class Base implements IO
+{
+ /**
+ * Return the original cli arguments
+ *
+ * @return array
+ */
+ public function getArguments(): array
+ {
+ return [];
+ }
+
+ /**
+ * Return the original cli argument or a given default
+ *
+ * @param string $name
+ * @param string $default
+ * @return string
+ */
+ public function getArgument(string $name, string $default = ''): string
+ {
+ return $default;
+ }
+
+ /**
+ * Return the piped in standard input
+ *
+ * @return string[]
+ */
+ public function getStandardInput(): array
+ {
+ return [];
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/IO/CollectorIO.php b/lib/captainhook/captainhook/src/Console/IO/CollectorIO.php
new file mode 100644
index 0000000000..2dfec2b31d
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/IO/CollectorIO.php
@@ -0,0 +1,151 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console\IO;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Console\IOUtil;
+use SebastianFeldmann\Cli\Reader\StandardInput;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Helper\HelperSet;
+use Symfony\Component\Console\Output\ConsoleOutputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\ConfirmationQuestion;
+use Symfony\Component\Console\Question\Question;
+
+/**
+ * Class CollectorIO
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.19.0
+ */
+class CollectorIO implements IO
+{
+ /**
+ * @var \CaptainHook\App\Console\IO
+ */
+ private IO $io;
+
+ /**
+ * @var array<\CaptainHook\App\Console\IO\Message>
+ */
+ private array $messages = [];
+
+ /**
+ * Constructor
+ *
+ */
+ public function __construct(IO $io)
+ {
+ $this->io = $io;
+ }
+
+
+ /**
+ *
+ * @param string $messages
+ * @param bool $newline
+ * @param int $verbosity
+ * @return void
+ */
+ public function write($messages, $newline = true, $verbosity = IO::NORMAL)
+ {
+ $this->messages[] = new Message($messages, $newline, $verbosity);
+ }
+
+ /**
+ * @param string $messages
+ * @param bool $newline
+ * @param int $verbosity
+ * @return void
+ */
+ public function writeError($messages, $newline = true, $verbosity = IO::NORMAL)
+ {
+ $this->messages[] = new Message($messages, $newline, $verbosity);
+ }
+
+ /**
+ * @return array<\CaptainHook\App\Console\IO\Message>
+ */
+ public function getMessages(): array
+ {
+ return $this->messages;
+ }
+
+ /**
+ * Return the original cli arguments
+ *
+ * @return array
+ */
+ public function getArguments(): array
+ {
+ return $this->io->getArguments();
+ }
+
+ /**
+ * Return the original cli argument or a given default
+ *
+ * @param string $name
+ * @param string $default
+ * @return string
+ */
+ public function getArgument(string $name, string $default = ''): string
+ {
+ return $this->io->getArgument($name, $default);
+ }
+
+ /**
+ * Return the piped in standard input
+ *
+ * @return string[]
+ */
+ public function getStandardInput(): array
+ {
+ return $this->io->getStandardInput();
+ }
+
+ public function isInteractive()
+ {
+ return $this->io->isInteractive();
+ }
+
+ public function isVerbose()
+ {
+ return $this->io->isVerbose();
+ }
+
+ public function isVeryVerbose()
+ {
+ return $this->io->isVeryVerbose();
+ }
+
+ public function isDebug()
+ {
+ return $this->io->isDebug();
+ }
+
+ public function ask($question, $default = null)
+ {
+ return $this->io->ask($question, $default);
+ }
+
+ public function askConfirmation($question, $default = true)
+ {
+ return $this->io->askConfirmation($question, $default);
+ }
+
+ public function askAndValidate($question, $validator, $attempts = null, $default = null)
+ {
+ return $this->io->askAndValidate($question, $validator, $attempts, $default);
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/IO/ComposerIO.php b/lib/captainhook/captainhook/src/Console/IO/ComposerIO.php
new file mode 100644
index 0000000000..1fa8162bc6
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/IO/ComposerIO.php
@@ -0,0 +1,112 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console\IO;
+
+use Composer\IO\IOInterface;
+
+/**
+ * Class ComposerIO
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 0.9.0
+ */
+class ComposerIO extends Base
+{
+ /**
+ * @var \Composer\IO\IOInterface
+ */
+ private $io;
+
+ /**
+ * ComposerIO constructor
+ *
+ * @param \Composer\IO\IOInterface $io
+ */
+ public function __construct(IOInterface $io)
+ {
+ $this->io = $io;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isInteractive()
+ {
+ return $this->io->isInteractive();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isVerbose()
+ {
+ return $this->io->isVerbose();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isVeryVerbose()
+ {
+ return $this->io->isVeryVerbose();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isDebug()
+ {
+ return $this->io->isDebug();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function write($messages, $newline = true, $verbosity = self::NORMAL)
+ {
+ $this->io->write($messages, $newline, $verbosity);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function writeError($messages, $newline = true, $verbosity = self::NORMAL)
+ {
+ $this->io->writeError($messages, $newline, $verbosity);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function ask($question, $default = null)
+ {
+ return $this->io->ask($question, $default);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function askConfirmation($question, $default = true)
+ {
+ return $this->io->askConfirmation($question, $default);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function askAndValidate($question, $validator, $attempts = null, $default = null)
+ {
+ return $this->io->askAndValidate($question, $validator, $attempts, $default);
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/IO/DefaultIO.php b/lib/captainhook/captainhook/src/Console/IO/DefaultIO.php
new file mode 100644
index 0000000000..7fc3c44775
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/IO/DefaultIO.php
@@ -0,0 +1,246 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console\IO;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Console\IOUtil;
+use RuntimeException;
+use SebastianFeldmann\Cli\Reader\StandardInput;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Helper\HelperSet;
+use Symfony\Component\Console\Output\ConsoleOutputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\ConfirmationQuestion;
+use Symfony\Component\Console\Question\Question;
+
+/**
+ * Class DefaultIO
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 0.9.0
+ */
+class DefaultIO extends Base
+{
+ /**
+ * Contents of the STDIN
+ *
+ * @var array
+ */
+ private array $stdIn = [];
+
+ /**
+ * @var \Symfony\Component\Console\Input\InputInterface
+ */
+ protected InputInterface $input;
+
+ /**
+ * @var \Symfony\Component\Console\Output\OutputInterface
+ */
+ protected OutputInterface $output;
+
+ /**
+ * @var \Symfony\Component\Console\Helper\HelperSet|null
+ */
+ protected ?HelperSet $helperSet;
+
+ /**
+ * @var array
+ */
+ private array $verbosityMap;
+
+ /**
+ * Constructor
+ *
+ * @param \Symfony\Component\Console\Input\InputInterface $input
+ * @param \Symfony\Component\Console\Output\OutputInterface $output
+ * @param \Symfony\Component\Console\Helper\HelperSet|null $helperSet
+ */
+ public function __construct(InputInterface $input, OutputInterface $output, ?HelperSet $helperSet = null)
+ {
+ $this->input = $input;
+ $this->output = $output;
+ $this->helperSet = $helperSet;
+ $this->verbosityMap = [
+ IO::QUIET => OutputInterface::VERBOSITY_QUIET,
+ IO::NORMAL => OutputInterface::VERBOSITY_NORMAL,
+ IO::VERBOSE => OutputInterface::VERBOSITY_VERBOSE,
+ IO::VERY_VERBOSE => OutputInterface::VERBOSITY_VERY_VERBOSE,
+ IO::DEBUG => OutputInterface::VERBOSITY_DEBUG
+ ];
+ }
+
+ /**
+ * Return the original cli arguments
+ *
+ * @return array
+ */
+ public function getArguments(): array
+ {
+ return $this->input->getArguments();
+ }
+
+ /**
+ * Return the original cli argument or a given default
+ *
+ * @param string $name
+ * @param string $default
+ * @return string
+ */
+ public function getArgument(string $name, string $default = ''): string
+ {
+ return (string)($this->getArguments()[$name] ?? $default);
+ }
+
+ /**
+ * Return the piped in standard input
+ *
+ * @return string[]
+ */
+ public function getStandardInput(): array
+ {
+ if (empty($this->stdIn)) {
+ $this->stdIn = explode(PHP_EOL, trim($this->input->getOption('input'), '"'));
+ }
+ return $this->stdIn;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isInteractive()
+ {
+ return $this->input->isInteractive();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isVerbose()
+ {
+ return $this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isVeryVerbose()
+ {
+ return $this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isDebug()
+ {
+ return $this->output->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function write($messages, $newline = true, $verbosity = self::NORMAL)
+ {
+ $this->doWrite($messages, $newline, false, $verbosity);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function writeError($messages, $newline = true, $verbosity = self::NORMAL)
+ {
+ $this->doWrite($messages, $newline, true, $verbosity);
+ }
+
+ /**
+ * Write to the appropriate user output
+ *
+ * @param array|string $messages
+ * @param bool $newline
+ * @param bool $stderr
+ * @param int $verbosity
+ * @return void
+ */
+ private function doWrite($messages, $newline, $stderr, $verbosity)
+ {
+ $sfVerbosity = $this->verbosityMap[$verbosity];
+ if ($sfVerbosity > $this->output->getVerbosity()) {
+ return;
+ }
+
+ $this->getOutputToWriteTo($stderr)->write($messages, $newline, $sfVerbosity);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function ask($question, $default = null)
+ {
+ if ($this->helperSet === null) {
+ throw new RuntimeException('You must set the helperSet before asking');
+ }
+ /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */
+ $helper = $this->helperSet->get('question');
+ $question = new Question($question, $default);
+
+ return $helper->ask($this->input, $this->getOutputToWriteTo(), $question);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function askConfirmation($question, $default = true)
+ {
+ if ($this->helperSet === null) {
+ throw new RuntimeException('You must set the helperSet before asking');
+ }
+ /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */
+ $helper = $this->helperSet->get('question');
+ $question = new ConfirmationQuestion($question, $default);
+
+ return IOUtil::answerToBool($helper->ask($this->input, $this->getOutputToWriteTo(), $question));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function askAndValidate($question, $validator, $attempts = null, $default = null)
+ {
+ if ($this->helperSet === null) {
+ throw new RuntimeException('You must set the helperSet before asking');
+ }
+ /** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */
+ $helper = $this->helperSet->get('question');
+ $question = new Question($question, $default);
+ $question->setValidator($validator);
+ $question->setMaxAttempts($attempts);
+
+ return $helper->ask($this->input, $this->getOutputToWriteTo(), $question);
+ }
+
+ /**
+ * Return the output to write to
+ *
+ * @param bool $stdErr
+ * @return \Symfony\Component\Console\Output\OutputInterface
+ */
+ private function getOutputToWriteTo($stdErr = false)
+ {
+ if ($stdErr && $this->output instanceof ConsoleOutputInterface) {
+ return $this->output->getErrorOutput();
+ }
+
+ return $this->output;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/IO/Message.php b/lib/captainhook/captainhook/src/Console/IO/Message.php
new file mode 100644
index 0000000000..8bc5ff0470
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/IO/Message.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\Console\IO;
+
+/**
+ * Class Message
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.19.0
+ */
+class Message
+{
+ /**
+ * Message, either a string or list of string for multiple lines
+ *
+ * @var string|array
+ */
+ private string|array $message;
+
+ /**
+ * Should message be ended with a new line character
+ *
+ * @var bool
+ */
+ private bool $newLine;
+
+ /**
+ * Current application verbosity
+ *
+ * @var int
+ */
+ private int $verbosity;
+
+ /**
+ * Constructor
+ *
+ * @param string|array $message
+ * @param bool $newLine
+ * @param int $verbosity
+ */
+ public function __construct(string|array $message, bool $newLine, int $verbosity)
+ {
+ $this->message = $message;
+ $this->newLine = $newLine;
+ $this->verbosity = $verbosity;
+ }
+
+ /**
+ * Returns the message to print
+ *
+ * @return string|array
+ */
+ public function message(): string|array
+ {
+ return $this->message;
+ }
+
+ /**
+ * If true message should end with a new line
+ *
+ * @return bool
+ */
+ public function newLine(): bool
+ {
+ return $this->newLine;
+ }
+
+ /**
+ * Minimum verbosity this message should be displayed at
+ *
+ * @return int
+ */
+ public function verbosity(): int
+ {
+ return $this->verbosity;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/IO/NullIO.php b/lib/captainhook/captainhook/src/Console/IO/NullIO.php
new file mode 100644
index 0000000000..70a70087e8
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/IO/NullIO.php
@@ -0,0 +1,93 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console\IO;
+
+/**
+ * Class NullIO
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 0.9.0
+ */
+class NullIO extends Base
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function isInteractive()
+ {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isVerbose()
+ {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isVeryVerbose()
+ {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isDebug()
+ {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function write($messages, $newline = true, $verbosity = self::NORMAL)
+ {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function writeError($messages, $newline = true, $verbosity = self::NORMAL)
+ {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function ask($question, $default = null)
+ {
+ return (string) $default;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function askConfirmation($question, $default = true)
+ {
+ return $default;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function askAndValidate($question, $validator, $attempts = null, $default = null)
+ {
+ return $default;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/IOUtil.php b/lib/captainhook/captainhook/src/Console/IOUtil.php
new file mode 100644
index 0000000000..324b1e6e82
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/IOUtil.php
@@ -0,0 +1,112 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console;
+
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * IOUtil class
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 0.9.0
+ */
+abstract class IOUtil
+{
+ public const PREFIX_OK = '✔';
+ public const PREFIX_FAIL = '✘>';
+
+ /**
+ * Maps config values to Symfony verbosity values
+ *
+ * @var array
+ */
+ private static array $verbosityMap = [
+ 'quiet' => OutputInterface::VERBOSITY_QUIET,
+ 'normal' => OutputInterface::VERBOSITY_NORMAL,
+ 'verbose' => OutputInterface::VERBOSITY_VERBOSE,
+ 'very-verbose' => OutputInterface::VERBOSITY_VERY_VERBOSE,
+ 'debug' => OutputInterface::VERBOSITY_DEBUG
+ ];
+
+ /**
+ * Return the Symfony verbosity for a given config value
+ *
+ * @param string $verbosity
+ * @return OutputInterface::VERBOSITY_*
+ */
+ public static function mapConfigVerbosity(string $verbosity): int
+ {
+ return self::$verbosityMap[strtolower($verbosity)] ?? OutputInterface::VERBOSITY_NORMAL;
+ }
+
+ /**
+ * Convert a user answer to boolean
+ *
+ * @param string $answer
+ * @return bool
+ */
+ public static function answerToBool(string $answer): bool
+ {
+ return in_array(strtolower($answer), ['y', 'yes', 'ok']);
+ }
+
+ /**
+ * Create formatted cli headline
+ *
+ * ">>>> HEADLINE <<<<"
+ * "==== HEADLINE ===="
+ *
+ * @param string $headline
+ * @param int $length
+ * @param string $pre
+ * @param string $post
+ * @return string
+ */
+ public static function formatHeadline(string $headline, int $length, string $pre = '=', string $post = '='): string
+ {
+ $headlineLength = mb_strlen($headline);
+ if ($headlineLength > ($length - 3)) {
+ return $headline;
+ }
+
+ $prefix = (int) floor(($length - $headlineLength - 2) / 2);
+ $suffix = (int) ceil(($length - $headlineLength - 2) / 2);
+
+ return str_repeat($pre, $prefix) . ' ' . $headline . ' ' . str_repeat($post, $suffix);
+ }
+
+ /**
+ * Convert everything to a string
+ *
+ * @param array|bool|string|null $arg
+ * @param string $default
+ * @return string
+ */
+ public static function argToString(mixed $arg, string $default = ''): string
+ {
+ return is_string($arg) ? $arg : $default;
+ }
+
+ /**
+ * Convert everything to a boolean
+ *
+ * @param array|bool|string|null $arg
+ * @param bool $default
+ * @return bool
+ */
+ public static function argToBool(mixed $arg, bool $default = false): bool
+ {
+ return is_bool($arg) ? $arg : $default;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Console/Runtime/Resolver.php b/lib/captainhook/captainhook/src/Console/Runtime/Resolver.php
new file mode 100644
index 0000000000..d264746f91
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Console/Runtime/Resolver.php
@@ -0,0 +1,64 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Console\Runtime;
+
+/**
+ * Class Resolver
+ *
+ * @package CaptainHook\App
+ */
+class Resolver
+{
+ /**
+ * PHAR flag, replaced by box during PHAR building
+ *
+ * @var string
+ */
+ private string $runtime = '@runtime@';
+
+ /**
+ * Path to the currently executed 'binary'
+ *
+ * @var string
+ */
+ private string $executable;
+
+ /**
+ * Resolver constructor.
+ *
+ * @param string $executable
+ */
+ public function __construct(string $executable = 'bin/vendor/captainhook')
+ {
+ $this->executable = $executable;
+ }
+
+ /**
+ * Return current executed 'binary'
+ *
+ * @return string
+ */
+ public function getExecutable(): string
+ {
+ return $this->executable;
+ }
+
+ /**
+ * Check if the current runtime is executed via PHAR
+ *
+ * @return bool
+ */
+ public function isPharRelease(): bool
+ {
+ return $this->runtime === 'PHAR';
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Event.php b/lib/captainhook/captainhook/src/Event.php
new file mode 100644
index 0000000000..e41e4b2122
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Event.php
@@ -0,0 +1,56 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App;
+
+use CaptainHook\App\Console\IO;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Event interface
+ *
+ * Allows event handlers to do output access the app setup or the repository.
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.11.0
+ */
+interface Event
+{
+ /**
+ * Returns the event trigger name
+ *
+ * @return string
+ */
+ public function name(): string;
+
+ /**
+ * Returns the captainhook config, most likely needed to access any original command line arguments
+ *
+ * @return \CaptainHook\App\Config
+ */
+ public function config(): Config;
+
+ /**
+ * Returns IO to do some output
+ *
+ * @return \CaptainHook\App\Console\IO
+ */
+ public function io(): IO;
+
+ /**
+ * Returns the git repository
+ *
+ * @return \SebastianFeldmann\Git\Repository
+ */
+ public function repository(): Repository;
+}
diff --git a/lib/captainhook/captainhook/src/Event/Dispatcher.php b/lib/captainhook/captainhook/src/Event/Dispatcher.php
new file mode 100644
index 0000000000..28568eef54
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Event/Dispatcher.php
@@ -0,0 +1,109 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Event;
+
+use CaptainHook\App\Config;
+use CaptainHook\App\Console\IO;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Event Dispatcher
+ *
+ * This allows the user to hook into the Cap'n on a deeper level. For example execute code if the hook execution fails.
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.11.0
+ */
+class Dispatcher
+{
+ /**
+ * List of all registered handlers
+ *
+ * @var array>
+ */
+ private $config = [];
+
+ /**
+ * Event factory to create all necessary events
+ *
+ * @var \CaptainHook\App\Event\Factory
+ */
+ private $factory;
+
+ /**
+ * Event Dispatcher
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \CaptainHook\App\Config $config
+ * @param \SebastianFeldmann\Git\Repository $repository
+ */
+ public function __construct(IO $io, Config $config, Repository $repository)
+ {
+ $this->factory = new Factory($io, $config, $repository);
+ }
+
+ /**
+ * Register handlers received from a Listener to the dispatcher
+ *
+ * @param array> $eventConfig
+ * @return void
+ */
+ public function subscribeHandlers(array $eventConfig): void
+ {
+ foreach ($eventConfig as $event => $handlers) {
+ foreach ($handlers as $handler) {
+ $this->subscribeHandlerToEvent($handler, $event);
+ }
+ }
+ }
+
+ /**
+ * Register a single event handler to an event
+ *
+ * @param \CaptainHook\App\Event\Handler $handler
+ * @param string $event
+ * @return void
+ */
+ public function subscribeHandlerToEvent(Handler $handler, string $event): void
+ {
+ $this->config[$event][] = $handler;
+ }
+
+ /**
+ * Trigger all event handlers registered for a given event
+ *
+ * @param string $eventName
+ * @throws \Exception
+ * @return void
+ */
+ public function dispatch(string $eventName): void
+ {
+ $event = $this->factory->createEvent($eventName);
+
+ foreach ($this->handlersFor($event->name()) as $handler) {
+ $handler->handle($event);
+ }
+ }
+
+ /**
+ * Return a list of handlers for a given event
+ *
+ * @param string $event
+ * @return \CaptainHook\App\Event\Handler[];
+ */
+ private function handlersFor(string $event): array
+ {
+ return $this->config[$event] ?? [];
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Event/Factory.php b/lib/captainhook/captainhook/src/Event/Factory.php
new file mode 100644
index 0000000000..3f796aabf0
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Event/Factory.php
@@ -0,0 +1,97 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Event;
+
+use CaptainHook\App\Config;
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Event;
+use RuntimeException;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Event Factory
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.11.0
+ */
+class Factory
+{
+ /**
+ * @var \CaptainHook\App\Config
+ */
+ protected $config;
+
+ /**
+ * @var \CaptainHook\App\Console\IO
+ */
+ private $io;
+
+ /**
+ * @var \SebastianFeldmann\Git\Repository
+ */
+ private $repository;
+
+ /**
+ * List of available events
+ *
+ * @var string[]
+ */
+ private $validEventIDs = [
+ 'onHookFailure' => HookFailed::class,
+ 'onHookSuccess' => HookSucceeded::class,
+ ];
+
+ /**
+ * Event Factory
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \CaptainHook\App\Config $config
+ * @param \SebastianFeldmann\Git\Repository $repository
+ */
+ public function __construct(IO $io, Config $config, Repository $repository)
+ {
+ $this->config = $config;
+ $this->io = $io;
+ $this->repository = $repository;
+ }
+
+ /**
+ * Create a CaptainHook event
+ *
+ * @param string $name
+ * @return \CaptainHook\App\Event
+ */
+ public function createEvent(string $name): Event
+ {
+ if (!$this->isEventIDValid($name)) {
+ throw new RuntimeException('invalid event name: ' . $name);
+ }
+
+ $class = $this->validEventIDs[$name];
+ /** @var \CaptainHook\App\Event $event */
+ $event = new $class($this->io, $this->config, $this->repository);
+ return $event;
+ }
+
+ /**
+ * Validates an event name
+ *
+ * @param string $name
+ * @return bool
+ */
+ public function isEventIDValid(string $name): bool
+ {
+ return array_key_exists($name, $this->validEventIDs);
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Event/Handler.php b/lib/captainhook/captainhook/src/Event/Handler.php
new file mode 100644
index 0000000000..e3d36d7d4c
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Event/Handler.php
@@ -0,0 +1,34 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Event;
+
+use CaptainHook\App\Event;
+
+/**
+ * Interface EventListener
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.11.0
+ */
+interface Handler
+{
+ /**
+ * Executes the handler to handle the given event
+ *
+ * @param \CaptainHook\App\Event $event
+ * @return void
+ * @throws \Exception
+ */
+ public function handle(Event $event);
+}
diff --git a/lib/captainhook/captainhook/src/Event/Hook.php b/lib/captainhook/captainhook/src/Event/Hook.php
new file mode 100644
index 0000000000..7fdcbf8dcb
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Event/Hook.php
@@ -0,0 +1,96 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Event;
+
+use CaptainHook\App\Config;
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Event;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Basic event class
+ *
+ * Makes sure the handler has access to the output the current app setup and the repository.
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.11.0
+ */
+abstract class Hook implements Event
+{
+ /**
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * @var \CaptainHook\App\Console\IO
+ */
+ protected $io;
+
+ /**
+ * @var \CaptainHook\App\Config
+ */
+ protected $config;
+
+ /**
+ * @var \SebastianFeldmann\Git\Repository
+ */
+ protected $repository;
+
+ /**
+ * Event
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \CaptainHook\App\Config $config
+ * @param \SebastianFeldmann\Git\Repository $repository
+ */
+ public function __construct(IO $io, Config $config, Repository $repository)
+ {
+ $this->io = $io;
+ $this->config = $config;
+ $this->repository = $repository;
+ }
+
+ /**
+ * @return string
+ */
+ public function name(): string
+ {
+ return $this->name;
+ }
+
+ /**
+ * @return \CaptainHook\App\Config
+ */
+ public function config(): Config
+ {
+ return $this->config;
+ }
+
+ /**
+ * @return \CaptainHook\App\Console\IO
+ */
+ public function io(): IO
+ {
+ return $this->io;
+ }
+
+ /**
+ * @return \SebastianFeldmann\Git\Repository
+ */
+ public function repository(): Repository
+ {
+ return $this->repository;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Event/HookFailed.php b/lib/captainhook/captainhook/src/Event/HookFailed.php
new file mode 100644
index 0000000000..366d014555
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Event/HookFailed.php
@@ -0,0 +1,25 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Event;
+
+/**
+ * Hook failed event
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.11.0
+ */
+class HookFailed extends Hook
+{
+ protected $name = 'onHookFailure';
+}
diff --git a/lib/captainhook/captainhook/src/Event/HookSucceeded.php b/lib/captainhook/captainhook/src/Event/HookSucceeded.php
new file mode 100644
index 0000000000..182a748bfe
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Event/HookSucceeded.php
@@ -0,0 +1,25 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Event;
+
+/**
+ * Hook succeeded event
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.11.0
+ */
+class HookSucceeded extends Hook
+{
+ protected $name = 'onHookSuccess';
+}
diff --git a/lib/captainhook/captainhook/src/Exception/ActionFailed.php b/lib/captainhook/captainhook/src/Exception/ActionFailed.php
new file mode 100644
index 0000000000..4273867c6f
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Exception/ActionFailed.php
@@ -0,0 +1,26 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Exception;
+
+use Exception;
+
+/**
+ * Class ActionFailed
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 0.9.0
+ */
+class ActionFailed extends Exception implements CaptainHookException
+{
+}
diff --git a/lib/captainhook/captainhook/src/Exception/CaptainHookException.php b/lib/captainhook/captainhook/src/Exception/CaptainHookException.php
new file mode 100644
index 0000000000..ceebf6c4d5
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Exception/CaptainHookException.php
@@ -0,0 +1,26 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Exception;
+
+use Throwable;
+
+/**
+ * CaptainHookException interface
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.9.0.
+ */
+interface CaptainHookException extends Throwable
+{
+}
diff --git a/lib/captainhook/captainhook/src/Exception/InvalidHookName.php b/lib/captainhook/captainhook/src/Exception/InvalidHookName.php
new file mode 100644
index 0000000000..e5b4c5b24e
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Exception/InvalidHookName.php
@@ -0,0 +1,26 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Exception;
+
+use Exception;
+
+/**
+ * Class InvalidHookName
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 0.9.0
+ */
+class InvalidHookName extends Exception implements CaptainHookException
+{
+}
diff --git a/lib/captainhook/captainhook/src/Exception/InvalidPlugin.php b/lib/captainhook/captainhook/src/Exception/InvalidPlugin.php
new file mode 100644
index 0000000000..61ba03ca12
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Exception/InvalidPlugin.php
@@ -0,0 +1,26 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Exception;
+
+use RuntimeException;
+
+/**
+ * Class InvalidPlugin
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.9.0
+ */
+class InvalidPlugin extends RuntimeException implements CaptainHookException
+{
+}
diff --git a/lib/captainhook/captainhook/src/Git/ChangedFiles.php b/lib/captainhook/captainhook/src/Git/ChangedFiles.php
new file mode 100644
index 0000000000..c42559c94b
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Git/ChangedFiles.php
@@ -0,0 +1,62 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Git;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Git\ChangedFiles\Detector\Factory;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Class ChangedFiles
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.2.0
+ */
+abstract class ChangedFiles
+{
+ /**
+ * Returns the list of changed files using the necessary Detector
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @param array $filter
+ * @return array
+ */
+ public static function getChangedFiles(IO $io, Repository $repository, array $filter): array
+ {
+ $factory = new Factory();
+ $detector = $factory->getDetector($io, $repository);
+
+ $files = $detector->getChangedFiles($filter);
+ self::displayFilesFound($io, $files);
+ return $files;
+ }
+
+ /**
+ * Output the changed files in verbose mode
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param array $files
+ * @return void
+ */
+ private static function displayFilesFound(IO $io, array $files): void
+ {
+ if ($io->isVerbose()) {
+ $io->write(' Changed files', true, IO::VERBOSE);
+ foreach ($files as $file) {
+ $io->write(' - ' . $file, true, IO::VERBOSE);
+ }
+ }
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Git/ChangedFiles/Detecting.php b/lib/captainhook/captainhook/src/Git/ChangedFiles/Detecting.php
new file mode 100644
index 0000000000..26eeeec08d
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Git/ChangedFiles/Detecting.php
@@ -0,0 +1,33 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Git\ChangedFiles;
+
+/**
+ * Detector interface
+ *
+ * Interface to detect changed files for the different hooks.
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.20.0
+ */
+interface Detecting
+{
+ /**
+ * Returns a list of changed files
+ *
+ * @param array $filter
+ * @return array
+ */
+ public function getChangedFiles(array $filter = []): array;
+}
diff --git a/lib/captainhook/captainhook/src/Git/ChangedFiles/Detector.php b/lib/captainhook/captainhook/src/Git/ChangedFiles/Detector.php
new file mode 100644
index 0000000000..e3d62911cf
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Git/ChangedFiles/Detector.php
@@ -0,0 +1,62 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Git\ChangedFiles;
+
+use CaptainHook\App\Console\IO;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Class Detector
+ *
+ * Base class for all ChangedFiles Detecting implementations.
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.20.0
+ */
+abstract class Detector implements Detecting
+{
+ /**
+ * Input output handling
+ *
+ * @var \CaptainHook\App\Console\IO
+ */
+ protected IO $io;
+
+ /**
+ * Git repository
+ *
+ * @var \SebastianFeldmann\Git\Repository
+ */
+ protected Repository $repository;
+
+ /**
+ * Constructor
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ */
+ public function __construct(IO $io, Repository $repository)
+ {
+ $this->io = $io;
+ $this->repository = $repository;
+ }
+
+ /**
+ * Returns a list of changed files
+ *
+ * @param array $filter
+ * @return array
+ */
+ abstract public function getChangedFiles(array $filter = []): array;
+}
diff --git a/lib/captainhook/captainhook/src/Git/ChangedFiles/Detector/Factory.php b/lib/captainhook/captainhook/src/Git/ChangedFiles/Detector/Factory.php
new file mode 100644
index 0000000000..4932f7f74c
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Git/ChangedFiles/Detector/Factory.php
@@ -0,0 +1,56 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Git\ChangedFiles\Detector;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Git\ChangedFiles\Detecting;
+use CaptainHook\App\Hooks;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Factory class
+ *
+ * Responsible for finding the previous - current ranges in every scenario
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.15.0
+ */
+class Factory
+{
+ /**
+ * List of available range detectors
+ *
+ * @var array
+ */
+ private static array $detectors = [
+ 'hook:pre-push' => '\\CaptainHook\\App\\Git\\ChangedFiles\\Detector\\PrePush',
+ 'hook:post-rewrite' => '\\CaptainHook\\App\\Git\\ChangedFiles\\Detector\\PostRewrite',
+ ];
+
+ /**
+ * Returns a ChangedFiles Detector
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return \CaptainHook\App\Git\ChangedFiles\Detecting
+ */
+ public function getDetector(IO $io, Repository $repository): Detecting
+ {
+ $command = $io->getArgument(Hooks::ARG_COMMAND);
+
+ /** @var \CaptainHook\App\Git\ChangedFiles\Detecting $class */
+ $class = self::$detectors[$command] ?? '\\CaptainHook\\App\\Git\\ChangedFiles\\Detector\\Fallback';
+ return new $class($io, $repository);
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Git/ChangedFiles/Detector/Fallback.php b/lib/captainhook/captainhook/src/Git/ChangedFiles/Detector/Fallback.php
new file mode 100644
index 0000000000..6405d7a4f5
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Git/ChangedFiles/Detector/Fallback.php
@@ -0,0 +1,44 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Git\ChangedFiles\Detector;
+
+use CaptainHook\App\Git\ChangedFiles\Detector;
+use CaptainHook\App\Hooks;
+
+/**
+ * Class Fallback
+ *
+ * This class should not be used it is just a fallback if the `pre-push` or `post-rewrite`
+ * variants are somehow not applicable.
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.20.0
+ */
+class Fallback extends Detector
+{
+ /**
+ * Returns the list of changed files in a best-guess kind of way
+ *
+ * @param array $filter
+ * @return array
+ */
+ public function getChangedFiles(array $filter = []): array
+ {
+ return $this->repository->getDiffOperator()->getChangedFiles(
+ $this->io->getArgument(Hooks::ARG_PREVIOUS_HEAD, 'HEAD@{1}'),
+ 'HEAD',
+ $filter
+ );
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Git/ChangedFiles/Detector/PostRewrite.php b/lib/captainhook/captainhook/src/Git/ChangedFiles/Detector/PostRewrite.php
new file mode 100644
index 0000000000..fac02877da
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Git/ChangedFiles/Detector/PostRewrite.php
@@ -0,0 +1,42 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Git\ChangedFiles\Detector;
+
+use CaptainHook\App\Git\ChangedFiles\Detector;
+use CaptainHook\App\Git\Range\Detector\PostRewrite as RangeDetector;
+
+/**
+ * Class PostRewrite
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.20.0
+ */
+class PostRewrite extends Detector
+{
+ /**
+ * Returns a list of changed files
+ *
+ * @param array $filter
+ * @return array
+ */
+ public function getChangedFiles(array $filter = []): array
+ {
+ $detector = new RangeDetector();
+ $ranges = $detector->getRanges($this->io);
+ $old = $ranges[0]->from()->id();
+ $new = $ranges[0]->to()->id();
+
+ return $this->repository->getDiffOperator()->getChangedFiles($old, $new, $filter);
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Git/ChangedFiles/Detector/PrePush.php b/lib/captainhook/captainhook/src/Git/ChangedFiles/Detector/PrePush.php
new file mode 100644
index 0000000000..44f10af888
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Git/ChangedFiles/Detector/PrePush.php
@@ -0,0 +1,122 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Git\ChangedFiles\Detector;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Git\ChangedFiles\Detector;
+use CaptainHook\App\Git\Range\Detector\PrePush as RangeDetector;
+use CaptainHook\App\Git\Range\PrePush as Range;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Class PrePush
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.20.0
+ */
+class PrePush extends Detector
+{
+ /**
+ * Reflog fallback switch
+ *
+ * @var bool
+ */
+ private bool $reflogFallback = false;
+
+ /**
+ * Activate the reflog fallback file detection
+ *
+ * @param bool $bool
+ * @return void
+ */
+ public function useReflogFallback(bool $bool): void
+ {
+ $this->reflogFallback = $bool;
+ }
+
+ /**
+ * Return list of changed files
+ *
+ * @param array $filter
+ * @return array
+ */
+ public function getChangedFiles(array $filter = []): array
+ {
+ $ranges = $this->getRanges();
+ if (empty($ranges)) {
+ return [];
+ }
+ $files = $this->collectChangedFiles($ranges, $filter);
+ if (count($files) > 0 || !$this->reflogFallback) {
+ return $files;
+ }
+ // by now we should have found something, but if the "branch: created" entry is gone from the reflog,
+ // try to find as many commits belonging to this branch as possible
+ $branch = $ranges[0]->to()->branch();
+ $revisions = $this->repository->getLogOperator()->getBranchRevsFromRefLog($branch);
+ return $this->repository->getLogOperator()->getChangedFilesInRevisions($revisions);
+ }
+
+ /**
+ * Create ranges from stdIn
+ *
+ * @return array<\CaptainHook\App\Git\Range\PrePush>
+ */
+ private function getRanges(): array
+ {
+ $detector = new RangeDetector();
+ return $detector->getRanges($this->io);
+ }
+
+ /**
+ * Collect all changed files from all ranges
+ *
+ * @param array<\CaptainHook\App\Git\Range\PrePush> $ranges
+ * @param array $filter
+ * @return array
+ */
+ private function collectChangedFiles(array $ranges, array $filter): array
+ {
+ $files = [];
+ foreach ($ranges as $range) {
+ if ($this->isKnownBranch($range)) {
+ $oldHash = $range->from()->id();
+ $newHash = $range->to()->id();
+ } else {
+ if (!$this->reflogFallback) {
+ continue;
+ }
+ // remote branch does not exist
+ // try to find the branch starting point with the reflog
+ $oldHash = $this->repository->getLogOperator()->getBranchRevFromRefLog($range->to()->branch());
+ $newHash = 'HEAD';
+ }
+ if (!empty($oldHash)) {
+ $files[] = $this->repository->getDiffOperator()->getChangedFiles($oldHash, $newHash, $filter);
+ }
+ }
+ return array_unique(array_merge(...$files));
+ }
+
+ /**
+ * If the remote branch is known the diff can be easily determined
+ *
+ * @param \CaptainHook\App\Git\Range\PrePush $range
+ * @return bool
+ */
+ private function isKnownBranch(Range $range): bool
+ {
+ return !$range->from()->isZeroRev();
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Git/Diff/FilterUtil.php b/lib/captainhook/captainhook/src/Git/Diff/FilterUtil.php
new file mode 100644
index 0000000000..6afc61f5cb
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Git/Diff/FilterUtil.php
@@ -0,0 +1,39 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Git\Diff;
+
+abstract class FilterUtil
+{
+ /**
+ * Converts a value into a valid diff filter array
+ *
+ * @param mixed $value
+ * @return array
+ */
+ public static function filterFromConfigValue($value): array
+ {
+ return self::sanitize(
+ is_array($value) ? $value : str_split((string) strtoupper($value === null ? '' : $value))
+ );
+ }
+
+ /**
+ * Remove all invalid filter options
+ *
+ * @param array $data
+ * @return array
+ */
+ public static function sanitize(array $data): array
+ {
+ return array_filter($data, fn($e) => in_array($e, ['A', 'C', 'D', 'M', 'R', 'T', 'U', 'X', 'B', '*']));
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Git/Range.php b/lib/captainhook/captainhook/src/Git/Range.php
new file mode 100644
index 0000000000..a99a61df5b
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Git/Range.php
@@ -0,0 +1,39 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Git;
+
+/**
+ * Range class
+ *
+ * Represents a git range with a starting ref and an end ref.
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.15.0
+ */
+interface Range
+{
+ /**
+ * Returns the start ref
+ *
+ * @return \CaptainHook\App\Git\Rev
+ */
+ public function from(): Rev;
+
+ /**
+ * Returns the end ref
+ *
+ * @return \CaptainHook\App\Git\Rev
+ */
+ public function to(): Rev;
+}
diff --git a/lib/captainhook/captainhook/src/Git/Range/Detecting.php b/lib/captainhook/captainhook/src/Git/Range/Detecting.php
new file mode 100644
index 0000000000..1454d3c5eb
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Git/Range/Detecting.php
@@ -0,0 +1,36 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Git\Range;
+
+use CaptainHook\App\Console\IO;
+
+/**
+ * Detecting interface
+ *
+ * Interface to gathering the previous state to current state ranges.
+ * To handle gathering the ranges for pre-push, post-rewrite, post-checkout separately.
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.15.0
+ */
+interface Detecting
+{
+ /**
+ * Returns a list of ranges marking before and after points to collect the changes happening in between
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @return array<\CaptainHook\App\Git\Range>
+ */
+ public function getRanges(IO $io): array;
+}
diff --git a/lib/captainhook/captainhook/src/Git/Range/Detector/Fallback.php b/lib/captainhook/captainhook/src/Git/Range/Detector/Fallback.php
new file mode 100644
index 0000000000..47caa61613
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Git/Range/Detector/Fallback.php
@@ -0,0 +1,47 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Git\Range\Detector;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Git\Range;
+use CaptainHook\App\Git\Range\Detecting;
+use CaptainHook\App\Git\Rev;
+use CaptainHook\App\Hooks;
+
+/**
+ * Fallback Detector
+ *
+ * If no detection strategy matches the fallback detector is used to find the right range.
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.15.0
+ */
+class Fallback implements Detecting
+{
+ /**
+ * Returns the fallback range
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @return \CaptainHook\App\Git\Range\Generic[]
+ */
+ public function getRanges(IO $io): array
+ {
+ return [
+ new Range\Generic(
+ new Rev\Generic($io->getArgument(Hooks::ARG_PREVIOUS_HEAD, 'HEAD@{1}')),
+ new Rev\Generic('HEAD')
+ )
+ ];
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Git/Range/Detector/PostRewrite.php b/lib/captainhook/captainhook/src/Git/Range/Detector/PostRewrite.php
new file mode 100644
index 0000000000..8324deda18
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Git/Range/Detector/PostRewrite.php
@@ -0,0 +1,59 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Git\Range\Detector;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Git\Range\Detecting;
+use CaptainHook\App\Git\Range\Generic as Range;
+use CaptainHook\App\Git\Rev\Generic as Rev;
+
+/**
+ * Class to access the pre-push stdIn data
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.15.0
+ */
+class PostRewrite implements Detecting
+{
+ /**
+ * Returns list of refs
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @return \CaptainHook\App\Git\Range[]
+ */
+ public function getRanges(IO $io): array
+ {
+ return $this->createFromStdIn($io->getStandardInput());
+ }
+
+ /**
+ * Create ranges from stdIn
+ *
+ * @param array $stdIn
+ * @return array<\CaptainHook\App\Git\Range>
+ */
+ private function createFromStdIn(array $stdIn): array
+ {
+ $ranges = [];
+ foreach ($stdIn as $line) {
+ if (!empty($line)) {
+ $parts = explode(' ', trim($line));
+ $from = new Rev(!empty($parts[1]) ? $parts[1] . '^' : 'HEAD@{1}');
+ $to = new Rev('HEAD');
+ $ranges[] = new Range($from, $to);
+ }
+ }
+ return $ranges;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Git/Range/Detector/PrePush.php b/lib/captainhook/captainhook/src/Git/Range/Detector/PrePush.php
new file mode 100644
index 0000000000..bbacfa5a70
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Git/Range/Detector/PrePush.php
@@ -0,0 +1,63 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Git\Range\Detector;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Git\Range\Detecting;
+use CaptainHook\App\Git\Range\PrePush as Range;
+use CaptainHook\App\Git\Rev\PrePush as Rev;
+use CaptainHook\App\Git\Rev\Util;
+
+/**
+ * Class to access the pre-push stdIn data
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.15.0
+ */
+class PrePush implements Detecting
+{
+ /**
+ * Returns list of refs
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @return array<\CaptainHook\App\Git\Range\PrePush>
+ */
+ public function getRanges(IO $io): array
+ {
+ return $this->createFromStdIn($io->getStandardInput());
+ }
+
+ /**
+ * Factory method
+ *
+ * @param array $stdIn
+ * @return array<\CaptainHook\App\Git\Range\PrePush>
+ */
+ private function createFromStdIn(array $stdIn): array
+ {
+ $ranges = [];
+ foreach ($stdIn as $line) {
+ if (empty($line)) {
+ continue;
+ }
+
+ [$localRef, $localHash, $remoteRef, $remoteHash] = explode(' ', trim($line));
+
+ $from = new Rev($remoteRef, $remoteHash, Util::extractBranchInfo($remoteRef)['branch']);
+ $to = new Rev($localRef, $localHash, Util::extractBranchInfo($localRef)['branch']);
+ $ranges[] = new Range($from, $to);
+ }
+ return $ranges;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Git/Range/Generic.php b/lib/captainhook/captainhook/src/Git/Range/Generic.php
new file mode 100644
index 0000000000..7b012cadca
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Git/Range/Generic.php
@@ -0,0 +1,70 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Git\Range;
+
+use CaptainHook\App\Git\Range;
+use CaptainHook\App\Git\Rev;
+
+/**
+ * Generic range implementation
+ *
+ * Most simple range implementation
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.15.0
+ */
+class Generic implements Range
+{
+ /**
+ * Starting reference
+ *
+ * @var \CaptainHook\App\Git\Rev
+ */
+ private Rev $from;
+
+ /**
+ * Ending reference
+ *
+ * @var \CaptainHook\App\Git\Rev
+ */
+ private Rev $to;
+
+ /**
+ * Constructor
+ *
+ */
+ public function __construct(Rev $from, Rev $to)
+ {
+ $this->from = $from;
+ $this->to = $to;
+ }
+
+ /**
+ * Return the git reference
+ *
+ * @return \CaptainHook\App\Git\Rev
+ */
+ public function from(): Rev
+ {
+ return $this->from;
+ }
+
+ /**
+ * @return \CaptainHook\App\Git\Rev
+ */
+ public function to(): Rev
+ {
+ return $this->to;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Git/Range/PrePush.php b/lib/captainhook/captainhook/src/Git/Range/PrePush.php
new file mode 100644
index 0000000000..3ced2eb527
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Git/Range/PrePush.php
@@ -0,0 +1,68 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Git\Range;
+
+use CaptainHook\App\Git;
+use CaptainHook\App\Git\Rev\PrePush as Rev;
+
+/**
+ * Class
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.15.0
+ */
+class PrePush implements Git\Range
+{
+ /**
+ * @var \CaptainHook\App\Git\Rev\PrePush
+ */
+ private Rev $from;
+
+ /**
+ * @var \CaptainHook\App\Git\Rev\PrePush
+ */
+ private Rev $to;
+
+ /**
+ * Constructor
+ *
+ * @param \CaptainHook\App\Git\Rev\PrePush $from
+ * @param \CaptainHook\App\Git\Rev\PrePush $to
+ */
+ public function __construct(Rev $from, Rev $to)
+ {
+ $this->from = $from;
+ $this->to = $to;
+ }
+
+ /**
+ * Returns the start ref
+ *
+ * @return \CaptainHook\App\Git\Rev\PrePush
+ */
+ public function from(): Rev
+ {
+ return $this->from;
+ }
+
+ /**
+ * Returns the end ref
+ *
+ * @return \CaptainHook\App\Git\Rev\PrePush
+ */
+ public function to(): Rev
+ {
+ return $this->to;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Git/Rev.php b/lib/captainhook/captainhook/src/Git/Rev.php
new file mode 100644
index 0000000000..6732b22be3
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Git/Rev.php
@@ -0,0 +1,35 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Git;
+
+/**
+ * Ref interface
+ *
+ * Git references can be used in git commands to identify positions in the git history.
+ * For example: HEAD, 4FD60E21,
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.15.0
+ */
+interface Rev
+{
+ /**
+ * Returns the ref id that can be used in a git command
+ *
+ * This can be completely a hash, branch name, ref-log position...
+ *
+ * @return string
+ */
+ public function id(): string;
+}
diff --git a/lib/captainhook/captainhook/src/Git/Rev/Generic.php b/lib/captainhook/captainhook/src/Git/Rev/Generic.php
new file mode 100644
index 0000000000..4a6c56966d
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Git/Rev/Generic.php
@@ -0,0 +1,54 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Git\Rev;
+
+use CaptainHook\App\Git\Rev;
+
+/**
+ * Generic range implementation
+ *
+ * The simplest imaginable range implementation without any extra information.
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.15.0
+ */
+class Generic implements Rev
+{
+ /**
+ * Referencing a git state
+ *
+ * @var string
+ */
+ private string $id;
+
+ /**
+ * Constructor
+ *
+ * @param string $id
+ */
+ public function __construct(string $id)
+ {
+ $this->id = $id;
+ }
+
+ /**
+ * Return the git reference
+ *
+ * @return string
+ */
+ public function id(): string
+ {
+ return $this->id;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Git/Rev/PrePush.php b/lib/captainhook/captainhook/src/Git/Rev/PrePush.php
new file mode 100644
index 0000000000..8be6c47b55
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Git/Rev/PrePush.php
@@ -0,0 +1,112 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Git\Rev;
+
+use CaptainHook\App\Git\Rev;
+
+/**
+ * Git pre-push reference
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.15.0
+ */
+class PrePush implements Rev
+{
+ /**
+ * Head path - refs/heads/main
+ *
+ * @var string
+ */
+ private string $head;
+
+ /**
+ * Git hash
+ *
+ * @var string
+ */
+ private string $hash;
+
+ /**
+ * Branch name
+ *
+ * @var string
+ */
+ private string $branch;
+
+ /**
+ * Constructor
+ *
+ * @param string $head
+ * @param string $hash
+ * @param string $branch
+ */
+ public function __construct(string $head, string $hash, string $branch)
+ {
+ $this->head = $head;
+ $this->hash = $hash;
+ $this->branch = $branch;
+ }
+
+ /**
+ * Head getter
+ *
+ * @return string
+ */
+ public function head(): string
+ {
+ return $this->head;
+ }
+
+ /**
+ * Hash getter
+ *
+ * @return string
+ */
+ public function hash(): string
+ {
+ return $this->hash;
+ }
+
+ /**
+ * Branch getter
+ *
+ * @return string
+ */
+ public function branch(): string
+ {
+ return $this->branch;
+ }
+
+ /**
+ * Returns the ref id that can be used in a git command
+ *
+ * This can be completely different thing hash, branch name, ref-log position...
+ *
+ * @return string
+ */
+ public function id(): string
+ {
+ return $this->hash;
+ }
+
+ /**
+ * Is this a git dummy hash
+ *
+ * @return bool
+ */
+ public function isZeroRev(): bool
+ {
+ return Util::isZeroHash($this->hash);
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Git/Rev/Util.php b/lib/captainhook/captainhook/src/Git/Rev/Util.php
new file mode 100644
index 0000000000..41bf754a46
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Git/Rev/Util.php
@@ -0,0 +1,57 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Git\Rev;
+
+/**
+ * Util class
+ *
+ * Does some simple format and validation stuff
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.15.0
+ */
+abstract class Util
+{
+ /**
+ * Indicates if commit hash is a zero hash 0000000000000000000000000000000000000000
+ *
+ * @param string $hash
+ * @return bool
+ */
+ public static function isZeroHash(string $hash): bool
+ {
+ return (bool) preg_match('/^0+$/', $hash);
+ }
+
+ /**
+ * Splits remote and branch
+ *
+ * origin/main => ['remote' => 'origin', 'branch' => 'main']
+ * main => ['remote' => 'origin', 'branch' => 'main']
+ * ref/origin/main => ['remote' => 'origin', 'branch' => 'main']
+ *
+ * @param string $ref
+ * @return array
+ */
+ public static function extractBranchInfo(string $ref): array
+ {
+ $ref = (string) preg_replace('#^refs/#', '', $ref);
+ $parts = explode('/', $ref);
+
+ return [
+ 'remote' => count($parts) > 1 ? array_shift($parts) : 'origin',
+ 'branch' => implode('/', $parts),
+ ];
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Action.php b/lib/captainhook/captainhook/src/Hook/Action.php
new file mode 100644
index 0000000000..4501325b63
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Action.php
@@ -0,0 +1,39 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook;
+
+use CaptainHook\App\Config;
+use CaptainHook\App\Console\IO;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Interface Action
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 0.9.0
+ */
+interface Action
+{
+ /**
+ * Executes 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;
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Branch/Action/BlockFixupAndSquashCommits.php b/lib/captainhook/captainhook/src/Hook/Branch/Action/BlockFixupAndSquashCommits.php
new file mode 100644
index 0000000000..c8f854fe28
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Branch/Action/BlockFixupAndSquashCommits.php
@@ -0,0 +1,231 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook\Branch\Action;
+
+use CaptainHook\App\Config;
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Console\IOUtil;
+use CaptainHook\App\Exception\ActionFailed;
+use CaptainHook\App\Git\Range\Detector\PrePush;
+use CaptainHook\App\Hook\Action;
+use CaptainHook\App\Hook\Restriction;
+use CaptainHook\App\Hooks;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Class BlockFixupAndSquashCommits
+ *
+ * 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:
+ *
+ * {
+ * "action": "\\CaptainHook\\App\\Hook\\Branch\\Action\\BlockFixupAndSquashCommits",
+ * "options": {
+ * "blockSquashCommits": true,
+ * "blockFixupCommits": true,
+ * "protectedBranches": ["main", "master", "integration"]
+ * },
+ * "conditions": []
+ * }
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.11.0
+ */
+class BlockFixupAndSquashCommits implements Action
+{
+ /**
+ * Should fixup! commits be blocked
+ *
+ * @var bool
+ */
+ private bool $blockFixupCommits = true;
+
+ /**
+ * Should squash! commits be blocked
+ *
+ * @var bool
+ */
+ private bool $blockSquashCommits = true;
+
+ /**
+ * List of protected branches
+ *
+ * If not specified all branches are protected
+ *
+ * @var array
+ */
+ private array $protectedBranches;
+
+ /**
+ * Return hook restriction
+ *
+ * @return \CaptainHook\App\Hook\Restriction
+ */
+ public static function getRestriction(): Restriction
+ {
+ return Restriction::fromArray([Hooks::PRE_PUSH]);
+ }
+
+ /**
+ * Execute the BlockFixupAndSquashCommits 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
+ {
+ $rangeDetector = new PrePush();
+ $rangesToPush = $rangeDetector->getRanges($io);
+
+ if (!$this->hasFoundRangesToCheck($rangesToPush)) {
+ return;
+ }
+
+ $this->handleOptions($action->getOptions());
+
+ foreach ($rangesToPush as $range) {
+ if (!empty($this->protectedBranches) && !in_array($range->from()->branch(), $this->protectedBranches)) {
+ return;
+ }
+ $commits = $this->getBlockedCommits($io, $repository, $range->from()->id(), $range->to()->id());
+
+ if (count($commits) > 0) {
+ $this->handleFailure($commits, $range->from()->branch());
+ }
+ }
+ }
+
+ /**
+ * Check if fixup or squash should be blocked
+ *
+ * @param \CaptainHook\App\Config\Options $options
+ * @return void
+ */
+ private function handleOptions(Config\Options $options): void
+ {
+ $this->blockSquashCommits = (bool) $options->get('blockSquashCommits', true);
+ $this->blockFixupCommits = (bool) $options->get('blockFixupCommits', true);
+ $this->protectedBranches = $options->get('protectedBranches', []);
+ }
+
+ /**
+ * Returns a list of commits that should be blocked
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @param string $remoteHash
+ * @param string $localHash
+ * @return array<\SebastianFeldmann\Git\Log\Commit>
+ * @throws \Exception
+ */
+ private function getBlockedCommits(IO $io, Repository $repository, string $remoteHash, string $localHash): array
+ {
+ $typesToCheck = $this->getTypesToBlock();
+ $blocked = [];
+ foreach ($repository->getLogOperator()->getCommitsBetween($remoteHash, $localHash) as $commit) {
+ $prefix = IOUtil::PREFIX_OK;
+ if ($this->hasToBeBlocked($commit->getSubject(), $typesToCheck)) {
+ $prefix = IOUtil::PREFIX_FAIL;
+ $blocked[] = $commit;
+ }
+ $io->write(
+ ' ' . $prefix . ' ' . $commit->getHash() . ' ' . $commit->getSubject(),
+ true,
+ IO::VERBOSE
+ );
+ }
+ return $blocked;
+ }
+
+ /**
+ * Returns a list of strings to look for in commit messages
+ *
+ * Will most likely return ['fixup!', 'squash!']
+ *
+ * @return array
+ */
+ private function getTypesToBlock(): array
+ {
+ $strings = [];
+ if ($this->blockFixupCommits) {
+ $strings[] = 'fixup!';
+ }
+ if ($this->blockSquashCommits) {
+ $strings[] = 'squash!';
+ }
+ return $strings;
+ }
+
+ /**
+ * Checks if the commit message starts with any of the given strings
+ *
+ * @param string $message
+ * @param array $typesToCheck
+ * @return bool
+ */
+ private function hasToBeBlocked(string $message, array $typesToCheck): bool
+ {
+ foreach ($typesToCheck as $type) {
+ if (str_starts_with($message, $type)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Generate a helpful error message and throw the exception
+ *
+ * @param \SebastianFeldmann\Git\Log\Commit[] $commits
+ * @param string $branch
+ * @return void
+ * @throws \CaptainHook\App\Exception\ActionFailed
+ */
+ private function handleFailure(array $commits, string $branch): void
+ {
+ $out = [];
+ foreach ($commits as $commit) {
+ $out[] = ' - ' . $commit->getHash() . ' ' . $commit->getSubject();
+ }
+ throw new ActionFailed(
+ 'You are prohibited to push the following commits:' . PHP_EOL
+ . ' --[ ' . $branch . ' ]-- ' . PHP_EOL
+ . PHP_EOL
+ . implode(PHP_EOL, $out)
+ );
+ }
+
+ /**
+ * Checks if we found valid ranges to check
+ *
+ * @param array<\CaptainHook\App\Git\Range\PrePush> $rangesToPush
+ * @return bool
+ */
+ private function hasFoundRangesToCheck(array $rangesToPush): bool
+ {
+ if (empty($rangesToPush)) {
+ return false;
+ }
+ if ($rangesToPush[0]->from()->isZeroRev() || $rangesToPush[0]->to()->isZeroRev()) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Branch/Action/EnsureNaming.php b/lib/captainhook/captainhook/src/Hook/Branch/Action/EnsureNaming.php
new file mode 100644
index 0000000000..4a80da8185
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Branch/Action/EnsureNaming.php
@@ -0,0 +1,105 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook\Branch\Action;
+
+use CaptainHook\App\Config;
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Exception\ActionFailed;
+use CaptainHook\App\Hook\Action;
+use CaptainHook\App\Hook\Restriction;
+use CaptainHook\App\Hooks;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Class EnsureNaming
+ *
+ * @package CaptainHook
+ * @author Felix Edelmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.4.0
+ */
+class EnsureNaming implements Action
+{
+ /**
+ * Return hook restriction
+ *
+ * @return \CaptainHook\App\Hook\Restriction
+ */
+ public static function getRestriction(): Restriction
+ {
+ return Restriction::fromArray([Hooks::PRE_COMMIT, Hooks::PRE_PUSH, Hooks::POST_CHECKOUT]);
+ }
+
+ /**
+ * Execute the configured 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
+ {
+ $regex = $this->getRegex($action->getOptions());
+ $errorMsg = $this->getErrorMessage($action->getOptions());
+ $successMsg = $this->getSuccessMessage($action->getOptions());
+
+ $branch = $repository->getInfoOperator()->getCurrentBranch();
+ if (!preg_match($regex, $branch)) {
+ throw new ActionFailed(sprintf($errorMsg, $regex));
+ }
+
+ $io->write(['', '', sprintf($successMsg, $regex), ''], true, IO::VERBOSE);
+ }
+
+ /**
+ * Extract regex from options array
+ *
+ * @param \CaptainHook\App\Config\Options $options
+ * @return string
+ * @throws \CaptainHook\App\Exception\ActionFailed
+ */
+ protected function getRegex(Config\Options $options): string
+ {
+ $regex = $options->get('regex', '');
+ if (empty($regex)) {
+ throw new ActionFailed('No regex option');
+ }
+ return $regex;
+ }
+
+ /**
+ * Determine the error message to use
+ *
+ * @param \CaptainHook\App\Config\Options $options
+ * @return string
+ */
+ protected function getErrorMessage(Config\Options $options): string
+ {
+ $msg = $options->get('error', '');
+ return !empty($msg) ? $msg : 'FAIL Branch name does not match regex: %s';
+ }
+
+ /**
+ * Determine the error message to use
+ *
+ * @param \CaptainHook\App\Config\Options $options
+ * @return string
+ */
+ protected function getSuccessMessage(Config\Options $options): string
+ {
+ $msg = $options->get('success', '');
+ return !empty($msg) ? $msg : 'OK Branch name does match regex: %s';
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Cli/Command.php b/lib/captainhook/captainhook/src/Hook/Cli/Command.php
new file mode 100644
index 0000000000..0540ded52b
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Cli/Command.php
@@ -0,0 +1,42 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook\Cli;
+
+use CaptainHook\App\Config;
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Hook\Action;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Class Notify
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.23.1
+ */
+class Command implements Action
+{
+ /**
+ * Executes 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
+ {
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Composer/Action/CheckLockFile.php b/lib/captainhook/captainhook/src/Hook/Composer/Action/CheckLockFile.php
new file mode 100644
index 0000000000..cfa0471010
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Composer/Action/CheckLockFile.php
@@ -0,0 +1,130 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook\Composer\Action;
+
+use CaptainHook\App\Config;
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Hook\Action;
+use Exception;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Class CheckLockFile
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 1.0.1
+ */
+class CheckLockFile implements Action
+{
+ /**
+ * Composer configuration keys that are relevant for the 'content-hash' creation
+ *
+ * @var array
+ */
+ private $relevantKeys = [
+ 'name',
+ 'version',
+ 'require',
+ 'require-dev',
+ 'conflict',
+ 'replace',
+ 'provide',
+ 'minimum-stability',
+ 'prefer-stable',
+ 'repositories',
+ 'extra',
+ ];
+
+ /**
+ * Executes the action
+ *
+ * @param \CaptainHook\App\Config $config
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @param \CaptainHook\App\Config\Action $action
+ * @throws \Exception
+ */
+ public function execute(Config $config, IO $io, Repository $repository, Config\Action $action): void
+ {
+ $path = $action->getOptions()->get('path', getcwd());
+ $name = $action->getOptions()->get('name', 'composer');
+ $pathname = $path . DIRECTORY_SEPARATOR . $name;
+ $lockFileHash = $this->getLockFileHash($pathname . '.lock');
+ $configFileHash = $this->getConfigFileHash($pathname . '.json');
+
+ if ($lockFileHash !== $configFileHash) {
+ throw new Exception('Your composer.lock file is out of date');
+ }
+ }
+
+ /**
+ * Read the composer.lock file and extract the composer.json hash
+ *
+ * @param string $composerLock
+ * @return string
+ * @throws \Exception
+ */
+ private function getLockFileHash(string $composerLock): string
+ {
+ $lockFile = json_decode($this->loadFile($composerLock));
+ $hashKey = 'content-hash';
+
+ if (!isset($lockFile->$hashKey)) {
+ throw new Exception('could not find content hash, please update composer to the latest version');
+ }
+
+ return $lockFile->$hashKey;
+ }
+
+ /**
+ * Read the composer.json file and create a md5 hash on its relevant content
+ *
+ * This more or less is composer internal code to generate the content-hash so this might not be the best idea
+ * and will be removed in the future.
+ *
+ * @param string $composerJson
+ * @return string
+ * @throws \Exception
+ */
+ private function getConfigFileHash(string $composerJson): string
+ {
+ $content = json_decode($this->loadFile($composerJson), true);
+ $relevantContent = [];
+
+ foreach (array_intersect($this->relevantKeys, array_keys($content)) as $key) {
+ $relevantContent[$key] = $content[$key];
+ }
+ if (isset($content['config']['platform'])) {
+ $relevantContent['config']['platform'] = $content['config']['platform'];
+ }
+ ksort($relevantContent);
+
+ return md5((string)json_encode($relevantContent));
+ }
+
+ /**
+ * Load a composer file
+ *
+ * @param string $file
+ * @return string
+ * @throws \Exception
+ */
+ private function loadFile(string $file): string
+ {
+ if (!file_exists($file)) {
+ throw new Exception($file . ' not found');
+ }
+ return (string)file_get_contents($file);
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition.php b/lib/captainhook/captainhook/src/Hook/Condition.php
new file mode 100644
index 0000000000..918b5175ba
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition.php
@@ -0,0 +1,35 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook;
+
+use CaptainHook\App\Console\IO;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Interface Conditions
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 4.2.0
+ */
+interface Condition
+{
+ /**
+ * Evaluates a condition
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return bool
+ */
+ public function isTrue(IO $io, Repository $repository): bool;
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/Branch/Files.php b/lib/captainhook/captainhook/src/Hook/Condition/Branch/Files.php
new file mode 100644
index 0000000000..4ac79c4301
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/Branch/Files.php
@@ -0,0 +1,81 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace CaptainHook\App\Hook\Condition\Branch;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Hook\Condition;
+use CaptainHook\App\Hook\FileList;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Files condition
+ *
+ * Example configuration:
+ *
+ * "action": "some-action"
+ * "conditions": [
+ * {"exec": "\\CaptainHook\\App\\Hook\\Condition\\Branch\\Files",
+ * "args": [
+ * {"compare-to": "main", "of-type": "php"}
+ * ]}
+ * ]
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.21.0
+ */
+class Files implements Condition
+{
+ /**
+ * Options
+ * - compare-to: source branch if known, otherwise the reflog is used to figure it out
+ * - in-directory: only check for files in given directory
+ * - of-type: only check for files of given type
+ *
+ * @var array
+ */
+ private array $options;
+
+ /**
+ * Constructor
+ *
+ * @param array $options
+ */
+ public function __construct(array $options = [])
+ {
+ $this->options = $options;
+ }
+
+ /**
+ * Check if the current branch contains changes to files
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return bool
+ */
+ public function isTrue(IO $io, Repository $repository): bool
+ {
+ $branch = $repository->getInfoOperator()->getCurrentBranch();
+ $start = $this->options['compared-to'] ?? $repository->getLogOperator()->getBranchRevFromRefLog($branch);
+
+ if (empty($start)) {
+ return false;
+ }
+
+ $files = $repository->getLogOperator()->getChangedFilesSince($start, ['A', 'C', 'M', 'R']);
+
+ return count(FileList::filter($files, $this->options)) > 0;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/Branch/Name.php b/lib/captainhook/captainhook/src/Hook/Condition/Branch/Name.php
new file mode 100644
index 0000000000..73b7148dca
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/Branch/Name.php
@@ -0,0 +1,55 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace CaptainHook\App\Hook\Condition\Branch;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Hook\Condition;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * OnBranch condition
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.0.0
+ */
+abstract class Name implements Condition
+{
+ /**
+ * Branch name to compare
+ *
+ * @var string
+ */
+ protected string $name;
+
+ /**
+ * OnBranch constructor.
+ *
+ * @param string $name
+ */
+ public function __construct(string $name)
+ {
+ $this->name = $name;
+ }
+
+ /**
+ * Check is the current branch is equal to the configured one
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return bool
+ */
+ abstract public function isTrue(IO $io, Repository $repository): bool;
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/Branch/NotOn.php b/lib/captainhook/captainhook/src/Hook/Condition/Branch/NotOn.php
new file mode 100644
index 0000000000..d30d07629f
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/Branch/NotOn.php
@@ -0,0 +1,41 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace CaptainHook\App\Hook\Condition\Branch;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Hook\Condition;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * NotOn condition
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.20.2
+ */
+class NotOn extends Name
+{
+ /**
+ * Check is the current branch is not equal to the configured one
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return bool
+ */
+ public function isTrue(IO $io, Repository $repository): bool
+ {
+ return trim($repository->getInfoOperator()->getCurrentBranch()) !== $this->name;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/Branch/NotOnMatching.php b/lib/captainhook/captainhook/src/Hook/Condition/Branch/NotOnMatching.php
new file mode 100644
index 0000000000..59ec917c03
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/Branch/NotOnMatching.php
@@ -0,0 +1,41 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace CaptainHook\App\Hook\Condition\Branch;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Hook\Condition;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * NotOnMatching condition
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.20.2
+ */
+class NotOnMatching extends Name
+{
+ /**
+ * Check is the current branch is not matched by the configured regex
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return bool
+ */
+ public function isTrue(IO $io, Repository $repository): bool
+ {
+ return preg_match($this->name, trim($repository->getInfoOperator()->getCurrentBranch())) === 0;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/Branch/On.php b/lib/captainhook/captainhook/src/Hook/Condition/Branch/On.php
new file mode 100644
index 0000000000..cf35f8b666
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/Branch/On.php
@@ -0,0 +1,41 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace CaptainHook\App\Hook\Condition\Branch;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Hook\Condition;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * On condition
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.20.2
+ */
+class On extends Name
+{
+ /**
+ * Check is the current branch is equal to the configured one
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return bool
+ */
+ public function isTrue(IO $io, Repository $repository): bool
+ {
+ return trim($repository->getInfoOperator()->getCurrentBranch()) === $this->name;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/Branch/OnMatching.php b/lib/captainhook/captainhook/src/Hook/Condition/Branch/OnMatching.php
new file mode 100644
index 0000000000..532641084a
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/Branch/OnMatching.php
@@ -0,0 +1,41 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace CaptainHook\App\Hook\Condition\Branch;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Hook\Condition;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * OnMatching condition
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.20.2
+ */
+class OnMatching extends Name
+{
+ /**
+ * Check is the current branch is matched by the configured regex
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return bool
+ */
+ public function isTrue(IO $io, Repository $repository): bool
+ {
+ return preg_match($this->name, trim($repository->getInfoOperator()->getCurrentBranch())) === 1;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/Cli.php b/lib/captainhook/captainhook/src/Hook/Condition/Cli.php
new file mode 100644
index 0000000000..7ad6b8381d
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/Cli.php
@@ -0,0 +1,66 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook\Condition;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Hook\Condition;
+use SebastianFeldmann\Cli\Processor;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Class Cli
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 4.2.0
+ */
+class Cli implements Condition
+{
+ /**
+ * Binary executor
+ *
+ * @var \SebastianFeldmann\Cli\Processor
+ */
+ private $processor;
+
+ /**
+ * @var string
+ */
+ private $command;
+
+ /**
+ * Cli constructor.
+ *
+ * @param \SebastianFeldmann\Cli\Processor $processor
+ * @param string $command
+ */
+ public function __construct(Processor $processor, string $command)
+ {
+ $this->processor = $processor;
+ $this->command = $command;
+ }
+
+ /**
+ * Evaluates a condition
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return bool
+ */
+ public function isTrue(IO $io, Repository $repository): bool
+ {
+ $result = $this->processor->run($this->command);
+
+ return $result->isSuccessful();
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/Config.php b/lib/captainhook/captainhook/src/Hook/Condition/Config.php
new file mode 100644
index 0000000000..6524bcb521
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/Config.php
@@ -0,0 +1,71 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook\Condition;
+
+use CaptainHook\App\Config as AppConfig;
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Hook\Condition;
+use RuntimeException;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Class FileChange
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 4.2.0
+ */
+abstract class Config implements ConfigDependant, Condition
+{
+ /**
+ * @var \CaptainHook\App\Config|null
+ */
+ protected ?AppConfig $config = null;
+
+ /**
+ * Config setter
+ *
+ * @param \CaptainHook\App\Config $config
+ * @return void
+ */
+ public function setConfig(AppConfig $config): void
+ {
+ $this->config = $config;
+ }
+
+ /**
+ * Check if the customer value exists and return izs boolish value
+ *
+ * @param string $value
+ * @param bool $default
+ * @return bool
+ */
+ protected function checkCustomValue(string $value, bool $default): bool
+ {
+ if (null === $this->config) {
+ throw new RuntimeException('config not set');
+ }
+ $customSettings = $this->config->getCustomSettings();
+ $valueToCheck = $customSettings[$value] ?? $default;
+ return filter_var($valueToCheck, FILTER_VALIDATE_BOOL);
+ }
+
+ /**
+ * Evaluates a condition
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return bool
+ */
+ abstract public function isTrue(IO $io, Repository $repository): bool;
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/Config/CustomValueIsFalsy.php b/lib/captainhook/captainhook/src/Hook/Condition/Config/CustomValueIsFalsy.php
new file mode 100644
index 0000000000..83e26616ae
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/Config/CustomValueIsFalsy.php
@@ -0,0 +1,67 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook\Condition\Config;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Hook\Condition;
+use CaptainHook\App\Hooks;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Class CustomValueIsFalsy
+ *
+ * Example configuration:
+ *
+ * "action": "some-action"
+ * "conditions": [
+ * {"exec": "\\CaptainHook\\App\\Hook\\Condition\\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
+ */
+class CustomValueIsFalsy extends Condition\Config
+{
+ /**
+ * Custom config value to check
+ *
+ * @var string
+ */
+ private string $value;
+
+ /**
+ * CustomValueIsFalsy constructor
+ *
+ * @param string $value
+ */
+ public function __construct(string $value)
+ {
+ $this->value = $value;
+ }
+
+ /**
+ * Evaluates the condition
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return bool
+ */
+ public function isTrue(IO $io, Repository $repository): bool
+ {
+ return !$this->checkCustomValue($this->value, false);
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/Config/CustomValueIsTruthy.php b/lib/captainhook/captainhook/src/Hook/Condition/Config/CustomValueIsTruthy.php
new file mode 100644
index 0000000000..2815aa3e00
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/Config/CustomValueIsTruthy.php
@@ -0,0 +1,67 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook\Condition\Config;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Hook\Condition;
+use CaptainHook\App\Hooks;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Class CustomValueIsTruthy
+ *
+ * Example configuration:
+ *
+ * "action": "some-action"
+ * "conditions": [
+ * {"exec": "\\CaptainHook\\App\\Hook\\Condition\\Config\\CustomValueIsTruthy",
+ * "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
+ */
+class CustomValueIsTruthy extends Condition\Config
+{
+ /**
+ * Custom config value to check
+ *
+ * @var string
+ */
+ private string $value;
+
+ /**
+ * CustomValueIsTruthy constructor
+ *
+ * @param string $value
+ */
+ public function __construct(string $value)
+ {
+ $this->value = $value;
+ }
+
+ /**
+ * Evaluates the condition
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return bool
+ */
+ public function isTrue(IO $io, Repository $repository): bool
+ {
+ return $this->checkCustomValue($this->value, false);
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/ConfigDependant.php b/lib/captainhook/captainhook/src/Hook/Condition/ConfigDependant.php
new file mode 100644
index 0000000000..d947105cc8
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/ConfigDependant.php
@@ -0,0 +1,36 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook\Condition;
+
+use CaptainHook\App\Config;
+
+/**
+ * Interface Conditions
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.17.2
+ */
+interface ConfigDependant
+{
+ /**
+ * Evaluates a condition
+ *
+ * This will be deprecated in version 6.0.0
+ * In version 6.0.0 the condition interface should change to include the Config
+ *
+ * @param \CaptainHook\App\Config $config
+ * @return void
+ */
+ public function setConfig(Config $config): void;
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/File.php b/lib/captainhook/captainhook/src/Hook/Condition/File.php
new file mode 100644
index 0000000000..0dbbe4885b
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/File.php
@@ -0,0 +1,101 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook\Condition;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Hook\Condition;
+use CaptainHook\App\Hook\Constrained;
+use CaptainHook\App\Hook\Restriction;
+use CaptainHook\App\Hooks;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Class FileChange
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.2.0
+ */
+abstract class File implements Condition, Constrained
+{
+ /**
+ * Return the hook restriction information
+ *
+ * @return \CaptainHook\App\Hook\Restriction
+ */
+ abstract public static function getRestriction(): Restriction;
+
+ /**
+ * Evaluates a condition
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return bool
+ */
+ abstract public function isTrue(IO $io, Repository $repository): bool;
+
+ /**
+ * Check if all of the given files can be found in a haystack of files
+ *
+ * IMPORTANT: If no files are provided this is always true.
+ *
+ * @param array $files
+ * @param array $haystack
+ * @return bool
+ */
+ protected function allFilesInHaystack(array $files, array $haystack): bool
+ {
+ foreach ($files as $filePattern) {
+ if (!$this->isFileInList($haystack, $filePattern)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Check if any of the given files can be found in a haystack of files
+ *
+ * IMPORTANT: If no files are provided this is always false.
+ *
+ * @param array $files
+ * @param array $haystack
+ * @return bool
+ */
+ protected function anyFileInHaystack(array $files, array $haystack): bool
+ {
+ foreach ($files as $filePattern) {
+ if ($this->isFileInList($haystack, $filePattern)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check if a file matching a `fnmatch` pattern was changed
+ *
+ * @param array $listOfFiles List of files to scan
+ * @param string $pattern Pattern in fnmatch format to look for
+ * @return bool
+ */
+ protected function isFileInList(array $listOfFiles, string $pattern): bool
+ {
+ foreach ($listOfFiles as $file) {
+ if (fnmatch($pattern, $file)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/FileChanged.php b/lib/captainhook/captainhook/src/Hook/Condition/FileChanged.php
new file mode 100644
index 0000000000..9bc371cc52
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/FileChanged.php
@@ -0,0 +1,91 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook\Condition;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Git;
+use CaptainHook\App\Hook\Restriction;
+use CaptainHook\App\Hooks;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Class FileChange
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 4.2.0
+ */
+abstract class FileChanged extends File
+{
+ /**
+ * List of file to watch
+ *
+ * @var array
+ */
+ protected array $filesToWatch;
+
+ /**
+ * Git filter options
+ *
+ * @var array
+ */
+ private array $filter;
+
+ /**
+ * FileChange constructor
+ *
+ * @param array $files
+ * @param string $filter
+ */
+ public function __construct(array $files, string $filter = 'ACMR')
+ {
+ $this->filesToWatch = $files;
+ $this->filter = !empty($filter) ? str_split($filter) : [];
+ }
+
+ /**
+ * Return the hook restriction information
+ *
+ * @return \CaptainHook\App\Hook\Restriction
+ */
+ public static function getRestriction(): Restriction
+ {
+ return Restriction::fromArray([Hooks::PRE_PUSH, Hooks::POST_CHECKOUT, Hooks::POST_MERGE, Hooks::POST_REWRITE]);
+ }
+
+ /**
+ * Evaluates a condition
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return bool
+ */
+ abstract public function isTrue(IO $io, Repository $repository): bool;
+
+ /**
+ * Use 'diff-tree' to find the changed files after this merge or checkout
+ *
+ * In case of a checkout it is easy because the arguments 'previousHead' and 'newHead' exist.
+ * In case of a merge determining this hashes is more difficult, so we are using the 'ref-log'
+ * to do it and using 'HEAD@{1}' as the last position before the merge and 'HEAD' as the
+ * current position after the merge.
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return array
+ */
+ protected function getChangedFiles(IO $io, Repository $repository): array
+ {
+ return Git\ChangedFiles::getChangedFiles($io, $repository, $this->filter);
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/FileChanged/All.php b/lib/captainhook/captainhook/src/Hook/Condition/FileChanged/All.php
new file mode 100644
index 0000000000..e6dcc0d477
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/FileChanged/All.php
@@ -0,0 +1,44 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook\Condition\FileChanged;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Hook\Condition\FileChanged;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Class All
+ *
+ * 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.
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 4.2.0
+ */
+class All extends FileChanged
+{
+ /**
+ * Check if all the configured files were changed within the applied change set
+ *
+ * IMPORTANT: If no files are configured this condition is always true.
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return bool
+ */
+ public function isTrue(IO $io, Repository $repository): bool
+ {
+ return $this->allFilesInHaystack($this->filesToWatch, $this->getChangedFiles($io, $repository));
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/FileChanged/Any.php b/lib/captainhook/captainhook/src/Hook/Condition/FileChanged/Any.php
new file mode 100644
index 0000000000..d40f6dd4df
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/FileChanged/Any.php
@@ -0,0 +1,58 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook\Condition\FileChanged;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Hook\Condition\FileChanged;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * 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.
+ *
+ * Example configuration:
+ *
+ * "action": "composer install"
+ * "conditions": [
+ * {"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileChange\\Any",
+ * "args": [
+ * [
+ * "composer.json",
+ * "composer.lock"
+ * ]
+ * ]}
+ * ]
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 4.2.0
+ */
+class Any extends FileChanged
+{
+ /**
+ * Check if any of the configured files was changed within the applied change set
+ *
+ * IMPORTANT: If no files are configured this condition is always false.
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return bool
+ */
+ public function isTrue(IO $io, Repository $repository): bool
+ {
+ return $this->anyFileInHaystack($this->filesToWatch, $this->getChangedFiles($io, $repository));
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/FileChanged/OfType.php b/lib/captainhook/captainhook/src/Hook/Condition/FileChanged/OfType.php
new file mode 100644
index 0000000000..b13b5a8a6d
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/FileChanged/OfType.php
@@ -0,0 +1,89 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook\Condition\FileChanged;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Git;
+use CaptainHook\App\Hook\Condition;
+use CaptainHook\App\Hook\Constrained;
+use CaptainHook\App\Hook\FileList;
+use CaptainHook\App\Hook\Restriction;
+use CaptainHook\App\Hooks;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Class OfType
+ *
+ * Example configuration:
+ *
+ * "action": "some-action"
+ * "conditions": [
+ * {"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileChanged\\OfType",
+ * "args": [
+ * "php"
+ * ]}
+ * ]
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.0.0
+ */
+class OfType implements Condition, Constrained
+{
+ /**
+ * File type to check e.g. 'php' or 'html'
+ *
+ * @var string
+ */
+ private string $suffix;
+
+ /**
+ * OfType constructor
+ *
+ * @param string $type
+ */
+ public function __construct(string $type)
+ {
+ $this->suffix = $type;
+ }
+
+ /**
+ * Return the hook restriction information
+ *
+ * @return \CaptainHook\App\Hook\Restriction
+ */
+ public static function getRestriction(): Restriction
+ {
+ return Restriction::fromArray([Hooks::PRE_PUSH, Hooks::POST_CHECKOUT, Hooks::POST_MERGE, Hooks::POST_REWRITE]);
+ }
+
+ /**
+ * Evaluates the condition
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return bool
+ */
+ public function isTrue(IO $io, Repository $repository): bool
+ {
+ $files = FileList::filterByType(
+ Git\ChangedFiles::getChangedFiles($io, $repository, ['A', 'C', 'M', 'R']),
+ ['of-type' => $this->suffix]
+ );
+
+ if (count($files) > 0) {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/FileStaged.php b/lib/captainhook/captainhook/src/Hook/Condition/FileStaged.php
new file mode 100644
index 0000000000..319a6cc9c4
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/FileStaged.php
@@ -0,0 +1,85 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook\Condition;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Git\Diff\FilterUtil;
+use CaptainHook\App\Hook\Restriction;
+use CaptainHook\App\Hooks;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Class FileChange
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.2.0
+ */
+abstract class FileStaged extends File
+{
+ /**
+ * List of file to watch
+ *
+ * @var array
+ */
+ protected array $filesToWatch;
+
+ /**
+ * --diff-filter options
+ *
+ * @var array
+ */
+ protected array $diffFilter;
+
+ /**
+ * FileStaged constructor
+ *
+ * @param mixed $files
+ * @param mixed $diffFilter
+ */
+ public function __construct($files, $diffFilter = [])
+ {
+ $this->filesToWatch = is_array($files) ? $files : explode(',', (string) $files);
+ $this->diffFilter = FilterUtil::filterFromConfigValue($diffFilter);
+ }
+
+ /**
+ * Return the hook restriction information
+ *
+ * @return \CaptainHook\App\Hook\Restriction
+ */
+ public static function getRestriction(): Restriction
+ {
+ return Restriction::fromArray([Hooks::PRE_COMMIT]);
+ }
+
+ /**
+ * Evaluates a condition
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return bool
+ */
+ abstract public function isTrue(IO $io, Repository $repository): bool;
+
+ /**
+ * Use 'diff-index --cached' to find the staged files before the commit
+ *
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return array
+ */
+ protected function getStagedFiles(Repository $repository): array
+ {
+ return $repository->getIndexOperator()->getStagedFiles($this->diffFilter);
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/FileStaged/All.php b/lib/captainhook/captainhook/src/Hook/Condition/FileStaged/All.php
new file mode 100644
index 0000000000..2c7e61a8d1
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/FileStaged/All.php
@@ -0,0 +1,54 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook\Condition\FileStaged;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Hook\Condition\FileStaged;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Class All
+ *
+ * The FileStaged condition is applicable for `pre-commit` hooks.
+ * It checks if all configured files are staged for commit.
+ *
+ * Example configuration:
+ *
+ * "action": "some-action"
+ * "conditions": [
+ * {"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\All",
+ * "args": [
+ * ["file1", "file2", "file3"]
+ * ]}
+ * ]
+ *
+ * The file list can also be defined as comma seperated string "file1,file2,file3"
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.2.0
+ */
+class All extends FileStaged
+{
+ /**
+ * Check if all the configured files are staged for commit
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return bool
+ */
+ public function isTrue(IO $io, Repository $repository): bool
+ {
+ return $this->allFilesInHaystack($this->filesToWatch, $this->getStagedFiles($repository));
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/FileStaged/Any.php b/lib/captainhook/captainhook/src/Hook/Condition/FileStaged/Any.php
new file mode 100644
index 0000000000..fe65b34015
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/FileStaged/Any.php
@@ -0,0 +1,53 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook\Condition\FileStaged;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Hook\Condition\FileStaged;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Class Any
+ *
+ * The FileStaged condition is applicable for `pre-commit hooks.
+ *
+ * Example configuration:
+ *
+ * "action": "some-action"
+ * "conditions": [
+ * {"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\Any",
+ * "args": [
+ * ["file1", "file2", "file3"]
+ * ]}
+ * ]
+ *
+ * The file list can also be defined as comma seperated string "file1,file2,file3"
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.2.0
+ */
+class Any extends FileStaged
+{
+ /**
+ * Check if any of the configured files is staged for commit
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return bool
+ */
+ public function isTrue(IO $io, Repository $repository): bool
+ {
+ return $this->anyFileInHaystack($this->filesToWatch, $this->getStagedFiles($repository));
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/FileStaged/InDirectory.php b/lib/captainhook/captainhook/src/Hook/Condition/FileStaged/InDirectory.php
new file mode 100644
index 0000000000..7ddd0562ce
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/FileStaged/InDirectory.php
@@ -0,0 +1,100 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook\Condition\FileStaged;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Git\Diff\FilterUtil;
+use CaptainHook\App\Hook\Condition;
+use CaptainHook\App\Hook\Constrained;
+use CaptainHook\App\Hook\Restriction;
+use CaptainHook\App\Hooks;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Class InDirectory
+ *
+ * All FileStaged conditions are only applicable for `pre-commit` hooks.
+ *
+ * Example configuration:
+ *
+ * "action": "some-action"
+ * "conditions": [
+ * {"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\InDirectory",
+ * "args": [
+ * "src/"
+ * ]}
+ * ]
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.6.1
+ */
+class InDirectory implements Condition, Constrained
+{
+ /**
+ * Directory path to check e.g. 'src/' or 'path/To/Custom/Directory/'
+ *
+ * @var string
+ */
+ private string $directory;
+
+ /**
+ * --diff-filter options
+ *
+ * @var array
+ */
+ private array $diffFilter;
+
+ /**
+ * InDirectory constructor
+ *
+ * @param mixed $directory
+ * @param array|string $diffFilter
+ */
+ public function __construct($directory, $diffFilter = [])
+ {
+ $this->directory = (string) $directory;
+ $this->diffFilter = FilterUtil::filterFromConfigValue($diffFilter);
+ }
+
+ /**
+ * Return the hook restriction information
+ *
+ * @return \CaptainHook\App\Hook\Restriction
+ */
+ public static function getRestriction(): Restriction
+ {
+ return Restriction::fromArray([Hooks::PRE_COMMIT]);
+ }
+
+ /**
+ * Evaluates the condition
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return bool
+ */
+ public function isTrue(IO $io, Repository $repository): bool
+ {
+ $files = $repository->getIndexOperator()->getStagedFiles($this->diffFilter);
+
+ $filtered = [];
+ foreach ($files as $file) {
+ if (strpos($file, $this->directory) === 0) {
+ $filtered[] = $file;
+ }
+ }
+
+ return count($filtered) > 0;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/FileStaged/OfType.php b/lib/captainhook/captainhook/src/Hook/Condition/FileStaged/OfType.php
new file mode 100644
index 0000000000..e56bf5c498
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/FileStaged/OfType.php
@@ -0,0 +1,98 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook\Condition\FileStaged;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Git\Diff\FilterUtil;
+use CaptainHook\App\Hook\Condition;
+use CaptainHook\App\Hook\Constrained;
+use CaptainHook\App\Hook\Restriction;
+use CaptainHook\App\Hooks;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Class OfType
+ *
+ * All FileStaged conditions are only applicable for `pre-commit` hooks.
+ * The diff filter argument is optional.
+ *
+ * Example configuration:
+ *
+ * "action": "some-action"
+ * "conditions": [
+ * {"exec": "\\CaptainHook\\App\\Hook\\Condition\\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"]
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.0.0
+ */
+class OfType implements Condition, Constrained
+{
+ /**
+ * File type to check e.g. 'php' or 'html'
+ *
+ * @var array
+ */
+ private array $suffixes;
+
+ /**
+ * --diff-filter option
+ *
+ * @var array
+ */
+ private array $diffFilter;
+
+ /**
+ * OfType constructor
+ *
+ * @param mixed $types
+ * @param array|string $filter
+ */
+ public function __construct($types, $filter = [])
+ {
+ $this->suffixes = is_array($types) ? $types : explode(',', (string) $types);
+ $this->diffFilter = FilterUtil::filterFromConfigValue($filter);
+ }
+
+ /**
+ * Return the hook restriction information
+ *
+ * @return \CaptainHook\App\Hook\Restriction
+ */
+ public static function getRestriction(): Restriction
+ {
+ return Restriction::fromArray([Hooks::PRE_COMMIT]);
+ }
+
+ /**
+ * Evaluates the condition
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return bool
+ */
+ public function isTrue(IO $io, Repository $repository): bool
+ {
+ $files = $repository->getIndexOperator()->getStagedFilesOfTypes($this->suffixes, $this->diffFilter);
+ return count($files) > 0;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/FileStaged/ThatIs.php b/lib/captainhook/captainhook/src/Hook/Condition/FileStaged/ThatIs.php
new file mode 100644
index 0000000000..6e749a3876
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/FileStaged/ThatIs.php
@@ -0,0 +1,142 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook\Condition\FileStaged;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Git\Diff\FilterUtil;
+use CaptainHook\App\Hook\Condition;
+use CaptainHook\App\Hook\Constrained;
+use CaptainHook\App\Hook\Restriction;
+use CaptainHook\App\Hooks;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Class ThatIs
+ *
+ * All FileStaged conditions are only applicable for `pre-commit` hooks.
+ *
+ * Example configuration:
+ *
+ * "action": "some-action"
+ * "conditions": [
+ * {"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\ThatIs",
+ * "args": [
+ * {"ofType": "php", "inDirectory": "foo/", "diff-filter": ["A", "C"]}
+ * ]}
+ * ]
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.16.0
+ */
+class ThatIs implements Condition, Constrained
+{
+ /**
+ * Directory path to check e.g. 'src/' or 'path/To/Custom/Directory/'
+ *
+ * @var string[]
+ */
+ private array $directories;
+
+ /**
+ * File type to check e.g. 'php' or 'html'
+ *
+ * @var string[]
+ */
+ private array $suffixes;
+
+ /**
+ * --diff-filter options
+ *
+ * @var array
+ */
+ private array $diffFilter;
+
+ /**
+ * OfType constructor
+ *
+ * @param array $options
+ */
+ public function __construct(array $options)
+ {
+ $this->directories = (array)($options['inDirectory'] ?? []);
+ $this->suffixes = (array)($options['ofType'] ?? []);
+
+ $diffFilter = $options['diffFilter'] ?? [];
+ $this->diffFilter = FilterUtil::sanitize(is_array($diffFilter) ? $diffFilter : str_split($diffFilter));
+ }
+
+ /**
+ * Return the hook restriction information
+ *
+ * @return \CaptainHook\App\Hook\Restriction
+ */
+ public static function getRestriction(): Restriction
+ {
+ return Restriction::fromArray([Hooks::PRE_COMMIT]);
+ }
+
+ /**
+ * Evaluates the condition
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return bool
+ */
+ public function isTrue(IO $io, Repository $repository): bool
+ {
+ $files = $repository->getIndexOperator()->getStagedFiles($this->diffFilter);
+ $files = $this->filterFilesByDirectory($files);
+ $files = $this->filterFilesByType($files);
+ return count($files) > 0;
+ }
+
+ /**
+ * Remove all files not in a given directory
+ *
+ * @param array $files
+ * @return array
+ */
+ private function filterFilesByDirectory(array $files): array
+ {
+ if (empty($this->directories)) {
+ return $files;
+ }
+ return array_filter($files, function ($file) {
+ foreach ($this->directories as $directory) {
+ if (str_starts_with($file, $directory)) {
+ return true;
+ }
+ }
+ return false;
+ });
+ }
+
+ /**
+ * Remove all files not of a configured type
+ *
+ * @param array $files
+ * @return array
+ */
+ private function filterFilesByType(array $files): array
+ {
+ if (empty($this->suffixes)) {
+ return $files;
+ }
+ return array_filter($files, fn($file) => in_array(
+ strtolower(pathinfo($file, PATHINFO_EXTENSION)),
+ $this->suffixes,
+ true
+ ));
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/Logic.php b/lib/captainhook/captainhook/src/Hook/Condition/Logic.php
new file mode 100644
index 0000000000..e58ac02ada
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/Logic.php
@@ -0,0 +1,59 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace CaptainHook\App\Hook\Condition;
+
+use CaptainHook\App\Hook\Condition;
+
+/**
+ * Logical condition base class
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @author Andreas Heigl
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.7.0
+ */
+abstract class Logic implements Condition
+{
+ /**
+ * List of conditions to logically connect
+ *
+ * @var \CaptainHook\App\Hook\Condition[]
+ */
+ protected array $conditions = [];
+
+ final private function __construct(Condition ...$conditions)
+ {
+ $this->conditions = $conditions;
+ }
+
+ /**
+ * Create a logic condition
+ *
+ * @param array $conditions
+ * @return \CaptainHook\App\Hook\Condition
+ */
+ public static function fromConditionsArray(array $conditions): Condition
+ {
+ $realConditions = [];
+ foreach ($conditions as $condition) {
+ if (! $condition instanceof Condition) {
+ continue;
+ }
+ $realConditions[] = $condition;
+ }
+
+ return new static(...$realConditions);
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/Logic/LogicAnd.php b/lib/captainhook/captainhook/src/Hook/Condition/Logic/LogicAnd.php
new file mode 100644
index 0000000000..34f373935d
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/Logic/LogicAnd.php
@@ -0,0 +1,40 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace CaptainHook\App\Hook\Condition\Logic;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Hook\Condition\Logic;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Connects multiple conditions with 'and'
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @author Andreas Heigl
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.7.0
+ */
+final class LogicAnd extends Logic
+{
+ public function isTrue(IO $io, Repository $repository): bool
+ {
+ foreach ($this->conditions as $condition) {
+ if (false === $condition->isTrue($io, $repository)) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/Logic/LogicOr.php b/lib/captainhook/captainhook/src/Hook/Condition/Logic/LogicOr.php
new file mode 100644
index 0000000000..3e5a7f3f95
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/Logic/LogicOr.php
@@ -0,0 +1,40 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace CaptainHook\App\Hook\Condition\Logic;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Hook\Condition\Logic;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Connects multiple conditions with 'or'
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @author Andreas Heigl
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.7.0
+ */
+final class LogicOr extends Logic
+{
+ public function isTrue(IO $io, Repository $repository): bool
+ {
+ foreach ($this->conditions as $condition) {
+ if (true === $condition->isTrue($io, $repository)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Condition/OnBranch.php b/lib/captainhook/captainhook/src/Hook/Condition/OnBranch.php
new file mode 100644
index 0000000000..530c901c64
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Condition/OnBranch.php
@@ -0,0 +1,31 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace CaptainHook\App\Hook\Condition;
+
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Hook\Condition;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * OnBranch condition
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.0.0
+ * @deprecated Replaced be CaptainHook\App\Hook\Condition\Branch\On
+ */
+class OnBranch extends Condition\Branch\On
+{
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Constrained.php b/lib/captainhook/captainhook/src/Hook/Constrained.php
new file mode 100644
index 0000000000..3cb4acdfdc
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Constrained.php
@@ -0,0 +1,35 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+declare(strict_types=1);
+
+namespace CaptainHook\App\Hook;
+
+/**
+ * Interface Constrained
+ *
+ * If your action is only applicable for a certain set of hooks you can limit its execution to specific hooks by
+ * implementing this interface and returning a restriction value object.
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.0.0
+ */
+interface Constrained
+{
+ /**
+ * Returns the list of hooks where this action is applicable
+ *
+ * @return \CaptainHook\App\Hook\Restriction
+ */
+ public static function getRestriction(): Restriction;
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Debug.php b/lib/captainhook/captainhook/src/Hook/Debug.php
new file mode 100644
index 0000000000..11e5e3e77d
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Debug.php
@@ -0,0 +1,83 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook;
+
+use CaptainHook\App\Config;
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Exception\ActionFailed;
+use Exception;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Debug hook to test hook triggering
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 4.0.4
+ */
+abstract class Debug implements Action
+{
+ /**
+ * Executes 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
+ */
+ abstract public function execute(Config $config, IO $io, Repository $repository, Config\Action $action): void;
+
+ /**
+ * Generate some debug output
+ *
+ * @param \CaptainHook\App\Console\IO $io
+ * @param \SebastianFeldmann\Git\Repository $repository
+ * @return void
+ */
+ protected function debugOutput(IO $io, Repository $repository): void
+ {
+ $originalHookArguments = $io->getArguments();
+
+ $currentGitTag = 'no tags yet';
+ try {
+ $currentGitTag = $repository->getInfoOperator()->getCurrentTag();
+ } catch (Exception $e) {
+ // ignore it, it just means there are no tags yet
+ }
+ $io->write($this->getArgumentOutput($originalHookArguments), false);
+ $io->write(' Current git-tag: ' . $currentGitTag);
+ $io->write(
+ ' StandardInput: ' . PHP_EOL .
+ ' ' . implode(PHP_EOL . ' ', $io->getStandardInput())
+ );
+ }
+
+ /**
+ * Format output to display original hook arguments
+ *
+ * Returns a string with a newline character at the end.
+ *
+ * @param array $args
+ * @return string
+ */
+ protected function getArgumentOutput(array $args): string
+ {
+ $out = ' Original arguments:' . PHP_EOL;
+ foreach ($args as $name => $value) {
+ $out .= ' ' . $name . ' =>> ' . $value . PHP_EOL;
+ }
+ return $out;
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Debug/Failure.php b/lib/captainhook/captainhook/src/Hook/Debug/Failure.php
new file mode 100644
index 0000000000..e3dd98279a
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Debug/Failure.php
@@ -0,0 +1,49 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook\Debug;
+
+use CaptainHook\App\Config;
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Exception\ActionFailed;
+use CaptainHook\App\Hook\Debug;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Debug hook to test hook triggering that fails the hook execution
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 5.20.1
+ */
+class Failure extends Debug
+{
+ /**
+ * Executes 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
+ {
+ $this->debugOutput($io, $repository);
+
+ throw new ActionFailed(
+ 'The \'Debug\' action is only for debugging purposes, '
+ . 'please remove the \'Debug\' action from your config'
+ );
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Debug/Success.php b/lib/captainhook/captainhook/src/Hook/Debug/Success.php
new file mode 100644
index 0000000000..7a9d47b01d
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Debug/Success.php
@@ -0,0 +1,43 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook\Debug;
+
+use CaptainHook\App\Config;
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Hook\Debug;
+use SebastianFeldmann\Git\Repository;
+
+/**
+ * Debug hook to test hook triggering that allows the hook to pass
+ *
+ * @package CaptainHook
+ * @author Sebastian Feldmann
+ * @link https://github.com/captainhook-git/captainhook
+ * @since Class available since Release 4.0.4
+ */
+class Success extends Debug
+{
+ /**
+ * Executes 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
+ {
+ $this->debugOutput($io, $repository);
+ }
+}
diff --git a/lib/captainhook/captainhook/src/Hook/Diff/Action/BlockSecrets.php b/lib/captainhook/captainhook/src/Hook/Diff/Action/BlockSecrets.php
new file mode 100644
index 0000000000..d0c4d0d7ac
--- /dev/null
+++ b/lib/captainhook/captainhook/src/Hook/Diff/Action/BlockSecrets.php
@@ -0,0 +1,343 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace CaptainHook\App\Hook\Diff\Action;
+
+use CaptainHook\App\Config;
+use CaptainHook\App\Console\IO;
+use CaptainHook\App\Console\IOUtil;
+use CaptainHook\App\Exception\ActionFailed;
+use CaptainHook\App\Git\Range\Detector\PrePush;
+use CaptainHook\App\Hook\Action;
+use CaptainHook\App\Hook\Constrained;
+use CaptainHook\App\Hook\Restriction;
+use CaptainHook\App\Hook\Util;
+use CaptainHook\App\Hooks;
+use CaptainHook\Secrets\Detector;
+use CaptainHook\Secrets\Entropy\Shannon;
+use CaptainHook\Secrets\Regex\Supplier\Ini;
+use CaptainHook\Secrets\Regex\Supplier\Json;
+use CaptainHook\Secrets\Regex\Supplier\PHP;
+use CaptainHook\Secrets\Regex\Supplier\Yaml;
+use CaptainHook\Secrets\Regexer;
+use Exception;
+use SebastianFeldmann\Git\Diff\File;
+use SebastianFeldmann\Git\Repository;
+
+class BlockSecrets implements Action, Constrained
+{
+ /**
+ * @var \CaptainHook\App\Console\IO
+ */
+ private IO $io;
+
+ /**
+ * @var \CaptainHook\Secrets\Detector
+ */
+ private Detector $detector;
+
+ /**
+ * List of allowed patterns
+ *
+ * @var array
+ */
+ private array $allowed;
+
+ /**
+ * Additional information for a file
+ *
+ * @var array