diff --git a/core/includes/config.inc b/core/includes/config.inc
index b4a0666..65326fc 100644
--- a/core/includes/config.inc
+++ b/core/includes/config.inc
@@ -4,6 +4,7 @@
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\NullStorage;
use Drupal\Core\Config\StorageInterface;
+use Symfony\Component\Yaml\Dumper;
/**
* @file
@@ -324,3 +325,45 @@ 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 string
+ * 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));
+
+ $diff = new Diff($source_data, $target_data);
+ $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),
+ );
+
+ return theme('table', $variables);
+}
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 3483330..6373ded 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -3303,5 +3303,36 @@ function drupal_common_theme() {
'container' => array(
'render element' => 'element',
),
+ 'diff_header_line' => array(
+ 'variables' => array('lineno' => NULL),
+ ),
+ 'diff_content_line' => array(
+ 'variables' => array('line' => NULL),
+ ),
+ 'diff_empty_line' => array(
+ 'variables' => array('line' => NULL),
+ ),
+
);
}
+
+/**
+ * Theme function for a header line in the diff.
+ */
+function theme_diff_header_line($vars) {
+ return '' . t('Line %lineno', array('%lineno' => $vars['lineno'])) . '';
+}
+
+/**
+ * Theme function for a content line in the diff.
+ */
+function theme_diff_content_line($vars) {
+ return '
' . $vars['line'] . '
';
+}
+
+/**
+ * Theme function for an empty line in the diff.
+ */
+function theme_diff_empty_line($vars) {
+ return $vars['line'];
+}
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 @@ 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 c15e02a..d394daf 100644
--- a/core/modules/config/config.admin.inc
+++ b/core/modules/config/config.admin.inc
@@ -40,6 +40,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(
@@ -61,10 +62,21 @@ 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);
+ $form[$config_change_type]['list']['#rows'][] = array(
+ 'name' => $config_file,
+ 'operations' => array(
+ 'data' => array(
+ '#type' => 'link',
+ '#title' => t('Diff'),
+ '#href' => 'admin/config/development/sync/diff/' . $config_file,
+ '#ajax' => array('dialog' => array('modal' =>TRUE, 'width' => '700px')),
+ ),
+ ),
+ );
}
}
}
@@ -114,3 +126,42 @@ 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__h3',
+ '#tag' => 'h3',
+ '#value' => t('View changes of @config_file', array('@config_file' => $config_file)),
+ );
+
+ $output['diff'] = array(
+ '#markup' => config_diff($target_storage, $source_storage, $config_file),
+ );
+
+ $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 a41fc09..7482171 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/system/system.diff.css b/core/modules/system/system.diff.css
new file mode 100644
index 0000000..8187561
--- /dev/null
+++ b/core/modules/system/system.diff.css
@@ -0,0 +1,86 @@
+
+html.js .diff-js-hidden { display: none; }
+
+/**
+ * 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-deletedline {
+ background-color: #ffa;
+ width: 50%;
+}
+td.diff-addedline {
+ background-color: #afa;
+ width: 50%;
+}
+td.diff-context {
+ background-color: #fafafa;
+}
+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;
+}