diff --git a/README.md b/README.md index 7cfa90e..6c24045 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # wp-cli-move -Sync your WordPress content (database and uploads) between stages using the power of WP-CLI aliases. +Sync your WordPress content (database, uploads, themes, plugins, mu-plugins, languages) between stages using the power of WP-CLI aliases. ## Install @@ -47,17 +47,17 @@ For more information about alias configuration, refer to the following WP-CLI do Depending on the sync direction, use either the `pull` or `push` commands. ```sh -wp move pull/push [] [--db] [--uploads] [--disable-compress] [--dry-run] +wp move pull/push [] [--db] [--uploads] [--themes] [--plugins] [--mu-plugins] [--languages] [--disable-compress] [--dry-run] ``` -If you omit the `--db` or `--uploads` flags, both data types will be synced by default. +If you omit the specific flags (--db, --uploads, --themes, --plugins, --mu-plugins, --languages), both database and uploads will be synced by default for backward compatibility. Note that the `` argument is optional. Configured aliases will be shown in a menu to choose from if left empty. > [!CAUTION] > Just like any tool that manipulates your data, it's **always a good idea to make a backup before running commands**. > -> Especially when syncing uploads, which uses the `rsync` command with the `--delete` flag under the hood and can wipe all your media files if used incorrectly. +> Especially when syncing content folders (uploads, themes, plugins, etc.), which uses the `rsync` command with the `--delete` flag under the hood and can wipe all your files if used incorrectly. > > **Be sure to know what you're doing.** @@ -68,6 +68,10 @@ Both `pull` and `push` commands use the same options. - `[]`: The alias you want to sync with. - `--db`: Sync only the database. - `--uploads`: Sync only the uploads. +- `--themes`: Sync only the themes. +- `--plugins`: Sync only the plugins. +- `--mu-plugins`: Sync only the mu-plugins. +- `--languages`: Sync only the languages. - `--disable-compress`: Disable database dump compression. - `--dry-run`: Print the command sequence without making any changes. @@ -95,6 +99,32 @@ Push your local content to your staging environment: wp move push staging ``` +### Syncing specific content types + +Pull only themes from production: + +```sh +wp move pull production --themes +``` + +Push only plugins to staging: + +```sh +wp move push staging --plugins +``` + +Sync multiple content types at once: + +```sh +wp move pull production --themes --plugins --mu-plugins +``` + +Sync everything (database, uploads, themes, plugins, mu-plugins, and languages): + +```sh +wp move pull production --db --uploads --themes --plugins --mu-plugins --languages +``` + ## Credits This WP-CLI package aims to replace the (still working but unmaintained) awesome [Wordmove](https://github.com/welaika/wordmove) Ruby gem 💎. It has been a time and life saver for many years. I'll be forever grateful to [@alessandro-fazzi](https://github.com/alessandro-fazzi) for creating it! 🙌 diff --git a/src/Model/Alias.php b/src/Model/Alias.php index 69a3e3e..019ea98 100644 --- a/src/Model/Alias.php +++ b/src/Model/Alias.php @@ -20,7 +20,8 @@ * port?: string, * path?: string, * } - * /** + */ +/** * @phpstan-import-type alias_config from MoveCommand */ final class Alias implements Stringable { @@ -136,9 +137,9 @@ public function get_ssh_url(): ?string { */ public function get_url(): string { if ( ! isset( $this->url ) ) { - $result = $this->run_wp( command: 'config get WP_HOME', quiet: true ); + $result = $this->run_wp( 'config get WP_HOME', false, true ); if ( ! $result->is_successful() ) { - $result = $this->run_wp( command: 'option get home', quiet: true ); + $result = $this->run_wp( 'option get home', false, true ); } $this->url = $result->stdout; } @@ -152,7 +153,47 @@ public function get_url(): string { * @return string */ public function get_upload_path(): string { - $result = $this->run_wp( command: 'eval "echo wp_get_upload_dir()[\'basedir\'] ?? \'\';"', quiet: true ); + $result = $this->run_wp( 'eval "echo wp_get_upload_dir()[\'basedir\'] ?? \'\';"', false, true ); + return $result->stdout; + } + + /** + * Get the themes path + * + * @return string + */ + public function get_themes_path(): string { + $result = $this->run_wp( 'eval "echo get_theme_root() ?? \'\';"', false, true ); + return $result->stdout; + } + + /** + * Get the plugins path + * + * @return string + */ + public function get_plugins_path(): string { + $result = $this->run_wp( 'eval "echo WP_PLUGIN_DIR ?? \'\';"', false, true ); + return $result->stdout; + } + + /** + * Get the mu-plugins path + * + * @return string + */ + public function get_mu_plugins_path(): string { + $result = $this->run_wp( 'eval "echo WPMU_PLUGIN_DIR ?? \'\';"', false, true ); + return $result->stdout; + } + + /** + * Get the languages path + * + * @return string + */ + public function get_languages_path(): string { + $result = $this->run_wp( 'eval "echo WP_LANG_DIR ?? \'\';"', false, true ); return $result->stdout; } @@ -385,9 +426,9 @@ private function dispatch_command( string $command, string $type = self::COMMAND case self::COMMAND_TYPE_RAW: /** @var ProcessRun $result */ $result = WP_CLI::launch( - command: $command, - exit_on_error: false, - return_detailed: true + $command, + false, + true ); break; default: @@ -396,10 +437,10 @@ private function dispatch_command( string $command, string $type = self::COMMAND } $process_result = new ProcessResult( - command: sprintf( '%s%s', self::COMMAND_TYPE_WP === $type ? 'wp ' : '', $result->command ), - exit_code: $result->return_code, - stdout: $result->stdout, - stderr: $result->stderr, + sprintf( '%s%s', self::COMMAND_TYPE_WP === $type ? 'wp ' : '', $result->command ), + $result->return_code, + $result->stdout, + $result->stderr, ); if ( ! $quiet ) { diff --git a/src/MoveCommand.php b/src/MoveCommand.php index d792523..8f44a8c 100644 --- a/src/MoveCommand.php +++ b/src/MoveCommand.php @@ -24,11 +24,19 @@ */ class MoveCommand { - private const DATA_TYPE_DB = 'db'; - private const DATA_TYPE_UPLOADS = 'uploads'; - private const DATA_TYPES = [ + private const DATA_TYPE_DB = 'db'; + private const DATA_TYPE_UPLOADS = 'uploads'; + private const DATA_TYPE_THEMES = 'themes'; + private const DATA_TYPE_PLUGINS = 'plugins'; + private const DATA_TYPE_MU_PLUGINS = 'mu-plugins'; + private const DATA_TYPE_LANGUAGES = 'languages'; + private const DATA_TYPES = [ self::DATA_TYPE_DB, self::DATA_TYPE_UPLOADS, + self::DATA_TYPE_THEMES, + self::DATA_TYPE_PLUGINS, + self::DATA_TYPE_MU_PLUGINS, + self::DATA_TYPE_LANGUAGES, ]; public const DEFAULT_MYSQLDUMP_ASSOC_ARGS = [ @@ -64,6 +72,18 @@ class MoveCommand { * [--uploads] * : Pull only the uploads folder. * + * [--themes] + * : Pull only the themes folder. + * + * [--plugins] + * : Pull only the plugins folder. + * + * [--mu-plugins] + * : Pull only the mu-plugins folder. + * + * [--languages] + * : Pull only the languages folder. + * * [--disable-compress] * : Disable database dump compression. * @@ -97,6 +117,18 @@ public function pull( array $args, array $assoc_args ): void { * [--uploads] * : Push only the uploads folder. * + * [--themes] + * : Push only the themes folder. + * + * [--plugins] + * : Push only the plugins folder. + * + * [--mu-plugins] + * : Push only the mu-plugins folder. + * + * [--languages] + * : Push only the languages folder. + * * [--disable-compress] * : Disable database dump compression. * @@ -143,6 +175,22 @@ private function sync( string $direction, ?string $alias, array $assoc_args ): v $this->sync_uploads( $from, $to, $dry_run ); } + if ( $data_types['themes'] ) { + $this->sync_themes( $from, $to, $dry_run ); + } + + if ( $data_types['plugins'] ) { + $this->sync_plugins( $from, $to, $dry_run ); + } + + if ( $data_types['mu-plugins'] ) { + $this->sync_mu_plugins( $from, $to, $dry_run ); + } + + if ( $data_types['languages'] ) { + $this->sync_languages( $from, $to, $dry_run ); + } + if ( $data_types['db'] ) { $this->{"{$direction}_db"}( $from, $to, ! $disable_compress, $dry_run ); } @@ -196,7 +244,7 @@ private function push_db( Alias $from, Alias $to, bool $compress = true, bool $d $from->export_db( $from, $from_dump_tmp, false, $dry_run ); // Search replace URLs - $this->replace_urls( from: $from, to: $to ); + $this->replace_urls( $from, $to ); // Export local DB with replaced URLs to remote $to_dump_tmp = $to->get_filename_tmp( false, true ); @@ -301,6 +349,115 @@ private function sync_uploads( Alias $from, Alias $to, bool $dry_run = false ): $this->log_section( sprintf( '%s %s', $to->is_local() ? '⬇️ Pulling uploads from' : '⬆️ Pushing uploads to', $remote ) ); + $this->sync_directory( $from, $to, $from_path, $to_path, $dry_run ); + } + + /** + * Rsync themes between two aliases + * + * @param Alias $from + * @param Alias $to + * @return void + */ + private function sync_themes( Alias $from, Alias $to, bool $dry_run = false ): void { + $remote = $from->is_local() ? $to : $from; + + // Get theme paths + $from_path = $from->get_themes_path(); + $to_path = $to->get_themes_path(); + + if ( ! $from_path || ! $to_path ) { + WP_CLI::error( 'Could not determine themes paths' ); + } + + $this->log_section( sprintf( '%s %s', $to->is_local() ? '⬇️ Pulling themes from' : '⬆️ Pushing themes to', $remote ) ); + + $this->sync_directory( $from, $to, $from_path, $to_path, $dry_run ); + } + + /** + * Rsync plugins between two aliases + * + * @param Alias $from + * @param Alias $to + * @return void + */ + private function sync_plugins( Alias $from, Alias $to, bool $dry_run = false ): void { + $remote = $from->is_local() ? $to : $from; + + // Get plugin paths + $from_path = $from->get_plugins_path(); + $to_path = $to->get_plugins_path(); + + if ( ! $from_path || ! $to_path ) { + WP_CLI::error( 'Could not determine plugins paths' ); + } + + $this->log_section( sprintf( '%s %s', $to->is_local() ? '⬇️ Pulling plugins from' : '⬆️ Pushing plugins to', $remote ) ); + + $this->sync_directory( $from, $to, $from_path, $to_path, $dry_run ); + } + + /** + * Rsync mu-plugins between two aliases + * + * @param Alias $from + * @param Alias $to + * @return void + */ + private function sync_mu_plugins( Alias $from, Alias $to, bool $dry_run = false ): void { + $remote = $from->is_local() ? $to : $from; + + // Get mu-plugin paths + $from_path = $from->get_mu_plugins_path(); + $to_path = $to->get_mu_plugins_path(); + + if ( ! $from_path || ! $to_path ) { + WP_CLI::error( 'Could not determine mu-plugins paths' ); + } + + $this->log_section( sprintf( '%s %s', $to->is_local() ? '⬇️ Pulling mu-plugins from' : '⬆️ Pushing mu-plugins to', $remote ) ); + + $this->sync_directory( $from, $to, $from_path, $to_path, $dry_run ); + } + + /** + * Rsync languages between two aliases + * + * @param Alias $from + * @param Alias $to + * @return void + */ + private function sync_languages( Alias $from, Alias $to, bool $dry_run = false ): void { + $remote = $from->is_local() ? $to : $from; + + // Get language paths + $from_path = $from->get_languages_path(); + $to_path = $to->get_languages_path(); + + if ( ! $from_path || ! $to_path ) { + WP_CLI::error( 'Could not determine languages paths' ); + } + + $this->log_section( sprintf( '%s %s', $to->is_local() ? '⬇️ Pulling languages from' : '⬆️ Pushing languages to', $remote ) ); + + $this->sync_directory( $from, $to, $from_path, $to_path, $dry_run ); + } + + /** + * Generic rsync directory sync between two aliases + * + * @param Alias $from + * @param Alias $to + * @param string $from_path + * @param string $to_path + * @param bool $dry_run + * @return void + */ + private function sync_directory( Alias $from, Alias $to, string $from_path, string $to_path, bool $dry_run = false ): void { + $remote = $from->is_local() ? $to : $from; + $local = $from->is_local() ? $from : $to; + $rsync_args = self::DEFAULT_RSYNC_ARGS; $rsync_args['rsh'] = $remote->generate_ssh_command( '' ); @@ -324,7 +481,19 @@ private function sync_uploads( Alias $from, Alias $to, bool $dry_run = false ): private function get_data_types( array $args ): array { $data_types = array_combine( self::DATA_TYPES, array_map( fn( string $type ): bool => (bool) Utils\get_flag_value( $args, $type, false ), self::DATA_TYPES ) ); - return count( array_filter( $data_types ) ) === 0 ? array_fill_keys( self::DATA_TYPES, true ) : $data_types; + // For backward compatibility, if no flags are provided, only sync db and uploads + if ( count( array_filter( $data_types ) ) === 0 ) { + return [ + self::DATA_TYPE_DB => true, + self::DATA_TYPE_UPLOADS => true, + self::DATA_TYPE_THEMES => false, + self::DATA_TYPE_PLUGINS => false, + self::DATA_TYPE_MU_PLUGINS => false, + self::DATA_TYPE_LANGUAGES => false, + ]; + } + + return $data_types; } /**