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`