From df80afb36b47824ecd4c6f936612a5e1da20298b Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Fri, 30 Oct 2026 17:03:28 +0100 Subject: [PATCH] Add --blink --commit mode: apply blink rules one by one, one commit per rule --- docs/blink.md | 5 ++ src/Blink/BlinkCommitRunner.php | 93 +++++++++++++++++++++++++++ src/Console/Command/CheckCommand.php | 17 +++++ tests/Blink/BlinkCommitRunnerTest.php | 37 +++++++++++ 4 files changed, 152 insertions(+) create mode 100644 src/Blink/BlinkCommitRunner.php create mode 100644 tests/Blink/BlinkCommitRunnerTest.php diff --git a/docs/blink.md b/docs/blink.md index 0fc62966b8..d83a230ddb 100644 --- a/docs/blink.md +++ b/docs/blink.md @@ -5,6 +5,7 @@ ```bash vendor/bin/ecs check --blink # report only (like a normal check) vendor/bin/ecs check --blink --fix # rewrite files in place +vendor/bin/ecs check --blink --commit # apply rules one by one, one git commit per rule vendor/bin/ecs check src --blink # limit to a path - pass the path before the flag ``` @@ -17,6 +18,10 @@ ECS resolves the configuration from `ecs.php` (paths, rules and skips) exactly a The exit code of `ecs-go` is passed through. +## Commit per rule + +`--blink --commit` applies the resolved rules one at a time, each in its own fix run, and turns every rule that actually changed files into a single git commit (`[Group] RuleName`, from the rule's namespace). Rules that change nothing are skipped. It requires `--blink`; used alone it errors. Run it on a clean working tree, since each commit stages tracked changes with `git add --update`. + ## Dumping the config The JSON ecs-go consumes can also be inspected on its own: diff --git a/src/Blink/BlinkCommitRunner.php b/src/Blink/BlinkCommitRunner.php new file mode 100644 index 0000000000..0dea268e5b --- /dev/null +++ b/src/Blink/BlinkCommitRunner.php @@ -0,0 +1,93 @@ +}>, skips: array} $configData + * @return ExitCode::* + */ + public function run(array $configData): int + { + if ($configData['rules'] === []) { + $this->easyCodingStandardStyle->warning('No rules resolved, nothing to commit.'); + return ExitCode::SUCCESS; + } + + $committedCount = 0; + + foreach ($configData['rules'] as $rule) { + $singleRuleConfig = [ + 'paths' => $configData['paths'], + 'rules' => [$rule], + 'skips' => $configData['skips'], + ]; + + $this->blinkRunner->run($singleRuleConfig, true); + + if (! $this->hasChanges()) { + continue; + } + + $message = $this->createCommitMessage($rule['class']); + $this->commit($message); + + $this->easyCodingStandardStyle->writeln('✓ ' . $message); + ++$committedCount; + } + + $this->easyCodingStandardStyle->success(sprintf('%d rule(s) committed', $committedCount)); + + return ExitCode::SUCCESS; + } + + /** + * Builds "[Group] ShortName" from the rule class, where the group is the + * namespace segment just above the class (e.g. ...\Fixer\Import\OrderedImportsFixer + * yields "[Import] OrderedImportsFixer"). Falls back to the bare short name. + */ + public function createCommitMessage(string $ruleClass): string + { + $parts = explode('\\', $ruleClass); + $shortName = array_pop($parts); + + $group = array_pop($parts); + if ($group === null) { + return $shortName; + } + + return sprintf('[%s] %s', $group, $shortName); + } + + private function hasChanges(): bool + { + exec('git status --porcelain --untracked-files=all 2>/dev/null', $statusLines, $exitCode); + + return $exitCode === 0 && array_filter($statusLines, static fn (string $line): bool => $line !== '') !== []; + } + + private function commit(string $message): void + { + exec('git add --update 2>/dev/null'); + exec(sprintf('git commit --message %s 2>/dev/null', escapeshellarg($message))); + } +} diff --git a/src/Console/Command/CheckCommand.php b/src/Console/Command/CheckCommand.php index c140ad8b19..0f10a3d59a 100644 --- a/src/Console/Command/CheckCommand.php +++ b/src/Console/Command/CheckCommand.php @@ -7,6 +7,7 @@ use Entropy\Console\Contract\CommandInterface; use Entropy\Console\Contract\DefaultCommandInterface; use Symplify\EasyCodingStandard\Application\EasyCodingStandardApplication; +use Symplify\EasyCodingStandard\Blink\BlinkCommitRunner; use Symplify\EasyCodingStandard\Blink\BlinkConfigDumper; use Symplify\EasyCodingStandard\Blink\BlinkRunner; use Symplify\EasyCodingStandard\Configuration\ConfigInitializer; @@ -27,6 +28,7 @@ public function __construct( private EasyCodingStandardApplication $easyCodingStandardApplication, private ConfigurationFactory $configurationFactory, private BlinkRunner $blinkRunner, + private BlinkCommitRunner $blinkCommitRunner, private BlinkConfigDumper $blinkConfigDumper, private EasyCodingStandardStyle $easyCodingStandardStyle, ) { @@ -45,6 +47,7 @@ public function getDescription(): string /** * @param bool $dirty Check only files with uncommitted git changes * @param bool $blink [EXPERIMENTAL] run the ecs-go Go binary instead of the PHP engine + * @param bool $commit [EXPERIMENTAL] with --blink, apply rules one by one and commit each separately * @param string $config Path to config file * @param string $outputFormat Select output format * @param string $memoryLimit Memory limit for check @@ -71,6 +74,7 @@ public function run( bool $debug = false, bool $dirty = false, bool $blink = false, + bool $commit = false, string $config = '', string $outputFormat = ConsoleOutputFormatter::NAME, string $memoryLimit = '', @@ -78,6 +82,14 @@ public function run( string $identifier = '', string ...$paths, ): int { + if ($commit && ! $blink) { + $this->easyCodingStandardStyle->error( + 'The --commit option works only together with --blink. It runs the ecs-go engine rule by rule and creates one git commit per rule. Re-run as: vendor/bin/ecs check --blink --commit' + ); + + return ExitCode::CHANGED_CODE_OR_FOUND_ERRORS; + } + // create ecs.php config file if does not exist yet, unless an explicit --config was provided if ($config === '' && ! $this->configInitializer->areSomeCheckersRegistered()) { $this->configInitializer->createConfig((string) getcwd()); @@ -103,6 +115,11 @@ public function run( // experimental: hand the resolved config to the ecs-go Go binary and skip the PHP engine if ($blink) { $configData = $this->blinkConfigDumper->dump($configuration->getSources()); + + if ($commit) { + return $this->blinkCommitRunner->run($configData); + } + $blinkExitCode = $this->blinkRunner->run($configData, $fix); return $blinkExitCode === ExitCode::SUCCESS ? ExitCode::SUCCESS : ExitCode::CHANGED_CODE_OR_FOUND_ERRORS; } diff --git a/tests/Blink/BlinkCommitRunnerTest.php b/tests/Blink/BlinkCommitRunnerTest.php new file mode 100644 index 0000000000..7438a2836a --- /dev/null +++ b/tests/Blink/BlinkCommitRunnerTest.php @@ -0,0 +1,37 @@ +blinkCommitRunner = new ReflectionClass(BlinkCommitRunner::class)->newInstanceWithoutConstructor(); + } + + public function testCreateCommitMessagePrefixesNamespaceGroup(): void + { + $message = $this->blinkCommitRunner->createCommitMessage( + OrderedImportsFixer::class + ); + + $this->assertSame('[Import] OrderedImportsFixer', $message); + } + + public function testCreateCommitMessageFallsBackToShortName(): void + { + $message = $this->blinkCommitRunner->createCommitMessage('OrderedImportsFixer'); + + $this->assertSame('OrderedImportsFixer', $message); + } +}