diff --git a/inc/Cli/Commands/WorkspaceCommand.php b/inc/Cli/Commands/WorkspaceCommand.php index 9c88d09..5a7e4b5 100644 --- a/inc/Cli/Commands/WorkspaceCommand.php +++ b/inc/Cli/Commands/WorkspaceCommand.php @@ -4081,6 +4081,9 @@ public function worktree( array $args, array $assoc_args ): void { break; case 'cleanup': + if ( ! empty($args[1]) ) { + $input['repo'] = (string) $args[1]; + } $input['dry_run'] = ! empty($assoc_args['dry-run']); $input['force'] = ! empty($assoc_args['force']); $input['skip_github'] = ! empty($assoc_args['skip-github']); diff --git a/inc/Workspace/WorkspaceActiveNoSignalCleanup.php b/inc/Workspace/WorkspaceActiveNoSignalCleanup.php index 0c9af36..3bd0741 100644 --- a/inc/Workspace/WorkspaceActiveNoSignalCleanup.php +++ b/inc/Workspace/WorkspaceActiveNoSignalCleanup.php @@ -266,7 +266,14 @@ public function worktree_active_no_signal_merged_apply( array $opts = array() ): return array( 'report_action_counts' => $report['summary']['by_suggested_action'] ?? array() ); }, 'prepare_row' => function ( array $row ): array|\WP_Error { - if ( 'merged_to_default' !== (string) ( $row['suggested_action'] ?? '' ) ) { + $is_merged_to_default = 'merged_to_default' === (string) ( $row['suggested_action'] ?? '' ) + || ( + 0 === (int) ( $row['dirty'] ?? -1 ) + && 0 === (int) ( $row['unpushed'] ?? -1 ) + && 0 === (int) ( $row['commits_outside_default'] ?? -1 ) + ); + + if ( ! $is_merged_to_default ) { return new \WP_Error('not_merged_to_default', 'row is not a clean merged-to-default candidate'); } @@ -1699,6 +1706,10 @@ private function is_generated_or_artifact_path( string $path ): bool { * @return string */ private function suggest_active_no_signal_action( array $row ): string { + if ( 0 === (int) ( $row['dirty'] ?? -1 ) && 0 === (int) ( $row['unpushed'] ?? -1 ) && 0 === (int) ( $row['commits_outside_default'] ?? -1 ) ) { + return 'merged_to_default'; + } + $effective_status = (string) ( $row['upstream_equivalence']['effective_status'] ?? '' ); if ( 'equivalent_clean' === $effective_status ) { return 'patch_equivalent_default'; @@ -1719,10 +1730,6 @@ private function suggest_active_no_signal_action( array $row ): string { return 'active_open_pr'; } - if ( 0 === (int) ( $row['commits_outside_default'] ?? -1 ) ) { - return 'merged_to_default'; - } - if ( true === ( $row['remote_tracking'] ?? null ) ) { return 'remote_tracking_clean'; } diff --git a/tests/worktree-cleanup-cli-scope.php b/tests/worktree-cleanup-cli-scope.php new file mode 100644 index 0000000..58adc40 --- /dev/null +++ b/tests/worktree-cleanup-cli-scope.php @@ -0,0 +1,25 @@ +invoke( + $cleanup, + array( + 'dirty' => 0, + 'unpushed' => 0, + 'commits_outside_default' => 0, + 'remote_tracking' => true, + 'upstream_equivalence' => array( + 'effective_status' => 'equivalent_clean', + ), + ) +); +worktree_cleanup_patch_equivalence_assert_same('merged_to_default', $merged_action, 'exact clean containment in the remote default branch should win over patch-equivalence and remote-tracking labels'); + +$merged = $root . '/merged-worktree'; +worktree_cleanup_patch_equivalence_run($primary, 'git worktree add -b merged-feature ../merged-worktree origin/main'); +worktree_cleanup_patch_equivalence_run($merged, 'git config user.email test@example.com'); +worktree_cleanup_patch_equivalence_run($merged, 'git config user.name Test'); +file_put_contents($merged . '/merged.txt', "merged\n"); +worktree_cleanup_patch_equivalence_run($merged, 'git add merged.txt'); +worktree_cleanup_patch_equivalence_run($merged, 'git commit -m merged-feature'); +worktree_cleanup_patch_equivalence_run($primary, 'git merge --ff-only merged-feature'); +worktree_cleanup_patch_equivalence_run($primary, 'git push origin main'); + +$merged_cleanup = new class($root) { + use WorkspaceCoreUtilities; + use WorkspaceActiveNoSignalCleanup; + use WorkspaceWorktreeCleanupEngine; + + protected const CLEANUP_GIT_PROBE_TIMEOUT = 5; + + protected string $workspace_path; + + public function __construct( string $workspace_path ) { + $this->workspace_path = $workspace_path; + } + + public function get_primary_path( string $repo ): string { + return $this->workspace_path . '/primary'; + } + + protected function resolve_remote_default_ref( string $primary_path, int $timeout_seconds = 0 ): string|WP_Error|null { + return 'refs/remotes/origin/main'; + } + + /** @return array */ + protected function protected_base_branch_names(): array { + return array( 'main', 'master', 'trunk', 'develop' ); + } + + protected function resolve_worktree_branch_from_head_file( string $wt_path ): ?string { + $result = $this->run_git($wt_path, 'branch --show-current', self::CLEANUP_GIT_PROBE_TIMEOUT); + if ( is_wp_error($result) ) { + return null; + } + + $branch = trim((string) ( $result['output'] ?? '' )); + return '' === $branch ? null : $branch; + } + + protected function probe_worktree_dirty_count( string $path, int $timeout_seconds = 0 ): int|WP_Error { + $result = $this->run_git($path, 'status --porcelain', $timeout_seconds); + if ( is_wp_error($result) ) { + return $result; + } + + $lines = array_filter(array_map('trim', explode("\n", (string) ( $result['output'] ?? '' )))); + return count($lines); + } + + /** @return array */ + public function worktree_active_no_signal_report( array $opts = array() ): array { + return array( + 'success' => true, + 'rows' => array( + array( + 'handle' => 'repo@merged-feature', + 'repo' => 'repo', + 'branch' => 'merged-feature', + 'path' => $this->workspace_path . '/merged-worktree', + 'dirty' => 0, + 'unpushed' => 0, + 'commits_outside_default' => 0, + 'suggested_action' => 'remote_tracking_clean', + ), + ), + 'summary' => array( 'inspected' => 1 ), + ); + } +}; + +$merged_apply = $merged_cleanup->worktree_active_no_signal_merged_apply(array( 'dry_run' => true )); +worktree_cleanup_patch_equivalence_assert_same(true, is_array($merged_apply), 'merged apply should return a dry-run result'); +worktree_cleanup_patch_equivalence_assert_same(1, $merged_apply['summary']['planned'], 'merged apply should plan clean rows with zero commits outside default even when an older report label was remote_tracking_clean'); +worktree_cleanup_patch_equivalence_assert_same('repo@merged-feature', $merged_apply['planned'][0]['handle'], 'merged apply should preserve the planned handle'); + echo "worktree-cleanup-patch-equivalence: ok\n";