|
20 | 20 | use DataMachineCode\Cleanup\CleanupRunEvidenceStoreInterface; |
21 | 21 | use DataMachineCode\Cleanup\DataMachineJobCleanupRunEvidenceStore; |
22 | 22 | use DataMachineCode\Workspace\Workspace; |
| 23 | +use DataMachineCode\Workspace\WorkspaceMutationLock; |
23 | 24 |
|
24 | 25 | defined('ABSPATH') || exit; |
25 | 26 |
|
@@ -133,7 +134,7 @@ public function list_repos( array $args, array $assoc_args ): void { |
133 | 134 | $result = $ability->execute($input); |
134 | 135 |
|
135 | 136 | if ( is_wp_error($result) ) { |
136 | | - $this->render_workspace_error($result); |
| 137 | + WP_CLI::error($result->get_error_message()); |
137 | 138 | return; |
138 | 139 | } |
139 | 140 |
|
@@ -1975,7 +1976,7 @@ private function renderGitOperationResult( string $operation, array $result, arr |
1975 | 1976 | * ## OPTIONS |
1976 | 1977 | * |
1977 | 1978 | * <operation> |
1978 | | - * : Worktree operation: add, list, remove, prune, cleanup, cleanup-artifacts, |
| 1979 | + * : Worktree operation: add, list, remove, prune, locks, cleanup, cleanup-artifacts, |
1979 | 1980 | * bounded-cleanup-eligible-apply, emergency-cleanup, reconcile-metadata, |
1980 | 1981 | * active-no-signal-report, active-no-signal-finalized-apply, |
1981 | 1982 | * active-no-signal-equivalent-clean-apply, |
@@ -2065,7 +2066,11 @@ private function renderGitOperationResult( string $operation, array $result, arr |
2065 | 2066 | * : Lifecycle state to record when finalizing a worktree. |
2066 | 2067 | * |
2067 | 2068 | * [--dry-run] |
2068 | | - * : Preview cleanup candidates without removing anything (cleanup only). |
| 2069 | + * : Preview cleanup candidates without removing anything (cleanup and locks only). |
| 2070 | + * |
| 2071 | + * [--prune-stale] |
| 2072 | + * : For `locks`, prune expired DB lock rows and old unlocked filesystem lock |
| 2073 | + * files. Active filesystem flocks are reported but never removed. |
2069 | 2074 | * |
2070 | 2075 | * [--apply-plan=<file>] |
2071 | 2076 | * : Low-level escape hatch for applying a previously reviewed JSON report. |
@@ -2211,6 +2216,11 @@ private function renderGitOperationResult( string $operation, array $result, arr |
2211 | 2216 | * # Prune stale worktree registry entries across all primaries |
2212 | 2217 | * wp datamachine-code workspace worktree prune |
2213 | 2218 | * |
| 2219 | + * # Inspect and safely prune stale workspace mutation locks |
| 2220 | + * wp datamachine-code workspace worktree locks --format=json |
| 2221 | + * wp datamachine-code workspace worktree locks --prune-stale --dry-run --format=json |
| 2222 | + * wp datamachine-code workspace worktree locks --prune-stale --format=json |
| 2223 | + * |
2214 | 2224 | * # Preview worktrees that would be removed (upstream gone or PR merged) |
2215 | 2225 | * wp datamachine-code workspace worktree cleanup --dry-run |
2216 | 2226 | * |
@@ -2287,7 +2297,18 @@ public function worktree( array $args, array $assoc_args ): void { |
2287 | 2297 | $operation = $args[0] ?? ''; |
2288 | 2298 |
|
2289 | 2299 | if ( '' === $operation ) { |
2290 | | - WP_CLI::error('Usage: wp datamachine-code workspace worktree <add|list|remove|prune|cleanup|cleanup-artifacts|bounded-cleanup-eligible-apply|emergency-cleanup|reconcile-metadata|active-no-signal-report|active-no-signal-finalized-apply|active-no-signal-equivalent-clean-apply|refresh-context|finalize|mark-cleanup-eligible> [<repo>] [<branch>] [--flags]'); |
| 2300 | + WP_CLI::error('Usage: wp datamachine-code workspace worktree <add|list|remove|prune|locks|cleanup|cleanup-artifacts|bounded-cleanup-eligible-apply|emergency-cleanup|reconcile-metadata|active-no-signal-report|active-no-signal-finalized-apply|active-no-signal-equivalent-clean-apply|refresh-context|finalize|mark-cleanup-eligible> [<repo>] [<branch>] [--flags]'); |
| 2301 | + return; |
| 2302 | + } |
| 2303 | + |
| 2304 | + if ( 'locks' === $operation ) { |
| 2305 | + $workspace = new Workspace(); |
| 2306 | + $workspace_path = $workspace->get_path(); |
| 2307 | + $dry_run = ! empty($assoc_args['dry-run']) || empty($assoc_args['prune-stale']); |
| 2308 | + $result = ! empty($assoc_args['prune-stale']) |
| 2309 | + ? WorkspaceMutationLock::prune_stale($workspace_path, $dry_run) |
| 2310 | + : WorkspaceMutationLock::status($workspace_path); |
| 2311 | + $this->render_workspace_lock_result($result, $assoc_args, ! empty($assoc_args['prune-stale'])); |
2291 | 2312 | return; |
2292 | 2313 | } |
2293 | 2314 |
|
@@ -2543,7 +2564,7 @@ public function worktree( array $args, array $assoc_args ): void { |
2543 | 2564 | $result = $ability->execute($input); |
2544 | 2565 |
|
2545 | 2566 | if ( is_wp_error($result) ) { |
2546 | | - WP_CLI::error($result->get_error_message()); |
| 2567 | + $this->render_workspace_error($result); |
2547 | 2568 | return; |
2548 | 2569 | } |
2549 | 2570 |
|
@@ -2766,6 +2787,63 @@ function ( $wt ) { |
2766 | 2787 | } |
2767 | 2788 | } |
2768 | 2789 |
|
| 2790 | + /** |
| 2791 | + * Render workspace mutation lock status or prune results. |
| 2792 | + * |
| 2793 | + * @param array<string,mixed> $result Lock status or prune result. |
| 2794 | + */ |
| 2795 | + private function render_workspace_lock_result( array $result, array $assoc_args, bool $prune ): void { |
| 2796 | + if ( 'json' === (string) ( $assoc_args['format'] ?? '' ) ) { |
| 2797 | + $json = wp_json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); |
| 2798 | + WP_CLI::log(false === $json ? '{}' : $json); |
| 2799 | + return; |
| 2800 | + } |
| 2801 | + |
| 2802 | + $status = $prune ? (array) ( $result['after'] ?? array() ) : $result; |
| 2803 | + $fs = (array) ( $status['filesystem'] ?? array() ); |
| 2804 | + $db = (array) ( $status['database'] ?? array() ); |
| 2805 | + |
| 2806 | + WP_CLI::log($prune ? 'Workspace mutation locks: stale prune complete' : 'Workspace mutation locks:'); |
| 2807 | + WP_CLI::log(sprintf('Active: %d Stale: %d', (int) ( $status['active'] ?? 0 ), (int) ( $status['stale'] ?? 0 ))); |
| 2808 | + WP_CLI::log(sprintf('Database: %s active, %s stale, available=%s', (string) ( $db['active'] ?? 0 ), (string) ( $db['stale'] ?? 0 ), ! empty($db['available']) ? 'yes' : 'no')); |
| 2809 | + WP_CLI::log(sprintf('Filesystem: %s active, %s stale, %s recent', (string) ( $fs['active'] ?? 0 ), (string) ( $fs['stale'] ?? 0 ), (string) ( $fs['recent'] ?? 0 ))); |
| 2810 | + |
| 2811 | + if ( $prune ) { |
| 2812 | + $filesystem = (array) ( $result['filesystem'] ?? array() ); |
| 2813 | + WP_CLI::log(sprintf('Filesystem removed: %d; skipped: %d', (int) ( $filesystem['removed_count'] ?? 0 ), (int) ( $filesystem['skipped_count'] ?? 0 ))); |
| 2814 | + if ( ! empty($result['dry_run']) ) { |
| 2815 | + WP_CLI::log('Dry-run only. Re-run without --dry-run to remove stale unlocked lock files.'); |
| 2816 | + } |
| 2817 | + } |
| 2818 | + |
| 2819 | + $locks = (array) ( $fs['locks'] ?? array() ); |
| 2820 | + if ( ! empty($locks) ) { |
| 2821 | + $items = array_map( |
| 2822 | + static function ( array $lock ): array { |
| 2823 | + $owner = (array) ( $lock['owner_evidence'] ?? array() ); |
| 2824 | + return array( |
| 2825 | + 'lock_key' => (string) ( $lock['lock_key'] ?? '' ), |
| 2826 | + 'scope' => (string) ( $lock['scope'] ?? '' ), |
| 2827 | + 'state' => (string) ( $lock['state'] ?? '' ), |
| 2828 | + 'age_seconds' => $lock['age_seconds'] ?? null, |
| 2829 | + 'safe_to_prune' => ! empty($lock['safe_to_prune']) ? 'yes' : 'no', |
| 2830 | + 'owner_source' => (string) ( $owner['source'] ?? '' ), |
| 2831 | + 'path' => (string) ( $lock['path'] ?? '' ), |
| 2832 | + ); |
| 2833 | + }, |
| 2834 | + $locks |
| 2835 | + ); |
| 2836 | + $this->format_items($items, array( 'lock_key', 'scope', 'state', 'age_seconds', 'safe_to_prune', 'owner_source', 'path' ), $assoc_args, 'lock_key'); |
| 2837 | + } |
| 2838 | + |
| 2839 | + $guidance = (array) ( $fs['guidance'] ?? $status['recovery_guidance'] ?? array() ); |
| 2840 | + if ( ! empty($guidance) ) { |
| 2841 | + WP_CLI::log(sprintf('Status: %s', (string) ( $guidance['status_command'] ?? 'wp datamachine-code workspace worktree locks --format=json' ))); |
| 2842 | + WP_CLI::log(sprintf('Prune: %s', (string) ( $guidance['dry_run_command'] ?? 'wp datamachine-code workspace worktree locks --prune-stale --dry-run --format=json' ))); |
| 2843 | + WP_CLI::log( (string) ( $guidance['safety'] ?? 'Active filesystem flocks are not pruned.' ) ); |
| 2844 | + } |
| 2845 | + } |
| 2846 | + |
2769 | 2847 | private function render_workspace_error( \WP_Error $error ): void { |
2770 | 2848 | $data = (array) $error->get_error_data(); |
2771 | 2849 | if ( 'workspace_repo_busy' !== $error->get_error_code() ) { |
@@ -2795,6 +2873,30 @@ private function render_workspace_error( \WP_Error $error ): void { |
2795 | 2873 | } |
2796 | 2874 | } |
2797 | 2875 |
|
| 2876 | + $filesystem_lock = is_array($data['filesystem_lock'] ?? null) ? (array) $data['filesystem_lock'] : array(); |
| 2877 | + if ( ! empty($filesystem_lock) ) { |
| 2878 | + WP_CLI::warning(sprintf('Filesystem lock: %s (%s)', (string) ( $filesystem_lock['lock_key'] ?? $data['lock_key'] ?? '-' ), (string) ( $filesystem_lock['state'] ?? 'unknown' ))); |
| 2879 | + WP_CLI::log(sprintf('Path: %s', (string) ( $filesystem_lock['path'] ?? $data['lock_path'] ?? '-' ))); |
| 2880 | + WP_CLI::log(sprintf('Age: %ss', (string) ( $filesystem_lock['age_seconds'] ?? '-' ))); |
| 2881 | + $owner_evidence = (array) ( $filesystem_lock['owner_evidence'] ?? array() ); |
| 2882 | + if ( ! empty($owner_evidence['source']) ) { |
| 2883 | + WP_CLI::log(sprintf('Owner src: %s', (string) $owner_evidence['source'])); |
| 2884 | + } |
| 2885 | + if ( ! empty($owner_evidence['message']) ) { |
| 2886 | + WP_CLI::log(sprintf('Owner note: %s', (string) $owner_evidence['message'])); |
| 2887 | + } |
| 2888 | + if ( ! empty($filesystem_lock['operator_guidance']) ) { |
| 2889 | + WP_CLI::log(sprintf('Guidance: %s', (string) $filesystem_lock['operator_guidance'])); |
| 2890 | + } |
| 2891 | + } |
| 2892 | + |
| 2893 | + if ( ! empty($data['status_command']) ) { |
| 2894 | + WP_CLI::log(sprintf('Inspect: %s', (string) $data['status_command'])); |
| 2895 | + } |
| 2896 | + if ( ! empty($data['stale_prune_command']) ) { |
| 2897 | + WP_CLI::log(sprintf('Recover: %s', (string) $data['stale_prune_command'])); |
| 2898 | + } |
| 2899 | + |
2798 | 2900 | WP_CLI::error($error->get_error_message()); |
2799 | 2901 | } |
2800 | 2902 |
|
|
0 commit comments