Skip to content

Commit 887c12c

Browse files
Refactor workspace handle parsing (#767)
* refactor: add workspace handle primitive * fix: satisfy workspace handle lint --------- Co-authored-by: homeboy-ci[bot] <266378653+homeboy-ci[bot]@users.noreply.github.com>
1 parent 510ab53 commit 887c12c

6 files changed

Lines changed: 173 additions & 55 deletions

File tree

inc/Runtime/ActiveWorkspaceProjector.php

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,13 @@
7272

7373
namespace DataMachineCode\Runtime;
7474

75+
use DataMachineCode\Workspace\WorkspaceHandle;
7576
use DataMachineCode\Workspace\WorktreeContextInjector;
7677

7778
defined('ABSPATH') || exit;
7879

80+
require_once dirname(__DIR__) . '/Workspace/WorkspaceHandle.php';
81+
7982
class ActiveWorkspaceProjector {
8083

8184

@@ -143,22 +146,20 @@ public static function project_into_snapshot( array $snapshot, int $job_id, arra
143146
* @return array<string,mixed>
144147
*/
145148
private static function build_entry( string $handle, array $overrides ): array {
146-
$metadata = WorktreeContextInjector::get_metadata($handle);
147-
$is_primary = ! str_contains($handle, '@');
149+
$metadata = WorktreeContextInjector::get_metadata($handle);
150+
$workspace_handle = WorkspaceHandle::parse($handle);
151+
$is_primary = ! $workspace_handle->is_worktree();
148152

149153
$entry = array(
150154
'handle' => $handle,
151155
'primary' => $is_primary,
152156
);
153157

154-
// Derive repo + branch from handle.
155-
$handle_parts = explode('@', $handle, 2);
156-
$repo_slug = $handle_parts[0] ?? '';
157-
if ( '' !== $repo_slug ) {
158-
$entry['repo'] = $repo_slug;
158+
if ( '' !== $workspace_handle->repo() ) {
159+
$entry['repo'] = $workspace_handle->repo();
159160
}
160-
if ( ! $is_primary && isset($handle_parts[1]) && '' !== $handle_parts[1] ) {
161-
$entry['branch'] = $handle_parts[1];
161+
if ( null !== $workspace_handle->branch_slug() && '' !== $workspace_handle->branch_slug() ) {
162+
$entry['branch'] = $workspace_handle->branch_slug();
162163
}
163164

164165
// Enrich from persisted metadata.

inc/Workspace/RunnerWorkspacePublisher.php

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
defined('ABSPATH') || exit;
1414

15+
require_once __DIR__ . '/WorkspaceHandle.php';
16+
1517
class RunnerWorkspacePublisher {
1618

1719
/**
@@ -41,13 +43,13 @@ public function publish( array $input ): array|\WP_Error {
4143
return new \WP_Error('runner_workspace_publish_missing_pr_title', 'pr_title is required.', array( 'status' => 400 ));
4244
}
4345

44-
$base = trim( (string) ( $input['base'] ?? $input['base_branch'] ?? $input['base_ref'] ?? '' ) );
46+
$base = trim( (string) ( $input['base'] ?? $input['base_branch'] ?? $input['base_ref'] ?? '' ) );
4547
$target = $this->resolve_publication_target($input, $handle, $target_repo, $base);
4648
if ( is_wp_error($target) ) {
4749
return $target;
4850
}
4951
$branch = $target['push_branch'];
50-
$body = $this->build_pull_request_body( (string) ( $input['pr_body'] ?? $input['body'] ?? '' ), $input );
52+
$body = $this->build_pull_request_body( (string) ( $input['pr_body'] ?? $input['body'] ?? '' ), $input );
5153

5254
$status = WorkspaceAbilities::gitStatus(array( 'name' => $handle ));
5355
if ( is_wp_error($status) ) {
@@ -175,7 +177,8 @@ private function resolve_publication_target( array $input, string $handle, strin
175177
return $head;
176178
}
177179

178-
$base_owner = strtolower(strtok($base_repo, '/') ?: '');
180+
$base_owner = strtok($base_repo, '/');
181+
$base_owner = false === $base_owner ? '' : strtolower($base_owner);
179182
$head_owner = $head['owner'];
180183
$head_repo = null === $head_owner ? $base_repo : $head_owner . '/' . substr($base_repo, (int) strpos($base_repo, '/') + 1);
181184

@@ -198,12 +201,12 @@ private function resolve_publication_target( array $input, string $handle, strin
198201
$head_ref = null === $head_owner ? $push_branch : $head_owner . ':' . $head['branch'];
199202

200203
return array(
201-
'base_repo' => $base_repo,
202-
'base_ref' => $base,
203-
'head_repo' => $head_repo,
204-
'head_ref' => $head_ref,
205-
'push_remote' => $push_remote,
206-
'push_branch' => $push_branch,
204+
'base_repo' => $base_repo,
205+
'base_ref' => $base,
206+
'head_repo' => $head_repo,
207+
'head_ref' => $head_ref,
208+
'push_remote' => $push_remote,
209+
'push_branch' => $push_branch,
207210
);
208211
}
209212

@@ -220,18 +223,25 @@ private function resolve_head_branch( array $input, string $handle ): array|\WP_
220223
if ( '' === $owner || '' === $head_branch ) {
221224
return new \WP_Error('runner_workspace_publish_invalid_head_branch', 'Head must be a branch or owner:branch.', array( 'status' => 400 ));
222225
}
223-
return array( 'owner' => $owner, 'branch' => $head_branch );
226+
return array(
227+
'owner' => $owner,
228+
'branch' => $head_branch,
229+
);
224230
}
225231

226-
return array( 'owner' => null, 'branch' => $branch );
232+
return array(
233+
'owner' => null,
234+
'branch' => $branch,
235+
);
227236
}
228237
}
229238

230-
if ( str_contains($handle, '@') ) {
231-
$slug = substr($handle, (int) strpos($handle, '@') + 1);
232-
if ( '' !== $slug ) {
233-
return array( 'owner' => null, 'branch' => $slug );
234-
}
239+
$workspace_handle = WorkspaceHandle::parse($handle);
240+
if ( null !== $workspace_handle->branch_slug() && '' !== $workspace_handle->branch_slug() ) {
241+
return array(
242+
'owner' => null,
243+
'branch' => $workspace_handle->branch_slug(),
244+
);
235245
}
236246

237247
return new \WP_Error('runner_workspace_publish_missing_head_branch', 'A head branch/ref or branch context is required.', array( 'status' => 400 ));

inc/Workspace/WorkspaceAliasResolver.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
defined('ABSPATH') || exit;
1111

12+
require_once __DIR__ . '/WorkspaceHandle.php';
13+
1214
class WorkspaceAliasResolver {
1315

1416

@@ -347,11 +349,11 @@ public static function sanitize_result( mixed $value, string $alias, string $han
347349
return $value;
348350
}
349351

350-
$sanitized = self::sanitize_scoped_string($value, $root);
351-
$sanitized = str_replace($handle, $alias, $sanitized);
352-
if ( str_contains($handle, '@') ) {
353-
list( $repo, $slug ) = explode('@', $handle, 2);
354-
$sanitized = str_replace(array( $repo, $slug ), array( $alias, $alias ), $sanitized);
352+
$sanitized = self::sanitize_scoped_string($value, $root);
353+
$sanitized = str_replace($handle, $alias, $sanitized);
354+
$workspace_handle = WorkspaceHandle::parse($handle);
355+
if ( $workspace_handle->is_worktree() ) {
356+
$sanitized = str_replace(array( $workspace_handle->repo(), (string) $workspace_handle->branch_slug() ), array( $alias, $alias ), $sanitized);
355357
}
356358

357359
return $sanitized;

inc/Workspace/WorkspaceCoreUtilities.php

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
defined('ABSPATH') || exit;
1616

17+
require_once __DIR__ . '/WorkspaceHandle.php';
18+
1719
trait WorkspaceCoreUtilities {
1820

1921
/**
@@ -225,31 +227,7 @@ public function get_repo_path( string $handle ): string {
225227
* @return array{repo: string, branch_slug: string|null, is_worktree: bool, dir_name: string}
226228
*/
227229
public function parse_handle( string $handle ): array {
228-
$handle = trim($handle);
229-
230-
if ( str_contains($handle, '@') ) {
231-
$parts = explode('@', $handle, 2);
232-
$repo = $this->sanitize_name($parts[0]);
233-
$slug = $this->sanitize_slug($parts[1]);
234-
235-
if ( '' !== $repo && '' !== $slug ) {
236-
return array(
237-
'repo' => $repo,
238-
'branch_slug' => $slug,
239-
'is_worktree' => true,
240-
'dir_name' => $repo . '@' . $slug,
241-
);
242-
}
243-
}
244-
245-
$repo = $this->sanitize_name($handle);
246-
247-
return array(
248-
'repo' => $repo,
249-
'branch_slug' => null,
250-
'is_worktree' => false,
251-
'dir_name' => $repo,
252-
);
230+
return WorkspaceHandle::parse($handle)->to_array();
253231
}
254232

255233
/**

inc/Workspace/WorkspaceHandle.php

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
/**
3+
* Workspace handle value helper.
4+
*
5+
* @package DataMachineCode\Workspace
6+
*/
7+
8+
namespace DataMachineCode\Workspace;
9+
10+
defined('ABSPATH') || exit;
11+
12+
/**
13+
* Parsed workspace handle identity.
14+
*/
15+
final class WorkspaceHandle {
16+
17+
private function __construct(
18+
private readonly string $repo,
19+
private readonly ?string $branch_slug,
20+
private readonly bool $is_worktree,
21+
private readonly string $dir_name
22+
) {}
23+
24+
/**
25+
* Parse a workspace handle into its canonical components.
26+
*/
27+
public static function parse( string $handle ): self {
28+
$handle = trim($handle);
29+
30+
if ( str_contains($handle, '@') ) {
31+
$parts = explode('@', $handle, 2);
32+
$repo = self::sanitize_name($parts[0] ?? '');
33+
$slug = self::sanitize_slug($parts[1] ?? '');
34+
35+
if ( '' !== $repo && '' !== $slug ) {
36+
return new self($repo, $slug, true, $repo . '@' . $slug);
37+
}
38+
}
39+
40+
$repo = self::sanitize_name($handle);
41+
42+
return new self($repo, null, false, $repo);
43+
}
44+
45+
public function repo(): string {
46+
return $this->repo;
47+
}
48+
49+
public function branch_slug(): ?string {
50+
return $this->branch_slug;
51+
}
52+
53+
public function is_worktree(): bool {
54+
return $this->is_worktree;
55+
}
56+
57+
public function dir_name(): string {
58+
return $this->dir_name;
59+
}
60+
61+
/**
62+
* @return array{repo: string, branch_slug: string|null, is_worktree: bool, dir_name: string}
63+
*/
64+
public function to_array(): array {
65+
return array(
66+
'repo' => $this->repo,
67+
'branch_slug' => $this->branch_slug,
68+
'is_worktree' => $this->is_worktree,
69+
'dir_name' => $this->dir_name,
70+
);
71+
}
72+
73+
private static function sanitize_slug( string $slug ): string {
74+
$slug = preg_replace('/[^a-zA-Z0-9._-]/', '', $slug);
75+
$slug = preg_replace('/-{2,}/', '-', (string) $slug);
76+
return trim( (string) $slug, '-.');
77+
}
78+
79+
private static function sanitize_name( string $name ): string {
80+
return (string) preg_replace('/[^a-zA-Z0-9._-]/', '', $name);
81+
}
82+
}

tests/workspace-handle.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
if ( ! defined('ABSPATH') ) {
6+
define('ABSPATH', __DIR__ . '/fixtures/');
7+
}
8+
9+
require_once dirname(__DIR__) . '/inc/Workspace/WorkspaceHandle.php';
10+
11+
use DataMachineCode\Workspace\WorkspaceHandle;
12+
13+
function assert_same( mixed $expected, mixed $actual, string $message ): void {
14+
if ( $expected !== $actual ) {
15+
throw new RuntimeException(sprintf('%s Expected %s, got %s.', $message, var_export($expected, true), var_export($actual, true)));
16+
}
17+
}
18+
19+
$primary = WorkspaceHandle::parse(' data-machine-code ');
20+
assert_same('data-machine-code', $primary->repo(), 'primary repo parses');
21+
assert_same(null, $primary->branch_slug(), 'primary has no branch slug');
22+
assert_same(false, $primary->is_worktree(), 'primary is not worktree');
23+
assert_same('data-machine-code', $primary->dir_name(), 'primary dir name matches repo');
24+
25+
$worktree = WorkspaceHandle::parse('data-machine-code@cook-workspace-identity-primitives');
26+
assert_same('data-machine-code', $worktree->repo(), 'worktree repo parses');
27+
assert_same('cook-workspace-identity-primitives', $worktree->branch_slug(), 'worktree branch slug parses');
28+
assert_same(true, $worktree->is_worktree(), 'worktree is worktree');
29+
assert_same('data-machine-code@cook-workspace-identity-primitives', $worktree->dir_name(), 'worktree dir name is canonical');
30+
assert_same(
31+
array(
32+
'repo' => 'data-machine-code',
33+
'branch_slug' => 'cook-workspace-identity-primitives',
34+
'is_worktree' => true,
35+
'dir_name' => 'data-machine-code@cook-workspace-identity-primitives',
36+
),
37+
$worktree->to_array(),
38+
'worktree array shape matches parse_handle contract'
39+
);
40+
41+
$fallback = WorkspaceHandle::parse('data-machine-code@');
42+
assert_same('data-machine-code', $fallback->repo(), 'invalid worktree falls back to primary parsing');
43+
assert_same(false, $fallback->is_worktree(), 'invalid worktree fallback is primary');
44+
45+
echo "workspace-handle: ok\n";

0 commit comments

Comments
 (0)