diff --git a/composer.json b/composer.json index 038fef45..39842f16 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "php-http/httplug": "^2.0", "psr/http-client": "^1.0", "psr/http-message": "^1.0", + "symfony/process": "^4.2", "thecodingmachine/safe": "^0.1.14", "zendframework/zend-diactoros": "^2.1" }, @@ -30,6 +31,7 @@ "psalm/plugin-phpunit": "^0.5.4", "roave/no-leaks": "^1.1", "squizlabs/php_codesniffer": "^3.4", + "thecodingmachine/phpstan-safe-rule": "^0.1.3", "vimeo/psalm": "^3.2" }, "config": { diff --git a/composer.lock b/composer.lock index c5553d0d..c403c26c 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": "47b50bf0289d3f0544bd00501fe36265", + "content-hash": "480acb36f3f455b3f1928912d5890e74", "packages": [ { "name": "beberlei/assert", @@ -675,6 +675,55 @@ ], "time": "2019-02-23T15:17:42+00:00" }, + { + "name": "symfony/process", + "version": "v4.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "1e6cbb41dadcaf29e0db034d6ad0d039a9df06e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/1e6cbb41dadcaf29e0db034d6ad0d039a9df06e6", + "reference": "1e6cbb41dadcaf29e0db034d6ad0d039a9df06e6", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2-dev" + } + }, + "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": "Symfony Process Component", + "homepage": "https://symfony.com", + "time": "2019-03-10T20:07:02+00:00" + }, { "name": "thecodingmachine/safe", "version": "v0.1.14", @@ -4398,6 +4447,54 @@ ], "time": "2019-02-06T07:57:58+00:00" }, + { + "name": "thecodingmachine/phpstan-safe-rule", + "version": "v0.1.3", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/phpstan-safe-rule.git", + "reference": "00f4845905feb5240ca62fb799e3c51ba85c9230" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/phpstan-safe-rule/zipball/00f4845905feb5240ca62fb799e3c51ba85c9230", + "reference": "00f4845905feb5240ca62fb799e3c51ba85c9230", + "shasum": "" + }, + "require": { + "php": "^7.1", + "phpstan/phpstan": "^0.10 | ^0.11", + "thecodingmachine/safe": "^0.1.11" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^7.5.2", + "squizlabs/php_codesniffer": "^3.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.1-dev" + } + }, + "autoload": { + "psr-4": { + "TheCodingMachine\\Safe\\PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "David NĂ©grier", + "email": "d.negrier@thecodingmachine.com" + } + ], + "description": "A PHPStan rule to detect safety issues. Must be used in conjunction with thecodingmachine/safe", + "time": "2019-03-07T13:52:42+00:00" + }, { "name": "theseer/tokenizer", "version": "1.1.2", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index f7f0e674..cf60658d 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -9,3 +9,4 @@ includes: - vendor/phpstan/phpstan-beberlei-assert/extension.neon - vendor/phpstan/phpstan-phpunit/extension.neon - vendor/phpstan/phpstan-strict-rules/rules.neon + - vendor/thecodingmachine/phpstan-safe-rule/phpstan-safe-rule.neon diff --git a/public/index.php b/public/index.php index 4d0616e3..4ff1a434 100755 --- a/public/index.php +++ b/public/index.php @@ -21,22 +21,16 @@ use Http\Discovery\Psr17FactoryDiscovery; use Psr\Http\Message\UriInterface; use RuntimeException; -use UnexpectedValueException; +use Symfony\Component\Process\Process; use Zend\Diactoros\ServerRequestFactory; use const E_NOTICE; use const E_STRICT; use const E_WARNING; -use const PHP_EOL; use function array_map; use function assert; -use function escapeshellarg; -use function exec; -use function implode; +use function explode; use function is_array; -use function is_string; -use function Safe\chdir; use function Safe\file_put_contents; -use function Safe\getcwd; use function Safe\preg_match; use function Safe\preg_replace; use function Safe\sprintf; @@ -58,68 +52,40 @@ static function ($errorCode, $message = '', $file = '', $line = 0) : void { $buildDir = __DIR__ . '/../build'; - $runInPath = static function (callable $function, string $path) { - $originalPath = getcwd(); + $cleanBuildDir = static function () use ($buildDir) : void { + (new Process(['rm', '-rf', $buildDir])) + ->mustRun(); - chdir($path); - - try { - $returnValue = $function(); - } finally { - chdir($originalPath); - } - - return $returnValue; + (new Process(['mkdir', $buildDir])) + ->mustRun(); }; - $execute = static function (string $commandString) : array { - // may the gods forgive me for this in-lined command addendum, but I CBA to - // fix proc_open's handling of exit codes. - exec($commandString . ' 2>&1', $output, $result); - - if ($result !== 0) { - throw new UnexpectedValueException(sprintf( - 'Command failed: "%s" "%s"', - $commandString, - implode(PHP_EOL, $output) - )); - } - - return $output; + $cloneRepository = static function (UriInterface $repositoryUri, string $targetPath) : void { + (new Process(['git', 'clone', $repositoryUri->__toString(), $targetPath])) + ->mustRun(); }; - $cleanBuildDir = static function () use ($buildDir, $execute) : void { - $execute('rm -rf ' . escapeshellarg($buildDir)); - $execute('mkdir ' . escapeshellarg($buildDir)); - }; + $getBranches = static function (string $repositoryDirectory) : MergeTargetCandidateBranches { + (new Process(['git', 'fetch'], $repositoryDirectory)) + ->mustRun(); - $cloneRepository = static function (UriInterface $repositoryUri, string $targetPath) use ($execute) : void { - $execute( - 'git clone ' - . escapeshellarg($repositoryUri->__toString()) - . ' ' . escapeshellarg($targetPath) + $branches = explode( + "\n", + (new Process(['git', 'branch', '-r'], $repositoryDirectory)) + ->mustRun() + ->getOutput() ); - }; - $getBranches = static function (string $repositoryDirectory) use ( - $runInPath, - $execute - ) : MergeTargetCandidateBranches { - return $runInPath(static function () use ($execute) { - $execute('git fetch'); - - return MergeTargetCandidateBranches::fromAllBranches(...array_map(static function (string $branch) : BranchName { - $sanitizedBranch = preg_replace( - '/^(?:remotes\\/)?origin\\//', - '', - trim($branch, "* \t\n\r\0\x0B") - ); - - assert(is_string($sanitizedBranch)); - - return BranchName::fromName($sanitizedBranch); - }, $execute('git branch -r'))); - }, $repositoryDirectory); + return MergeTargetCandidateBranches::fromAllBranches(...array_map(static function (string $branch) : BranchName { + /** @var string $sanitizedBranch */ + $sanitizedBranch = preg_replace( + '/^(?:remotes\\/)?origin\\//', + '', + trim($branch, "* \t\n\r\0\x0B") + ); + + return BranchName::fromName($sanitizedBranch); + }, $branches)); }; $createTag = static function ( @@ -128,55 +94,45 @@ static function ($errorCode, $message = '', $file = '', $line = 0) : void { string $tagName, string $changelog, SecretKeyId $keyId - ) use ( - $runInPath, - $execute ) : void { $tagFileName = tempnam(sys_get_temp_dir(), 'created_tag'); file_put_contents($tagFileName, $changelog); - $runInPath(static function () use ($sourceBranch, $tagName, $keyId, $tagFileName, $execute) : void { - $execute(sprintf('git checkout "%s"', $sourceBranch->name())); - $execute(sprintf( - 'git tag %s -F %s --cleanup=verbatim --local-user=%s', - escapeshellarg($tagName), - escapeshellarg($tagFileName), - escapeshellarg($keyId->id()) - )); - }, $repositoryDirectory); + (new Process(['git', 'checkout', $sourceBranch->name()], $repositoryDirectory)) + ->mustRun(); + + (new Process( + ['git', 'tag', $tagName, '-F', $tagFileName, '--cleanup=verbatim', '--local-user=' . $keyId->id()], + $repositoryDirectory + )) + ->mustRun(); }; $push = static function ( string $repositoryDirectory, string $symbol, ?string $alias = null - ) use ( - $runInPath, - $execute ) : void { - $runInPath(static function () use ($symbol, $alias, $execute) : void { - $execute(sprintf( - 'git push origin %s', - $alias !== null ? escapeshellarg($symbol) . ':' . escapeshellarg($alias) : escapeshellarg($symbol) - )); - }, $repositoryDirectory); + $pushedRef = $alias !== null ? $symbol . ':' . $alias : $symbol; + + (new Process(['git', 'push', 'origin', $pushedRef], $repositoryDirectory)) + ->mustRun(); }; - $importGpgKey = static function (string $keyContents) use ($execute) : SecretKeyId { + $importGpgKey = static function (string $keyContents) : SecretKeyId { $keyFileName = tempnam(sys_get_temp_dir(), 'imported-key'); file_put_contents($keyFileName, $keyContents); - $output = $execute(sprintf('gpg --import %s 2>&1 | grep "secret key imported"', escapeshellarg($keyFileName))); + $output = (new Process(['gpg', '--import', $keyFileName])) + ->mustRun() + ->getErrorOutput(); Assert::that($output) - ->keyExists(0); - - Assert::that($output[0]) - ->regex('/key\\s+([A-F0-9]+):\s+/i'); + ->regex('/key\\s+([A-F0-9]+):\\s+secret\\s+key\\s+imported/im'); - preg_match('/key\\s+([A-F0-9]+):\s+/i', $output[0], $matches); + preg_match('/key\\s+([A-F0-9]+):\\s+secret\\s+key\\s+imported/im', $output, $matches); assert(is_array($matches));