diff --git a/PhpcsChanged/Cli.php b/PhpcsChanged/Cli.php index 9800270..ba5ba77 100644 --- a/PhpcsChanged/Cli.php +++ b/PhpcsChanged/Cli.php @@ -12,6 +12,7 @@ use PhpcsChanged\ShellOperator; use function PhpcsChanged\{getNewPhpcsMessages, getNewPhpcsMessagesFromFiles, getVersion}; use function PhpcsChanged\SvnWorkflow\{getSvnUnifiedDiff, isNewSvnFile, getSvnBasePhpcsOutput, getSvnNewPhpcsOutput, validateSvnFileExists}; +use function PhpcsChanged\GitWorkflow\{getGitUnifiedDiff, isNewGitFile, getGitBasePhpcsOutput, getGitNewPhpcsOutput, validateGitFileExists}; function getDebug($debugEnabled) { return function(...$outputs) use ($debugEnabled) { @@ -24,10 +25,14 @@ function getDebug($debugEnabled) { }; } -function printErrorAndExit($output) { +function printError($output) { fwrite(STDERR, 'phpcs-changed: Fatal error!' . PHP_EOL); fwrite(STDERR, $output . PHP_EOL); - die(1); +} + +function printErrorAndExit($output) { + printError($output); + exit(1); } function getLongestString(array $strings): int { @@ -51,7 +56,7 @@ function printVersion() { phpcs-changed version {$version} EOF; - die(0); + exit(0); } function printHelp() { @@ -76,12 +81,13 @@ function printHelp() { echo <<' => 'This is the file to check.', + '--svn ' => 'This is the svn-versioned file to check.', + '--git ' => 'This is the git-versioned file to check.', ]); echo <<getMessage()); - exit(0); + $shell->exitWithCode(0); + throw $err; // Just in case we do not actually exit } catch( \Exception $err ) { - printErrorAndExit($err->getMessage()); + $shell->printError($err->getMessage()); + $shell->exitWithCode(1); + throw $err; // Just in case we do not actually exit + } + + $debug('processing data...'); + $fileName = DiffLineMap::getFileNameFromDiff($unifiedDiff); + return getNewPhpcsMessages($unifiedDiff, PhpcsMessages::fromPhpcsJson($oldFilePhpcsOutput, $fileName), PhpcsMessages::fromPhpcsJson($newFilePhpcsOutput, $fileName)); +} + +function runGitWorkflow($gitFile, $options, ShellOperator $shell, callable $debug): PhpcsMessages { + $git = getenv('GIT') ?: 'git'; + $phpcs = getenv('PHPCS') ?: 'phpcs'; + $cat = getenv('CAT') ?: 'cat'; + + try { + $debug('validating executables'); + $shell->validateExecutableExists('git', $git); + $shell->validateExecutableExists('phpcs', $phpcs); + $shell->validateExecutableExists('cat', $cat); + $debug('executables are valid'); + + $phpcsStandard = $options['standard'] ?? null; + $phpcsStandardOption = $phpcsStandard ? ' --standard=' . escapeshellarg($phpcsStandard) : ''; + validateGitFileExists($gitFile, $git, [$shell, 'isReadable'], [$shell, 'executeCommand'], $debug); + $unifiedDiff = getGitUnifiedDiff($gitFile, $git, [$shell, 'executeCommand'], $debug); + $isNewFile = isNewGitFile($gitFile, $git, [$shell, 'executeCommand'], $debug); + $oldFilePhpcsOutput = $isNewFile ? '' : getGitBasePhpcsOutput($gitFile, $git, $phpcs, $phpcsStandardOption, [$shell, 'executeCommand'], $debug); + $newFilePhpcsOutput = getGitNewPhpcsOutput($gitFile, $phpcs, $cat, $phpcsStandardOption, [$shell, 'executeCommand'], $debug); + } catch(NonFatalException $err) { + $debug($err->getMessage()); + $shell->exitWithCode(0); + throw $err; // Just in case we do not actually exit + } catch(\Exception $err) { + $shell->printError($err->getMessage()); + $shell->exitWithCode(1); + throw $err; // Just in case we do not actually exit } $debug('processing data...'); diff --git a/PhpcsChanged/GitWorkflow.php b/PhpcsChanged/GitWorkflow.php new file mode 100644 index 0000000..289bbc0 --- /dev/null +++ b/PhpcsChanged/GitWorkflow.php @@ -0,0 +1,67 @@ + file.php.phpcs phpcs-changed --report json --diff file.php.diff --phpcs-orig file.php.orig.phpcs --phpcs-new file.php.phpcs ``` -Alernatively, we can have the script use svn and phpcs itself: +Alernatively, we can have the script use svn and phpcs itself by using the `--svn` option: ``` phpcs-changed --svn file.php --report json @@ -140,6 +140,13 @@ Both will output something like: } ``` +If the file was versioned by git, we can do the same with the `--git` option: + +``` +phpcs-changed --git file.php --report json +``` + + ### CLI Options You can use `--report` to customize the output type. `full` (the default) is human-readable and `json` prints a JSON object as shown above. These match the phpcs reporters of the same names. diff --git a/bin/phpcs-changed b/bin/phpcs-changed index 37a42ea..d36386e 100755 --- a/bin/phpcs-changed +++ b/bin/phpcs-changed @@ -13,6 +13,7 @@ use function PhpcsChanged\Cli\{ getDebug, runManualWorkflow, runSvnWorkflow, + runGitWorkflow, reportMessagesAndExit }; use PhpcsChanged\UnixShell; @@ -26,6 +27,7 @@ $options = getopt( 'phpcs-orig:', 'phpcs-new:', 'svn:', + 'git:', 'standard:', 'report:', 'debug', @@ -58,4 +60,10 @@ if ($svnFile) { reportMessagesAndExit(runSvnWorkflow($svnFile, $options, $shell, $debug), $reportType); } +$gitFile = $options['git'] ?? null; +if ($gitFile) { + $shell = new UnixShell(); + reportMessagesAndExit(runGitWorkflow($gitFile, $options, $shell, $debug), $reportType); +} + printHelp(); diff --git a/index.php b/index.php index 9557b28..887484e 100644 --- a/index.php +++ b/index.php @@ -5,6 +5,7 @@ use PhpcsChanged\DiffLineMap; use PhpcsChanged\PhpcsMessages; +use PhpcsChanged\ShellException; require_once __DIR__ . '/PhpcsChanged/Version.php'; require_once __DIR__ . '/PhpcsChanged/DiffLine.php'; @@ -17,7 +18,9 @@ require_once __DIR__ . '/PhpcsChanged/JsonReporter.php'; require_once __DIR__ . '/PhpcsChanged/FullReporter.php'; require_once __DIR__ . '/PhpcsChanged/NonFatalException.php'; +require_once __DIR__ . '/PhpcsChanged/ShellException.php'; require_once __DIR__ . '/PhpcsChanged/SvnWorkflow.php'; +require_once __DIR__ . '/PhpcsChanged/GitWorkflow.php'; require_once __DIR__ . '/PhpcsChanged/ShellOperator.php'; require_once __DIR__ . '/PhpcsChanged/UnixShell.php'; @@ -42,7 +45,7 @@ function getNewPhpcsMessagesFromFiles(string $diffFile, string $phpcsOldFile, st $oldFilePhpcsOutput = file_get_contents($phpcsOldFile); $newFilePhpcsOutput = file_get_contents($phpcsNewFile); if (! $unifiedDiff || ! $oldFilePhpcsOutput || ! $newFilePhpcsOutput) { - throw new \Exception('Cannot read input files.'); // TODO: make custom Exception + throw new ShellException('Cannot read input files.'); } return getNewPhpcsMessages( $unifiedDiff, diff --git a/tests/GitWorkflowTest.php b/tests/GitWorkflowTest.php new file mode 100644 index 0000000..dd50751 --- /dev/null +++ b/tests/GitWorkflowTest.php @@ -0,0 +1,277 @@ +assertTrue(isNewGitFile($gitFile, $git, $executeCommand, $debug)); + } + + public function testIsNewGitFileReturnsFalseForOldFile() { + $gitFile = 'foobar.php'; + $git = 'git'; + $executeCommand = function($command) { + if (false !== strpos($command, "git status --short 'foobar.php'")) { + return ' M foobar.php'; // note the leading space + } + }; + $debug = function($message) {}; //phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + $this->assertFalse(isNewGitFile($gitFile, $git, $executeCommand, $debug)); + } + + public function testGetGitUnifiedDiff() { + $gitFile = 'foobar.php'; + $git = 'git'; + $diff = <<assertEquals($diff, getGitUnifiedDiff($gitFile, $git, $executeCommand, $debug)); + } + + public function testGetGitUnifiedDiffThrowsNonFatalIfDiffFails() { + $this->expectException(NonFatalException::class); + $gitFile = 'foobar.php'; + $git = 'git'; + $executeCommand = function($command) { + if (! $command || false === strpos($command, "git diff --staged --no-prefix 'foobar.php'")) { + return 'foobar'; + } + return ''; + }; + $debug = function($message) {}; //phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + getGitUnifiedDiff($gitFile, $git, $executeCommand, $debug); + } + + public function testFullGitWorkflow() { + $gitFile = 'foobar.php'; + $debug = function($message) {}; //phpcs:ignore VariableAnalysis + $shell = new class() implements ShellOperator { + public function isReadable(string $fileName): bool { + return ($fileName === 'foobar.php'); + } + + public function exitWithCode(int $code): void {} // phpcs:ignore VariableAnalysis + + public function printError(string $message): void {} // phpcs:ignore VariableAnalysis + + public function validateExecutableExists(string $name, string $command): void {} // phpcs:ignore VariableAnalysis + + public function executeCommand(string $command): string { + if (false !== strpos($command, "git diff --staged --no-prefix 'foobar.php'")) { + return << 'ERROR', + 'severity' => 5, + 'fixable' => false, + 'column' => 5, + 'source' => 'ImportDetection.Imports.RequireImports.Import', + 'line' => 20, + 'message' => 'Found unused symbol Emergent.', + ], + ], 'bin/foobar.php'); + $messages = runGitWorkflow($gitFile, $options, $shell, $debug); + $this->assertEquals($expected->getMessages(), $messages->getMessages()); + } + + public function testFullGitWorkflowForUnchangedFile() { + $this->expectException(NonFatalException::class); + $gitFile = 'foobar.php'; + $debug = function($message) {}; //phpcs:ignore VariableAnalysis + $shell = new class() implements ShellOperator { + public function isReadable(string $fileName): bool { + return ($fileName === 'foobar.php'); + } + + public function validateExecutableExists(string $name, string $command): void {} // phpcs:ignore VariableAnalysis + + public function exitWithCode(int $code): void {} // phpcs:ignore VariableAnalysis + + public function printError(string $message): void {} // phpcs:ignore VariableAnalysis + + public function executeCommand(string $command): string { + if (false !== strpos($command, "git diff --staged --no-prefix 'foobar.php'")) { + return <<expectException(ShellException::class); + $gitFile = 'foobar.php'; + $debug = function($message) {}; //phpcs:ignore VariableAnalysis + $shell = new class() implements ShellOperator { + public function isReadable(string $fileName): bool { + return ($fileName === 'foobar.php'); + } + + public function validateExecutableExists(string $name, string $command): void {} // phpcs:ignore VariableAnalysis + + public function exitWithCode(int $code): void {} // phpcs:ignore VariableAnalysis + + public function printError(string $message): void {} // phpcs:ignore VariableAnalysis + + public function executeCommand(string $command): string { + if (false !== strpos($command, "git diff --staged --no-prefix 'foobar.php'")) { + return << 'ERROR', + 'severity' => 5, + 'fixable' => false, + 'column' => 5, + 'source' => 'ImportDetection.Imports.RequireImports.Import', + 'line' => 4, + 'message' => 'Found unused symbol Emergent.', + ], + [ + 'type' => 'ERROR', + 'severity' => 5, + 'fixable' => false, + 'column' => 5, + 'source' => 'ImportDetection.Imports.RequireImports.Import', + 'line' => 5, + 'message' => 'Found unused symbol Emergent.', + ], + ], '/dev/null'); + $messages = runGitWorkflow($gitFile, $options, $shell, $debug); + $this->assertEquals($expected->getMessages(), $messages->getMessages()); + } +} diff --git a/tests/SvnWorkflowTest.php b/tests/SvnWorkflowTest.php index d4309de..37877ae 100644 --- a/tests/SvnWorkflowTest.php +++ b/tests/SvnWorkflowTest.php @@ -7,6 +7,7 @@ use PhpcsChanged\PhpcsMessages; use PhpcsChanged\NonFatalException; use PhpcsChanged\ShellOperator; +use PhpcsChanged\ShellException; use function PhpcsChanged\Cli\runSvnWorkflow; use function PhpcsChanged\SvnWorkflow\{isNewSvnFile, getSvnUnifiedDiff}; @@ -112,6 +113,10 @@ public function isReadable(string $fileName): bool { public function validateExecutableExists(string $name, string $command): void {} // phpcs:ignore VariableAnalysis + public function printError(string $message): void {} // phpcs:ignore VariableAnalysis + + public function exitWithCode(int $code): void {} // phpcs:ignore VariableAnalysis + public function executeCommand(string $command): string { if (false !== strpos($command, "svn diff 'foobar.php'")) { return <<assertEquals($expected->getMessages(), $messages->getMessages()); } + public function testFullSvnWorkflowForUnchangedFile() { + $this->expectException(NonFatalException::class); + $svnFile = 'foobar.php'; + $debug = function($message) {}; //phpcs:ignore VariableAnalysis + $shell = new class() implements ShellOperator { + public function isReadable(string $fileName): bool { + return ($fileName === 'foobar.php'); + } + + public function validateExecutableExists(string $name, string $command): void {} // phpcs:ignore VariableAnalysis + + public function exitWithCode(int $code): void {} // phpcs:ignore VariableAnalysis + + public function printError(string $message): void {} // phpcs:ignore VariableAnalysis + + public function executeCommand(string $command): string { + if (false !== strpos($command, "svn diff 'foobar.php'")) { + return <<expectException(ShellException::class); + $svnFile = 'foobar.php'; + $debug = function($message) {}; //phpcs:ignore VariableAnalysis + $shell = new class() implements ShellOperator { + public function isReadable(string $fileName): bool { + return ($fileName === 'foobar.php'); + } + + public function validateExecutableExists(string $name, string $command): void {} // phpcs:ignore VariableAnalysis + + public function exitWithCode(int $code): void {} // phpcs:ignore VariableAnalysis + + public function printError(string $message): void {} // phpcs:ignore VariableAnalysis + + public function executeCommand(string $command): string { + if (false !== strpos($command, "svn diff 'foobar.php'")) { + return <<