diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 765404c..3f39878 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -8,6 +8,9 @@ $finder = Finder::create() ->in(__DIR__ . '/src') - ->append([__DIR__ . '/.php-cs-fixer.php']); + ->append([ + __DIR__ . '/bin/readme-examples-sync', + __DIR__ . '/.php-cs-fixer.php', + ]); return PhpCsFixerConfigFactory::create(TicketSwapRuleSet::create())->setFinder($finder); diff --git a/README.md b/README.md index f635846..6d943cc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ -# README Examples Sync Hook +# README Examples Sync -A [CaptainHook](https://github.com/captainhook-git/captainhook) action that automatically syncs PHP code examples in your README.md with actual source files. This ensures your documentation always shows up-to-date, working code examples. +A PHP script that automatically syncs code examples in your README.md with actual source files. +This ensures your documentation always shows up-to-date, working code examples. ## Installation @@ -8,33 +9,67 @@ A [CaptainHook](https://github.com/captainhook-git/captainhook) action that auto composer require --dev ruudk/readme-examples-sync-hook ``` -## Configuration - -Add the hook to your `captainhook.json` configuration file in the `pre-commit` section: - -```json -{ - "pre-commit": { - "enabled": true, - "actions": [ - { - "action": "\\Ruudk\\ReadmeExamplesSyncHook\\SyncReadmeExamples" - } - ] - } -} +## Usage + +Run the script from your project root: + +```bash +vendor/bin/readme-examples-sync +``` + +### Git Hook Integration + +The easiest way to automatically sync your README on commit is using [Lefthook](https://lefthook.dev): + +1. Install Lefthook (if not already installed): + ```bash + # macOS + brew install lefthook + ``` + +2. Create a `lefthook.yml` file in your project root: + ```yaml + pre-commit: + parallel: false + commands: + sync-readme-examples: + glob: + - "*.php" + - "*.md" + run: vendor/bin/readme-examples-sync + stage_fixed: true + ``` + +3. Install the hooks: + ```bash + lefthook install + ``` + +That's it! Now your README will automatically sync whenever you commit changes to PHP or Markdown files. + +#### Alternative: Manual Git Hook + +If you prefer not to use Lefthook, you can manually create a `.git/hooks/pre-commit` file: + +```bash +#!/bin/bash +vendor/bin/readme-examples-sync +``` + +Don't forget to make the hook executable: + +```bash +chmod +x .git/hooks/pre-commit ``` ## How It Works -This hook scans your README.md file for special HTML comments that mark code examples: +This script scans your README.md file for special HTML comments that mark code examples: 1. **Source code sync**: Updates code blocks marked with `` with the actual content from source files -2. **Output sync** (optional): When you add `` comments, the hook executes PHP files and captures their output to display results +2. **Output sync** (optional): When you add `` comments, the script executes PHP files and captures their output to display results -The hook automatically stages the updated README.md if changes are detected, ensuring your documentation stays in sync with your code. - -## Usage +The script automatically stages the updated README.md if changes are detected (when run in a git repository), ensuring your documentation stays in sync with your code. ### Syncing Source Code diff --git a/bin/readme-examples-sync b/bin/readme-examples-sync new file mode 100755 index 0000000..ca4f3a6 --- /dev/null +++ b/bin/readme-examples-sync @@ -0,0 +1,40 @@ +#!/usr/bin/env php +sync($repositoryRoot); + +exit($exitCode); diff --git a/captainhook.json b/captainhook.json deleted file mode 100644 index 4dc875e..0000000 --- a/captainhook.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "commit-msg": { - "enabled": true, - "actions": [] - }, - "pre-commit": { - "enabled": true, - "actions": [ - { - "action": "composer normalize --diff --dry-run" - }, - { - "action": "\\CaptainHook\\App\\Hook\\PHP\\Action\\Linting" - }, - { - "action": "vendor/bin/php-cs-fixer check --diff" - }, - { - "action": "vendor/bin/phpstan" - } - ] - } -} diff --git a/composer.json b/composer.json index ab45a11..e18e6ad 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,6 @@ "php": "^8.4" }, "require-dev": { - "captainhook/captainhook": "^5.25", "ergebnis/composer-normalize": "^2.48", "friendsofphp/php-cs-fixer": "^3.85", "phpstan/extension-installer": "^1.4", @@ -29,10 +28,18 @@ "Ruudk\\ReadmeExamplesSyncHook\\": "src/" } }, + "bin": [ + "bin/readme-examples-sync" + ], "config": { "allow-plugins": { "ergebnis/composer-normalize": true, "phpstan/extension-installer": true } + }, + "scripts": { + "post-install-cmd": [ + "command -v lefthook >/dev/null 2>&1 && lefthook install || true" + ] } } diff --git a/composer.lock b/composer.lock index 4f729d0..e8b90ca 100644 --- a/composer.lock +++ b/composer.lock @@ -4,149 +4,9 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9b81486503f5cfa83bccd83172220409", + "content-hash": "ba01c0e2ae51a1c4fe2ca0fc6c80d799", "packages": [], "packages-dev": [ - { - "name": "captainhook/captainhook", - "version": "5.25.11", - "source": { - "type": "git", - "url": "https://github.com/captainhook-git/captainhook.git", - "reference": "f2278edde4b45af353861aae413fc3840515bb80" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/captainhook-git/captainhook/zipball/f2278edde4b45af353861aae413fc3840515bb80", - "reference": "f2278edde4b45af353861aae413fc3840515bb80", - "shasum": "" - }, - "require": { - "captainhook/secrets": "^0.9.4", - "ext-json": "*", - "ext-spl": "*", - "ext-xml": "*", - "php": ">=8.0", - "sebastianfeldmann/camino": "^0.9.2", - "sebastianfeldmann/cli": "^3.3", - "sebastianfeldmann/git": "^3.14", - "symfony/console": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", - "symfony/filesystem": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", - "symfony/process": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" - }, - "replace": { - "sebastianfeldmann/captainhook": "*" - }, - "require-dev": { - "composer/composer": "~1 || ^2.0", - "mikey179/vfsstream": "~1" - }, - "bin": [ - "bin/captainhook" - ], - "type": "library", - "extra": { - "captainhook": { - "config": "captainhook.json" - }, - "branch-alias": { - "dev-main": "6.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "CaptainHook\\App\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Sebastian Feldmann", - "email": "sf@sebastian-feldmann.info" - } - ], - "description": "PHP git hook manager", - "homepage": "http://php.captainhook.info/", - "keywords": [ - "commit-msg", - "git", - "hooks", - "post-merge", - "pre-commit", - "pre-push", - "prepare-commit-msg" - ], - "support": { - "issues": "https://github.com/captainhook-git/captainhook/issues", - "source": "https://github.com/captainhook-git/captainhook/tree/5.25.11" - }, - "funding": [ - { - "url": "https://github.com/sponsors/sebastianfeldmann", - "type": "github" - } - ], - "time": "2025-08-12T12:14:57+00:00" - }, - { - "name": "captainhook/secrets", - "version": "0.9.7", - "source": { - "type": "git", - "url": "https://github.com/captainhook-git/secrets.git", - "reference": "d62c97f75f81ac98e22f1c282482bd35fa82f631" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/captainhook-git/secrets/zipball/d62c97f75f81ac98e22f1c282482bd35fa82f631", - "reference": "d62c97f75f81ac98e22f1c282482bd35fa82f631", - "shasum": "" - }, - "require": { - "ext-mbstring": "*", - "php": ">=8.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "CaptainHook\\Secrets\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Sebastian Feldmann", - "email": "sf@sebastian-feldmann.info" - } - ], - "description": "Utility classes to detect secrets", - "keywords": [ - "commit-msg", - "keys", - "passwords", - "post-merge", - "prepare-commit-msg", - "secrets", - "tokens" - ], - "support": { - "issues": "https://github.com/captainhook-git/secrets/issues", - "source": "https://github.com/captainhook-git/secrets/tree/0.9.7" - }, - "funding": [ - { - "url": "https://github.com/sponsors/sebastianfeldmann", - "type": "github" - } - ], - "time": "2025-04-08T07:10:48+00:00" - }, { "name": "clue/ndjson-react", "version": "v1.3.0", @@ -2325,182 +2185,6 @@ ], "time": "2025-02-07T04:55:46+00:00" }, - { - "name": "sebastianfeldmann/camino", - "version": "0.9.5", - "source": { - "type": "git", - "url": "https://github.com/sebastianfeldmann/camino.git", - "reference": "bf2e4c8b2a029e9eade43666132b61331e3e8184" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianfeldmann/camino/zipball/bf2e4c8b2a029e9eade43666132b61331e3e8184", - "reference": "bf2e4c8b2a029e9eade43666132b61331e3e8184", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "SebastianFeldmann\\Camino\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Sebastian Feldmann", - "email": "sf@sebastian-feldmann.info" - } - ], - "description": "Path management the OO way", - "homepage": "https://github.com/sebastianfeldmann/camino", - "keywords": [ - "file system", - "path" - ], - "support": { - "issues": "https://github.com/sebastianfeldmann/camino/issues", - "source": "https://github.com/sebastianfeldmann/camino/tree/0.9.5" - }, - "funding": [ - { - "url": "https://github.com/sebastianfeldmann", - "type": "github" - } - ], - "time": "2022-01-03T13:15:10+00:00" - }, - { - "name": "sebastianfeldmann/cli", - "version": "3.4.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianfeldmann/cli.git", - "reference": "6fa122afd528dae7d7ec988a604aa6c600f5d9b5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianfeldmann/cli/zipball/6fa122afd528dae7d7ec988a604aa6c600f5d9b5", - "reference": "6fa122afd528dae7d7ec988a604aa6c600f5d9b5", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "require-dev": { - "symfony/process": "^4.3 | ^5.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.4.x-dev" - } - }, - "autoload": { - "psr-4": { - "SebastianFeldmann\\Cli\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Sebastian Feldmann", - "email": "sf@sebastian-feldmann.info" - } - ], - "description": "PHP cli helper classes", - "homepage": "https://github.com/sebastianfeldmann/cli", - "keywords": [ - "cli" - ], - "support": { - "issues": "https://github.com/sebastianfeldmann/cli/issues", - "source": "https://github.com/sebastianfeldmann/cli/tree/3.4.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianfeldmann", - "type": "github" - } - ], - "time": "2024-11-26T10:19:01+00:00" - }, - { - "name": "sebastianfeldmann/git", - "version": "3.14.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianfeldmann/git.git", - "reference": "22584df8df01d95b0700000cfd855779fae7d8ea" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianfeldmann/git/zipball/22584df8df01d95b0700000cfd855779fae7d8ea", - "reference": "22584df8df01d95b0700000cfd855779fae7d8ea", - "shasum": "" - }, - "require": { - "ext-json": "*", - "ext-libxml": "*", - "ext-simplexml": "*", - "php": ">=8.0", - "sebastianfeldmann/cli": "^3.0" - }, - "require-dev": { - "mikey179/vfsstream": "^1.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "SebastianFeldmann\\Git\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Sebastian Feldmann", - "email": "sf@sebastian-feldmann.info" - } - ], - "description": "PHP git wrapper", - "homepage": "https://github.com/sebastianfeldmann/git", - "keywords": [ - "git" - ], - "support": { - "issues": "https://github.com/sebastianfeldmann/git/issues", - "source": "https://github.com/sebastianfeldmann/git/tree/3.14.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianfeldmann", - "type": "github" - } - ], - "time": "2025-06-05T16:05:10+00:00" - }, { "name": "symfony/console", "version": "v7.3.2", diff --git a/lefthook.yaml b/lefthook.yaml new file mode 100644 index 0000000..46fcd4a --- /dev/null +++ b/lefthook.yaml @@ -0,0 +1,19 @@ +pre-commit: + parallel: true + commands: + composer-normalize: + glob: + - "composer.json" + - "composer.lock" + run: composer normalize --diff + stage_fixed: true + + php-cs-fixer: + glob: + - "bin/readme-examples-sync" + - "*.php" + run: vendor/bin/php-cs-fixer fix --config .php-cs-fixer.php -- {staged_files} + stage_fixed: true + + phpstan: + run: vendor/bin/phpstan diff --git a/src/SyncReadmeExamples.php b/src/SyncReadmeExamples.php index 92b37f4..ecd8c71 100644 --- a/src/SyncReadmeExamples.php +++ b/src/SyncReadmeExamples.php @@ -4,53 +4,54 @@ namespace Ruudk\ReadmeExamplesSyncHook; -use CaptainHook\App\Config; -use CaptainHook\App\Config\Action as ActionConfig; -use CaptainHook\App\Console\IO; -use CaptainHook\App\Hook\Action; -use Override; -use SebastianFeldmann\Git\Repository; - -final class SyncReadmeExamples implements Action +final class SyncReadmeExamples { - #[Override] - public function execute(Config $config, IO $io, Repository $repository, ActionConfig $action) : void + /** + * Sync README.md with example files + * + * @return int Exit code (0 for success, 1 for error) + */ + public function sync(string $repositoryRoot) : int { - $readmePath = $repository->getRoot() . '/README.md'; + $readmePath = $repositoryRoot . '/README.md'; if ( ! file_exists($readmePath)) { - $io->write('README.md not found, skipping sync', true, IO::VERBOSE); + $this->writeOutput('README.md not found, skipping sync'); - return; + return 0; } - $io->write('Syncing README.md with example files...', true, IO::VERBOSE); + $this->writeOutput('Syncing README.md with example files...'); $readme = file_get_contents($readmePath); if ($readme === false) { - $io->write('Failed to read README.md'); + $this->writeError('Failed to read README.md'); - return; + return 1; } - $updatedReadme = $this->syncReadmeWithExamples($readme, $repository->getRoot(), $io); + $updatedReadme = $this->syncReadmeWithExamples($readme, $repositoryRoot); if ($readme !== $updatedReadme) { file_put_contents($readmePath, $updatedReadme); - $io->write('✓ README.md has been synced with example files'); + $this->writeOutput('✓ README.md has been synced with example files'); - // Stage the README changes - exec('git add README.md'); + // Stage the README changes if in a git repository + if (is_dir($repositoryRoot . '/.git')) { + exec('git add README.md'); + } } else { - $io->write('✓ README.md is already in sync'); + $this->writeOutput('✓ README.md is already in sync'); } + + return 0; } /** * Sync README content with example files */ - private function syncReadmeWithExamples(string $readme, string $repositoryRoot, IO $io) : string + private function syncReadmeWithExamples(string $readme, string $repositoryRoot) : string { $lines = explode("\n", $readme); $result = []; @@ -77,7 +78,7 @@ private function syncReadmeWithExamples(string $readme, string $repositoryRoot, } // Insert new code from source file - $code = $this->getExampleCode($sourceFile, $repositoryRoot, $io); + $code = $this->getExampleCode($sourceFile, $repositoryRoot); if ($code !== null) { // Add code lines without trailing newline on last line @@ -110,7 +111,7 @@ private function syncReadmeWithExamples(string $readme, string $repositoryRoot, } // Insert new output from executing the file - $output = $this->executeExample($sourceFile, $repositoryRoot, $io); + $output = $this->executeExample($sourceFile, $repositoryRoot); if ($output !== null) { $outputLines = explode("\n", rtrim($output)); @@ -136,12 +137,12 @@ private function syncReadmeWithExamples(string $readme, string $repositoryRoot, /** * Get code from example file with path adjustments */ - private function getExampleCode(string $sourceFile, string $repositoryRoot, IO $io) : ?string + private function getExampleCode(string $sourceFile, string $repositoryRoot) : ?string { $fullPath = $repositoryRoot . '/' . $sourceFile; if ( ! file_exists($fullPath)) { - $io->write(sprintf('Source file not found: %s', $sourceFile), true, IO::VERBOSE); + $this->writeOutput(sprintf('Warning: Source file not found: %s', $sourceFile)); return null; } @@ -149,7 +150,7 @@ private function getExampleCode(string $sourceFile, string $repositoryRoot, IO $ $code = file_get_contents($fullPath); if ($code === false) { - $io->write(sprintf('Failed to read source file: %s', $sourceFile), true, IO::VERBOSE); + $this->writeOutput(sprintf('Warning: Failed to read source file: %s', $sourceFile)); return null; } @@ -186,12 +187,12 @@ private function getExampleCode(string $sourceFile, string $repositoryRoot, IO $ /** * Execute example file and capture output */ - private function executeExample(string $sourceFile, string $repositoryRoot, IO $io) : ?string + private function executeExample(string $sourceFile, string $repositoryRoot) : ?string { $fullPath = $repositoryRoot . '/' . $sourceFile; if ( ! file_exists($fullPath)) { - $io->write(sprintf('Source file not found: %s', $sourceFile), true, IO::VERBOSE); + $this->writeOutput(sprintf('Warning: Source file not found: %s', $sourceFile)); return null; } @@ -204,7 +205,7 @@ private function executeExample(string $sourceFile, string $repositoryRoot, IO $ $code = file_get_contents($fullPath); if ($code === false) { - $io->write(sprintf('Failed to read source file: %s', $sourceFile), true, IO::VERBOSE); + $this->writeOutput(sprintf('Warning: Failed to read source file: %s', $sourceFile)); return null; } @@ -217,7 +218,7 @@ private function executeExample(string $sourceFile, string $repositoryRoot, IO $ ); if ($tempFile === false) { - $io->write(sprintf('Failed to create temp file for: %s', $sourceFile), true, IO::VERBOSE); + $this->writeOutput(sprintf('Warning: Failed to create temp file for: %s', $sourceFile)); return null; } @@ -229,7 +230,7 @@ private function executeExample(string $sourceFile, string $repositoryRoot, IO $ $output = shell_exec($command); if ($output === null || $output === false) { - $io->write(sprintf('Failed to execute: %s', $sourceFile), true, IO::VERBOSE); + $this->writeOutput(sprintf('Warning: Failed to execute: %s', $sourceFile)); return null; } @@ -299,4 +300,20 @@ private function getLanguageFromExtension(string $filePath) : string default => '', }; } + + /** + * Write output message to stdout + */ + private function writeOutput(string $message) : void + { + echo $message . PHP_EOL; + } + + /** + * Write error message to stderr + */ + private function writeError(string $message) : void + { + fwrite(STDERR, $message . PHP_EOL); + } }