Skip to content
84 changes: 82 additions & 2 deletions src/Console/Command/Hook.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use CaptainHook\App\Config;
use CaptainHook\App\Console\IOUtil;
use CaptainHook\App\Hook\Util;
use CaptainHook\App\Runner\Hook as RunnerHook;
use Exception;
use RuntimeException;
use Symfony\Component\Console\Input\InputInterface;
Expand Down Expand Up @@ -56,6 +57,50 @@ protected function configure(): void
InputOption::VALUE_OPTIONAL,
'Relative path from your config file to your bootstrap file'
);

$this->addOption(
'list-actions',
'l',
InputOption::VALUE_NONE,
'List actions for this hook without running the hook'
);

$this->addOption(
'action',
'a',
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
'Run only the actions listed'
);

$this->addOption(
'disable-plugins',
null,
InputOption::VALUE_NONE,
'Disable all hook plugins'
);
}

/**
* Initialize the command by checking/modifying inputs before validation
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
protected function initialize(InputInterface $input, OutputInterface $output): void
{
// If `--list-actions` is present, we will ignore any arguments, since
// this option intends to output a list of actions for the hook without
// running the hook. So, if any arguments are required but not present
// in the input, we will set them to an empty string in the input to
// suppress any validation errors.
if ($input->getOption('list-actions') === true) {
foreach ($this->getDefinition()->getArguments() as $arg) {
if ($arg->isRequired() && $input->getArgument($arg->getName()) === null) {
$input->setArgument($arg->getName(), '');
}
}
}
}

/**
Expand All @@ -74,15 +119,24 @@ protected function execute(InputInterface $input, OutputInterface $output): int

// use ansi coloring if not disabled in captainhook.json
$output->setDecorated($config->useAnsiColors());
// use the configured verbosity to manage general output verbosity
$output->setVerbosity(IOUtil::mapConfigVerbosity($config->getVerbosity()));

// If the verbose option is present on the command line, then use it.
// Otherwise, use the verbosity setting from the configuration.
if (!$input->hasOption('verbose') || !$input->getOption('verbose')) {
$output->setVerbosity(IOUtil::mapConfigVerbosity($config->getVerbosity()));
}

try {
$this->handleBootstrap($config);

$class = '\\CaptainHook\\App\\Runner\\Hook\\' . Util::getHookCommand($this->hookName);
/** @var \CaptainHook\App\Runner\Hook $hook */
$hook = new $class($io, $config, $repository);
// If list-actions is true, then list the hook actions instead of running them.
if ($input->getOption('list-actions') === true) {
$this->listActions($output, $hook);
return 0;
}
$hook->run();
return 0;
} catch (Exception $e) {
Expand Down Expand Up @@ -134,4 +188,30 @@ private function handleError(OutputInterface $output, Exception $e): int

return 1;
}

/**
* Print out a list of actions for this hook
*
* @param OutputInterface $output
* @param RunnerHook $hook
*/
private function listActions(OutputInterface $output, RunnerHook $hook): void
{
$output->writeln('<comment>Listing ' . $hook->getName() . ' actions:</comment>');

if (!$hook->isEnabled()) {
$output->writeln(' - hook is disabled');
return;
}

$actions = $hook->getActions();
if (count($actions) === 0) {
$output->writeln(' - no actions configured');
return;
}

foreach ($actions as $action) {
$output->writeln(" - <fg=blue>{$action->getAction()}</>");
}
}
}
22 changes: 22 additions & 0 deletions src/Console/IO/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,28 @@ public function getArgument(string $name, string $default = ''): string
return $default;
}

/**
* Return the original cli options
*
* @return array
*/
public function getOptions(): array
{
return [];
}

/**
* Return the original cli option or a given default
*
* @param string $name
* @param string|string[]|bool|null $default
* @return string|string[]|bool|null
*/
public function getOption(string $name, $default = null)
{
return $default;
}

/**
* Return the piped in standard input
*
Expand Down
22 changes: 22 additions & 0 deletions src/Console/IO/DefaultIO.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,28 @@ public function getArgument(string $name, string $default = ''): string
return (string)($this->getArguments()[$name] ?? $default);
}

/**
* Return the original cli options
*
* @return array
*/
public function getOptions(): array
{
return $this->input->getOptions();
}

/**
* Return the original cli option or a given default
*
* @param string $name
* @param string|string[]|bool|null $default
* @return string|string[]|bool|null
*/
public function getOption(string $name, $default = null)
{
return $this->getOptions()[$name] ?? $default;
}

