diff --git a/README.md b/README.md index dbb0b37..bbeb63e 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,11 @@ script in each project. --- [--filename-format=] - Use a custom format for archive filename. Defaults to '{name}.{version}'. - This is ignored if a custom filename is provided or version does not exist. + Use a custom format for archive filename. Available substitutions: {name}, {version}. + This is ignored if the parameter is provided or the version cannot be determined. + --- + default: "{name}.{version}" + --- ## Installing diff --git a/src/Dist_Archive_Command.php b/src/Dist_Archive_Command.php index 3115650..ac71d48 100644 --- a/src/Dist_Archive_Command.php +++ b/src/Dist_Archive_Command.php @@ -59,51 +59,20 @@ class Dist_Archive_Command { * --- * * [--filename-format=] - * : Use a custom format for archive filename. Defaults to '{name}.{version}'. - * This is ignored if a custom filename is provided or version does not exist. + * : Use a custom format for archive filename. Available substitutions: {name}, {version}. + * This is ignored if the parameter is provided or the version cannot be determined. + * --- + * default: "{name}.{version}" + * --- * * @when before_wp_load */ public function __invoke( $args, $assoc_args ) { - list( $path ) = $args; - $path = rtrim( realpath( $path ), '/' ); - if ( ! is_dir( $path ) ) { - WP_CLI::error( 'Provided input path is not a directory.' ); - } - - $this->checker = new GitIgnoreChecker( $path, '.distignore' ); - if ( isset( $args[1] ) ) { - // If the end of the string is a filename (file.ext), use it for the output archive filename. - if ( 1 === preg_match( '/^[a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?\.[a-zA-Z0-9_-]+$/', basename( $args[1] ) ) ) { - $archive_filename = basename( $args[1] ); + list( $source_dir_path, $destination_dir_path, $archive_file_name, $archive_output_dir_name ) = $this->get_file_paths_and_names( $args, $assoc_args ); - // If only the filename was supplied, use the plugin's parent directory for output. - if ( basename( $args[1] ) === $args[1] ) { - $archive_path = dirname( $path ); - } else { - // Otherwise use the supplied directory. - $archive_path = dirname( $args[1] ); - } - } else { - $archive_path = $args[1]; - $archive_filename = null; - } - } else { - if ( 0 !== strpos( $path, '/' ) ) { - $archive_path = dirname( getcwd() . '/' . $path ); - } else { - $archive_path = dirname( $path ); - } - $archive_filename = null; - } - - // If the path is not absolute, it is relative. - if ( 0 !== strpos( $archive_path, '/' ) ) { - $archive_path = rtrim( getcwd() . '/' . ltrim( $archive_path, '/' ), '/' ); - } - - $dist_ignore_filepath = $path . '/.distignore'; + $this->checker = new GitIgnoreChecker( $source_dir_path, '.distignore' ); + $dist_ignore_filepath = $source_dir_path . '/.distignore'; if ( file_exists( $dist_ignore_filepath ) ) { $file_ignore_rules = explode( PHP_EOL, file_get_contents( $dist_ignore_filepath ) ); } else { @@ -111,59 +80,12 @@ public function __invoke( $args, $assoc_args ) { $file_ignore_rules = []; } - $source_base = basename( $path ); - $archive_base = isset( $assoc_args['plugin-dirname'] ) ? rtrim( $assoc_args['plugin-dirname'], '/' ) : $source_base; - - $version = ''; - - /** - * If the path is a theme (meaning it contains a style.css file) - * parse the theme's version from the headers using a regex pattern. - * The pattern used is extracted from the get_file_data() function in core. - * - * @link https://developer.wordpress.org/reference/functions/get_file_data/ - */ - if ( file_exists( $path . '/style.css' ) ) { - $contents = file_get_contents( $path . '/style.css', false, null, 0, 5000 ); - $contents = str_replace( "\r", "\n", $contents ); - $pattern = '/^' . preg_quote( 'Version', ',' ) . ':(.*)$/mi'; - if ( preg_match( $pattern, $contents, $match ) && $match[1] ) { - $version = trim( preg_replace( '/\s*(?:\*\/|\?>).*/', '', $match[1] ) ); - } - } - - if ( empty( $version ) ) { - foreach ( glob( $path . '/*.php' ) as $php_file ) { - $contents = file_get_contents( $php_file, false, null, 0, 5000 ); - $version = $this->get_version_in_code( $contents ); - if ( ! empty( $version ) ) { - $version = trim( $version ); - break; - } - } - } - - if ( empty( $version ) && file_exists( $path . '/composer.json' ) ) { - $composer_obj = json_decode( file_get_contents( $path . '/composer.json' ) ); - if ( ! empty( $composer_obj->version ) ) { - $version = trim( $composer_obj->version ); - } - } - - if ( ! empty( $version ) && false !== stripos( $version, '-alpha' ) && is_dir( $path . '/.git' ) ) { - $response = WP_CLI::launch( "cd {$path}; git log --pretty=format:'%h' -n 1", false, true ); - $maybe_hash = trim( $response->stdout ); - if ( $maybe_hash && 7 === strlen( $maybe_hash ) ) { - $version .= '-' . $maybe_hash; - } - } - - if ( $archive_base !== $source_base || $this->is_path_contains_symlink( $path ) ) { - $tmp_dir = sys_get_temp_dir() . '/' . uniqid( "{$archive_base}.{$version}" ); - $new_path = $tmp_dir . DIRECTORY_SEPARATOR . $archive_base; + if ( basename( $source_dir_path ) !== $archive_output_dir_name || $this->is_path_contains_symlink( $source_dir_path ) ) { + $tmp_dir = sys_get_temp_dir() . '/' . uniqid( $archive_file_name ); + $new_path = "{$tmp_dir}/{$archive_output_dir_name}"; mkdir( $new_path, 0777, true ); - foreach ( $this->get_file_list( $path ) as $relative_filepath ) { - $source_item = $path . $relative_filepath; + foreach ( $this->get_file_list( $source_dir_path ) as $relative_filepath ) { + $source_item = $source_dir_path . $relative_filepath; if ( is_dir( $source_item ) ) { mkdir( "{$new_path}/{$relative_filepath}", 0777, true ); } else { @@ -172,28 +94,10 @@ public function __invoke( $args, $assoc_args ) { } $source_path = $new_path; } else { - $source_path = $path; + $source_path = $source_dir_path; } - if ( is_null( $archive_filename ) ) { - - if ( ! empty( $version ) ) { - if ( ! empty( $assoc_args['filename-format'] ) ) { - $archive_filename = str_replace( [ '{name}', '{version}' ], [ $archive_base, $version ], $assoc_args['filename-format'] ); - } else { - $archive_filename = $archive_base . '.' . $version; - } - } else { - $archive_filename = $archive_base; - } - - if ( 'zip' === $assoc_args['format'] ) { - $archive_filename .= '.zip'; - } elseif ( 'targz' === $assoc_args['format'] ) { - $archive_filename .= '.tar.gz'; - } - } - $archive_absolute_filepath = "{$archive_path}/{$archive_filename}"; + $archive_absolute_filepath = "{$destination_dir_path}/{$archive_file_name}"; if ( file_exists( $archive_absolute_filepath ) ) { WP_CLI::warning( 'Archive file already exists' ); @@ -214,24 +118,16 @@ public function __invoke( $args, $assoc_args ) { chdir( dirname( $source_path ) ); - if ( Utils\get_flag_value( $assoc_args, 'create-target-dir' ) ) { - $this->maybe_create_directory( $archive_absolute_filepath ); - } - - if ( ! is_dir( dirname( $archive_path ) ) ) { - WP_CLI::error( "Target directory does not exist: {$archive_path}" ); - } - // If the files are being zipped in place, we need the exclusion rules. // whereas if they were copied for any reasons above, the rules have already been applied. - if ( $source_path !== $path || empty( $file_ignore_rules ) ) { + if ( $source_path !== $source_dir_path || empty( $file_ignore_rules ) ) { if ( 'zip' === $assoc_args['format'] ) { - $cmd = "zip -r '{$archive_absolute_filepath}' {$archive_base}"; + $cmd = "zip -r '{$archive_absolute_filepath}' {$archive_output_dir_name}"; } elseif ( 'targz' === $assoc_args['format'] ) { - $cmd = "tar -zcvf {$archive_absolute_filepath} {$archive_base}"; + $cmd = "tar -zcvf {$archive_absolute_filepath} {$archive_output_dir_name}"; } } else { - $tmp_dir = sys_get_temp_dir() . '/' . uniqid( "{$archive_base}.{$version}" ); + $tmp_dir = sys_get_temp_dir() . '/' . uniqid( $archive_file_name ); mkdir( $tmp_dir, 0777, true ); if ( 'zip' === $assoc_args['format'] ) { $include_list_filepath = $tmp_dir . '/include-file-list.txt'; @@ -249,7 +145,7 @@ function ( $relative_path ) use ( $source_path ) { ) ) ); - $cmd = "zip -r '{$archive_absolute_filepath}' {$archive_base} -i@{$include_list_filepath}"; + $cmd = "zip -r '{$archive_absolute_filepath}' {$archive_output_dir_name} -i@{$include_list_filepath}"; } elseif ( 'targz' === $assoc_args['format'] ) { $exclude_list_filepath = "{$tmp_dir}/exclude-file-list.txt"; $excludes = array_filter( @@ -266,7 +162,7 @@ function ( $ignored_file ) use ( $source_path ) { trim( implode( "\n", $excludes ) ) ); $anchored_flag = ( php_uname( 's' ) === 'Linux' ) ? '--anchored ' : ''; - $cmd = "tar {$anchored_flag} --exclude-from={$exclude_list_filepath} -zcvf {$archive_absolute_filepath} {$archive_base}"; + $cmd = "tar {$anchored_flag} --exclude-from={$exclude_list_filepath} -zcvf {$archive_absolute_filepath} {$archive_output_dir_name}"; } } @@ -283,16 +179,150 @@ function ( $ignored_file ) use ( $source_path ) { } } + /** + * Determine the full paths and names to use from the CLI input. + * + * I.e. the source directory, the output directory, the output filename, and the directory name the archive will + * extract to. + * + * @param non-empty-array $args Source path (required), target (path or name, optional). + * @param array{format:string,filename-format:string,plugin-dirname?:string,create-target-dir?:bool} $assoc_args + * + * @return string[] $source_dir_path, $destination_dir_path, $destination_archive_name, $archive_output_dir_name + */ + private function get_file_paths_and_names( $args, $assoc_args ) { + + $source_dir_path = realpath( $args[0] ); + if ( ! is_dir( $source_dir_path ) ) { + WP_CLI::error( 'Provided input path is not a directory.' ); + } + + if ( isset( $args[1] ) ) { + $destination_input = $args[1]; + // If the end of the string is a filename (file.ext), use it for the output archive filename. + if ( 1 === preg_match( '/(zip$|tar$|tar.gz$)/', $destination_input ) ) { + $archive_file_name = basename( $destination_input ); + + // If only the filename was supplied, use the plugin's parent directory for output, otherwise use + // the supplied directory. + $destination_dir_path = basename( $destination_input ) === $destination_input + ? dirname( $source_dir_path ) + : dirname( $destination_input ); + + } else { + // Only a path was supplied, not a filename. + $destination_dir_path = $destination_input; + $archive_file_name = null; + } + } else { + // Use the plugin's parent directory for output. + $destination_dir_path = dirname( $source_dir_path ); + $archive_file_name = null; + } + + // Convert relative path to absolute path (check does it begin with e.g. "c:" or "/"). + if ( 1 !== preg_match( '/(^[a-zA-Z]+:|^\/)/', $destination_dir_path ) ) { + $destination_dir_path = getcwd() . '/' . $destination_dir_path; + } + + if ( Utils\get_flag_value( $assoc_args, 'create-target-dir' ) ) { + $this->maybe_create_directory( $destination_dir_path ); + } + + $destination_dir_path = realpath( $destination_dir_path ); + + if ( ! is_dir( $destination_dir_path ) ) { + WP_CLI::error( "Target directory does not exist: {$destination_dir_path}" ); + } + + // Use the optionally supplied plugin-dirname, or use the name of the directory containing the source files. + $archive_output_dir_name = isset( $assoc_args['plugin-dirname'] ) + ? rtrim( $assoc_args['plugin-dirname'], '/' ) + : basename( $source_dir_path ); + + if ( is_null( $archive_file_name ) ) { + $version = $this->get_version( $source_dir_path ); + + // If the version number has been found, substitute it into the filename-format template, or just use the name. + $archive_file_stem = ! empty( $version ) + ? str_replace( [ '{name}', '{version}' ], [ $archive_output_dir_name, $version ], $assoc_args['filename-format'] ) + : $archive_output_dir_name; + + $archive_file_name = 'zip' === $assoc_args['format'] + ? $archive_file_stem . '.zip' + : $archive_file_stem . '.tar.gz'; + } + + return [ $source_dir_path, $destination_dir_path, $archive_file_name, $archive_output_dir_name ]; + } + + /** + * Determine the plugin version from style.css, the main plugin .php file, or composer.json. + * + * Append the commit hash to `-alpha` versions. + * + * @param string $source_dir_path + * + * @return string + */ + private function get_version( $source_dir_path ) { + + $version = ''; + + /** + * If the path is a theme (meaning it contains a style.css file) + * parse the theme's version from the headers using a regex pattern. + * The pattern used is extracted from the get_file_data() function in core. + * + * @link https://developer.wordpress.org/reference/functions/get_file_data/ + */ + if ( file_exists( $source_dir_path . '/style.css' ) ) { + $contents = file_get_contents( $source_dir_path . '/style.css', false, null, 0, 5000 ); + $contents = str_replace( "\r", "\n", $contents ); + $pattern = '/^' . preg_quote( 'Version', ',' ) . ':(.*)$/mi'; + if ( preg_match( $pattern, $contents, $match ) && $match[1] ) { + $version = trim( preg_replace( '/\s*(?:\*\/|\?>).*/', '', $match[1] ) ); + } + } + + if ( empty( $version ) ) { + foreach ( glob( $source_dir_path . '/*.php' ) as $php_file ) { + $contents = file_get_contents( $php_file, false, null, 0, 5000 ); + $version = $this->get_version_in_code( $contents ); + if ( ! empty( $version ) ) { + $version = trim( $version ); + break; + } + } + } + + if ( empty( $version ) && file_exists( $source_dir_path . '/composer.json' ) ) { + $composer_obj = json_decode( file_get_contents( $source_dir_path . '/composer.json' ) ); + if ( ! empty( $composer_obj->version ) ) { + $version = trim( $composer_obj->version ); + } + } + + if ( ! empty( $version ) && false !== stripos( $version, '-alpha' ) && is_dir( $source_dir_path . '/.git' ) ) { + $response = WP_CLI::launch( "cd {$source_dir_path}; git log --pretty=format:'%h' -n 1", false, true ); + $maybe_hash = trim( $response->stdout ); + if ( $maybe_hash && 7 === strlen( $maybe_hash ) ) { + $version .= '-' . $maybe_hash; + } + } + + return $version; + } + /** * Create the directory for a target file if it does not exist yet. * - * @param string $archive_file Path and filename of the target file. + * @param string $destination_dir_path Directory path for the target file. * @return void */ - private function maybe_create_directory( $archive_file ) { - $directory = dirname( $archive_file ); - if ( ! is_dir( $directory ) ) { - mkdir( $directory, $mode = 0777, $recursive = true ); + private function maybe_create_directory( $destination_dir_path ) { + if ( ! is_dir( $destination_dir_path ) ) { + mkdir( $destination_dir_path, $mode = 0777, $recursive = true ); } } @@ -402,18 +432,18 @@ protected function escapeshellcmd( $cmd, $whitelist ) { * If the plugin contains a symlink, we will first copy it to a temp directory, potentially omitting any * symlinks that are excluded via the `.distignore` file, avoiding recursive loops as described in #57. * - * @param string $path The filepath to the directory to check. + * @param string $source_dir_path The path to the directory to check. * * @return bool */ - protected function is_path_contains_symlink( $path ) { + protected function is_path_contains_symlink( $source_dir_path ) { - if ( ! is_dir( $path ) ) { - throw new Exception( 'Path `' . $path . '` is not a directory' ); + if ( ! is_dir( $source_dir_path ) ) { + throw new Exception( 'Path `' . $source_dir_path . '` is not a directory' ); } $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator( $path, RecursiveDirectoryIterator::SKIP_DOTS ), + new RecursiveDirectoryIterator( $source_dir_path, RecursiveDirectoryIterator::SKIP_DOTS ), RecursiveIteratorIterator::SELF_FIRST ); @@ -434,21 +464,17 @@ protected function is_path_contains_symlink( $path ) { * * Exclude list should contain directory names when no files in that directory exist in the include list. * - * @param string $path Path to process. + * @param string $source_dir_path Path to process. * @param bool $excluded Whether to return the list of files to exclude. Default (false) returns the list of files to include. * @return string[] Filtered list of files to include or exclude (depending on $excluded flag). */ - private function get_file_list( $path, $excluded = false ) { + private function get_file_list( $source_dir_path, $excluded = false ) { $included_files = []; $excluded_files = []; - if ( ! is_dir( $path ) ) { - throw new Exception( "Path '{$path}' is not a directory." ); - } - $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator( $path, RecursiveDirectoryIterator::SKIP_DOTS ), + new RecursiveDirectoryIterator( $source_dir_path, RecursiveDirectoryIterator::SKIP_DOTS ), RecursiveIteratorIterator::SELF_FIRST ); @@ -457,7 +483,7 @@ private function get_file_list( $path, $excluded = false ) { * @var SplFileInfo $item */ foreach ( $iterator as $item ) { - $relative_filepath = str_replace( $path, '', $item->getPathname() ); + $relative_filepath = str_replace( $source_dir_path, '', $item->getPathname() ); if ( $this->checker->isPathIgnored( $relative_filepath ) ) { $excluded_files[] = $relative_filepath; } else { @@ -465,9 +491,9 @@ private function get_file_list( $path, $excluded = false ) { } } - // Check all excluded directories and remove the from the excluded list if they contain included files. + // Check all excluded directories and remove them from the excluded list if they contain included files. foreach ( $excluded_files as $excluded_file_index => $excluded_relative_path ) { - if ( ! is_dir( $path . $excluded_relative_path ) ) { + if ( ! is_dir( $source_dir_path . $excluded_relative_path ) ) { continue; } foreach ( $included_files as $included_relative_path ) {