diff --git a/README.md b/README.md index dd60217..ec1380e 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,10 @@ This is just a starting point. - Code formating : [php-cs-fixer](https://github.com/FriendsOfPhp/PHP-CS-Fixer) configured using prestashop standard, ready to use out of the box. - Code analysis : [phpstan](https://phpstan.org/) almost ready to use with Prestashop standard, it asks a question then you're ready. - `fill-indexes` command, to add required index.php files. (see below for details) +- git pre-commit hook installer (details below) More tools will come - [prestashop/header-stamp](https://github.com/PrestaShopCorp/header-stamp/) (update license header in files) -- a tool to install a precommit hook to ensure everything is ok before commiting. - GitHub actions - ... @@ -42,23 +42,10 @@ You can even take an additionnal step by [defining an alias](https://duckduckgo. ## Provided commands -* psdt:php-cs-fixer -* psdt:phpstan -* psdt:fill-indexes - -### fill-indexes - -`composer psdt:fill-indexes` - -Add the missing index.php files on each folder. -Existing index.php files are not overriden. - -This is a security requirement of Prestashop to avoid the contents to be listed. - -More information [on the official documentation](https://devdocs.prestashop.com/1.7/modules/sell/techvalidation-checklist/#a-file-indexphp-exists-in-each-folder). - -I can't include [prestashop/autoindex](https://github.com/PrestaShopCorp/autoindex) because [it targets php 5.6](https://github.com/PrestaShopCorp/autoindex/blob/92e10242f94a99163dece280f6bd7b7c2b79c158/composer.json#L23) and has other issues. -My replacement is simpler and doesn't require additionnal dependencies. +* [psdt:php-cs-fixer](#fill-indexes) +* [psdt:phpstan](#phpstan) +* [psdt:fill-indexes](#fill-indexes) +* [psdt:install-precommit-hook](#git-pre-commit-hook-installer) (not supported on Windows yet) ### php-cs-fixer @@ -92,6 +79,35 @@ Autoinstallation provided by this package. Allows complying with the [Prestashop standards](https://devdocs.prestashop.com/1.7/development/coding-standards/). + +### fill-indexes + +`composer psdt:fill-indexes` + +Add the missing index.php files on each folder. +Existing index.php files are not overriden. + +This is a security requirement of Prestashop to avoid the contents to be listed. + +More information [on the official documentation](https://devdocs.prestashop.com/1.7/modules/sell/techvalidation-checklist/#a-file-indexphp-exists-in-each-folder). + +I can't include [prestashop/autoindex](https://github.com/PrestaShopCorp/autoindex) because [it targets php 5.6](https://github.com/PrestaShopCorp/autoindex/blob/92e10242f94a99163dece280f6bd7b7c2b79c158/composer.json#L23) and has other issues. +My replacement is simpler and doesn't require additionnal dependencies. + +### Git Pre-commit hook installer + +This command need to be run only once. +It does : +- add a composer script `pre-commit` +- add file `precommit.sh` +- make it executable +- symlink it to .git/hooks/pre-commit + +So before a commit is can be performed the composer script `pre-commit` must succeed (return 0). + +You can tweak the script by just editing the composer script. +You can run the `pre-commit` at any time `composer + ## Installation `composer require --dev sebsept/ps_dev_base` diff --git a/resources/precommit.sh b/resources/precommit.sh new file mode 100644 index 0000000..011ec90 --- /dev/null +++ b/resources/precommit.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env sh +# this file must be symlinked in .git/hooks with the name "pre-commit" +# it fire composer script "pre-commit" which launch phpstan + phpcsfix +# +# install with `ln -s $(pwd)/precommit.sh .git/hooks/pre-commit` + +(cd "./" && echo "start pre-commit hook ..." & exec 'composer' 'run-script' 'pre-commit') \ No newline at end of file diff --git a/src/Command/BaseCommand.php b/src/Command/BaseCommand.php index dd415f8..c66cb1e 100644 --- a/src/Command/BaseCommand.php +++ b/src/Command/BaseCommand.php @@ -90,6 +90,8 @@ final protected function isComposerScriptDefined(): bool */ final protected function runComposerScript(OutputInterface $output): void { + $output->write(sprintf('Now running composer script %s : ', $this->getComposerScriptName())); + $this->getApplication()->find('run-script')->run( new ArrayInput([ 'script' => $this->getComposerScriptName(), diff --git a/src/Command/SebSept/PrecommitHook.php b/src/Command/SebSept/PrecommitHook.php new file mode 100644 index 0000000..03d3978 --- /dev/null +++ b/src/Command/SebSept/PrecommitHook.php @@ -0,0 +1,210 @@ + + * @copyright since 2021 Sébastien Monterisi + * @license https://opensource.org/licenses/MIT MIT License + */ + +declare(strict_types=1); + +namespace SebSept\PsDevToolsPlugin\Command\SebSept; + +use Exception; +use SebSept\PsDevToolsPlugin\Command\PrestashopDevTools\PrestashopDevToolsCsFixer; +use SebSept\PsDevToolsPlugin\Command\PrestashopDevTools\PrestashopDevToolsPhpStan; +use SebSept\PsDevToolsPlugin\Command\ScriptCommand; +use SplFileInfo; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Filesystem\Filesystem; + +final class PrecommitHook extends ScriptCommand // implements ConfigurableCommand +{ + const SOURCE_PRECOMMIT_FILE = __DIR__ . '/../../../resources/precommit.sh'; + const PRECOMMIT_FILE = 'precommit.sh'; + const PRECOMMIT_HOOK_FILE = '.git/hooks/pre-commit'; + + /** @var Filesystem */ + private $fs; + + final protected function configure(): void + { + $this->setName('install-precommit-hook'); + $this->setDescription('Install a git pre-commit hook'); + $this->addOption('reconfigure', null, InputOption::VALUE_NONE, 'rerun configuration and file installations.'); + $this->setHelp( + $this->getDescription() . <<<'HELP' + + * creates file precommit.sh that trigger composer script pre-commit + * adds a composer script pre-commit + * symlinks precommit.sh to .git/hooks/precommit + * next runs will trigger the composer script pre-commit + +The composer scripts by default triggers the other scripts @phpstan, @csfix (dry-run) and composer validate. +This is the default, you can edit the content of the script in composer.json + +The --reconfigure allows to resetup, rerun the 3 first steps and override contents. + +This is tested on GNU/Linux, it probably works fine on MacOS. Probably not on Windows (feedback and fix are welcome). + +HELP + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + try { + $this->fs = new Filesystem(); + /** @var bool $reconfigure */ + $reconfigure = $input->getOption('reconfigure'); + $preCommitHookFileRelativePath = str_replace(__DIR__ . DIRECTORY_SEPARATOR, '', self::PRECOMMIT_HOOK_FILE); + + if ($reconfigure) { + $composerScriptIsDefined = $precommitFileExists = $precommitFileIsSymlinked = $precommitFileIsExecutable = $readyToRun + = false; + } else { + $composerScriptIsDefined = $this->isComposerScriptDefined(); + $precommitFileExists = $this->precommitFileExists(); + $precommitFileIsSymlinked = $this->isPrecommitFileSymlinked(); + $precommitFileIsExecutable = $this->isPrecommitFileExecutable(); + $readyToRun = $composerScriptIsDefined && $precommitFileExists && $precommitFileIsSymlinked && $precommitFileIsExecutable; + } + + $composerScriptIsDefined + ? $this->getIO()->write(sprintf('Composer script %s is installed.', $this->getComposerScriptName())) + : $this->addComposerScript($this->getComposerScripts()); + + $precommitFileExists + ? $this->getIO()->write(sprintf('Precommit file %s is present.', self::PRECOMMIT_FILE)) + : $this->copyPrecommitFile(); + + $precommitFileIsSymlinked + ? $this->getIO()->write(sprintf('%s is symlinked to %s', self::PRECOMMIT_FILE, $preCommitHookFileRelativePath)) + : $this->symLinkPrecommitFile(); + + $precommitFileIsExecutable + ? $this->getIO()->write(sprintf('%s is executable.', $preCommitHookFileRelativePath)) + : $this->makePrecommitFileExecutable(); + + $reconfigure && $this->getIO()->write($this->getAdditionnalHelp()); + + $readyToRun + ? $this->runComposerScript($output) + : $this->getIO()->write(sprintf('run %s command again to run composer script %s', $this->getName(), $this->getComposerScriptName())); + + return 0; + } catch (Exception $exception) { + $this->getIO()->error(sprintf('%s failed : %s', $this->getComposerScriptName(), $exception->getMessage())); + // throw $exception; // for debug purpose. + return 1; + } + } + + /** + * Name of composer script. + * In case it change, it also needs to be changed in the information text displayed. + * + * @see execute() + */ + public function getComposerScriptName(): string + { + return 'pre-commit'; + } + + /** + * Composer scripts to launch depending if tools are configured. + * + * @return array + */ + private function getComposerScripts(): array + { + $scripts = ['composer validate']; + !(new PrestashopDevToolsCsFixer())->isToolConfigured() + ?: array_push($scripts, 'vendor/bin/php-cs-fixer fix --dry-run --ansi'); + !(new PrestashopDevToolsPhpStan())->isToolConfigured() + ?: array_push($scripts, '@phpstan'); + + return $scripts; + } + + private function precommitFileExists(): bool + { + return $this->fs->exists(self::PRECOMMIT_FILE); + } + + private function copyPrecommitFile(): void + { + $this->getIO()->write('Copying pre-commit script file ...', false); + $this->fs->copy(self::SOURCE_PRECOMMIT_FILE, self::PRECOMMIT_FILE, true); + $this->getIO()->write('OK'); + } + + private function isPrecommitFileSymlinked(): bool + { + $gitPrecommitFile = new SplFileInfo(getcwd() . DIRECTORY_SEPARATOR . self::PRECOMMIT_HOOK_FILE); + $precommitFile = new SplFileInfo(getcwd() . DIRECTORY_SEPARATOR . self::PRECOMMIT_FILE); + + return $gitPrecommitFile->isLink() + && $precommitFile->getPathname() === $gitPrecommitFile->getLinkTarget(); + } + + private function symLinkPrecommitFile(): void + { + $gitLink = getcwd() . DIRECTORY_SEPARATOR . self::PRECOMMIT_HOOK_FILE; + $precommitScript = getcwd() . DIRECTORY_SEPARATOR . self::PRECOMMIT_FILE; + + // remove existing git file + if (file_exists($gitLink)) { + $this->getIO()->write('Removing git hook...', false); + if (!unlink($gitLink)) { + throw new Exception('Failed to remove existing pre-commit file ' . $gitLink); + } + $this->getIO()->write(' OK'); + } + + // create symlink + $this->getIO()->write('Symlink to precommit hook...', false); + if (!symlink($precommitScript, $gitLink)) { + throw new Exception('Failed to symlink.'); + } + $this->getIO()->write(' OK'); + } + + private function isPrecommitFileExecutable(): bool + { + return (new SplFileInfo(self::PRECOMMIT_FILE))->isExecutable(); + } + + private function makePrecommitFileExecutable(): void + { + $this->getIO()->write('Make pre-commit script file executable ...', false); + $this->fs->chmod(self::PRECOMMIT_FILE, 0755); + $this->getIO()->write('OK'); + } + + private function getAdditionnalHelp(): string + { + return <<<'INFOS' + +Before the next commit the git precommit hook will be triggered. +If the pre-commit script return 0 (success), commit will be performed, otherwise aborted. +In case, you can't read the precommit script output and find what's wrong +just run composer psdt:pre-commit . + +You can also run this command at any time, before processing the commit, stashing changes for example. +You can edit the script content by editing the script entry pre-commit in composer.json. +INFOS; + } +} diff --git a/src/Composer/PsDevToolsCommandProvider.php b/src/Composer/PsDevToolsCommandProvider.php index 820eb86..02e7da0 100644 --- a/src/Composer/PsDevToolsCommandProvider.php +++ b/src/Composer/PsDevToolsCommandProvider.php @@ -25,6 +25,7 @@ use SebSept\PsDevToolsPlugin\Command\PrestashopDevTools\PrestashopDevToolsPhpStan; use SebSept\PsDevToolsPlugin\Command\SebSept\HelloCommand; use SebSept\PsDevToolsPlugin\Command\SebSept\IndexPhpFiller; +use SebSept\PsDevToolsPlugin\Command\SebSept\PrecommitHook; final class PsDevToolsCommandProvider implements CommandProvider { @@ -35,6 +36,7 @@ public function getCommands(): array new PrestashopDevToolsPhpStan(), new PrestashopDevToolsCsFixer(), new IndexPhpFiller(), + new PrecommitHook(), ]; } }