From 76ff69e05ddc3fcecee4ba9c7442de596a6b6224 Mon Sep 17 00:00:00 2001 From: Raphael Stolt Date: Tue, 21 Jan 2025 13:57:29 +0100 Subject: [PATCH] Initial implementation --- .editorconfig | 19 +++++++++ .gitattributes | 13 ++++++ .github/workflows/lint.yml | 32 ++++++++++++++ .github/workflows/test.yml | 34 +++++++++++++++ .gitignore | 7 +++ .php-cs-fixer.php | 28 ++++++++++++ CHANGELOG.md | 14 ++++++ LICENSE.md | 21 +++++++++ README.md | 39 +++++++++++++++++ composer.json | 48 +++++++++++++++++++++ phpstan.neon.dist | 21 +++++++++ phpunit.xml.dist | 18 ++++++++ src/Difftastic.php | 87 ++++++++++++++++++++++++++++++++++++++ tests/DifftasticTest.php | 48 +++++++++++++++++++++ 14 files changed, 429 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .php-cs-fixer.php create mode 100644 CHANGELOG.md create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpstan.neon.dist create mode 100644 phpunit.xml.dist create mode 100644 src/Difftastic.php create mode 100644 tests/DifftasticTest.php diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..36ec1c6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.yml] +indent_style = space +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9eca169 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,13 @@ +* text=auto eol=lf + +.editorconfig export-ignore +.gitattributes export-ignore +.github/ export-ignore +.gitignore export-ignore +.php-cs-fixer.php export-ignore +CHANGELOG.md export-ignore +LICENSE.md export-ignore +phpstan.neon.dist export-ignore +phpunit.xml.dist export-ignore +README.md export-ignore +tests/ export-ignore diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..16ca44b --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,32 @@ +name: lint + +on: push + +jobs: + build: + name: lint + runs-on: ubuntu-latest + + strategy: + fail-fast: true + matrix: + php: + - "8.3" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install difftastic + run: sudo snap install difftastic + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "${{ matrix.php }}" + + - name: Install Composer dependencies + run: composer install --no-progress --prefer-dist --optimize-autoloader + + - name: Check coding styles + run: composer run-script cs-lint diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..7be9c66 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,34 @@ +name: test + +on: push + +jobs: + build: + name: "PHPUnit (PHP ${{ matrix.php }})" + runs-on: ubuntu-latest + + strategy: + matrix: + php: + - "8.1" + - "8.2" + - "8.3" + - "8.4" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install difftastic + run: sudo snap install difftastic + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "${{ matrix.php }}" + + - name: Install Composer dependencies + run: composer install --no-progress --prefer-dist --optimize-autoloader + + - name: Run tests + run: composer run-script test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b647d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +vendor/ +coverage-reports/ +composer.lock +.php_cs.cache +.phpunit.result.cache +.phpunit.cache +.idea/ diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..59c1a79 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,28 @@ +in([__DIR__, __DIR__ . DIRECTORY_SEPARATOR . 'tests']); + +$rules = [ + 'psr_autoloading' => false, + '@PSR2' => true, + 'phpdoc_order' => true, + 'ordered_imports' => true, + 'native_function_invocation' => [ + 'include' => ['@internal'], + 'exclude' => ['file_put_contents'] + ] +]; + +$cacheDir = \getenv('HOME') ? \getenv('HOME') : __DIR__; + +$config = new Config(); + +return $config->setRules($rules) + ->setParallelConfig(ParallelConfigFactory::detect()) + ->setFinder($finder) + ->setCacheFile($cacheDir . '/.php-cs-fixer.cache'); diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..72ad1ff --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to +[Semantic Versioning](http://semver.org/). + +## [Unreleased] + +## v0.0.1 - 2025-01-24 + +- Initial release. + +[Unreleased]: https://github.com/raphaelstolt/difftastic-php/compare/v0.0.1...HEAD diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..212cb93 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2025 - ∞ Raphael Stolt + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..26d755b --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# Difftastic PHP + +This Composer package provides a wrapper around [difftastic](https://github.com/Wilfred/difftastic) +for usage in PHP based projects; therefor it requires `difftastic` to be [installed](https://difftastic.wilfred.me.uk/installation.html). + +## Installation + +The difftastic wrapper for PHP can be installed through Composer. + +``` bash +composer require stolt/difftastic-php +``` + +## Usage + +```php +use Stolt\Difftastic; + +$difftastic = new Difftastic(); +$diff = $difftastic->diff('[1, 2, 3]', '[3, 2, 1]'); +``` + +### Running tests + +``` bash +composer test +``` + +### License + +This PHP package is licensed under the MIT license. Please see [LICENSE.md](LICENSE.md) for more details. + +### Changelog + +Please see [CHANGELOG.md](CHANGELOG.md) for more details. + +### Contributing + +Please see [CONTRIBUTING.md](.github/CONTRIBUTING.md) for more details. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..ba9480f --- /dev/null +++ b/composer.json @@ -0,0 +1,48 @@ +{ + "name": "stolt/difftastic-php", + "description": "A wrapper around the difftastic CLI.", + "keywords": ["difftastic", "wrapper", "dev"], + "type": "library", + "license": "MIT", + "autoload": { + "psr-4": { + "Stolt\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Stolt\\Tests\\": "tests/" + } + }, + "authors": [ + { + "name": "Raphael Stolt", + "email": "raphael.stolt@gmail.com" + } + ], + "require": { + "php": ">=8.1" + }, + "scripts": { + "test": "phpunit", + "test-with-coverage": "export XDEBUG_MODE=coverage && phpunit --coverage-html coverage-reports", + "cs-fix": "php-cs-fixer --allow-risky=yes fix . -vv || true", + "cs-lint": "php-cs-fixer fix --diff --stop-on-violation --verbose --dry-run --allow-risky=yes", + "static-analyse": "phpstan analyse --configuration phpstan.neon.dist", + "pre-commit-check": [ + "@test", + "@cs-lint", + "@static-analyse" + ] + }, + "config": { + "preferred-install": "dist", + "sort-packages": true, + "optimize-autoloader": true + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^11.4.4||^10.5.25" + } +} diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..cd547dd --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,21 @@ +parameters: + level: 8 + paths: + - src + - tests + treatPhpDocTypesAsCertain: false + reportUnmatchedIgnoredErrors: false + ignoreErrors: + - identifier: missingType.iterableValue + - identifier: argument.type + - identifier: identical.alwaysTrue + - identifier: method.alreadyNarrowedType + - '#Constant WORKING_DIRECTORY not found.#' + - '#Mockery\\MockInterface#' + - '#Mockery\\LegacyMockInterface#' + - '#Mockery\\ExpectationInterface#' + - '#array_filter#' + - '#is never read#' + - '#TMock#' + - '#unknown class#' + - '#set_error_handler expects#' diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..d51182e --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,18 @@ + + + + + + tests/ + + + + + src/ + + + diff --git a/src/Difftastic.php b/src/Difftastic.php new file mode 100644 index 0000000..fa79da4 --- /dev/null +++ b/src/Difftastic.php @@ -0,0 +1,87 @@ +isDifftasticCommandAvailable() === false) { + throw new RuntimeException('Difftastic CLI not available'); + } + // is difftastic binary installed [x] + // set config/option values [x] (see https://github.com/joeldrapper/difftastic-ruby/blob/main/lib/difftastic/differ.rb) + // determine which difftastic binary to use, difftastic or difft [x] + } + + private function isDifftasticCommandAvailable(): bool + { + $this->difftasticBinaryCommand = $this->getDifftasticBinaryByOs(); + + \exec('where ' . $this->difftasticBinaryCommand . ' 2>&1', $output, $returnValue); + if ($this->isWindows() === false) { + \exec('which ' . $this->difftasticBinaryCommand . ' 2>&1', $output, $returnValue); + } + + return $returnValue === 0; + } + + private function getDifftasticBinaryByOs(): string + { + return match (\strtolower(PHP_OS_FAMILY)) { + 'darwin' => 'difft', + 'linux' => 'difftastic', + 'windows' => 'difftastic', + default => throw new RuntimeException( + 'Unsupported operating system ' . PHP_OS_FAMILY + ) + }; + } + private function createTemporaryFile($name, $content): string + { + $file = DIRECTORY_SEPARATOR . + \trim(\sys_get_temp_dir(), DIRECTORY_SEPARATOR) . + DIRECTORY_SEPARATOR . + \ltrim($name, DIRECTORY_SEPARATOR); + + file_put_contents($file, $content); + + \register_shutdown_function(function () use ($file) { + \unlink($file); + }); + + return $file; + } + + private function isWindows($os = PHP_OS): bool + { + if (\strtoupper(\substr($os, 0, 3)) !== 'WIN') { + return false; + } + + return true; + } + + public function diff(string $a, string $b): string + { + $aFile = $this->createTemporaryFile('a', $a); + $bFile = $this->createTemporaryFile('b', $b); + + \exec( + $this->difftasticBinaryCommand . ' ' . $aFile . ' ' . $bFile . ' 2>&1', + $output, + $returnValue + ); + + return \implode(PHP_EOL, $output); + } +} diff --git a/tests/DifftasticTest.php b/tests/DifftasticTest.php new file mode 100644 index 0000000..c509185 --- /dev/null +++ b/tests/DifftasticTest.php @@ -0,0 +1,48 @@ +getMethod('getDifftasticBinaryByOs'); + $getDifftasticBinaryByOsMethod->setAccessible(true); + + $difftastic = new Difftastic(); + + $expectedBinary = 'difftastic'; + + if (\strtolower(PHP_OS_FAMILY) === 'darwin') { + $expectedBinary = 'difft'; + } + + $actualBinary = $getDifftasticBinaryByOsMethod->invokeArgs($difftastic, []); + + $this->assertEquals($expectedBinary, $actualBinary); + } + + #[Test] + #[Ticket('https://github.com/Wilfred/difftastic/issues/809')] + public function returnsExpectedDiff(): void + { + $difftastic = new Difftastic(); + $diff = $difftastic->diff('[1, 2, 3]', '[3, 2, 1]'); + + $expectedDifftasticOutput = <<assertEquals($diff, $expectedDifftasticOutput); + } +}