관리-도구
편집 파일: diff.tar
.gitignore 0000604 00000000054 15224664711 0006537 0 ustar 00 /.idea /composer.lock /vendor /.php_cs.cache .php_cs 0000604 00000005636 15224664711 0006037 0 ustar 00 <?php $header = <<<'EOF' This file is part of sebastian/diff. (c) Sebastian Bergmann <sebastian@phpunit.de> For the full copyright and license information, please view the LICENSE file that was distributed with this source code. EOF; return PhpCsFixer\Config::create() ->setRiskyAllowed(true) ->setRules( [ 'array_syntax' => ['syntax' => 'long'], 'binary_operator_spaces' => [ 'align_double_arrow' => true, 'align_equals' => true ], 'blank_line_after_namespace' => true, 'blank_line_before_return' => true, 'braces' => true, 'cast_spaces' => true, 'concat_space' => ['spacing' => 'one'], 'elseif' => true, 'encoding' => true, 'full_opening_tag' => true, 'function_declaration' => true, 'header_comment' => ['header' => $header, 'separate' => 'none'], 'indentation_type' => true, 'line_ending' => true, 'lowercase_constants' => true, 'lowercase_keywords' => true, 'method_argument_space' => true, 'native_function_invocation' => true, 'no_alias_functions' => true, 'no_blank_lines_after_class_opening' => true, 'no_blank_lines_after_phpdoc' => true, 'no_closing_tag' => true, 'no_empty_phpdoc' => true, 'no_empty_statement' => true, 'no_extra_consecutive_blank_lines' => true, 'no_leading_namespace_whitespace' => true, 'no_singleline_whitespace_before_semicolons' => true, 'no_spaces_after_function_name' => true, 'no_spaces_inside_parenthesis' => true, 'no_trailing_comma_in_list_call' => true, 'no_trailing_whitespace' => true, 'no_unused_imports' => true, 'no_whitespace_in_blank_line' => true, 'phpdoc_align' => true, 'phpdoc_indent' => true, 'phpdoc_no_access' => true, 'phpdoc_no_empty_return' => true, 'phpdoc_no_package' => true, 'phpdoc_scalar' => true, 'phpdoc_separation' => true, 'phpdoc_to_comment' => true, 'phpdoc_trim' => true, 'phpdoc_types' => true, 'phpdoc_var_without_name' => true, 'self_accessor' => true, 'simplified_null_return' => true, 'single_blank_line_at_eof' => true, 'single_import_per_statement' => true, 'single_line_after_imports' => true, 'single_quote' => true, 'ternary_operator_spaces' => true, 'trim_array_spaces' => true, 'visibility_required' => true, ] ) ->setFinder( PhpCsFixer\Finder::create() ->files() ->in(__DIR__ . '/src') ->in(__DIR__ . '/tests') ->name('*.php') ); src/Parser.php 0000604 00000005670 15224664711 0007314 0 ustar 00 <?php /* * This file is part of sebastian/diff. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff; /** * Unified diff parser. */ class Parser { /** * @param string $string * * @return Diff[] */ public function parse($string) { $lines = \preg_split('(\r\n|\r|\n)', $string); if (!empty($lines) && $lines[\count($lines) - 1] == '') { \array_pop($lines); } $lineCount = \count($lines); $diffs = array(); $diff = null; $collected = array(); for ($i = 0; $i < $lineCount; ++$i) { if (\preg_match('(^---\\s+(?P<file>\\S+))', $lines[$i], $fromMatch) && \preg_match('(^\\+\\+\\+\\s+(?P<file>\\S+))', $lines[$i + 1], $toMatch)) { if ($diff !== null) { $this->parseFileDiff($diff, $collected); $diffs[] = $diff; $collected = array(); } $diff = new Diff($fromMatch['file'], $toMatch['file']); ++$i; } else { if (\preg_match('/^(?:diff --git |index [\da-f\.]+|[+-]{3} [ab])/', $lines[$i])) { continue; } $collected[] = $lines[$i]; } } if ($diff !== null && \count($collected)) { $this->parseFileDiff($diff, $collected); $diffs[] = $diff; } return $diffs; } /** * @param Diff $diff * @param array $lines */ private function parseFileDiff(Diff $diff, array $lines) { $chunks = array(); $chunk = null; foreach ($lines as $line) { if (\preg_match('/^@@\s+-(?P<start>\d+)(?:,\s*(?P<startrange>\d+))?\s+\+(?P<end>\d+)(?:,\s*(?P<endrange>\d+))?\s+@@/', $line, $match)) { $chunk = new Chunk( $match['start'], isset($match['startrange']) ? \max(1, $match['startrange']) : 1, $match['end'], isset($match['endrange']) ? \max(1, $match['endrange']) : 1 ); $chunks[] = $chunk; $diffLines = array(); continue; } if (\preg_match('/^(?P<type>[+ -])?(?P<line>.*)/', $line, $match)) { $type = Line::UNCHANGED; if ($match['type'] === '+') { $type = Line::ADDED; } elseif ($match['type'] === '-') { $type = Line::REMOVED; } $diffLines[] = new Line($type, $match['line']); if (null !== $chunk) { $chunk->setLines($diffLines); } } } $diff->setChunks($chunks); } } src/Differ.php 0000604 00000024430 15224664711 0007252 0 ustar 00 <?php /* * This file is part of sebastian/diff. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff; use SebastianBergmann\Diff\LCS\LongestCommonSubsequence; use SebastianBergmann\Diff\LCS\TimeEfficientImplementation; use SebastianBergmann\Diff\LCS\MemoryEfficientImplementation; /** * Diff implementation. */ class Differ { /** * @var string */ private $header; /** * @var bool */ private $showNonDiffLines; /** * @param string $header * @param bool $showNonDiffLines */ public function __construct($header = "--- Original\n+++ New\n", $showNonDiffLines = true) { $this->header = $header; $this->showNonDiffLines = $showNonDiffLines; } /** * Returns the diff between two arrays or strings as string. * * @param array|string $from * @param array|string $to * @param LongestCommonSubsequence $lcs * * @return string */ public function diff($from, $to, LongestCommonSubsequence $lcs = null) { $from = $this->validateDiffInput($from); $to = $this->validateDiffInput($to); $diff = $this->diffToArray($from, $to, $lcs); $old = $this->checkIfDiffInOld($diff); $start = isset($old[0]) ? $old[0] : 0; $end = \count($diff); if ($tmp = \array_search($end, $old)) { $end = $tmp; } return $this->getBuffer($diff, $old, $start, $end); } /** * Casts variable to string if it is not a string or array. * * @param mixed $input * * @return string */ private function validateDiffInput($input) { if (!\is_array($input) && !\is_string($input)) { return (string) $input; } return $input; } /** * Takes input of the diff array and returns the old array. * Iterates through diff line by line, * * @param array $diff * * @return array */ private function checkIfDiffInOld(array $diff) { $inOld = false; $i = 0; $old = array(); foreach ($diff as $line) { if ($line[1] === 0 /* OLD */) { if ($inOld === false) { $inOld = $i; } } elseif ($inOld !== false) { if (($i - $inOld) > 5) { $old[$inOld] = $i - 1; } $inOld = false; } ++$i; } return $old; } /** * Generates buffer in string format, returning the patch. * * @param array $diff * @param array $old * @param int $start * @param int $end * * @return string */ private function getBuffer(array $diff, array $old, $start, $end) { $buffer = $this->header; if (!isset($old[$start])) { $buffer = $this->getDiffBufferElementNew($diff, $buffer, $start); ++$start; } for ($i = $start; $i < $end; $i++) { if (isset($old[$i])) { $i = $old[$i]; $buffer = $this->getDiffBufferElementNew($diff, $buffer, $i); } else { $buffer = $this->getDiffBufferElement($diff, $buffer, $i); } } return $buffer; } /** * Gets individual buffer element. * * @param array $diff * @param string $buffer * @param int $diffIndex * * @return string */ private function getDiffBufferElement(array $diff, $buffer, $diffIndex) { if ($diff[$diffIndex][1] === 1 /* ADDED */) { $buffer .= '+' . $diff[$diffIndex][0] . "\n"; } elseif ($diff[$diffIndex][1] === 2 /* REMOVED */) { $buffer .= '-' . $diff[$diffIndex][0] . "\n"; } elseif ($this->showNonDiffLines === true) { $buffer .= ' ' . $diff[$diffIndex][0] . "\n"; } return $buffer; } /** * Gets individual buffer element with opening. * * @param array $diff * @param string $buffer * @param int $diffIndex * * @return string */ private function getDiffBufferElementNew(array $diff, $buffer, $diffIndex) { if ($this->showNonDiffLines === true) { $buffer .= "@@ @@\n"; } return $this->getDiffBufferElement($diff, $buffer, $diffIndex); } /** * Returns the diff between two arrays or strings as array. * * Each array element contains two elements: * - [0] => mixed $token * - [1] => 2|1|0 * * - 2: REMOVED: $token was removed from $from * - 1: ADDED: $token was added to $from * - 0: OLD: $token is not changed in $to * * @param array|string $from * @param array|string $to * @param LongestCommonSubsequence $lcs * * @return array */ public function diffToArray($from, $to, LongestCommonSubsequence $lcs = null) { if (\is_string($from)) { $fromMatches = $this->getNewLineMatches($from); $from = $this->splitStringByLines($from); } elseif (\is_array($from)) { $fromMatches = array(); } else { throw new \InvalidArgumentException('"from" must be an array or string.'); } if (\is_string($to)) { $toMatches = $this->getNewLineMatches($to); $to = $this->splitStringByLines($to); } elseif (\is_array($to)) { $toMatches = array(); } else { throw new \InvalidArgumentException('"to" must be an array or string.'); } list($from, $to, $start, $end) = self::getArrayDiffParted($from, $to); if ($lcs === null) { $lcs = $this->selectLcsImplementation($from, $to); } $common = $lcs->calculate(\array_values($from), \array_values($to)); $diff = array(); if ($this->detectUnmatchedLineEndings($fromMatches, $toMatches)) { $diff[] = array( '#Warning: Strings contain different line endings!', 0 ); } foreach ($start as $token) { $diff[] = array($token, 0 /* OLD */); } \reset($from); \reset($to); foreach ($common as $token) { while (($fromToken = \reset($from)) !== $token) { $diff[] = array(\array_shift($from), 2 /* REMOVED */); } while (($toToken = \reset($to)) !== $token) { $diff[] = array(\array_shift($to), 1 /* ADDED */); } $diff[] = array($token, 0 /* OLD */); \array_shift($from); \array_shift($to); } while (($token = \array_shift($from)) !== null) { $diff[] = array($token, 2 /* REMOVED */); } while (($token = \array_shift($to)) !== null) { $diff[] = array($token, 1 /* ADDED */); } foreach ($end as $token) { $diff[] = array($token, 0 /* OLD */); } return $diff; } /** * Get new strings denoting new lines from a given string. * * @param string $string * * @return array */ private function getNewLineMatches($string) { \preg_match_all('(\r\n|\r|\n)', $string, $stringMatches); return $stringMatches; } /** * Checks if input is string, if so it will split it line-by-line. * * @param string $input * * @return array */ private function splitStringByLines($input) { return \preg_split('(\r\n|\r|\n)', $input); } /** * @param array $from * @param array $to * * @return LongestCommonSubsequence */ private function selectLcsImplementation(array $from, array $to) { // We do not want to use the time-efficient implementation if its memory // footprint will probably exceed this value. Note that the footprint // calculation is only an estimation for the matrix and the LCS method // will typically allocate a bit more memory than this. $memoryLimit = 100 * 1024 * 1024; if ($this->calculateEstimatedFootprint($from, $to) > $memoryLimit) { return new MemoryEfficientImplementation; } return new TimeEfficientImplementation; } /** * Calculates the estimated memory footprint for the DP-based method. * * @param array $from * @param array $to * * @return int|float */ private function calculateEstimatedFootprint(array $from, array $to) { $itemSize = PHP_INT_SIZE === 4 ? 76 : 144; return $itemSize * \pow(\min(\count($from), \count($to)), 2); } /** * Returns true if line ends don't match on fromMatches and toMatches. * * @param array $fromMatches * @param array $toMatches * * @return bool */ private function detectUnmatchedLineEndings(array $fromMatches, array $toMatches) { return isset($fromMatches[0], $toMatches[0]) && \count($fromMatches[0]) === \count($toMatches[0]) && $fromMatches[0] !== $toMatches[0]; } /** * @param array $from * @param array $to * * @return array */ private static function getArrayDiffParted(array &$from, array &$to) { $start = array(); $end = array(); \reset($to); foreach ($from as $k => $v) { $toK = \key($to); if ($toK === $k && $v === $to[$k]) { $start[$k] = $v; unset($from[$k], $to[$k]); } else { break; } } \end($from); \end($to); do { $fromK = \key($from); $toK = \key($to); if (null === $fromK || null === $toK || \current($from) !== \current($to)) { break; } \prev($from); \prev($to); $end = array($fromK => $from[$fromK]) + $end; unset($from[$fromK], $to[$toK]); } while (true); return array($from, $to, $start, $end); } } src/Diff.php 0000604 00000002247 15224664711 0006725 0 ustar 00 <?php /* * This file is part of sebastian/diff. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff; class Diff { /** * @var string */ private $from; /** * @var string */ private $to; /** * @var Chunk[] */ private $chunks; /** * @param string $from * @param string $to * @param Chunk[] $chunks */ public function __construct($from, $to, array $chunks = array()) { $this->from = $from; $this->to = $to; $this->chunks = $chunks; } /** * @return string */ public function getFrom() { return $this->from; } /** * @return string */ public function getTo() { return $this->to; } /** * @return Chunk[] */ public function getChunks() { return $this->chunks; } /** * @param Chunk[] $chunks */ public function setChunks(array $chunks) { $this->chunks = $chunks; } } src/LCS/LongestCommonSubsequence.php 0000604 00000001152 15224664711 0013457 0 ustar 00 <?php /* * This file is part of sebastian/diff. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff\LCS; /** * Interface for implementations of longest common subsequence calculation. */ interface LongestCommonSubsequence { /** * Calculates the longest common subsequence of two arrays. * * @param array $from * @param array $to * * @return array */ public function calculate(array $from, array $to); } src/LCS/MemoryEfficientLongestCommonSubsequenceImplementation.php 0000604 00000004522 15224664711 0021377 0 ustar 00 <?php /* * This file is part of sebastian/diff. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff\LCS; /** * Memory-efficient implementation of longest common subsequence calculation. */ class MemoryEfficientImplementation implements LongestCommonSubsequence { /** * Calculates the longest common subsequence of two arrays. * * @param array $from * @param array $to * * @return array */ public function calculate(array $from, array $to) { $cFrom = \count($from); $cTo = \count($to); if ($cFrom === 0) { return array(); } if ($cFrom === 1) { if (\in_array($from[0], $to, true)) { return array($from[0]); } return array(); } $i = (int) ($cFrom / 2); $fromStart = \array_slice($from, 0, $i); $fromEnd = \array_slice($from, $i); $llB = $this->length($fromStart, $to); $llE = $this->length(\array_reverse($fromEnd), \array_reverse($to)); $jMax = 0; $max = 0; for ($j = 0; $j <= $cTo; $j++) { $m = $llB[$j] + $llE[$cTo - $j]; if ($m >= $max) { $max = $m; $jMax = $j; } } $toStart = \array_slice($to, 0, $jMax); $toEnd = \array_slice($to, $jMax); return \array_merge( $this->calculate($fromStart, $toStart), $this->calculate($fromEnd, $toEnd) ); } /** * @param array $from * @param array $to * * @return array */ private function length(array $from, array $to) { $current = \array_fill(0, \count($to) + 1, 0); $cFrom = \count($from); $cTo = \count($to); for ($i = 0; $i < $cFrom; $i++) { $prev = $current; for ($j = 0; $j < $cTo; $j++) { if ($from[$i] === $to[$j]) { $current[$j + 1] = $prev[$j] + 1; } else { $current[$j + 1] = \max($current[$j], $prev[$j + 1]); } } } return $current; } } src/LCS/TimeEfficientLongestCommonSubsequenceImplementation.php 0000604 00000003645 15224664711 0021032 0 ustar 00 <?php /* * This file is part of sebastian/diff. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff\LCS; /** * Time-efficient implementation of longest common subsequence calculation. */ class TimeEfficientImplementation implements LongestCommonSubsequence { /** * Calculates the longest common subsequence of two arrays. * * @param array $from * @param array $to * * @return array */ public function calculate(array $from, array $to) { $common = array(); $fromLength = \count($from); $toLength = \count($to); $width = $fromLength + 1; $matrix = new \SplFixedArray($width * ($toLength + 1)); for ($i = 0; $i <= $fromLength; ++$i) { $matrix[$i] = 0; } for ($j = 0; $j <= $toLength; ++$j) { $matrix[$j * $width] = 0; } for ($i = 1; $i <= $fromLength; ++$i) { for ($j = 1; $j <= $toLength; ++$j) { $o = ($j * $width) + $i; $matrix[$o] = \max( $matrix[$o - 1], $matrix[$o - $width], $from[$i - 1] === $to[$j - 1] ? $matrix[$o - $width - 1] + 1 : 0 ); } } $i = $fromLength; $j = $toLength; while ($i > 0 && $j > 0) { if ($from[$i - 1] === $to[$j - 1]) { $common[] = $from[$i - 1]; --$i; --$j; } else { $o = ($j * $width) + $i; if ($matrix[$o - $width] > $matrix[$o - 1]) { --$j; } else { --$i; } } } return \array_reverse($common); } } src/Line.php 0000604 00000001625 15224664711 0006743 0 ustar 00 <?php /* * This file is part of sebastian/diff. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff; class Line { const ADDED = 1; const REMOVED = 2; const UNCHANGED = 3; /** * @var int */ private $type; /** * @var string */ private $content; /** * @param int $type * @param string $content */ public function __construct($type = self::UNCHANGED, $content = '') { $this->type = $type; $this->content = $content; } /** * @return string */ public function getContent() { return $this->content; } /** * @return int */ public function getType() { return $this->type; } } src/Chunk.php 0000604 00000003305 15224664711 0007121 0 ustar 00 <?php /* * This file is part of sebastian/diff. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff; class Chunk { /** * @var int */ private $start; /** * @var int */ private $startRange; /** * @var int */ private $end; /** * @var int */ private $endRange; /** * @var array */ private $lines; /** * @param int $start * @param int $startRange * @param int $end * @param int $endRange * @param array $lines */ public function __construct($start = 0, $startRange = 1, $end = 0, $endRange = 1, array $lines = array()) { $this->start = (int) $start; $this->startRange = (int) $startRange; $this->end = (int) $end; $this->endRange = (int) $endRange; $this->lines = $lines; } /** * @return int */ public function getStart() { return $this->start; } /** * @return int */ public function getStartRange() { return $this->startRange; } /** * @return int */ public function getEnd() { return $this->end; } /** * @return int */ public function getEndRange() { return $this->endRange; } /** * @return array */ public function getLines() { return $this->lines; } /** * @param array $lines */ public function setLines(array $lines) { $this->lines = $lines; } } phpunit.xml 0000604 00000001265 15224664711 0006765 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/6.0/phpunit.xsd" bootstrap="vendor/autoload.php" forceCoversAnnotation="true" beStrictAboutCoversAnnotation="true" beStrictAboutOutputDuringTests="true" beStrictAboutTodoAnnotatedTests="true" verbose="true"> <testsuite> <directory suffix="Test.php">tests</directory> </testsuite> <filter> <whitelist processUncoveredFilesFromWhitelist="true"> <directory suffix=".php">src</directory> </whitelist> </filter> </phpunit> LICENSE 0000604 00000003015 15224664711 0005554 0 ustar 00 sebastian/diff Copyright (c) 2002-2017, Sebastian Bergmann <sebastian@phpunit.de>. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Sebastian Bergmann nor the names of his contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. build.xml 0000604 00000001416 15224664711 0006373 0 ustar 00 <?xml version="1.0" encoding="UTF-8"?> <project name="diff" default="setup"> <target name="setup" depends="clean,composer"/> <target name="clean" description="Cleanup build artifacts"> <delete dir="${basedir}/vendor"/> <delete file="${basedir}/composer.lock"/> </target> <target name="composer" depends="clean" description="Install dependencies with Composer"> <exec executable="composer" taskname="composer"> <arg value="update"/> <arg value="--no-interaction"/> <arg value="--no-progress"/> <arg value="--no-ansi"/> <arg value="--no-suggest"/> <arg value="--optimize-autoloader"/> <arg value="--prefer-stable"/> </exec> </target> </project> composer.json 0000604 00000001335 15224664711 0007274 0 ustar 00 { "name": "sebastian/diff", "description": "Diff implementation", "keywords": ["diff"], "homepage": "https://github.com/sebastianbergmann/diff", "license": "BSD-3-Clause", "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" }, { "name": "Kore Nordmann", "email": "mail@kore-nordmann.de" } ], "require": { "php": "^5.3.3 || ^7.0" }, "require-dev": { "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" }, "autoload": { "classmap": [ "src/" ] }, "extra": { "branch-alias": { "dev-master": "1.4-dev" } } } README.md 0000604 00000012614 15224664711 0006033 0 ustar 00 # sebastian/diff Diff implementation for PHP, factored out of PHPUnit into a stand-alone component. ## Installation You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/): composer require sebastian/diff If you only need this library during development, for instance to run your project's test suite, then you should add it as a development-time dependency: composer require --dev sebastian/diff ### Usage The `Differ` class can be used to generate a textual representation of the difference between two strings: ```php use SebastianBergmann\Diff\Differ; $differ = new Differ; print $differ->diff('foo', 'bar'); ``` The code above yields the output below: --- Original +++ New @@ @@ -foo +bar The `Parser` class can be used to parse a unified diff into an object graph: ```php use SebastianBergmann\Diff\Parser; use SebastianBergmann\Git; $git = new Git('/usr/local/src/money'); $diff = $git->getDiff( '948a1a07768d8edd10dcefa8315c1cbeffb31833', 'c07a373d2399f3e686234c4f7f088d635eb9641b' ); $parser = new Parser; print_r($parser->parse($diff)); ``` The code above yields the output below: Array ( [0] => SebastianBergmann\Diff\Diff Object ( [from:SebastianBergmann\Diff\Diff:private] => a/tests/MoneyTest.php [to:SebastianBergmann\Diff\Diff:private] => b/tests/MoneyTest.php [chunks:SebastianBergmann\Diff\Diff:private] => Array ( [0] => SebastianBergmann\Diff\Chunk Object ( [start:SebastianBergmann\Diff\Chunk:private] => 87 [startRange:SebastianBergmann\Diff\Chunk:private] => 7 [end:SebastianBergmann\Diff\Chunk:private] => 87 [endRange:SebastianBergmann\Diff\Chunk:private] => 7 [lines:SebastianBergmann\Diff\Chunk:private] => Array ( [0] => SebastianBergmann\Diff\Line Object ( [type:SebastianBergmann\Diff\Line:private] => 3 [content:SebastianBergmann\Diff\Line:private] => * @covers SebastianBergmann\Money\Money::add ) [1] => SebastianBergmann\Diff\Line Object ( [type:SebastianBergmann\Diff\Line:private] => 3 [content:SebastianBergmann\Diff\Line:private] => * @covers SebastianBergmann\Money\Money::newMoney ) [2] => SebastianBergmann\Diff\Line Object ( [type:SebastianBergmann\Diff\Line:private] => 3 [content:SebastianBergmann\Diff\Line:private] => */ ) [3] => SebastianBergmann\Diff\Line Object ( [type:SebastianBergmann\Diff\Line:private] => 2 [content:SebastianBergmann\Diff\Line:private] => public function testAnotherMoneyWithSameCurrencyObjectCanBeAdded() ) [4] => SebastianBergmann\Diff\Line Object ( [type:SebastianBergmann\Diff\Line:private] => 1 [content:SebastianBergmann\Diff\Line:private] => public function testAnotherMoneyObjectWithSameCurrencyCanBeAdded() ) [5] => SebastianBergmann\Diff\Line Object ( [type:SebastianBergmann\Diff\Line:private] => 3 [content:SebastianBergmann\Diff\Line:private] => { ) [6] => SebastianBergmann\Diff\Line Object ( [type:SebastianBergmann\Diff\Line:private] => 3 [content:SebastianBergmann\Diff\Line:private] => $a = new Money(1, new Currency('EUR')); ) [7] => SebastianBergmann\Diff\Line Object ( [type:SebastianBergmann\Diff\Line:private] => 3 [content:SebastianBergmann\Diff\Line:private] => $b = new Money(2, new Currency('EUR')); ) ) ) ) ) ) .travis.yml 0000604 00000000743 15224664711 0006665 0 ustar 00 language: php php: - 5.3 - 5.4 - 5.5 - 5.6 - 7.0 - 7.0snapshot - 7.1 - 7.1snapshot - master sudo: false before_install: - composer self-update - composer clear-cache install: - travis_retry composer update --no-interaction --no-ansi --no-progress --no-suggest --optimize-autoloader --prefer-stable script: - ./vendor/bin/phpunit --coverage-clover=coverage.xml after_success: - bash <(curl -s https://codecov.io/bash) notifications: email: false tests/ParserTest.php 0000604 00000010214 15224664711 0010515 0 ustar 00 <?php /* * This file is part of sebastian/diff. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff; use PHPUnit\Framework\TestCase; /** * @covers SebastianBergmann\Diff\Parser * * @uses SebastianBergmann\Diff\Chunk * @uses SebastianBergmann\Diff\Diff * @uses SebastianBergmann\Diff\Line */ class ParserTest extends TestCase { /** * @var Parser */ private $parser; protected function setUp() { $this->parser = new Parser; } public function testParse() { $content = \file_get_contents(__DIR__ . '/fixtures/patch.txt'); $diffs = $this->parser->parse($content); $this->assertInternalType('array', $diffs); $this->assertContainsOnlyInstancesOf('SebastianBergmann\Diff\Diff', $diffs); $this->assertCount(1, $diffs); $chunks = $diffs[0]->getChunks(); $this->assertInternalType('array', $chunks); $this->assertContainsOnlyInstancesOf('SebastianBergmann\Diff\Chunk', $chunks); $this->assertCount(1, $chunks); $this->assertEquals(20, $chunks[0]->getStart()); $this->assertCount(4, $chunks[0]->getLines()); } public function testParseWithMultipleChunks() { $content = \file_get_contents(__DIR__ . '/fixtures/patch2.txt'); $diffs = $this->parser->parse($content); $this->assertCount(1, $diffs); $chunks = $diffs[0]->getChunks(); $this->assertCount(3, $chunks); $this->assertEquals(20, $chunks[0]->getStart()); $this->assertEquals(320, $chunks[1]->getStart()); $this->assertEquals(600, $chunks[2]->getStart()); $this->assertCount(5, $chunks[0]->getLines()); $this->assertCount(5, $chunks[1]->getLines()); $this->assertCount(4, $chunks[2]->getLines()); } public function testParseWithRemovedLines() { $content = <<<A diff --git a/Test.txt b/Test.txt index abcdefg..abcdefh 100644 --- a/Test.txt +++ b/Test.txt @@ -49,9 +49,8 @@ A -B A; $diffs = $this->parser->parse($content); $this->assertInternalType('array', $diffs); $this->assertContainsOnlyInstancesOf('SebastianBergmann\Diff\Diff', $diffs); $this->assertCount(1, $diffs); $chunks = $diffs[0]->getChunks(); $this->assertInternalType('array', $chunks); $this->assertContainsOnlyInstancesOf('SebastianBergmann\Diff\Chunk', $chunks); $this->assertCount(1, $chunks); $chunk = $chunks[0]; $this->assertSame(49, $chunk->getStart()); $this->assertSame(49, $chunk->getEnd()); $this->assertSame(9, $chunk->getStartRange()); $this->assertSame(8, $chunk->getEndRange()); $lines = $chunk->getLines(); $this->assertInternalType('array', $lines); $this->assertContainsOnlyInstancesOf('SebastianBergmann\Diff\Line', $lines); $this->assertCount(2, $lines); /** @var Line $line */ $line = $lines[0]; $this->assertSame('A', $line->getContent()); $this->assertSame(Line::UNCHANGED, $line->getType()); $line = $lines[1]; $this->assertSame('B', $line->getContent()); $this->assertSame(Line::REMOVED, $line->getType()); } public function testParseDiffForMulitpleFiles() { $content = <<<A diff --git a/Test.txt b/Test.txt index abcdefg..abcdefh 100644 --- a/Test.txt +++ b/Test.txt @@ -1,3 +1,2 @@ A -B diff --git a/Test123.txt b/Test123.txt index abcdefg..abcdefh 100644 --- a/Test2.txt +++ b/Test2.txt @@ -1,2 +1,3 @@ A +B A; $diffs = $this->parser->parse($content); $this->assertCount(2, $diffs); /** @var Diff $diff */ $diff = $diffs[0]; $this->assertSame('a/Test.txt', $diff->getFrom()); $this->assertSame('b/Test.txt', $diff->getTo()); $this->assertCount(1, $diff->getChunks()); $diff = $diffs[1]; $this->assertSame('a/Test2.txt', $diff->getFrom()); $this->assertSame('b/Test2.txt', $diff->getTo()); $this->assertCount(1, $diff->getChunks()); } } tests/LCS/LongestCommonSubsequenceTest.php 0000604 00000013241 15224664711 0014674 0 ustar 00 <?php /* * This file is part of sebastian/diff. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff\LCS; use PHPUnit\Framework\TestCase; abstract class LongestCommonSubsequenceTest extends TestCase { /** * @var LongestCommonSubsequence */ private $implementation; /** * @var string */ private $memoryLimit; /** * @var int[] */ private $stress_sizes = array(1, 2, 3, 100, 500, 1000, 2000); protected function setUp() { $this->memoryLimit = \ini_get('memory_limit'); \ini_set('memory_limit', '256M'); $this->implementation = $this->createImplementation(); } /** * @return LongestCommonSubsequence */ abstract protected function createImplementation(); protected function tearDown() { \ini_set('memory_limit', $this->memoryLimit); } public function testBothEmpty() { $from = array(); $to = array(); $common = $this->implementation->calculate($from, $to); $this->assertEquals(array(), $common); } public function testIsStrictComparison() { $from = array( false, 0, 0.0, '', null, array(), true, 1, 1.0, 'foo', array('foo', 'bar'), array('foo' => 'bar') ); $to = $from; $common = $this->implementation->calculate($from, $to); $this->assertEquals($from, $common); $to = array( false, false, false, false, false, false, true, true, true, true, true, true ); $expected = array( false, true, ); $common = $this->implementation->calculate($from, $to); $this->assertEquals($expected, $common); } public function testEqualSequences() { foreach ($this->stress_sizes as $size) { $range = \range(1, $size); $from = $range; $to = $range; $common = $this->implementation->calculate($from, $to); $this->assertEquals($range, $common); } } public function testDistinctSequences() { $from = array('A'); $to = array('B'); $common = $this->implementation->calculate($from, $to); $this->assertEquals(array(), $common); $from = array('A', 'B', 'C'); $to = array('D', 'E', 'F'); $common = $this->implementation->calculate($from, $to); $this->assertEquals(array(), $common); foreach ($this->stress_sizes as $size) { $from = \range(1, $size); $to = \range($size + 1, $size * 2); $common = $this->implementation->calculate($from, $to); $this->assertEquals(array(), $common); } } public function testCommonSubsequence() { $from = array('A', 'C', 'E', 'F', 'G'); $to = array('A', 'B', 'D', 'E', 'H'); $expected = array('A', 'E'); $common = $this->implementation->calculate($from, $to); $this->assertEquals($expected, $common); $from = array('A', 'C', 'E', 'F', 'G'); $to = array('B', 'C', 'D', 'E', 'F', 'H'); $expected = array('C', 'E', 'F'); $common = $this->implementation->calculate($from, $to); $this->assertEquals($expected, $common); foreach ($this->stress_sizes as $size) { $from = $size < 2 ? array(1) : \range(1, $size + 1, 2); $to = $size < 3 ? array(1) : \range(1, $size + 1, 3); $expected = $size < 6 ? array(1) : \range(1, $size + 1, 6); $common = $this->implementation->calculate($from, $to); $this->assertEquals($expected, $common); } } public function testSingleElementSubsequenceAtStart() { foreach ($this->stress_sizes as $size) { $from = \range(1, $size); $to = \array_slice($from, 0, 1); $common = $this->implementation->calculate($from, $to); $this->assertEquals($to, $common); } } public function testSingleElementSubsequenceAtMiddle() { foreach ($this->stress_sizes as $size) { $from = \range(1, $size); $to = \array_slice($from, (int) $size / 2, 1); $common = $this->implementation->calculate($from, $to); $this->assertEquals($to, $common); } } public function testSingleElementSubsequenceAtEnd() { foreach ($this->stress_sizes as $size) { $from = \range(1, $size); $to = \array_slice($from, $size - 1, 1); $common = $this->implementation->calculate($from, $to); $this->assertEquals($to, $common); } } public function testReversedSequences() { $from = array('A', 'B'); $to = array('B', 'A'); $expected = array('A'); $common = $this->implementation->calculate($from, $to); $this->assertEquals($expected, $common); foreach ($this->stress_sizes as $size) { $from = \range(1, $size); $to = \array_reverse($from); $common = $this->implementation->calculate($from, $to); $this->assertEquals(array(1), $common); } } public function testStrictTypeCalculate() { $diff = $this->implementation->calculate(array('5'), array('05')); $this->assertInternalType('array', $diff); $this->assertCount(0, $diff); } } tests/LCS/MemoryEfficientImplementationTest.php 0000604 00000001034 15224664711 0015675 0 ustar 00 <?php /* * This file is part of sebastian/diff. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff\LCS; /** * @covers SebastianBergmann\Diff\LCS\MemoryEfficientImplementation */ class MemoryEfficientImplementationTest extends LongestCommonSubsequenceTest { protected function createImplementation() { return new MemoryEfficientImplementation; } } tests/LCS/TimeEfficientImplementationTest.php 0000604 00000001026 15224664711 0015324 0 ustar 00 <?php /* * This file is part of sebastian/diff. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff\LCS; /** * @covers SebastianBergmann\Diff\LCS\TimeEfficientImplementation */ class TimeEfficientImplementationTest extends LongestCommonSubsequenceTest { protected function createImplementation() { return new TimeEfficientImplementation; } } tests/fixtures/patch.txt 0000604 00000000310 15224664711 0011415 0 ustar 00 diff --git a/Foo.php b/Foo.php index abcdefg..abcdefh 100644 --- a/Foo.php +++ b/Foo.php @@ -20,4 +20,5 @@ class Foo const ONE = 1; const TWO = 2; + const THREE = 3; const FOUR = 4; tests/fixtures/patch2.txt 0000604 00000000632 15224664711 0011506 0 ustar 00 diff --git a/Foo.php b/Foo.php index abcdefg..abcdefh 100644 --- a/Foo.php +++ b/Foo.php @@ -20,4 +20,5 @@ class Foo const ONE = 1; const TWO = 2; + const THREE = 3; const FOUR = 4; @@ -320,4 +320,5 @@ class Foo const A = 'A'; const B = 'B'; + const C = 'C'; const D = 'D'; @@ -600,4 +600,5 @@ class Foo public function doSomething() { + return 'foo'; } tests/ChunkTest.php 0000604 00000003012 15224664711 0010327 0 ustar 00 <?php /* * This file is part of sebastian/diff. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff; use PHPUnit\Framework\TestCase; /** * @covers SebastianBergmann\Diff\Chunk */ class ChunkTest extends TestCase { /** * @var Chunk */ private $chunk; protected function setUp() { $this->chunk = new Chunk; } public function testCanBeCreatedWithoutArguments() { $this->assertInstanceOf('SebastianBergmann\Diff\Chunk', $this->chunk); } public function testStartCanBeRetrieved() { $this->assertEquals(0, $this->chunk->getStart()); } public function testStartRangeCanBeRetrieved() { $this->assertEquals(1, $this->chunk->getStartRange()); } public function testEndCanBeRetrieved() { $this->assertEquals(0, $this->chunk->getEnd()); } public function testEndRangeCanBeRetrieved() { $this->assertEquals(1, $this->chunk->getEndRange()); } public function testLinesCanBeRetrieved() { $this->assertEquals(array(), $this->chunk->getLines()); } public function testLinesCanBeSet() { $this->assertEquals(array(), $this->chunk->getLines()); $testValue = array('line0', 'line1'); $this->chunk->setLines($testValue); $this->assertEquals($testValue, $this->chunk->getLines()); } } tests/DifferTest.php 0000604 00000036076 15224664711 0010476 0 ustar 00 <?php /* * This file is part of sebastian/diff. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff; use SebastianBergmann\Diff\LCS\MemoryEfficientImplementation; use SebastianBergmann\Diff\LCS\TimeEfficientImplementation; use PHPUnit\Framework\TestCase; /** * @covers SebastianBergmann\Diff\Differ * * @uses SebastianBergmann\Diff\LCS\MemoryEfficientImplementation * @uses SebastianBergmann\Diff\LCS\TimeEfficientImplementation * @uses SebastianBergmann\Diff\Chunk * @uses SebastianBergmann\Diff\Diff * @uses SebastianBergmann\Diff\Line * @uses SebastianBergmann\Diff\Parser */ class DifferTest extends TestCase { const REMOVED = 2; const ADDED = 1; const OLD = 0; /** * @var Differ */ private $differ; protected function setUp() { $this->differ = new Differ; } /** * @param array $expected * @param string|array $from * @param string|array $to * @dataProvider arrayProvider */ public function testArrayRepresentationOfDiffCanBeRenderedUsingTimeEfficientLcsImplementation(array $expected, $from, $to) { $this->assertEquals($expected, $this->differ->diffToArray($from, $to, new TimeEfficientImplementation)); } /** * @param string $expected * @param string $from * @param string $to * @dataProvider textProvider */ public function testTextRepresentationOfDiffCanBeRenderedUsingTimeEfficientLcsImplementation($expected, $from, $to) { $this->assertEquals($expected, $this->differ->diff($from, $to, new TimeEfficientImplementation)); } /** * @param array $expected * @param string|array $from * @param string|array $to * @dataProvider arrayProvider */ public function testArrayRepresentationOfDiffCanBeRenderedUsingMemoryEfficientLcsImplementation(array $expected, $from, $to) { $this->assertEquals($expected, $this->differ->diffToArray($from, $to, new MemoryEfficientImplementation)); } /** * @param string $expected * @param string $from * @param string $to * @dataProvider textProvider */ public function testTextRepresentationOfDiffCanBeRenderedUsingMemoryEfficientLcsImplementation($expected, $from, $to) { $this->assertEquals($expected, $this->differ->diff($from, $to, new MemoryEfficientImplementation)); } public function testCustomHeaderCanBeUsed() { $differ = new Differ('CUSTOM HEADER'); $this->assertEquals( "CUSTOM HEADER@@ @@\n-a\n+b\n", $differ->diff('a', 'b') ); } public function testTypesOtherThanArrayAndStringCanBePassed() { $this->assertEquals( "--- Original\n+++ New\n@@ @@\n-1\n+2\n", $this->differ->diff(1, 2) ); } /** * @param string $diff * @param Diff[] $expected * @dataProvider diffProvider */ public function testParser($diff, array $expected) { $parser = new Parser; $result = $parser->parse($diff); $this->assertEquals($expected, $result); } public function arrayProvider() { return array( array( array( array('a', self::REMOVED), array('b', self::ADDED) ), 'a', 'b' ), array( array( array('ba', self::REMOVED), array('bc', self::ADDED) ), 'ba', 'bc' ), array( array( array('ab', self::REMOVED), array('cb', self::ADDED) ), 'ab', 'cb' ), array( array( array('abc', self::REMOVED), array('adc', self::ADDED) ), 'abc', 'adc' ), array( array( array('ab', self::REMOVED), array('abc', self::ADDED) ), 'ab', 'abc' ), array( array( array('bc', self::REMOVED), array('abc', self::ADDED) ), 'bc', 'abc' ), array( array( array('abc', self::REMOVED), array('abbc', self::ADDED) ), 'abc', 'abbc' ), array( array( array('abcdde', self::REMOVED), array('abcde', self::ADDED) ), 'abcdde', 'abcde' ), 'same start' => array( array( array(17, self::OLD), array('b', self::REMOVED), array('d', self::ADDED), ), array(30 => 17, 'a' => 'b'), array(30 => 17, 'c' => 'd'), ), 'same end' => array( array( array(1, self::REMOVED), array(2, self::ADDED), array('b', self::OLD), ), array(1 => 1, 'a' => 'b'), array(1 => 2, 'a' => 'b'), ), 'same start (2), same end (1)' => array( array( array(17, self::OLD), array(2, self::OLD), array(4, self::REMOVED), array('a', self::ADDED), array(5, self::ADDED), array('x', self::OLD), ), array(30 => 17, 1 => 2, 2 => 4, 'z' => 'x'), array(30 => 17, 1 => 2, 3 => 'a', 2 => 5, 'z' => 'x'), ), 'same' => array( array( array('x', self::OLD), ), array('z' => 'x'), array('z' => 'x'), ), 'diff' => array( array( array('y', self::REMOVED), array('x', self::ADDED), ), array('x' => 'y'), array('z' => 'x'), ), 'diff 2' => array( array( array('y', self::REMOVED), array('b', self::REMOVED), array('x', self::ADDED), array('d', self::ADDED), ), array('x' => 'y', 'a' => 'b'), array('z' => 'x', 'c' => 'd'), ), 'test line diff detection' => array( array( array( '#Warning: Strings contain different line endings!', self::OLD, ), array( '<?php', self::OLD, ), array( '', self::OLD, ), ), "<?php\r\n", "<?php\n" ) ); } public function textProvider() { return array( array( "--- Original\n+++ New\n@@ @@\n-a\n+b\n", 'a', 'b' ), array( "--- Original\n+++ New\n@@ @@\n-ba\n+bc\n", 'ba', 'bc' ), array( "--- Original\n+++ New\n@@ @@\n-ab\n+cb\n", 'ab', 'cb' ), array( "--- Original\n+++ New\n@@ @@\n-abc\n+adc\n", 'abc', 'adc' ), array( "--- Original\n+++ New\n@@ @@\n-ab\n+abc\n", 'ab', 'abc' ), array( "--- Original\n+++ New\n@@ @@\n-bc\n+abc\n", 'bc', 'abc' ), array( "--- Original\n+++ New\n@@ @@\n-abc\n+abbc\n", 'abc', 'abbc' ), array( "--- Original\n+++ New\n@@ @@\n-abcdde\n+abcde\n", 'abcdde', 'abcde' ), array( "--- Original\n+++ New\n@@ @@\n-A\n+A1\n B\n", "A\nB", "A1\nB", ), array( <<<EOF --- Original +++ New @@ @@ a -b +p @@ @@ i -j +w k EOF , "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk", "a\np\nc\nd\ne\nf\ng\nh\ni\nw\nk", ), array( <<<EOF --- Original +++ New @@ @@ a -b +p @@ @@ i -j +w k EOF , "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk", "a\np\nc\nd\ne\nf\ng\nh\ni\nw\nk", ), ); } public function diffProvider() { $serialized_arr = <<<EOL a:1:{i:0;O:27:"SebastianBergmann\Diff\Diff":3:{s:33:"