diff --git a/bump b/bump
new file mode 100644
index 000000000..e440e5c84
--- /dev/null
+++ b/bump
@@ -0,0 +1 @@
+3
\ No newline at end of file
diff --git a/doc/installation/exotic.md b/doc/installation/exotic.md
index bd8e74306..27598ddcd 100644
--- a/doc/installation/exotic.md
+++ b/doc/installation/exotic.md
@@ -18,7 +18,8 @@ You can manipulate how GrumPHP works based on settings in your composer file:
"extra": {
"grumphp": {
"config-default-path": "path/to/grumphp.yml",
- "project-path": "path/to/your/project/folder"
+ "project-path": "path/to/your/project/folder",
+ "disable-plugin": false
}
}
}
@@ -48,6 +49,14 @@ After changing a parameter, you might want to re-initialize your git hooks:
php ./vendor/bin/grumphp git:init
```
+**disable-plugin**
+
+*Default: false*
+
+In some cases, you don't want composer to initialise GrumPHP automatically.
+For example: on global installations activating the plugin is questionable.
+Luckily you can opt-out on this behaviour.
+
## Changing paths by using environment variables
diff --git a/resources/config/console.yml b/resources/config/console.yml
index f67abd82f..e2987fbe8 100644
--- a/resources/config/console.yml
+++ b/resources/config/console.yml
@@ -22,6 +22,7 @@ services:
arguments:
- '@config'
- '@grumphp.util.filesystem'
+ - '@GrumPHP\Util\Paths'
tags:
- { name: 'console.command' }
GrumPHP\Console\Command\RunCommand:
diff --git a/src/Composer/DevelopmentIntegrator.php b/src/Composer/DevelopmentIntegrator.php
index 7b91c4a47..a7db747c4 100644
--- a/src/Composer/DevelopmentIntegrator.php
+++ b/src/Composer/DevelopmentIntegrator.php
@@ -7,7 +7,7 @@
use Composer\Script\Event;
use GrumPHP\Collection\ProcessArgumentsCollection;
use GrumPHP\Process\ProcessFactory;
-use Symfony\Component\Filesystem\Filesystem;
+use GrumPHP\Util\Filesystem;
class DevelopmentIntegrator
{
@@ -19,11 +19,11 @@ public static function integrate(Event $event): void
$filesystem = new Filesystem();
$composerBinDir = $event->getComposer()->getConfig()->get('bin-dir');
- $executable = getcwd().'/bin/grumphp';
+ $executable = dirname(__DIR__, 2).$filesystem->ensureValidSlashes('/bin/grumphp');
$composerExecutable = $composerBinDir.'/grumphp';
$filesystem->copy(
- self::noramlizePath($executable),
- self::noramlizePath($composerExecutable)
+ $filesystem->ensureValidSlashes($executable),
+ $filesystem->ensureValidSlashes($composerExecutable)
);
$commandlineArgs = ProcessArgumentsCollection::forExecutable($composerExecutable);
@@ -42,9 +42,4 @@ public static function integrate(Event $event): void
$event->getIO()->write(''.$process->getOutput().'');
}
-
- private static function noramlizePath(string $path): string
- {
- return str_replace('/', DIRECTORY_SEPARATOR, $path);
- }
}
diff --git a/src/Composer/GrumPHPPlugin.php b/src/Composer/GrumPHPPlugin.php
index 8bac678f2..4212b6575 100644
--- a/src/Composer/GrumPHPPlugin.php
+++ b/src/Composer/GrumPHPPlugin.php
@@ -6,47 +6,55 @@
use Composer\Composer;
use Composer\DependencyResolver\Operation\InstallOperation;
+use Composer\DependencyResolver\Operation\OperationInterface;
use Composer\DependencyResolver\Operation\UninstallOperation;
use Composer\DependencyResolver\Operation\UpdateOperation;
use Composer\EventDispatcher\EventSubscriberInterface;
-use Composer\Installer\PackageEvents;
use Composer\Installer\PackageEvent;
+use Composer\Installer\PackageEvents;
use Composer\IO\IOInterface;
use Composer\Package\PackageInterface;
use Composer\Plugin\PluginInterface;
use Composer\Script\Event;
use Composer\Script\ScriptEvents;
-use GrumPHP\Collection\ProcessArgumentsCollection;
-use GrumPHP\Console\Command\ConfigureCommand;
-use GrumPHP\Console\Command\Git\DeInitCommand;
-use GrumPHP\Console\Command\Git\InitCommand;
-use GrumPHP\Locator\ExternalCommand;
-use GrumPHP\Process\ProcessFactory;
-use Symfony\Component\Process\ExecutableFinder;
class GrumPHPPlugin implements PluginInterface, EventSubscriberInterface
{
- const PACKAGE_NAME = 'phpro/grumphp';
+ private const PACKAGE_NAME = 'phpro/grumphp';
+ private const APP_NAME = 'grumphp';
+ private const COMMAND_CONFIGURE = 'configure';
+ private const COMMAND_INIT = 'git:init';
+ private const COMMAND_DEINIT = 'git:deinit';
/**
* @var Composer
*/
- protected $composer;
+ private $composer;
/**
* @var IOInterface
*/
- protected $io;
+ private $io;
+
+ /**
+ * @var bool
+ */
+ private $handledPackageEvent = false;
+
+ /**
+ * @var bool
+ */
+ private $configureScheduled = false;
/**
* @var bool
*/
- protected $configureScheduled = false;
+ private $initScheduled = false;
/**
* @var bool
*/
- protected $initScheduled = false;
+ private $hasBeenRemoved = false;
/**
* {@inheritdoc}
@@ -65,127 +73,213 @@ public function activate(Composer $composer, IOInterface $io): void
public static function getSubscribedEvents(): array
{
return [
- PackageEvents::POST_PACKAGE_INSTALL => 'postPackageInstall',
- PackageEvents::POST_PACKAGE_UPDATE => 'postPackageUpdate',
- PackageEvents::PRE_PACKAGE_UNINSTALL => 'prePackageUninstall',
+ PackageEvents::PRE_PACKAGE_INSTALL => 'detectGrumphpAction',
+ PackageEvents::POST_PACKAGE_INSTALL => 'detectGrumphpAction',
+ PackageEvents::PRE_PACKAGE_UPDATE => 'detectGrumphpAction',
+ PackageEvents::PRE_PACKAGE_UNINSTALL => 'detectGrumphpAction',
ScriptEvents::POST_INSTALL_CMD => 'runScheduledTasks',
ScriptEvents::POST_UPDATE_CMD => 'runScheduledTasks',
];
}
/**
- * When this package is updated, the git hook is also initialized.
+ * This method can be called by pre/post package events;
+ * We make sure to only run it once. This way Grumphp won't execute multiple times.
+ * The goal is to run it as fast as possible.
+ * For first install, this should also happen on POST install (because otherwise the plugin doesn't exist yet)
*/
- public function postPackageInstall(PackageEvent $event): void
+ public function detectGrumphpAction(PackageEvent $event): void
{
- /** @var InstallOperation $operation */
- $operation = $event->getOperation();
- $package = $operation->getPackage();
+ if ($this->handledPackageEvent || !$this->guardPluginIsEnabled()) {
+ $this->handledPackageEvent = true;
+ return;
+ }
- if (!$this->guardIsGrumPhpPackage($package)) {
+ $this->handledPackageEvent = true;
+
+ $grumPhpOperations = $this->detectGrumphpOperations($event->getOperations());
+ if (!count($grumPhpOperations)) {
return;
}
- // Schedule init when command is completed
- $this->configureScheduled = true;
- $this->initScheduled = true;
- }
+ // Check all GrumPHP operations to see if they are unanimously removing GrumPHP
+ // For example: an update might trigger an uninstall first - but we don't care about that.
+ $removalScheduled = array_reduce(
+ $grumPhpOperations,
+ function (?bool $theVote, OperationInterface $operation): bool {
+ $myVote = $operation instanceof UninstallOperation;
- /**
- * When this package is updated, the git hook is also updated.
- */
- public function postPackageUpdate(PackageEvent $event): void
- {
- /** @var UpdateOperation $operation */
- $operation = $event->getOperation();
- $package = $operation->getTargetPackage();
+ return null === $theVote ? $myVote : ($theVote && $myVote);
+ },
+ null
+ );
- if (!$this->guardIsGrumPhpPackage($package)) {
+ // Remove immediately once when we are positive about removal. (now that our dependencies are still there)
+ if ($removalScheduled) {
+ $this->runGrumPhpCommand(self::COMMAND_DEINIT);
+ $this->hasBeenRemoved = true;
return;
}
- // Schedule init when command is completed
+ // Schedule install at the end of the process if we don't need to uninstall
$this->initScheduled = true;
+ $this->configureScheduled = true;
}
/**
- * When this package is uninstalled, the generated git hooks need to be removed.
+ * Runs the scheduled tasks after an update / install command.
*/
- public function prePackageUninstall(PackageEvent $event): void
+ public function runScheduledTasks(Event $event): void
{
- /** @var UninstallOperation $operation */
- $operation = $event->getOperation();
- $package = $operation->getPackage();
-
- if (!$this->guardIsGrumPhpPackage($package)) {
- return;
+ if ($this->configureScheduled) {
+ $this->runGrumPhpCommand(self::COMMAND_CONFIGURE);
}
- // First remove the hook, before everything is deleted!
- $this->deInitGitHook();
+ if ($this->initScheduled) {
+ $this->runGrumPhpCommand(self::COMMAND_INIT);
+ }
}
- public function runScheduledTasks(Event $event): void
+ /**
+ * @param OperationInterface[] $operations
+ *
+ * @return OperationInterface[]
+ */
+ private function detectGrumphpOperations(array $operations): array
{
- if ($this->initScheduled) {
- $this->runGrumPhpCommand(ConfigureCommand::COMMAND_NAME);
- }
- if ($this->initScheduled) {
- $this->initGitHook();
- }
+ return array_values(array_filter(
+ $operations,
+ function (OperationInterface $operation): bool {
+ $package = $this->detectOperationPackage($operation);
+ return $this->guardIsGrumPhpPackage($package);
+ }
+ ));
}
- protected function guardIsGrumPhpPackage(PackageInterface $package): bool
+ private function detectOperationPackage(OperationInterface $operation): ?PackageInterface
{
- return self::PACKAGE_NAME === $package->getName();
+ switch (true) {
+ case $operation instanceof UpdateOperation:
+ return $operation->getTargetPackage();
+ case $operation instanceof InstallOperation:
+ case $operation instanceof UninstallOperation:
+ return $operation->getPackage();
+ default:
+ return null;
+ }
}
/**
- * Initialize git hooks.
+ * This method also detects aliases / replaces statements which makes grumphp-shim possible.
*/
- protected function initGitHook(): void
+ private function guardIsGrumPhpPackage(?PackageInterface $package): bool
{
- $this->runGrumPhpCommand(InitCommand::COMMAND_NAME);
+ if (!$package) {
+ return false;
+ }
+
+ $normalizedNames = array_map('strtolower', $package->getNames());
+
+ return in_array(self::PACKAGE_NAME, $normalizedNames, true);
}
- /**
- * Deinitialize git hooks.
- */
- protected function deInitGitHook(): void
+ private function guardPluginIsEnabled(): bool
{
- $this->runGrumPhpCommand(DeInitCommand::COMMAND_NAME);
+ $extra = $this->composer->getPackage()->getExtra();
+
+ return !(bool) ($extra['grumphp']['disable-plugin'] ?? false);
}
- /**
- * Run the GrumPHP console to (de)init the git hooks.
- */
- protected function runGrumPhpCommand(string $command): void
+ private function runGrumPhpCommand(string $command): void
{
- $config = $this->composer->getConfig();
- $commandLocator = new ExternalCommand($config->get('bin-dir'), new ExecutableFinder());
- $executable = $commandLocator->locate('grumphp');
+ if (!$grumphp = $this->detectGrumphpExecutable()) {
+ $this->pluginErrored('no-exectuable');
+ return;
+ }
+
+ // Respect composer CLI settings
+ $ansi = $this->io->isDecorated() ? '--ansi' : '--no-ansi';
+ $silent = $command === self::COMMAND_CONFIGURE ? '--silent' : '';
+ $interaction = $this->io->isInteractive() ? '' : '--no-interaction';
- $commandlineArgs = ProcessArgumentsCollection::forExecutable($executable);
- $commandlineArgs->add($command);
- $commandlineArgs->add('--no-interaction');
+ // Windows requires double double quotes
+ // https://bugs.php.net/bug.php?id=49139
+ $windowsIsInsane = function (string $command) {
+ return $this->runsOnWindows() ? '"'.$command.'"' : $command;
+ };
- $process = ProcessFactory::fromArguments($commandlineArgs);
+ // Run command
+ $process = @proc_open(
+ $run = $windowsIsInsane(implode(' ', array_map(
+ function (string $argument): string {
+ return escapeshellarg($argument);
+ },
+ array_filter([$grumphp, $command, $ansi, $silent, $interaction])
+ ))),
+ // Map process to current io
+ $descriptorspec = array(
+ 0 => array('file', 'php://stdin', 'r'),
+ 1 => array('file', 'php://stdout', 'w'),
+ 2 => array('file', 'php://stderr', 'w'),
+ ),
+ $pipes = []
+ );
// Check executable which is running:
- if ($this->io->isVeryVerbose()) {
- $this->io->write('Running process : '.$process->getCommandLine());
+ if ($this->io->isVerbose()) {
+ $this->io->write('Running process : '.$run);
+ }
+
+ if (!is_resource($process)) {
+ $this->pluginErrored('no-process');
+ return;
}
- $process->run();
- if (!$process->isSuccessful()) {
- $this->io->write(
- 'GrumPHP can not sniff your commits. Did you specify the correct git-dir?'
- );
- $this->io->write(''.$process->getErrorOutput().'');
+ // Loop on process until it exits normally.
+ do {
+ $status = proc_get_status($process);
+ } while ($status && $status['running']);
+
+ $exitCode = $status['exitcode'] ?? -1;
+ proc_close($process);
+ if ($exitCode !== 0) {
+ $this->pluginErrored('invalid-exit-code');
return;
}
+ }
+
+ private function detectGrumphpExecutable(): ?string
+ {
+ $config = $this->composer->getConfig();
+ $binDir = $this->ensurePlatformSpecificDirectorySeparator((string) $config->get('bin-dir'));
+ $suffixes = $this->runsOnWindows() ? ['.bat', ''] : ['.phar', ''];
+
+ return array_reduce(
+ $suffixes,
+ function (?string $carry, string $suffix) use ($binDir): ?string {
+ $possiblePath = $binDir.DIRECTORY_SEPARATOR.self::APP_NAME.$suffix;
+ if ($carry || !file_exists($possiblePath)) {
+ return $carry;
+ }
+
+ return $possiblePath;
+ }
+ );
+ }
+
+ private function runsOnWindows(): bool
+ {
+ return defined('PHP_WINDOWS_VERSION_BUILD');
+ }
- $this->io->write(''.$process->getOutput().'');
+ private function ensurePlatformSpecificDirectorySeparator(string $path): string
+ {
+ return str_replace('/', DIRECTORY_SEPARATOR, $path);
+ }
+
+ private function pluginErrored(string $reason)
+ {
+ $this->io->writeError('GrumPHP can not sniff your commits! ('.$reason.')');
}
}
diff --git a/src/Configuration/ContainerBuilder.php b/src/Configuration/ContainerBuilder.php
index 5f286f4b3..587451b0a 100644
--- a/src/Configuration/ContainerBuilder.php
+++ b/src/Configuration/ContainerBuilder.php
@@ -4,17 +4,18 @@
namespace GrumPHP\Configuration;
+use GrumPHP\Util\Filesystem;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass;
use Symfony\Component\DependencyInjection\ContainerBuilder as SymfonyContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
-use Symfony\Component\Filesystem\Filesystem;
final class ContainerBuilder
{
public static function buildFromConfiguration(string $path): SymfonyContainerBuilder
{
+ $filesystem = new Filesystem();
$container = new SymfonyContainerBuilder();
// Add compiler passes:
@@ -28,7 +29,8 @@ public static function buildFromConfiguration(string $path): SymfonyContainerBui
$container->addCompilerPass(new AddConsoleCommandPass());
// Load basic service file + custom user configuration
- $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../../resources/config'));
+ $configDir = dirname(__DIR__, 2).$filesystem->ensureValidSlashes('/resources/config');
+ $loader = new YamlFileLoader($container, new FileLocator($configDir));
$loader->load('console.yml');
$loader->load('formatter.yml');
$loader->load('linters.yml');
@@ -41,7 +43,6 @@ public static function buildFromConfiguration(string $path): SymfonyContainerBui
$loader->load('util.yml');
// Load grumphp.yml file:
- $filesystem = new Filesystem();
if ($filesystem->exists($path)) {
$loader->load($path);
}
diff --git a/src/Console/Command/ConfigureCommand.php b/src/Console/Command/ConfigureCommand.php
index 6728bd741..5871532b5 100644
--- a/src/Console/Command/ConfigureCommand.php
+++ b/src/Console/Command/ConfigureCommand.php
@@ -7,6 +7,7 @@
use Exception;
use GrumPHP\Configuration\GrumPHP;
use GrumPHP\Util\Filesystem;
+use GrumPHP\Util\Paths;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
@@ -29,18 +30,23 @@ class ConfigureCommand extends Command
* @var Filesystem
*/
protected $filesystem;
+ /**
+ * @var Paths
+ */
+ private $paths;
/**
* @var InputInterface
*/
protected $input;
- public function __construct(GrumPHP $config, Filesystem $filesystem)
+ public function __construct(GrumPHP $config, Filesystem $filesystem, Paths $paths)
{
parent::__construct();
$this->config = $config;
$this->filesystem = $filesystem;
+ $this->paths = $paths;
}
public static function getDefaultName(): string
@@ -59,6 +65,12 @@ protected function configure(): void
InputOption::VALUE_NONE,
'Forces overwriting the configuration file when it already exists.'
);
+ $this->addOption(
+ 'silent',
+ null,
+ InputOption::VALUE_NONE,
+ 'Only output what really matters.'
+ );
}
/**
@@ -67,11 +79,10 @@ protected function configure(): void
public function execute(InputInterface $input, OutputInterface $output): int
{
$this->input = $input;
-
- $grumphpConfigName = $this->input->getOption('config');
+ $configFile = $this->paths->getConfigFile();
$force = $input->getOption('force');
- if ($this->filesystem->exists($grumphpConfigName) && !$force) {
- if ($input->isInteractive()) {
+ if ($this->filesystem->exists($configFile) && !$force) {
+ if (!$input->getOption('silent')) {
$output->writeln('GrumPHP is already configured!');
}
@@ -87,14 +98,14 @@ public function execute(InputInterface $input, OutputInterface $output): int
}
// Check write action
- $written = $this->writeConfiguration($configuration);
+ $written = $this->writeConfiguration($configFile, $configuration);
if (!$written) {
$output->writeln('The configuration file could not be saved. Give me some permissions!');
return 1;
}
- if ($input->isInteractive()) {
+ if (!$input->getOption('silent')) {
$output->writeln('GrumPHP is configured and ready to kick ass!');
}
@@ -113,6 +124,7 @@ protected function buildConfiguration(InputInterface $input, OutputInterface $ou
'Do you want to create a grumphp.yml file?',
'Yes'
);
+
$question = new ConfirmationQuestion($questionString, true);
if (!$helper->ask($input, $output, $question)) {
return [];
@@ -122,7 +134,7 @@ protected function buildConfiguration(InputInterface $input, OutputInterface $ou
$tasks = [];
if ($input->isInteractive()) {
$question = new ChoiceQuestion(
- 'Which tasks do you want to run?',
+ $this->createQuestionString('Which tasks do you want to run?', null, ''),
$this->config->getRegisteredTasks()
);
$question->setMultiselect(true);
@@ -142,16 +154,15 @@ protected function buildConfiguration(InputInterface $input, OutputInterface $ou
protected function createQuestionString(string $question, string $default = null, string $separator = ':'): string
{
return null !== $default ?
- sprintf('%s [%s]%s ', $question, $default, $separator) :
- sprintf('%s%s ', $question, $separator);
+ sprintf('%s [%s]%s ', $question, $default, $separator) :
+ sprintf('%s%s ', $question, $separator);
}
- protected function writeConfiguration(array $configuration): bool
+ protected function writeConfiguration(string $configFile, array $configuration): bool
{
try {
$yaml = Yaml::dump($configuration);
- $grumphpConfigName = $this->input->getOption('config');
- $this->filesystem->dumpFile($grumphpConfigName, $yaml);
+ $this->filesystem->dumpFile($configFile, $yaml);
return true;
} catch (Exception $e) {
diff --git a/src/Util/Paths.php b/src/Util/Paths.php
index 844ce9c76..badfe8445 100644
--- a/src/Util/Paths.php
+++ b/src/Util/Paths.php
@@ -74,6 +74,11 @@ public function getBinDir(): string
return $this->guessedPaths->getBinDir();
}
+ public function getConfigFile(): string
+ {
+ return $this->guessedPaths->getConfigFile();
+ }
+
public function getProjectDirRelativeToGitDir(): string
{
return $this->filesystem->makePathRelative(
diff --git a/test/E2E/AbstractE2ETestCase.php b/test/E2E/AbstractE2ETestCase.php
index 167ec8668..f289a0d3a 100644
--- a/test/E2E/AbstractE2ETestCase.php
+++ b/test/E2E/AbstractE2ETestCase.php
@@ -290,6 +290,7 @@ protected function installComposer(string $path, array $arguments = [])
'install',
'--optimize-autoloader',
'--no-interaction',
+ '-vvv'
],
$arguments),
$path