From 7bfd69941d8de2d3dea6bc1ca0cf9a046dcb4761 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sun, 7 Jun 2026 11:28:37 -0400 Subject: [PATCH 1/2] refactor: share active cleanup revalidation --- inc/Workspace/Workspace.php | 243 +++++++++++++++++------------------- 1 file changed, 117 insertions(+), 126 deletions(-) diff --git a/inc/Workspace/Workspace.php b/inc/Workspace/Workspace.php index bc72a75..7d96768 100644 --- a/inc/Workspace/Workspace.php +++ b/inc/Workspace/Workspace.php @@ -2343,53 +2343,24 @@ private function build_current_remote_tracking_clean_cleanup_evidence( string $h } } - if ( in_array($branch, $this->protected_base_branch_names(), true) ) { - return new \WP_Error('primary_protected_branch', 'refusing to auto-finalize a protected primary branch worktree'); - } - - $validation = $this->validate_containment($path, $this->workspace_path); - if ( ! $validation['valid'] ) { - return new \WP_Error('external_worktree', 'worktree path is outside the workspace root'); - } - - $real_path = (string) ( $validation['real_path'] ?? '' ); - if ( '' === $real_path || ! is_dir($real_path) ) { - return new \WP_Error('missing_worktree', 'worktree path no longer exists'); - } - - $git_marker = rtrim($real_path, '/') . '/.git'; - if ( is_dir($git_marker) ) { - return new \WP_Error('primary_checkout', 'refusing to mark a primary checkout cleanup_eligible'); - } - if ( ! is_file($git_marker) ) { - return new \WP_Error('not_a_worktree', 'worktree marker missing'); - } - - $current_branch = $this->resolve_worktree_branch_from_head_file($real_path); - if ( $branch !== $current_branch ) { - return new \WP_Error('branch_identity_mismatch', 'worktree branch identity changed before apply'); - } - - $dirty = $this->probe_worktree_dirty_count($real_path, self::CLEANUP_GIT_PROBE_TIMEOUT); - if ( is_wp_error($dirty) ) { - return $dirty; - } - if ( 0 !== (int) $dirty ) { - return new \WP_Error('dirty_worktree', 'worktree is dirty'); - } - - $unpushed = $this->count_unpushed_commits($real_path, self::CLEANUP_GIT_PROBE_TIMEOUT); - if ( is_wp_error($unpushed) ) { - return $unpushed; - } - if ( 0 !== (int) $unpushed ) { - return new \WP_Error('unpushed_commits', 'worktree has unpushed commits'); + $facts = $this->validate_current_cleanup_worktree( + $repo, + $path, + $branch, + array( + 'require_clean' => true, + 'missing_primary_code' => 'primary_missing', + 'dirty_error_message' => 'worktree is dirty', + 'unpushed_error_message' => 'worktree has unpushed commits', + ) + ); + if ( is_wp_error($facts) ) { + return $facts; } - $primary_path = $this->get_primary_path($repo); - if ( '' === $primary_path || ! is_dir($primary_path . '/.git') ) { - return new \WP_Error('primary_missing', 'primary checkout missing'); - } + $dirty = (int) $facts['dirty']; + $unpushed = (int) $facts['unpushed']; + $primary_path = (string) $facts['primary_path']; $remote_ref = 'refs/remotes/origin/' . $branch; $remote = $this->run_git($primary_path, sprintf('rev-parse --verify --quiet %s', escapeshellarg($remote_ref)), self::CLEANUP_GIT_PROBE_TIMEOUT); @@ -2420,50 +2391,22 @@ private function build_current_remote_tracking_clean_cleanup_evidence( string $h * @return array|\WP_Error */ private function build_current_effective_clean_cleanup_evidence( string $repo, string $wt_path ): array|\WP_Error { - $validation = $this->validate_containment($wt_path, $this->workspace_path); - if ( ! $validation['valid'] ) { - return new \WP_Error('external_worktree', 'worktree path is outside the workspace root'); + $facts = $this->validate_current_cleanup_worktree($repo, $wt_path); + if ( is_wp_error($facts) ) { + return $facts; } - $real_path = (string) ( $validation['real_path'] ?? '' ); - if ( '' === $real_path || ! is_dir($real_path) ) { - return new \WP_Error('missing_worktree', 'worktree path no longer exists'); - } + $real_path = (string) $facts['real_path']; + $primary_path = (string) $facts['primary_path']; + $dirty = (int) $facts['dirty']; + $unpushed = (int) $facts['unpushed']; + $branch = (string) $facts['branch']; - $git_marker = rtrim($real_path, '/') . '/.git'; - if ( is_dir($git_marker) ) { - return new \WP_Error('primary_checkout', 'refusing to mark a primary checkout cleanup_eligible'); - } - if ( ! is_file($git_marker) ) { - return new \WP_Error('not_a_worktree', 'worktree marker missing'); - } - - $primary_path = $this->get_primary_path($repo); - if ( ! is_dir($primary_path . '/.git') ) { - return new \WP_Error('missing_primary', 'primary checkout missing'); - } - - $dirty = $this->probe_worktree_dirty_count($real_path, self::CLEANUP_GIT_PROBE_TIMEOUT); - if ( is_wp_error($dirty) ) { - return $dirty; - } - $unpushed = $this->count_unpushed_commits($real_path, self::CLEANUP_GIT_PROBE_TIMEOUT); - if ( is_wp_error($unpushed) ) { - return $unpushed; - } $default_ref = $this->resolve_remote_default_ref($primary_path, self::CLEANUP_GIT_PROBE_TIMEOUT); if ( ! is_string($default_ref) || '' === $default_ref ) { return new \WP_Error('missing_default_ref', 'primary checkout default ref could not be resolved'); } - $branch = (string) $this->resolve_worktree_branch_from_head_file($real_path); - if ( '' === $branch ) { - return new \WP_Error('missing_branch_identity', 'worktree branch identity could not be resolved'); - } - if ( in_array($branch, $this->protected_base_branch_names(), true) ) { - return new \WP_Error('primary_protected_branch', 'refusing to auto-finalize a protected primary branch worktree'); - } - $upstream_equivalence = ( 0 === (int) $dirty && 0 === (int) $unpushed ) ? $this->build_clean_upstream_equivalence_evidence($primary_path, $real_path, $default_ref, $branch) : $this->build_dirty_unpushed_upstream_equivalence_evidence($primary_path, $real_path, $default_ref); @@ -2501,53 +2444,24 @@ private function build_current_merged_to_default_cleanup_evidence( string $handl } } - if ( in_array($branch, $this->protected_base_branch_names(), true) ) { - return new \WP_Error('primary_protected_branch', 'refusing to auto-finalize a protected primary branch worktree'); - } - - $validation = $this->validate_containment($path, $this->workspace_path); - if ( ! $validation['valid'] ) { - return new \WP_Error('external_worktree', 'worktree path is outside the workspace root'); - } - - $real_path = (string) ( $validation['real_path'] ?? '' ); - if ( '' === $real_path || ! is_dir($real_path) ) { - return new \WP_Error('missing_worktree', 'worktree path no longer exists'); - } - - $git_marker = rtrim($real_path, '/') . '/.git'; - if ( is_dir($git_marker) ) { - return new \WP_Error('primary_checkout', 'refusing to mark a primary checkout cleanup_eligible'); - } - if ( ! is_file($git_marker) ) { - return new \WP_Error('not_a_worktree', 'worktree marker missing'); - } - - $current_branch = $this->resolve_worktree_branch_from_head_file($real_path); - if ( $branch !== $current_branch ) { - return new \WP_Error('branch_identity_mismatch', 'worktree branch identity changed before apply'); - } - - $primary_path = $this->get_primary_path($repo); - if ( ! is_dir($primary_path . '/.git') ) { - return new \WP_Error('missing_primary', 'primary checkout missing'); - } - - $dirty = $this->probe_worktree_dirty_count($real_path, self::CLEANUP_GIT_PROBE_TIMEOUT); - if ( is_wp_error($dirty) ) { - return $dirty; - } - if ( (int) $dirty > 0 ) { - return new \WP_Error('dirty_worktree', 'refusing to mark dirty worktree cleanup_eligible from merged-to-default evidence'); + $facts = $this->validate_current_cleanup_worktree( + $repo, + $path, + $branch, + array( + 'require_clean' => true, + 'dirty_error_message' => 'refusing to mark dirty worktree cleanup_eligible from merged-to-default evidence', + 'unpushed_error_message' => 'refusing to mark worktree with unpushed commits cleanup_eligible from merged-to-default evidence', + ) + ); + if ( is_wp_error($facts) ) { + return $facts; } - $unpushed = $this->count_unpushed_commits($real_path, self::CLEANUP_GIT_PROBE_TIMEOUT); - if ( is_wp_error($unpushed) ) { - return $unpushed; - } - if ( (int) $unpushed > 0 ) { - return new \WP_Error('unpushed_commits', 'refusing to mark worktree with unpushed commits cleanup_eligible from merged-to-default evidence'); - } + $real_path = (string) $facts['real_path']; + $primary_path = (string) $facts['primary_path']; + $dirty = (int) $facts['dirty']; + $unpushed = (int) $facts['unpushed']; $default_ref = $this->resolve_remote_default_ref($primary_path, self::CLEANUP_GIT_PROBE_TIMEOUT); if ( ! is_string($default_ref) || '' === $default_ref ) { @@ -2597,6 +2511,83 @@ private function build_current_merged_to_default_cleanup_evidence( string $handl ); } + /** + * Revalidate the current worktree state before writing cleanup metadata. + * + * @param string $repo Repository name. + * @param string $path Worktree path. + * @param string|null $expected_branch Expected branch, or null to resolve it from the worktree. + * @param array $opts Validation options. + * @return array|\WP_Error + */ + private function validate_current_cleanup_worktree( string $repo, string $path, ?string $expected_branch = null, array $opts = array() ): array|\WP_Error { + if ( null !== $expected_branch && in_array($expected_branch, $this->protected_base_branch_names(), true) ) { + return new \WP_Error('primary_protected_branch', 'refusing to auto-finalize a protected primary branch worktree'); + } + + $validation = $this->validate_containment($path, $this->workspace_path); + if ( ! $validation['valid'] ) { + return new \WP_Error('external_worktree', 'worktree path is outside the workspace root'); + } + + $real_path = (string) ( $validation['real_path'] ?? '' ); + if ( '' === $real_path || ! is_dir($real_path) ) { + return new \WP_Error('missing_worktree', 'worktree path no longer exists'); + } + + $git_marker = rtrim($real_path, '/') . '/.git'; + if ( is_dir($git_marker) ) { + return new \WP_Error('primary_checkout', 'refusing to mark a primary checkout cleanup_eligible'); + } + if ( ! is_file($git_marker) ) { + return new \WP_Error('not_a_worktree', 'worktree marker missing'); + } + + $current_branch = (string) $this->resolve_worktree_branch_from_head_file($real_path); + if ( null !== $expected_branch && $expected_branch !== $current_branch ) { + return new \WP_Error('branch_identity_mismatch', 'worktree branch identity changed before apply'); + } + if ( null === $expected_branch && '' === $current_branch ) { + return new \WP_Error('missing_branch_identity', 'worktree branch identity could not be resolved'); + } + if ( null === $expected_branch && in_array($current_branch, $this->protected_base_branch_names(), true) ) { + return new \WP_Error('primary_protected_branch', 'refusing to auto-finalize a protected primary branch worktree'); + } + + $primary_path = $this->get_primary_path($repo); + $missing_primary_code = (string) ( $opts['missing_primary_code'] ?? 'missing_primary' ); + if ( '' === $primary_path || ! is_dir($primary_path . '/.git') ) { + return new \WP_Error($missing_primary_code, 'primary checkout missing'); + } + + $dirty = $this->probe_worktree_dirty_count($real_path, self::CLEANUP_GIT_PROBE_TIMEOUT); + if ( is_wp_error($dirty) ) { + return $dirty; + } + + $unpushed = $this->count_unpushed_commits($real_path, self::CLEANUP_GIT_PROBE_TIMEOUT); + if ( is_wp_error($unpushed) ) { + return $unpushed; + } + + if ( ! empty($opts['require_clean']) ) { + if ( 0 !== (int) $dirty ) { + return new \WP_Error('dirty_worktree', (string) ( $opts['dirty_error_message'] ?? 'worktree is dirty' )); + } + if ( 0 !== (int) $unpushed ) { + return new \WP_Error('unpushed_commits', (string) ( $opts['unpushed_error_message'] ?? 'worktree has unpushed commits' )); + } + } + + return array( + 'real_path' => $real_path, + 'primary_path' => $primary_path, + 'branch' => null !== $expected_branch ? $expected_branch : $current_branch, + 'dirty' => (int) $dirty, + 'unpushed' => (int) $unpushed, + ); + } + /** * Build a skip row for finalized active/no-signal apply. * From 80de813873bbfb37eab6da9e83b9a414479d4812 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Sun, 7 Jun 2026 11:36:01 -0400 Subject: [PATCH 2/2] fix: align active cleanup options --- inc/Workspace/Workspace.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/inc/Workspace/Workspace.php b/inc/Workspace/Workspace.php index 7d96768..6ccdbb7 100644 --- a/inc/Workspace/Workspace.php +++ b/inc/Workspace/Workspace.php @@ -2348,9 +2348,9 @@ private function build_current_remote_tracking_clean_cleanup_evidence( string $h $path, $branch, array( - 'require_clean' => true, - 'missing_primary_code' => 'primary_missing', - 'dirty_error_message' => 'worktree is dirty', + 'require_clean' => true, + 'missing_primary_code' => 'primary_missing', + 'dirty_error_message' => 'worktree is dirty', 'unpushed_error_message' => 'worktree has unpushed commits', ) ); @@ -2449,8 +2449,8 @@ private function build_current_merged_to_default_cleanup_evidence( string $handl $path, $branch, array( - 'require_clean' => true, - 'dirty_error_message' => 'refusing to mark dirty worktree cleanup_eligible from merged-to-default evidence', + 'require_clean' => true, + 'dirty_error_message' => 'refusing to mark dirty worktree cleanup_eligible from merged-to-default evidence', 'unpushed_error_message' => 'refusing to mark worktree with unpushed commits cleanup_eligible from merged-to-default evidence', ) );