├── ChangeLog.md ├── LICENSE ├── README.md ├── composer.json └── src ├── Chunk.php ├── Diff.php ├── Differ.php ├── Exception ├── ConfigurationException.php ├── Exception.php └── InvalidArgumentException.php ├── Line.php ├── LongestCommonSubsequenceCalculator.php ├── MemoryEfficientLongestCommonSubsequenceCalculator.php ├── Output ├── AbstractChunkOutputBuilder.php ├── DiffOnlyOutputBuilder.php ├── DiffOutputBuilderInterface.php ├── StrictUnifiedDiffOutputBuilder.php └── UnifiedDiffOutputBuilder.php ├── Parser.php └── TimeEfficientLongestCommonSubsequenceCalculator.php /ChangeLog.md: -------------------------------------------------------------------------------- 1 | # ChangeLog 2 | 3 | All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles. 4 | 5 | ## [4.0.2] - 2020-06-30 6 | 7 | ### Added 8 | 9 | * This component is now supported on PHP 8 10 | 11 | ## [4.0.1] - 2020-05-08 12 | 13 | ### Fixed 14 | 15 | * [#99](https://github.com/sebastianbergmann/diff/pull/99): Regression in unified diff output of identical strings 16 | 17 | ## [4.0.0] - 2020-02-07 18 | 19 | ### Removed 20 | 21 | * Removed support for PHP 7.1 and PHP 7.2 22 | 23 | ## [3.0.2] - 2019-02-04 24 | 25 | ### Changed 26 | 27 | * `Chunk::setLines()` now ensures that the `$lines` array only contains `Line` objects 28 | 29 | ## [3.0.1] - 2018-06-10 30 | 31 | ### Fixed 32 | 33 | * Removed `"minimum-stability": "dev",` from `composer.json` 34 | 35 | ## [3.0.0] - 2018-02-01 36 | 37 | * The `StrictUnifiedDiffOutputBuilder` implementation of the `DiffOutputBuilderInterface` was added 38 | 39 | ### Changed 40 | 41 | * The default `DiffOutputBuilderInterface` implementation now generates context lines (unchanged lines) 42 | 43 | ### Removed 44 | 45 | * Removed support for PHP 7.0 46 | 47 | ### Fixed 48 | 49 | * [#70](https://github.com/sebastianbergmann/diff/issues/70): Diffing of arrays no longer works 50 | 51 | ## [2.0.1] - 2017-08-03 52 | 53 | ### Fixed 54 | 55 | * [#66](https://github.com/sebastianbergmann/diff/pull/66): Restored backwards compatibility for PHPUnit 6.1.4, 6.2.0, 6.2.1, 6.2.2, and 6.2.3 56 | 57 | ## [2.0.0] - 2017-07-11 [YANKED] 58 | 59 | ### Added 60 | 61 | * [#64](https://github.com/sebastianbergmann/diff/pull/64): Show line numbers for chunks of a diff 62 | 63 | ### Removed 64 | 65 | * This component is no longer supported on PHP 5.6 66 | 67 | [4.0.2]: https://github.com/sebastianbergmann/diff/compare/4.0.1...4.0.2 68 | [4.0.1]: https://github.com/sebastianbergmann/diff/compare/4.0.0...4.0.1 69 | [4.0.0]: https://github.com/sebastianbergmann/diff/compare/3.0.2...4.0.0 70 | [3.0.2]: https://github.com/sebastianbergmann/diff/compare/3.0.1...3.0.2 71 | [3.0.1]: https://github.com/sebastianbergmann/diff/compare/3.0.0...3.0.1 72 | [3.0.0]: https://github.com/sebastianbergmann/diff/compare/2.0...3.0.0 73 | [2.0.1]: https://github.com/sebastianbergmann/diff/compare/c341c98ce083db77f896a0aa64f5ee7652915970...2.0.1 74 | [2.0.0]: https://github.com/sebastianbergmann/diff/compare/1.4...c341c98ce083db77f896a0aa64f5ee7652915970 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | sebastian/diff 2 | 3 | Copyright (c) 2002-2020, Sebastian Bergmann . 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions 8 | are met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in 15 | the documentation and/or other materials provided with the 16 | distribution. 17 | 18 | * Neither the name of Sebastian Bergmann nor the names of his 19 | contributors may be used to endorse or promote products derived 20 | from this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33 | POSSIBILITY OF SUCH DAMAGE. 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # diff 2 | 3 | [![Integrate](https://github.com/localheinz/diff/workflows/Integrate/badge.svg)](https://github.com/localheinz/diff/actions) 4 | [![Merge](https://github.com/localheinz/diff/workflows/Merge/badge.svg)](https://github.com/localheinz/diff/actions) 5 | [![Release](https://github.com/localheinz/diff/workflows/Release/badge.svg)](https://github.com/localheinz/diff/actions) 6 | [![Renew](https://github.com/localheinz/diff/workflows/Renew/badge.svg)](https://github.com/localheinz/diff/actions) 7 | 8 | [![Code Coverage](https://codecov.io/gh/localheinz/diff/branch/main/graph/badge.svg)](https://codecov.io/gh/localheinz/diff) 9 | [![Type Coverage](https://shepherd.dev/github/localheinz/diff/coverage.svg)](https://shepherd.dev/github/localheinz/diff) 10 | 11 | [![Latest Stable Version](https://poser.pugx.org/localheinz/diff/v/stable)](https://packagist.org/packages/localheinz/diff) 12 | [![Total Downloads](https://poser.pugx.org/localheinz/diff/downloads)](https://packagist.org/packages/localheinz/diff) 13 | [![Monthly Downloads](http://poser.pugx.org/localheinz/diff/d/monthly)](https://packagist.org/packages/localheinz/diff) 14 | 15 | This is a fork of [`sebastian/diff`](https://github.com/sebastianbergmann/diff) for use with [`ergebnis/composer-normalize`](https://github.com/ergebnis/composer-normalize), with permission from Sebastian Bergmann. 16 | 17 | Please use [`sebastian/diff`](https://github.com/sebastianbergmann/diff) instead. 18 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "localheinz/diff", 3 | "description": "Fork of sebastian/diff for use with ergebnis/composer-normalize", 4 | "keywords": [ 5 | "diff", 6 | "udiff", 7 | "unidiff", 8 | "unified diff" 9 | ], 10 | "homepage": "https://github.com/localheinz/diff", 11 | "license": "BSD-3-Clause", 12 | "authors": [ 13 | { 14 | "name": "Sebastian Bergmann", 15 | "email": "sebastian@phpunit.de" 16 | }, 17 | { 18 | "name": "Kore Nordmann", 19 | "email": "mail@kore-nordmann.de" 20 | } 21 | ], 22 | "require": { 23 | "php": "~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "^7.5.0 || ^8.5.23", 27 | "symfony/process": "^4.2 || ^5" 28 | }, 29 | "config": { 30 | "optimize-autoloader": true, 31 | "platform": { 32 | "php": "7.1.33" 33 | }, 34 | "sort-packages": true 35 | }, 36 | "autoload": { 37 | "classmap": [ 38 | "src/" 39 | ] 40 | }, 41 | "autoload-dev": { 42 | "classmap": [ 43 | "tests/" 44 | ] 45 | }, 46 | "prefer-stable": true 47 | } 48 | -------------------------------------------------------------------------------- /src/Chunk.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Localheinz\Diff; 11 | 12 | final class Chunk 13 | { 14 | /** 15 | * @var int 16 | */ 17 | private $start; 18 | 19 | /** 20 | * @var int 21 | */ 22 | private $startRange; 23 | 24 | /** 25 | * @var int 26 | */ 27 | private $end; 28 | 29 | /** 30 | * @var int 31 | */ 32 | private $endRange; 33 | 34 | /** 35 | * @var Line[] 36 | */ 37 | private $lines; 38 | 39 | public function __construct(int $start = 0, int $startRange = 1, int $end = 0, int $endRange = 1, array $lines = []) 40 | { 41 | $this->start = $start; 42 | $this->startRange = $startRange; 43 | $this->end = $end; 44 | $this->endRange = $endRange; 45 | $this->lines = $lines; 46 | } 47 | 48 | public function getStart(): int 49 | { 50 | return $this->start; 51 | } 52 | 53 | public function getStartRange(): int 54 | { 55 | return $this->startRange; 56 | } 57 | 58 | public function getEnd(): int 59 | { 60 | return $this->end; 61 | } 62 | 63 | public function getEndRange(): int 64 | { 65 | return $this->endRange; 66 | } 67 | 68 | /** 69 | * @return Line[] 70 | */ 71 | public function getLines(): array 72 | { 73 | return $this->lines; 74 | } 75 | 76 | /** 77 | * @param Line[] $lines 78 | */ 79 | public function setLines(array $lines): void 80 | { 81 | foreach ($lines as $line) { 82 | if (!$line instanceof Line) { 83 | throw new InvalidArgumentException; 84 | } 85 | } 86 | 87 | $this->lines = $lines; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Diff.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Localheinz\Diff; 11 | 12 | final class Diff 13 | { 14 | /** 15 | * @var string 16 | */ 17 | private $from; 18 | 19 | /** 20 | * @var string 21 | */ 22 | private $to; 23 | 24 | /** 25 | * @var Chunk[] 26 | */ 27 | private $chunks; 28 | 29 | /** 30 | * @param Chunk[] $chunks 31 | */ 32 | public function __construct(string $from, string $to, array $chunks = []) 33 | { 34 | $this->from = $from; 35 | $this->to = $to; 36 | $this->chunks = $chunks; 37 | } 38 | 39 | public function getFrom(): string 40 | { 41 | return $this->from; 42 | } 43 | 44 | public function getTo(): string 45 | { 46 | return $this->to; 47 | } 48 | 49 | /** 50 | * @return Chunk[] 51 | */ 52 | public function getChunks(): array 53 | { 54 | return $this->chunks; 55 | } 56 | 57 | /** 58 | * @param Chunk[] $chunks 59 | */ 60 | public function setChunks(array $chunks): void 61 | { 62 | $this->chunks = $chunks; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Differ.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Localheinz\Diff; 11 | 12 | use const PHP_INT_SIZE; 13 | use const PREG_SPLIT_DELIM_CAPTURE; 14 | use const PREG_SPLIT_NO_EMPTY; 15 | use function array_shift; 16 | use function array_unshift; 17 | use function array_values; 18 | use function count; 19 | use function current; 20 | use function end; 21 | use function get_class; 22 | use function gettype; 23 | use function is_array; 24 | use function is_object; 25 | use function is_string; 26 | use function key; 27 | use function min; 28 | use function preg_split; 29 | use function prev; 30 | use function reset; 31 | use function sprintf; 32 | use function substr; 33 | use Localheinz\Diff\Output\DiffOutputBuilderInterface; 34 | use Localheinz\Diff\Output\UnifiedDiffOutputBuilder; 35 | 36 | final class Differ 37 | { 38 | public const OLD = 0; 39 | 40 | public const ADDED = 1; 41 | 42 | public const REMOVED = 2; 43 | 44 | public const DIFF_LINE_END_WARNING = 3; 45 | 46 | public const NO_LINE_END_EOF_WARNING = 4; 47 | 48 | /** 49 | * @var DiffOutputBuilderInterface 50 | */ 51 | private $outputBuilder; 52 | 53 | /** 54 | * @param DiffOutputBuilderInterface $outputBuilder 55 | * 56 | * @throws InvalidArgumentException 57 | */ 58 | public function __construct($outputBuilder = null) 59 | { 60 | if ($outputBuilder instanceof DiffOutputBuilderInterface) { 61 | $this->outputBuilder = $outputBuilder; 62 | } elseif (null === $outputBuilder) { 63 | $this->outputBuilder = new UnifiedDiffOutputBuilder; 64 | } elseif (is_string($outputBuilder)) { 65 | // PHPUnit 6.1.4, 6.2.0, 6.2.1, 6.2.2, and 6.2.3 support 66 | // @see https://github.com/sebastianbergmann/phpunit/issues/2734#issuecomment-314514056 67 | // @deprecated 68 | $this->outputBuilder = new UnifiedDiffOutputBuilder($outputBuilder); 69 | } else { 70 | throw new InvalidArgumentException( 71 | sprintf( 72 | 'Expected builder to be an instance of DiffOutputBuilderInterface, or a string, got %s.', 73 | is_object($outputBuilder) ? 'instance of "' . get_class($outputBuilder) . '"' : gettype($outputBuilder) . ' "' . $outputBuilder . '"' 74 | ) 75 | ); 76 | } 77 | } 78 | 79 | /** 80 | * Returns the diff between two arrays or strings as string. 81 | * 82 | * @param array|string $from 83 | * @param array|string $to 84 | */ 85 | public function diff($from, $to, ?LongestCommonSubsequenceCalculator $lcs = null): string 86 | { 87 | $diff = $this->diffToArray( 88 | $this->normalizeDiffInput($from), 89 | $this->normalizeDiffInput($to), 90 | $lcs 91 | ); 92 | 93 | return $this->outputBuilder->getDiff($diff); 94 | } 95 | 96 | /** 97 | * Returns the diff between two arrays or strings as array. 98 | * 99 | * Each array element contains two elements: 100 | * - [0] => mixed $token 101 | * - [1] => 2|1|0 102 | * 103 | * - 2: REMOVED: $token was removed from $from 104 | * - 1: ADDED: $token was added to $from 105 | * - 0: OLD: $token is not changed in $to 106 | * 107 | * @param array|string $from 108 | * @param array|string $to 109 | * @param ?LongestCommonSubsequenceCalculator $lcs 110 | */ 111 | public function diffToArray($from, $to, ?LongestCommonSubsequenceCalculator $lcs = null): array 112 | { 113 | if (is_string($from)) { 114 | $from = $this->splitStringByLines($from); 115 | } elseif (!is_array($from)) { 116 | throw new InvalidArgumentException('"from" must be an array or string.'); 117 | } 118 | 119 | if (is_string($to)) { 120 | $to = $this->splitStringByLines($to); 121 | } elseif (!is_array($to)) { 122 | throw new InvalidArgumentException('"to" must be an array or string.'); 123 | } 124 | 125 | [$from, $to, $start, $end] = self::getArrayDiffParted($from, $to); 126 | 127 | if ($lcs === null) { 128 | $lcs = $this->selectLcsImplementation($from, $to); 129 | } 130 | 131 | $common = $lcs->calculate(array_values($from), array_values($to)); 132 | $diff = []; 133 | 134 | foreach ($start as $token) { 135 | $diff[] = [$token, self::OLD]; 136 | } 137 | 138 | reset($from); 139 | reset($to); 140 | 141 | foreach ($common as $token) { 142 | while (($fromToken = reset($from)) !== $token) { 143 | $diff[] = [array_shift($from), self::REMOVED]; 144 | } 145 | 146 | while (($toToken = reset($to)) !== $token) { 147 | $diff[] = [array_shift($to), self::ADDED]; 148 | } 149 | 150 | $diff[] = [$token, self::OLD]; 151 | 152 | array_shift($from); 153 | array_shift($to); 154 | } 155 | 156 | while (($token = array_shift($from)) !== null) { 157 | $diff[] = [$token, self::REMOVED]; 158 | } 159 | 160 | while (($token = array_shift($to)) !== null) { 161 | $diff[] = [$token, self::ADDED]; 162 | } 163 | 164 | foreach ($end as $token) { 165 | $diff[] = [$token, self::OLD]; 166 | } 167 | 168 | if ($this->detectUnmatchedLineEndings($diff)) { 169 | array_unshift($diff, ["#Warning: Strings contain different line endings!\n", self::DIFF_LINE_END_WARNING]); 170 | } 171 | 172 | return $diff; 173 | } 174 | 175 | /** 176 | * Casts variable to string if it is not a string or array. 177 | * 178 | * @return array|string 179 | */ 180 | private function normalizeDiffInput($input) 181 | { 182 | if (!is_array($input) && !is_string($input)) { 183 | return (string) $input; 184 | } 185 | 186 | return $input; 187 | } 188 | 189 | /** 190 | * Checks if input is string, if so it will split it line-by-line. 191 | */ 192 | private function splitStringByLines(string $input): array 193 | { 194 | return preg_split('/(.*\R)/', $input, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); 195 | } 196 | 197 | private function selectLcsImplementation(array $from, array $to): LongestCommonSubsequenceCalculator 198 | { 199 | // We do not want to use the time-efficient implementation if its memory 200 | // footprint will probably exceed this value. Note that the footprint 201 | // calculation is only an estimation for the matrix and the LCS method 202 | // will typically allocate a bit more memory than this. 203 | $memoryLimit = 100 * 1024 * 1024; 204 | 205 | if ($this->calculateEstimatedFootprint($from, $to) > $memoryLimit) { 206 | return new MemoryEfficientLongestCommonSubsequenceCalculator; 207 | } 208 | 209 | return new TimeEfficientLongestCommonSubsequenceCalculator; 210 | } 211 | 212 | /** 213 | * Calculates the estimated memory footprint for the DP-based method. 214 | * 215 | * @return float|int 216 | */ 217 | private function calculateEstimatedFootprint(array $from, array $to) 218 | { 219 | $itemSize = PHP_INT_SIZE === 4 ? 76 : 144; 220 | 221 | return $itemSize * min(count($from), count($to)) ** 2; 222 | } 223 | 224 | /** 225 | * Returns true if line ends don't match in a diff. 226 | */ 227 | private function detectUnmatchedLineEndings(array $diff): bool 228 | { 229 | $newLineBreaks = ['' => true]; 230 | $oldLineBreaks = ['' => true]; 231 | 232 | foreach ($diff as $entry) { 233 | if (self::OLD === $entry[1]) { 234 | $ln = $this->getLinebreak($entry[0]); 235 | $oldLineBreaks[$ln] = true; 236 | $newLineBreaks[$ln] = true; 237 | } elseif (self::ADDED === $entry[1]) { 238 | $newLineBreaks[$this->getLinebreak($entry[0])] = true; 239 | } elseif (self::REMOVED === $entry[1]) { 240 | $oldLineBreaks[$this->getLinebreak($entry[0])] = true; 241 | } 242 | } 243 | 244 | // if either input or output is a single line without breaks than no warning should be raised 245 | if (['' => true] === $newLineBreaks || ['' => true] === $oldLineBreaks) { 246 | return false; 247 | } 248 | 249 | // two way compare 250 | foreach ($newLineBreaks as $break => $set) { 251 | if (!isset($oldLineBreaks[$break])) { 252 | return true; 253 | } 254 | } 255 | 256 | foreach ($oldLineBreaks as $break => $set) { 257 | if (!isset($newLineBreaks[$break])) { 258 | return true; 259 | } 260 | } 261 | 262 | return false; 263 | } 264 | 265 | private function getLinebreak($line): string 266 | { 267 | if (!is_string($line)) { 268 | return ''; 269 | } 270 | 271 | $lc = substr($line, -1); 272 | 273 | if ("\r" === $lc) { 274 | return "\r"; 275 | } 276 | 277 | if ("\n" !== $lc) { 278 | return ''; 279 | } 280 | 281 | if ("\r\n" === substr($line, -2)) { 282 | return "\r\n"; 283 | } 284 | 285 | return "\n"; 286 | } 287 | 288 | private static function getArrayDiffParted(array &$from, array &$to): array 289 | { 290 | $start = []; 291 | $end = []; 292 | 293 | reset($to); 294 | 295 | foreach ($from as $k => $v) { 296 | $toK = key($to); 297 | 298 | if ($toK === $k && $v === $to[$k]) { 299 | $start[$k] = $v; 300 | 301 | unset($from[$k], $to[$k]); 302 | } else { 303 | break; 304 | } 305 | } 306 | 307 | end($from); 308 | end($to); 309 | 310 | do { 311 | $fromK = key($from); 312 | $toK = key($to); 313 | 314 | if (null === $fromK || null === $toK || current($from) !== current($to)) { 315 | break; 316 | } 317 | 318 | prev($from); 319 | prev($to); 320 | 321 | $end = [$fromK => $from[$fromK]] + $end; 322 | unset($from[$fromK], $to[$toK]); 323 | } while (true); 324 | 325 | return [$from, $to, $start, $end]; 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /src/Exception/ConfigurationException.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Localheinz\Diff; 11 | 12 | use function get_class; 13 | use function gettype; 14 | use function is_object; 15 | use function sprintf; 16 | use Exception; 17 | 18 | final class ConfigurationException extends InvalidArgumentException 19 | { 20 | public function __construct( 21 | string $option, 22 | string $expected, 23 | $value, 24 | int $code = 0, 25 | Exception $previous = null 26 | ) { 27 | parent::__construct( 28 | sprintf( 29 | 'Option "%s" must be %s, got "%s".', 30 | $option, 31 | $expected, 32 | is_object($value) ? get_class($value) : (null === $value ? '' : gettype($value) . '#' . $value) 33 | ), 34 | $code, 35 | $previous 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Exception/Exception.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Localheinz\Diff; 11 | 12 | interface Exception 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Localheinz\Diff; 11 | 12 | class InvalidArgumentException extends \InvalidArgumentException implements Exception 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Line.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Localheinz\Diff; 11 | 12 | final class Line 13 | { 14 | public const ADDED = 1; 15 | 16 | public const REMOVED = 2; 17 | 18 | public const UNCHANGED = 3; 19 | 20 | /** 21 | * @var int 22 | */ 23 | private $type; 24 | 25 | /** 26 | * @var string 27 | */ 28 | private $content; 29 | 30 | public function __construct(int $type = self::UNCHANGED, string $content = '') 31 | { 32 | $this->type = $type; 33 | $this->content = $content; 34 | } 35 | 36 | public function getContent(): string 37 | { 38 | return $this->content; 39 | } 40 | 41 | public function getType(): int 42 | { 43 | return $this->type; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/LongestCommonSubsequenceCalculator.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Localheinz\Diff; 11 | 12 | interface LongestCommonSubsequenceCalculator 13 | { 14 | /** 15 | * Calculates the longest common subsequence of two arrays. 16 | */ 17 | public function calculate(array $from, array $to): array; 18 | } 19 | -------------------------------------------------------------------------------- /src/MemoryEfficientLongestCommonSubsequenceCalculator.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Localheinz\Diff; 11 | 12 | use function array_fill; 13 | use function array_merge; 14 | use function array_reverse; 15 | use function array_slice; 16 | use function count; 17 | use function in_array; 18 | use function max; 19 | 20 | final class MemoryEfficientLongestCommonSubsequenceCalculator implements LongestCommonSubsequenceCalculator 21 | { 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function calculate(array $from, array $to): array 26 | { 27 | $cFrom = count($from); 28 | $cTo = count($to); 29 | 30 | if ($cFrom === 0) { 31 | return []; 32 | } 33 | 34 | if ($cFrom === 1) { 35 | if (in_array($from[0], $to, true)) { 36 | return [$from[0]]; 37 | } 38 | 39 | return []; 40 | } 41 | 42 | $i = (int) ($cFrom / 2); 43 | $fromStart = array_slice($from, 0, $i); 44 | $fromEnd = array_slice($from, $i); 45 | $llB = $this->length($fromStart, $to); 46 | $llE = $this->length(array_reverse($fromEnd), array_reverse($to)); 47 | $jMax = 0; 48 | $max = 0; 49 | 50 | for ($j = 0; $j <= $cTo; $j++) { 51 | $m = $llB[$j] + $llE[$cTo - $j]; 52 | 53 | if ($m >= $max) { 54 | $max = $m; 55 | $jMax = $j; 56 | } 57 | } 58 | 59 | $toStart = array_slice($to, 0, $jMax); 60 | $toEnd = array_slice($to, $jMax); 61 | 62 | return array_merge( 63 | $this->calculate($fromStart, $toStart), 64 | $this->calculate($fromEnd, $toEnd) 65 | ); 66 | } 67 | 68 | private function length(array $from, array $to): array 69 | { 70 | $current = array_fill(0, count($to) + 1, 0); 71 | $cFrom = count($from); 72 | $cTo = count($to); 73 | 74 | for ($i = 0; $i < $cFrom; $i++) { 75 | $prev = $current; 76 | 77 | for ($j = 0; $j < $cTo; $j++) { 78 | if ($from[$i] === $to[$j]) { 79 | $current[$j + 1] = $prev[$j] + 1; 80 | } else { 81 | $current[$j + 1] = max($current[$j], $prev[$j + 1]); 82 | } 83 | } 84 | } 85 | 86 | return $current; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Output/AbstractChunkOutputBuilder.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Localheinz\Diff\Output; 11 | 12 | use function count; 13 | 14 | abstract class AbstractChunkOutputBuilder implements DiffOutputBuilderInterface 15 | { 16 | /** 17 | * Takes input of the diff array and returns the common parts. 18 | * Iterates through diff line by line. 19 | */ 20 | protected function getCommonChunks(array $diff, int $lineThreshold = 5): array 21 | { 22 | $diffSize = count($diff); 23 | $capturing = false; 24 | $chunkStart = 0; 25 | $chunkSize = 0; 26 | $commonChunks = []; 27 | 28 | for ($i = 0; $i < $diffSize; ++$i) { 29 | if ($diff[$i][1] === 0 /* OLD */) { 30 | if ($capturing === false) { 31 | $capturing = true; 32 | $chunkStart = $i; 33 | $chunkSize = 0; 34 | } else { 35 | ++$chunkSize; 36 | } 37 | } elseif ($capturing !== false) { 38 | if ($chunkSize >= $lineThreshold) { 39 | $commonChunks[$chunkStart] = $chunkStart + $chunkSize; 40 | } 41 | 42 | $capturing = false; 43 | } 44 | } 45 | 46 | if ($capturing !== false && $chunkSize >= $lineThreshold) { 47 | $commonChunks[$chunkStart] = $chunkStart + $chunkSize; 48 | } 49 | 50 | return $commonChunks; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Output/DiffOnlyOutputBuilder.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Localheinz\Diff\Output; 11 | 12 | use function fclose; 13 | use function fopen; 14 | use function fwrite; 15 | use function stream_get_contents; 16 | use function substr; 17 | use Localheinz\Diff\Differ; 18 | 19 | /** 20 | * Builds a diff string representation in a loose unified diff format 21 | * listing only changes lines. Does not include line numbers. 22 | */ 23 | final class DiffOnlyOutputBuilder implements DiffOutputBuilderInterface 24 | { 25 | /** 26 | * @var string 27 | */ 28 | private $header; 29 | 30 | public function __construct(string $header = "--- Original\n+++ New\n") 31 | { 32 | $this->header = $header; 33 | } 34 | 35 | public function getDiff(array $diff): string 36 | { 37 | $buffer = fopen('php://memory', 'r+b'); 38 | 39 | if ('' !== $this->header) { 40 | fwrite($buffer, $this->header); 41 | 42 | if ("\n" !== substr($this->header, -1, 1)) { 43 | fwrite($buffer, "\n"); 44 | } 45 | } 46 | 47 | foreach ($diff as $diffEntry) { 48 | if ($diffEntry[1] === Differ::ADDED) { 49 | fwrite($buffer, '+' . $diffEntry[0]); 50 | } elseif ($diffEntry[1] === Differ::REMOVED) { 51 | fwrite($buffer, '-' . $diffEntry[0]); 52 | } elseif ($diffEntry[1] === Differ::DIFF_LINE_END_WARNING) { 53 | fwrite($buffer, ' ' . $diffEntry[0]); 54 | 55 | continue; // Warnings should not be tested for line break, it will always be there 56 | } else { /* Not changed (old) 0 */ 57 | continue; // we didn't write the non changs line, so do not add a line break either 58 | } 59 | 60 | $lc = substr($diffEntry[0], -1); 61 | 62 | if ($lc !== "\n" && $lc !== "\r") { 63 | fwrite($buffer, "\n"); // \No newline at end of file 64 | } 65 | } 66 | 67 | $diff = stream_get_contents($buffer, -1, 0); 68 | fclose($buffer); 69 | 70 | return $diff; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Output/DiffOutputBuilderInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Localheinz\Diff\Output; 11 | 12 | /** 13 | * Defines how an output builder should take a generated 14 | * diff array and return a string representation of that diff. 15 | */ 16 | interface DiffOutputBuilderInterface 17 | { 18 | public function getDiff(array $diff): string; 19 | } 20 | -------------------------------------------------------------------------------- /src/Output/StrictUnifiedDiffOutputBuilder.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Localheinz\Diff\Output; 11 | 12 | use function array_merge; 13 | use function array_splice; 14 | use function count; 15 | use function fclose; 16 | use function fopen; 17 | use function fwrite; 18 | use function is_bool; 19 | use function is_int; 20 | use function is_string; 21 | use function max; 22 | use function min; 23 | use function sprintf; 24 | use function stream_get_contents; 25 | use function substr; 26 | use Localheinz\Diff\ConfigurationException; 27 | use Localheinz\Diff\Differ; 28 | 29 | /** 30 | * Strict Unified diff output builder. 31 | * 32 | * Generates (strict) Unified diff's (unidiffs) with hunks. 33 | */ 34 | final class StrictUnifiedDiffOutputBuilder implements DiffOutputBuilderInterface 35 | { 36 | private static $default = [ 37 | 'collapseRanges' => true, // ranges of length one are rendered with the trailing `,1` 38 | 'commonLineThreshold' => 6, // number of same lines before ending a new hunk and creating a new one (if needed) 39 | 'contextLines' => 3, // like `diff: -u, -U NUM, --unified[=NUM]`, for patch/git apply compatibility best to keep at least @ 3 40 | 'fromFile' => null, 41 | 'fromFileDate' => null, 42 | 'toFile' => null, 43 | 'toFileDate' => null, 44 | ]; 45 | 46 | /** 47 | * @var bool 48 | */ 49 | private $changed; 50 | 51 | /** 52 | * @var bool 53 | */ 54 | private $collapseRanges; 55 | 56 | /** 57 | * @var int >= 0 58 | */ 59 | private $commonLineThreshold; 60 | 61 | /** 62 | * @var string 63 | */ 64 | private $header; 65 | 66 | /** 67 | * @var int >= 0 68 | */ 69 | private $contextLines; 70 | 71 | public function __construct(array $options = []) 72 | { 73 | $options = array_merge(self::$default, $options); 74 | 75 | if (!is_bool($options['collapseRanges'])) { 76 | throw new ConfigurationException('collapseRanges', 'a bool', $options['collapseRanges']); 77 | } 78 | 79 | if (!is_int($options['contextLines']) || $options['contextLines'] < 0) { 80 | throw new ConfigurationException('contextLines', 'an int >= 0', $options['contextLines']); 81 | } 82 | 83 | if (!is_int($options['commonLineThreshold']) || $options['commonLineThreshold'] <= 0) { 84 | throw new ConfigurationException('commonLineThreshold', 'an int > 0', $options['commonLineThreshold']); 85 | } 86 | 87 | $this->assertString($options, 'fromFile'); 88 | $this->assertString($options, 'toFile'); 89 | $this->assertStringOrNull($options, 'fromFileDate'); 90 | $this->assertStringOrNull($options, 'toFileDate'); 91 | 92 | $this->header = sprintf( 93 | "--- %s%s\n+++ %s%s\n", 94 | $options['fromFile'], 95 | null === $options['fromFileDate'] ? '' : "\t" . $options['fromFileDate'], 96 | $options['toFile'], 97 | null === $options['toFileDate'] ? '' : "\t" . $options['toFileDate'] 98 | ); 99 | 100 | $this->collapseRanges = $options['collapseRanges']; 101 | $this->commonLineThreshold = $options['commonLineThreshold']; 102 | $this->contextLines = $options['contextLines']; 103 | } 104 | 105 | public function getDiff(array $diff): string 106 | { 107 | if (0 === count($diff)) { 108 | return ''; 109 | } 110 | 111 | $this->changed = false; 112 | 113 | $buffer = fopen('php://memory', 'r+b'); 114 | fwrite($buffer, $this->header); 115 | 116 | $this->writeDiffHunks($buffer, $diff); 117 | 118 | if (!$this->changed) { 119 | fclose($buffer); 120 | 121 | return ''; 122 | } 123 | 124 | $diff = stream_get_contents($buffer, -1, 0); 125 | 126 | fclose($buffer); 127 | 128 | // If the last char is not a linebreak: add it. 129 | // This might happen when both the `from` and `to` do not have a trailing linebreak 130 | $last = substr($diff, -1); 131 | 132 | return "\n" !== $last && "\r" !== $last 133 | ? $diff . "\n" 134 | : $diff; 135 | } 136 | 137 | private function writeDiffHunks($output, array $diff): void 138 | { 139 | // detect "No newline at end of file" and insert into `$diff` if needed 140 | 141 | $upperLimit = count($diff); 142 | 143 | if (0 === $diff[$upperLimit - 1][1]) { 144 | $lc = substr($diff[$upperLimit - 1][0], -1); 145 | 146 | if ("\n" !== $lc) { 147 | array_splice($diff, $upperLimit, 0, [["\n\\ No newline at end of file\n", Differ::NO_LINE_END_EOF_WARNING]]); 148 | } 149 | } else { 150 | // search back for the last `+` and `-` line, 151 | // check if has trailing linebreak, else add under it warning under it 152 | $toFind = [1 => true, 2 => true]; 153 | 154 | for ($i = $upperLimit - 1; $i >= 0; --$i) { 155 | if (isset($toFind[$diff[$i][1]])) { 156 | unset($toFind[$diff[$i][1]]); 157 | $lc = substr($diff[$i][0], -1); 158 | 159 | if ("\n" !== $lc) { 160 | array_splice($diff, $i + 1, 0, [["\n\\ No newline at end of file\n", Differ::NO_LINE_END_EOF_WARNING]]); 161 | } 162 | 163 | if (!count($toFind)) { 164 | break; 165 | } 166 | } 167 | } 168 | } 169 | 170 | // write hunks to output buffer 171 | 172 | $cutOff = max($this->commonLineThreshold, $this->contextLines); 173 | $hunkCapture = false; 174 | $sameCount = $toRange = $fromRange = 0; 175 | $toStart = $fromStart = 1; 176 | $i = 0; 177 | 178 | /** @var int $i */ 179 | foreach ($diff as $i => $entry) { 180 | if (0 === $entry[1]) { // same 181 | if (false === $hunkCapture) { 182 | ++$fromStart; 183 | ++$toStart; 184 | 185 | continue; 186 | } 187 | 188 | ++$sameCount; 189 | ++$toRange; 190 | ++$fromRange; 191 | 192 | if ($sameCount === $cutOff) { 193 | $contextStartOffset = ($hunkCapture - $this->contextLines) < 0 194 | ? $hunkCapture 195 | : $this->contextLines; 196 | 197 | // note: $contextEndOffset = $this->contextLines; 198 | // 199 | // because we never go beyond the end of the diff. 200 | // with the cutoff/contextlines here the follow is never true; 201 | // 202 | // if ($i - $cutOff + $this->contextLines + 1 > \count($diff)) { 203 | // $contextEndOffset = count($diff) - 1; 204 | // } 205 | // 206 | // ; that would be true for a trailing incomplete hunk case which is dealt with after this loop 207 | 208 | $this->writeHunk( 209 | $diff, 210 | $hunkCapture - $contextStartOffset, 211 | $i - $cutOff + $this->contextLines + 1, 212 | $fromStart - $contextStartOffset, 213 | $fromRange - $cutOff + $contextStartOffset + $this->contextLines, 214 | $toStart - $contextStartOffset, 215 | $toRange - $cutOff + $contextStartOffset + $this->contextLines, 216 | $output 217 | ); 218 | 219 | $fromStart += $fromRange; 220 | $toStart += $toRange; 221 | 222 | $hunkCapture = false; 223 | $sameCount = $toRange = $fromRange = 0; 224 | } 225 | 226 | continue; 227 | } 228 | 229 | $sameCount = 0; 230 | 231 | if ($entry[1] === Differ::NO_LINE_END_EOF_WARNING) { 232 | continue; 233 | } 234 | 235 | $this->changed = true; 236 | 237 | if (false === $hunkCapture) { 238 | $hunkCapture = $i; 239 | } 240 | 241 | if (Differ::ADDED === $entry[1]) { // added 242 | ++$toRange; 243 | } 244 | 245 | if (Differ::REMOVED === $entry[1]) { // removed 246 | ++$fromRange; 247 | } 248 | } 249 | 250 | if (false === $hunkCapture) { 251 | return; 252 | } 253 | 254 | // we end here when cutoff (commonLineThreshold) was not reached, but we where capturing a hunk, 255 | // do not render hunk till end automatically because the number of context lines might be less than the commonLineThreshold 256 | 257 | $contextStartOffset = $hunkCapture - $this->contextLines < 0 258 | ? $hunkCapture 259 | : $this->contextLines; 260 | 261 | // prevent trying to write out more common lines than there are in the diff _and_ 262 | // do not write more than configured through the context lines 263 | $contextEndOffset = min($sameCount, $this->contextLines); 264 | 265 | $fromRange -= $sameCount; 266 | $toRange -= $sameCount; 267 | 268 | $this->writeHunk( 269 | $diff, 270 | $hunkCapture - $contextStartOffset, 271 | $i - $sameCount + $contextEndOffset + 1, 272 | $fromStart - $contextStartOffset, 273 | $fromRange + $contextStartOffset + $contextEndOffset, 274 | $toStart - $contextStartOffset, 275 | $toRange + $contextStartOffset + $contextEndOffset, 276 | $output 277 | ); 278 | } 279 | 280 | private function writeHunk( 281 | array $diff, 282 | int $diffStartIndex, 283 | int $diffEndIndex, 284 | int $fromStart, 285 | int $fromRange, 286 | int $toStart, 287 | int $toRange, 288 | $output 289 | ): void { 290 | fwrite($output, '@@ -' . $fromStart); 291 | 292 | if (!$this->collapseRanges || 1 !== $fromRange) { 293 | fwrite($output, ',' . $fromRange); 294 | } 295 | 296 | fwrite($output, ' +' . $toStart); 297 | 298 | if (!$this->collapseRanges || 1 !== $toRange) { 299 | fwrite($output, ',' . $toRange); 300 | } 301 | 302 | fwrite($output, " @@\n"); 303 | 304 | for ($i = $diffStartIndex; $i < $diffEndIndex; ++$i) { 305 | if ($diff[$i][1] === Differ::ADDED) { 306 | $this->changed = true; 307 | fwrite($output, '+' . $diff[$i][0]); 308 | } elseif ($diff[$i][1] === Differ::REMOVED) { 309 | $this->changed = true; 310 | fwrite($output, '-' . $diff[$i][0]); 311 | } elseif ($diff[$i][1] === Differ::OLD) { 312 | fwrite($output, ' ' . $diff[$i][0]); 313 | } elseif ($diff[$i][1] === Differ::NO_LINE_END_EOF_WARNING) { 314 | $this->changed = true; 315 | fwrite($output, $diff[$i][0]); 316 | } 317 | //} elseif ($diff[$i][1] === Differ::DIFF_LINE_END_WARNING) { // custom comment inserted by PHPUnit/diff package 318 | // skip 319 | //} else { 320 | // unknown/invalid 321 | //} 322 | } 323 | } 324 | 325 | private function assertString(array $options, string $option): void 326 | { 327 | if (!is_string($options[$option])) { 328 | throw new ConfigurationException($option, 'a string', $options[$option]); 329 | } 330 | } 331 | 332 | private function assertStringOrNull(array $options, string $option): void 333 | { 334 | if (null !== $options[$option] && !is_string($options[$option])) { 335 | throw new ConfigurationException($option, 'a string or ', $options[$option]); 336 | } 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /src/Output/UnifiedDiffOutputBuilder.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Localheinz\Diff\Output; 11 | 12 | use function array_splice; 13 | use function count; 14 | use function fclose; 15 | use function fopen; 16 | use function fwrite; 17 | use function max; 18 | use function min; 19 | use function stream_get_contents; 20 | use function strlen; 21 | use function substr; 22 | use Localheinz\Diff\Differ; 23 | 24 | /** 25 | * Builds a diff string representation in unified diff format in chunks. 26 | */ 27 | final class UnifiedDiffOutputBuilder extends AbstractChunkOutputBuilder 28 | { 29 | /** 30 | * @var bool 31 | */ 32 | private $collapseRanges = true; 33 | 34 | /** 35 | * @var int >= 0 36 | */ 37 | private $commonLineThreshold = 6; 38 | 39 | /** 40 | * @var int >= 0 41 | */ 42 | private $contextLines = 3; 43 | 44 | /** 45 | * @var string 46 | */ 47 | private $header; 48 | 49 | /** 50 | * @var bool 51 | */ 52 | private $addLineNumbers; 53 | 54 | public function __construct(string $header = "--- Original\n+++ New\n", bool $addLineNumbers = false) 55 | { 56 | $this->header = $header; 57 | $this->addLineNumbers = $addLineNumbers; 58 | } 59 | 60 | public function getDiff(array $diff): string 61 | { 62 | $buffer = fopen('php://memory', 'r+b'); 63 | 64 | if ('' !== $this->header) { 65 | fwrite($buffer, $this->header); 66 | 67 | if ("\n" !== substr($this->header, -1, 1)) { 68 | fwrite($buffer, "\n"); 69 | } 70 | } 71 | 72 | if (0 !== count($diff)) { 73 | $this->writeDiffHunks($buffer, $diff); 74 | } 75 | 76 | $diff = stream_get_contents($buffer, -1, 0); 77 | 78 | fclose($buffer); 79 | 80 | // If the diff is non-empty and last char is not a linebreak: add it. 81 | // This might happen when both the `from` and `to` do not have a trailing linebreak 82 | $last = substr($diff, -1); 83 | 84 | return 0 !== strlen($diff) && "\n" !== $last && "\r" !== $last 85 | ? $diff . "\n" 86 | : $diff; 87 | } 88 | 89 | private function writeDiffHunks($output, array $diff): void 90 | { 91 | // detect "No newline at end of file" and insert into `$diff` if needed 92 | 93 | $upperLimit = count($diff); 94 | 95 | if (0 === $diff[$upperLimit - 1][1]) { 96 | $lc = substr($diff[$upperLimit - 1][0], -1); 97 | 98 | if ("\n" !== $lc) { 99 | array_splice($diff, $upperLimit, 0, [["\n\\ No newline at end of file\n", Differ::NO_LINE_END_EOF_WARNING]]); 100 | } 101 | } else { 102 | // search back for the last `+` and `-` line, 103 | // check if has trailing linebreak, else add under it warning under it 104 | $toFind = [1 => true, 2 => true]; 105 | 106 | for ($i = $upperLimit - 1; $i >= 0; --$i) { 107 | if (isset($toFind[$diff[$i][1]])) { 108 | unset($toFind[$diff[$i][1]]); 109 | $lc = substr($diff[$i][0], -1); 110 | 111 | if ("\n" !== $lc) { 112 | array_splice($diff, $i + 1, 0, [["\n\\ No newline at end of file\n", Differ::NO_LINE_END_EOF_WARNING]]); 113 | } 114 | 115 | if (!count($toFind)) { 116 | break; 117 | } 118 | } 119 | } 120 | } 121 | 122 | // write hunks to output buffer 123 | 124 | $cutOff = max($this->commonLineThreshold, $this->contextLines); 125 | $hunkCapture = false; 126 | $sameCount = $toRange = $fromRange = 0; 127 | $toStart = $fromStart = 1; 128 | $i = 0; 129 | 130 | /** @var int $i */ 131 | foreach ($diff as $i => $entry) { 132 | if (0 === $entry[1]) { // same 133 | if (false === $hunkCapture) { 134 | ++$fromStart; 135 | ++$toStart; 136 | 137 | continue; 138 | } 139 | 140 | ++$sameCount; 141 | ++$toRange; 142 | ++$fromRange; 143 | 144 | if ($sameCount === $cutOff) { 145 | $contextStartOffset = ($hunkCapture - $this->contextLines) < 0 146 | ? $hunkCapture 147 | : $this->contextLines; 148 | 149 | // note: $contextEndOffset = $this->contextLines; 150 | // 151 | // because we never go beyond the end of the diff. 152 | // with the cutoff/contextlines here the follow is never true; 153 | // 154 | // if ($i - $cutOff + $this->contextLines + 1 > \count($diff)) { 155 | // $contextEndOffset = count($diff) - 1; 156 | // } 157 | // 158 | // ; that would be true for a trailing incomplete hunk case which is dealt with after this loop 159 | 160 | $this->writeHunk( 161 | $diff, 162 | $hunkCapture - $contextStartOffset, 163 | $i - $cutOff + $this->contextLines + 1, 164 | $fromStart - $contextStartOffset, 165 | $fromRange - $cutOff + $contextStartOffset + $this->contextLines, 166 | $toStart - $contextStartOffset, 167 | $toRange - $cutOff + $contextStartOffset + $this->contextLines, 168 | $output 169 | ); 170 | 171 | $fromStart += $fromRange; 172 | $toStart += $toRange; 173 | 174 | $hunkCapture = false; 175 | $sameCount = $toRange = $fromRange = 0; 176 | } 177 | 178 | continue; 179 | } 180 | 181 | $sameCount = 0; 182 | 183 | if ($entry[1] === Differ::NO_LINE_END_EOF_WARNING) { 184 | continue; 185 | } 186 | 187 | if (false === $hunkCapture) { 188 | $hunkCapture = $i; 189 | } 190 | 191 | if (Differ::ADDED === $entry[1]) { 192 | ++$toRange; 193 | } 194 | 195 | if (Differ::REMOVED === $entry[1]) { 196 | ++$fromRange; 197 | } 198 | } 199 | 200 | if (false === $hunkCapture) { 201 | return; 202 | } 203 | 204 | // we end here when cutoff (commonLineThreshold) was not reached, but we where capturing a hunk, 205 | // do not render hunk till end automatically because the number of context lines might be less than the commonLineThreshold 206 | 207 | $contextStartOffset = $hunkCapture - $this->contextLines < 0 208 | ? $hunkCapture 209 | : $this->contextLines; 210 | 211 | // prevent trying to write out more common lines than there are in the diff _and_ 212 | // do not write more than configured through the context lines 213 | $contextEndOffset = min($sameCount, $this->contextLines); 214 | 215 | $fromRange -= $sameCount; 216 | $toRange -= $sameCount; 217 | 218 | $this->writeHunk( 219 | $diff, 220 | $hunkCapture - $contextStartOffset, 221 | $i - $sameCount + $contextEndOffset + 1, 222 | $fromStart - $contextStartOffset, 223 | $fromRange + $contextStartOffset + $contextEndOffset, 224 | $toStart - $contextStartOffset, 225 | $toRange + $contextStartOffset + $contextEndOffset, 226 | $output 227 | ); 228 | } 229 | 230 | private function writeHunk( 231 | array $diff, 232 | int $diffStartIndex, 233 | int $diffEndIndex, 234 | int $fromStart, 235 | int $fromRange, 236 | int $toStart, 237 | int $toRange, 238 | $output 239 | ): void { 240 | if ($this->addLineNumbers) { 241 | fwrite($output, '@@ -' . $fromStart); 242 | 243 | if (!$this->collapseRanges || 1 !== $fromRange) { 244 | fwrite($output, ',' . $fromRange); 245 | } 246 | 247 | fwrite($output, ' +' . $toStart); 248 | 249 | if (!$this->collapseRanges || 1 !== $toRange) { 250 | fwrite($output, ',' . $toRange); 251 | } 252 | 253 | fwrite($output, " @@\n"); 254 | } else { 255 | fwrite($output, "@@ @@\n"); 256 | } 257 | 258 | for ($i = $diffStartIndex; $i < $diffEndIndex; ++$i) { 259 | if ($diff[$i][1] === Differ::ADDED) { 260 | fwrite($output, '+' . $diff[$i][0]); 261 | } elseif ($diff[$i][1] === Differ::REMOVED) { 262 | fwrite($output, '-' . $diff[$i][0]); 263 | } elseif ($diff[$i][1] === Differ::OLD) { 264 | fwrite($output, ' ' . $diff[$i][0]); 265 | } elseif ($diff[$i][1] === Differ::NO_LINE_END_EOF_WARNING) { 266 | fwrite($output, "\n"); // $diff[$i][0] 267 | } else { /* Not changed (old) Differ::OLD or Warning Differ::DIFF_LINE_END_WARNING */ 268 | fwrite($output, ' ' . $diff[$i][0]); 269 | } 270 | } 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/Parser.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Localheinz\Diff; 11 | 12 | use function array_pop; 13 | use function count; 14 | use function max; 15 | use function preg_match; 16 | use function preg_split; 17 | 18 | /** 19 | * Unified diff parser. 20 | */ 21 | final class Parser 22 | { 23 | /** 24 | * @return Diff[] 25 | */ 26 | public function parse(string $string): array 27 | { 28 | $lines = preg_split('(\r\n|\r|\n)', $string); 29 | 30 | if (!empty($lines) && $lines[count($lines) - 1] === '') { 31 | array_pop($lines); 32 | } 33 | 34 | $lineCount = count($lines); 35 | $diffs = []; 36 | $diff = null; 37 | $collected = []; 38 | 39 | for ($i = 0; $i < $lineCount; ++$i) { 40 | if (preg_match('(^---\\s+(?P\\S+))', $lines[$i], $fromMatch) && 41 | preg_match('(^\\+\\+\\+\\s+(?P\\S+))', $lines[$i + 1], $toMatch)) { 42 | if ($diff !== null) { 43 | $this->parseFileDiff($diff, $collected); 44 | 45 | $diffs[] = $diff; 46 | $collected = []; 47 | } 48 | 49 | $diff = new Diff($fromMatch['file'], $toMatch['file']); 50 | 51 | ++$i; 52 | } else { 53 | if (preg_match('/^(?:diff --git |index [\da-f\.]+|[+-]{3} [ab])/', $lines[$i])) { 54 | continue; 55 | } 56 | 57 | $collected[] = $lines[$i]; 58 | } 59 | } 60 | 61 | if ($diff !== null && count($collected)) { 62 | $this->parseFileDiff($diff, $collected); 63 | 64 | $diffs[] = $diff; 65 | } 66 | 67 | return $diffs; 68 | } 69 | 70 | private function parseFileDiff(Diff $diff, array $lines): void 71 | { 72 | $chunks = []; 73 | $chunk = null; 74 | $diffLines = []; 75 | 76 | foreach ($lines as $line) { 77 | if (preg_match('/^@@\s+-(?P\d+)(?:,\s*(?P\d+))?\s+\+(?P\d+)(?:,\s*(?P\d+))?\s+@@/', $line, $match)) { 78 | $chunk = new Chunk( 79 | (int) $match['start'], 80 | isset($match['startrange']) ? max(1, (int) $match['startrange']) : 1, 81 | (int) $match['end'], 82 | isset($match['endrange']) ? max(1, (int) $match['endrange']) : 1 83 | ); 84 | 85 | $chunks[] = $chunk; 86 | $diffLines = []; 87 | 88 | continue; 89 | } 90 | 91 | if (preg_match('/^(?P[+ -])?(?P.*)/', $line, $match)) { 92 | $type = Line::UNCHANGED; 93 | 94 | if ($match['type'] === '+') { 95 | $type = Line::ADDED; 96 | } elseif ($match['type'] === '-') { 97 | $type = Line::REMOVED; 98 | } 99 | 100 | $diffLines[] = new Line($type, $match['line']); 101 | 102 | if (null !== $chunk) { 103 | $chunk->setLines($diffLines); 104 | } 105 | } 106 | } 107 | 108 | $diff->setChunks($chunks); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/TimeEfficientLongestCommonSubsequenceCalculator.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Localheinz\Diff; 11 | 12 | use function array_reverse; 13 | use function count; 14 | use function max; 15 | use SplFixedArray; 16 | 17 | final class TimeEfficientLongestCommonSubsequenceCalculator implements LongestCommonSubsequenceCalculator 18 | { 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function calculate(array $from, array $to): array 23 | { 24 | $common = []; 25 | $fromLength = count($from); 26 | $toLength = count($to); 27 | $width = $fromLength + 1; 28 | $matrix = new SplFixedArray($width * ($toLength + 1)); 29 | 30 | for ($i = 0; $i <= $fromLength; ++$i) { 31 | $matrix[$i] = 0; 32 | } 33 | 34 | for ($j = 0; $j <= $toLength; ++$j) { 35 | $matrix[$j * $width] = 0; 36 | } 37 | 38 | for ($i = 1; $i <= $fromLength; ++$i) { 39 | for ($j = 1; $j <= $toLength; ++$j) { 40 | $o = ($j * $width) + $i; 41 | $matrix[$o] = max( 42 | $matrix[$o - 1], 43 | $matrix[$o - $width], 44 | $from[$i - 1] === $to[$j - 1] ? $matrix[$o - $width - 1] + 1 : 0 45 | ); 46 | } 47 | } 48 | 49 | $i = $fromLength; 50 | $j = $toLength; 51 | 52 | while ($i > 0 && $j > 0) { 53 | if ($from[$i - 1] === $to[$j - 1]) { 54 | $common[] = $from[$i - 1]; 55 | --$i; 56 | --$j; 57 | } else { 58 | $o = ($j * $width) + $i; 59 | 60 | if ($matrix[$o - $width] > $matrix[$o - 1]) { 61 | --$j; 62 | } else { 63 | --$i; 64 | } 65 | } 66 | } 67 | 68 | return array_reverse($common); 69 | } 70 | } 71 | --------------------------------------------------------------------------------