@@ -50,6 +50,7 @@ trait WorkspaceWorktreeCleanupEngine {
5050 public function worktree_cleanup_merged ( array $ opts = array () ): array |\WP_Error {
5151 $ dry_run = ! empty ($ opts ['dry_run ' ]);
5252 $ force = ! empty ($ opts ['force ' ]);
53+ $ discard_unpushed = ! empty ($ opts ['discard_unpushed ' ]);
5354 $ skip_github = ! empty ($ opts ['skip_github ' ]);
5455 $ direct_apply_plan = ! empty ($ opts ['direct_apply_plan ' ]);
5556 $ inventory_only = ! empty ($ opts ['inventory_only ' ]);
@@ -112,7 +113,7 @@ public function worktree_cleanup_merged( array $opts = array() ): array|\WP_Erro
112113 $ force = false ;
113114
114115 if ( $ direct_apply_plan && ! $ dry_run ) {
115- return $ this ->apply_worktree_cleanup_plan_candidates ($ planned_candidates , $ force , $ started_at , $ stale_liveness_only , $ remove_timeout_seconds );
116+ return $ this ->apply_worktree_cleanup_plan_candidates ($ planned_candidates , $ force , $ started_at , $ stale_liveness_only , $ remove_timeout_seconds, $ discard_unpushed );
116117 }
117118 }
118119
@@ -942,13 +943,14 @@ private function get_wp_error_data( \WP_Error $error ): mixed {
942943 * batch so the operator can keep going (next call without changes
943944 * re-derives the same list cheaply).
944945 *
945- * @param array $opts Options: dry_run, limit, older_than, sort, force, via_jobs, source.
946+ * @param array $opts Options: dry_run, limit, older_than, sort, force, discard_unpushed, via_jobs, source.
946947 * @return array<string,mixed>|\WP_Error
947948 */
948949 public function worktree_bounded_cleanup_eligible_apply ( array $ opts = array () ): array |\WP_Error {
949950 $ started_at = microtime (true );
950951 $ dry_run = ! empty ($ opts ['dry_run ' ]);
951952 $ force = ! empty ($ opts ['force ' ]);
953+ $ discard_unpushed = ! empty ($ opts ['discard_unpushed ' ]);
952954 $ via_jobs = ! empty ($ opts ['via_jobs ' ]);
953955 $ include_repaired_metadata = ! empty ($ opts ['include_repaired_metadata ' ]);
954956 $ older_than = isset ($ opts ['older_than ' ]) ? trim ( (string ) $ opts ['older_than ' ]) : '' ;
@@ -1020,28 +1022,30 @@ public function worktree_bounded_cleanup_eligible_apply( array $opts = array() )
10201022 ),
10211023 'continuation ' => $ continuation ,
10221024 'evidence ' => array (
1023- 'elapsed_ms ' => (int ) round (( microtime (true ) - $ started_at ) * 1000 ),
1024- 'inventory_total ' => count ($ all_candidates ),
1025- 'planned_handles ' => array_values (array_filter (array_map (fn ( $ row ) => is_array ($ row ) ? (string ) ( $ row ['handle ' ] ?? '' ) : '' , $ batch ))),
1026- 'remove_timeout ' => $ remove_timeout_seconds ,
1027- 'source ' => $ source ,
1025+ 'elapsed_ms ' => (int ) round (( microtime (true ) - $ started_at ) * 1000 ),
1026+ 'inventory_total ' => count ($ all_candidates ),
1027+ 'planned_handles ' => array_values (array_filter (array_map (fn ( $ row ) => is_array ($ row ) ? (string ) ( $ row ['handle ' ] ?? '' ) : '' , $ batch ))),
1028+ 'discard_unpushed ' => $ discard_unpushed ,
1029+ 'remove_timeout ' => $ remove_timeout_seconds ,
1030+ 'source ' => $ source ,
10281031 ),
10291032 );
10301033 }
10311034
10321035 if ( $ via_jobs ) {
1033- return $ this ->schedule_bounded_cleanup_eligible_chunks ($ batch , $ deferred , $ force , $ source , $ started_at , $ continuation , $ include_repaired_metadata , $ remove_timeout_seconds );
1036+ return $ this ->schedule_bounded_cleanup_eligible_chunks ($ batch , $ deferred , $ force , $ source , $ started_at , $ continuation , $ include_repaired_metadata , $ remove_timeout_seconds, $ discard_unpushed );
10341037 }
10351038
1036- $ processed = 0 ;
1037- $ removed = array ();
1038- $ skipped = $ inventory_skipped ;
1039- $ bytes_reclaimed = 0 ;
1040- $ timeout_handles = array ();
1039+ $ processed = 0 ;
1040+ $ removed = array ();
1041+ $ skipped = $ inventory_skipped ;
1042+ $ bytes_reclaimed = 0 ;
1043+ $ timeout_handles = array ();
1044+ $ discarded_unpushed = array ();
10411045
10421046 foreach ( $ batch as $ candidate ) {
10431047 ++$ processed ;
1044- $ revalidated = $ this ->revalidate_bounded_cleanup_eligible_candidate ($ candidate , $ force );
1048+ $ revalidated = $ this ->revalidate_bounded_cleanup_eligible_candidate ($ candidate , $ force, false , $ discard_unpushed );
10451049 if ( isset ($ revalidated ['skipped ' ]) ) {
10461050 $ skipped [] = $ revalidated ['skipped ' ];
10471051 continue ;
@@ -1090,17 +1094,32 @@ function () use ( $repo, $branch, $wt_path, $force, $remove_timeout_seconds ) {
10901094 continue ;
10911095 }
10921096
1093- $ removed [] = array_merge (
1097+ $ unpushed_count = (int ) ( $ validated ['unpushed ' ] ?? 0 );
1098+ $ removed_row = array_merge (
10941099 array (
1095- 'handle ' => (string ) ( $ candidate ['handle ' ] ?? '' ),
1096- 'repo ' => $ repo ,
1097- 'branch ' => $ branch ,
1098- 'path ' => $ wt_path ,
1099- 'size_bytes ' => $ size ,
1100- 'reason_code ' => 'cleanup_eligible ' ,
1100+ 'handle ' => (string ) ( $ candidate ['handle ' ] ?? '' ),
1101+ 'repo ' => $ repo ,
1102+ 'branch ' => $ branch ,
1103+ 'path ' => $ wt_path ,
1104+ 'size_bytes ' => $ size ,
1105+ 'reason_code ' => 'cleanup_eligible ' ,
1106+ 'unpushed_before_remove ' => $ unpushed_count ,
1107+ 'discarded_unpushed_commits ' => $ discard_unpushed && $ unpushed_count > 0 ,
1108+ 'path_exists_after ' => is_dir ($ wt_path ),
11011109 ),
11021110 is_array ($ candidate ['metadata ' ] ?? null ) ? array ( 'metadata ' => $ candidate ['metadata ' ] ) : array ()
11031111 );
1112+ $ removed [] = $ removed_row ;
1113+ if ( $ discard_unpushed && $ unpushed_count > 0 ) {
1114+ $ discarded_unpushed [] = array (
1115+ 'handle ' => (string ) ( $ candidate ['handle ' ] ?? '' ),
1116+ 'repo ' => $ repo ,
1117+ 'branch ' => $ branch ,
1118+ 'path ' => $ wt_path ,
1119+ 'unpushed_before_remove ' => $ unpushed_count ,
1120+ 'path_exists_after ' => is_dir ($ wt_path ),
1121+ );
1122+ }
11041123 $ bytes_reclaimed += max (0 , $ size );
11051124 }
11061125
@@ -1124,20 +1143,23 @@ function () use ( $repo, $branch, $wt_path, $force, $remove_timeout_seconds ) {
11241143 'removed ' => $ removed ,
11251144 'skipped ' => $ skipped ,
11261145 'summary ' => array (
1127- 'processed ' => $ processed ,
1128- 'removed ' => count ($ removed ),
1129- 'skipped ' => count ($ skipped ),
1130- 'bytes_reclaimed ' => $ bytes_reclaimed ,
1131- 'limit ' => $ limit ,
1146+ 'processed ' => $ processed ,
1147+ 'removed ' => count ($ removed ),
1148+ 'skipped ' => count ($ skipped ),
1149+ 'bytes_reclaimed ' => $ bytes_reclaimed ,
1150+ 'limit ' => $ limit ,
1151+ 'discarded_unpushed ' => count ($ discarded_unpushed ),
11321152 ),
11331153 'continuation ' => $ continuation ,
11341154 'evidence ' => array (
1135- 'elapsed_ms ' => (int ) round (( microtime (true ) - $ started_at ) * 1000 ),
1136- 'inventory_total ' => count ($ all_candidates ),
1137- 'removed_handles ' => array_values (array_filter (array_map (fn ( $ row ) => (string ) $ row ['handle ' ], $ removed ))),
1138- 'skipped_handles ' => array_values (array_filter (array_map (fn ( $ row ) => (string ) ( $ row ['handle ' ] ?? '' ), $ skipped ))),
1139- 'remove_timeout ' => $ remove_timeout_seconds ,
1140- 'source ' => $ source ,
1155+ 'elapsed_ms ' => (int ) round (( microtime (true ) - $ started_at ) * 1000 ),
1156+ 'inventory_total ' => count ($ all_candidates ),
1157+ 'removed_handles ' => array_values (array_filter (array_map (fn ( $ row ) => (string ) $ row ['handle ' ], $ removed ))),
1158+ 'skipped_handles ' => array_values (array_filter (array_map (fn ( $ row ) => (string ) ( $ row ['handle ' ] ?? '' ), $ skipped ))),
1159+ 'discard_unpushed ' => $ discard_unpushed ,
1160+ 'discarded_unpushed ' => $ discarded_unpushed ,
1161+ 'remove_timeout ' => $ remove_timeout_seconds ,
1162+ 'source ' => $ source ,
11411163 ),
11421164 );
11431165 }
@@ -1150,15 +1172,15 @@ function () use ( $repo, $branch, $wt_path, $force, $remove_timeout_seconds ) {
11501172 * @param float $started_at Start timestamp.
11511173 * @return array<string,mixed>
11521174 */
1153- private function apply_worktree_cleanup_plan_candidates ( array $ candidates , bool $ force , float $ started_at , bool $ stale_liveness_only = false , int $ remove_timeout_seconds = self ::CLEANUP_GIT_REMOVE_TIMEOUT ): array {
1175+ private function apply_worktree_cleanup_plan_candidates ( array $ candidates , bool $ force , float $ started_at , bool $ stale_liveness_only = false , int $ remove_timeout_seconds = self ::CLEANUP_GIT_REMOVE_TIMEOUT , bool $ discard_unpushed = false ): array {
11541176 $ processed = 0 ;
11551177 $ removed = array ();
11561178 $ skipped = array ();
11571179 $ bytes_reclaimed = 0 ;
11581180
11591181 foreach ( $ candidates as $ candidate ) {
11601182 ++$ processed ;
1161- $ revalidated = $ this ->revalidate_bounded_cleanup_eligible_candidate ($ candidate , $ force , $ stale_liveness_only );
1183+ $ revalidated = $ this ->revalidate_bounded_cleanup_eligible_candidate ($ candidate , $ force , $ stale_liveness_only, $ discard_unpushed );
11621184 if ( isset ($ revalidated ['skipped ' ]) ) {
11631185 $ skipped [] = $ revalidated ['skipped ' ];
11641186 continue ;
@@ -1253,7 +1275,7 @@ function () use ( $repo, $branch, $wt_path, $force, $remove_timeout_seconds ) {
12531275 * @param bool $force Allow dirty worktrees.
12541276 * @return array<string,mixed>
12551277 */
1256- private function revalidate_bounded_cleanup_eligible_candidate ( array $ candidate , bool $ force , bool $ stale_liveness_only = false ): array {
1278+ private function revalidate_bounded_cleanup_eligible_candidate ( array $ candidate , bool $ force , bool $ stale_liveness_only = false , bool $ discard_unpushed = false ): array {
12571279 $ handle = (string ) ( $ candidate ['handle ' ] ?? '' );
12581280 $ repo = (string ) ( $ candidate ['repo ' ] ?? '' );
12591281 $ branch = (string ) ( $ candidate ['branch ' ] ?? '' );
@@ -1435,21 +1457,27 @@ private function revalidate_bounded_cleanup_eligible_candidate( array $candidate
14351457 );
14361458 }
14371459
1438- if ( $ unpushed > 0 && ! $ allow_effective_clean_removal ) {
1460+ if ( $ unpushed > 0 && ! $ allow_effective_clean_removal && ! $ discard_unpushed ) {
14391461 return array (
14401462 'skipped ' => array (
14411463 'handle ' => $ handle ,
14421464 'repo ' => $ repo ,
14431465 'branch ' => $ branch ,
14441466 'path ' => $ wt_path ,
14451467 'reason_code ' => 'unpushed_commits ' ,
1446- 'reason ' => sprintf ('%d unpushed commit(s) — bounded cleanup-eligible apply refuses to remove even with force =true ' , $ unpushed ),
1468+ 'reason ' => sprintf ('%d unpushed commit(s) — bounded cleanup-eligible apply refuses to remove without discard_unpushed =true ' , $ unpushed ),
14471469 'unpushed ' => $ unpushed ,
14481470 ),
14491471 );
14501472 }
14511473
1452- return array_merge ($ candidate , array ( 'path ' => $ real_path ));
1474+ return array_merge (
1475+ $ candidate ,
1476+ array (
1477+ 'path ' => $ real_path ,
1478+ 'unpushed ' => (int ) $ unpushed ,
1479+ )
1480+ );
14531481 }
14541482
14551483 /**
@@ -1467,7 +1495,7 @@ private function revalidate_bounded_cleanup_eligible_candidate( array $candidate
14671495 * @param array<string,mixed> $continuation Continuation envelope.
14681496 * @return array<string,mixed>|\WP_Error
14691497 */
1470- private function schedule_bounded_cleanup_eligible_chunks ( array $ batch , array $ deferred , bool $ force , string $ source , float $ started_at , array $ continuation , bool $ include_repaired_metadata = false , int $ remove_timeout_seconds = self ::CLEANUP_GIT_REMOVE_TIMEOUT ): array |\WP_Error {
1498+ private function schedule_bounded_cleanup_eligible_chunks ( array $ batch , array $ deferred , bool $ force , string $ source , float $ started_at , array $ continuation , bool $ include_repaired_metadata = false , int $ remove_timeout_seconds = self ::CLEANUP_GIT_REMOVE_TIMEOUT , bool $ discard_unpushed = false ): array |\WP_Error {
14711499 if ( ! class_exists ('\DataMachine\Engine\Tasks\TaskScheduler ' ) ) {
14721500 return new \WP_Error ('task_scheduler_unavailable ' , 'Data Machine TaskScheduler is unavailable; cannot schedule bounded cleanup-eligible apply chunks. ' , array ( 'status ' => 500 ));
14731501 }
@@ -1520,6 +1548,7 @@ private function schedule_bounded_cleanup_eligible_chunks( array $batch, array $
15201548 'chunk_index ' => count ($ item_params ),
15211549 'rows ' => array ( $ row ),
15221550 'force ' => $ force ,
1551+ 'discard_unpushed ' => $ discard_unpushed ,
15231552 'skip_github ' => true ,
15241553 'include_repaired_metadata ' => $ include_repaired_metadata ,
15251554 'remove_timeout ' => $ remove_timeout_seconds ,
@@ -1559,12 +1588,13 @@ private function schedule_bounded_cleanup_eligible_chunks( array $batch, array $
15591588 ),
15601589 'continuation ' => $ continuation ,
15611590 'evidence ' => array (
1562- 'elapsed_ms ' => (int ) round (( microtime (true ) - $ started_at ) * 1000 ),
1563- 'planned_handles ' => array_values (array_filter (array_map (fn ( $ row ) => (string ) ( $ row ['handle ' ] ?? '' ), $ batch ))),
1564- 'batch_job_id ' => (int ) ( $ batch_result ['batch_job_id ' ] ?? 0 ),
1565- 'direct_job_ids ' => $ batch_result ['job_ids ' ] ?? array (),
1566- 'remove_timeout ' => $ remove_timeout_seconds ,
1567- 'source ' => $ source ,
1591+ 'elapsed_ms ' => (int ) round (( microtime (true ) - $ started_at ) * 1000 ),
1592+ 'planned_handles ' => array_values (array_filter (array_map (fn ( $ row ) => (string ) ( $ row ['handle ' ] ?? '' ), $ batch ))),
1593+ 'batch_job_id ' => (int ) ( $ batch_result ['batch_job_id ' ] ?? 0 ),
1594+ 'direct_job_ids ' => $ batch_result ['job_ids ' ] ?? array (),
1595+ 'discard_unpushed ' => $ discard_unpushed ,
1596+ 'remove_timeout ' => $ remove_timeout_seconds ,
1597+ 'source ' => $ source ,
15681598 ),
15691599 );
15701600 }
@@ -1616,6 +1646,9 @@ private function build_bounded_cleanup_resume_command( int $limit, array $opts,
16161646 if ( ! empty ($ opts ['force ' ]) ) {
16171647 $ parts [] = '--force ' ;
16181648 }
1649+ if ( ! empty ($ opts ['discard_unpushed ' ]) ) {
1650+ $ parts [] = '--discard-unpushed ' ;
1651+ }
16191652 if ( ! empty ($ opts ['include_repaired_metadata ' ]) ) {
16201653 $ parts [] = '--include-repaired-metadata ' ;
16211654 }
0 commit comments