Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@
"post meta patch",
"post meta pluck",
"post meta update",
"post revision",
"post revision diff",
"post revision restore",
"post term",
"post term add",
"post term list",
Expand Down
1 change: 1 addition & 0 deletions entity-command.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
)
);
WP_CLI::add_command( 'post meta', 'Post_Meta_Command' );
WP_CLI::add_command( 'post revision', 'Post_Revision_Command' );
WP_CLI::add_command( 'post term', 'Post_Term_Command' );
WP_CLI::add_command( 'post-type', 'Post_Type_Command' );
WP_CLI::add_command( 'site', 'Site_Command' );
Expand Down
108 changes: 108 additions & 0 deletions features/post-revision.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
Feature: Manage WordPress post revisions

Background:
Given a WP install

Scenario: Restore a post revision
When I run `wp post create --post_title='Original Post' --post_content='Original content' --porcelain`
Then STDOUT should be a number
And save STDOUT as {POST_ID}

When I run `wp post update {POST_ID} --post_content='Updated content'`
Then STDOUT should contain:
"""
Success: Updated post {POST_ID}.
"""

When I run `wp post list --post_type=revision --post_parent={POST_ID} --fields=ID,post_title --format=ids`
Then STDOUT should not be empty
And save STDOUT as {REVISION_ID}

When I run `wp post revision restore {REVISION_ID}`
Then STDOUT should contain:
"""
Success: Restored revision
"""

When I run `wp post get {POST_ID} --field=post_content`
Then STDOUT should contain:
"""
Original content
"""

Scenario: Restore invalid revision should fail
When I try `wp post revision restore 99999`
Then STDERR should contain:
"""
Error: Invalid revision ID
"""
And the return code should be 1

Scenario: Show diff between two revisions
When I run `wp post create --post_title='Test Post' --post_content='First version' --porcelain`
Then STDOUT should be a number
And save STDOUT as {POST_ID}

When I run `wp post update {POST_ID} --post_content='Second version'`
Then STDOUT should contain:
"""
Success: Updated post {POST_ID}.
"""

When I run `wp post list --post_type=revision --post_parent={POST_ID} --fields=ID --format=csv --orderby=ID --order=ASC`
Then STDOUT should not be empty

When I run `wp post list --post_type=revision --post_parent={POST_ID} --format=ids --orderby=ID --order=ASC`
Then STDOUT should not be empty
Comment on lines +56 to +74
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test scenario is incomplete. It creates revisions and verifies they exist, but it never actually runs the wp post revision diff command to test the diff functionality between two revisions. Add test steps to execute the diff command with two revision IDs and verify the output.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c1c8cf9. The test scenario now:

  • Creates three revisions (two updates)
  • Extracts individual revision IDs from the list
  • Actually runs wp post revision diff with two revision IDs
  • Verifies the command executes successfully


Scenario: Show diff between revision and current post
When I run `wp post create --post_title='Diff Test' --post_content='Original text' --porcelain`
Then STDOUT should be a number
And save STDOUT as {POST_ID}

When I run `wp post update {POST_ID} --post_content='Modified text'`
Then STDOUT should contain:
"""
Success: Updated post {POST_ID}.
"""

When I run `wp post list --post_type=revision --post_parent={POST_ID} --fields=ID --format=ids --orderby=ID --order=ASC`
Then STDOUT should not be empty
And save STDOUT as {REVISION_ID}

When I run `wp post revision diff {REVISION_ID}`
Then the return code should be 0

Scenario: Diff with invalid revision should fail
When I try `wp post revision diff 99999`
Then STDERR should contain:
"""
Error: Invalid 'from' ID
"""
And the return code should be 1

Scenario: Diff between two invalid revisions should fail
When I try `wp post revision diff 99998 99999`
Then STDERR should contain:
"""
Error: Invalid 'from' ID
"""
And the return code should be 1

Scenario: Diff with specific field
When I run `wp post create --post_title='Field Test' --post_content='Some content' --porcelain`
Then STDOUT should be a number
And save STDOUT as {POST_ID}

When I run `wp post update {POST_ID} --post_title='Modified Field Test'`
Then STDOUT should contain:
"""
Success: Updated post {POST_ID}.
"""

When I run `wp post list --post_type=revision --post_parent={POST_ID} --fields=ID --format=ids --orderby=ID --order=ASC`
Then STDOUT should not be empty
And save STDOUT as {REVISION_ID}

When I run `wp post revision diff {REVISION_ID} --field=post_title`
Then the return code should be 0
2 changes: 1 addition & 1 deletion phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
<exclude-pattern>*/src/Network_Meta_Command\.php$</exclude-pattern>
<exclude-pattern>*/src/Network_Namespace\.php$</exclude-pattern>
<exclude-pattern>*/src/Option_Command\.php$</exclude-pattern>
<exclude-pattern>*/src/Post(_Block|_Meta|_Term|_Type)?_Command\.php$</exclude-pattern>
<exclude-pattern>*/src/Post(_Block|_Meta|_Revision|_Term|_Type)?_Command\.php$</exclude-pattern>
<exclude-pattern>*/src/Signup_Command\.php$</exclude-pattern>
<exclude-pattern>*/src/Site(_Meta|_Option)?_Command\.php$</exclude-pattern>
<exclude-pattern>*/src/Term(_Meta)?_Command\.php$</exclude-pattern>
Expand Down
186 changes: 186 additions & 0 deletions src/Post_Revision_Command.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
<?php

