diff --git a/features/dist-archive.feature b/features/dist-archive.feature
index ff4d809..ee8f500 100644
--- a/features/dist-archive.feature
+++ b/features/dist-archive.feature
@@ -494,3 +494,41 @@ Feature: Generate a distribution archive of a project
"""
And the {RUN_DIR}/subdir/hello-world-dist.zip file should exist
And the return code should be 0
+
+ Scenario: Uses version from plugin header when multiple versions are present
+ Given an empty directory
+ And a lifterlms/.distignore file:
+ """
+ """
+ And a lifterlms/lifterlms.php file:
+ """
+
-
+
diff --git a/src/Dist_Archive_Command.php b/src/Dist_Archive_Command.php
index f4c66dc..fafcf58 100644
--- a/src/Dist_Archive_Command.php
+++ b/src/Dist_Archive_Command.php
@@ -12,6 +12,8 @@ class Dist_Archive_Command {
*/
private $checker;
+ private Version_Tool $version_tool;
+
/**
* Create a distribution archive based on a project's .distignore file.
*
@@ -69,6 +71,8 @@ class Dist_Archive_Command {
*/
public function __invoke( $args, $assoc_args ) {
+ $this->version_tool = new Version_Tool();
+
list( $source_dir_path, $destination_dir_path, $archive_file_name, $archive_output_dir_name ) = $this->get_file_paths_and_names( $args, $assoc_args );
$this->checker = new GitIgnoreChecker( $source_dir_path, '.distignore' );
@@ -241,7 +245,7 @@ private function get_file_paths_and_names( $args, $assoc_args ) {
: basename( $source_dir_path );
if ( is_null( $archive_file_name ) ) {
- $version = $this->get_version( $source_dir_path );
+ $version = $this->version_tool->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 )
@@ -326,82 +330,6 @@ private function maybe_create_directory( $destination_dir_path ) {
}
}
- /**
- * Gets the content of a version tag in any doc block in the given source code string.
- *
- * The version tag might be specified as "@version x.y.z" or "Version: x.y.z" and it can
- * be preceded by an asterisk (*).
- *
- * @param string $code_str The source code string to look into.
- * @return null|string The detected version string.
- */
- private function get_version_in_code( $code_str ) {
- $tokens = array_values(
- array_filter(
- token_get_all( $code_str ),
- function ( $token ) {
- return ! is_array( $token ) || T_WHITESPACE !== $token[0];
- }
- )
- );
- foreach ( $tokens as $token ) {
- if ( T_DOC_COMMENT === $token[0] ) {
- $version = $this->get_version_in_docblock( $token[1] );
- if ( null !== $version ) {
- return $version;
- }
- }
- }
- return null;
- }
-
- /**
- * Gets the content of a version tag in a docblock.
- *
- * @param string $docblock Docblock to parse.
- * @return null|string The content of the version tag.
- */
- private function get_version_in_docblock( $docblock ) {
- $docblocktags = $this->parse_doc_block( $docblock );
- if ( isset( $docblocktags['version'] ) ) {
- return $docblocktags['version'];
- }
- return null;
- }
-
- /**
- * Parses a docblock and gets an array of tags with their values.
- *
- * The tags might be specified as "@version x.y.z" or "Version: x.y.z" and they can
- * be preceded by an asterisk (*).
- *
- * This code is based on the 'phpactor' package.
- * @see https://github.com/phpactor/docblock/blob/master/lib/Parser.php
- *
- * @param string $docblock Docblock to parse.
- * @return array Associative array of parsed data.
- */
- private function parse_doc_block( $docblock ) {
- $tag_documentor = '{@([a-zA-Z0-9-_\\\]+)\s*?(.*)?}';
- $tag_property = '{\s*\*?\s*(.*?):(.*)}';
- $lines = explode( PHP_EOL, $docblock );
- $tags = [];
-
- foreach ( $lines as $line ) {
- if ( 0 === preg_match( $tag_documentor, $line, $matches ) ) {
- if ( 0 === preg_match( $tag_property, $line, $matches ) ) {
- continue;
- }
- }
-
- $tag_name = strtolower( $matches[1] );
- $metadata = trim( isset( $matches[2] ) ? $matches[2] : '' );
-
- $tags[ $tag_name ] = $metadata;
- }
- return $tags;
- }
-
/**
* Run PHP's escapeshellcmd() then undo escaping known intentional characters.
*
diff --git a/src/Version_Tool.php b/src/Version_Tool.php
new file mode 100644
index 0000000..8ef8e1d
--- /dev/null
+++ b/src/Version_Tool.php
@@ -0,0 +1,119 @@
+).*/', '', $match[1] ) );
+ }
+ }
+
+ if ( empty( $version ) ) {
+ foreach ( glob( $path . '/*.php' ) as $php_file ) {
+ $headers = $this->get_file_data(
+ $php_file,
+ array(
+ 'name' => 'Plugin Name',
+ 'version' => 'Version',
+ )
+ );
+ if ( empty( $headers['name'] ) ) {
+ continue;
+ }
+ if ( ! empty( $headers['version'] ) ) {
+ $version = $headers['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;
+ }
+ }
+
+ return $version;
+ }
+
+ /**
+ * Retrieves metadata from a file.
+ *
+ * Modified slightly from WordPress 6.5.2 wp-includes/functions.php:6830
+ * @see get_file_data()
+ * @see https://github.com/WordPress/WordPress/blob/ddc3f387b5df4687f5b829119d0c0f797be674bf/wp-includes/functions.php#L6830-L6888
+ *
+ * Searches for metadata in the first 8 KB of a file, such as a plugin or theme.
+ * Each piece of metadata must be on its own line. Fields can not span multiple
+ * lines, the value will get cut at the end of the first line.
+ *
+ * @link https://codex.wordpress.org/File_Header
+ *
+ * @param string $file Absolute path to the file.
+ * @param array $all_headers List of headers, in the format `array( 'HeaderKey' => 'Header Name' )`.
+ * @return string[] Array of file header values keyed by header name.
+ */
+ private function get_file_data( string $file, array $all_headers ): array {
+
+ /**
+ * @see wp_initial_constants()
+ * `define( 'KB_IN_BYTES', 1024 );`
+ */
+ $kb_in_bytes = 1024;
+
+ // Pull only the first 8 KB of the file in.
+ $file_data = file_get_contents( $file, false, null, 0, 8 * $kb_in_bytes );
+
+ if ( false === $file_data ) {
+ $file_data = '';
+ }
+
+ // Make sure we catch CR-only line endings.
+ $file_data = str_replace( "\r", "\n", $file_data );
+
+ /**
+ * Strips close comment and close php tags from file headers used by WP.
+ *
+ * functions.php:6763
+ *
+ * @param string $str Header comment to clean up.
+ * @return string
+ */
+ $_cleanup_header_comment = function ( $str ) {
+ return trim( preg_replace( '/\s*(?:\*\/|\?>).*/', '', $str ) );
+ };
+
+ foreach ( $all_headers as $field => $regex ) {
+ if ( preg_match( '/^(?:[ \t]*<\?php)?[ \t\/*#@]*' . preg_quote( $regex, '/' ) . ':(.*)$/mi', $file_data, $match ) && $match[1] ) {
+ $all_headers[ $field ] = $_cleanup_header_comment( $match[1] );
+ } else {
+ $all_headers[ $field ] = '';
+ }
+ }
+
+ return $all_headers;
+ }
+}