Commit 039dbb98 authored by webchick's avatar webchick

Issue #1821548 by heyrocker, swentel, Dean Reilly, dawehner, alexpott,...

Issue #1821548 by heyrocker, swentel, Dean Reilly, dawehner, alexpott, vijaycs85, jhedstrom, adamdicarlo: Add a 'diff' of some kind to the CMI UI.
parent c582d6f2
......@@ -4,6 +4,7 @@
use Drupal\Core\Config\ConfigException;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\StorageInterface;
use Symfony\Component\Yaml\Dumper;
/**
* @file
......@@ -422,3 +423,43 @@ function config_get_entity_type_by_name($name) {
function config_typed() {
return drupal_container()->get('config.typed');
}
/**
* Return a formatted diff of a named config between two storages.
*
* @param Drupal\Core\Config\StorageInterface $source_storage
* The storage to diff configuration from.
* @param Drupal\Core\Config\StorageInterface $target_storage
* The storage to diff configuration to.
* @param string $name
* The name of the configuration object to diff.
*
* @return core/lib/Drupal/Component/Diff
* A formatted string showing the difference between the two storages.
*
* @todo Make renderer injectable
*/
function config_diff(StorageInterface $source_storage, StorageInterface $target_storage, $name) {
require_once DRUPAL_ROOT . '/core/lib/Drupal/Component/Diff/DiffEngine.php';
// The output should show configuration object differences formatted as YAML.
// But the configuration is not necessarily stored in files. Therefore, they
// need to be read and parsed, and lastly, dumped into YAML strings.
$dumper = new Dumper();
$dumper->setIndentation(2);
$source_data = explode("\n", $dumper->dump($source_storage->read($name), PHP_INT_MAX));
$target_data = explode("\n", $dumper->dump($target_storage->read($name), PHP_INT_MAX));
// Check for new or removed files.
if ($source_data === array('false')) {
// Added file.
$source_data = array(t('File added'));
}
if ($target_data === array('false')) {
// Deleted file.
$target_data = array(t('File removed'));
}
return new Diff($source_data, $target_data);
}
......@@ -1111,11 +1111,11 @@ function _end_diff() {
function _block_header($xbeg, $xlen, $ybeg, $ylen) {
return array(
array(
'data' => theme('diff_header_line', array('lineno' => $xbeg + $this->line_stats['offset']['x'])),
'data' => $xbeg + $this->line_stats['offset']['x'],
'colspan' => 2,
),
array(
'data' => theme('diff_header_line', array('lineno' => $ybeg + $this->line_stats['offset']['y'])),
'data' => $ybeg + $this->line_stats['offset']['y'],
'colspan' => 2,
)
);
......@@ -1143,7 +1143,7 @@ function addedLine($line) {
'class' => 'diff-marker',
),
array(
'data' => theme('diff_content_line', array('line' => $line)),
'data' => $line,
'class' => 'diff-context diff-addedline',
)
);
......@@ -1159,7 +1159,7 @@ function deletedLine($line) {
'class' => 'diff-marker',
),
array(
'data' => theme('diff_content_line', array('line' => $line)),
'data' => $line,
'class' => 'diff-context diff-deletedline',
)
);
......@@ -1172,7 +1172,7 @@ function contextLine($line) {
return array(
' ',
array(
'data' => theme('diff_content_line', array('line' => $line)),
'data' => $line,
'class' => 'diff-context',
)
);
......@@ -1181,7 +1181,7 @@ function contextLine($line) {
function emptyLine() {
return array(
' ',
theme('diff_empty_line', array('line' => ' ')),
' ',
);
}
......
......@@ -41,6 +41,7 @@ function config_admin_sync_form(array &$form, array &$form_state, StorageInterfa
if (empty($config_files)) {
continue;
}
// @todo A table caption would be more appropriate, but does not have the
// visual importance of a heading.
$form[$config_change_type]['heading'] = array(
......@@ -62,10 +63,24 @@ function config_admin_sync_form(array &$form, array &$form_state, StorageInterfa
}
$form[$config_change_type]['list'] = array(
'#theme' => 'table',
'#header' => array('Name'),
'#header' => array('Name', 'Operations'),
);
foreach ($config_files as $config_file) {
$form[$config_change_type]['list']['#rows'][] = array($config_file);
$links['view_diff'] = array(
'title' => t('View differences'),
'href' => 'admin/config/development/sync/diff/' . $config_file,
'ajax' => array('dialog' => array('modal' =>TRUE, 'width' => '700px')),
);
$form[$config_change_type]['list']['#rows'][] = array(
'name' => $config_file,
'operations' => array(
'data' => array(
'#type' => 'operations',
'#links' => $links,
),
),
);
}
}
}
......@@ -116,3 +131,54 @@ function config_admin_import_form_submit($form, &$form_state) {
drupal_set_message(t('The import failed due to an error. Any errors have been logged.'), 'error');
}
}
/**
* Page callback: Shows diff of specificed configuration file.
*
* @param string $config_file
* The name of the configuration file.
*
* @return string
* Table showing a two-way diff between the active and staged configuration.
*/
function config_admin_diff_page($config_file) {
// Retrieve a list of differences between last known state and active store.
$source_storage = drupal_container()->get('config.storage.staging');
$target_storage = drupal_container()->get('config.storage');
// Add the CSS for the inline diff.
$output['#attached']['css'][] = drupal_get_path('module', 'system') . '/system.diff.css';
$output['title'] = array(
'#theme' => 'html_tag',
'#tag' => 'h3',
'#value' => t('View changes of @config_file', array('@config_file' => $config_file)),
);
$diff = config_diff($target_storage, $source_storage, $config_file);
$formatter = new DrupalDiffFormatter();
$formatter->show_header = FALSE;
$variables = array(
'header' => array(
array('data' => t('Old'), 'colspan' => '2'),
array('data' => t('New'), 'colspan' => '2'),
),
'rows' => $formatter->format($diff),
);
$output['diff'] = array(
'#markup' => theme('table', $variables),
);
$output['back'] = array(
'#type' => 'link',
'#title' => "Back to 'Synchronize configuration' page.",
'#href' => 'admin/config/development/sync',
'#attributes' => array(
'class' => array('dialog-cancel'),
),
);
return $output;
}
......@@ -48,6 +48,14 @@ function config_menu() {
'access arguments' => array('synchronize configuration'),
'file' => 'config.admin.inc',
);
$items['admin/config/development/sync/diff/%'] = array(
'title' => 'Configuration file diff',
'description' => 'Diff between active and staged configuraiton.',
'page callback' => 'config_admin_diff_page',
'page arguments' => array(5),
'access arguments' => array('synchronize configuration'),
'file' => 'config.admin.inc',
);
$items['admin/config/development/sync/import'] = array(
'title' => 'Import',
'type' => MENU_DEFAULT_LOCAL_TASK,
......
<?php
/**
* @file
* Contains \Drupal\config\Tests\ConfigDiffTest.
*/
namespace Drupal\config\Tests;
use Drupal\simpletest\DrupalUnitTestBase;
/**
* Tests config snapshot creation and updating.
*/
class ConfigDiffTest extends DrupalUnitTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('config_test', 'system');
public static function getInfo() {
return array(
'name' => 'Diff functionality',
'description' => 'Calculating the difference between two sets of configuration.',
'group' => 'Configuration',
);
}
/**
* Tests calculating the difference between two sets of configuration.
*/
function testDiff() {
$active = $this->container->get('config.storage');
$staging = $this->container->get('config.storage.staging');
$config_name = 'config_test.system';
$change_key = 'foo';
$remove_key = '404';
$add_key = 'biff';
$add_data = 'bangpow';
$change_data = 'foobar';
$original_data = array(
'foo' => 'bar',
'404' => 'herp',
);
// Install the default config.
config_install_default_config('module', 'config_test');
// Change a configuration value in staging.
$staging_data = $original_data;
$staging_data[$change_key] = $change_data;
$staging_data[$add_key] = $add_data;
$staging->write($config_name, $staging_data);
// Verify that the diff reflects a change.
$diff = config_diff($active, $staging, $config_name);
$this->assertEqual($diff->edits[0]->type, 'change', 'The first item in the diff is a change.');
$this->assertEqual($diff->edits[0]->orig[0], $change_key . ': ' . $original_data[$change_key], format_string("The active value for key '%change_key' is '%original_data'.", array('%change_key' => $change_key, '%original_data' => $original_data[$change_key])));
$this->assertEqual($diff->edits[0]->closing[0], $change_key . ': ' . $change_data, format_string("The staging value for key '%change_key' is '%change_data'.", array('%change_key' => $change_key, '%change_data' => $change_data)));
// Reset data back to original, and remove a key
$staging_data = $original_data;
unset($staging_data[$remove_key]);
$staging->write($config_name, $staging_data);
// Verify that the diff reflects a removed key.
$diff = config_diff($active, $staging, $config_name);
$this->assertEqual($diff->edits[0]->type, 'copy', 'The first item in the diff is a copy.');
$this->assertEqual($diff->edits[1]->type, 'delete', 'The second item in the diff is a delete.');
$this->assertEqual($diff->edits[1]->orig[0], $remove_key . ': ' . $original_data[$remove_key], format_string("The active value for key '%remove_key' is '%original_data'.", array('%remove_key' => $remove_key, '%original_data' => $original_data[$remove_key])));
$this->assertFalse($diff->edits[1]->closing, format_string("The key '%remove_key' does not exist in staging.", array('%remove_key' => $remove_key)));
// Reset data back to original and add a key
$staging_data = $original_data;
$staging_data[$add_key] = $add_data;
$staging->write($config_name, $staging_data);
// Verify that the diff reflects an added key.
$diff = config_diff($active, $staging, $config_name);
$this->assertEqual($diff->edits[0]->type, 'copy', 'The first item in the diff is a copy.');
$this->assertEqual($diff->edits[1]->type, 'add', 'The second item in the diff is an add.');
$this->assertFalse($diff->edits[1]->orig, format_string("The key '%add_key' does not exist in active.", array('%add_key' => $add_key)));
$this->assertEqual($diff->edits[1]->closing[0], $add_key . ': ' . $add_data, format_string("The staging value for key '%add_key' is '%add_data'.", array('%add_key' => $add_key, '%add_data' => $add_data)));
}
}
\ No newline at end of file
......@@ -126,6 +126,49 @@ function testImportLock() {
$this->assertNotEqual($new_site_name, config('system.site')->get('name'));
}
/**
* Tests the screen that shows differences between active and staging.
*/
function testImportDiff() {
$active = $this->container->get('config.storage');
$staging = $this->container->get('config.storage.staging');
$config_name = 'config_test.system';
$change_key = 'foo';
$remove_key = '404';
$add_key = 'biff';
$add_data = 'bangpow';
$change_data = 'foobar';
$original_data = array(
'foo' => 'bar',
'404' => 'herp',
);
// Change a configuration value in staging.
$staging_data = $original_data;
$staging_data[$change_key] = $change_data;
$staging_data[$add_key] = $add_data;
$staging->write($config_name, $staging_data);
// Load the diff UI and verify that the diff reflects the change.
$this->drupalGet('admin/config/development/sync/diff/' . $config_name);
// Reset data back to original, and remove a key
$staging_data = $original_data;
unset($staging_data[$remove_key]);
$staging->write($config_name, $staging_data);
// Load the diff UI and verify that the diff reflects a removed key.
$this->drupalGet('admin/config/development/sync/diff/' . $config_name);
// Reset data back to original and add a key
$staging_data = $original_data;
$staging_data[$add_key] = $add_data;
$staging->write($config_name, $staging_data);
// Load the diff UI and verify that the diff reflects an added key.
$this->drupalGet('admin/config/development/sync/diff/' . $config_name);
}
function prepareSiteNameUpdate($new_site_name) {
$staging = $this->container->get('config.storage.staging');
// Create updated configuration object.
......
/**
* Inline diff metadata
*/
.diff-inline-metadata {
padding:4px;
border:1px solid #ddd;
background:#fff;
margin:0px 0px 10px;
}
.diff-inline-legend { font-size:11px; }
.diff-inline-legend span,
.diff-inline-legend label { margin-right:5px; }
/**
* Inline diff markup
*/
span.diff-deleted { color:#ccc; }
span.diff-deleted img { border: solid 2px #ccc; }
span.diff-changed { background:#ffb; }
span.diff-changed img { border:solid 2px #ffb; }
span.diff-added { background:#cfc; }
span.diff-added img { border: solid 2px #cfc; }
/**
* Traditional split diff theming
*/
table.diff {
border-spacing: 4px;
margin-bottom: 20px;
table-layout: fixed;
width: 100%;
}
table.diff tr.even, table.diff tr.odd {
background-color: inherit;
border: none;
}
td.diff-prevlink {
text-align: left;
}
td.diff-nextlink {
text-align: right;
}
td.diff-section-title, div.diff-section-title {
background-color: #f0f0ff;
font-size: 0.83em;
font-weight: bold;
padding: 0.1em 1em;
}
td.diff-context {
background-color: #fafafa;
}
td.diff-deletedline {
background-color: #ffa;
width: 50%;
}
td.diff-addedline {
background-color: #afa;
width: 50%;
}
span.diffchange {
color: #f00;
font-weight: bold;
}
table.diff col.diff-marker {
width: 1.4em;
}
table.diff col.diff-content {
width: 50%;
}
table.diff th {
padding-right: inherit;
}
table.diff td div {
overflow: auto;
padding: 0.1ex 0.5em;
word-wrap: break-word;
}
table.diff td {
padding: 0.1ex 0.4em;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment