Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 46 additions & 13 deletions inc/Workspace/WorkspaceWorktreeCleanupEngine.php
Original file line number Diff line number Diff line change
Expand Up @@ -1057,18 +1057,20 @@ public function worktree_bounded_cleanup_eligible_apply( array $opts = array() )
return $this->schedule_bounded_cleanup_eligible_chunks($batch, $deferred, $force, $source, $started_at, $continuation, $include_repaired_metadata, $remove_timeout_seconds, $discard_unpushed);
}

$processed = 0;
$removed = array();
$skipped = $inventory_skipped;
$bytes_reclaimed = 0;
$timeout_handles = array();
$discarded_unpushed = array();
$processed = 0;
$processed_candidates = array();
$removed = array();
$skipped = $inventory_skipped;
$bytes_reclaimed = 0;
$timeout_handles = array();
$discarded_unpushed = array();

foreach ( $batch as $candidate ) {
++$processed;
$revalidated = $this->revalidate_bounded_cleanup_eligible_candidate($candidate, $force, false, $discard_unpushed);
if ( isset($revalidated['skipped']) ) {
$skipped[] = $revalidated['skipped'];
$processed_candidates[] = $this->build_bounded_cleanup_processed_candidate($candidate, 'skipped', $revalidated['skipped']);
$skipped[] = $revalidated['skipped'];
continue;
}

Expand Down Expand Up @@ -1107,16 +1109,17 @@ function () use ( $repo, $branch, $wt_path, $force, $remove_timeout_seconds ) {
);

if ( is_wp_error($remove) ) {
$skip = $this->build_worktree_remove_failure_skip($candidate, $remove, $remove_timeout_seconds);
$skipped[] = $skip;
$skip = $this->build_worktree_remove_failure_skip($candidate, $remove, $remove_timeout_seconds);
$processed_candidates[] = $this->build_bounded_cleanup_processed_candidate($validated, 'skipped', $skip);
$skipped[] = $skip;
if ( 'remove_timeout' === (string) ( $skip['reason_code'] ?? '' ) ) {
$timeout_handles[] = (string) ( $skip['handle'] ?? '' );
}
continue;
}

$unpushed_count = (int) ( $validated['unpushed'] ?? 0 );
$removed_row = array_merge(
$unpushed_count = (int) ( $validated['unpushed'] ?? 0 );
$removed_row = array_merge(
array(
'handle' => (string) ( $candidate['handle'] ?? '' ),
'repo' => $repo,
Expand All @@ -1130,7 +1133,8 @@ function () use ( $repo, $branch, $wt_path, $force, $remove_timeout_seconds ) {
),
is_array($candidate['metadata'] ?? null) ? array( 'metadata' => $candidate['metadata'] ) : array()
);
$removed[] = $removed_row;
$removed[] = $removed_row;
$processed_candidates[] = $this->build_bounded_cleanup_processed_candidate($validated, 'removed', $removed_row);
if ( $discard_unpushed && $unpushed_count > 0 ) {
$discarded_unpushed[] = array(
'handle' => (string) ( $candidate['handle'] ?? '' ),
Expand Down Expand Up @@ -1160,7 +1164,8 @@ function () use ( $repo, $branch, $wt_path, $force, $remove_timeout_seconds ) {
'destructive' => true,
'workspace_path' => $this->workspace_path,
'generated_at' => gmdate('c'),
'candidates' => $batch,
'planned_candidates' => $batch,
'candidates' => $processed_candidates,
'removed' => $removed,
'skipped' => $skipped,
'summary' => array(
Expand All @@ -1186,6 +1191,31 @@ function () use ( $repo, $branch, $wt_path, $force, $remove_timeout_seconds ) {
);
}

/**
* Attach final revalidation/removal outcome to a processed bounded cleanup row.
*
* @param array<string,mixed> $candidate Planned or revalidated candidate row.
* @param string $action Final action: removed or skipped.
* @param array<string,mixed> $outcome Fresh removal or blocker row.
* @return array<string,mixed>
*/
private function build_bounded_cleanup_processed_candidate( array $candidate, string $action, array $outcome ): array {
$row = $candidate;
foreach ( array( 'dirty', 'unpushed', 'path', 'size_bytes' ) as $field ) {
if ( array_key_exists($field, $outcome) ) {
$row[ $field ] = $outcome[ $field ];
}
}

$row['final_action'] = $action;
$row['final_reason_code'] = (string) ( $outcome['reason_code'] ?? $action );
if ( isset($outcome['reason']) ) {
$row['final_reason'] = (string) $outcome['reason'];
}

return $row;
}

/**
* Apply DB-backed cleanup rows without rebuilding a full workspace scan.
*
Expand Down Expand Up @@ -1500,6 +1530,7 @@ private function revalidate_bounded_cleanup_eligible_candidate( array $candidate
'reason_code' => 'dirty_worktree',
'reason' => sprintf('working tree dirty (%d entries) — bounded cleanup-eligible apply refuses to override; rerun with force=true after review', $dirty_count),
'dirty' => $dirty_count,
'unpushed' => (int) $unpushed,
),
);
}
Expand All @@ -1513,6 +1544,7 @@ private function revalidate_bounded_cleanup_eligible_candidate( array $candidate
'path' => $wt_path,
'reason_code' => 'unpushed_commits',
'reason' => sprintf('%d unpushed commit(s) — bounded cleanup-eligible apply refuses to remove without discard_unpushed=true', $unpushed),
'dirty' => $dirty_count,
'unpushed' => $unpushed,
),
);
Expand All @@ -1522,6 +1554,7 @@ private function revalidate_bounded_cleanup_eligible_candidate( array $candidate
$candidate,
array(
'path' => $real_path,
'dirty' => $dirty_count,
'unpushed' => (int) $unpushed,
)
);
Expand Down
59 changes: 59 additions & 0 deletions tests/bounded-cleanup-processed-candidates.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

if ( ! defined('ABSPATH') ) {
define('ABSPATH', __DIR__ . '/fixtures/');
}

require_once dirname(__DIR__) . '/inc/Workspace/WorkspaceWorktreeCleanupEngine.php';

use DataMachineCode\Workspace\WorkspaceWorktreeCleanupEngine;

function bounded_cleanup_processed_candidates_assert_same( mixed $expected, mixed $actual, string $message ): void {
if ( $expected !== $actual ) {
throw new RuntimeException(sprintf('%s Expected %s, got %s.', $message, var_export($expected, true), var_export($actual, true)));
}
}

final class BoundedCleanupProcessedCandidateHarness {
use WorkspaceWorktreeCleanupEngine;

public function processed( array $candidate, string $action, array $outcome ): array {
return $this->build_bounded_cleanup_processed_candidate($candidate, $action, $outcome);
}
}

$harness = new BoundedCleanupProcessedCandidateHarness();

$candidate = array(
'handle' => 'repo@stale-cleanup-row',
'repo' => 'repo',
'branch' => 'stale-cleanup-row',
'path' => '/tmp/repo@stale-cleanup-row',
'reason_code' => 'cleanup_eligible',
'dirty' => 0,
);

$processed = $harness->processed(
$candidate,
'skipped',
array(
'handle' => 'repo@stale-cleanup-row',
'repo' => 'repo',
'branch' => 'stale-cleanup-row',
'path' => '/tmp/repo@stale-cleanup-row',
'reason_code' => 'dirty_worktree',
'reason' => 'working tree dirty (2 entries)',
'dirty' => 2,
'unpushed' => 1,
)
);

bounded_cleanup_processed_candidates_assert_same(2, $processed['dirty'], 'processed candidate carries fresh dirty count');
bounded_cleanup_processed_candidates_assert_same(1, $processed['unpushed'], 'processed candidate carries fresh unpushed count');
bounded_cleanup_processed_candidates_assert_same('skipped', $processed['final_action'], 'processed candidate records final action');
bounded_cleanup_processed_candidates_assert_same('dirty_worktree', $processed['final_reason_code'], 'processed candidate records blocker bucket');
bounded_cleanup_processed_candidates_assert_same('cleanup_eligible', $processed['reason_code'], 'planned reason remains available separately');

echo "bounded-cleanup-processed-candidates: ok\n";
Loading