null, 'branch' => 'main']; } if (preg_match('/github\.com\/([^\/]+)\/([^\/\?#]+)/', $url, $matches)) { $repo = $matches[1] . '/' . $matches[2]; $branch = 'main'; if (preg_match('/\/tree\/([^\/]+)/', $url, $branchMatch)) { $branch = $branchMatch[1]; } return ['repo' => $repo, 'branch' => $branch]; } return ['repo' => null, 'branch' => 'main']; } public function extractRepo(string $url): string { $url = trim($url, '/'); if (str_contains($url, 'github.com/')) { $parts = explode('github.com/', $url); if (isset($parts[1])) { $path = trim($parts[1], '/'); $path = explode('/tree/', $path)[0]; $path = explode('/blob/', $path)[0]; return $path; } } return $url; } public function getBranches(string $githubUrl): array { if ($githubUrl === '' || $githubUrl === '0' || ! str_contains($githubUrl, 'github.com')) { return []; } try { $repo = $this->extractRepo($githubUrl); $gitUrl = "https://github.com/{$repo}.git"; $result = Process::timeout(30)->run('git ls-remote --heads ' . escapeshellarg($gitUrl) . ' 2>/dev/null'); if ($result->successful() && trim($result->output())) { $branches = []; foreach (explode("\n", trim($result->output())) as $line) { $parts = explode("\t", $line); if (isset($parts[1]) && str_starts_with($parts[1], 'refs/heads/')) { $branches[] = str_replace('refs/heads/', '', $parts[1]); } } return $branches; } $response = Http::timeout(10)->get("https://api.github.com/repos/{$repo}/branches"); if ($response->successful()) { $data = $response->json(); return array_column($data, 'name'); } } catch (\Exception) { // Ignore } return []; } public function getLatestCommit(string $githubUrl, string $branch = 'main'): ?string { if ($githubUrl === '' || $githubUrl === '0') { return null; } try { $repo = $this->extractRepo($githubUrl); $gitUrl = "https://github.com/{$repo}.git"; $result = Process::timeout(30)->run('git ls-remote ' . escapeshellarg($gitUrl) . ' ' . escapeshellarg($branch) . ' 2>/dev/null'); if ($result->successful() && trim($result->output())) { $parts = explode("\t", trim($result->output())); if ($parts[0] !== '' && $parts[0] !== '0') { return substr($parts[0], 0, 7); } } $response = Http::timeout(10)->get("https://api.github.com/repos/{$repo}/commits?per_page=1"); if ($response->successful()) { $data = $response->json(); if (! empty($data[0]['sha'])) { return substr($data[0]['sha'], 0, 7); } } } catch (\Exception) { // Ignore } return null; } public function getLatestRelease(string $githubUrl): ?string { if ($githubUrl === '' || $githubUrl === '0') { return null; } try { $repo = $this->extractRepo($githubUrl); $response = Http::timeout(10)->get("https://api.github.com/repos/{$repo}/releases/latest"); if ($response->successful()) { $data = $response->json(); if (! empty($data['tag_name'])) { return $data['tag_name']; } } } catch (\Exception) { // Ignore } return null; } public function hasUpdates(string $githubUrl, string $localCommit, string $branch = 'main'): bool { $remoteCommit = $this->getLatestCommit($githubUrl, $branch); if ($remoteCommit === null || $localCommit === 'N/A') { return false; } return $localCommit !== $remoteCommit; } }