diff --git a/PhpcsChanged/Cli.php b/PhpcsChanged/Cli.php index f8feb89..e7043d7 100644 --- a/PhpcsChanged/Cli.php +++ b/PhpcsChanged/Cli.php @@ -16,7 +16,7 @@ use PhpcsChanged\CacheManager; use function PhpcsChanged\{getNewPhpcsMessages, getNewPhpcsMessagesFromFiles, getVersion}; use function PhpcsChanged\SvnWorkflow\{getSvnUnifiedDiff, getSvnFileInfo, isNewSvnFile, getSvnUnmodifiedPhpcsOutput, getSvnModifiedPhpcsOutput, getSvnRevisionId}; -use function PhpcsChanged\GitWorkflow\{getGitMergeBase, getGitUnifiedDiff, isNewGitFile, getGitUnmodifiedPhpcsOutput, getGitModifiedPhpcsOutput, validateGitFileExists, getModifiedGitFileHash, getUnmodifiedGitFileHash}; +use function PhpcsChanged\GitWorkflow\{getGitMergeBase, getGitUnifiedDiff}; function getDebug(bool $debugEnabled): callable { return @@ -345,30 +345,26 @@ function runGitWorkflow(CliOptions $options, ShellOperator $shell, CacheManager function runGitWorkflowForFile(string $gitFile, CliOptions $options, ShellOperator $shell, CacheManager $cache, callable $debug): PhpcsMessages { $git = getenv('GIT') ?: 'git'; - $phpcs = getenv('PHPCS') ?: 'phpcs'; - $cat = getenv('CAT') ?: 'cat'; $phpcsStandard = $options->phpcsStandard; - $phpcsStandardOption = $phpcsStandard ? ' --standard=' . escapeshellarg($phpcsStandard) : ''; - $warningSeverity = $options->warningSeverity; - $phpcsStandardOption .= isset($warningSeverity) ? ' --warning-severity=' . escapeshellarg($warningSeverity) : ''; $errorSeverity = $options->errorSeverity; - $phpcsStandardOption .= isset($errorSeverity) ? ' --error-severity=' . escapeshellarg($errorSeverity) : ''; $fileName = $shell->getFileNameFromPath($gitFile); try { - validateGitFileExists($gitFile, $shell, $options); + if (! $shell->isReadable($gitFile)) { + throw new ShellException("Cannot read file '{$gitFile}'"); + } $modifiedFilePhpcsOutput = null; $modifiedFileHash = ''; if (isCachingEnabled($options->toArray())) { - $modifiedFileHash = getModifiedGitFileHash($gitFile, $git, $cat, [$shell, 'executeCommand'], $options->toArray(), $debug); + $modifiedFileHash = $shell->getGitHashOfModifiedFile($gitFile); $modifiedFilePhpcsOutput = $cache->getCacheForFile($gitFile, 'new', $modifiedFileHash, $phpcsStandard ?? '', $warningSeverity ?? '', $errorSeverity ?? ''); $debug(($modifiedFilePhpcsOutput ? 'Using' : 'Not using') . " cache for modified file '{$gitFile}' at hash '{$modifiedFileHash}', and standard '{$phpcsStandard}'"); } if (! $modifiedFilePhpcsOutput) { - $modifiedFilePhpcsOutput = getGitModifiedPhpcsOutput($gitFile, $git, $phpcs, $cat, $phpcsStandardOption, [$shell, 'executeCommand'], $options->toArray(), $debug); + $modifiedFilePhpcsOutput = $shell->getPhpcsOutputOfModifiedGitFile($gitFile); if (isCachingEnabled($options->toArray())) { $cache->setCacheForFile($gitFile, 'new', $modifiedFileHash, $phpcsStandard ?? '', $warningSeverity ?? '', $errorSeverity ?? '', $modifiedFilePhpcsOutput); } @@ -383,7 +379,7 @@ function runGitWorkflowForFile(string $gitFile, CliOptions $options, ShellOperat throw new NoChangesException("Modified file '{$gitFile}' has no PHPCS messages; skipping"); } - $isNewFile = isNewGitFile($gitFile, $git, [$shell, 'executeCommand'], $options->toArray(), $debug); + $isNewFile = $shell->doesUnmodifiedFileExistInGit($gitFile); if ($isNewFile) { $debug('Skipping the linting of the unmodified file as it is a new file.'); } @@ -393,12 +389,12 @@ function runGitWorkflowForFile(string $gitFile, CliOptions $options, ShellOperat $unmodifiedFilePhpcsOutput = null; $unmodifiedFileHash = ''; if (isCachingEnabled($options->toArray())) { - $unmodifiedFileHash = getUnmodifiedGitFileHash($gitFile, $git, $cat, [$shell, 'executeCommand'], $options->toArray(), $debug); + $unmodifiedFileHash = $shell->getGitHashOfUnmodifiedFile($gitFile); $unmodifiedFilePhpcsOutput = $cache->getCacheForFile($gitFile, 'old', $unmodifiedFileHash, $phpcsStandard ?? '', $warningSeverity ?? '', $errorSeverity ?? ''); $debug(($unmodifiedFilePhpcsOutput ? 'Using' : 'Not using') . " cache for unmodified file '{$gitFile}' at hash '{$unmodifiedFileHash}', and standard '{$phpcsStandard}'"); } if (! $unmodifiedFilePhpcsOutput) { - $unmodifiedFilePhpcsOutput = getGitUnmodifiedPhpcsOutput($gitFile, $git, $phpcs, $phpcsStandardOption, [$shell, 'executeCommand'], $options->toArray(), $debug); + $unmodifiedFilePhpcsOutput = $shell->getPhpcsOutputOfUnmodifiedGitFile($gitFile); if (isCachingEnabled($options->toArray())) { $cache->setCacheForFile($gitFile, 'old', $unmodifiedFileHash, $phpcsStandard ?? '', $warningSeverity ?? '', $errorSeverity ?? '', $unmodifiedFilePhpcsOutput); } diff --git a/PhpcsChanged/CliOptions.php b/PhpcsChanged/CliOptions.php index 65cb8ea..66ade4f 100644 --- a/PhpcsChanged/CliOptions.php +++ b/PhpcsChanged/CliOptions.php @@ -113,19 +113,19 @@ public static function fromArray(array $options): self { $cliOptions->files = $options['files']; } if (isset($options['svn'])) { - $cliOptions->mode = 'svn'; + $cliOptions->mode = Modes::SVN; } if (isset($options['git'])) { - $cliOptions->mode = 'git-staged'; + $cliOptions->mode = Modes::GIT_STAGED; } if (isset($options['git-unstaged'])) { - $cliOptions->mode = 'git-unstaged'; + $cliOptions->mode = Modes::GIT_UNSTAGED; } if (isset($options['git-staged'])) { - $cliOptions->mode = 'git-staged'; + $cliOptions->mode = Modes::GIT_STAGED; } if (isset($options['git-base'])) { - $cliOptions->mode = 'git-base'; + $cliOptions->mode = Modes::GIT_BASE; $cliOptions->gitBase = $options['git-base']; } if (isset($options['report'])) { @@ -144,15 +144,15 @@ public static function fromArray(array $options): self { $cliOptions->useCache = false; } if (isset($options['diff'])) { - $cliOptions->mode = 'manual'; + $cliOptions->mode = Modes::MANUAL; $cliOptions->diffFile = $options['diff']; } if (isset($options['phpcs-unmodified'])) { - $cliOptions->mode = 'manual'; + $cliOptions->mode = Modes::MANUAL; $cliOptions->phpcsUnmodified = $options['phpcs-unmodified']; } if (isset($options['phpcs-modified'])) { - $cliOptions->mode = 'manual'; + $cliOptions->mode = Modes::MANUAL; $cliOptions->phpcsModified = $options['phpcs-modified']; } if (isset($options['s'])) { diff --git a/PhpcsChanged/GitWorkflow.php b/PhpcsChanged/GitWorkflow.php index 503cdd0..b19b440 100644 --- a/PhpcsChanged/GitWorkflow.php +++ b/PhpcsChanged/GitWorkflow.php @@ -9,39 +9,6 @@ use PhpcsChanged\ShellOperator; use function PhpcsChanged\Cli\getDebug; -function validateGitFileExists(string $gitFile, ShellOperator $shell, CliOptions $options): void { - $debug = getDebug($options->debug); - if (isset($options->noVerifyGitFile)) { - $debug('skipping Git file exists check.'); - return; - } - if (! $shell->isReadable($gitFile)) { - throw new ShellException("Cannot read file '{$gitFile}'"); - } - if (! $shell->doesFileExistInGit($gitFile)) { - throw new ShellException("File does not appear to be tracked by git: '{$gitFile}'"); - } -} - -function isRunFromGitRoot(string $git, callable $executeCommand, array $options, callable $debug): bool { - static $isRunFromGitRoot; - if (isset($options['no-cache-git-root'])) { - $isRunFromGitRoot = null; - } - if (null !== $isRunFromGitRoot) { - return $isRunFromGitRoot; - } - - $gitRootCommand = "{$git} rev-parse --show-toplevel"; - $gitRoot = $executeCommand($gitRootCommand); - $gitRoot = trim($gitRoot); - $isRunFromGitRoot = (getcwd() === $gitRoot); - - $debug('is run from git root: ' . var_export($isRunFromGitRoot, true)); - - return $isRunFromGitRoot; -} - function getGitMergeBase(string $git, callable $executeCommand, array $options, callable $debug): string { if ( empty($options['git-base']) ) { return ''; @@ -69,132 +36,3 @@ function getGitUnifiedDiff(string $gitFile, string $git, callable $executeComman $debug('diff command output:', $unifiedDiff); return $unifiedDiff; } - -function isNewGitFile(string $gitFile, string $git, callable $executeCommand, array $options, callable $debug): bool { - if ( isset($options['git-base']) && ! empty($options['git-base']) ) { - return isNewGitFileRemote( $gitFile, $git, $executeCommand, $options, $debug ); - } else { - return isNewGitFileLocal( $gitFile, $git, $executeCommand, $options, $debug ); - } -} - -function isNewGitFileRemote(string $gitFile, string $git, callable $executeCommand, array $options, callable $debug): bool { - $gitStatusCommand = "{$git} cat-file -e " . escapeshellarg($options['git-base']) . ':$(' . getFullPathToFileCommand($gitFile, $git) . ') 2>/dev/null'; - $debug('checking status of file with command:', $gitStatusCommand); - /** @var int */ - $return_val = 1; - $gitStatusOutput = []; - $gitStatusOutput = $executeCommand($gitStatusCommand, $gitStatusOutput, $return_val); - $debug('status command output:', $gitStatusOutput); - $debug('status command return val:', $return_val); - return 0 !== $return_val; -} - -function isNewGitFileLocal(string $gitFile, string $git, callable $executeCommand, array $options, callable $debug): bool { - $gitStatusCommand = "{$git} status --porcelain " . escapeshellarg($gitFile); - $debug('checking git status of file with command:', $gitStatusCommand); - $gitStatusOutput = $executeCommand($gitStatusCommand); - $debug('git status output:', $gitStatusOutput); - if (! $gitStatusOutput || false === strpos($gitStatusOutput, $gitFile)) { - return false; - } - if (isset($gitStatusOutput[0]) && $gitStatusOutput[0] === '?') { - throw new ShellException("File does not appear to be tracked by git: '{$gitFile}'"); - } - return isset($gitStatusOutput[0]) && $gitStatusOutput[0] === 'A'; -} - -function getGitUnmodifiedPhpcsOutput(string $gitFile, string $git, string $phpcs, string $phpcsStandardOption, callable $executeCommand, array $options, callable $debug): string { - $unmodifiedFileContents = getUnmodifiedGitRevisionContentsCommand($gitFile, $git, $options, $executeCommand, $debug); - - $unmodifiedFilePhpcsOutputCommand = "{$unmodifiedFileContents} | {$phpcs} --report=json -q" . $phpcsStandardOption . ' --stdin-path=' . escapeshellarg($gitFile) . ' -'; - $debug('running unmodified file phpcs command:', $unmodifiedFilePhpcsOutputCommand); - $unmodifiedFilePhpcsOutput = $executeCommand($unmodifiedFilePhpcsOutputCommand); - if (! $unmodifiedFilePhpcsOutput) { - throw new ShellException("Cannot get unmodified file phpcs output for file '{$gitFile}'"); - } - $debug('unmodified file phpcs command output:', $unmodifiedFilePhpcsOutput); - return $unmodifiedFilePhpcsOutput; -} - -function getGitModifiedPhpcsOutput(string $gitFile, string $git, string $phpcs, string $cat, string $phpcsStandardOption, callable $executeCommand, array $options, callable $debug): string { - $modifiedFileContents = getModifiedGitRevisionContentsCommand($gitFile, $git, $cat, $options, $executeCommand, $debug); - - $modifiedFilePhpcsOutputCommand = "{$modifiedFileContents} | {$phpcs} --report=json -q" . $phpcsStandardOption . ' --stdin-path=' . escapeshellarg($gitFile) .' -'; - $debug('running modified file phpcs command:', $modifiedFilePhpcsOutputCommand); - $modifiedFilePhpcsOutput = $executeCommand($modifiedFilePhpcsOutputCommand); - if (! $modifiedFilePhpcsOutput) { - throw new ShellException("Cannot get modified file phpcs output for file '{$gitFile}'"); - } - $debug('modified file phpcs command output:', $modifiedFilePhpcsOutput); - if (false !== strpos($modifiedFilePhpcsOutput, 'You must supply at least one file or directory to process')) { - $debug('phpcs output implies modified file is empty'); - return ''; - } - return $modifiedFilePhpcsOutput; -} - -function getFullPathToFileCommand(string $gitFile, string $git): string { - return "{$git} ls-files --full-name " . escapeshellarg($gitFile); -} - -function getModifiedGitRevisionContentsCommand(string $gitFile, string $git, string $cat, array $options, callable $executeCommand, callable $debug): string { - $fullPathCommand = getFullPathToFileCommand($gitFile, $git); - if (isset($options['git-base']) && ! empty($options['git-base'])) { - // for git-base mode, we get the contents of the file from the HEAD version of the file in the current branch - if (isRunFromGitRoot($git, $executeCommand, $options, $debug)) { - return "{$git} show HEAD:" . escapeshellarg($gitFile); - } - return "{$git} show HEAD:$({$fullPathCommand})"; - } else if (isset($options['git-unstaged'])) { - // for git-unstaged mode, we get the contents of the file from the current working copy - return "{$cat} " . escapeshellarg($gitFile); - } - // default mode is git-staged, so we get the contents from the staged version of the file - if (isRunFromGitRoot($git, $executeCommand, $options, $debug)) { - return "{$git} show :0:" . escapeshellarg($gitFile); - } - return "{$git} show :0:$({$fullPathCommand})"; -} - -function getUnmodifiedGitRevisionContentsCommand(string $gitFile, string $git, array $options, callable $executeCommand, callable $debug): string { - if (isset($options['git-base']) && ! empty($options['git-base'])) { - // git-base - $rev = escapeshellarg($options['git-base']); - } else if (isset($options['git-unstaged'])) { - // git-unstaged - $rev = ':0'; // :0 in this case means "staged version or HEAD if there is no staged version" - } else { - // git-staged - $rev = 'HEAD'; - } - if (isRunFromGitRoot($git, $executeCommand, $options, $debug)) { - return "{$git} show {$rev}:" . escapeshellarg($gitFile); - } - $fullPathCommand = getFullPathToFileCommand($gitFile, $git); - return "{$git} show {$rev}:$({$fullPathCommand})"; -} - -function getModifiedGitFileHash(string $gitFile, string $git, string $cat, callable $executeCommand, array $options, callable $debug): string { - $fileContents = getModifiedGitRevisionContentsCommand($gitFile, $git, $cat, $options, $executeCommand, $debug); - $command = "{$fileContents} | {$git} hash-object --stdin"; - $debug('running modified file git hash command:', $command); - $hash = $executeCommand($command); - if (! $hash) { - throw new ShellException("Cannot get modified file hash for file '{$gitFile}'"); - } - $debug('modified file git hash command output:', $hash); - return $hash; -} - -function getUnmodifiedGitFileHash(string $gitFile, string $git, string $cat, callable $executeCommand, array $options, callable $debug): string { - $fileContents = getUnmodifiedGitRevisionContentsCommand($gitFile, $git, $options, $executeCommand, $debug); - $command = "{$fileContents} | {$git} hash-object --stdin"; - $debug('running unmodified file git hash command:', $command); - $hash = $executeCommand($command); - if (! $hash) { - throw new ShellException("Cannot get unmodified file hash for file '{$gitFile}'"); - } - $debug('unmodified file git hash command output:', $hash); - return $hash; -} diff --git a/PhpcsChanged/ShellOperator.php b/PhpcsChanged/ShellOperator.php index 9b627fe..09275ea 100644 --- a/PhpcsChanged/ShellOperator.php +++ b/PhpcsChanged/ShellOperator.php @@ -11,9 +11,8 @@ interface ShellOperator { public function validateExecutableExists(string $name, string $command): void; - public function executeCommand(string $command, array &$output = null, int &$return_val = null): string; - - public function doesFileExistInGit(string $fileName): bool; + // TODO: remove executeCommand from the interface and rely on the more specific methods. + public function executeCommand(string $command, int &$return_val = null): string; public function isReadable(string $fileName): bool; @@ -24,4 +23,14 @@ public function exitWithCode(int $code): void; public function printError(string $message): void; public function getFileNameFromPath(string $path): string; + + public function doesUnmodifiedFileExistInGit(string $fileName): bool; + + public function getGitHashOfModifiedFile(string $fileName): string; + + public function getGitHashOfUnmodifiedFile(string $fileName): string; + + public function getPhpcsOutputOfModifiedGitFile(string $fileName): string; + + public function getPhpcsOutputOfUnmodifiedGitFile(string $fileName): string; } diff --git a/PhpcsChanged/UnixShell.php b/PhpcsChanged/UnixShell.php index 20dce7f..80ecbe9 100644 --- a/PhpcsChanged/UnixShell.php +++ b/PhpcsChanged/UnixShell.php @@ -5,6 +5,7 @@ use PhpcsChanged\ShellOperator; use PhpcsChanged\CliOptions; +use PhpcsChanged\Modes; use function PhpcsChanged\Cli\printError; use function PhpcsChanged\Cli\getDebug; @@ -17,6 +18,11 @@ class UnixShell implements ShellOperator { */ private $options; + /** + * @var Array + */ + private $fullPaths = []; + public function __construct(CliOptions $options) { $this->options = $options; } @@ -28,22 +34,174 @@ public function validateExecutableExists(string $name, string $command): void { } } - public function executeCommand(string $command, array &$output = null, int &$return_val = null): string { + public function executeCommand(string $command, int &$return_val = null): string { + $output = []; exec($command, $output, $return_val); - return join(PHP_EOL, $output) . PHP_EOL; + return implode(PHP_EOL, $output) . PHP_EOL; + } + + private function doesFileExistInGitBase(string $fileName): bool { + $debug = getDebug($this->options->debug); + $git = getenv('GIT') ?: 'git'; + $gitStatusCommand = "{$git} cat-file -e " . escapeshellarg($this->options->gitBase) . ':' . escapeshellarg($this->getFullGitPathToFile($fileName)) . ' 2>/dev/null'; + $debug('checking status of file with command:', $gitStatusCommand); + /** @var int */ + $return_val = 1; + $gitStatusOutput = $this->executeCommand($gitStatusCommand, $return_val); + $debug('status command output:', $gitStatusOutput); + $debug('status command return val:', $return_val); + return 0 !== $return_val; } - public function doesFileExistInGit(string $fileName): bool { + private function getGitStatusForFile(string $fileName): string { $debug = getDebug($this->options->debug); $git = getenv('GIT') ?: 'git'; $gitStatusCommand = "{$git} status --porcelain " . escapeshellarg($fileName); - $debug('checking git existence of file with command:', $gitStatusCommand); + $debug('checking git status of file with command:', $gitStatusCommand); $gitStatusOutput = $this->executeCommand($gitStatusCommand); $debug('git status output:', $gitStatusOutput); - if (isset($gitStatusOutput[0]) && $gitStatusOutput[0] === '?') { + return $gitStatusOutput; + } + + private function isFileStagedForAdding(string $fileName): bool { + $gitStatusOutput = $this->getGitStatusForFile($fileName); + if (! $gitStatusOutput || false === strpos($gitStatusOutput, $fileName)) { return false; } - return true; + if (isset($gitStatusOutput[0]) && $gitStatusOutput[0] === '?') { + throw new ShellException("File does not appear to be tracked by git: '{$fileName}'"); + } + return isset($gitStatusOutput[0]) && $gitStatusOutput[0] === 'A'; + } + + public function doesUnmodifiedFileExistInGit(string $fileName): bool { + if ($this->options->mode === Modes::GIT_BASE) { + return $this->doesFileExistInGitBase($fileName); + } + return $this->isFileStagedForAdding($fileName); + } + + private function getFullGitPathToFile(string $fileName): string { + if (array_key_exists($fileName, $this->fullPaths)) { + return $this->fullPaths[$fileName]; + } + $debug = getDebug($this->options->debug); + $git = getenv('GIT') ?: 'git'; + if (! $this->options->noVerifyGitFile) { + $gitStatusOutput = $this->getGitStatusForFile($fileName); + if (! $gitStatusOutput || false === strpos($gitStatusOutput, $fileName)) { + throw new ShellException("File does not appear to be tracked by git: '{$fileName}'"); + } + if (isset($gitStatusOutput[0]) && $gitStatusOutput[0] === '?') { + throw new ShellException("File does not appear to be tracked by git: '{$fileName}'"); + } + } + $command = "{$git} ls-files --full-name " . escapeshellarg($fileName); + $debug('getting full path to file with command:', $command); + $fullPath = trim($this->executeCommand($command)); + // This will not change so we can cache it. + $this->fullPaths[$fileName] = $fullPath; + return $fullPath; + } + + private function getModifiedFileContentsCommand(string $fileName): string { + $git = getenv('GIT') ?: 'git'; + $cat = getenv('CAT') ?: 'cat'; + $fullPath = $this->getFullGitPathToFile($fileName); + if ($this->options->mode === Modes::GIT_BASE) { + // for git-base mode, we get the contents of the file from the HEAD version of the file in the current branch + return "{$git} show HEAD:" . escapeshellarg($fullPath); + } + if ($this->options->mode === Modes::GIT_UNSTAGED) { + // for git-unstaged mode, we get the contents of the file from the current working copy + return "{$cat} " . escapeshellarg($fileName); + } + // default mode is git-staged, so we get the contents from the staged version of the file + return "{$git} show :0:" . escapeshellarg($fullPath); + } + + private function getUnmodifiedFileContentsCommand(string $fileName): string { + $git = getenv('GIT') ?: 'git'; + if ($this->options->mode === Modes::GIT_BASE) { + $rev = escapeshellarg($this->options->gitBase); + } else if ($this->options->mode === Modes::GIT_UNSTAGED) { + $rev = ':0'; // :0 in this case means "staged version or HEAD if there is no staged version" + } else { + // git-staged is the default + $rev = 'HEAD'; + } + $fullPath = $this->getFullGitPathToFile($fileName); + return "{$git} show {$rev}:" . escapeshellarg($fullPath); + } + + public function getGitHashOfModifiedFile(string $fileName): string { + $debug = getDebug($this->options->debug); + $git = getenv('GIT') ?: 'git'; + $fileContentsCommand = $this->getModifiedFileContentsCommand($fileName); + $command = "{$fileContentsCommand} | {$git} hash-object --stdin"; + $debug('running modified file git hash command:', $command); + $hash = $this->executeCommand($command); + if (! $hash) { + throw new ShellException("Cannot get modified file hash for file '{$fileName}'"); + } + $debug('modified file git hash command output:', $hash); + return $hash; + } + + public function getGitHashOfUnmodifiedFile(string $fileName): string { + $debug = getDebug($this->options->debug); + $git = getenv('GIT') ?: 'git'; + $fileContentsCommand = $this->getUnmodifiedFileContentsCommand($fileName); + $command = "{$fileContentsCommand} | {$git} hash-object --stdin"; + $debug('running unmodified file git hash command:', $command); + $hash = $this->executeCommand($command); + if (! $hash) { + throw new ShellException("Cannot get unmodified file hash for file '{$fileName}'"); + } + $debug('unmodified file git hash command output:', $hash); + return $hash; + } + + private function getPhpcsStandardOption(): string { + $phpcsStandard = $this->options->phpcsStandard; + $phpcsStandardOption = $phpcsStandard ? ' --standard=' . escapeshellarg($phpcsStandard) : ''; + $warningSeverity = $this->options->warningSeverity; + $phpcsStandardOption .= isset($warningSeverity) ? ' --warning-severity=' . escapeshellarg($warningSeverity) : ''; + $errorSeverity = $this->options->errorSeverity; + $phpcsStandardOption .= isset($errorSeverity) ? ' --error-severity=' . escapeshellarg($errorSeverity) : ''; + return $phpcsStandardOption; + } + + public function getPhpcsOutputOfModifiedGitFile(string $fileName): string { + $debug = getDebug($this->options->debug); + $phpcs = getenv('PHPCS') ?: 'phpcs'; + $fileContentsCommand = $this->getModifiedFileContentsCommand($fileName); + $modifiedFilePhpcsOutputCommand = "{$fileContentsCommand} | {$phpcs} --report=json -q" . $this->getPhpcsStandardOption() . ' --stdin-path=' . escapeshellarg($fileName) .' -'; + $debug('running modified file phpcs command:', $modifiedFilePhpcsOutputCommand); + $modifiedFilePhpcsOutput = $this->executeCommand($modifiedFilePhpcsOutputCommand); + if (! $modifiedFilePhpcsOutput) { + throw new ShellException("Cannot get modified file phpcs output for file '{$fileName}'"); + } + $debug('modified file phpcs command output:', $modifiedFilePhpcsOutput); + if (false !== strpos($modifiedFilePhpcsOutput, 'You must supply at least one file or directory to process')) { + $debug('phpcs output implies modified file is empty'); + return ''; + } + return $modifiedFilePhpcsOutput; + } + + public function getPhpcsOutputOfUnmodifiedGitFile(string $fileName): string { + $debug = getDebug($this->options->debug); + $phpcs = getenv('PHPCS') ?: 'phpcs'; + $unmodifiedFileContentsCommand = $this->getUnmodifiedFileContentsCommand($fileName); + $unmodifiedFilePhpcsOutputCommand = "{$unmodifiedFileContentsCommand} | {$phpcs} --report=json -q" . $this->getPhpcsStandardOption() . ' --stdin-path=' . escapeshellarg($fileName) . ' -'; + $debug('running unmodified file phpcs command:', $unmodifiedFilePhpcsOutputCommand); + $unmodifiedFilePhpcsOutput = $this->executeCommand($unmodifiedFilePhpcsOutputCommand); + if (! $unmodifiedFilePhpcsOutput) { + throw new ShellException("Cannot get unmodified file phpcs output for file '{$fileName}'"); + } + $debug('unmodified file phpcs command output:', $unmodifiedFilePhpcsOutput); + return $unmodifiedFilePhpcsOutput; } public function isReadable(string $fileName): bool { diff --git a/tests/GitWorkflowTest.php b/tests/GitWorkflowTest.php index 9d9f440..50aadb1 100644 --- a/tests/GitWorkflowTest.php +++ b/tests/GitWorkflowTest.php @@ -14,7 +14,7 @@ use PhpcsChangedTests\PhpcsFixture; use PhpcsChangedTests\TestCache; use function PhpcsChanged\Cli\runGitWorkflow; -use function PhpcsChanged\GitWorkflow\{isNewGitFile, getGitUnifiedDiff}; +use function PhpcsChanged\GitWorkflow\getGitUnifiedDiff; final class GitWorkflowTest extends TestCase { public $fixture; @@ -26,28 +26,6 @@ public function setUp(): void { $this->phpcs = new PhpcsFixture(); } - public function testIsNewGitFileReturnsTrueForNewFile() { - $gitFile = 'foobar.php'; - $git = 'git'; - $executeCommand = function($command) { - if (false !== strpos($command, "git status --porcelain 'foobar.php'")) { - return $this->fixture->getNewFileInfo('foobar.php'); - } - }; - $this->assertTrue(isNewGitFile($gitFile, $git, $executeCommand, array(), '\PhpcsChangedTests\Debug')); - } - - public function testIsNewGitFileReturnsFalseForOldFile() { - $gitFile = 'foobar.php'; - $git = 'git'; - $executeCommand = function($command) { - if (false !== strpos($command, "git status --porcelain 'foobar.php'")) { - return $this->fixture->getModifiedFileInfo('foobar.php'); - } - }; - $this->assertFalse(isNewGitFile($gitFile, $git, $executeCommand, array(), '\PhpcsChangedTests\Debug')); - } - public function testGetGitUnifiedDiff() { $gitFile = 'foobar.php'; $git = 'git'; @@ -67,10 +45,12 @@ public function testFullGitWorkflowForOneFileStaged() { $fixture = $this->fixture->getAddedLineDiff('foobar.php', 'use Foobar;'); $shell->registerCommand("git diff --staged --no-prefix 'foobar.php'", $fixture); $shell->registerCommand("git status --porcelain 'foobar.php'", $this->fixture->getModifiedFileInfo('foobar.php')); - $shell->registerCommand("git show HEAD:$(git ls-files --full-name 'foobar.php')", $this->phpcs->getResults('STDIN', [20])->toPhpcsJson()); - $shell->registerCommand("git show :0:$(git ls-files --full-name 'foobar.php')", $this->phpcs->getResults('STDIN', [20, 21], 'Found unused symbol Foobar.')->toPhpcsJson()); + $shell->registerCommand("git ls-files --full-name 'foobar.php'", "files/foobar.php"); + $shell->registerCommand("git show HEAD:'files/foobar.php'", $this->phpcs->getResults('STDIN', [20])->toPhpcsJson()); + $shell->registerCommand("git show :0:'files/foobar.php'", $this->phpcs->getResults('STDIN', [20, 21], 'Found unused symbol Foobar.')->toPhpcsJson()); $shell->registerCommand("git rev-parse --show-toplevel", 'run-from-git-root'); $options = CliOptions::fromArray(['no-cache-git-root' => 1, 'git-staged' => 1, 'files' => [$gitFile]]); + $shell->setOptions($options); $cache = new CacheManager( new TestCache() ); $expected = $this->phpcs->getResults('bin/foobar.php', [20], 'Found unused symbol Foobar.'); $messages = runGitWorkflow($options, $shell, $cache, '\PhpcsChangedTests\Debug'); @@ -83,10 +63,12 @@ public function testFullGitWorkflowForOneFileUnstaged() { $fixture = $this->fixture->getAddedLineDiff('foobar.php', 'use Foobar;'); $shell->registerCommand("git diff --no-prefix 'foobar.php'", $fixture); $shell->registerCommand("git status --porcelain 'foobar.php'", $this->fixture->getModifiedFileInfo('foobar.php')); - $shell->registerCommand("git show :0:$(git ls-files --full-name 'foobar.php')", $this->phpcs->getResults('STDIN', [20], 'Found unused symbol Foobar.')->toPhpcsJson()); + $shell->registerCommand("git ls-files --full-name 'foobar.php'", "files/foobar.php"); + $shell->registerCommand("git show :0:'files/foobar.php'", $this->phpcs->getResults('STDIN', [20], 'Found unused symbol Foobar.')->toPhpcsJson()); $shell->registerCommand("cat 'foobar.php'", $this->phpcs->getResults('STDIN', [21, 20], 'Found unused symbol Foobar.')->toPhpcsJson()); $shell->registerCommand("git rev-parse --show-toplevel", 'run-from-git-root'); $options = CliOptions::fromArray(['no-cache-git-root' => 1, 'git-unstaged' => '1', 'files' => [$gitFile]]); + $shell->setOptions($options); $cache = new CacheManager( new TestCache() ); $expected = $this->phpcs->getResults('bin/foobar.php', [20], 'Found unused symbol Foobar.'); $messages = runGitWorkflow($options, $shell, $cache, '\PhpcsChangedTests\Debug'); @@ -97,6 +79,7 @@ public function testFullGitWorkflowForOneChangedFileWithoutPhpcsMessagesLintsOnl $gitFile = 'foobar.php'; $shell = new TestShell([$gitFile]); $shell->registerCommand("git status --porcelain 'foobar.php'", $this->fixture->getModifiedFileInfo('foobar.php')); + $shell->registerCommand("git ls-files --full-name 'foobar.php'", "files/foobar.php"); $shell->registerCommand("cat 'foobar.php' | phpcs", $this->phpcs->getEmptyResults()->toPhpcsJson()); $options = CliOptions::fromArray([ @@ -104,6 +87,7 @@ public function testFullGitWorkflowForOneChangedFileWithoutPhpcsMessagesLintsOnl 'git-unstaged' => '1', 'files' => [$gitFile], ]); + $shell->setOptions($options); $cache = new CacheManager( new TestCache(), '\PhpcsChangedTests\Debug' ); $expected = $this->phpcs->getEmptyResults(); @@ -111,7 +95,7 @@ public function testFullGitWorkflowForOneChangedFileWithoutPhpcsMessagesLintsOnl $this->assertEquals($expected->getMessages(), $messages->getMessages()); $this->assertFalse($shell->wasCommandCalled("git diff --no-prefix 'foobar.php'")); - $this->assertFalse($shell->wasCommandCalled("git show :0:$(git ls-files --full-name 'foobar.php') | phpcs")); + $this->assertFalse($shell->wasCommandCalled("git show :0:'files/foobar.php' | phpcs")); } public function testFullGitWorkflowForOneFileUnstagedCachesDataThenUsesCache() { @@ -120,9 +104,10 @@ public function testFullGitWorkflowForOneFileUnstagedCachesDataThenUsesCache() { $fixture = $this->fixture->getAddedLineDiff('foobar.php', 'use Foobar;'); $shell->registerCommand("git diff --no-prefix 'foobar.php'", $fixture); $shell->registerCommand("git status --porcelain 'foobar.php'", $this->fixture->getModifiedFileInfo('foobar.php')); - $shell->registerCommand("git show :0:$(git ls-files --full-name 'foobar.php') | phpcs", $this->phpcs->getResults('STDIN', [20], 'Found unused symbol Foobar.')->toPhpcsJson()); + $shell->registerCommand("git ls-files --full-name 'foobar.php'", "files/foobar.php"); + $shell->registerCommand("git show :0:'files/foobar.php' | phpcs", $this->phpcs->getResults('STDIN', [20], 'Found unused symbol Foobar.')->toPhpcsJson()); $shell->registerCommand("cat 'foobar.php' | phpcs", $this->phpcs->getResults('STDIN', [21, 20], 'Found unused symbol Foobar.')->toPhpcsJson()); - $shell->registerCommand("git show :0:$(git ls-files --full-name 'foobar.php') | git hash-object --stdin", 'previous-file-hash'); + $shell->registerCommand("git show :0:'files/foobar.php' | git hash-object --stdin", 'previous-file-hash'); $shell->registerCommand("cat 'foobar.php' | git hash-object --stdin", 'new-file-hash'); $shell->registerCommand("git rev-parse --show-toplevel", 'run-from-git-root'); $options = CliOptions::fromArray([ @@ -131,6 +116,7 @@ public function testFullGitWorkflowForOneFileUnstagedCachesDataThenUsesCache() { 'cache' => false, // getopt is weird and sets options to false 'files' => [$gitFile], ]); + $shell->setOptions($options); $cache = new CacheManager( new TestCache(), '\PhpcsChangedTests\Debug' ); $expected = $this->phpcs->getResults('bin/foobar.php', [20], 'Found unused symbol Foobar.'); @@ -139,7 +125,7 @@ public function testFullGitWorkflowForOneFileUnstagedCachesDataThenUsesCache() { $shell->resetCommandsCalled(); $messages = runGitWorkflow($options, $shell, $cache, '\PhpcsChangedTests\Debug'); $this->assertEquals($expected->getMessages(), $messages->getMessages()); - $this->assertFalse($shell->wasCommandCalled("git show :0:$(git ls-files --full-name 'foobar.php') | phpcs")); + $this->assertFalse($shell->wasCommandCalled("git show :0:'files/foobar.php' | phpcs")); $this->assertFalse($shell->wasCommandCalled("cat 'foobar.php' | phpcs")); } @@ -149,9 +135,10 @@ public function testFullGitWorkflowForOneFileUnstagedCachesDataThenUsesCacheWith $fixture = $this->fixture->getAddedLineDiff('foobar.php', 'use Foobar;'); $shell->registerCommand("git diff --no-prefix 'foobar.php'", $fixture); $shell->registerCommand("git status --porcelain 'foobar.php'", $this->fixture->getModifiedFileInfo('foobar.php')); - $shell->registerCommand("git show :0:$(git ls-files --full-name 'foobar.php') | phpcs", $this->phpcs->getResults('STDIN', [20], 'Found unused symbol Foobar.')->toPhpcsJson()); + $shell->registerCommand("git ls-files --full-name 'foobar.php'", "files/foobar.php"); + $shell->registerCommand("git show :0:'files/foobar.php' | phpcs", $this->phpcs->getResults('STDIN', [20], 'Found unused symbol Foobar.')->toPhpcsJson()); $shell->registerCommand("cat 'foobar.php' | phpcs", $this->phpcs->getResults('STDIN', [21, 20], 'Found unused symbol Foobar.')->toPhpcsJson()); - $shell->registerCommand("git show :0:$(git ls-files --full-name 'foobar.php') | git hash-object --stdin", 'previous-file-hash'); + $shell->registerCommand("git show :0:'files/foobar.php' | git hash-object --stdin", 'previous-file-hash'); $shell->registerCommand("cat 'foobar.php' | git hash-object --stdin", 'new-file-hash'); $shell->registerCommand("git rev-parse --show-toplevel", 'run-from-git-root'); $options = CliOptions::fromArray([ @@ -163,6 +150,7 @@ public function testFullGitWorkflowForOneFileUnstagedCachesDataThenUsesCacheWith 'error-severity' => '2', 'files' => [$gitFile], ]); + $shell->setOptions($options); $cache = new CacheManager( new TestCache(), '\PhpcsChangedTests\Debug' ); $expected = $this->phpcs->getResults('bin/foobar.php', [20], 'Found unused symbol Foobar.'); @@ -172,7 +160,7 @@ public function testFullGitWorkflowForOneFileUnstagedCachesDataThenUsesCacheWith $messages = runGitWorkflow($options, $shell, $cache, '\PhpcsChangedTests\Debug'); $this->assertEquals($expected->getMessages(), $messages->getMessages()); - $this->assertFalse($shell->wasCommandCalled("git show :0:$(git ls-files --full-name 'foobar.php') | phpcs")); + $this->assertFalse($shell->wasCommandCalled("git show :0:'files/foobar.php' | phpcs")); $this->assertFalse($shell->wasCommandCalled("cat 'foobar.php' | phpcs")); foreach( $cache->getEntries() as $entry ) { @@ -186,9 +174,10 @@ public function testFullGitWorkflowForOneFileUnstagedCachesDataThenUsesCacheWith $fixture = $this->fixture->getAddedLineDiff('foobar.php', 'use Foobar;'); $shell->registerCommand("git diff --no-prefix 'foobar.php'", $fixture); $shell->registerCommand("git status --porcelain 'foobar.php'", $this->fixture->getModifiedFileInfo('foobar.php')); - $shell->registerCommand("git show :0:$(git ls-files --full-name 'foobar.php') | phpcs --report=json -q --standard='standard' --warning-severity='0' --error-severity='0'", $this->phpcs->getResults('STDIN', [20], 'Found unused symbol Foobar.')->toPhpcsJson()); + $shell->registerCommand("git ls-files --full-name 'foobar.php'", "files/foobar.php"); + $shell->registerCommand("git show :0:'files/foobar.php' | phpcs --report=json -q --standard='standard' --warning-severity='0' --error-severity='0'", $this->phpcs->getResults('STDIN', [20], 'Found unused symbol Foobar.')->toPhpcsJson()); $shell->registerCommand("cat 'foobar.php' | phpcs", $this->phpcs->getResults('STDIN', [21, 20], 'Found unused symbol Foobar.')->toPhpcsJson()); - $shell->registerCommand("git show :0:$(git ls-files --full-name 'foobar.php') | git hash-object --stdin", 'previous-file-hash'); + $shell->registerCommand("git show :0:'files/foobar.php' | git hash-object --stdin", 'previous-file-hash'); $shell->registerCommand("cat 'foobar.php' | git hash-object --stdin", 'new-file-hash'); $shell->registerCommand("git rev-parse --show-toplevel", 'run-from-git-root'); $options = CliOptions::fromArray([ @@ -200,6 +189,7 @@ public function testFullGitWorkflowForOneFileUnstagedCachesDataThenUsesCacheWith 'error-severity' => '0', 'files' => [$gitFile], ]); + $shell->setOptions($options); $cache = new CacheManager( new TestCache(), '\PhpcsChangedTests\Debug' ); $expected = $this->phpcs->getResults('bin/foobar.php', [20], 'Found unused symbol Foobar.'); @@ -209,7 +199,7 @@ public function testFullGitWorkflowForOneFileUnstagedCachesDataThenUsesCacheWith $messages = runGitWorkflow($options, $shell, $cache, '\PhpcsChangedTests\Debug'); $this->assertEquals($expected->getMessages(), $messages->getMessages()); - $this->assertFalse($shell->wasCommandCalled("git show :0:$(git ls-files --full-name 'foobar.php') | phpcs")); + $this->assertFalse($shell->wasCommandCalled("git show :0:'files/foobar.php' | phpcs")); $this->assertFalse($shell->wasCommandCalled("cat 'foobar.php' | phpcs")); $cacheEntries = $cache->getEntries(); @@ -225,9 +215,10 @@ public function testFullGitWorkflowForOneFileUnstagedCachesDataThenUsesCacheWith $fixture = $this->fixture->getAddedLineDiff('foobar.php', 'use Foobar;'); $shell->registerCommand("git diff --no-prefix 'foobar.php'", $fixture); $shell->registerCommand("git status --porcelain 'foobar.php'", $this->fixture->getModifiedFileInfo('foobar.php')); - $shell->registerCommand("git show :0:$(git ls-files --full-name 'foobar.php') | phpcs", $this->phpcs->getResults('STDIN', [20], 'Found unused symbol Foobar.')->toPhpcsJson()); + $shell->registerCommand("git ls-files --full-name 'foobar.php'", "files/foobar.php"); + $shell->registerCommand("git show :0:'files/foobar.php' | phpcs", $this->phpcs->getResults('STDIN', [20], 'Found unused symbol Foobar.')->toPhpcsJson()); $shell->registerCommand("cat 'foobar.php' | phpcs", $this->phpcs->getResults('STDIN', [21, 20], 'Found unused symbol Foobar.')->toPhpcsJson()); - $shell->registerCommand("git show :0:$(git ls-files --full-name 'foobar.php') | git hash-object --stdin", 'previous-file-hash'); + $shell->registerCommand("git show :0:'files/foobar.php' | git hash-object --stdin", 'previous-file-hash'); $shell->registerCommand("cat 'foobar.php' | git hash-object --stdin", 'new-file-hash'); $shell->registerCommand("git rev-parse --show-toplevel", 'run-from-git-root'); $options = CliOptions::fromArray([ @@ -237,6 +228,7 @@ public function testFullGitWorkflowForOneFileUnstagedCachesDataThenUsesCacheWith 'standard' => 'standard', 'files' => [$gitFile], ]); + $shell->setOptions($options); $cache = new CacheManager( new TestCache(), '\PhpcsChangedTests\Debug' ); $expected = $this->phpcs->getResults('bin/foobar.php', [20], 'Found unused symbol Foobar.'); @@ -246,7 +238,7 @@ public function testFullGitWorkflowForOneFileUnstagedCachesDataThenUsesCacheWith $messages = runGitWorkflow($options, $shell, $cache, '\PhpcsChangedTests\Debug'); $this->assertEquals($expected->getMessages(), $messages->getMessages()); - $this->assertFalse($shell->wasCommandCalled("git show :0:$(git ls-files --full-name 'foobar.php') | phpcs")); + $this->assertFalse($shell->wasCommandCalled("git show :0:'files/foobar.php' | phpcs")); $this->assertFalse($shell->wasCommandCalled("cat 'foobar.php' | phpcs")); $cacheEntries = $cache->getEntries(); @@ -260,11 +252,12 @@ public function testFullGitWorkflowForOneFileUnstagedCachesDataThenClearsOldCach $gitFile = 'foobar.php'; $shell = new TestShell([$gitFile]); $fixture = $this->fixture->getAddedLineDiff('foobar.php', 'use Foobar;'); + $shell->registerCommand("git ls-files --full-name 'foobar.php'", "files/foobar.php"); $shell->registerCommand("git diff --no-prefix 'foobar.php'", $fixture); $shell->registerCommand("git status --porcelain 'foobar.php'", $this->fixture->getModifiedFileInfo('foobar.php')); - $shell->registerCommand("git show :0:$(git ls-files --full-name 'foobar.php') | phpcs", $this->phpcs->getResults('STDIN', [20], 'Found unused symbol Foobar.')->toPhpcsJson()); + $shell->registerCommand("git show :0:'files/foobar.php' | phpcs", $this->phpcs->getResults('STDIN', [20], 'Found unused symbol Foobar.')->toPhpcsJson()); $shell->registerCommand("cat 'foobar.php' | phpcs", $this->phpcs->getResults('STDIN', [21, 20], 'Found unused symbol Foobar.')->toPhpcsJson()); - $shell->registerCommand("git show :0:$(git ls-files --full-name 'foobar.php') | git hash-object --stdin", 'previous-file-hash'); + $shell->registerCommand("git show :0:'files/foobar.php' | git hash-object --stdin", 'previous-file-hash'); $shell->registerCommand("cat 'foobar.php' | git hash-object --stdin", 'new-file-hash'); $shell->registerCommand("git rev-parse --show-toplevel", 'run-from-git-root'); $options = CliOptions::fromArray([ @@ -273,17 +266,18 @@ public function testFullGitWorkflowForOneFileUnstagedCachesDataThenClearsOldCach 'cache' => false, // getopt is weird and sets options to false 'files' => [$gitFile], ]); + $shell->setOptions($options); $cache = new CacheManager( new TestCache(), '\PhpcsChangedTests\Debug' ); $expected = $this->phpcs->getResults('bin/foobar.php', [20], 'Found unused symbol Foobar.'); runGitWorkflow($options, $shell, $cache, '\PhpcsChangedTests\Debug'); - $shell->deregisterCommand("git show :0:$(git ls-files --full-name 'foobar.php') | git hash-object --stdin"); - $shell->registerCommand("git show :0:$(git ls-files --full-name 'foobar.php') | git hash-object --stdin", 'old-file-hash-2'); + $shell->deregisterCommand("git show :0:'files/foobar.php' | git hash-object --stdin"); + $shell->registerCommand("git show :0:'files/foobar.php' | git hash-object --stdin", 'old-file-hash-2'); $shell->resetCommandsCalled(); $messages = runGitWorkflow($options, $shell, $cache, '\PhpcsChangedTests\Debug'); $this->assertEquals($expected->getMessages(), $messages->getMessages()); - $this->assertTrue($shell->wasCommandCalled("git show :0:$(git ls-files --full-name 'foobar.php') | phpcs")); + $this->assertTrue($shell->wasCommandCalled("git show :0:'files/foobar.php' | phpcs")); $this->assertFalse($shell->wasCommandCalled("cat 'foobar.php' | phpcs")); } @@ -293,9 +287,10 @@ public function testFullGitWorkflowForOneFileUnstagedCachesDataThenClearsNewCach $fixture = $this->fixture->getAddedLineDiff('foobar.php', 'use Foobar;'); $shell->registerCommand("git diff --no-prefix 'foobar.php'", $fixture); $shell->registerCommand("git status --porcelain 'foobar.php'", $this->fixture->getModifiedFileInfo('foobar.php')); - $shell->registerCommand("git show :0:$(git ls-files --full-name 'foobar.php') | phpcs", $this->phpcs->getResults('STDIN', [20], 'Found unused symbol Foobar.')->toPhpcsJson()); + $shell->registerCommand("git ls-files --full-name 'foobar.php'", "files/foobar.php"); + $shell->registerCommand("git show :0:'files/foobar.php' | phpcs", $this->phpcs->getResults('STDIN', [20], 'Found unused symbol Foobar.')->toPhpcsJson()); $shell->registerCommand("cat 'foobar.php' | phpcs", $this->phpcs->getResults('STDIN', [21, 20], 'Found unused symbol Foobar.')->toPhpcsJson()); - $shell->registerCommand("git show :0:$(git ls-files --full-name 'foobar.php') | git hash-object --stdin", 'previous-file-hash'); + $shell->registerCommand("git show :0:'files/foobar.php' | git hash-object --stdin", 'previous-file-hash'); $shell->registerCommand("cat 'foobar.php' | git hash-object --stdin", 'new-file-hash'); $shell->registerCommand("git rev-parse --show-toplevel", 'run-from-git-root'); $options = CliOptions::fromArray([ @@ -304,6 +299,7 @@ public function testFullGitWorkflowForOneFileUnstagedCachesDataThenClearsNewCach 'cache' => false, // getopt is weird and sets options to false 'files' => [$gitFile], ]); + $shell->setOptions($options); $cache = new CacheManager( new TestCache(), '\PhpcsChangedTests\Debug' ); $expected = $this->phpcs->getResults('bin/foobar.php', [20], 'Found unused symbol Foobar.'); @@ -314,7 +310,7 @@ public function testFullGitWorkflowForOneFileUnstagedCachesDataThenClearsNewCach $shell->resetCommandsCalled(); $messages = runGitWorkflow($options, $shell, $cache, '\PhpcsChangedTests\Debug'); $this->assertEquals($expected->getMessages(), $messages->getMessages()); - $this->assertFalse($shell->wasCommandCalled("git show :0:$(git ls-files --full-name 'foobar.php') | phpcs")); + $this->assertFalse($shell->wasCommandCalled("git show :0:'files/foobar.php' | phpcs")); $this->assertTrue($shell->wasCommandCalled("cat 'foobar.php' | phpcs")); } @@ -327,12 +323,15 @@ public function testFullGitWorkflowForMultipleFilesStaged() { $shell->registerCommand("git diff --staged --no-prefix 'baz.php'", $fixture); $shell->registerCommand("git status --porcelain 'foobar.php'", $this->fixture->getModifiedFileInfo('foobar.php')); $shell->registerCommand("git status --porcelain 'baz.php'", $this->fixture->getModifiedFileInfo('baz.php')); - $shell->registerCommand("git show HEAD:$(git ls-files --full-name 'foobar.php')", $this->phpcs->getResults('STDIN', [20])->toPhpcsJson()); - $shell->registerCommand("git show HEAD:$(git ls-files --full-name 'baz.php')", $this->phpcs->getResults('STDIN', [20])->toPhpcsJson()); - $shell->registerCommand("git show :0:$(git ls-files --full-name 'foobar.php')", $this->phpcs->getResults('STDIN', [20, 21], 'Found unused symbol Foobar.')->toPhpcsJson()); - $shell->registerCommand("git show :0:$(git ls-files --full-name 'baz.php')", $this->phpcs->getResults('STDIN', [20, 21], 'Found unused symbol Baz.')->toPhpcsJson()); + $shell->registerCommand("git ls-files --full-name 'foobar.php'", "files/foobar.php"); + $shell->registerCommand("git ls-files --full-name 'baz.php'", "files/baz.php"); + $shell->registerCommand("git show HEAD:'files/foobar.php'", $this->phpcs->getResults('STDIN', [20])->toPhpcsJson()); + $shell->registerCommand("git show HEAD:'files/baz.php'", $this->phpcs->getResults('STDIN', [20])->toPhpcsJson()); + $shell->registerCommand("git show :0:'files/foobar.php'", $this->phpcs->getResults('STDIN', [20, 21], 'Found unused symbol Foobar.')->toPhpcsJson()); + $shell->registerCommand("git show :0:'files/baz.php'", $this->phpcs->getResults('STDIN', [20, 21], 'Found unused symbol Baz.')->toPhpcsJson()); $shell->registerCommand("git rev-parse --show-toplevel", 'run-from-git-root'); $options = CliOptions::fromArray(['no-cache-git-root' => 1, 'git-staged' => 1, 'files' => $gitFiles]); + $shell->setOptions($options); $cache = new CacheManager( new TestCache() ); $expected = PhpcsMessages::merge([ $this->phpcs->getResults('bin/foobar.php', [20], 'Found unused symbol Foobar.'), @@ -348,10 +347,12 @@ public function testFullGitWorkflowForUnchangedFileWithPhpcsMessages() { $fixture = $this->fixture->getEmptyFileDiff(); $shell->registerCommand("git diff --staged --no-prefix 'foobar.php'", $fixture); $shell->registerCommand("git status --porcelain 'foobar.php'", ''); - $shell->registerCommand("git show HEAD:$(git ls-files --full-name 'foobar.php')", $this->phpcs->getResults('STDIN', [20])->toPhpcsJson()); - $shell->registerCommand("git show :0:$(git ls-files --full-name 'foobar.php')", $this->phpcs->getResults('STDIN', [20])->toPhpcsJson()); + $shell->registerCommand("git ls-files --full-name 'foobar.php'", "files/foobar.php"); + $shell->registerCommand("git show HEAD:'files/foobar.php'", $this->phpcs->getResults('STDIN', [20])->toPhpcsJson()); + $shell->registerCommand("git show :0:'files/foobar.php'", $this->phpcs->getResults('STDIN', [20])->toPhpcsJson()); $shell->registerCommand("git rev-parse --show-toplevel", 'run-from-git-root'); $options = CliOptions::fromArray(['no-cache-git-root' => 1, 'git-staged' => 1, 'files' => [$gitFile]]); + $shell->setOptions($options); $cache = new CacheManager( new TestCache() ); $expected = PhpcsMessages::fromArrays([], '/dev/null'); $messages = runGitWorkflow($options, $shell, $cache, '\PhpcsChangedTests\Debug'); @@ -362,10 +363,12 @@ public function testFullGitWorkflowForUnchangedFileWithoutPhpcsMessages() { $gitFile = 'foobar.php'; $shell = new TestShell([$gitFile]); $shell->registerCommand("git status --porcelain 'foobar.php'", ''); - $shell->registerCommand("git show HEAD:$(git ls-files --full-name 'foobar.php')", $this->phpcs->getResults('STDIN', [])->toPhpcsJson()); - $shell->registerCommand("git show :0:$(git ls-files --full-name 'foobar.php')", $this->phpcs->getResults('STDIN', [])->toPhpcsJson()); + $shell->registerCommand("git ls-files --full-name 'foobar.php'", "files/foobar.php"); + $shell->registerCommand("git show HEAD:'files/foobar.php'", $this->phpcs->getResults('STDIN', [])->toPhpcsJson()); + $shell->registerCommand("git show :0:'files/foobar.php'", $this->phpcs->getResults('STDIN', [])->toPhpcsJson()); $shell->registerCommand("git rev-parse --show-toplevel", 'run-from-git-root'); $options = CliOptions::fromArray(['no-cache-git-root' => 1, 'git-staged' => 1, 'files' => [$gitFile]]); + $shell->setOptions($options); $cache = new CacheManager( new TestCache() ); $expected = PhpcsMessages::fromArrays([], '/dev/null'); $messages = runGitWorkflow($options, $shell, $cache, '\PhpcsChangedTests\Debug'); @@ -379,10 +382,12 @@ public function testFullGitWorkflowForNonGitFile() { $fixture = $this->fixture->getEmptyFileDiff(); $shell->registerCommand("git diff --staged --no-prefix 'foobar.php'", $fixture); $shell->registerCommand("git status --porcelain 'foobar.php'", "?? foobar.php" ); - $shell->registerCommand("git show HEAD:$(git ls-files --full-name 'foobar.php')", $this->fixture->getNonGitFileShow('foobar.php'), 128); - $shell->registerCommand("git show :0:$(git ls-files --full-name 'foobar.php')", $this->phpcs->getResults('STDIN', [20], 'Found unused symbol Foobar.')->toPhpcsJson()); + $shell->registerCommand("git ls-files --full-name 'foobar.php'", "files/foobar.php"); + $shell->registerCommand("git show HEAD:'files/foobar.php'", $this->fixture->getNonGitFileShow('foobar.php'), 128); + $shell->registerCommand("git show :0:'files/foobar.php'", $this->phpcs->getResults('STDIN', [20], 'Found unused symbol Foobar.')->toPhpcsJson()); $shell->registerCommand("git rev-parse --show-toplevel", 'run-from-git-root'); $options = CliOptions::fromArray(['no-cache-git-root' => 1, 'git-staged' => 1, 'files' => [$gitFile]]); + $shell->setOptions($options); $cache = new CacheManager( new TestCache() ); runGitWorkflow($options, $shell, $cache, '\PhpcsChangedTests\Debug'); } @@ -393,9 +398,11 @@ public function testFullGitWorkflowForNewFile() { $fixture = $this->fixture->getNewFileDiff('foobar.php'); $shell->registerCommand("git diff --staged --no-prefix 'foobar.php'", $fixture); $shell->registerCommand("git status --porcelain 'foobar.php'", $this->fixture->getNewFileInfo('foobar.php')); - $shell->registerCommand("git show :0:$(git ls-files --full-name 'foobar.php')", $this->phpcs->getResults('STDIN', [5, 6], 'Found unused symbol Foobar.')->toPhpcsJson()); + $shell->registerCommand("git ls-files --full-name 'foobar.php'", "files/foobar.php"); + $shell->registerCommand("git show :0:'files/foobar.php", $this->phpcs->getResults('STDIN', [5, 6], 'Found unused symbol Foobar.')->toPhpcsJson()); $shell->registerCommand("git rev-parse --show-toplevel", 'run-from-git-root'); $options = CliOptions::fromArray(['no-cache-git-root' => 1, 'git-staged' => 1, 'files' => [$gitFile]]); + $shell->setOptions($options); $cache = new CacheManager( new TestCache() ); $expected = $this->phpcs->getResults('foobar.php', [5, 6], 'Found unused symbol Foobar.'); $messages = runGitWorkflow($options, $shell, $cache, '\PhpcsChangedTests\Debug'); @@ -413,9 +420,11 @@ public function testFullGitWorkflowForEmptyNewFile() { Run "phpcs --help" for usage information '; - $shell->registerCommand("git show :0:$(git ls-files --full-name 'foobar.php')", $fixture, 1); + $shell->registerCommand("git ls-files --full-name 'foobar.php'", "files/foobar.php"); + $shell->registerCommand("git show :0:'files/foobar.php", $fixture, 1); $options = CliOptions::fromArray(['no-cache-git-root' => 1, 'git-staged' => 1, 'files' => [$gitFile]]); + $shell->setOptions($options); $cache = new CacheManager( new TestCache() ); $expected = PhpcsMessages::fromArrays([], '/dev/null'); $messages = runGitWorkflow($options, $shell, $cache, '\PhpcsChangedTests\Debug'); @@ -426,16 +435,18 @@ function testFullGitWorkflowForInterBranchDiff() { $gitFile = 'bin/foobar.php'; $shell = new TestShell([$gitFile]); $fixture = $this->fixture->getAltAddedLineDiff('foobar.php', 'use Foobar;'); + $shell->registerCommand("git ls-files --full-name 'bin/foobar.php'", "files/bin/foobar.php"); $shell->registerCommand("git merge-base 'master' HEAD", "0123456789abcdef0123456789abcdef01234567\n"); $shell->registerCommand("git diff '0123456789abcdef0123456789abcdef01234567'... --no-prefix 'bin/foobar.php'", $fixture); $shell->registerCommand("git status --porcelain 'bin/foobar.php'", ''); - $shell->registerCommand("git cat-file -e '0123456789abcdef0123456789abcdef01234567':$(git ls-files --full-name 'bin/foobar.php')", ''); - $shell->registerCommand("git show '0123456789abcdef0123456789abcdef01234567':$(git ls-files --full-name 'bin/foobar.php') | phpcs --report=json -q --stdin-path='bin/foobar.php' -", $this->phpcs->getResults('\/srv\/www\/wordpress-default\/public_html\/test\/bin\/foobar.php', [6], 'Found unused symbol Foobar.')->toPhpcsJson()); - $shell->registerCommand("git show '0123456789abcdef0123456789abcdef01234567':$(git ls-files --full-name 'bin/foobar.php') | git hash-object --stdin", 'previous-file-hash'); - $shell->registerCommand("git show HEAD:$(git ls-files --full-name 'bin/foobar.php') | phpcs --report=json -q --stdin-path='bin/foobar.php' -", $this->phpcs->getResults('\/srv\/www\/wordpress-default\/public_html\/test\/bin\/foobar.php', [6, 7], 'Found unused symbol Foobar.')->toPhpcsJson()); - $shell->registerCommand("git show HEAD:$(git ls-files --full-name 'bin/foobar.php') | git hash-object --stdin", 'new-file-hash'); + $shell->registerCommand("git cat-file -e '0123456789abcdef0123456789abcdef01234567':'files/bin/foobar.php'", ''); + $shell->registerCommand("git show '0123456789abcdef0123456789abcdef01234567':'files/bin/foobar.php' | phpcs --report=json -q --stdin-path='bin/foobar.php' -", $this->phpcs->getResults('\/srv\/www\/wordpress-default\/public_html\/test\/bin\/foobar.php', [6], 'Found unused symbol Foobar.')->toPhpcsJson()); + $shell->registerCommand("git show '0123456789abcdef0123456789abcdef01234567':'files/bin/foobar.php' | git hash-object --stdin", 'previous-file-hash'); + $shell->registerCommand("git show HEAD:'files/bin/foobar.php' | phpcs --report=json -q --stdin-path='bin/foobar.php' -", $this->phpcs->getResults('\/srv\/www\/wordpress-default\/public_html\/test\/bin\/foobar.php', [6, 7], 'Found unused symbol Foobar.')->toPhpcsJson()); + $shell->registerCommand("git show HEAD:'files/bin/foobar.php' | git hash-object --stdin", 'new-file-hash'); $shell->registerCommand("git rev-parse --show-toplevel", 'run-from-git-root'); $options = CliOptions::fromArray(['no-cache-git-root' => 1, 'git-base' => 'master', 'files' => [$gitFile]]); + $shell->setOptions($options); $cache = new CacheManager( new TestCache() ); $expected = $this->phpcs->getResults('bin/foobar.php', [6], 'Found unused symbol Foobar.'); $messages = runGitWorkflow($options, $shell, $cache, '\PhpcsChangedTests\Debug'); @@ -448,14 +459,16 @@ function testNameDetectionInFullGitWorkflowForInterBranchDiff() { $shell->registerCommand("git status --porcelain 'test.php'", $this->fixture->getModifiedFileInfo('test.php')); $fixture = $this->fixture->getAltNewFileDiff('test.php'); + $shell->registerCommand("git ls-files --full-name 'test.php'", "files/test.php"); $shell->registerCommand("git merge-base 'master' HEAD", "0123456789abcdef0123456789abcdef01234567\n"); $shell->registerCommand("git diff '0123456789abcdef0123456789abcdef01234567'... --no-prefix 'test.php'", $fixture); - $shell->registerCommand("git cat-file -e '0123456789abcdef0123456789abcdef01234567':$(git ls-files --full-name 'test.php')", '', 128); - $shell->registerCommand("git show HEAD:$(git ls-files --full-name 'test.php') | phpcs --report=json -q --stdin-path='test.php' -", $this->phpcs->getResults('\/srv\/www\/wordpress-default\/public_html\/test\/test.php', [6, 7, 8], "Found unused symbol 'Foobar'.")->toPhpcsJson()); - $shell->registerCommand("git show '0123456789abcdef0123456789abcdef01234567':$(git ls-files --full-name 'test.php') | git hash-object --stdin", 'previous-file-hash'); - $shell->registerCommand("git show HEAD:$(git ls-files --full-name 'test.php') | git hash-object --stdin", 'new-file-hash'); + $shell->registerCommand("git cat-file -e '0123456789abcdef0123456789abcdef01234567':'files/test.php'", '', 128); + $shell->registerCommand("git show HEAD:'files/test.php' | phpcs --report=json -q --stdin-path='test.php' -", $this->phpcs->getResults('\/srv\/www\/wordpress-default\/public_html\/test\/test.php', [6, 7, 8], "Found unused symbol 'Foobar'.")->toPhpcsJson()); + $shell->registerCommand("git show '0123456789abcdef0123456789abcdef01234567':'files/test.php' | git hash-object --stdin", 'previous-file-hash'); + $shell->registerCommand("git show HEAD:'files/test.php | git hash-object --stdin", 'new-file-hash'); $shell->registerCommand("git rev-parse --show-toplevel", 'run-from-git-root'); $options = CliOptions::fromArray(['no-cache-git-root' => 1, 'git-base' => 'master', 'files' => [$gitFile]]); + $shell->setOptions($options); $cache = new CacheManager( new TestCache() ); $expected = PhpcsMessages::merge([ $this->phpcs->getResults('test.php', [6], "Found unused symbol 'Foobar'."), diff --git a/tests/helpers/TestShell.php b/tests/helpers/TestShell.php index fcb8faa..7db0204 100644 --- a/tests/helpers/TestShell.php +++ b/tests/helpers/TestShell.php @@ -3,7 +3,10 @@ namespace PhpcsChangedTests; +use PhpcsChanged\CliOptions; +use PhpcsChanged\Modes; use PhpcsChanged\ShellOperator; +use PhpcsChanged\ShellException; class TestShell implements ShellOperator { @@ -15,12 +18,18 @@ class TestShell implements ShellOperator { private $fileHashes = []; + private $options; + public function __construct(array $readableFileNames) { foreach ($readableFileNames as $fileName) { $this->registerReadableFileName($fileName); } } + public function setOptions(CliOptions $options): void { + $this->options = $options; + } + public function registerReadableFileName(string $fileName, bool $override = false): bool { if (!isset($this->readableFileNames[$fileName]) || $override ) { $this->readableFileNames[$fileName] = true; @@ -66,11 +75,10 @@ public function getFileHash(string $fileName): string { return $this->fileHashes[$fileName] ?? $fileName; } - public function executeCommand(string $command, array &$output = null, int &$return_val = null): string { + public function executeCommand(string $command, int &$return_val = null): string { foreach ($this->commands as $registeredCommand => $return) { if ($registeredCommand === substr($command, 0, strlen($registeredCommand)) ) { $return_val = $return['return_val']; - $output = $return['output']; $this->commandsCalled[$registeredCommand] = $command; return $return['output']; } @@ -92,16 +100,126 @@ public function getFileNameFromPath(string $path): string { return end($parts); } - public function doesFileExistInGit(string $fileName): bool { - if (! $this->isReadable($fileName)) { - return false; + private function getFullGitPathToFile(string $fileName): string { + $git = getenv('GIT') ?: 'git'; + $command = "{$git} ls-files --full-name " . escapeshellarg($fileName); + $fullPath = $this->executeCommand($command); + return trim($fullPath); + } + + private function getModifiedFileContentsCommand(string $fileName): string { + $git = getenv('GIT') ?: 'git'; + $cat = getenv('CAT') ?: 'cat'; + $fullPath = $this->getFullGitPathToFile($fileName); + if ($this->options->mode === Modes::GIT_BASE) { + // for git-base mode, we get the contents of the file from the HEAD version of the file in the current branch + return "{$git} show HEAD:" . escapeshellarg($fullPath); + } + if ($this->options->mode === Modes::GIT_UNSTAGED) { + // for git-unstaged mode, we get the contents of the file from the current working copy + return "{$cat} " . escapeshellarg($fileName); } + // default mode is git-staged, so we get the contents from the staged version of the file + return "{$git} show :0:" . escapeshellarg($fullPath); + } + + private function getUnmodifiedFileContentsCommand(string $fileName): string { + $git = getenv('GIT') ?: 'git'; + if ($this->options->mode === Modes::GIT_BASE) { + $rev = escapeshellarg($this->options->gitBase); + } else if ($this->options->mode === Modes::GIT_UNSTAGED) { + $rev = ':0'; // :0 in this case means "staged version or HEAD if there is no staged version" + } else { + // git-staged is the default + $rev = 'HEAD'; + } + $fullPath = $this->getFullGitPathToFile($fileName); + return "{$git} show {$rev}:" . escapeshellarg($fullPath); + } + + public function getGitHashOfModifiedFile(string $fileName): string { + $git = getenv('GIT') ?: 'git'; + $fileContentsCommand = $this->getModifiedFileContentsCommand($fileName); + $command = "{$fileContentsCommand} | {$git} hash-object --stdin"; + $hash = $this->executeCommand($command); + if (! $hash) { + throw new ShellException("Cannot get modified file hash for file '{$fileName}'"); + } + return $hash; + } + + public function getGitHashOfUnmodifiedFile(string $fileName): string { + $git = getenv('GIT') ?: 'git'; + $fileContentsCommand = $this->getUnmodifiedFileContentsCommand($fileName); + $command = "{$fileContentsCommand} | {$git} hash-object --stdin"; + $hash = $this->executeCommand($command); + if (! $hash) { + throw new ShellException("Cannot get unmodified file hash for file '{$fileName}'"); + } + return $hash; + } + + private function getPhpcsStandardOption(): string { + $phpcsStandard = $this->options->phpcsStandard; + $phpcsStandardOption = $phpcsStandard ? ' --standard=' . escapeshellarg($phpcsStandard) : ''; + $warningSeverity = $this->options->warningSeverity; + $phpcsStandardOption .= isset($warningSeverity) ? ' --warning-severity=' . escapeshellarg($warningSeverity) : ''; + $errorSeverity = $this->options->errorSeverity; + $phpcsStandardOption .= isset($errorSeverity) ? ' --error-severity=' . escapeshellarg($errorSeverity) : ''; + return $phpcsStandardOption; + } + + public function getPhpcsOutputOfModifiedGitFile(string $fileName): string { + $phpcs = getenv('PHPCS') ?: 'phpcs'; + $fileContentsCommand = $this->getModifiedFileContentsCommand($fileName); + $modifiedFilePhpcsOutputCommand = "{$fileContentsCommand} | {$phpcs} --report=json -q" . $this->getPhpcsStandardOption() . ' --stdin-path=' . escapeshellarg($fileName) .' -'; + $modifiedFilePhpcsOutput = $this->executeCommand($modifiedFilePhpcsOutputCommand); + if (! $modifiedFilePhpcsOutput) { + throw new ShellException("Cannot get modified file phpcs output for file '{$fileName}'"); + } + if (false !== strpos($modifiedFilePhpcsOutput, 'You must supply at least one file or directory to process')) { + return ''; + } + return $modifiedFilePhpcsOutput; + } + + public function getPhpcsOutputOfUnmodifiedGitFile(string $fileName): string { + $phpcs = getenv('PHPCS') ?: 'phpcs'; + $unmodifiedFileContentsCommand = $this->getUnmodifiedFileContentsCommand($fileName); + $unmodifiedFilePhpcsOutputCommand = "{$unmodifiedFileContentsCommand} | {$phpcs} --report=json -q" . $this->getPhpcsStandardOption() . ' --stdin-path=' . escapeshellarg($fileName) . ' -'; + $unmodifiedFilePhpcsOutput = $this->executeCommand($unmodifiedFilePhpcsOutputCommand); + if (! $unmodifiedFilePhpcsOutput) { + throw new ShellException("Cannot get unmodified file phpcs output for file '{$fileName}'"); + } + return $unmodifiedFilePhpcsOutput; + } + + private function doesFileExistInGitBase(string $fileName): bool { + $git = getenv('GIT') ?: 'git'; + $gitStatusCommand = "{$git} cat-file -e " . escapeshellarg($this->options->gitBase) . ':' . escapeshellarg($this->getFullGitPathToFile($fileName)) . ' 2>/dev/null'; + /** @var int */ + $return_val = 1; + $this->executeCommand($gitStatusCommand, $return_val); + return 0 !== $return_val; + } + + private function isFileStagedForAdding(string $fileName): bool { $git = getenv('GIT') ?: 'git'; $gitStatusCommand = "{$git} status --porcelain " . escapeshellarg($fileName); $gitStatusOutput = $this->executeCommand($gitStatusCommand); - if (isset($gitStatusOutput[0]) && $gitStatusOutput[0] === '?') { + if (! $gitStatusOutput || false === strpos($gitStatusOutput, $fileName)) { return false; } - return true; + if (isset($gitStatusOutput[0]) && $gitStatusOutput[0] === '?') { + throw new ShellException("File does not appear to be tracked by git: '{$fileName}'"); + } + return isset($gitStatusOutput[0]) && $gitStatusOutput[0] === 'A'; + } + + public function doesUnmodifiedFileExistInGit(string $fileName): bool { + if ($this->options->mode === Modes::GIT_BASE) { + return $this->doesFileExistInGitBase($fileName); + } + return $this->isFileStagedForAdding($fileName); } }