diff --git a/core/includes/config.inc b/core/includes/config.inc index 3591263aa3bd4f55adf18ed3b10695238ade93fa..ffd7036c8907fb44fa1c5d079674aebde94e0fe6 100644 --- a/core/includes/config.inc +++ b/core/includes/config.inc @@ -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); +} diff --git a/core/lib/Drupal/Component/Diff/DiffEngine.php b/core/lib/Drupal/Component/Diff/DiffEngine.php index 1236610721918a8be4d64d7a7139869fdb3a8c99..f426b96ea457d4af617d207660ffbb29c10a5bd0 100644 --- a/core/lib/Drupal/Component/Diff/DiffEngine.php +++ b/core/lib/Drupal/Component/Diff/DiffEngine.php @@ -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' => ' ')), + ' ', ); } diff --git a/core/modules/config/config.admin.inc b/core/modules/config/config.admin.inc index 38138858d48912fefc14ca0e8da0b76f634b7baa..718c773a760d45cf0098e47ad360f4010c132797 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 f8a1874a37880f8798b882e01c1da49f59ddb18a..e3b60278ce9a3e54246c49723564d68032d87878 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 0000000000000000000000000000000000000000..09f126bb48de3278b88d37aa823281a0675d0418 --- /dev/null +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigDiffTest.php @@ -0,0 +1,89 @@ + '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 7900b7b96ed99b447021d43740d3cbb2a0f8a520..3788d67c92b31fc3121e7b0cf65ef225cab41254 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 @@ 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. diff --git a/core/modules/system/system.diff.css b/core/modules/system/system.diff.css new file mode 100644 index 0000000000000000000000000000000000000000..1c7359830fc4b44f1ed12058f51789723e675719 --- /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; +}