config.drush.inc

  1. 8.0.x commands/core/config.drush.inc
  2. 7.x commands/core/config.drush.inc
  3. master commands/core/config.drush.inc

Provides Configuration Management commands.

Functions

Namesort descending Description
config_config_edit_complete Command argument complete callback.
config_config_export_complete Command argument complete callback.
config_config_get_complete Command argument complete callback.
config_config_import_complete Command argument complete callback.
config_config_set_complete Command argument complete callback.
config_config_view_complete Command argument complete callback.
config_drush_command Implementation of hook_drush_command().
config_drush_help Implementation of hook_drush_help().
drush_config_edit Edit command callback.
drush_config_export Command callback: Export config to specified directory (usually staging).
drush_config_export_validate
drush_config_get Config get command callback.
drush_config_get_object Show and return a config object
drush_config_get_value Show and return a value from config system.
drush_config_import Command callback. Import from specified config directory (defaults to staging).
drush_config_import_validate
drush_config_list Config list command callback
drush_config_merge
drush_config_merge_rollback If drush_config_merge() exits with an error, then Drush will call the rollback function, so that we can clean up. We call the cleanup function explicitly if we exit with no error.
drush_config_set Config set command callback.
_drush_cm_commit_transmitted_configuration_changes
_drush_cm_copy_remote_configuration_via_git
_drush_cm_copy_remote_configuration_via_rsync
_drush_cm_export_local_configuration
_drush_cm_export_remote_configuration_before_merge
_drush_cm_get_configuration_path
_drush_cm_get_initial_vcs_state
_drush_cm_get_remote_configuration_path
_drush_cm_merge_local_and_remote_configurations
_drush_cm_merge_to_original_branch
_drush_cm_prepare_for_export
_drush_cm_prepare_for_local_configuration_export
_drush_config_directories_complete Helper function for command argument complete callback.
_drush_config_export
_drush_config_import
_drush_config_import_partial Imports a partial set of configurations.
_drush_config_merge_action_abandon If the user wants to abandon the work of their merge, then clean up our temporary branches and return TRUE to cause the calling function to exit without committing.
_drush_config_merge_cleanup Reset our state after a config-merge command
_drush_config_names_complete Helper function for command argument complete callback.
_drush_format_config_changes_table Print a table of config changes.
_drush_print_config_changes_table Print a table of config changes.

File

