Skip to content

Commit a52de95

Browse files
fix: add explicit discard for unpushed cleanup worktrees (#742)
* fix: add explicit discard for unpushed cleanup worktrees * fix: satisfy cleanup discard lint --------- Co-authored-by: homeboy-ci[bot] <266378653+homeboy-ci[bot]@users.noreply.github.com>
1 parent 8ca8b0b commit a52de95

6 files changed

Lines changed: 194 additions & 75 deletions

File tree

inc/Abilities/WorkspaceAbilities.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2300,6 +2300,10 @@ private function registerAbilities(): void {
23002300
'type' => 'boolean',
23012301
'description' => 'Allow apply on dirty worktrees. Unpushed-commit gate is never overridden.',
23022302
),
2303+
'discard_unpushed' => array(
2304+
'type' => 'boolean',
2305+
'description' => 'Explicitly discard unpushed commits for bounded cleanup-eligible rows. This is a data-loss mode and is separate from force.',
2306+
),
23032307
'via_jobs' => array(
23042308
'type' => 'boolean',
23052309
'description' => 'Schedule each candidate as a single-row worktree_cleanup_chunk job for resumable async apply.',
@@ -4120,14 +4124,15 @@ public static function worktreeCleanupArtifacts( array $input ): array|\WP_Error
41204124
/**
41214125
* Apply only worktrees with explicit lifecycle cleanup_eligible metadata in a bounded batch.
41224126
*
4123-
* @param array $input Input parameters (dry_run, limit, older_than, sort, force, via_jobs, remove_timeout, source).
4127+
* @param array $input Input parameters (dry_run, limit, older_than, sort, force, discard_unpushed, via_jobs, remove_timeout, source).
41244128
* @return array<string,mixed>|\WP_Error
41254129
*/
41264130
public static function worktreeBoundedCleanupEligibleApply( array $input ): array|\WP_Error {
41274131
$workspace = new Workspace();
41284132
$opts = array(
41294133
'dry_run' => ! empty($input['dry_run']),
41304134
'force' => ! empty($input['force']),
4135+
'discard_unpushed' => ! empty($input['discard_unpushed']),
41314136
'via_jobs' => ! empty($input['via_jobs']),
41324137
'include_repaired_metadata' => ! empty($input['include_repaired_metadata']),
41334138
);

inc/Cli/Commands/WorkspaceCommand.php

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3326,6 +3326,11 @@ private function renderGitOperationResult( string $operation, array $result, arr
33263326
* include repaired metadata rows as operator-approved cleanup candidates.
33273327
* Apply still runs fresh dirty/unpushed/containment/primary safety probes.
33283328
*
3329+
* [--discard-unpushed]
3330+
* : With bounded-cleanup-eligible-apply only, explicitly discard unpushed
3331+
* commits after reviewed cleanup eligibility and fresh safety probes. This
3332+
* is a data-loss mode and is not implied by --force.
3333+
*
33293334
* [--older-than=<duration>]
33303335
* : Limit cleanup candidates to worktrees with lifecycle `created_at`
33313336
* metadata older than the compact duration (cleanup only, e.g. 7d, 24h).
@@ -3484,6 +3489,7 @@ private function renderGitOperationResult( string $operation, array $result, arr
34843489
* # (cheap inventory only — no full git worktree scan, no GitHub lookup)
34853490
* wp datamachine-code workspace worktree bounded-cleanup-eligible-apply --dry-run --limit=25
34863491
* wp datamachine-code workspace worktree bounded-cleanup-eligible-apply --limit=25
3492+
* wp datamachine-code workspace worktree bounded-cleanup-eligible-apply --discard-unpushed --limit=25
34873493
* wp datamachine-code workspace worktree bounded-cleanup-eligible-apply --via-jobs --limit=10 --older-than=7d
34883494
* wp datamachine-code workspace worktree bounded-cleanup-eligible-apply --dry-run --include-repaired-metadata --older-than=7d --limit=25
34893495
* wp datamachine-code workspace worktree bounded-cleanup-eligible-apply --include-repaired-metadata --older-than=7d --limit=25
@@ -3855,6 +3861,7 @@ public function worktree( array $args, array $assoc_args ): void {
38553861
case 'bounded-cleanup-eligible-apply':
38563862
$input['dry_run'] = ! empty($assoc_args['dry-run']);
38573863
$input['force'] = ! empty($assoc_args['force']);
3864+
$input['discard_unpushed'] = ! empty($assoc_args['discard-unpushed']);
38583865
$input['via_jobs'] = ! empty($assoc_args['via-jobs']);
38593866
$input['include_repaired_metadata'] = ! empty($assoc_args['include-repaired-metadata']);
38603867
$input['source'] = self::CLEANUP_CLI_SOURCE;
@@ -6461,15 +6468,32 @@ private function render_worktree_bounded_cleanup_eligible_apply_result( array $r
64616468
WP_CLI::log('Removed worktrees:');
64626469
$rows = array_map(
64636470
fn( $row ) => array(
6464-
'handle' => $row['handle'] ?? '',
6465-
'repo' => $row['repo'] ?? '',
6466-
'branch' => $row['branch'] ?? '',
6467-
'size' => $this->format_bytes($row['size_bytes'] ?? null),
6468-
'path' => $row['path'] ?? '',
6471+
'handle' => $row['handle'] ?? '',
6472+
'repo' => $row['repo'] ?? '',
6473+
'branch' => $row['branch'] ?? '',
6474+
'size' => $this->format_bytes($row['size_bytes'] ?? null),
6475+
'unpushed' => (int) ( $row['unpushed_before_remove'] ?? 0 ),
6476+
'path' => $row['path'] ?? '',
64696477
),
64706478
$removed
64716479
);
6472-
$this->format_items($rows, array( 'handle', 'repo', 'branch', 'size', 'path' ), array( 'format' => 'table' ), 'handle');
6480+
$this->format_items($rows, array( 'handle', 'repo', 'branch', 'size', 'unpushed', 'path' ), array( 'format' => 'table' ), 'handle');
6481+
}
6482+
6483+
$evidence = (array) ( $result['evidence'] ?? array() );
6484+
$discarded_unpushed = (array) ( $evidence['discarded_unpushed'] ?? array() );
6485+
if ( ! empty($discarded_unpushed) ) {
6486+
WP_CLI::warning('Discarded unpushed commits for cleanup-eligible worktrees. Evidence follows.');
6487+
$rows = array_map(
6488+
fn( $row ) => array(
6489+
'handle' => $row['handle'] ?? '',
6490+
'unpushed_before' => (int) ( $row['unpushed_before_remove'] ?? 0 ),
6491+
'path_exists_after' => ! empty($row['path_exists_after']) ? 'yes' : 'no',
6492+
'path' => $row['path'] ?? '',
6493+
),
6494+
$discarded_unpushed
6495+
);
6496+
$this->format_items($rows, array( 'handle', 'unpushed_before', 'path_exists_after', 'path' ), array( 'format' => 'table' ), 'handle');
64736497
}
64746498

64756499
if ( ! empty($skipped) ) {

inc/Tasks/WorktreeCleanupChunkTask.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ public function executeTask( int $jobId, array $params ): void {
108108
'worktrees' => $workspace->worktree_cleanup_merged(
109109
array(
110110
'apply_plan' => array( 'candidates' => $rows ),
111+
'direct_apply_plan' => true,
112+
'force' => ! empty($params['force']),
113+
'discard_unpushed' => ! empty($params['discard_unpushed']),
111114
'skip_github' => array_key_exists('skip_github', $params) ? (bool) $params['skip_github'] : true,
112115
'include_repaired_metadata' => ! empty($params['include_repaired_metadata']),
113116
'stale_liveness_only' => ! empty($params['stale_liveness_only']),

inc/Workspace/WorkspaceWorktreeCleanupEngine.php

Lines changed: 78 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)