From f1c06669322d9594d5b29b398762ff5893305609 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:01:33 +0000 Subject: [PATCH 1/5] Initial plan From 23cd649ef36373817fbf3b7b510f9a49f63da9af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:10:48 +0000 Subject: [PATCH 2/5] Add wp term prune command to delete terms with 0 or 1 published posts Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- composer.json | 1 + features/term-prune.feature | 137 ++++++++++++++++++++++++++++++++++++ src/Term_Command.php | 103 +++++++++++++++++++++++++++ 3 files changed, 241 insertions(+) create mode 100644 features/term-prune.feature diff --git a/composer.json b/composer.json index a5258111a..8ca40bac7 100644 --- a/composer.json +++ b/composer.json @@ -190,6 +190,7 @@ "term meta pluck", "term meta update", "term recount", + "term prune", "term update", "user", "user add-cap", diff --git a/features/term-prune.feature b/features/term-prune.feature new file mode 100644 index 000000000..6bb4dfae1 --- /dev/null +++ b/features/term-prune.feature @@ -0,0 +1,137 @@ +Feature: Prune unused taxonomy terms + + Background: + Given a WP install + + Scenario: Prune terms with no published posts + When I run `wp term create post_tag 'Unused Tag' --slug=unused-tag --porcelain` + Then STDOUT should be a number + And save STDOUT as {TERM_ID} + + When I run `wp term prune post_tag` + Then STDOUT should contain: + """ + Deleted post_tag {TERM_ID}. + """ + And STDOUT should contain: + """ + Success: + """ + And the return code should be 0 + + When I try `wp term get post_tag {TERM_ID}` + Then STDERR should contain: + """ + Error: Term doesn't exist. + """ + + Scenario: Does not prune terms with more than one published post + When I run `wp term create post_tag 'Popular Tag' --slug=popular-tag --porcelain` + Then STDOUT should be a number + And save STDOUT as {TERM_ID} + + When I run `wp post create --post_title='Post 1' --post_status=publish --porcelain` + Then STDOUT should be a number + And save STDOUT as {POST_ID_1} + + When I run `wp post create --post_title='Post 2' --post_status=publish --porcelain` + Then STDOUT should be a number + And save STDOUT as {POST_ID_2} + + When I run `wp post term set {POST_ID_1} post_tag {TERM_ID} --by=id` + Then STDOUT should not be empty + + When I run `wp post term set {POST_ID_2} post_tag {TERM_ID} --by=id` + Then STDOUT should not be empty + + When I run `wp term prune post_tag` + Then STDOUT should not contain: + """ + Deleted post_tag {TERM_ID}. + """ + + When I run `wp term get post_tag {TERM_ID} --field=name` + Then STDOUT should be: + """ + Popular Tag + """ + + Scenario: Prune terms with exactly one published post + When I run `wp term create post_tag 'Single Post Tag' --slug=single-post-tag --porcelain` + Then STDOUT should be a number + And save STDOUT as {TERM_ID} + + When I run `wp post create --post_title='Post 1' --post_status=publish --porcelain` + Then STDOUT should be a number + And save STDOUT as {POST_ID} + + When I run `wp post term set {POST_ID} post_tag {TERM_ID} --by=id` + Then STDOUT should not be empty + + When I run `wp term prune post_tag` + Then STDOUT should contain: + """ + Deleted post_tag {TERM_ID}. + """ + And the return code should be 0 + + When I try `wp term get post_tag {TERM_ID}` + Then STDERR should contain: + """ + Error: Term doesn't exist. + """ + + Scenario: Dry run previews terms without deleting them + When I run `wp term create post_tag 'Unused Tag' --slug=unused-tag --porcelain` + Then STDOUT should be a number + And save STDOUT as {TERM_ID} + + When I run `wp term prune post_tag --dry-run` + Then STDOUT should contain: + """ + Would delete post_tag {TERM_ID}. + """ + And STDOUT should contain: + """ + Success: + """ + And the return code should be 0 + + When I run `wp term get post_tag {TERM_ID} --field=name` + Then STDOUT should be: + """ + Unused Tag + """ + + Scenario: Prune with an invalid taxonomy + When I try `wp term prune nonexistent_taxonomy` + Then STDERR should be: + """ + Error: Taxonomy nonexistent_taxonomy doesn't exist. + """ + And the return code should be 1 + + Scenario: Prune multiple taxonomies at once + # Assign an extra post to the default Uncategorized category so its count + # exceeds the prune threshold and it won't interfere with the test. + When I run `wp post create --post_title='Extra Post' --post_status=publish --post_category=1 --porcelain` + Then STDOUT should be a number + + When I run `wp term create post_tag 'Unused Tag' --slug=unused-tag --porcelain` + Then STDOUT should be a number + And save STDOUT as {TAG_TERM_ID} + + When I run `wp term create category 'Unused Category' --slug=unused-category --porcelain` + Then STDOUT should be a number + And save STDOUT as {CAT_TERM_ID} + + When I run `wp term prune post_tag category` + Then STDOUT should contain: + """ + Deleted post_tag {TAG_TERM_ID}. + """ + And STDOUT should contain: + """ + Deleted category {CAT_TERM_ID}. + """ + And the return code should be 0 diff --git a/src/Term_Command.php b/src/Term_Command.php index 6547d834c..3e2560271 100644 --- a/src/Term_Command.php +++ b/src/Term_Command.php @@ -35,6 +35,11 @@ * Success: Updated category term count * Success: Updated post_tag term count * + * # Prune terms with 0 or 1 published posts + * $ wp term prune post_tag + * Deleted post_tag 15. + * Success: Pruned 1 of 5 terms. + * * @package wp-cli */ class Term_Command extends WP_CLI_Command { @@ -682,6 +687,104 @@ public function recount( $args ) { } } + /** + * Removes terms with 0 or 1 published posts from one or more taxonomies. + * + * Useful for cleaning up large sites with many unused or barely-used terms. + * The term count is based on the number of published posts assigned to each + * term. + * + * ## OPTIONS + * + * ... + * : One or more taxonomies to prune. + * + * [--dry-run] + * : Preview the terms to be pruned, without actually deleting them. + * + * ## EXAMPLES + * + * # Prune post tags with 0 or 1 published posts. + * $ wp term prune post_tag + * Deleted post_tag 15. + * Success: Pruned 1 of 5 terms. + * + * # Dry run to preview which terms would be pruned. + * $ wp term prune post_tag --dry-run + * Would delete post_tag 15. + * Success: 1 post_tag term would be pruned. + * + * # Prune multiple taxonomies at once. + * $ wp term prune category post_tag + * Deleted category 8. + * Success: Pruned 1 of 3 terms. + * Deleted post_tag 15. + * Success: Pruned 1 of 5 terms. + */ + public function prune( $args, $assoc_args ) { + foreach ( $args as $taxonomy ) { + if ( ! taxonomy_exists( $taxonomy ) ) { + WP_CLI::error( "Taxonomy {$taxonomy} doesn't exist." ); + } + } + + $dry_run = (bool) Utils\get_flag_value( $assoc_args, 'dry-run', false ); + + foreach ( $args as $taxonomy ) { + $terms = get_terms( + [ + 'taxonomy' => $taxonomy, + 'hide_empty' => false, + ] + ); + + // This should never happen because of the taxonomy_exists check above. + if ( is_wp_error( $terms ) ) { + WP_CLI::warning( "Could not retrieve terms for taxonomy {$taxonomy}." ); + continue; + } + + /** + * @var \WP_Term[] $terms + */ + + $total = 0; + $successes = 0; + $errors = 0; + + foreach ( $terms as $term ) { + if ( $term->count > 1 ) { + continue; + } + + ++$total; + + if ( $dry_run ) { + WP_CLI::log( "Would delete {$taxonomy} {$term->term_id}." ); + ++$successes; + continue; + } + + $result = wp_delete_term( $term->term_id, $taxonomy ); + + if ( is_wp_error( $result ) ) { + WP_CLI::warning( $result ); + ++$errors; + } elseif ( $result ) { + WP_CLI::log( "Deleted {$taxonomy} {$term->term_id}." ); + ++$successes; + } + } + + if ( $dry_run ) { + $term_word = Utils\pluralize( 'term', $successes ); + WP_CLI::success( "{$successes} {$taxonomy} {$term_word} would be pruned." ); + } else { + Utils\report_batch_operation_results( 'term', 'prune', $total, $successes, $errors ); + } + } + } + /** * Migrate a term of a taxonomy to another taxonomy. * From ffec9faa32169898522a9712e0ea91700380808f Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 2 Mar 2026 17:52:28 +0100 Subject: [PATCH 3/5] Update src/Term_Command.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Term_Command.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Term_Command.php b/src/Term_Command.php index 3e2560271..d84f43b20 100644 --- a/src/Term_Command.php +++ b/src/Term_Command.php @@ -773,6 +773,9 @@ public function prune( $args, $assoc_args ) { } elseif ( $result ) { WP_CLI::log( "Deleted {$taxonomy} {$term->term_id}." ); ++$successes; + } else { + WP_CLI::warning( "Failed to delete {$taxonomy} {$term->term_id}." ); + ++$errors; } } From 7140614dc21acc67c287e5eb05850918fdc46af4 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 2 Mar 2026 17:52:35 +0100 Subject: [PATCH 4/5] Update src/Term_Command.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Term_Command.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Term_Command.php b/src/Term_Command.php index d84f43b20..818acd66e 100644 --- a/src/Term_Command.php +++ b/src/Term_Command.php @@ -748,16 +748,17 @@ public function prune( $args, $assoc_args ) { * @var \WP_Term[] $terms */ - $total = 0; - $successes = 0; - $errors = 0; + $total = count( $terms ); + $prunable = 0; + $successes = 0; + $errors = 0; foreach ( $terms as $term ) { if ( $term->count > 1 ) { continue; } - ++$total; + ++$prunable; if ( $dry_run ) { WP_CLI::log( "Would delete {$taxonomy} {$term->term_id}." ); From be42053f0c6b5ca91ebed105502ab2677bdddc15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:57:12 +0000 Subject: [PATCH 5/5] Combine two foreach loops in prune() into a single loop Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- src/Term_Command.php | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Term_Command.php b/src/Term_Command.php index 818acd66e..4c051b216 100644 --- a/src/Term_Command.php +++ b/src/Term_Command.php @@ -722,15 +722,13 @@ public function recount( $args ) { * Success: Pruned 1 of 5 terms. */ public function prune( $args, $assoc_args ) { + $dry_run = (bool) Utils\get_flag_value( $assoc_args, 'dry-run', false ); + foreach ( $args as $taxonomy ) { if ( ! taxonomy_exists( $taxonomy ) ) { WP_CLI::error( "Taxonomy {$taxonomy} doesn't exist." ); } - } - $dry_run = (bool) Utils\get_flag_value( $assoc_args, 'dry-run', false ); - - foreach ( $args as $taxonomy ) { $terms = get_terms( [ 'taxonomy' => $taxonomy, @@ -748,18 +746,15 @@ public function prune( $args, $assoc_args ) { * @var \WP_Term[] $terms */ - $total = count( $terms ); - $prunable = 0; - $successes = 0; - $errors = 0; + $total = count( $terms ); + $successes = 0; + $errors = 0; foreach ( $terms as $term ) { if ( $term->count > 1 ) { continue; } - ++$prunable; - if ( $dry_run ) { WP_CLI::log( "Would delete {$taxonomy} {$term->term_id}." ); ++$successes;