diff --git a/composer.json b/composer.json
index 0c743040..caed252e 100644
--- a/composer.json
+++ b/composer.json
@@ -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",
diff --git a/entity-command.php b/entity-command.php
index 0cecf202..cdad9235 100644
--- a/entity-command.php
+++ b/entity-command.php
@@ -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' );
diff --git a/features/post-revision.feature b/features/post-revision.feature
new file mode 100644
index 00000000..db80ee77
--- /dev/null
+++ b/features/post-revision.feature
@@ -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
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index 0df76141..6ab1a682 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -65,7 +65,7 @@
*/src/Network_Meta_Command\.php$
*/src/Network_Namespace\.php$
*/src/Option_Command\.php$
- */src/Post(_Block|_Meta|_Term|_Type)?_Command\.php$
+ */src/Post(_Block|_Meta|_Revision|_Term|_Type)?_Command\.php$
*/src/Signup_Command\.php$
*/src/Site(_Meta|_Option)?_Command\.php$
*/src/Term(_Meta)?_Command\.php$
diff --git a/src/Post_Revision_Command.php b/src/Post_Revision_Command.php
new file mode 100644
index 00000000..961cf646
--- /dev/null
+++ b/src/Post_Revision_Command.php
@@ -0,0 +1,186 @@
+
+ */
+ 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
+ *
+ *
+ * : 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
+ *
+ *
+ * : The 'from' revision ID or post ID.
+ *
+ * []
+ * : The 'to' revision ID or post ID. If not provided, compares with the current post.
+ *
+ * [--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 );
+ }
+}