/**
* Return the piped in standard input
*
Expand Down
1 change: 1 addition & 0 deletions src/Hook/File/Action/Check.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public function execute(Config $config, IO $io, Repository $repository, Config\A
* Setup the action, reading and validating all config settings
*
* @param \CaptainHook\App\Config\Options $options
* @codeCoverageIgnore
*/
protected function setUp(Config\Options $options): void
{
Expand Down
81 changes: 81 additions & 0 deletions src/Plugin/Hook/DisallowActionChanges.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

/**
* This file is part of CaptainHook
*
* (c) Sebastian Feldmann <sf@sebastian-feldmann.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace CaptainHook\App\Plugin\Hook;

use CaptainHook\App\Config;
use CaptainHook\App\Plugin;
use CaptainHook\App\Plugin\Hook\Exception\ActionChangedFiles;
use CaptainHook\App\Runner\Hook as RunnerHook;
use SebastianFeldmann\Git\Diff\File;

/**
* DisallowActionChanges runner plugin
*
* @package CaptainHook
* @author Sebastian Feldmann <sf@sebastian-feldmann.info>
* @link https://github.com/captainhookphp/captainhook
* @since Class available since Release 5.11.0.
*/
class DisallowActionChanges extends PreserveWorkingTree implements Plugin\Hook
{
/**
* @var iterable
*/
private $priorDiff = [];

/**
* An array of actions that made changes to files. Each action name is the
* key for an array of file changes made by that action.
*
* @var array<string, File[]>
*/
private $actionChanges = [];

public function beforeHook(RunnerHook $hook): void
{
parent::beforeHook($hook);

// Get a diff of the current state of the working tree. Since we ran
// the parent beforeHook(), which moves changes out of the working
// tree, this should be an empty diff.
$this->priorDiff = $this->repository->getDiffOperator()->compareTo();
}

public function afterAction(RunnerHook $hook, Config\Action $action): void
{
$afterDiff = $this->repository->getDiffOperator()->compareTo();

// Did this action make any changes?
if ($afterDiff != $this->priorDiff) {
$this->actionChanges[$action->getAction()] = $afterDiff;
}

$this->priorDiff = $afterDiff;
}

public function afterHook(RunnerHook $hook): void
{
parent::afterHook($hook);

if (count($this->actionChanges) > 0) {
$message = '';
foreach ($this->actionChanges as $action => $changes) {
$message .= '<error>Action \'' . $action
. '\' on hook ' . $hook->getName()
. ' modified files</error>'
. PHP_EOL;
}

throw new ActionChangedFiles($message);
}
}
}
56 changes: 56 additions & 0 deletions src/Plugin/Hook/Example.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace CaptainHook\App\Plugin\Hook;

use CaptainHook\App\Config;
use CaptainHook\App\Plugin;
use CaptainHook\App\Runner\Hook as RunnerHook;

class Example extends Base implements Plugin\Hook
{
/**
* Runs before the hook.
*
* @param RunnerHook $hook This is the current hook that's running.
*/
public function beforeHook(RunnerHook $hook): void
{
$this->io->write(['<fg=magenta>Plugin ' . self::class . '::beforeHook()</>', '']);
}

/**
* Runs before each action.
*
* @param RunnerHook $hook This is the current hook that's running.
* @param Config\Action $action This is the configuration for action that will
* run immediately following this method.
*/
public function beforeAction(RunnerHook $hook, Config\Action $action): void
{
$this->io->write(['', ' <fg=cyan>Plugin ' . self::class . '::beforeAction()</>']);
}

/**
* Runs after each action.
*
* @param RunnerHook $hook This is the current hook that's running.
* @param Config\Action $action This is the configuration for action that just
* ran immediately before this method.
*/
public function afterAction(RunnerHook $hook, Config\Action $action): void
{
$this->io->write(['', ' <fg=cyan>Plugin ' . self::class . '::afterAction()</>', '']);
}

/**
* Runs after the hook.
*
* @param RunnerHook $hook This is the current hook that's running.
*/
public function afterHook(RunnerHook $hook): void
{
$this->io->write(['', '<fg=magenta>Plugin ' . self::class . '::afterHook()</>']);
}
}
27 changes: 27 additions & 0 deletions src/Plugin/Hook/Exception/ActionChangedFiles.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

/**
* This file is part of CaptainHook
*
* (c) Sebastian Feldmann <sf@sebastian-feldmann.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace CaptainHook\App\Plugin\Hook\Exception;

use CaptainHook\App\Exception\CaptainHookException;
use RuntimeException;

/**
* Class ActionChangedFiles
*
* @package CaptainHook
* @author Sebastian Feldmann <sf@sebastian-feldmann.info>
* @link https://github.com/captainhookphp/captainhook
* @since Class available since Release 5.11.0.
*/
class ActionChangedFiles extends RuntimeException implements CaptainHookException
{
}
Loading