Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ curl -o /tmp/wp-cli-stable.phar https://raw.githubusercontent.com/wp-cli/builds/
WP_CLI_PACKAGES_DIR=bin/packages WP_CLI_CONFIG_PATH=/dev/null wp handbook gen-all --stable-phar=/tmp/wp-cli-stable.phar
```

This also records the stable release in `bin/command-history.json`, which keeps track of the first bundled WP-CLI release in which each command and option appeared. Command pages show this as "Available since WP-CLI X.Y.Z" and "Options added later". This is the bundle release, not the version of the package that provides the command: if a package is updated on its own, a command can be available earlier. Nothing is shown for commands and options that were already part of the oldest recorded release (`baseline`).

To backfill the history from older releases, pass their Phars to `wp handbook update-command-history`. Releases can be given in any order, and the oldest release is kept for every command and option:

```
WP_CLI_CONFIG_PATH=/dev/null wp handbook update-command-history wp-cli-2.4.0.phar wp-cli-2.5.0.phar
```

The initial history was backfilled from all releases since 2.4.0, the oldest release that runs on PHP 8.

## Automated Handbook Generation

The handbook can be regenerated automatically using the "Regenerate Handbook" GitHub Actions workflow. This workflow can be triggered in two ways:
Expand Down
217 changes: 205 additions & 12 deletions bin/Handbook_Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ class Handbook_Command {
*/
const UNRELEASED_COMMAND_NOTE = 'This command is not part of a stable WP-CLI release yet.';

/**
* Commands that are not bundled with WP-CLI, but documented in the handbook.
*/
const NON_BUNDLED_COMMANDS = [ 'admin', 'find', 'profile', 'dist-archive' ];

/**
* Commands that are not documented in the handbook.
*/
const IGNORED_COMMANDS = [ 'website', 'handbook' ];

/**
* Version of the latest stable WP-CLI release, if a stable Phar was given.
*
Expand All @@ -37,6 +47,15 @@ class Handbook_Command {
*/
private static $stable_commands = null;

/**
* Releases in which commands and options first appeared, as stored in `bin/command-history.json`.
*
* `baseline` is the oldest recorded release. Anything available in it may be older.
*
* @var array{baseline: string|null, commands: array<string, array{since: string, generic: bool, options: array<string, string>}>}|null
*/
private static $command_history = null;

/**
* @return string
*/
Expand Down Expand Up @@ -280,16 +299,20 @@ public function gen_commands( $args, $assoc_args ) {
// Check non-bundled commands installed.
$runner = WP_CLI::get_runner();
$have_nonbundled_installed = true;
foreach ( [ 'admin', 'find', 'profile', 'dist-archive' ] as $cmd ) {
foreach ( self::NON_BUNDLED_COMMANDS as $cmd ) {
$have_nonbundled_installed = $have_nonbundled_installed && is_array( $runner->find_command_to_run( [ $cmd ] ) );
}
if ( ! $have_nonbundled_installed ) {
WP_CLI::error( sprintf( "Install non-bundled packages by running '%s' first.", 'bin/install_packages.sh' ) );
}

self::$command_history = self::load_command_history();

$stable_phar = Utils\get_flag_value( $assoc_args, 'stable-phar' );
if ( $stable_phar ) {
self::load_stable_reference( $stable_phar );
self::record_command_history( self::$command_history, (string) self::$stable_version, (array) self::$stable_commands );
self::save_command_history( self::$command_history );
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

self::empty_dir( self::get_handbook_path() . '/commands/' );
Expand All @@ -306,7 +329,7 @@ public function gen_commands( $args, $assoc_args ) {
$verbose = Utils\get_flag_value( $assoc_args, 'verbose', false );

foreach ( $wp['subcommands'] as $cmd ) {
if ( in_array( $cmd['name'], [ 'website', 'handbook' ], true ) ) {
if ( in_array( $cmd['name'], self::IGNORED_COMMANDS, true ) ) {
continue;
}
self::gen_cmd_pages( $cmd, [] /*parent*/, $verbose );
Expand All @@ -315,34 +338,82 @@ public function gen_commands( $args, $assoc_args ) {
WP_CLI::success( 'Generated all command pages.' );
}

/**
* Records in which releases commands and options first appeared.
*
* Updates `bin/command-history.json`, which is used to add "Available since" information to command pages.
* Releases can be given in any order. For each command and option, the oldest release is kept.
*
* Packages are skipped, so only bundled commands are recorded.
*
* ## OPTIONS
*
* <phar>...
* : Paths to Phars of stable WP-CLI releases.
*
* ## EXAMPLES
*
* $ wp handbook update-command-history wp-cli-2.11.0.phar wp-cli-2.12.0.phar
*
* @subcommand update-command-history
*
* @param string[] $args Paths to WP-CLI Phars.
* @return void
*/
public function update_command_history( $args ) {
$history = self::load_command_history();
foreach ( $args as $phar ) {
list( $version, $commands ) = self::dump_phar_commands( $phar, '--skip-packages' );
self::record_command_history( $history, $version, $commands );
WP_CLI::log( sprintf( 'Recorded WP-CLI %s (%d commands).', $version, count( $commands ) ) );
}
self::save_command_history( $history );
WP_CLI::success( sprintf( 'Updated bin/command-history.json with %d commands.', count( $history['commands'] ) ) );
}

/**
* Loads the commands and options available in the latest stable release.
*
* @param string $phar Path to the stable WP-CLI Phar.
* @return void
*/
private static function load_stable_reference( $phar ) {
list( self::$stable_version, self::$stable_commands ) = self::dump_phar_commands( $phar );
if ( ! preg_match( '/^\d+\.\d+\.\d+$/', self::$stable_version ) ) {
WP_CLI::error( 'Not a stable WP-CLI release: ' . self::$stable_version );
}

WP_CLI::log( sprintf( 'Comparing against WP-CLI %s (%d commands).', self::$stable_version, count( self::$stable_commands ) ) );
}

/**
* Gets the version and the commands of a WP-CLI Phar.
*
* @param string $phar Path to the WP-CLI Phar.
* @param string $global_args Global arguments to pass, e.g. `--skip-packages`.
* @return array{0: string, 1: array<string, array{options: string[], generic: bool}>} Version and flattened commands.
*/
private static function dump_phar_commands( $phar, $global_args = '' ) {
if ( ! is_file( $phar ) ) {
WP_CLI::error( "Stable Phar not found: {$phar}" );
WP_CLI::error( "Phar not found: {$phar}" );
}

$version = self::run_phar( $phar, 'cli version' );
if ( ! preg_match( '/^WP-CLI (\d+\.\d+\.\d+)$/', $version, $matches ) ) {
WP_CLI::error( "Not a stable WP-CLI release: {$version}" );
$version = self::run_phar( $phar, trim( "{$global_args} cli version" ) );
if ( ! preg_match( '/^WP-CLI (\S+)$/', $version, $matches ) ) {
WP_CLI::error( "Could not determine version of {$phar}: {$version}" );
}
self::$stable_version = $matches[1];

$dump = json_decode( self::run_phar( $phar, 'cli cmd-dump' ), true );
$dump = json_decode( self::run_phar( $phar, trim( "{$global_args} cli cmd-dump" ) ), true );
if ( ! is_array( $dump ) || ! isset( $dump['subcommands'] ) || ! is_array( $dump['subcommands'] ) ) {
WP_CLI::error( 'Could not dump commands of stable Phar.' );
WP_CLI::error( "Could not dump commands of {$phar}." );
}

self::$stable_commands = [];
$commands = [];
foreach ( $dump['subcommands'] as $cmd ) {
self::flatten_cmd_dump( $cmd, [], self::$stable_commands );
self::flatten_cmd_dump( $cmd, [], $commands );
}

WP_CLI::log( sprintf( 'Comparing against WP-CLI %s (%d commands).', self::$stable_version, count( self::$stable_commands ) ) );
return [ $matches[1], $commands ];
}

/**
Expand Down Expand Up @@ -475,6 +546,127 @@ private static function get_release_note( $full_name, $synopsis ) {
);
}

/**
* Loads the command history from `bin/command-history.json`.
*
* @return array{baseline: string|null, commands: array<string, array{since: string, generic: bool, options: array<string, string>}>}
*/
private static function load_command_history() {
$file = self::get_handbook_path() . '/bin/command-history.json';
$history = is_file( $file ) ? json_decode( (string) file_get_contents( $file ), true ) : null;
if ( ! is_array( $history ) ) {
return [
'baseline' => null,
'commands' => [],
];
}
return $history;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/**
* Saves the command history to `bin/command-history.json`.
*
* @param array{baseline: string|null, commands: array<string, array{since: string, generic: bool, options: array<string, string>}>} $history
* @return void
*/
private static function save_command_history( $history ) {
ksort( $history['commands'] );
$data = $history;
foreach ( $data['commands'] as $full_name => $entry ) {
// Keep options a JSON object even if empty.
$data['commands'][ $full_name ]['options'] = (object) $entry['options'];
}
file_put_contents(
self::get_handbook_path() . '/bin/command-history.json',
json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) . PHP_EOL
);
}

/**
* Records the commands and options of a release in the command history, keeping the oldest release for each.
*
* @param array{baseline: string|null, commands: array<string, array{since: string, generic: bool, options: array<string, string>}>} $history
* @param string $version Stable release version, e.g. `2.12.0`.
* @param array<string, array{options: string[], generic: bool}> $commands Flattened commands of that release.
* @return void
*/
private static function record_command_history( &$history, $version, $commands ) {
if ( ! preg_match( '/^\d+\.\d+\.\d+$/', $version ) ) {
WP_CLI::error( "Only stable releases can be recorded in the command history, got {$version}." );
}

$is_older = static function ( $since ) use ( $version ) {
return null === $since || version_compare( $version, $since, '<' );
};

if ( $is_older( $history['baseline'] ) ) {
$history['baseline'] = $version;
}

$skipped = array_merge( self::NON_BUNDLED_COMMANDS, self::IGNORED_COMMANDS );
foreach ( $commands as $full_name => $command ) {
if ( in_array( explode( ' ', $full_name )[0], $skipped, true ) ) {
continue;
}

$entry = isset( $history['commands'][ $full_name ] ) ? $history['commands'][ $full_name ] : [
'since' => $version,
'generic' => false,
'options' => [],
];

if ( $is_older( $entry['since'] ) ) {
$entry['since'] = $version;
}
$entry['generic'] = $entry['generic'] || $command['generic'];
foreach ( $command['options'] as $option ) {
if ( $is_older( isset( $entry['options'][ $option ] ) ? $entry['options'][ $option ] : null ) ) {
$entry['options'][ $option ] = $version;
}
}
ksort( $entry['options'] );

$history['commands'][ $full_name ] = $entry;
}
}

/**
* Builds a note about the releases in which a command and its options first appeared.
*
* Nothing is shown for commands and options that were already part of the oldest recorded release.
*
* @param string $full_name Full command name, e.g. `block type list`.
* @param string $synopsis Command synopsis.
* @return string Markdown note, or an empty string if there is nothing to show.
*/
private static function get_since_note( $full_name, $synopsis ) {
if ( empty( self::$command_history['commands'][ $full_name ] ) ) {
return '';
}

$entry = self::$command_history['commands'][ $full_name ];
$parts = [];

if ( $entry['since'] !== self::$command_history['baseline'] ) {
$parts[] = sprintf( 'Available since WP-CLI %s.', $entry['since'] );
}

// Commands accepting arbitrary options might have supported a newly documented option already.
if ( ! $entry['generic'] ) {
$later_options = [];
foreach ( self::get_option_names( $synopsis ) as $option ) {
if ( isset( $entry['options'][ $option ] ) && version_compare( $entry['options'][ $option ], $entry['since'], '>' ) ) {
$later_options[] = sprintf( '`%s` (%s)', $option, $entry['options'][ $option ] );
}
}
if ( $later_options ) {
$parts[] = sprintf( 'Options added later: %s.', implode( ', ', $later_options ) );
}
}

return implode( ' ', $parts );
}

/**
* Update the commands data array with new data
*
Expand Down Expand Up @@ -791,6 +983,7 @@ private static function gen_cmd_pages( $cmd, $parent = [], $verbose = false ) {
}
$binding['has-subcommands'] = isset( $cmd['subcommands'] ) ? [ true ] : false;
$binding['release-note'] = self::get_release_note( $binding['synopsis'], $cmd['synopsis'] ?? '' );
$binding['since-note'] = self::get_since_note( $binding['synopsis'], $cmd['synopsis'] ?? '' );

$hook_name = $cmd['hook'];
$hook_description = $hook_name ? Utils\get_hook_description( $hook_name ) : null;
Expand Down
Loading
Loading