From f919b1249bbc28f112bc6327c27b782050d3b361 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 12 Jan 2026 12:44:23 +0000
Subject: [PATCH 1/9] Initial plan
From 8f2b422088b37344588325b0664a2a5111a17069 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 12 Jan 2026 12:50:51 +0000
Subject: [PATCH 2/9] Add Post_Revision_Command with restore and diff
subcommands
Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>
---
composer.json | 3 +
entity-command.php | 1 +
features/post-revision.feature | 108 ++++++++++++++++++++++
phpcs.xml.dist | 2 +-
src/Post_Revision_Command.php | 159 +++++++++++++++++++++++++++++++++
5 files changed, 272 insertions(+), 1 deletion(-)
create mode 100644 features/post-revision.feature
create mode 100644 src/Post_Revision_Command.php
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..c827e126
--- /dev/null
+++ b/features/post-revision.feature
@@ -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
+
+ 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..f3ca23a7
--- /dev/null
+++ b/src/Post_Revision_Command.php
@@ -0,0 +1,159 @@
+fetcher = new PostFetcher();
+ }
+
+ /**
+ * 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 );
+
+ if ( false === $restored_post_id || null === $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. 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 ( ! property_exists( $from_revision, $field ) || ! property_exists( $to_revision, $field ) ) {
+ WP_CLI::error( "Invalid field '{$field}'." );
+ }
+
+ // Get the field values
+ $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 );
+ }
+}
From 98d85906af44c31fe7ee1b4bce5df029ac8cc4d4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 12 Jan 2026 12:53:56 +0000
Subject: [PATCH 3/9] Address code review feedback: validate fields properly
and remove unused property
Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>
---
src/Post_Revision_Command.php | 41 +++++++++++++++++++++++++++--------
1 file changed, 32 insertions(+), 9 deletions(-)
diff --git a/src/Post_Revision_Command.php b/src/Post_Revision_Command.php
index f3ca23a7..5d42de75 100644
--- a/src/Post_Revision_Command.php
+++ b/src/Post_Revision_Command.php
@@ -1,6 +1,5 @@
fetcher = new PostFetcher();
- }
+ /**
+ * Valid post fields that can be compared.
+ *
+ * @var array
+ */
+ 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.
@@ -122,11 +138,18 @@ public function diff( $args, $assoc_args ) {
}
// Validate field
- if ( ! property_exists( $from_revision, $field ) || ! property_exists( $to_revision, $field ) ) {
- WP_CLI::error( "Invalid field '{$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}." );
+ }
+ if ( ! isset( $to_revision->{$field} ) ) {
+ WP_CLI::error( "Field '{$field}' not found on revision/post {$to_id}." );
}
- // Get the field values
$left_string = $from_revision->{$field};
$right_string = $to_revision->{$field};
From 598c559c4736a7371387ebf4a642ab25e1c7466f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 12 Jan 2026 12:55:31 +0000
Subject: [PATCH 4/9] Fix error message display and simplify return value check
Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>
---
src/Post_Revision_Command.php | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/Post_Revision_Command.php b/src/Post_Revision_Command.php
index 5d42de75..41e8f7ee 100644
--- a/src/Post_Revision_Command.php
+++ b/src/Post_Revision_Command.php
@@ -70,7 +70,7 @@ public function restore( $args ) {
// Restore the revision
$restored_post_id = wp_restore_post_revision( $revision_id );
- if ( false === $restored_post_id || null === $restored_post_id ) {
+ if ( false === $restored_post_id ) {
WP_CLI::error( "Failed to restore revision {$revision_id}." );
}
@@ -147,7 +147,8 @@ public function diff( $args, $assoc_args ) {
WP_CLI::error( "Field '{$field}' not found on revision {$from_id}." );
}
if ( ! isset( $to_revision->{$field} ) ) {
- WP_CLI::error( "Field '{$field}' not found on revision/post {$to_id}." );
+ $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};
From af620a236d24ec18489252a0a4bb103fbcd62258 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 12 Jan 2026 12:57:53 +0000
Subject: [PATCH 5/9] Handle null return value from wp_restore_post_revision
and improve error handling
Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>
---
src/Post_Revision_Command.php | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/Post_Revision_Command.php b/src/Post_Revision_Command.php
index 41e8f7ee..7edcb6d0 100644
--- a/src/Post_Revision_Command.php
+++ b/src/Post_Revision_Command.php
@@ -70,7 +70,8 @@ public function restore( $args ) {
// Restore the revision
$restored_post_id = wp_restore_post_revision( $revision_id );
- if ( false === $restored_post_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}." );
}
@@ -146,6 +147,8 @@ public function diff( $args, $assoc_args ) {
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}." );
From 9b54f2f9c70aa69b143d7680b525ead5cd5c8bac Mon Sep 17 00:00:00 2001
From: Pascal Birchler
Date: Tue, 13 Jan 2026 12:27:47 +0100
Subject: [PATCH 6/9] Update src/Post_Revision_Command.php
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
src/Post_Revision_Command.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Post_Revision_Command.php b/src/Post_Revision_Command.php
index 7edcb6d0..1b1b7522 100644
--- a/src/Post_Revision_Command.php
+++ b/src/Post_Revision_Command.php
@@ -71,7 +71,7 @@ public function restore( $args ) {
$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 ) {
+ if ( false === $restored_post_id ) {
WP_CLI::error( "Failed to restore revision {$revision_id}." );
}
From 3e511a1395831c4bc815d222e286e5301b631666 Mon Sep 17 00:00:00 2001
From: Pascal Birchler
Date: Tue, 13 Jan 2026 12:28:03 +0100
Subject: [PATCH 7/9] Update src/Post_Revision_Command.php
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
src/Post_Revision_Command.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Post_Revision_Command.php b/src/Post_Revision_Command.php
index 1b1b7522..4f2de5db 100644
--- a/src/Post_Revision_Command.php
+++ b/src/Post_Revision_Command.php
@@ -87,7 +87,7 @@ public function restore( $args ) {
* : The 'from' revision ID or post ID.
*
* []
- * : The 'to' revision ID. If not provided, compares with the current post.
+ * : The 'to' revision ID or post ID. If not provided, compares with the current post.
*
* [--field=]
* : Compare specific field(s). Default: post_content
From acc29070b2ae6fc2dc6eaf18a146595f324cdb00 Mon Sep 17 00:00:00 2001
From: Pascal Birchler
Date: Tue, 13 Jan 2026 12:28:22 +0100
Subject: [PATCH 8/9] Update src/Post_Revision_Command.php
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
src/Post_Revision_Command.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Post_Revision_Command.php b/src/Post_Revision_Command.php
index 4f2de5db..961cf646 100644
--- a/src/Post_Revision_Command.php
+++ b/src/Post_Revision_Command.php
@@ -145,7 +145,7 @@ public function diff( $args, $assoc_args ) {
// 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}." );
+ 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
From c1c8cf91aa7e45697ffe8c09c916b3855ebd11bc Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 13 Jan 2026 11:30:43 +0000
Subject: [PATCH 9/9] Complete test scenario for diff between two revisions
Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com>
---
features/post-revision.feature | 19 ++++++++++++++++---
1 file changed, 16 insertions(+), 3 deletions(-)
diff --git a/features/post-revision.feature b/features/post-revision.feature
index c827e126..db80ee77 100644
--- a/features/post-revision.feature
+++ b/features/post-revision.feature
@@ -49,11 +49,24 @@ Feature: Manage WordPress post revisions
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 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} --format=ids --orderby=ID --order=ASC`
+ 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`