diff --git a/src/Configuration/ConfigurationFactory.php b/src/Configuration/ConfigurationFactory.php index ee256294bcb..08f22ce6e8f 100644 --- a/src/Configuration/ConfigurationFactory.php +++ b/src/Configuration/ConfigurationFactory.php @@ -6,6 +6,7 @@ use Rector\ChangesReporting\Output\ConsoleOutputFormatter; use Rector\Configuration\Parameter\SimpleParameterProvider; +use Rector\FileSystem\GitDirtyFileFetcher; use Rector\ValueObject\Configuration; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Style\SymfonyStyle; @@ -145,6 +146,11 @@ private function resolvePaths(InputInterface $input): array return $commandLinePaths; } + $optionDirty = (bool) $input->getOption(\Rector\Configuration\Option::DIRTY); + if ($optionDirty) { + return (new GitDirtyFileFetcher())->fetchDirtyFiles(); + } + // fallback to parameter $configPaths = SimpleParameterProvider::provideArrayParameter(Option::PATHS); $this->setFilesWithoutExtensionParameter($configPaths); diff --git a/src/Configuration/Option.php b/src/Configuration/Option.php index 30f2d3d794c..08b30701105 100644 --- a/src/Configuration/Option.php +++ b/src/Configuration/Option.php @@ -98,6 +98,11 @@ final class Option */ public const CLEAR_CACHE = 'clear-cache'; + /** + * @var string + */ + public const DIRTY = 'dirty'; + /** * @var string */ diff --git a/src/Console/ProcessConfigureDecorator.php b/src/Console/ProcessConfigureDecorator.php index 7f5d3dab9bc..18b34006fb9 100644 --- a/src/Console/ProcessConfigureDecorator.php +++ b/src/Console/ProcessConfigureDecorator.php @@ -66,6 +66,13 @@ public static function decorate(Command $command): void 'Filter only files with specific suffix in name, e.g. "Controller"' ); + $command->addOption( + Option::DIRTY, + null, + InputOption::VALUE_NONE, + 'Only process files with uncommitted changes according to Git (git status --porcelain)' + ); + $command->addOption(Option::DEBUG, null, InputOption::VALUE_NONE, 'Display debug output.'); $command->addOption(Option::MEMORY_LIMIT, null, InputOption::VALUE_REQUIRED, 'Memory limit for process'); $command->addOption(Option::CLEAR_CACHE, null, InputOption::VALUE_NONE, 'Clear unchanged files cache'); diff --git a/src/FileSystem/GitDirtyFileFetcher.php b/src/FileSystem/GitDirtyFileFetcher.php new file mode 100644 index 00000000000..f05c3e3cfbc --- /dev/null +++ b/src/FileSystem/GitDirtyFileFetcher.php @@ -0,0 +1,96 @@ + uses exec(). + * + * @var callable|null + */ + private $commandRunner; + + /** + * @param callable|null $commandRunner signature: fn(string $cmd): array|string + */ + public function __construct(?callable $commandRunner = null) + { + $this->commandRunner = $commandRunner; + } + + /** + * Return relative file paths reported by `git status --porcelain`. + * + * - includes modified (M), added (A), untracked (??) files + * - strips the two-character status prefix and whitespace + * + * @return string[] relative paths + */ + public function fetchDirtyFiles(): array + { + $lines = $this->runCommand('git status --porcelain'); + + $files = []; + + foreach ($lines as $line) { + $line = (string)$line; + if (trim($line) === '') { + continue; + } + + // porcelain format: two-char status space path (path can include spaces) + // e.g. " M src/Service/Foo.php", "?? newfile.php" + if (strlen($line) <= 3) { + continue; + } + + $path = trim(substr($line, 3)); + + if ($path === '') { + continue; + } + + // prefer files on disk to avoid passing directories + if (is_file($path)) { + $files[] = $path; + } + } + + // remove duplicates and empty entries + $files = array_filter($files); + + return array_values($files); + } + + /** + * @return string[] lines + */ + private function runCommand(string $command): array + { + if ($this->commandRunner !== null) { + $result = ($this->commandRunner)($command); + if (is_array($result)) { + return $result; + } + if (is_string($result)) { + return explode("\n", $result); + } + return []; + } + + $output = []; + @exec($command, $output); + return $output; + } +}