Skip to content
Draft
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
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
121 changes: 121 additions & 0 deletions features/post-revision.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
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 update {POST_ID} --post_content='Third 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=ids --orderby=ID --order=ASC`
Then STDOUT should not be empty
And save STDOUT as {REVISION_IDS}

When I run `echo "{REVISION_IDS}" | awk '{print $1}'`
Then save STDOUT as {REVISION_ID_1}

When I run `echo "{REVISION_IDS}" | awk '{print $2}'`
Then save STDOUT as {REVISION_ID_2}

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

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 ( false === $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 or post 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}." );
}
}
} 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 post/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