commands/core/config.drush.inc
View source
  1. <?php
  2. /**
  3. * @file
  4. * Provides Configuration Management commands.
  5. */
  6. use Drush\Log\LogLevel;
  7. use Drupal\Core\Config\StorageComparer;
  8. use Drupal\Core\Config\ConfigImporter;
  9. use Drupal\Core\Config\FileStorage;
  10. use Symfony\Component\Yaml\Parser;
  11. /**
  12. * Implementation of hook_drush_help().
  13. */
  14. function config_drush_help($section) {
  15. switch ($section) {
  16. case 'meta:config:title':
  17. return dt('Config commands');
  18. case 'meta:config:summary':
  19. return dt('Interact with the configuration system.');
  20. case 'drush:config-merge':
  21. return dt('Combine configuration data from this site with the configuration data of another site; if there are conflicts, open an interactive three-way merge tool comparing the changes with the base revision');
  22. }
  23. }
  24. /**
  25. * Implementation of hook_drush_command().
  26. */
  27. function config_drush_command() {
  28. $items['config-get'] = array(
  29. 'description' => 'Display a config value, or a whole configuration object.',
  30. 'arguments' => array(
  31. 'config-name' => 'The config object name, for example "system.site".',
  32. 'key' => 'The config key, for example "page.front". Optional.',
  33. ),
  34. 'required-arguments' => 1,
  35. 'options' => array(
  36. 'source' => array(
  37. 'description' => 'The config storage source to read. recognized values are \'active \'and \'staging\'.',
  38. 'example-value' => 'active',
  39. 'value' => 'required',
  40. ),
  41. 'include-overridden' => array(
  42. 'description' => 'Include overridden values.',
  43. )
  44. ),
  45. 'examples' => array(
  46. 'drush config-get system.site' => 'Displays the system.site config.',
  47. 'drush config-get system.site page.front' => 'gets system.site:page.front value.',
  48. ),
  49. 'outputformat' => array(
  50. 'default' => 'yaml',
  51. 'pipe-format' => 'var_export',
  52. ),
  53. 'aliases' => array('cget'),
  54. 'core' => array('8+'),
  55. );
  56. $items['config-set'] = array(
  57. 'description' => 'Set config value directly in active configuration.',
  58. 'arguments' => array(
  59. 'config-name' => 'The config object name, for example "system.site".',
  60. 'key' => 'The config key, for example "page.front".',
  61. 'value' => 'The value to assign to the config key. Use \'-\' to read from STDIN.',
  62. ),
  63. 'options' => array(
  64. 'format' => array(
  65. 'description' => 'Format to parse the object. Use "string" for string (default), and "yaml" for YAML.',
  66. 'example-value' => 'yaml',
  67. 'value' => 'required',
  68. ),
  69. // A convenient way to pass a multiline value within a backend request.
  70. 'value' => array(
  71. 'description' => 'The value to assign to the config key (if any).',
  72. 'hidden' => TRUE,
  73. ),
  74. ),
  75. 'examples' => array(
  76. 'drush config-set system.site page.front node' => 'Sets system.site:page.front to "node".',
  77. ),
  78. 'aliases' => array('cset'),
  79. 'core' => array('8+'),
  80. );
  81. $items['config-export'] = array(
  82. 'description' => 'Export config from the active directory.',
  83. 'core' => array('8+'),
  84. 'aliases' => array('cex'),
  85. 'arguments' => array(
  86. 'label' => "A config directory label (i.e. a key in \$config_directories array in settings.php). Defaults to 'staging'",
  87. ),
  88. 'options' => array(
  89. 'add' => 'Run `git add -p` after exporting. This lets you choose which config changes to stage for commit.',
  90. 'commit' => 'Run `git add -A` and `git commit` after exporting. This commits everything that was exported without prompting.',
  91. 'message' => 'Commit comment for the exported configuration. Optional; may only be used with --commit or --push.',
  92. 'push' => 'Run `git push` after committing. Implies --commit.',
  93. 'remote' => array(
  94. 'description' => 'The remote git branch to use to push changes. Defaults to "origin".',
  95. 'example-value' => 'origin',
  96. ),
  97. 'branch' => array(
  98. 'description' => 'Make commit on provided working branch. Ignored if used without --commit or --push.',
  99. 'example-value' => 'branchname',
  100. ),
  101. 'destination' => 'An arbitrary directory that should receive the exported files. An alternative to label argument',
  102. ),
  103. );
  104. $items['config-import'] = array(
  105. 'description' => 'Import config from a config directory.',
  106. 'arguments' => array(
  107. 'label' => "A config directory label (i.e. a key in \$config_directories array in settings.php). Defaults to 'staging'",
  108. ),
  109. 'options' => array(
  110. 'preview' => array(
  111. 'description' => 'Format for displaying proposed changes. Recognized values: list, diff. Defaults to list',
  112. 'example-value' => 'list',
  113. ),
  114. 'source' => 'An arbitrary directory that holds the configuration files. An alternative to label argument',
  115. 'partial' => 'Allows for partial config imports from the source directory. Only updates and new configs will be processed with this flag (missing configs will not be deleted).',
  116. ),
  117. 'core' => array('8+'),
  118. 'aliases' => array('cim'),
  119. );
  120. $items['config-list'] = array(
  121. 'description' => 'List config names by prefix.',
  122. 'core' => array('8+'),
  123. 'aliases' => array('cli'),
  124. 'arguments' => array(
  125. 'prefix' => 'The config prefix. For example, "system". No prefix will return all names in the system.',
  126. ),
  127. 'examples' => array(
  128. 'drush config-list system' => 'Return a list of all system config names.',
  129. 'drush config-list "image.style"' => 'Return a list of all image styles.',
  130. 'drush config-list --format="json"' => 'Return all config names as json.',
  131. ),
  132. 'outputformat' => array(
  133. 'default' => 'list',
  134. 'pipe-format' => 'var_export',
  135. 'output-data-type' => 'format-list',
  136. ),
  137. );
  138. $items['config-edit'] = array(
  139. 'description' => 'Open a config file in a text editor. Edits are imported into active configration after closing editor.',
  140. 'core' => array('8+'),
  141. 'aliases' => array('cedit'),
  142. 'arguments' => array(
  143. 'config-name' => 'The config object name, for example "system.site".',
  144. ),
  145. 'options' => array(
  146. 'bg' => 'Run editor in the background. Does not work with editors such as `vi` that run in the terminal. Supresses config-import at the end.',
  147. 'file' => 'Import from a file instead of interactively editing a given config.',
  148. ),
  149. 'examples' => array(
  150. 'drush config-edit image.style.large' => 'Edit the image style configurations.',
  151. 'drush config-edit' => 'Choose a config file to edit.',
  152. 'drush config-edit --choice=2' => 'Edit the second file in the choice list.',
  153. 'drush --bg config-edit image.style.large' => 'Return to shell prompt as soon as the editor window opens.',
  154. ),
  155. );
  156. $items['config-merge'] = array(
  157. 'description' => 'Merge configuration data from two sites.',
  158. 'aliases' => array('cm'),
  159. 'required-arguments' => 1,
  160. 'arguments' => array(
  161. 'site' => 'Alias for the site containing the other configuration data to merge.',
  162. 'label' => "A config directory label (i.e. a key in \$config_directories array in settings.php). Defaults to 'staging'",
  163. ),
  164. 'options' => array(
  165. 'base' => 'The commit hash or tag for the base of the three-way merge operation. This should be the most recent commit that was deployed to the site specified in the first argument.',
  166. 'branch' => array(
  167. 'description' => 'A branch to use when doing the configuration merge. Optional. Default is to use a temporary branch.',
  168. 'example-value' => 'branch-name',
  169. ),
  170. 'message' => 'Commit comment for the merged configuration.',
  171. 'no-commit' => 'Do not commit the fetched configuration; leave the modified files unstaged.',
  172. 'tool' => array(
  173. 'description' => 'Specific tool to use with `git mergetool`. Use --tool=0 to prevent use of mergetool. Optional. Defaults to whatever tool is configured in git.',
  174. 'example-value' => 'kdiff3',
  175. ),
  176. 'fetch-only' => "Don't run `git mergetool`; fetch all configuration changes from both sites, and merge them onto the working branch. May result in unresolved merge conflicts.",
  177. 'git' => "Fetch changes from the other site using git instead of rsync.",
  178. 'remote' => array(
  179. 'description' => 'The remote git branch to use to fetch changes. Defaults to "origin".',
  180. 'example-value' => 'origin',
  181. ),
  182. 'temp' => array(
  183. 'description' => "Export destination site's configuration to a temporary directory.",
  184. 'example-value' => 'path',
  185. ),
  186. ),
  187. 'examples' => array(
  188. 'drush @dev config-merge @production' => 'Merge configuration changes from the production site with the configuration changes made on the development site.',
  189. 'drush @dev config-merge /path/to/drupal#sitefolder' => 'Merge configuration changes from the site indicated by the provided site specification.',
  190. ),
  191. 'topics' => array('docs-cm'),
  192. );
  193. return $items;
  194. }
  195. /**
  196. * Config list command callback
  197. *
  198. * @param string $prefix
  199. * The config prefix to retrieve, or empty to return all.
  200. */
  201. function drush_config_list($prefix = '') {
  202. $names = \Drupal::configFactory()->listAll($prefix);
  203. if (empty($names)) {
  204. // Just in case there is no config.
  205. if (!$prefix) {
  206. return drush_set_error(dt('No config storage names found.'));
  207. }
  208. else {
  209. return drush_set_error(dt('No config storage names found matching @prefix', array('@prefix' => $prefix)));
  210. }
  211. }
  212. return $names;
  213. }
  214. /**
  215. * Config get command callback.
  216. *
  217. * @param $config_name
  218. * The config name.
  219. * @param $key
  220. * The config key.
  221. */
  222. function drush_config_get($config_name, $key = NULL) {
  223. if (!isset($key)) {
  224. return drush_config_get_object($config_name);
  225. }
  226. else {
  227. return drush_config_get_value($config_name, $key);
  228. }
  229. }
  230. /**
  231. * Config set command callback.
  232. *
  233. * @param $config_name
  234. * The config name.
  235. * @param $key
  236. * The config key.
  237. * @param $data
  238. * The data to save to config.
  239. */
  240. function drush_config_set($config_name, $key = NULL, $data = NULL) {
  241. // This hidden option is a convenient way to pass a value without passing a key.
  242. $data = drush_get_option('value', $data);
  243. if (!isset($data)) {
  244. return drush_set_error('DRUSH_CONFIG_ERROR', dt('No config value specified.'));
  245. }
  246. $config = Drupal::configFactory()->getEditable($config_name);
  247. // Check to see if config key already exists.
  248. if ($config->get($key) === NULL) {
  249. $new_key = TRUE;
  250. }
  251. else {
  252. $new_key = FALSE;
  253. }
  254. // Special flag indicating that the value has been passed via STDIN.
  255. if ($data === '-') {
  256. $data = stream_get_contents(STDIN);
  257. }
  258. // Now, we parse the value.
  259. switch (drush_get_option('format', 'string')) {
  260. case 'yaml':
  261. $parser = new Parser();
  262. $data = $parser->parse($data, TRUE);
  263. }
  264. if (is_array($data) && drush_confirm(dt('Do you want to update or set multiple keys on !name config.', array('!name' => $config_name)))) {
  265. foreach ($data as $key => $value) {
  266. $config->set($key, $value);
  267. }
  268. return $config->save();
  269. }
  270. else {
  271. $confirmed = FALSE;
  272. if ($config->isNew() && drush_confirm(dt('!name config does not exist. Do you want to create a new config object?', array('!name' => $config_name)))) {
  273. $confirmed = TRUE;
  274. }
  275. elseif ($new_key && drush_confirm(dt('!key key does not exist in !name config. Do you want to create a new config key?', array('!key' => $key, '!name' => $config_name)))) {
  276. $confirmed = TRUE;
  277. }
  278. elseif (drush_confirm(dt('Do you want to update !key key in !name config?', array('!key' => $key, '!name' => $config_name)))) {
  279. $confirmed = TRUE;
  280. }
  281. if ($confirmed && !drush_get_context('DRUSH_SIMULATE')) {
  282. return $config->set($key, $data)->save();
  283. }
  284. }
  285. }
  286. function drush_config_export_validate() {
  287. if ($destination = drush_get_option('destination')) {
  288. $additional = array();
  289. $values = drush_sitealias_evaluate_path($destination, $additional, TRUE);
  290. if (!isset($values['path'])) {
  291. return drush_set_error('config_export_target', 'The destination directory could not be evaluated.');
  292. }
  293. $destination = $values['path'];
  294. drush_set_option('destination', $destination);
  295. if (!file_exists($destination)) {
  296. $parent = dirname($destination);
  297. if (!is_dir($parent)) {
  298. return drush_set_error('config_export_target', 'The destination parent directory does not exist.');
  299. }
  300. if (!is_writable($parent)) {
  301. return drush_set_error('config_export_target', 'The destination parent directory is not writable.');
  302. }
  303. }
  304. else {
  305. if (!is_dir($destination)) {
  306. return drush_set_error('config_export_target', 'The destination is not a directory.');
  307. }
  308. if (!is_writable($destination)) {
  309. return drush_set_error('config_export_target', 'The destination directory is not writable.');
  310. }
  311. }
  312. }
  313. }
  314. /**
  315. * Command callback: Export config to specified directory (usually staging).
  316. */
  317. function drush_config_export($destination = NULL) {
  318. global $config_directories;
  319. // Determine which target directory to use.
  320. if ($target = drush_get_option('destination')) {
  321. $destination_dir = $target;
  322. // It is important to be able to specify a destination directory that
  323. // does not exist yet, for exporting on remote systems
  324. drush_mkdir($destination_dir);
  325. }
  326. else {
  327. $choices = drush_map_assoc(array_keys($config_directories));
  328. unset($choices[CONFIG_ACTIVE_DIRECTORY]);
  329. if (!isset($destination) && count($choices) >= 2) {
  330. $destination = drush_choice($choices, 'Choose a destination.');
  331. if (empty($destination)) {
  332. return drush_user_abort();
  333. }
  334. }
  335. elseif (!isset($destination)) {
  336. $destination = CONFIG_STAGING_DIRECTORY;
  337. }
  338. $destination_dir = config_get_config_directory($destination);
  339. }
  340. // Prepare a new branch, if applicable
  341. $remote = drush_get_option('push', FALSE);
  342. $original_branch = FALSE;
  343. $branch = FALSE;
  344. if ($remote) {
  345. // Get the branch that we're on at the moment
  346. $result = drush_shell_cd_and_exec($destination_dir, 'git rev-parse --abbrev-ref HEAD');
  347. if (!$result) {
  348. return drush_set_error('DRUSH_CONFIG_EXPORT_NO_GIT', dt("The drush config-export command requires that the selected configuration directory !dir be under git revision control when using --commit or --push options.", array('!dir' => $destination_dir)));
  349. }
  350. $output = drush_shell_exec_output();
  351. $original_branch = $output[0];
  352. $branch = drush_get_option('branch', FALSE);
  353. if (!$branch) {
  354. $branch = $original_branch;
  355. }
  356. if ($branch != $original_branch) {
  357. // Switch to the working branch; create it if it does not exist.
  358. // We do NOT want to use -B here, as we do NOT want to reset the
  359. // branch if it already exists.
  360. $result = drush_shell_cd_and_exec($destination_dir, 'git checkout %s', $branch);
  361. if (!$result) {
  362. $result = drush_shell_cd_and_exec($destination_dir, 'git checkout -b %s', $branch);
  363. }
  364. }
  365. }
  366. // Do the actual config export operation
  367. $result = _drush_config_export($destination, $destination_dir, $branch);
  368. // Regardless of the result of the export, reset to our original branch.
  369. if ($branch != $original_branch) {
  370. drush_shell_cd_and_exec($destination_dir, 'git checkout %s', $original_branch);
  371. }
  372. return $result;
  373. }
  374. function _drush_config_export($destination, $destination_dir, $branch) {
  375. $commit = drush_get_option('commit');
  376. $comment = drush_get_option('message', FALSE);
  377. if (!$comment) {
  378. $comment = "Exported configuration.";
  379. }
  380. if (count(glob($destination_dir . '/*')) > 0) {
  381. // Retrieve a list of differences between the active and target configuration (if any).
  382. $target_storage = new FileStorage($destination_dir);
  383. /** @var \Drupal\Core\Config\StorageInterface $active_storage */
  384. $active_storage = Drupal::service('config.storage');
  385. $config_comparer = new StorageComparer($active_storage, $target_storage, Drupal::service('config.manager'));
  386. if (!$config_comparer->createChangelist()->hasChanges()) {
  387. return drush_log(dt('There are no changes to export.'), LogLevel::OK);
  388. }
  389. drush_print("The following configuration changes have been made since the last export:\n");
  390. $change_list = array();
  391. foreach ($config_comparer->getAllCollectionNames() as $collection) {
  392. $change_list[$collection] = $config_comparer->getChangelist(NULL, $collection);
  393. }
  394. // Print a table with changes in color, then re-generate again without
  395. // color to place in the commit comment.
  396. _drush_print_config_changes_table($change_list);
  397. $tbl = _drush_format_config_changes_table($change_list);
  398. $output = $tbl->getTable();
  399. if (!stristr(PHP_OS, 'WIN')) {
  400. $output = str_replace("\r\n", PHP_EOL, $output);
  401. }
  402. $comment .= "\n\n$output";
  403. if (!$commit && !drush_confirm(dt('The current contents of your export directory (!target) will be deleted.', array('!target' => $destination_dir)))) {
  404. return drush_user_abort();
  405. }
  406. // Only delete .yml files, and not .htaccess or .git.
  407. drush_scan_directory($destination_dir, '/\.yml$/', array('.', '..'), 'unlink');
  408. }
  409. // Write all .yml files.
  410. $source_storage = Drupal::service('config.storage');
  411. $destination_storage = new FileStorage($destination_dir);
  412. foreach ($source_storage->listAll() as $name) {
  413. $destination_storage->write($name, $source_storage->read($name));
  414. }
  415. // Export configuration collections.
  416. foreach (\Drupal::service('config.storage')->getAllCollectionNames() as $collection) {
  417. $source_storage = $source_storage->createCollection($collection);
  418. $destination_storage = $destination_storage->createCollection($collection);
  419. foreach ($source_storage->listAll() as $name) {
  420. $destination_storage->write($name, $source_storage->read($name));
  421. }
  422. }
  423. drush_log(dt('Configuration successfully exported to !target.', array('!target' => $destination_dir)), LogLevel::SUCCESS);
  424. drush_backend_set_result($destination_dir);
  425. // Commit and push, or add exported configuration if requested.
  426. $remote = drush_get_option('push', FALSE);
  427. if ($commit || $remote) {
  428. // There must be changed files at the destination dir; if there are not, then
  429. // we will skip the commit-and-push step
  430. $result = drush_shell_cd_and_exec($destination_dir, 'git status --porcelain .');
  431. if (!$result) {
  432. return drush_set_error('DRUSH_CONFIG_EXPORT_FAILURE', dt("`git status` failed."));
  433. }
  434. $uncommitted_changes = drush_shell_exec_output();
  435. if (!empty($uncommitted_changes)) {
  436. $result = drush_shell_cd_and_exec($destination_dir, 'git add -A .');
  437. if (!$result) {
  438. return drush_set_error('DRUSH_CONFIG_EXPORT_FAILURE', dt("`git add -A` failed."));
  439. }
  440. $comment_file = drush_save_data_to_temp_file($comment);
  441. $result = drush_shell_cd_and_exec($destination_dir, 'git commit --file=%s', $comment_file);
  442. if (!$result) {
  443. return drush_set_error('DRUSH_CONFIG_EXPORT_FAILURE', dt("`git commit` failed. Output:\n\n!output", array('!output' => implode("\n", drush_shell_exec_output()))));
  444. }
  445. if ($remote) {
  446. // Remote might be FALSE, if --push was not specified, or
  447. // it might be TRUE if --push was not given a value.
  448. if (!is_string($remote)) {
  449. $remote = 'origin';
  450. }
  451. $result = drush_shell_cd_and_exec($destination_dir, 'git push --set-upstream %s %s', $remote, $branch);
  452. if (!$result) {
  453. return drush_set_error('DRUSH_CONFIG_EXPORT_FAILURE', dt("`git push` failed."));
  454. }
  455. }
  456. }
  457. }
  458. elseif (drush_get_option('add')) {
  459. drush_shell_exec_interactive('git add -p %s', $destination_dir);
  460. }
  461. $values = array(
  462. 'destination' => $destination_dir,
  463. );
  464. return $values;
  465. }
  466. function drush_config_import_validate() {
  467. if ($source = drush_get_option('source')) {
  468. if (!file_exists($source)) {
  469. return drush_set_error('config_import_target', 'The source directory does not exist.');
  470. }
  471. if (!is_dir($source)) {
  472. return drush_set_error('config_import_target', 'The source is not a directory.');
  473. }
  474. }
  475. }
  476. /**
  477. * Command callback. Import from specified config directory (defaults to staging).
  478. */
  479. function drush_config_import($source = NULL) {
  480. global $config_directories;
  481. if ($target = drush_get_option('source')) {
  482. $source_dir = $target;
  483. }
  484. else {
  485. $choices = drush_map_assoc(array_keys($config_directories));
  486. unset($choices[CONFIG_ACTIVE_DIRECTORY]);
  487. if (!isset($source) && count($choices) >= 2) {
  488. $source= drush_choice($choices, 'Choose a source.');
  489. if (empty($source)) {
  490. return drush_user_abort();
  491. }
  492. }
  493. elseif (!isset($source)) {
  494. $source = CONFIG_STAGING_DIRECTORY;
  495. }
  496. $source_dir = config_get_config_directory($source);
  497. }
  498. // Retrieve a list of differences between the active and source configuration (if any).
  499. $source_storage = new FileStorage($source_dir);
  500. /** @var \Drupal\Core\Config\StorageInterface $active_storage */
  501. $active_storage = Drupal::service('config.storage');
  502. if (drush_get_option('partial', FALSE)) {
  503. // With partial imports, the comparison must only be made against configs
  504. // that exist in the source directory.
  505. $temp_active_storage = new FileStorage(drush_tempdir());
  506. foreach ($source_storage->listAll() as $name) {
  507. // Copy active storage to our temporary active store.
  508. if ($existing = $active_storage->read($name)) {
  509. $temp_active_storage->write($name, $existing);
  510. }
  511. }
  512. $active_storage = $temp_active_storage;
  513. }
  514. $config_comparer = new StorageComparer($source_storage, $active_storage, Drupal::service('config.manager'));
  515. if (!$config_comparer->createChangelist()->hasChanges()) {
  516. return drush_log(dt('There are no changes to import.'), LogLevel::OK);
  517. }
  518. if (drush_get_option('preview', 'list') == 'list') {
  519. $change_list = array();
  520. foreach ($config_comparer->getAllCollectionNames() as $collection) {
  521. $change_list[$collection] = $config_comparer->getChangelist(NULL, $collection);
  522. }
  523. _drush_print_config_changes_table($change_list);
  524. }
  525. else {
  526. $destination_dir = drush_tempdir();
  527. drush_invoke_process('@self', 'config-export', array(), array('destination' => $destination_dir));
  528. // @todo Can DiffFormatter produce a CLI pretty diff?
  529. drush_shell_exec('diff -x %s -u %s %s', '*.git', $destination_dir, $source_dir);
  530. $output = drush_shell_exec_output();
  531. drush_print(implode("\n", $output));
  532. }
  533. if (drush_confirm(dt('Import the listed configuration changes?'))) {
  534. if (drush_get_option('partial')) {
  535. // Partial imports require different processing.
  536. return drush_op('_drush_config_import_partial', $source_storage);
  537. }
  538. return drush_op('_drush_config_import', $config_comparer);
  539. }
  540. }
  541. // Copied from submitForm() at /core/modules/config/src/Form/ConfigSync.php
  542. function _drush_config_import(StorageComparer $storage_comparer) {
  543. $config_importer = new ConfigImporter(
  544. $storage_comparer,
  545. Drupal::service('event_dispatcher'),
  546. Drupal::service('config.manager'),
  547. Drupal::lock(),
  548. Drupal::service('config.typed'),
  549. Drupal::moduleHandler(),
  550. Drupal::service('module_installer'),
  551. Drupal::service('theme_handler'),
  552. Drupal::service('string_translation')
  553. );
  554. if ($config_importer->alreadyImporting()) {
  555. drush_log('Another request may be synchronizing configuration already.', LogLevel::WARNING);
  556. }
  557. else{
  558. try {
  559. $config_importer->import();
  560. drupal_flush_all_caches();
  561. drush_log('The configuration was imported successfully.', LogLevel::SUCCESS);
  562. }
  563. catch (ConfigException $e) {
  564. // Return a negative result for UI purposes. We do not differentiate
  565. // between an actual synchronization error and a failed lock, because
  566. // concurrent synchronizations are an edge-case happening only when
  567. // multiple developers or site builders attempt to do it without
  568. // coordinating.
  569. watchdog_exception('config_import', $e);
  570. return drush_set_error('config_import_fail', 'The import failed due to an error. Any errors have been logged.');
  571. }
  572. }
  573. }
  574. /**
  575. * Imports a partial set of configurations.
  576. */
  577. function _drush_config_import_partial(FileStorage $source) {
  578. /** @var \Drupal\Core\Config\StorageInterface $active_storage */
  579. $active_storage = Drupal::service('config.storage');
  580. foreach ($source->listAll() as $name) {
  581. $active_storage->write($name, $source->read($name));
  582. }
  583. }
  584. /**
  585. * Edit command callback.
  586. */
  587. function drush_config_edit($config_name = '') {
  588. if (empty($config_name) && $file = drush_get_option('file', FALSE)) {
  589. // If not provided, assume config name from the given file.
  590. $config_name = basename($file, '.yml');
  591. }
  592. // Identify and validate input.
  593. if ($config_name) {
  594. $config = Drupal::configFactory()->getEditable($config_name);
  595. if ($config->isNew()) {
  596. return drush_set_error(dt('Config !name does not exist', array('!name' => $config_name)));
  597. }
  598. }
  599. else {
  600. $config_names = \Drupal::configFactory()->listAll();
  601. $choice = drush_choice($config_names, 'Choose a configuration.');
  602. if (empty($choice)) {
  603. return drush_user_abort();
  604. }
  605. else {
  606. $config_name = $config_names[$choice];
  607. $config = Drupal::configFactory()->getEditable($config_name);
  608. }
  609. }
  610. $active_storage = $config->getStorage();
  611. $contents = $active_storage->read($config_name);
  612. $temp_storage = new FileStorage(drush_tempdir());
  613. if ($file) {
  614. $temp_storage->write($config_name, \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file)));
  615. // Show difference.
  616. $existing = new FileStorage(drush_tempdir());
  617. $existing->write($config_name, $contents);
  618. // @todo Can DiffFormatter produce a CLI pretty diff?
  619. drush_shell_exec('diff -u %s %s', $existing->getFilePath($config_name), $temp_storage->getFilePath($config_name));
  620. $output = drush_shell_exec_output();
  621. drush_print(implode("\n", $output));
  622. if (!drush_confirm(dt('Keep these changes?'))) {
  623. return drush_user_abort(dt('Config not edited.'));
  624. }
  625. }
  626. else {
  627. // Write tmp YAML file for editing.
  628. $temp_storage->write($config_name, $contents);
  629. // $filepath = drush_save_data_to_temp_file();
  630. $exec = drush_get_editor();
  631. drush_shell_exec_interactive($exec, $temp_storage->getFilePath($config_name));
  632. }
  633. // Perform import operation if user did not immediately exit editor.
  634. if (!drush_get_option('bg', FALSE)) {
  635. $new_data = $temp_storage->read($config_name);
  636. $temp_storage->delete($config_name);
  637. $config->setData($new_data);
  638. $config->save();
  639. }
  640. }
  641. function drush_config_merge($alias = '', $config_label = 'staging') {
  642. // Allow the user to provide a specific working branch to do live work on.
  643. $working_branch = drush_get_option('branch', FALSE);
  644. $working_branch_is_tmp = FALSE;
  645. if (!$working_branch) {
  646. $working_branch = 'drush-live-config-temp';
  647. $working_branch_is_tmp = TRUE;
  648. }
  649. // Use in log and commit messages
  650. $site_label = $alias;
  651. // If '$alias' is a 'sites' folder, then convert it into a site
  652. // specification, root#uri
  653. if (($alias[0] != '@') && is_dir(DRUPAL_ROOT . '/sites/' . $alias)) {
  654. $alias = DRUPAL_ROOT . "#$alias";
  655. }
  656. // Figure out what our base commit is going to be for this operation.
  657. $merge_info = array(
  658. 'base' => drush_get_option('base', FALSE),
  659. 'message' => drush_get_option('message', ''),
  660. 'commit' => !drush_get_option('no-commit', FALSE),
  661. 'git-transport' => drush_get_option('git', FALSE),
  662. 'remote' => drush_get_option('remote', 'origin'),
  663. 'tool' => drush_get_option('tool', ''),
  664. 'temp' => drush_get_option('temp', ''),
  665. 'config-label' => $config_label,
  666. 'live-site' => $alias,
  667. 'dev-site' => '@self',
  668. 'live-config' => $working_branch,
  669. 'dev-config' => 'drush-dev-config-temp',
  670. 'autodelete-live-config' => $working_branch_is_tmp,
  671. 'autodelete-dev-config' => TRUE,
  672. 'commit_needed' => FALSE,
  673. );
  674. $result = _drush_cm_get_initial_vcs_state($merge_info);
  675. if ($result === FALSE) {
  676. return FALSE;
  677. }
  678. $result = _drush_cm_prepare_for_export($merge_info);
  679. if ($result === FALSE) {
  680. return FALSE;
  681. }
  682. $result = _drush_cm_export_remote_configuration_before_merge($merge_info);
  683. if ($result === FALSE) {
  684. return FALSE;
  685. }
  686. // Copy the exported configuration from 'live-site', either via git pull or via rsync
  687. if ($merge_info['git-transport']) {
  688. $result = _drush_cm_copy_remote_configuration_via_git($merge_info);
  689. }
  690. else {
  691. $result = _drush_cm_copy_remote_configuration_via_rsync($merge_info);
  692. }
  693. if ($result === FALSE) {
  694. return FALSE;
  695. }
  696. // Exit if there were no changes from 'live-site'.
  697. if (empty($merge_info['changed_configuration_files'])) {
  698. drush_log(dt("No configuration changes on !site; nothing to do here.", array('!site' => $merge_info['live-site'])), 'ok');
  699. _drush_config_merge_cleanup($merge_info);
  700. return TRUE;
  701. }
  702. $result = _drush_cm_commit_transmitted_configuration_changes($merge_info);
  703. if ($result === FALSE) {
  704. return FALSE;
  705. }
  706. $result = _drush_cm_prepare_for_local_configuration_export($merge_info);
  707. if ($result === FALSE) {
  708. return FALSE;
  709. }
  710. $result = _drush_cm_export_local_configuration($merge_info);
  711. if ($result === FALSE) {
  712. return FALSE;
  713. }
  714. // Check to see if the export changed any files. If it did not, then
  715. // skip the merge, and process only the config pulled in from the other site.
  716. // TODO: This needs to be a diff against the base commit. In 'git' mode,
  717. // we probably want to just skip this test and always merge. Maybe always do this?
  718. $configuration_path = _drush_cm_get_configuration_path($merge_info);
  719. $result = drush_shell_cd_and_exec($configuration_path, 'git status --porcelain .');
  720. if (!$result) {
  721. return drush_set_error('DRUSH_CONFIG_MERGE_FAILURE', dt("`git status` failed."));
  722. }
  723. $changed_configuration_files = drush_shell_exec_output();
  724. if (empty($changed_configuration_files)) {
  725. drush_log(dt("No configuration changes on !site; no merge necessary.", array('!site' => $merge_info['dev-site'])), 'ok');
  726. }
  727. else {
  728. $result = _drush_cm_merge_local_and_remote_configurations($merge_info);
  729. }
  730. if ($result === FALSE) {
  731. return FALSE;
  732. }
  733. $result = _drush_cm_merge_to_original_branch($merge_info);
  734. return $result;
  735. }
  736. function _drush_cm_get_configuration_path(&$merge_info) {
  737. // Find the current configuration path
  738. if (!isset($merge_info['configuration_path'])) {
  739. // Get the configuration path from the local site.
  740. $merge_info['configuration_path'] = config_get_config_directory($merge_info['config-label']);
  741. }
  742. return $merge_info['configuration_path'];
  743. }
  744. function _drush_cm_get_remote_configuration_path(&$merge_info) {
  745. if (!isset($merge_info['remote_configuration_path'])) {
  746. $configdir_values = drush_invoke_process($merge_info['live-site'], 'drupal-directory', array('config-' . $merge_info['config-label']));
  747. $merge_info['remote_configuration_path'] = trim($configdir_values['output']);
  748. }
  749. return $merge_info['remote_configuration_path'];
  750. }
  751. function _drush_cm_get_initial_vcs_state(&$merge_info) {
  752. $configuration_path = _drush_cm_get_configuration_path($merge_info);
  753. // Is the selected configuration directory under git revision control? If not, fail.
  754. $result = drush_shell_cd_and_exec($configuration_path, 'git rev-parse --abbrev-ref HEAD');
  755. if (!$result) {
  756. return drush_set_error('DRUSH_CONFIG_MERGE_NO_GIT', dt("The drush config-merge command requires that the selected configuration directory !dir be under git revision control.", array('!dir' => $configuration_path)));
  757. }
  758. $output = drush_shell_exec_output();
  759. $original_branch = $output[0];
  760. drush_log(dt("Original branch is !branch", array('!branch' => $original_branch)), 'debug');
  761. $merge_info['original-branch'] = $original_branch;
  762. // Find the current sha-hash
  763. $result = drush_shell_cd_and_exec($configuration_path, 'git rev-parse HEAD');
  764. if (!$result) {
  765. return drush_set_error('DRUSH_CONFIG_MERGE_NO_GIT', dt("`git rev-parse HEAD` failed."));
  766. }
  767. $output = drush_shell_exec_output();
  768. $merge_info['original_hash'] = $output[0];
  769. // Fail if there are any uncommitted changes on the current branch
  770. // inside the configuration path.
  771. $result = drush_shell_cd_and_exec($configuration_path, 'git status --porcelain .');
  772. if (!$result) {
  773. return drush_set_error('DRUSH_CONFIG_MERGE_FAILURE', dt("`git status` failed."));
  774. }
  775. $uncommitted_changes = drush_shell_exec_output();
  776. if (!empty($uncommitted_changes)) {
  777. return drush_set_error('DRUSH_CONFIG_MERGE_UNCOMMITTED_CHANGES', dt("Working set has uncommitted changes; please commit or discard them before merging. `git stash` before `drush config-merge`, and `git stash pop` afterwards can be useful here.\n\n!changes", array('!changes' => $uncommitted_changes)));
  778. }
  779. }
  780. function _drush_cm_prepare_for_export(&$merge_info) {
  781. $configuration_path = _drush_cm_get_configuration_path($merge_info);
  782. // The git transport only works if both sites have the same config path, so look up the
  783. // remote config path to see if this is the case, and error out if it is not.
  784. if ($merge_info['git-transport']) {
  785. $remote_configuration_path = _drush_cm_get_remote_configuration_path($merge_info);
  786. // n.b. $configuration_path is a relative path, whereas drupal-directory will give us
  787. // an absolute path. We therefore compare only the ends of the strings.
  788. if ($configuration_path != substr(trim($remote_configuration_path), -strlen($configuration_path))) {
  789. return drush_set_error('CONFIG_MERGE_INCOMPATIBLE_PATHS', dt("The --git option only works when the configuration path is the same on the source and destination sites. On your source site, the configuration path is !remote; on the target site, it was !local. You must use the default transport mechanism (rsync).", array('!remote' => $configdir_values['output'], '!local' => $configuration_path)));
  790. }
  791. }
  792. // If the user did not supply a base commit, then we'll fill in
  793. // the current original hash as our base commit.
  794. if (!$merge_info['base']) {
  795. $merge_info['base'] = $merge_info['original_hash'];
  796. }
  797. // Decide how we are going to transfer the exported configuration.
  798. $merge_info['export_options'] = array();
  799. // Check to see if the user wants to use git to transfer the configuration changes;
  800. // if so, set up the appropriate options to pass along to config-export.
  801. if ($merge_info['git-transport']) {
  802. $merge_info['export_options']['push'] = TRUE;
  803. $merge_info['export_options']['remote'] = $merge_info['remote'];
  804. $merge_info['export_options']['branch'] = $merge_info['live-config'];
  805. }
  806. elseif ($merge_info['temp']) {
  807. if ($merge_info['temp'] === TRUE) {
  808. $merge_info['export_options']['destination'] = '%temp/config';
  809. }
  810. else {
  811. $merge_info['export_options']['destination'] = $merge_info['temp'];
  812. }
  813. }
  814. // In rsync mode, this is where we will copy from. (Skip this assignment if
  815. // someone already set up or looked up the remote path.)
  816. if (!isset($merge_info['remote_configuration_path'])) {
  817. $merge_info['remote_configuration_path'] = "%config-" . $merge_info['config-label'];
  818. }
  819. $merge_info['rsync_options'] = array('delete' => TRUE);
  820. // Make a temporary copy of our configuration directory, so that we
  821. // can record what changed after calling config-export and merging.
  822. $merge_info['original_configuration_files'] = drush_tempdir() . '/original';
  823. drush_copy_dir($configuration_path, $merge_info['original_configuration_files'], FILE_EXISTS_OVERWRITE);
  824. }
  825. function _drush_cm_export_remote_configuration_before_merge(&$merge_info) {
  826. // Run config-export on the live site.
  827. $values = drush_invoke_process($merge_info['live-site'], 'config-export', array($merge_info['config-label']), $merge_info['export_options']);
  828. if ($values['error_status']) {
  829. return drush_set_error('DRUSH_CONFIG_MERGE_CANNOT_EXPORT', dt("Could not export configuration for site !site", array('!site' => $merge_info['live-site'])));
  830. }
  831. // After we run config-export, we remember the path to the directory
  832. // where the exported configuration was written.
  833. if (!empty($values['object']) && ($merge_info['temp'])) {
  834. $merge_info['remote_configuration_path'] = $values['object'];
  835. // $merge_info['rsync_options']['remove-source-files'] = TRUE;
  836. }
  837. }
  838. function _drush_cm_copy_remote_configuration_via_git(&$merge_info) {
  839. $configuration_path = _drush_cm_get_configuration_path($merge_info);
  840. // If the config-export command worked, and exported changes, then this should
  841. // pull down the appropriate commit, which should change files in $configuration_path
  842. // (and nowhere else).
  843. $result = drush_shell_cd_and_exec($configuration_path, 'git pull %s %s', $merge_info['remote'], $merge_info['live-config']);
  844. if (!$result) {
  845. return drush_set_error('DRUSH_CONFIG_EXPORT_FAILURE', dt("`git pull` failed. Output:\n\n!output", array('!output' => implode("\n", drush_shell_exec_output()))));
  846. }
  847. $result = drush_shell_cd_and_exec($configuration_path, 'git checkout %s', $merge_info['live-config']);
  848. if (!$result) {
  849. return drush_set_error('DRUSH_CONFIG_MERGE_FAILURE', dt("Could not switch to working branch !b", array('!b' => $merge_info['live-config'])));
  850. }
  851. // Let's check to see if anything changed in the branch we just pulled over.
  852. $result = drush_shell_cd_and_exec($configuration_path, 'git diff-tree --no-commit-id --name-only -r HEAD %s .', $merge_info['original_hash']);
  853. if (!$result) {
  854. return drush_set_error('DRUSH_CONFIG_EXPORT_FAILURE', dt("`git diff-tree` failed."));
  855. }
  856. $merge_info['changed_configuration_files'] = drush_shell_exec_output();
  857. }
  858. function _drush_cm_copy_remote_configuration_via_rsync(&$merge_info) {
  859. $configuration_path = _drush_cm_get_configuration_path($merge_info);
  860. $remote_configuration_path = _drush_cm_get_remote_configuration_path($merge_info);
  861. // Create a new temporary branch to hold the configuration changes
  862. // from the site 'live-config'. The last parameter is the 'start point',
  863. // which is like checking out the specified sha-hash before creating the
  864. // branch.
  865. if ($merge_info['autodelete-live-config']) {
  866. $result = drush_shell_cd_and_exec($configuration_path, 'git checkout -B %s %s', $merge_info['live-config'], $merge_info['base']);
  867. if (!$result) {
  868. return drush_set_error('DRUSH_CONFIG_MERGE_FAILURE', dt("Could not create temporary branch !b", array('!b' => $merge_info['live-config'])));
  869. }
  870. }
  871. else {
  872. $result = drush_shell_cd_and_exec($configuration_path, 'git checkout -b %s', $merge_info['live-config']);
  873. }
  874. // We set the upstream branch as a service for the user, to help with
  875. // cleanup should this process end before completion. We skip this if
  876. // the branch already existed (i.e. with --branch option).
  877. if ($result) {
  878. drush_shell_cd_and_exec($configuration_path, 'git branch --set-upstream-to=%s', $merge_info['original-branch']);
  879. }
  880. // Copy the exported configuration files from 'live-site' via rsync and commit them
  881. $values = drush_invoke_process($merge_info['dev-site'], 'core-rsync', array($merge_info['live-site'] . ":$remote_configuration_path/", $merge_info['dev-site'] . ":$configuration_path/"), $merge_info['rsync_options']);
  882. if ($values['error_status']) {
  883. return drush_set_error('DRUSH_CONFIG_MERGE_RSYNC_FAILED', dt("Could not rsync from !live to !dev.", array('!live' => $merge_info['live-config'], '!dev' => $merge_info['dev-config'])));
  884. }
  885. // Commit the new changes to the branch prepared for @live. Exit with
  886. // "nothing to do" if there are no changes to be committed.
  887. $result = drush_shell_cd_and_exec($configuration_path, 'git status --porcelain .');
  888. if (!$result) {
  889. return drush_set_error('DRUSH_CONFIG_MERGE_FAILURE', dt("`git status` failed."));
  890. }
  891. $merge_info['changed_configuration_files'] = drush_shell_exec_output();
  892. $merge_info['commit_needed'] = TRUE;
  893. }
  894. function _drush_cm_commit_transmitted_configuration_changes(&$merge_info) {
  895. $configuration_path = _drush_cm_get_configuration_path($merge_info);
  896. // Commit the files brought over via rsync.
  897. if ($merge_info['commit_needed']) {
  898. $result = drush_shell_cd_and_exec($configuration_path, 'git add -A .');
  899. if (!$result) {
  900. return drush_set_error('DRUSH_CONFIG_MERGE_FAILURE', dt("`git add -A` failed."));
  901. }
  902. // Note that this commit will be `merge --squash`-ed away. We'll put in
  903. // a descriptive message to help users understand where it came from, if
  904. // they end up with dirty branches after an aborted run.
  905. $result = drush_shell_cd_and_exec($configuration_path, 'git commit -m %s', 'Drush config-merge exported configuration from ' . $merge_info['live-site'] . ' ' . $merge_info['message']);
  906. if (!$result) {
  907. return drush_set_error('DRUSH_CONFIG_MERGE_FAILURE', dt("`git commit` failed."));
  908. }
  909. }
  910. }
  911. function _drush_cm_prepare_for_local_configuration_export(&$merge_info) {
  912. $configuration_path = _drush_cm_get_configuration_path($merge_info);
  913. // Create a new temporary branch to hold the configuration changes
  914. // from the dev site ('@self').
  915. $result = drush_shell_cd_and_exec($configuration_path, 'git checkout -B %s %s', $merge_info['dev-config'], $merge_info['base']);
  916. if (!$result) {
  917. return drush_set_error('DRUSH_CONFIG_MERGE_FAILURE', dt("Could not create temporary branch !b", array('!b' => $merge_info['dev-config'])));
  918. }
  919. // We set the upstream branch as a service for the user, to help with
  920. // cleanup should this process end before completion.
  921. drush_shell_cd_and_exec($configuration_path, 'git branch --set-upstream-to=%s', $merge_info['original-branch']);
  922. }
  923. function _drush_cm_export_local_configuration(&$merge_info) {
  924. // Run drush @dev cex label
  925. $values = drush_invoke_process($merge_info['dev-site'], 'config-export', array($merge_info['config_label']));
  926. if ($values['error_status']) {
  927. return drush_set_error('DRUSH_CONFIG_MERGE_CANNOT_EXPORT', dt("Could not export configuration for site !site", array('!site' => $merge_info['dev-site'])));
  928. }
  929. }
  930. function _drush_cm_merge_local_and_remote_configurations(&$merge_info) {
  931. $configuration_path = _drush_cm_get_configuration_path($merge_info);
  932. $result = drush_shell_cd_and_exec($configuration_path, 'git add -A .');
  933. if (!$result) {
  934. return drush_set_error('DRUSH_CONFIG_MERGE_FAILURE', dt("`git add -A` failed."));
  935. }
  936. // Note that this commit will be `merge --squash`-ed away. We'll put in
  937. // a descriptive message to help users understand where it came from, if
  938. // they end up with dirty branches after an aborted run.
  939. $result = drush_shell_cd_and_exec($configuration_path, 'git commit -m %s', 'Drush config-merge exported configuration from ' . $merge_info['dev-site'] . ' ' . $merge_info['message']);
  940. if (!$result) {
  941. return drush_set_error('DRUSH_CONFIG_MERGE_FAILURE', dt("`git commit` failed."));
  942. }
  943. // git checkout live-config && git rebase dev-config.
  944. // This will put us back on the live-config branch,
  945. // merge in the changes from the temporary dev branch,
  946. // and rebase the live-config branch to include all of
  947. // the commits from the dev config branch.
  948. $result = drush_shell_cd_and_exec($configuration_path, 'git checkout %s && git rebase %s', $merge_info['live-config'], $merge_info['dev-config']);
  949. // We don't need the dev-config branch any more, so we'll get rid of
  950. // it right away, so there is less to clean up / hang around should
  951. // we happen to abort before everything is done.
  952. if ($merge_info['autodelete-dev-config']) {
  953. drush_shell_cd_and_exec($configuration_path, 'git branch -D %s 2>/dev/null', $merge_info['dev-config']);
  954. }
  955. // If there are MERGE CONFLICTS: prompt the user and run 3-way diff tool.
  956. $result = drush_shell_cd_and_exec($configuration_path, 'git status --porcelain .', $configuration_path);
  957. if (!$result) {
  958. return drush_set_error('DRUSH_CONFIG_MERGE_FAILURE', dt("`git status` failed."));
  959. }
  960. // Check to see if any line in the output starts with 'UU'.
  961. // This means "both sides updated" -- i.e. a conflict.
  962. $conflicting_configuration_changes = drush_shell_exec_output();
  963. $conflicting_files = array_reduce(
  964. $conflicting_configuration_changes,
  965. function($reduce, $item) use ($configuration_path) {
  966. if (substr($item,0,2) == "UU") {
  967. $reduce[] = str_replace($configuration_path . '/', '', substr($item, 3));
  968. }
  969. return $reduce;
  970. },
  971. array()
  972. );
  973. // Report on any conflicts found.
  974. if (!empty($conflicting_files)) {
  975. drush_print("\nCONFLICTS:\n");
  976. drush_print(implode("\n", $conflicting_files));
  977. drush_print("\n");
  978. }
  979. // Stop right here if the user specified --merge-only.
  980. if (drush_get_option('fetch-only', FALSE)) {
  981. drush_log(dt("Specified --fetch-only, so stopping here after the merge. Use `git checkout !b` to return to your original branch.", array('!b' => $merge_info['original-branch'])), 'ok');
  982. return TRUE;
  983. }
  984. // If there are any conflicts, run the merge tool.
  985. if (!empty($conflicting_files)) {
  986. if (!$merge_info['tool'] && ($merge_info['tool'] != '')) {
  987. // If --tool=0, then we will never run the merge tool
  988. return drush_set_error('DRUSH_CONFLICTS_NOT_MERGED', dt("There were conflicts that needed merging, but mergetool disabled via --tool option. Rolling back; run again with --fetch-only to stop prior to merge."));
  989. }
  990. $choice = 'mergetool';
  991. while ($choice == 'mergetool') {
  992. if (empty($merge_info['tool'])) {
  993. $result = drush_shell_cd_and_exec($configuration_path, 'git mergetool .');
  994. }
  995. else {
  996. $result = drush_shell_cd_and_exec($configuration_path, 'git mergetool --tool=%s .', $merge_info['tool']);
  997. }
  998. // There is no good way to tell what the result of 'git mergetool'
  999. // was.
  1000. //
  1001. // The documentation says that $result will be FALSE if the user
  1002. // quits without saving; however, in my experience, git mergetool
  1003. // hangs, and never returns if kdiff3 or meld exits without saving.
  1004. //
  1005. // We will not allow the user to continue if 'git mergetool' exits with
  1006. // an error. If there was no error, we will ask the user how to continue,
  1007. // since save and exit does not necessarily mean that the user was
  1008. // satisfied with the result of the merge.
  1009. $done = array();
  1010. if ($result) {
  1011. if ($merge_info['commit']) {
  1012. $done = array('done' => dt("All conflicts resolved! Commit changes, re-import configuration and exit."));
  1013. }
  1014. else {
  1015. $done = array('done' => dt("All conflicts resolved! Re-import configuration and exit with unstaged changes."));
  1016. }
  1017. }
  1018. $selections = $done + array(
  1019. 'abandon' => dt("Abandon merge; erase all work, and go back to original state."),
  1020. 'mergetool' => dt("Run mergetool again."),
  1021. );
  1022. $choice = drush_choice($selections, dt("Done with merge. What would you like to do next?"));
  1023. // If the user cancels, we must call drush_user_abort() for things to work right.
  1024. if ($choice === FALSE) {
  1025. return drush_user_abort();
  1026. }
  1027. // If there is an action function, then call it.
  1028. $fn = '_drush_config_merge_action_' . $choice;
  1029. if (function_exists($fn)) {
  1030. $choice = $fn($merge_info);
  1031. }
  1032. // If the action function returns TRUE or FALSE, then
  1033. // return with that result without taking further action.
  1034. if (is_bool($choice)) {
  1035. return $choice;
  1036. }
  1037. }
  1038. // Commit the results of the merge to the working branch. This
  1039. // commit will be squash-merged with the others below; if the
  1040. // --no-commit option was selected, the results of the squash-merge
  1041. // will remain unstaged.
  1042. $result = drush_shell_cd_and_exec($configuration_path, 'git add -A .');
  1043. if (!$result) {
  1044. return drush_set_error('DRUSH_CONFIG_MERGE_FAILURE', dt("`git add -A` failed."));
  1045. }
  1046. $result = drush_shell_cd_and_exec($configuration_path, 'git commit -m %s', 'Drush config-merge merge commit for ' . $merge_info['live-site']. ' configuration with ' . $merge_info['dev-site'] . ' configuration.');
  1047. if (!$result) {
  1048. return drush_set_error('DRUSH_CONFIG_MERGE_FAILURE', dt("`git commit` failed."));
  1049. }
  1050. }
  1051. }
  1052. function _drush_cm_merge_to_original_branch(&$merge_info) {
  1053. $configuration_path = _drush_cm_get_configuration_path($merge_info);
  1054. // Merge the results of the 3-way merge back to the original branch.
  1055. drush_shell_cd_and_exec($configuration_path, 'git checkout %s', $merge_info['original-branch']);
  1056. // Run 'git merge' and 'git commit' as separate operations, as 'git merge --squash'
  1057. // seems to ignore the --commit option.
  1058. $result = drush_shell_cd_and_exec($configuration_path, 'git merge --no-commit --squash %s', $merge_info['live-config']);
  1059. if (!$result) {
  1060. return drush_set_error('DRUSH_CONFIG_MERGE_FAILURE', dt("`git merge --squash` failed. Output:\n\n!output", array('!output' => implode("\n", drush_shell_exec_output()))));
  1061. }
  1062. // Re-import the merged changes into the database for the local site.
  1063. drush_set_option('strict', 0);
  1064. $result = drush_invoke('config-import', array($merge_info['config_label']));
  1065. if ($result === FALSE) {
  1066. // If there was an error, or nothing to import, return FALSE,
  1067. // signaling rollback.
  1068. return FALSE;
  1069. }
  1070. // Check to see if the merge resulted in any changed files.
  1071. // If there were no changes in dev, then there might not be
  1072. // anything to do here.
  1073. $result = drush_shell_cd_and_exec($configuration_path, 'git status --porcelain .');
  1074. if (!$result) {
  1075. return drush_set_error('DRUSH_CONFIG_MERGE_FAILURE', dt("`git status` failed."));
  1076. }
  1077. $files_changed_by_merge = drush_shell_exec_output();
  1078. // If there were any files changed in the merge, then import them and commit.
  1079. if (!empty($files_changed_by_merge)) {
  1080. if ($merge_info['commit']) {
  1081. if (empty($merge_info['message'])) {
  1082. // The 'dev-site' is probably just '@self', so we'll put the site-name
  1083. // in the comment, which hopefully will read okay
  1084. $config = Drupal::config('system.site');
  1085. $site_name = $config->get('name');
  1086. $merge_info['message'] = dt("Merged configuration from !live in !site", array('!live' => $merge_info['live-site'], '!site' => $site_name));
  1087. // Retrieve a list of differences between the active and target configuration (if any).
  1088. $target_storage = new FileStorage($configuration_path);
  1089. /** @var \Drupal\Core\Config\StorageInterface $active_storage */
  1090. $active_storage = new FileStorage($merge_info['original_configuration_files']);
  1091. $config_comparer = new StorageComparer($active_storage, $target_storage, Drupal::service('config.manager'));
  1092. if ($config_comparer->createChangelist()->hasChanges()) {
  1093. $change_list = array();
  1094. foreach ($config_comparer->getAllCollectionNames() as $collection) {
  1095. $change_list[$collection] = $config_comparer->getChangelist(NULL, $collection);
  1096. }
  1097. $tbl = _drush_format_config_changes_table($change_list);
  1098. $output = $tbl->getTable();
  1099. if (!stristr(PHP_OS, 'WIN')) {
  1100. $output = str_replace("\r\n", PHP_EOL, $output);
  1101. }
  1102. $merge_info['message'] .= "\n\n$output";
  1103. }
  1104. }
  1105. $comment_file = drush_save_data_to_temp_file($merge_info['message']);
  1106. $result = drush_shell_cd_and_exec($configuration_path, 'git commit --file=%s', $comment_file);
  1107. if (!$result) {
  1108. return drush_set_error('DRUSH_CONFIG_MERGE_FAILURE', dt("`git commit` failed."));
  1109. }
  1110. }
  1111. }
  1112. _drush_config_merge_cleanup($merge_info);
  1113. return TRUE;
  1114. }
  1115. /**
  1116. * If drush_config_merge() exits with an error, then Drush will
  1117. * call the rollback function, so that we can clean up. We call
  1118. * the cleanup function explicitly if we exit with no error.
  1119. */
  1120. function drush_config_merge_rollback() {
  1121. _drush_config_merge_cleanup(drush_get_context('DRUSH_CONFIG_MERGE_INFO'));
  1122. }
  1123. /**
  1124. * If the user wants to abandon the work of their merge, then
  1125. * clean up our temporary branches and return TRUE to cause
  1126. * the calling function to exit without committing.
  1127. */
  1128. function _drush_config_merge_action_abandon(&$merge_info) {
  1129. _drush_config_merge_cleanup($merge_info);
  1130. drush_log(dt("All changes erased."), 'ok');
  1131. return TRUE;
  1132. }
  1133. /* Helper functions */
  1134. /**
  1135. * Reset our state after a config-merge command
  1136. */
  1137. function _drush_config_merge_cleanup($merge_info) {
  1138. if (!empty($merge_info) && !empty($merge_info['configuration_path'])) {
  1139. $configuration_path = $merge_info['configuration_path'];
  1140. // If we are in the middle of a rebase, we must abort, or
  1141. // git will remember this state for a long time (that is,
  1142. // you can switch away from this branch and come back later,
  1143. // and you'll still be in a "rebasing" state.)
  1144. drush_shell_cd_and_exec($configuration_path, 'git rebase --abort');
  1145. // Violently delete any untracked files in the configuration path
  1146. // without prompting. This isn't as dangerous as it sounds;
  1147. // drush config-merge refuses to run if you have untracked files
  1148. // here, and you can get anything that Drush config-merge put here
  1149. // via `drush cex` (or just run config-merge again).
  1150. drush_shell_cd_and_exec($configuration_path, 'git clean -d -f .');
  1151. // Switch back to the branch we started on.
  1152. $result = drush_shell_cd_and_exec($configuration_path, 'git checkout %s', $merge_info['original-branch']);
  1153. if (!$result) {
  1154. drush_log(dt("Could not return to original branch !branch", array('!branch' => $merge_info['original-branch'])), 'warning');
  1155. }
  1156. // Delete our temporary branches
  1157. if ($merge_info['autodelete-live-config']) {
  1158. drush_shell_cd_and_exec($configuration_path, 'git branch -D %s 2>/dev/null', $merge_info['live-config']);
  1159. }
  1160. if ($merge_info['autodelete-dev-config']) {
  1161. drush_shell_cd_and_exec($configuration_path, 'git branch -D %s 2>/dev/null', $merge_info['dev-config']);
  1162. }
  1163. }
  1164. }
  1165. /**
  1166. * Show and return a config object
  1167. *
  1168. * @param $config_name
  1169. * The config object name.
  1170. */
  1171. function drush_config_get_object($config_name) {
  1172. $source = drush_get_option('source', 'active');
  1173. $include_overridden = drush_get_option('include-overridden', FALSE);
  1174. if ($include_overridden) {
  1175. // Displaying overrides only applies to active storage.
  1176. $config = \Drupal::config($config_name);
  1177. $data = $config->get();
  1178. }
  1179. elseif ($source == 'active') {
  1180. $config = \Drupal::service('config.storage');
  1181. $data = $config->read($config_name);
  1182. }
  1183. elseif ($source == 'staging') {
  1184. $config = \Drupal::service('config.storage.staging');
  1185. $data = $config->read($config_name);
  1186. }
  1187. else {
  1188. return drush_set_error(dt('Unknown value !value for config source.', array('!value' => $source)));
  1189. }
  1190. if ($data === FALSE) {
  1191. return drush_set_error(dt('Config !name does not exist in !source configuration.', array('!name' => $config_name, '!source' => $source)));
  1192. }
  1193. if (empty($data)) {
  1194. drush_log(dt('Config !name exists but has no data.', array('!name' => $config_name)), LogLevel::NOTICE);
  1195. return;
  1196. }
  1197. return $data;
  1198. }
  1199. /**
  1200. * Show and return a value from config system.
  1201. *
  1202. * @param $config_name
  1203. * The config name.
  1204. * @param $key
  1205. * The config key.
  1206. */
  1207. function drush_config_get_value($config_name, $key) {
  1208. $config = Drupal::config($config_name);
  1209. if ($config->isNew()) {
  1210. return drush_set_error(dt('Config !name does not exist', array('!name' => $config_name)));
  1211. }
  1212. $value = $config->get($key);
  1213. $returns[$config_name . ':' . $key] = $value;
  1214. if ($value === NULL) {
  1215. return drush_set_error('DRUSH_CONFIG_ERROR', dt('No matching key found in !name config.', array('!name' => $config_name)));
  1216. }
  1217. else {
  1218. return $returns;
  1219. }
  1220. }
  1221. /**
  1222. * Print a table of config changes.
  1223. *
  1224. * @param array $config_changes
  1225. * An array of changes keyed by collection.
  1226. */
  1227. function _drush_format_config_changes_table(array $config_changes, $use_color = FALSE) {
  1228. if (!$use_color) {
  1229. $red = "%s";
  1230. $yellow = "%s";
  1231. $green = "%s";
  1232. }
  1233. else {
  1234. $red = "\033[31;40m\033[1m%s\033[0m";
  1235. $yellow = "\033[1;33;40m\033[1m%s\033[0m";
  1236. $green = "\033[1;32;40m\033[1m%s\033[0m";
  1237. }
  1238. $rows = array();
  1239. $rows[] = array('Collection', 'Config', 'Operation');
  1240. foreach ($config_changes as $collection => $changes) {
  1241. foreach ($changes as $change => $configs) {
  1242. switch ($change) {
  1243. case 'delete':
  1244. $colour = $red;
  1245. break;
  1246. case 'update':
  1247. $colour = $yellow;
  1248. break;
  1249. case 'create':
  1250. $colour = $green;
  1251. break;
  1252. default:
  1253. $colour = "%s";
  1254. break;
  1255. }
  1256. foreach($configs as $config) {
  1257. $rows[] = array(
  1258. $collection,
  1259. $config,
  1260. sprintf($colour, $change)
  1261. );
  1262. }
  1263. }
  1264. }
  1265. $tbl = _drush_format_table($rows);
  1266. return $tbl;
  1267. }
  1268. /**
  1269. * Print a table of config changes.
  1270. *
  1271. * @param array $config_changes
  1272. * An array of changes keyed by collection.
  1273. */
  1274. function _drush_print_config_changes_table(array $config_changes) {
  1275. $tbl = _drush_format_config_changes_table($config_changes, !drush_get_context('DRUSH_NOCOLOR'));
  1276. $output = $tbl->getTable();
  1277. if (!stristr(PHP_OS, 'WIN')) {
  1278. $output = str_replace("\r\n", PHP_EOL, $output);
  1279. }
  1280. drush_print(rtrim($output));
  1281. return $tbl;
  1282. }
  1283. /**
  1284. * Command argument complete callback.
  1285. */
  1286. function config_config_get_complete() {
  1287. return _drush_config_names_complete();
  1288. }
  1289. /**
  1290. * Command argument complete callback.
  1291. */
  1292. function config_config_set_complete() {
  1293. return _drush_config_names_complete();
  1294. }
  1295. /**
  1296. * Command argument complete callback.
  1297. */
  1298. function config_config_view_complete() {
  1299. return _drush_config_names_complete();
  1300. }
  1301. /**
  1302. * Command argument complete callback.
  1303. */
  1304. function config_config_edit_complete() {
  1305. return _drush_config_names_complete();
  1306. }
  1307. /**
  1308. * Command argument complete callback.
  1309. */
  1310. function config_config_import_complete() {
  1311. return _drush_config_directories_complete();
  1312. }
  1313. /**
  1314. * Command argument complete callback.
  1315. */
  1316. function config_config_export_complete() {
  1317. return _drush_config_directories_complete();
  1318. }
  1319. /**
  1320. * Helper function for command argument complete callback.
  1321. *
  1322. * @return
  1323. * Array of available config directories.
  1324. */
  1325. function _drush_config_directories_complete() {
  1326. drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION);
  1327. global $config_directories;
  1328. return array('values' => array_keys($config_directories));
  1329. }
  1330. /**
  1331. * Helper function for command argument complete callback.
  1332. *
  1333. * @return
  1334. * Array of available config names.
  1335. */
  1336. function _drush_config_names_complete() {
  1337. drush_bootstrap_max();
  1338. return array('values' => $storage = \Drupal::service('config.storage')->listAll());
  1339. }