@@ -60,42 +60,101 @@ public function download(string $repo, string $ref, ?string $dst = NULL): string
6060 }
6161
6262 public static function parseUri (string $ src ): array {
63+ // Try GitHub-specific patterns first (Phase 2).
64+ $ github_pattern = static ::detectGitHubUrlPattern ($ src );
65+ if ($ github_pattern !== NULL ) {
66+ [$ repo , $ ref ] = $ github_pattern ;
67+
68+ // Validate the extracted ref.
69+ if (!Validator::gitRef ($ ref )) {
70+ throw new \RuntimeException (sprintf ('Invalid git reference: "%s". Reference must be a valid git tag, branch, or commit hash. ' , $ ref ));
71+ }
72+
73+ return [$ repo , $ ref ];
74+ }
75+
76+ // Fall back to #ref parsing (standard git reference syntax).
6377 if (str_starts_with ($ src , 'https:// ' )) {
64- if (!preg_match ('# ^(https://[^/]+/[^/]+/[^@ ]+)(?:@ (.+))?$# ' , $ src , $ matches )) {
65- throw new \RuntimeException (sprintf ('Invalid remote repository format: "%s". ' , $ src ));
78+ if (!preg_match ('~ ^(https://[^/]+/[^/]+/[^# ]+)(?:# (.+))?$~ ' , $ src , $ matches )) {
79+ throw new \RuntimeException (sprintf ('Invalid remote repository format: "%s". Use # to specify a reference (e.g., repo.git#tag). ' , $ src ));
6680 }
6781 $ repo = $ matches [1 ];
6882 $ ref = $ matches [2 ] ?? static ::REF_HEAD ;
6983 }
7084 elseif (str_starts_with ($ src , 'git@ ' )) {
71- if (!preg_match ('# ^(git@[^:]+:[^/]+/[^@]+ )(?:@ (.+))?$# ' , $ src , $ matches )) {
72- throw new \RuntimeException (sprintf ('Invalid remote repository format: "%s". ' , $ src ));
85+ if (!preg_match ('~ ^(git@[^:]+:[^#]+ )(?:# (.+))?$~ ' , $ src , $ matches )) {
86+ throw new \RuntimeException (sprintf ('Invalid remote repository format: "%s". Use # to specify a reference (e.g., git@host:repo#tag). ' , $ src ));
7387 }
7488 $ repo = $ matches [1 ];
7589 $ ref = $ matches [2 ] ?? static ::REF_HEAD ;
7690 }
7791 elseif (str_starts_with ($ src , 'file:// ' )) {
78- if (!preg_match ('# ^file://(.+?)(?:@ (.+))?$# ' , $ src , $ matches )) {
79- throw new \RuntimeException (sprintf ('Invalid local repository format: "%s". ' , $ src ));
92+ if (!preg_match ('~ ^file://(.+?)(?:# (.+))?$~ ' , $ src , $ matches )) {
93+ throw new \RuntimeException (sprintf ('Invalid local repository format: "%s". Use # to specify a reference. ' , $ src ));
8094 }
8195 $ repo = $ matches [1 ];
8296 $ ref = $ matches [2 ] ?? static ::REF_HEAD ;
8397 }
8498 else {
85- if (!preg_match ('# ^(.+?)(?:@ (.+))?$# ' , $ src , $ matches )) {
86- throw new \RuntimeException (sprintf ('Invalid local repository format: "%s". ' , $ src ));
99+ if (!preg_match ('~ ^(.+?)(?:# (.+))?$~ ' , $ src , $ matches )) {
100+ throw new \RuntimeException (sprintf ('Invalid local repository format: "%s". Use # to specify a reference. ' , $ src ));
87101 }
88102 $ repo = rtrim ($ matches [1 ], '/ ' );
89103 $ ref = $ matches [2 ] ?? static ::REF_HEAD ;
90104 }
91105
92- if ($ ref != static :: REF_STABLE && $ ref != static :: REF_HEAD && ! Validator::gitCommitSha ( $ ref ) && !Validator:: gitCommitShaShort ($ ref )) {
93- throw new \RuntimeException (sprintf ('Invalid reference format : "%s". Supported formats are: %s, %s, %s, %s. ' , $ ref , static :: REF_STABLE , static :: REF_HEAD , ' 40-character commit hash ' , ' 7-character commit hash ' ));
106+ if (! Validator::gitRef ($ ref )) {
107+ throw new \RuntimeException (sprintf ('Invalid git reference : "%s". Reference must be a valid git tag, branch, or commit hash. ' , $ ref ));
94108 }
95109
96110 return [$ repo , $ ref ];
97111 }
98112
113+ /**
114+ * Detect and parse GitHub-specific URL patterns.
115+ *
116+ * Supports direct GitHub URLs copied from browser and alternative # syntax.
117+ *
118+ * @param string $uri
119+ * The input URI.
120+ *
121+ * @return array{0: string, 1: string}|null
122+ * Array of [repo_url, ref] if GitHub pattern detected, NULL otherwise.
123+ */
124+ protected static function detectGitHubUrlPattern (string $ uri ): ?array {
125+ // Pattern 1: /releases/tag/{ref}
126+ // Example: https://github.com/drevops/vortex/releases/tag/25.11.0
127+ if (preg_match ('#^(https://github\.com/[^/]+/[^/]+)/releases/tag/(.+)$# ' , $ uri , $ matches )) {
128+ return [$ matches [1 ], $ matches [2 ]];
129+ }
130+
131+ // Pattern 2: /tree/{ref}
132+ // Example: https://github.com/drevops/vortex/tree/1.2.3
133+ if (preg_match ('#^(https://github\.com/[^/]+/[^/]+)/tree/(.+)$# ' , $ uri , $ matches )) {
134+ return [$ matches [1 ], $ matches [2 ]];
135+ }
136+
137+ // Pattern 3: /commit/{ref}
138+ // Example: https://github.com/drevops/vortex/commit/abcd123
139+ if (preg_match ('#^(https://github\.com/[^/]+/[^/]+)/commit/(.+)$# ' , $ uri , $ matches )) {
140+ return [$ matches [1 ], $ matches [2 ]];
141+ }
142+
143+ // Pattern 4: .git#{ref} (HTTPS) - alternative to @ syntax
144+ // Example: https://github.com/drevops/vortex.git#25.11.0
145+ if (preg_match ('~^(https://[^#]+\.git)#(.+)$~ ' , $ uri , $ matches )) {
146+ return [$ matches [1 ], $ matches [2 ]];
147+ }
148+
149+ // Pattern 5: git@...#{ref} (SSH) - alternative to @ syntax
150+ // Example: git@github.com:drevops/vortex#stable
151+ if (preg_match ('~^(git@[^#]+)#(.+)$~ ' , $ uri , $ matches )) {
152+ return [$ matches [1 ], $ matches [2 ]];
153+ }
154+
155+ return NULL ;
156+ }
157+
99158 protected function downloadFromRemote (string $ repo , string $ ref , ?string $ destination ): string {
100159 if ($ destination === NULL ) {
101160 throw new \InvalidArgumentException ('Destination cannot be null for remote downloads. ' );
0 commit comments