22/**
33 * GitHub Remote
44 *
5- * One place for GitHub-specific URL manipulation:
6- * - "is this a GitHub remote?"
7- * - "parse owner/repo out of the URL"
8- * - shared API URL helpers for authenticated GitHub operations
9- *
10- * Shared by Workspace GitHub lookups so the regex + host-detection logic has
11- * a single source of truth.
5+ * One place for GitHub-specific repository URL manipulation:
6+ * - detect supported GitHub/GitHub Enterprise remotes
7+ * - parse owner/repo/host descriptors out of clone or web URLs
8+ * - render clone, web, and API URLs from the same descriptor
129 *
1310 * @package DataMachineCode\Support
1411 * @since 0.7.0
2017
2118final class GitHubRemote {
2219
20+ public const PUBLIC_WEB_BASE_URL = 'https://github.com ' ;
21+ public const PUBLIC_API_BASE_URL = 'https://api.github.com ' ;
22+ public const PUBLIC_SSH_HOST = 'github.com ' ;
2323
2424
2525 /**
26- * Detect a GitHub remote. Matches https://github.com/... and
27- * git@github.com:... Both forms count .
26+ * Detect a supported GitHub remote. Matches public GitHub and
27+ * GitHub Enterprise-style hosts such as github.a8c.com .
2828 */
2929 public static function isGitHubRemote ( string $ url ): bool {
30- return (bool ) preg_match ('#(https?://github\.com/|git@github\.com:)# ' , $ url );
30+ return null !== self ::descriptor ($ url );
31+ }
32+
33+ /**
34+ * Build a repository descriptor from a slug, clone URL, or web URL.
35+ *
36+ * @return array{
37+ * host:string,
38+ * web_base_url:string,
39+ * api_base_url:string,
40+ * ssh_host:string,
41+ * owner:string,
42+ * repo:string,
43+ * slug:string,
44+ * https_clone_url:string,
45+ * ssh_clone_url:string,
46+ * web_url:string
47+ * }|null
48+ */
49+ public static function descriptor ( string $ remote_or_slug ): ?array {
50+ $ value = trim ($ remote_or_slug );
51+ if ( '' === $ value ) {
52+ return null ;
53+ }
54+
55+ $ host = self ::PUBLIC_SSH_HOST ;
56+ $ owner = '' ;
57+ $ repo = '' ;
58+
59+ if ( preg_match ('#^([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+)$# ' , $ value , $ m ) ) {
60+ $ owner = $ m [1 ];
61+ $ repo = $ m [2 ];
62+ } elseif ( preg_match ('#^https?://([^/]+)/([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+?)(?:\.git)?(?:/.*)?$# ' , $ value , $ m ) ) {
63+ $ host = strtolower ($ m [1 ]);
64+ $ owner = $ m [2 ];
65+ $ repo = $ m [3 ];
66+ } elseif ( preg_match ('#^(?:ssh://)?git@([^:/]+)[:/]([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+?)(?:\.git)?/?$# ' , $ value , $ m ) ) {
67+ $ host = strtolower ($ m [1 ]);
68+ $ owner = $ m [2 ];
69+ $ repo = $ m [3 ];
70+ } else {
71+ return null ;
72+ }
73+
74+ if ( ! self ::isGitHubHost ($ host ) || '' === $ owner || '' === $ repo ) {
75+ return null ;
76+ }
77+
78+ $ repo = preg_replace ('/\.git$/ ' , '' , $ repo ) ?? $ repo ;
79+ $ slug = $ owner . '/ ' . $ repo ;
80+
81+ return array (
82+ 'host ' => $ host ,
83+ 'web_base_url ' => self ::webBaseUrl ($ host ),
84+ 'api_base_url ' => self ::apiBaseUrl ($ host ),
85+ 'ssh_host ' => $ host ,
86+ 'owner ' => $ owner ,
87+ 'repo ' => $ repo ,
88+ 'slug ' => $ slug ,
89+ 'https_clone_url ' => self ::webBaseUrl ($ host ) . '/ ' . $ slug . '.git ' ,
90+ 'ssh_clone_url ' => 'git@ ' . $ host . ': ' . $ slug . '.git ' ,
91+ 'web_url ' => self ::webBaseUrl ($ host ) . '/ ' . $ slug ,
92+ );
3193 }
3294
3395 /**
34- * Extract `owner/repo` from a GitHub URL.
96+ * Extract `owner/repo` from a GitHub URL or slug .
3597 *
3698 * Accepts both `https://github.com/owner/repo(.git)(/)?` and
3799 * `git@github.com:owner/repo(.git)`. Returns null for any URL that
@@ -41,10 +103,43 @@ public static function isGitHubRemote( string $url ): bool {
41103 * @return string|null `owner/repo` or null.
42104 */
43105 public static function slug ( string $ url ): ?string {
44- if ( preg_match ('#github\.com[:/]([\w.-]+)/([\w.-]+?)(?:\.git)?/?$# ' , $ url , $ m ) ) {
45- return $ m [1 ] . '/ ' . $ m [2 ];
106+ $ descriptor = self ::descriptor ($ url );
107+ return null !== $ descriptor ? $ descriptor ['slug ' ] : null ;
108+ }
109+
110+ /**
111+ * Build a clone URL from a GitHub descriptor input.
112+ */
113+ public static function cloneUrl ( string $ remote_or_slug , string $ protocol = 'https ' ): ?string {
114+ $ descriptor = self ::descriptor ($ remote_or_slug );
115+ if ( null === $ descriptor ) {
116+ return null ;
117+ }
118+
119+ return 'ssh ' === $ protocol ? $ descriptor ['ssh_clone_url ' ] : $ descriptor ['https_clone_url ' ];
120+ }
121+
122+ /**
123+ * Build a GitHub web URL for a repo, optionally under a path.
124+ */
125+ public static function webUrl ( string $ remote_or_slug , string $ path = '' ): ?string {
126+ $ descriptor = self ::descriptor ($ remote_or_slug );
127+ if ( null === $ descriptor ) {
128+ return null ;
129+ }
130+
131+ if ( '' === $ path ) {
132+ return $ descriptor ['web_url ' ];
46133 }
47- return null ;
134+
135+ return $ descriptor ['web_url ' ] . '/ ' . ltrim ($ path , '/ ' );
136+ }
137+
138+ /**
139+ * Build a GitHub branch web URL for a repo.
140+ */
141+ public static function branchUrl ( string $ remote_or_slug , string $ branch ): ?string {
142+ return self ::webUrl ($ remote_or_slug , 'tree/ ' . rawurlencode ($ branch ));
48143 }
49144
50145 /**
@@ -58,11 +153,29 @@ public static function slug( string $url ): ?string {
58153 * `slug()` or a value the caller already trusts). No sanitization
59154 * is attempted here beyond string concatenation.
60155 */
61- public static function apiUrl ( string $ slug , string $ path = '' ): string {
62- $ base = 'https://api.github.com/repos/ ' . $ slug ;
156+ public static function apiUrl ( string $ remote_or_slug , string $ path = '' ): string {
157+ $ descriptor = self ::descriptor ($ remote_or_slug );
158+ $ slug = null !== $ descriptor ? $ descriptor ['slug ' ] : $ remote_or_slug ;
159+ $ base = ( null !== $ descriptor ? $ descriptor ['api_base_url ' ] : self ::PUBLIC_API_BASE_URL ) . '/repos/ ' . $ slug ;
63160 if ( '' === $ path ) {
64161 return $ base ;
65162 }
66163 return $ base . '/ ' . ltrim ($ path , '/ ' );
67164 }
165+
166+ /**
167+ * Build a GitHub REST API URL not scoped to a repository.
168+ */
169+ public static function apiBaseUrl ( string $ host = self ::PUBLIC_SSH_HOST ): string {
170+ return self ::PUBLIC_SSH_HOST === strtolower ($ host ) ? self ::PUBLIC_API_BASE_URL : self ::webBaseUrl ($ host ) . '/api/v3 ' ;
171+ }
172+
173+ private static function webBaseUrl ( string $ host ): string {
174+ return 'https:// ' . strtolower ($ host );
175+ }
176+
177+ private static function isGitHubHost ( string $ host ): bool {
178+ $ host = strtolower ($ host );
179+ return self ::PUBLIC_SSH_HOST === $ host || str_starts_with ($ host , 'github. ' );
180+ }
68181}
0 commit comments