Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow running only actions specified on the command line #137

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
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(), '');
}
}
}
Comment on lines +97 to +103
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about creating three separate commands (e.g. hook:pre-commit:list-actions) instead of adding those as options with need for this magic?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of separating the list functionality from the execution functionality
I think one command is enough captainhook list-actions pre-commit :)
Then this can be done in a separate Runner\ListActions
Maybe we can move the getHookConfigsToHandle functionality to the Config class. This way we would not have duplicate code in Runner\Hook and Runner\ListActions

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ramsey have you seen this comment as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds good to me.

}

/**
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()));
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did this because I found that captainhook didn't respect the verbosity option (i.e., -v, -vv, and -vvv) passed on the command line. It was always setting the output to whatever was configured in captainhook.json.

IMO, if you pass verbosity on the command line, it should override whatever is configured in captainhook.json.


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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method does nothing here. It only has a comment in it, but it was showing up in coverage reports as an uncovered method.

*/
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
{
}