use WP_CLI\Utils;

/**
* Manages post revisions.
*
* ## EXAMPLES
*
* # Restore a post revision
* $ wp post revision restore 123
* Success: Restored revision 123.
*
* # Show diff between two revisions
* $ wp post revision diff 123 456
*
* @package wp-cli
*/
class Post_Revision_Command {

/**
* Valid post fields that can be compared.
*
* @var array<string>
*/
private $valid_fields = [
'post_title',
'post_content',
'post_excerpt',
'post_name',
'post_status',
'post_type',
'post_author',
'post_date',
'post_date_gmt',
'post_modified',
'post_modified_gmt',
'post_parent',
'menu_order',
'comment_status',
'ping_status',
];

/**
* Restores a post revision.
*
* ## OPTIONS
*
* <revision_id>
* : The revision ID to restore.
*
* ## EXAMPLES
*
* # Restore a post revision
* $ wp post revision restore 123
* Success: Restored revision 123.
*
* @subcommand restore
*/
public function restore( $args ) {
$revision_id = (int) $args[0];

// Get the revision post
$revision = wp_get_post_revision( $revision_id );

if ( ! $revision ) {
WP_CLI::error( "Invalid revision ID {$revision_id}." );
}

// Restore the revision
$restored_post_id = wp_restore_post_revision( $revision_id );

// wp_restore_post_revision() returns post ID on success, false on failure, or null if revision is same as current
if ( ! $restored_post_id ) {
WP_CLI::error( "Failed to restore revision {$revision_id}." );
}

WP_CLI::success( "Restored revision {$revision_id}." );
}

/**
* Shows the difference between two revisions.
*
* ## OPTIONS
*
* <from>
* : The 'from' revision ID or post ID.
*
* [<to>]
* : The 'to' revision ID. If not provided, compares with the current post.
*
* [--field=<field>]
* : Compare specific field(s). Default: post_content
*
* ## EXAMPLES
*
* # Show diff between two revisions
* $ wp post revision diff 123 456
*
* # Show diff between a revision and the current post
* $ wp post revision diff 123
*
* @subcommand diff
*/
public function diff( $args, $assoc_args ) {
$from_id = (int) $args[0];
$to_id = isset( $args[1] ) ? (int) $args[1] : null;
$field = Utils\get_flag_value( $assoc_args, 'field', 'post_content' );

// Get the 'from' revision or post
$from_revision = wp_get_post_revision( $from_id );
if ( ! $from_revision instanceof \WP_Post ) {
// Try as a regular post
$from_revision = get_post( $from_id );
if ( ! $from_revision instanceof \WP_Post ) {
WP_CLI::error( "Invalid 'from' ID {$from_id}." );
}
}

// Get the 'to' revision or post
$to_revision = null;
if ( $to_id ) {
$to_revision = wp_get_post_revision( $to_id );
if ( ! $to_revision instanceof \WP_Post ) {
// Try as a regular post
$to_revision = get_post( $to_id );
if ( ! $to_revision instanceof \WP_Post ) {
WP_CLI::error( "Invalid 'to' ID {$to_id}." );
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for fetching a post or revision object is duplicated for both $from_revision and $to_revision. Consider extracting this into a private helper method like get_post_or_revision( $id, $name ) to reduce code duplication and improve readability. The method could accept the ID and a name ('from' or 'to') for error messages.

} elseif ( 'revision' === $from_revision->post_type ) {
// If no 'to' ID provided, use the parent post of the revision
$to_revision = get_post( $from_revision->post_parent );
if ( ! $to_revision instanceof \WP_Post ) {
WP_CLI::error( "Could not find parent post for revision {$from_id}." );
}
} else {
WP_CLI::error( "Please provide a 'to' revision ID when comparing posts." );
}

// Validate field
if ( ! in_array( $field, $this->valid_fields, true ) ) {
WP_CLI::error( "Invalid field '{$field}'. Valid fields: " . implode( ', ', $this->valid_fields ) );
}

// Get the field values - use isset to check if field exists on the object
if ( ! isset( $from_revision->{$field} ) ) {
WP_CLI::error( "Field '{$field}' not found on revision {$from_id}." );
}

// $to_revision is guaranteed to be non-null at this point due to earlier validation
if ( ! isset( $to_revision->{$field} ) ) {
$to_error_id = $to_id ?? $to_revision->ID;
WP_CLI::error( "Field '{$field}' not found on revision/post {$to_error_id}." );
}

$left_string = $from_revision->{$field};
$right_string = $to_revision->{$field};

// Generate the diff
$diff_args = [
'title_left' => sprintf(
'%s (%s) - ID %d',
$from_revision->post_title,
$from_revision->post_modified,
$from_revision->ID
),
'title_right' => sprintf(
'%s (%s) - ID %d',
$to_revision->post_title,
$to_revision->post_modified,
$to_revision->ID
),
];

$diff = wp_text_diff( $left_string, $right_string, $diff_args );

if ( ! $diff ) {
WP_CLI::success( 'No difference found.' );
return;
}

// Output the diff
WP_CLI::line( $diff );
}
}
Loading