Browse Source

Use our own differ

Resolves #7218
pull/7292/head
Brandon Kelly 5 years ago
parent
commit
1ef148e467
  1. 2
      CHANGELOG-v3.6.md
  2. 2
      CHANGELOG.md
  3. 1
      composer.json
  4. 134
      composer.lock
  5. 178
      src/helpers/Diff.php
  6. 24
      src/helpers/ProjectConfig.php
  7. 3595
      tests/_data/diff/a.php
  8. 3595
      tests/_data/diff/b.php
  9. 94
      tests/unit/helpers/DiffHelperTest.php

2
CHANGELOG-v3.6.md

@ -51,6 +51,7 @@
- Added `craft\gql\types\input\criteria\Entry`.
- Added `craft\gql\types\input\criteria\Tag`.
- Added `craft\gql\types\input\criteria\User`.
- Added `craft\helpers\Diff`.
- Added `craft\helpers\Gql::eagerLoadComplexity()`.
- Added `craft\helpers\Gql::nPlus1Complexity()`.
- Added `craft\helpers\Gql::singleQueryComplexity()`.
@ -76,6 +77,7 @@
### Changed
- Renamed the `backup` and `restore` commands to `db/backup` and `db/restore`. ([#7023](https://github.com/craftcms/cms/issues/7023))
- Relational fields now include all related elements’ titles as search keywords, including disabled elements. ([#7079](https://github.com/craftcms/cms/issues/7079))
- Improved the performance of project config change diffs. ([#7218](https://github.com/craftcms/cms/issues/7218))
- The `withoutKey` Twig filter can now accept an array, for removing multiple keys at once. ([#7230](https://github.com/craftcms/cms/issues/7230))
- It’s now possible to add new log targets by overriding `components.log.targets` in `config/app.php`, rather than the entire `log` component config.
- `craft\base\ElementExporterInterface::export()` can now return raw response data, or a resource, if `isFormattable()` returns `false`. If a resource is returned, it will be streamed to the browser. ([#7148](https://github.com/craftcms/cms/issues/7148))

2
CHANGELOG.md

@ -30,6 +30,7 @@
- Added `craft\gql\types\input\criteria\Entry`.
- Added `craft\gql\types\input\criteria\Tag`.
- Added `craft\gql\types\input\criteria\User`.
- Added `craft\helpers\Diff`.
- Added `craft\models\Site::getName()`.
- Added `craft\models\Site::setBaseUrl()`.
- Added `craft\models\Site::setName()`.
@ -43,6 +44,7 @@
- Added `craft\test\TestSetup::SITE_URL`.
### Changed
- Improved the performance of project config change diffs. ([#7218](https://github.com/craftcms/cms/issues/7218))
- The `withoutKey` Twig filter can now accept an array, for removing multiple keys at once. ([#7230](https://github.com/craftcms/cms/issues/7230))
- `craft\behaviors\EnvAttributeParserBehavior::$attributes` can now be set to an array with key/value pairs, where the key is the attribute name, and the value is the raw (unparsed) value, or a callable that returns the raw value.
- `craft\models\Site::$baseUrl` is now a magic property, which returns the parsed base URL. ([#3964](https://github.com/craftcms/cms/issues/3964))

1
composer.json

@ -40,7 +40,6 @@
"league/oauth2-client": "^2.6.0",
"mikehaertl/php-shellcommand": "^1.6.2",
"pixelandtonic/imagine": "~1.2.4.1",
"sebastian/diff": "^3.0.2|^4.0.4",
"seld/cli-prompt": "^1.0.3",
"symfony/yaml": "^5.1.8",
"true/punycode": "^2.1.1",

134
composer.lock

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "1417ebe131c3bb4ca046ce50bd740a23",
"content-hash": "5b98e323e88e34e93bf8b403487f10fd",
"packages": [
{
"name": "cebe/markdown",
@ -2475,72 +2475,6 @@
},
"time": "2020-05-12T15:16:56+00:00"
},
{
"name": "sebastian/diff",
"version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
"reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/14f72dd46eaf2f2293cbe79c93cc0bc43161a211",
"reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"require-dev": {
"phpunit/phpunit": "^7.5 || ^8.0",
"symfony/process": "^2 || ^3.3 || ^4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
},
{
"name": "Kore Nordmann",
"email": "mail@kore-nordmann.de"
}
],
"description": "Diff implementation",
"homepage": "https://github.com/sebastianbergmann/diff",
"keywords": [
"diff",
"udiff",
"unidiff",
"unified diff"
],
"support": {
"issues": "https://github.com/sebastianbergmann/diff/issues",
"source": "https://github.com/sebastianbergmann/diff/tree/3.0.3"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
}
],
"time": "2020-11-30T07:59:04+00:00"
},
{
"name": "seld/cli-prompt",
"version": "1.0.3",
@ -7215,6 +7149,72 @@
],
"time": "2020-11-30T08:04:30+00:00"
},
{
"name": "sebastian/diff",
"version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
"reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/14f72dd46eaf2f2293cbe79c93cc0bc43161a211",
"reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"require-dev": {
"phpunit/phpunit": "^7.5 || ^8.0",
"symfony/process": "^2 || ^3.3 || ^4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
},
{
"name": "Kore Nordmann",
"email": "mail@kore-nordmann.de"
}
],
"description": "Diff implementation",
"homepage": "https://github.com/sebastianbergmann/diff",
"keywords": [
"diff",
"udiff",
"unidiff",
"unified diff"
],
"support": {
"issues": "https://github.com/sebastianbergmann/diff/issues",
"source": "https://github.com/sebastianbergmann/diff/tree/3.0.3"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
}
],
"time": "2020-11-30T07:59:04+00:00"
},
{
"name": "sebastian/environment",
"version": "4.2.4",

178
src/helpers/Diff.php

@ -0,0 +1,178 @@
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/
namespace craft\helpers;
use Symfony\Component\Yaml\Yaml;
/**
* Diff helper
*
* @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
* @since 3.6.0
*/
class Diff
{
/**
* Generates a diff for two values, represented as YAML.
*
* @param $from
* @param $to
* @param int $indent The indent size that nested values should have
* @param int $contextLines The number of lines to show before and after changes
* @return string
*/
public static function diff($from, $to, int $indent = 2, int $contextLines = 3): string
{
$diff = '';
$lines = static::_diff($from, $to, $indent, 0);
$lastChange = null;
foreach ($lines as $i => $line) {
if ($line[0] === null) {
continue;
}
// Is this the first change we've seen?
if ($contextLines > 0) {
if ($lastChange === null) {
for ($j = max($i - $contextLines, 0); $j < $i; $j++) {
$diff .= ' ' . $lines[$j][1] . "\n";
}
} else if ($lastChange < $i - $contextLines * 2 + 2) {
// More than 2X the context size
for ($j = $lastChange + 1; $j < $lastChange + $contextLines + 1; $j++) {
$diff .= ' ' . $lines[$j][1] . "\n";
}
$diff .= "...\n";
for ($j = $i - $contextLines; $j < $i; $j++) {
$diff .= ' ' . $lines[$j][1] . "\n";
}
} else {
// Within two contexts so just show the whole chunk
for ($j = $lastChange + 1; $j < $i; $j++) {
$diff .= ' ' . $lines[$j][1] . "\n";
}
}
}
$diff .= $lines[$i][0] . ' ' . $lines[$i][1] . "\n";
$lastChange = $i;
}
// Remaining context
if ($lastChange !== null && $contextLines > 0) {
$max = min($lastChange + $contextLines, count($lines) - 1);
for ($i = $lastChange + 1; $i < $max; $i++) {
$diff .= ' ' . $lines[$i][1] . "\n";
}
}
return rtrim($diff);
}
/**
* @param $from
* @param $to
* @param int $indent
* @param int $level
* @return array[]
*/
private static function _diff($from, $to, int $indent, int $level): array
{
// Are we done doing recursion?
if (
(!is_array($from) || !ArrayHelper::isAssociative($from)) ||
(!is_array($to) || !ArrayHelper::isAssociative($to))
) {
if (static::compare($from, $to)) {
return static::_buildLinesForValue($from, $indent, $level);
} else {
$lines = [];
ArrayHelper::append($lines, ...static::_buildLinesForValue($from, $indent, $level, '-'));
ArrayHelper::append($lines, ...static::_buildLinesForValue($to, $indent, $level, '+'));
return $lines;
}
}
$lines = [];
$toKeys = array_keys($to);
$toCursor = 0;
foreach ($from as $key => $value) {
// Do both arrays have this key?
if (array_key_exists($key, $to)) {
$toPos = array_search($key, $toKeys);
// Output any keys in $to that come before this one
if ($toPos > $toCursor) {
$newKeys = array_slice($toKeys, $toCursor, $toPos - $toCursor);
static::_buildLinesForValue(ArrayHelper::filter($to, $newKeys), $indent, $level, '+');
}
$lines[] = static::_buildLine("$key:", $indent, $level);
ArrayHelper::append($lines, ...static::_diff($value, $to[$key], $indent, $level + 1));
$toCursor = $toPos + 1;
} else {
ArrayHelper::append($lines, ...static::_buildLinesForValue([$key => $value], $indent, $level, '-'));
}
}
// Output any remaining $to keys
$newKeys = array_slice($toKeys, $toCursor);
if (!empty($newKeys)) {
ArrayHelper::append($lines, ...static::_buildLinesForValue(ArrayHelper::filter($to, $newKeys), $indent, $level, '+'));
}
return $lines;
}
private static function _buildLinesForValue($value, int $indent, int $level, ?string $char = null): array
{
$lines = [];
$yamlLines = explode("\n", rtrim(Yaml::dump($value, 20 - $level, $indent)));
foreach ($yamlLines as $line) {
$lines[] = static::_buildLine($line, $indent, $level, $char);
}
return $lines;
}
private static function _buildLine(string $line, int $indent, int $level, ?string $char = null): array
{
return [$char, str_repeat(' ', $indent * $level) . $line];
}
/**
* Compares two arrays and returns whether they are identical.
*
* If the values are both arrays, they will be compared recursively.
*
* @param mixed $a
* @param mixed $b
* @param bool $strict Whether strict comparisons should be used
* @return bool
* @since 3.6.0
*/
public static function compare($a, $b, bool $strict = true): bool
{
if (!is_array($a) || !is_array($b)) {
return $strict ? $a === $b : $a == $b;
}
if (array_keys($a) !== array_keys($b)) {
return false;
}
foreach ($a as $key => $value) {
if (!static::compare($value, $b[$key], $strict)) {
return false;
}
}
return true;
}
}

24
src/helpers/ProjectConfig.php

@ -13,9 +13,6 @@ use craft\services\Gql as GqlService;
use craft\services\ProjectConfig as ProjectConfigService;
use craft\services\Sites;
use craft\services\UserGroups;
use SebastianBergmann\Diff\Differ;
use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder;
use Symfony\Component\Yaml\Yaml;
use yii\base\InvalidConfigException;
use yii\caching\ChainedDependency;
use yii\caching\ExpressionDependency;
@ -460,26 +457,13 @@ class ProjectConfig
$cacheKey = ProjectConfigService::DIFF_CACHE_KEY . ($invert ? ':reverse' : '');
return Craft::$app->getCache()->getOrSet($cacheKey, function() use ($projectConfig, $invert): string {
$currentConfig = $projectConfig->get();
$pendingConfig = $projectConfig->get(null, true);
$currentYaml = Yaml::dump(static::cleanupConfig($currentConfig), 20, 2);
$pendingYaml = Yaml::dump(static::cleanupConfig($pendingConfig), 20, 2);
$builder = new UnifiedDiffOutputBuilder('');
$differ = new Differ($builder);
$currentConfig = static::cleanupConfig($projectConfig->get());
$pendingConfig = static::cleanupConfig($projectConfig->get(null, true));
if ($invert) {
$diff = $differ->diff($pendingYaml, $currentYaml);
} else {
$diff = $differ->diff($currentYaml, $pendingYaml);
return Diff::diff($pendingConfig, $currentConfig);
}
// Cleanup
$diff = preg_replace("/^@@ @@\n/", '', $diff);
$diff = preg_replace('/^[\+\-]?/m', '$0 ', $diff);
$diff = str_replace(' @@ @@', '...', $diff);
$diff = rtrim($diff);
return $diff;
return Diff::diff($currentConfig, $pendingConfig);
}, null, new ChainedDependency([
'dependencies' => [
$projectConfig->getCacheDependency(),

3595
tests/_data/diff/a.php
File diff suppressed because it is too large
View File

3595
tests/_data/diff/b.php
File diff suppressed because it is too large
View File

94
tests/unit/helpers/DiffHelperTest.php

@ -0,0 +1,94 @@
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/
namespace crafttests\unit\helpers;
use Codeception\Test\Unit;
use craft\helpers\Diff;
/**
* Unit tests for the Diff Helper class.
*
* @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
* @since 3.6.0
*/
class DiffHelperTest extends Unit
{
/**
* @dataProvider compareDataProvider
*
* @param bool $expected
* @param mixed $a
* @param mixed $b
* @param bool $strict
*/
public function testCompare(bool $expected, $a, $b, bool $strict)
{
self::assertSame($expected, Diff::compare($a, $b, $strict));
}
/**
* @dataProvider diffDataProvider
*
* @param string $expected
* @param mixed $from
* @param mixed $to
* @param int $indent
* @param int $contextLines
*/
public function testDiff(string $expected, $from, $to, int $indent = 2, int $contextLines = 3)
{
self::assertSame($expected, Diff::diff($from, $to, $indent, $contextLines));
}
/**
* @return array
*/
public function compareDataProvider(): array
{
return [
[true, 1, '1', false],
[false, 1, '1', true],
[true, ['foo' => ['bar' => 'baz']], ['foo' => ['bar' => 'baz']], true],
[false, ['foo' => ['bar' => 'baz']], ['foo' => ['bar' => 'qux']], true],
[false, ['foo' => true], ['foo' => true, 'bar' => true], true],
];
}
/**
* @return array
*/
public function diffDataProvider(): array
{
return [
['', 'foo', 'foo'],
["- foo\n+ bar", 'foo', 'bar'],
[
"- - foo\n- - bar\n- - baz\n+ - foo\n+ - bar\n+ - qux",
['foo', 'bar', 'baz'],
['foo', 'bar', 'qux']
],
[
" foo:\n- - bar\n- - baz\n+ - bar\n+ - qux",
['foo' => ['bar', 'baz']],
['foo' => ['bar', 'qux']]
],
[
"- - bar\n- - baz\n+ - bar\n+ - qux",
['foo' => ['bar', 'baz']],
['foo' => ['bar', 'qux']],
4,
0
],
[
" dateModified:\n- 1607544575\n+ 1607544576\n email:\n fromEmail:",
include dirname(__DIR__, 2) . '/_data/diff/a.php',
include dirname(__DIR__, 2) . '/_data/diff/b.php'
]
];
}
}
Loading…
Cancel
Save