summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorwebchick2013-03-10 06:31:56 (GMT)
committerwebchick2013-03-10 06:31:56 (GMT)
commit039dbb98cc26cc8b98c425d8b2fabb0829139e28 (patch)
treedfca7ce5f0024be24df7708605e8c078efbc1ebc
parentc582d6f21952bea0fc8bb688c18cd8b9c7e620b7 (diff)
Issue #1821548 by heyrocker, swentel, Dean Reilly, dawehner, alexpott, vijaycs85, jhedstrom, adamdicarlo: Add a 'diff' of some kind to the CMI UI.
-rw-r--r--core/includes/config.inc41
-rw-r--r--core/lib/Drupal/Component/Diff/DiffEngine.php12
-rw-r--r--core/modules/config/config.admin.inc70
-rw-r--r--core/modules/config/config.module8
-rw-r--r--core/modules/config/lib/Drupal/config/Tests/ConfigDiffTest.php89
-rw-r--r--core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php43
-rw-r--r--core/modules/system/system.diff.css83
7 files changed, 338 insertions, 8 deletions
diff --git a/core/includes/config.inc b/core/includes/config.inc
index 3591263..ffd7036 100644
--- a/core/includes/config.inc
+++ b/core/includes/config.inc
@@ -4,6 +4,7 @@ use Drupal\Core\Config\Config;
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);
+}
diff --git a/core/lib/Drupal/Component/Diff/DiffEngine.php b/core/lib/Drupal/Component/Diff/DiffEngine.php
index 1236610..f426b96 100644
--- a/core/lib/Drupal/Component/Diff/DiffEngine.php
+++ b/core/lib/Drupal/Component/Diff/DiffEngine.php
@@ -1111,11 +1111,11 @@ class DrupalDiffFormatter extends DiffFormatter {
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 @@ class DrupalDiffFormatter extends DiffFormatter {
'class' => 'diff-marker',
),
array(
- 'data' => theme('diff_content_line', array('line' => $line)),
+ 'data' => $line,
'class' => 'diff-context diff-addedline',
)
);
@@ -1159,7 +1159,7 @@ class DrupalDiffFormatter extends DiffFormatter {
'class' => 'diff-marker',
),
array(
- 'data' => theme('diff_content_line', array('line' => $line)),
+ 'data' => $line,
'class' => 'diff-context diff-deletedline',
)
);
@@ -1172,7 +1172,7 @@ class DrupalDiffFormatter extends DiffFormatter {
return array(
' ',
array(
- 'data' => theme('diff_content_line', array('line' => $line)),
+ 'data' => $line,
'class' => 'diff-context',
)
);
@@ -1181,7 +1181,7 @@ class DrupalDiffFormatter extends DiffFormatter {
function emptyLine() {
return array(
' ',
- theme('diff_empty_line', array('line' => ' ')),
+ ' ',
);
}
diff --git a/core/modules/config/config.admin.inc b/core/modules/config/config.admin.inc
index 3813885..718c773 100644
--- a/core/modules/config/config.admin.inc
+++ b/core/modules/config/config.admin.inc
@@ -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;
+}
diff --git a/core/modules/config/config.module b/core/modules/config/config.module
index f8a1874..e3b6027 100644
--- a/core/modules/config/config.module
+++ b/core/modules/config/config.module
@@ -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,
diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigDiffTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigDiffTest.php
new file mode 100644
index 0000000..09f126b
--- /dev/null
+++ b/core/modules/config/lib/Drupal/config/Tests/ConfigDiffTest.php
@@ -0,0 +1,89 @@
+<?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
diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php
index 7900b7b..3788d67 100644
--- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php
+++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php
@@ -126,6 +126,49 @@ class ConfigImportUITest extends WebTestBase {
$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.
diff --git a/core/modules/system/system.diff.css b/core/modules/system/system.diff.css
new file mode 100644
index 0000000..1c73598
--- /dev/null
+++ b/core/modules/system/system.diff.css
@@ -0,0 +1,83 @@
+/**
+ * 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;
+}