| | } |
| | |
| | - /** |
| | - * Iterates over all repositories in the current path. |
| | - * Encapsulates the discovery and sorting (business logic). |
| | - */ |
| | - public function eachRepository(callable $callback): void { |
| | - if (!is_dir($this->path)) { |
| | - return; |
| | + public function getObjectSize(string $sha): int { |
| | + $loose = "{$this->objPath}/" . substr($sha, 0, 2) . "/" . substr($sha, 2); |
| | + if (file_exists($loose)) { |
| | + $f = @fopen($loose, 'rb'); |
| | + if (!$f) return 0; |
| | + $data = fread($f, 128); |
| | + fclose($f); |
| | + $inflated = @gzuncompress($data); |
| | + if (!$inflated) return 0; |
| | + $header = explode("\0", $inflated, 2)[0]; |
| | + $parts = explode(' ', $header); |
| | + return isset($parts[1]) ? (int)$parts[1] : 0; |
| | } |
| | + $data = $this->fromPack($sha); |
| | + return $data ? strlen($data) : 0; |
| | + } |
| | |
| | + public function eachRepository(callable $callback): void { |
| | + if (!is_dir($this->path)) return; |
| | $repos = []; |
| | - $globPath = $this->path . '/*.git'; |
| | - |
| | - foreach (glob($globPath) as $path) { |
| | + foreach (glob($this->path . '/*.git') as $path) { |
| | if (is_dir($path)) { |
| | $name = basename($path, '.git'); |
| | - $displayName = urldecode($name); |
| | - $repos[$name] = [ |
| | - 'path' => $path, |
| | - 'name' => $displayName, |
| | - 'safe_name' => $name |
| | - ]; |
| | + $repos[$name] = ['path' => $path, 'name' => urldecode($name), 'safe_name' => $name]; |
| | } |
| | - } |
| | - |
| | - // Sort alphabetically (Business Logic) |
| | - uasort($repos, function($a, $b) { |
| | - return strcasecmp($a['name'], $b['name']); |
| | - }); |
| | - |
| | - // Pass each repo to the lambda |
| | - foreach ($repos as $repo) { |
| | - $callback($repo); |
| | } |
| | + uasort($repos, fn($a, $b) => strcasecmp($a['name'], $b['name'])); |
| | + foreach ($repos as $repo) $callback($repo); |
| | } |
| | |
| | - /** |
| | - * Determines the primary branch (main, master, trunk, etc). |
| | - */ |
| | public function getMainBranch(): ?array { |
| | $branches = []; |
| | - $this->eachBranch(function($name, $sha) use (&$branches) { |
| | - $branches[$name] = $sha; |
| | - }); |
| | - |
| | - $priority = ['main', 'master', 'trunk', 'develop']; |
| | - foreach ($priority as $branch) { |
| | - if (isset($branches[$branch])) { |
| | - return ['name' => $branch, 'hash' => $branches[$branch]]; |
| | - } |
| | + $this->eachBranch(function($name, $sha) use (&$branches) { $branches[$name] = $sha; }); |
| | + foreach (['main', 'master', 'trunk', 'develop'] as $b) { |
| | + if (isset($branches[$b])) return ['name' => $b, 'hash' => $branches[$b]]; |
| | } |
| | - |
| | if (!empty($branches)) { |
| | - $first = array_key_first($branches); |
| | - return ['name' => $first, 'hash' => $branches[$first]]; |
| | + $f = array_key_first($branches); |
| | + return ['name' => $f, 'hash' => $branches[$f]]; |
| | } |
| | - |
| | return null; |
| | - } |
| | - |
| | - public function eachBranch(callable $callback): void { |
| | - $this->scanRefs('refs/heads', $callback); |
| | } |
| | |
| | - public function eachTag(callable $callback): void { |
| | - $this->scanRefs('refs/tags', $callback); |
| | - } |
| | + public function eachBranch(callable $callback): void { $this->scanRefs('refs/heads', $callback); } |
| | + public function eachTag(callable $callback): void { $this->scanRefs('refs/tags', $callback); } |
| | |
| | public function walk(string $refOrSha, callable $callback): void { |
| | $sha = $this->resolve($refOrSha); |
| | if (!$sha) return; |
| | - |
| | $data = $this->read($sha); |
| | if (!$data) return; |
| | - |
| | if (preg_match('/^tree ([0-9a-f]{40})$/m', $data, $m)) { |
| | - $treeSha = $m[1]; |
| | - $data = $this->read($treeSha); |
| | + $data = $this->read($m[1]); |
| | if (!$data) return; |
| | - } elseif (!$this->isTreeData($data)) { |
| | - return; |
| | - } |
| | + } elseif (!$this->isTreeData($data)) return; |
| | |
| | $pos = 0; |
| | while ($pos < strlen($data)) { |
| | $space = strpos($data, ' ', $pos); |
| | $null = strpos($data, "\0", $space); |
| | if ($space === false || $null === false) break; |
| | - |
| | $mode = substr($data, $pos, $space - $pos); |
| | $name = substr($data, $space + 1, $null - $space - 1); |
| | $entrySha = bin2hex(substr($data, $null + 1, 20)); |
| | - |
| | - $callback((object)[ |
| | - 'name' => $name, |
| | - 'sha' => $entrySha, |
| | - 'isDir' => ($mode === '40000'), |
| | - 'mode' => $mode |
| | - ]); |
| | - |
| | + $callback((object)['name' => $name, 'sha' => $entrySha, 'isDir' => ($mode === '40000'), 'mode' => $mode]); |
| | $pos = $null + 21; |
| | } |
| | } |
| | |
| | private function isTreeData(string $data): bool { |
| | if (strlen($data) < 25) return false; |
| | if (preg_match('/^(40000|100644|100755|120000) /', $data)) { |
| | $null = strpos($data, "\0"); |
| | - if ($null !== false && ($null + 21 <= strlen($data))) { |
| | - return true; |
| | - } |
| | + return ($null !== false && ($null + 21 <= strlen($data))); |
| | } |
| | return false; |
| | } |
| | |
| | public function history(string $refOrSha, int $limit, callable $callback): void { |
| | $currentSha = $this->resolve($refOrSha); |
| | $count = 0; |
| | - |
| | while ($currentSha && $count < $limit) { |
| | $data = $this->read($currentSha); |
| | if (!$data) break; |
| | - |
| | $message = (strpos($data, "\n\n") !== false) ? substr($data, strpos($data, "\n\n") + 2) : ''; |
| | preg_match('/^author (.*) <(.*)> (\d+)/m', $data, $auth); |
| | - |
| | - $callback((object)[ |
| | - 'sha' => $currentSha, |
| | - 'message' => trim($message), |
| | - 'author' => $auth[1] ?? 'Unknown', |
| | - 'email' => $auth[2] ?? '', |
| | - 'date' => isset($auth[3]) ? (int)$auth[3] : 0 |
| | - ]); |
| | - |
| | + $callback((object)['sha' => $currentSha, 'message' => trim($message), 'author' => $auth[1] ?? 'Unknown', 'email' => $auth[2] ?? '', 'date' => (int)($auth[3] ?? 0)]); |
| | $currentSha = preg_match('/^parent ([0-9a-f]{40})$/m', $data, $m) ? $m[1] : null; |
| | $count++; |
 |
| | private function resolve(string $input): ?string { |
| | if (preg_match('/^[0-9a-f]{40}$/', $input)) return $input; |
| | - |
| | if ($input === 'HEAD' && file_exists($h = "{$this->path}/HEAD")) { |
| | $head = trim(file_get_contents($h)); |
| | return (strpos($head, 'ref: ') === 0) ? $this->resolve(substr($head, 5)) : $head; |
| | } |
| | - |
| | foreach ([$input, "refs/heads/$input", "refs/tags/$input"] as $p) { |
| | if (file_exists($f = "{$this->path}/$p")) return trim(file_get_contents($f)); |
| | } |
| | - |
| | if (file_exists($packed = "{$this->path}/packed-refs")) { |
| | foreach (file($packed) as $line) { |
| | if ($line[0] === '#' || $line[0] === '^') continue; |
| | $parts = explode(' ', trim($line)); |
| | - if (count($parts) < 2) continue; |
| | - if ($parts[1] === $input || $parts[1] === "refs/heads/$input" || $parts[1] === "refs/tags/$input") return $parts[0]; |
| | + if (count($parts) >= 2 && ($parts[1] === $input || $parts[1] === "refs/heads/$input" || $parts[1] === "refs/tags/$input")) return $parts[0]; |
| | } |
| | } |
| | return null; |
| | - } |
| | - |
| | - private function findTree(string $sha): ?string { |
| | - $data = $this->read($sha); |
| | - return ($data && preg_match('/^tree ([0-9a-f]{40})$/m', $data, $m)) ? $m[1] : null; |
| | } |
| | |
 |
| | $packs = glob("{$this->objPath}/pack/*.idx"); |
| | if (!$packs) return null; |
| | - |
| | foreach ($packs as $idxFile) { |
| | $f = @fopen($idxFile, 'rb'); |
| | if (!$f) continue; |
| | - |
| | fseek($f, 8 + (hexdec(substr($sha, 0, 2)) * 4)); |
| | $count = unpack('N', fread($f, 4))[1]; |
| | fseek($f, 8 + (255 * 4)); |
| | $total = unpack('N', fread($f, 4))[1]; |
| | - |
| | fseek($f, 8 + (256 * 4)); |
| | $idx = -1; |
| | for ($i = 0; $i < $total; $i++) { |
| | if (bin2hex(fread($f, 20)) === $sha) { $idx = $i; break; } |
| | } |
| | if ($idx === -1) { fclose($f); continue; } |
| | - |
| | fseek($f, 8 + (256 * 4) + ($total * 20) + ($total * 4) + ($idx * 4)); |
| | $offset = unpack('N', fread($f, 4))[1]; |
| | fclose($f); |
| | - |
| | $pf = @fopen(str_replace('.idx', '.pack', $idxFile), 'rb'); |
| | if (!$pf) continue; |
| | fseek($pf, $offset); |
| | $header = ord(fread($pf, 1)); |
| | while ($header & 128) { $header = ord(fread($pf, 1)); } |
| | - |
| | $data = @gzuncompress(fread($pf, 1024 * 512)); |
| | fclose($pf); |
| | return $data ?: null; |
| | } |
| | return null; |
| | } |
| | } |
| | + |
| | |