Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/treetrek.git

Updates viewer to use Git class

AuthorDave Jarvis <email>
Date2026-02-08 14:54:46 GMT-0800
Commit19af2da5e79d7e59a0d060dd06ce472150521299
Parent10dff41
Git.php
<?php
class Git {
- private $repoPath;
- private $gitDir;
-
- public function __construct( $repoPath ) {
- $repoPath = realpath($repoPath);
-
- if ($repoPath === false) {
- throw new Exception( "Invalid repository path" );
- }
-
- $allowedBase = realpath(REPOS_PATH);
- if ($allowedBase === false || strpos($repoPath, $allowedBase) !== 0) {
- throw new Exception( "Repository path outside allowed directory" );
- }
-
- $this->repoPath = rtrim( $repoPath, '/' );
- $this->gitDir = $this->repoPath;
-
- if( !is_dir( $this->gitDir . '/objects' ) ||
- !is_dir( $this->gitDir . '/refs' ) ) {
- throw new Exception( "Not a valid bare git repository: {$repoPath}" );
- }
- }
-
- /**
- * Iterate over all files using a callback/lambda.
-
- * @param callable $callback Function to call for each file.
- */
- public function forEachFile( callable $callback ) {
- $treeHash = $this->getHeadTreeHash();
- $this->traverseTree( $treeHash, '', $callback );
- }
-
- /**
- * Get the tree hash for HEAD commit.
- */
- private function getHeadTreeHash() {
- $head = $this->getHead();
- $commitHash = $this->resolveRef( $head );
- $commit = $this->readCommit( $commitHash );
- return $commit['tree'];
- }
-
- /**
- * Get HEAD reference
- */
- private function getHead() {
- $headFile = $this->gitDir . '/HEAD';
- if( !file_exists( $headFile ) ) {
- throw new Exception( "HEAD file not found" );
- }
-
- $content = trim( file_get_contents( $headFile ) );
-
- // HEAD typically contains "ref: refs/heads/main" or similar
- if( strpos( $content, 'ref:' ) === 0 ) {
- return trim( substr( $content, 4 ) );
- }
-
- return $content;
- }
-
- /**
- * Resolve a reference to a commit hash
- */
- private function resolveRef( $ref ) {
- $refFile = $this->gitDir . '/' . $ref;
-
- if( file_exists( $refFile ) ) {
- return trim( file_get_contents( $refFile ) );
- }
-
- $packedRefs = $this->gitDir . '/packed-refs';
-
- if( file_exists( $packedRefs ) ) {
- $lines = file( $packedRefs, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
- foreach ( $lines as $line ) {
- if( $line[0] === '#' || $line[0] === '^' ) continue;
-
- $parts = preg_split( '/\s+/', $line, 2 );
- if( count( $parts ) === 2 && $parts[1] === $ref ) {
- return $parts[0];
- }
- }
- }
-
- throw new Exception( "Could not resolve ref: {$ref}" );
- }
-
- /**
- * Read a Git object.
- */
- private function readObject( $hash ) {
- $objectPath = $this->gitDir . '/objects/' . substr( $hash, 0, 2 ) . '/' . substr( $hash, 2 );
-
- if( !file_exists( $objectPath ) ) {
- throw new Exception( "Object not found: {$hash}" );
- }
-
- $compressed = file_get_contents( $objectPath );
- $data = @gzuncompress( $compressed );
-
- if( $data === false ) {
- throw new Exception( "Failed to decompress object: {$hash}" );
- }
-
- // Parse header: "type size\0content"
- $nullPos = strpos( $data, "\0" );
- $header = substr( $data, 0, $nullPos );
- $content = substr( $data, $nullPos + 1 );
-
- list( $type, $size ) = explode( ' ', $header, 2 );
-
- return [
- 'type' => $type,
- 'size' => ( int )$size,
- 'content' => $content
- ];
- }
-
- /**
- * Read a commit object.
- */
- private function readCommit( $hash ) {
- $obj = $this->readObject( $hash );
-
- if( $obj['type'] !== 'commit' ) {
- throw new Exception( "Expected commit object, got {$obj['type']}" );
- }
-
- $lines = explode( "\n", $obj['content'] );
- $commit = [];
-
- foreach ( $lines as $line ) {
- if( strpos( $line, 'tree ' ) === 0 ) {
- $commit['tree'] = substr( $line, 5 );
- } elseif( strpos( $line, 'author ' ) === 0 ) {
- $commit['author'] = $this->parseCommitLine( substr( $line, 7 ) );
- } elseif( strpos( $line, 'committer ' ) === 0 ) {
- $commit['committer'] = $this->parseCommitLine( substr( $line, 10 ) );
- }
- }
-
- return $commit;
- }
-
- /**
- * Parse author/committer line.
- */
- private function parseCommitLine( $line ) {
- if( preg_match( '/^(.+?)\s+<(.+?)>\s+(\d+)\s+([+-]\d{4})$/', $line, $matches ) ) {
- return [
- 'name' => $matches[1],
- 'email' => $matches[2],
- 'timestamp' => ( int )$matches[3],
- 'timezone' => $matches[4],
- 'date' => date( 'Y-m-d H:i:s', ( int )$matches[3] )
- ];
- }
-
- return ['name' => $line, 'date' => null];
- }
-
- /**
- * Traverse tree and call callback for each file.
- */
- private function traverseTree( $hash, $prefix, callable $callback ) {
- $obj = $this->readObject( $hash );
-
- if( $obj['type'] !== 'tree' ) {
- throw new Exception( "Expected tree object, got {$obj['type']}" );
- }
-
- $content = $obj['content'];
- $pos = 0;
- $len = strlen( $content );
-
- while ( $pos < $len ) {
- $nullPos = strpos( $content, "\0", $pos );
- $modeAndName = substr( $content, $pos, $nullPos - $pos );
-
- list( $mode, $name ) = explode( ' ', $modeAndName, 2 );
-
- $objHash = bin2hex( substr( $content, $nullPos + 1, 20 ) );
-
- $path = $prefix . $name;
-
- if( $mode === '40000' ) {
- // Subdirectory, recurse
- $this->traverseTree( $objHash, $path . '/', $callback );
- } else {
- // File - read blob and get media type
- $blobObj = $this->readObject( $objHash );
- $blobContent = $blobObj['content'];
-
- // Inspect content first, fallback to extension
- // Note: MediaTypeSniffer must be defined elsewhere
- $mediaType = MediaTypeSniffer::getMediaType( $blobContent );
- if( $mediaType === 'application/octet-stream' ) {
- $mediaType = MediaTypeSniffer::getMediaTypeByExtension( $path );
- }
-
- $commitInfo = $this->getHeadCommitInfo();
-
- $fileInfo = [
- 'path' => $path,
- 'name' => basename( $path ),
- 'size' => $blobObj['size'],
- 'mode' => $mode,
- 'hash' => $objHash,
- 'media_type' => $mediaType,
- 'last_updated' => $commitInfo['date'],
- 'contributor' => $commitInfo['name']
- ];
-
- $callback( $fileInfo );
- }
-
- $pos = $nullPos + 21;
- }
- }
-
- /**
- * Get HEAD commit information (date and contributor).
- */
- private function getHeadCommitInfo() {
- static $cache = null;
-
- if( $cache === null ) {
- try {
- $head = $this->getHead();
- $commitHash = $this->resolveRef( $head );
- $commit = $this->readCommit( $commitHash );
-
- $cache = [
- 'name' => $commit['committer']['name'] ?? 'Unknown',
- 'date' => $commit['committer']['date'] ?? null
- ];
- } catch ( Exception $e ) {
- $cache = [
- 'name' => 'Unknown',
- 'date' => null
- ];
- }
- }
-
- return $cache;
- }
-}
-?>
+ private string $basePath;
+
+ public function __construct(string $basePath) {
+ $this->basePath = realpath($basePath) ?: '';
+ if ($this->basePath === '' || !is_dir($this->basePath)) {
+ throw new InvalidArgumentException('Invalid base path');
+ }
+ }
+
+ public function listRepositories(): array {
+ $repos = [];
+ $iterator = new DirectoryIterator($this->basePath);
+ foreach ($iterator as $item) {
+ if ($item->isDot()) continue;
+ if ($item->isDir()) {
+ $gitDir = $item->getPathname() . DIRECTORY_SEPARATOR . '.git';
+ if (is_dir($gitDir)) {
+ $repos[] = $item->getBasename();
+ }
+ }
+ }
+ sort($repos);
+ return $repos;
+ }
+
+ public function readRepository(string $name): array {
+ $repoPath = $this->basePath . DIRECTORY_SEPARATOR . $name;
+ $gitPath = $repoPath . DIRECTORY_SEPARATOR . '.git';
+
+ if (!is_dir($gitPath)) {
+ throw new InvalidArgumentException('Not a git repository');
+ }
+
+ $result = [
+ 'files' => [],
+ 'directories' => [],
+ 'commits' => [],
+ 'tags' => []
+ ];
+
+ $result['files'] = $this->scanFiles($repoPath);
+ $result['directories'] = $this->scanDirectories($repoPath);
+ $result['commits'] = $this->readCommits($gitPath);
+ $result['tags'] = $this->readTags($gitPath);
+
+ return $result;
+ }
+
+ private function scanFiles(string $path): array {
+ $files = [];
+ $iterator = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
+ RecursiveIteratorIterator::LEAVES_ONLY
+ );
+
+ foreach ($iterator as $file) {
+ if ($file->isFile()) {
+ $relative = str_replace($path . DIRECTORY_SEPARATOR, '', $file->getPathname());
+ if (strpos($relative, '.git' . DIRECTORY_SEPARATOR) !== 0 && $relative !== '.git') {
+ $files[] = $relative;
+ }
+ }
+ }
+
+ sort($files);
+ return $files;
+ }
+
+ private function scanDirectories(string $path): array {
+ $dirs = [];
+ $iterator = new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
+ RecursiveIteratorIterator::SELF_FIRST
+ );
+
+ foreach ($iterator as $dir) {
+ if ($dir->isDir()) {
+ $relative = str_replace($path . DIRECTORY_SEPARATOR, '', $dir->getPathname());
+ if (strpos($relative, '.git' . DIRECTORY_SEPARATOR) !== 0 && $relative !== '.git') {
+ $dirs[] = $relative;
+ }
+ }
+ }
+
+ sort($dirs);
+ return $dirs;
+ }
+
+ private function readCommits(string $gitPath): array {
+ $commits = [];
+ $headFile = $gitPath . DIRECTORY_SEPARATOR . 'HEAD';
+
+ if (!file_exists($headFile)) {
+ return $commits;
+ }
+
+ $head = trim(file_get_contents($headFile));
+ $ref = '';
+
+ if (strpos($head, 'ref: ') === 0) {
+ $refPath = $gitPath . DIRECTORY_SEPARATOR . substr($head, 5);
+ if (file_exists($refPath)) {
+ $ref = trim(file_get_contents($refPath));
+ } else {
+ $packedRefs = $this->readPackedRefs($gitPath, substr($head, 5));
+ if ($packedRefs !== null) {
+ $ref = $packedRefs;
+ }
+ }
+ } else {
+ $ref = $head;
+ }
+
+ if ($ref === '' || strlen($ref) !== 40 || !ctype_xdigit($ref)) {
+ return $commits;
+ }
+
+ $this->walkCommits($gitPath, $ref, $commits, 0);
+ return $commits;
+ }
+
+ private function readPackedRefs(string $gitPath, string $refName): ?string {
+ $packedPath = $gitPath . DIRECTORY_SEPARATOR . 'packed-refs';
+ if (!file_exists($packedPath)) {
+ return null;
+ }
+
+ $content = file_get_contents($packedPath);
+ $lines = explode("\n", $content);
+
+ foreach ($lines as $line) {
+ $line = trim($line);
+ if ($line === '' || $line[0] === '#') continue;
+
+ $parts = preg_split('/\s+/', $line, 2);
+ if (count($parts) === 2 && $parts[1] === $refName) {
+ $sha = $parts[0];
+ if (strlen($sha) === 40 && ctype_xdigit($sha)) {
+ return $sha;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private function walkCommits(string $gitPath, string $sha, array &$commits, int $depth): void {
+ if ($depth > 1000) return;
+ if (strlen($sha) !== 40 || !ctype_xdigit($sha)) return;
+
+ foreach ($commits as $existing) {
+ if ($existing['sha'] === $sha) return;
+ }
+
+ $content = $this->getObject($gitPath, $sha);
+ if ($content === null) return;
+
+ $nullPos = strpos($content, "\x00");
+ if ($nullPos === false) return;
+
+ $header = substr($content, 0, $nullPos);
+ $body = substr($content, $nullPos + 1);
+
+ if (!str_starts_with($header, 'commit ')) return;
+
+ $lines = explode("\n", $body);
+ $commit = ['sha' => $sha, 'tree' => '', 'parent' => [], 'author' => '', 'committer' => '', 'message' => ''];
+ $inMessage = false;
+ $messageLines = [];
+
+ foreach ($lines as $line) {
+ if ($inMessage) {
+ $messageLines[] = $line;
+ } elseif ($line === '') {
+ $inMessage = true;
+ } elseif (str_starts_with($line, 'tree ')) {
+ $commit['tree'] = substr($line, 5);
+ } elseif (str_starts_with($line, 'parent ')) {
+ $parent = substr($line, 7);
+ if (strlen($parent) === 40 && ctype_xdigit($parent)) {
+ $commit['parent'][] = $parent;
+ }
+ } elseif (str_starts_with($line, 'author ')) {
+ $commit['author'] = substr($line, 7);
+ } elseif (str_starts_with($line, 'committer ')) {
+ $commit['committer'] = substr($line, 10);
+ }
+ }
+
+ $commit['message'] = implode("\n", $messageLines);
+ $commits[] = $commit;
+
+ foreach ($commit['parent'] as $parent) {
+ $this->walkCommits($gitPath, $parent, $commits, $depth + 1);
+ }
+ }
+
+ private function readTags(string $gitPath): array {
+ $tags = [];
+ $tagsPath = $gitPath . DIRECTORY_SEPARATOR . 'refs' . DIRECTORY_SEPARATOR . 'tags';
+
+ if (is_dir($tagsPath)) {
+ $iterator = new DirectoryIterator($tagsPath);
+ foreach ($iterator as $item) {
+ if ($item->isFile() && !$item->isDot()) {
+ $name = $item->getBasename();
+ $sha = trim(file_get_contents($item->getPathname()));
+ if (strlen($sha) === 40 && ctype_xdigit($sha)) {
+ $tags[] = ['name' => $name, 'sha' => $sha];
+ }
+ }
+ }
+ }
+
+ $packedPath = $gitPath . DIRECTORY_SEPARATOR . 'packed-refs';
+ if (file_exists($packedPath)) {
+ $content = file_get_contents($packedPath);
+ $lines = explode("\n", $content);
+
+ foreach ($lines as $line) {
+ $line = trim($line);
+ if ($line === '' || $line[0] === '#') continue;
+
+ $parts = preg_split('/\s+/', $line, 2);
+ if (count($parts) === 2 && str_starts_with($parts[1], 'refs/tags/')) {
+ $name = substr($parts[1], 10);
+ $sha = $parts[0];
+ if (strlen($sha) === 40 && ctype_xdigit($sha)) {
+ $exists = false;
+ foreach ($tags as $tag) {
+ if ($tag['name'] === $name) {
+ $exists = true;
+ break;
+ }
+ }
+ if (!$exists) {
+ $tags[] = ['name' => $name, 'sha' => $sha];
+ }
+ }
+ }
+ }
+ }
+
+ usort($tags, fn($a, $b) => strcmp($a['name'], $b['name']));
+ return $tags;
+ }
+
+ private function getObject(string $gitPath, string $sha): ?string {
+ $loosePath = $gitPath . DIRECTORY_SEPARATOR . 'objects' . DIRECTORY_SEPARATOR . substr($sha, 0, 2) . DIRECTORY_SEPARATOR . substr($sha, 2);
+ if (file_exists($loosePath)) {
+ $content = file_get_contents($loosePath);
+ if ($content === false) return null;
+ $uncompressed = @gzuncompress($content);
+ return $uncompressed !== false ? $uncompressed : null;
+ }
+
+ return $this->getPackedObject($gitPath, $sha);
+ }
+
+ private function getPackedObject(string $gitPath, string $sha): ?string {
+ $packDir = $gitPath . DIRECTORY_SEPARATOR . 'objects' . DIRECTORY_SEPARATOR . 'pack';
+ if (!is_dir($packDir)) return null;
+
+ foreach (glob($packDir . DIRECTORY_SEPARATOR . '*.idx') as $idxFile) {
+ $content = $this->readIndexFile($idxFile);
+ if ($content === null) continue;
+
+ $binSha = hex2bin($sha);
+ if (!isset($content[$binSha])) continue;
+
+ $offset = $content[$binSha];
+ $packFile = substr($idxFile, 0, -3) . 'pack';
+
+ $obj = $this->unpackObject($packFile, $offset, $gitPath);
+ if ($obj !== null) return $obj;
+ }
+
+ return null;
+ }
+
+ private function readIndexFile(string $path): ?array {
+ $content = file_get_contents($path);
+ if ($content === false) return null;
+
+ $offset = 0;
+ $signature = substr($content, 0, 4);
+
+ if ($signature === "\xFFtOc") {
+ $version = unpack('N', substr($content, 4, 4))[1];
+ $offset = 8;
+ } else {
+ $version = 1;
+ }
+
+ if ($version !== 1 && $version !== 2) return null;
+
+ $fanout = unpack('N*', substr($content, $offset, 256 * 4));
+ $offset += 256 * 4;
+ $count = $fanout[256];
+
+ $result = [];
+ if ($version === 1) {
+ for ($i = 0; $i < $count; $i++) {
+ $sha = substr($content, $offset + 4, 20);
+ $off = unpack('N', substr($content, $offset, 4))[1];
+ $result[$sha] = $off;
+ $offset += 24;
+ }
+ } else {
+ $shas = [];
+ for ($i = 0; $i < $count; $i++) {
+ $shas[] = substr($content, $offset, 20);
+ $offset += 20;
+ }
+ $offset += $count * 4;
+ for ($i = 0; $i < $count; $i++) {
+ $off = unpack('N', substr($content, $offset, 4))[1];
+ $result[$shas[$i]] = $off;
+ $offset += 4;
+ }
+ }
+
+ return $result;
+ }
+
+ private function unpackObject(string $packFile, int $offset, string $gitPath): ?string {
+ $fp = fopen($packFile, 'rb');
+ if (!$fp) return null;
+
+ fseek($fp, $offset, SEEK_SET);
+ $byte = ord(fread($fp, 1));
+ $type = ($byte >> 4) & 7;
+ $size = $byte & 0xF;
+ $shift = 4;
+
+ while (($byte & 0x80) !== 0) {
+ $byte = ord(fread($fp, 1));
+ $size |= ($byte & 0x7F) << $shift;
+ $shift += 7;
+ }
+
+ switch ($type) {
+ case 1: case 2: case 3: case 4:
+ return $this->readCompressed($fp, $size);
+ case 6:
+ return $this->readOfsDelta($fp, $offset, $size, $gitPath);
+ case 7:
+ return $this->readRefDelta($fp, $size, $gitPath);
+ default:
+ fclose($fp);
+ return null;
+ }
+ }
+
+ private function readCompressed($fp, int $size): ?string {
+ $data = '';
+ stream_filter_append($fp, 'zlib.inflate', STREAM_FILTER_READ);
+ while (strlen($data) < $size && !feof($fp)) {
+ $chunk = fread($fp, $size - strlen($data));
+ if ($chunk === false) break;
+ $data .= $chunk;
+ }
+ fclose($fp);
+ return strlen($data) === $size ? $data : null;
+ }
+
+ private function readOfsDelta($fp, int $objOffset, int $deltaSize, string $gitPath): ?string {
+ $offset = 0;
+ $byte = ord(fread($fp, 1));
+ $baseOffset = $byte & 0x7F;
+ while (($byte & 0x80) !== 0) {
+ $byte = ord(fread($fp, 1));
+ $baseOffset = (($baseOffset + 1) << 7) | ($byte & 0x7F);
+ }
+
+ $baseOffset = $objOffset - $baseOffset;
+ fclose($fp);
+
+ $packFile = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['args'][0];
+ $base = $this->unpackObject($packFile, $baseOffset, $gitPath);
+ if ($base === null) return null;
+
+ $delta = $this->readDeltaData($packFile, $objOffset + 1 + strlen(decbin($objOffset - $baseOffset)) / 7, $deltaSize);
+ if ($delta === null) return null;
+
+ return $this->applyDelta($base, $delta);
+ }
+
+ private function readRefDelta($fp, int $deltaSize, string $gitPath): ?string {
+ $sha = bin2hex(fread($fp, 20));
+ fclose($fp);
+
+ $base = $this->getObject($gitPath, $sha);
+ if ($base === null) return null;
+
+ $packFile = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['args'][0];
+ $delta = $this->readDeltaData($packFile, ftell($fp), $deltaSize);
+ if ($delta === null) return null;
+
+ return $this->applyDelta($base, $delta);
+ }
+
+ private function readDeltaData(string $packFile, int $offset, int $size): ?string {
+ $fp = fopen($packFile, 'rb');
+ if (!$fp) return null;
+ fseek($fp, $offset, SEEK_SET);
+
+ stream_filter_append($fp, 'zlib.inflate', STREAM_FILTER_READ);
+ $data = '';
+ while (strlen($data) < $size && !feof($fp)) {
+ $chunk = fread($fp, $size - strlen($data));
+ if ($chunk === false) break;
+ $data .= $chunk;
+ }
+ fclose($fp);
+ return strlen($data) === $size ? $data : null;
+ }
+
+ private function applyDelta(string $base, string $delta): ?string {
+ $pos = 0;
+ $srcSize = 0;
+ do {
+ $byte = ord($delta[$pos++]);
+ $srcSize |= ($byte & 0x7F) << (($pos - 1) * 7);
+ } while (($byte & 0x80) !== 0);
+
+ $dstSize = 0;
+ $shift = 0;
+ do {
+ $byte = ord($delta[$pos++]);
+ $dstSize |= ($byte & 0x7F) << $shift;
+ $shift += 7;
+ } while (($byte & 0x80) !== 0);
+
+ if (strlen($base) !== $srcSize) return null;
+
+ $result = '';
+ $deltaLen = strlen($delta);
+
+ while ($pos < $deltaLen) {
+ $byte = ord($delta[$pos++]);
+
+ if (($byte & 0x80) !== 0) {
+ $cpOff = 0;
+ $cpSize = 0;
+
+ if ($byte & 0x01) $cpOff |= ord($delta[$pos++]);
+ if ($byte & 0x02) $cpOff |= ord($delta[$pos++]) << 8;
+ if ($byte & 0x04) $cpOff |= ord($delta[$pos++]) << 16;
+ if ($byte & 0x08) $cpOff |= ord($delta[$pos++]) << 24;
+
+ if ($byte & 0x10) $cpSize |= ord($delta[$pos++]);
+ if ($byte & 0x20) $cpSize |= ord($delta[$pos++]) << 8;
+ if ($byte & 0x40) $cpSize |= ord($delta[$pos++]) << 16;
+
+ if ($cpSize === 0) $cpSize = 0x10000;
+
+ $result .= substr($base, $cpOff, $cpSize);
+ } elseif ($byte !== 0) {
+ $result .= substr($delta, $pos, $byte);
+ $pos += $byte;
+ } else {
+ return null;
+ }
+ }
+
+ return strlen($result) === $dstSize ? $result : null;
+ }
+}
+
kimi-viewer.php
*/
-function getHomeDirectory() {
- if (!empty($_SERVER['HOME'])) {
- return $_SERVER['HOME'];
- }
-
- if (!empty(getenv('HOME'))) {
- return getenv('HOME');
- }
-
- if (function_exists('posix_getpwuid') && function_exists('posix_getuid')) {
- $userInfo = posix_getpwuid(posix_getuid());
-
- if (!empty($userInfo['dir'])) {
- return $userInfo['dir'];
- }
- }
-
- return '';
-}
-
-define('REPOS_PATH', getHomeDirectory() . '/repos');
-define('SITE_TITLE', "Dave Jarvis' Repositories");
-
-ini_set('display_errors', 0);
-ini_set('log_errors', 1);
-ini_set('error_log', __DIR__ . '/error.log');
-
-function getRepositories() {
- $repos = [];
- if (!is_dir(REPOS_PATH)) {
- return $repos;
- }
-
- foreach (glob(REPOS_PATH . '/*.git') as $path) {
- if (is_dir($path)) {
- $name = basename($path, '.git');
- $displayName = urldecode($name);
- $repos[$name] = [
- 'path' => $path,
- 'name' => $displayName,
- 'safe_name' => $name
- ];
- }
- }
-
- uasort($repos, function($a, $b) {
- return strcasecmp($a['name'], $b['name']);
- });
-
- return $repos;
-}
-
-function getCurrentRepo() {
- $repos = getRepositories();
- $requested = $_GET['repo'] ?? '';
- $decodedRequested = urldecode($requested);
-
- foreach ($repos as $key => $repo) {
- if ($repo['safe_name'] === $requested || $repo['name'] === $decodedRequested) {
- return $repo;
- }
- }
-
- return null;
-}
-
-function sanitizePath($path) {
- $path = str_replace(['..', '\\', "\0"], ['', '/', ''], $path);
- return preg_replace('/[^a-zA-Z0-9_\-\.\/]/', '', $path);
-}
-
-function getObjectPath($repoPath, $hash) {
- if (!preg_match('/^[a-f0-9]{40}$/', $hash)) {
- return false;
- }
- $dir = substr($hash, 0, 2);
- $file = substr($hash, 2);
- return $repoPath . '/objects/' . $dir . '/' . $file;
-}
-
-function readPackedObject($repoPath, $hash) {
- $packDir = $repoPath . '/objects/pack';
- if (!is_dir($packDir)) {
- return false;
- }
-
- foreach (glob($packDir . '/*.idx') as $idxFile) {
- $result = readFromPackIndex($idxFile, $hash);
- if ($result !== false) {
- return $result;
- }
- }
-
- return false;
-}
-
-function readFromPackIndex($idxFile, $hash) {
- $packFile = str_replace('.idx', '.pack', $idxFile);
- if (!file_exists($packFile)) {
- return false;
- }
-
- $idx = fopen($idxFile, 'rb');
- if (!$idx) return false;
-
- $magic = fread($idx, 4);
- $version = 0;
-
- if ($magic === "\377tOc") {
- $versionData = fread($idx, 4);
- $version = unpack('N', $versionData)[1];
- if ($version !== 2) {
- fclose($idx);
- return false;
- }
- } else {
- fseek($idx, 0);
- }
-
- fseek($idx, 256 * 4 - 4);
- $numObjects = unpack('N', fread($idx, 4))[1];
-
- fseek($idx, 256 * 4);
- $targetHash = hex2bin($hash);
- $left = 0;
- $right = $numObjects - 1;
- $foundOffset = -1;
-
- while ($left <= $right) {
- $mid = (int)(($left + $right) / 2);
- fseek($idx, 256 * 4 + $mid * 20);
- $midHash = fread($idx, 20);
-
- $cmp = strcmp($midHash, $targetHash);
- if ($cmp === 0) {
- fseek($idx, 256 * 4 + $numObjects * 20 + $mid * 4);
- $offset = unpack('N', fread($idx, 4))[1];
-
- if ($offset & 0x80000000) {
- fseek($idx, 256 * 4 + $numObjects * 24 + ($offset & 0x7fffffff) * 8);
- $offset = unpack('J', fread($idx, 8))[1];
- }
-
- $foundOffset = $offset;
- break;
- } elseif ($cmp < 0) {
- $left = $mid + 1;
- } else {
- $right = $mid - 1;
- }
- }
-
- fclose($idx);
-
- if ($foundOffset < 0) {
- return false;
- }
-
- return readPackObject($packFile, $foundOffset);
-}
-
-/**
- * FIXED: Uses stream-aware decompression to avoid trailing data errors
- */
-function uncompressGitData($handle) {
- $inflator = inflate_init(ZLIB_ENCODING_ANY);
- $output = '';
- while (!feof($handle)) {
- $chunk = fread($handle, 8192);
- if ($chunk === false) break;
- $output .= inflate_add($inflator, $chunk, PHP_ZLIB_FINISH_FLUSH);
- if (inflate_get_status($inflator) === ZLIB_STREAM_END) break;
- }
- return $output;
-}
-
-function readPackObject($packFile, $offset) {
- $pack = fopen($packFile, 'rb');
- if (!$pack) return false;
-
- fseek($pack, $offset);
- $byte = ord(fread($pack, 1));
- $type = ($byte >> 4) & 0x07;
- $size = $byte & 0x0f;
- $shift = 4;
-
- while ($byte & 0x80) {
- $byte = ord(fread($pack, 1));
- $size |= ($byte & 0x7f) << $shift;
- $shift += 7;
- }
-
- // Handle Offset Deltas (Type 6)
- if ($type === 6) {
- $byte = ord(fread($pack, 1));
- $baseOffset = $byte & 0x7f;
- while ($byte & 0x80) {
- $byte = ord(fread($pack, 1));
- $baseOffset = (($baseOffset + 1) << 7) | ($byte & 0x7f);
- }
- $deltaData = uncompressGitData($pack);
- $baseObj = readPackObject($packFile, $offset - $baseOffset);
- fclose($pack);
- return [
- 'type' => $baseObj['type'],
- 'content' => applyGitDelta($baseObj['content'], $deltaData)
- ];
- }
-
- // Standard Objects (Commit, Tree, Blob)
- $uncompressed = uncompressGitData($pack);
- fclose($pack);
-
- $types = ['', 'commit', 'tree', 'blob', 'tag'];
- return [
- 'type' => $types[$type] ?? 'unknown',
- 'content' => $uncompressed
- ];
-}
-
-function applyGitDelta($base, $delta) {
- $pos = 0;
- $readVarInt = function() use (&$delta, &$pos) {
- $res = 0; $shift = 0;
- do {
- $b = ord($delta[$pos++]);
- $res |= ($b & 0x7f) << $shift;
- $shift += 7;
- } while ($b & 0x80);
- return $res;
- };
-
- $baseSize = $readVarInt();
- $targetSize = $readVarInt();
- $res = '';
-
- while ($pos < strlen($delta)) {
- $opcode = ord($delta[$pos++]);
- if ($opcode & 0x80) { // Copy from base
- $off = 0; $sz = 0;
- if ($opcode & 0x01) $off |= ord($delta[$pos++]);
- if ($opcode & 0x02) $off |= ord($delta[$pos++] ) << 8;
- if ($opcode & 0x04) $off |= ord($delta[$pos++] ) << 16;
- if ($opcode & 0x08) $off |= ord($delta[$pos++] ) << 24;
- if ($opcode & 0x10) $sz |= ord($delta[$pos++]);
- if ($opcode & 0x20) $sz |= ord($delta[$pos++] ) << 8;
- if ($opcode & 0x40) $sz |= ord($delta[$pos++] ) << 16;
- if ($sz === 0) $sz = 0x10000;
- $res .= substr($base, $off, $sz);
- } else { // Insert new data
- $res .= substr($delta, $pos, $opcode);
- $pos += $opcode;
- }
- }
- return $res;
-}
-
-function readGitObject($repoPath, $hash) {
- $path = getObjectPath($repoPath, $hash);
- if ($path && file_exists($path)) {
- $content = @file_get_contents($path);
- if ($content !== false) {
- $decompressed = @gzuncompress($content);
- if ($decompressed !== false) {
- $nullPos = strpos($decompressed, "\0");
- if ($nullPos !== false) {
- $header = substr($decompressed, 0, $nullPos);
- $parts = explode(' ', $header, 2);
- return [
- 'type' => $parts[0] ?? 'unknown',
- 'size' => $parts[1] ?? 0,
- 'content' => substr($decompressed, $nullPos + 1)
- ];
- }
- }
- }
- }
-
- return readPackedObject($repoPath, $hash);
-}
-
-function parseTree($content) {
- $entries = [];
- $offset = 0;
- $len = strlen($content);
-
- while ($offset < $len) {
- $spacePos = strpos($content, ' ', $offset);
- if ($spacePos === false) break;
-
- $mode = substr($content, $offset, $spacePos - $offset);
- $offset = $spacePos + 1;
-
- $nullPos = strpos($content, "\0", $offset);
- if ($nullPos === false) break;
-
- $name = substr($content, $offset, $nullPos - $offset);
- $offset = $nullPos + 1;
-
- if ($offset + 20 > $len) break;
- $hash = bin2hex(substr($content, $offset, 20));
- $offset += 20;
-
- $isTree = in_array($mode, ['040000', '40000', '160000']);
-
- $entries[] = [
- 'mode' => $mode,
- 'name' => $name,
- 'hash' => $hash,
- 'type' => $isTree ? 'tree' : 'blob'
- ];
- }
-
- usort($entries, function($a, $b) {
- if ($a['type'] !== $b['type']) {
- return $a['type'] === 'tree' ? -1 : 1;
- }
- return strcasecmp($a['name'], $b['name']);
- });
-
- return $entries;
-}
-
-function parseCommit($content) {
- $lines = explode("\n", $content);
- $commit = [
- 'tree' => '',
- 'parents' => [],
- 'author' => '',
- 'committer' => '',
- 'message' => ''
- ];
-
- $inMessage = false;
- $messageLines = [];
-
- foreach ($lines as $line) {
- if ($inMessage) {
- $messageLines[] = $line;
- } elseif ($line === '') {
- $inMessage = true;
- } elseif (strpos($line, 'tree ') === 0) {
- $commit['tree'] = substr($line, 5);
- } elseif (strpos($line, 'parent ') === 0) {
- $commit['parents'][] = substr($line, 7);
- } elseif (strpos($line, 'author ') === 0) {
- $commit['author'] = substr($line, 7);
- } elseif (strpos($line, 'committer ') === 0) {
- $commit['committer'] = substr($line, 10);
- }
- }
-
- $commit['message'] = implode("\n", $messageLines);
- return $commit;
-}
-
-function getHead($repoPath) {
- $headFile = $repoPath . '/HEAD';
- if (!file_exists($headFile)) {
- return false;
- }
-
- $content = trim(file_get_contents($headFile));
-
- if (preg_match('/^[a-f0-9]{40}$/', $content)) {
- return ['type' => 'detached', 'hash' => $content, 'ref' => null];
- }
-
- if (strpos($content, 'ref: ') === 0) {
- $ref = substr($content, 5);
- $refFile = $repoPath . '/' . $ref;
- if (file_exists($refFile)) {
- $hash = trim(file_get_contents($refFile));
- return ['type' => 'ref', 'hash' => $hash, 'ref' => $ref];
- }
- }
-
- return false;
-}
-
-function listRefs($repoPath) {
- $refs = [];
-
- $branchesDir = $repoPath . '/refs/heads';
- if (is_dir($branchesDir)) {
- foreach (glob($branchesDir . '/*') as $file) {
- if (is_file($file)) {
- $name = basename($file);
- $hash = trim(file_get_contents($file));
- $refs['branches'][$name] = $hash;
- }
- }
- }
-
- $tagsDir = $repoPath . '/refs/tags';
- if (is_dir($tagsDir)) {
- foreach (glob($tagsDir . '/*') as $file) {
- if (is_file($file)) {
- $name = basename($file);
- $content = file_get_contents($file);
- $refs['tags'][$name] = trim($content);
- }
- }
- }
-
- return $refs;
-}
-
-function formatDate($line) {
- if (preg_match('/(\d+)\s+([\+\-]\d{4})$/', $line, $matches)) {
- $timestamp = $matches[1];
- return date('Y-m-d H:i:s', $timestamp);
- }
- return 'Unknown';
-}
-
-function getAuthor($line) {
- if (preg_match('/^([^<]+)/', $line, $matches)) {
- return trim($matches[1]);
- }
- return $line;
-}
-
-function getLog($repoPath, $commitHash, $max = 20) {
- $log = [];
- $seen = [];
- $queue = [$commitHash];
-
- while (!empty($queue) && count($log) < $max) {
- $hash = array_shift($queue);
- if (isset($seen[$hash])) continue;
- $seen[$hash] = true;
-
- $obj = readGitObject($repoPath, $hash);
- if (!$obj || $obj['type'] !== 'commit') continue;
-
- $commit = parseCommit($obj['content']);
- $commit['hash'] = $hash;
- $log[] = $commit;
-
- foreach ($commit['parents'] as $parent) {
- $queue[] = $parent;
- }
- }
-
- return $log;
+require_once 'Git.php';
+
+function getHomeDirectory() {
+ if (!empty($_SERVER['HOME'])) {
+ return $_SERVER['HOME'];
+ }
+
+ if (!empty(getenv('HOME'))) {
+ return getenv('HOME');
+ }
+
+ if (function_exists('posix_getpwuid') && function_exists('posix_getuid')) {
+ $userInfo = posix_getpwuid(posix_getuid());
+
+ if (!empty($userInfo['dir'])) {
+ return $userInfo['dir'];
+ }
+ }
+
+ return '';
+}
+
+define('REPOS_PATH', getHomeDirectory() . '/repos');
+define('SITE_TITLE', "Dave Jarvis' Repositories");
+
+ini_set('display_errors', 0);
+ini_set('log_errors', 1);
+ini_set('error_log', __DIR__ . '/error.log');
+
+// Global Git instance
+$git = new Git(REPOS_PATH);
+
+function getRepositories() {
+ global $git;
+ $repos = [];
+ $repoList = $git->listRepositories();
+
+ foreach ($repoList as $name) {
+ $path = REPOS_PATH . DIRECTORY_SEPARATOR . $name . '.git';
+ $displayName = urldecode($name);
+ $repos[$name] = [
+ 'path' => $path,
+ 'name' => $displayName,
+ 'safe_name' => $name
+ ];
+ }
+
+ uasort($repos, function($a, $b) {
+ return strcasecmp($a['name'], $b['name']);
+ });
+
+ return $repos;
+}
+
+function getCurrentRepo() {
+ $repos = getRepositories();
+ $requested = $_GET['repo'] ?? '';
+ $decodedRequested = urldecode($requested);
+
+ foreach ($repos as $key => $repo) {
+ if ($repo['safe_name'] === $requested || $repo['name'] === $decodedRequested) {
+ return $repo;
+ }
+ }
+
+ return null;
+}
+
+function sanitizePath($path) {
+ $path = str_replace(['..', '\\', "\0"], ['', '/', ''], $path);
+ return preg_replace('/[^a-zA-Z0-9_\-\.\/]/', '', $path);
+}
+
+// These functions now bridge to the Git class
+function readGitObject($repoPath, $hash) {
+ global $git;
+ $repoName = basename($repoPath, '.git');
+
+ // Use the class to get the object content
+ // Note: The Git class returns raw uncompressed content
+ $content = $git->getObject($repoPath . DIRECTORY_SEPARATOR . '.git', $hash);
+
+ if (!$content) return false;
+
+ // Helper to identify type for the existing UI logic
+ if (str_starts_with($content, 'commit ')) return ['type' => 'commit', 'content' => substr($content, strpos($content, "\0") + 1)];
+ if (str_starts_with($content, 'tree ')) return ['type' => 'tree', 'content' => substr($content, strpos($content, "\0") + 1)];
+
+ return ['type' => 'blob', 'content' => $content];
+}
+
+function parseTree($content) {
+ // Retaining your original tree parser logic as it matches Git's binary format
+ $entries = [];
+ $offset = 0;
+ $len = strlen($content);
+
+ while ($offset < $len) {
+ $spacePos = strpos($content, ' ', $offset);
+ if ($spacePos === false) break;
+
+ $mode = substr($content, $offset, $spacePos - $offset);
+ $offset = $spacePos + 1;
+
+ $nullPos = strpos($content, "\0", $offset);
+ if ($nullPos === false) break;
+
+ $name = substr($content, $offset, $nullPos - $offset);
+ $offset = $nullPos + 1;
+
+ if ($offset + 20 > $len) break;
+ $hash = bin2hex(substr($content, $offset, 20));
+ $offset += 20;
+
+ $isTree = in_array($mode, ['040000', '40000', '160000']);
+
+ $entries[] = [
+ 'mode' => $mode,
+ 'name' => $name,
+ 'hash' => $hash,
+ 'type' => $isTree ? 'tree' : 'blob'
+ ];
+ }
+
+ usort($entries, function($a, $b) {
+ if ($a['type'] !== $b['type']) {
+ return $a['type'] === 'tree' ? -1 : 1;
+ }
+ return strcasecmp($a['name'], $b['name']);
+ });
+
+ return $entries;
+}
+
+function parseCommit($content) {
+ $lines = explode("\n", $content);
+ $commit = [
+ 'tree' => '',
+ 'parents' => [],
+ 'author' => '',
+ 'committer' => '',
+ 'message' => ''
+ ];
+
+ $inMessage = false;
+ $messageLines = [];
+
+ foreach ($lines as $line) {
+ if ($inMessage) {
+ $messageLines[] = $line;
+ } elseif ($line === '') {
+ $inMessage = true;
+ } elseif (strpos($line, 'tree ') === 0) {
+ $commit['tree'] = substr($line, 5);
+ } elseif (strpos($line, 'parent ') === 0) {
+ $commit['parents'][] = substr($line, 7);
+ } elseif (strpos($line, 'author ') === 0) {
+ $commit['author'] = substr($line, 7);
+ } elseif (strpos($line, 'committer ') === 0) {
+ $commit['committer'] = substr($line, 10);
+ }
+ }
+
+ $commit['message'] = implode("\n", $messageLines);
+ return $commit;
+}
+
+function getHead($repoPath) {
+ $headFile = $repoPath . '/HEAD';
+ if (!file_exists($headFile)) {
+ return false;
+ }
+
+ $content = trim(file_get_contents($headFile));
+
+ if (preg_match('/^[a-f0-9]{40}$/', $content)) {
+ return ['type' => 'detached', 'hash' => $content, 'ref' => null];
+ }
+
+ if (strpos($content, 'ref: ') === 0) {
+ $ref = substr($content, 5);
+ $refFile = $repoPath . '/' . $ref;
+ if (file_exists($refFile)) {
+ $hash = trim(file_get_contents($refFile));
+ return ['type' => 'ref', 'hash' => $hash, 'ref' => $ref];
+ }
+ }
+
+ return false;
+}
+
+function listRefs($repoPath) {
+ $refs = [];
+
+ $branchesDir = $repoPath . '/refs/heads';
+ if (is_dir($branchesDir)) {
+ foreach (glob($branchesDir . '/*') as $file) {
+ if (is_file($file)) {
+ $name = basename($file);
+ $hash = trim(file_get_contents($file));
+ $refs['branches'][$name] = $hash;
+ }
+ }
+ }
+
+ $tagsDir = $repoPath . '/refs/tags';
+ if (is_dir($tagsDir)) {
+ foreach (glob($tagsDir . '/*') as $file) {
+ if (is_file($file)) {
+ $name = basename($file);
+ $content = file_get_contents($file);
+ $refs['tags'][$name] = trim($content);
+ }
+ }
+ }
+
+ return $refs;
+}
+
+function formatDate($line) {
+ if (preg_match('/(\d+)\s+([\+\-]\d{4})$/', $line, $matches)) {
+ $timestamp = $matches[1];
+ return date('Y-m-d H:i:s', $timestamp);
+ }
+ return 'Unknown';
+}
+
+function getAuthor($line) {
+ if (preg_match('/^([^<]+)/', $line, $matches)) {
+ return trim($matches[1]);
+ }
+ return $line;
}
Delta702 lines added, 697 lines removed, 5-line increase