Dave Jarvis' Repositories

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

Splits Git into smaller classes

AuthorDave Jarvis <email>
Date2026-02-09 22:39:25 GMT-0800
Commit13f748a428b36e1c18e574b223d5e48ec0b1dde6
Parent60d52c5
Git.php
<?php
require_once 'File.php';
-
-class Git {
- private const CHUNK_SIZE = 128;
- private const MAX_READ = 16777216;
- private const MODE_TREE = '40000';
- private const MODE_TREE_A = '040000';
-
- private string $path;
- private string $objPath;
- private array $packFiles;
-
- private array $fileHandles = [];
- private array $fanoutCache = [];
- private array $shaBucketCache = [];
- private array $offsetBucketCache = [];
- private ?string $lastPack = null;
-
- // Profiling
- private array $pStats = [];
- private array $pTimers = [];
-
- public function __construct( string $repoPath ) {
- $this->setRepository($repoPath);
- }
-
- public function __destruct() {
- foreach( $this->fileHandles as $handle ) {
- if( is_resource( $handle ) ) {
- fclose( $handle );
- }
- }
- }
-
- // --- Profiling Methods ---
-
- private function enter( string $name ): void {
- $this->pTimers[$name] = microtime( true );
- }
-
- private function leave( string $name ): void {
- if( !isset( $this->pTimers[$name] ) ) return;
-
- $elapsed = microtime( true ) - $this->pTimers[$name];
-
- // Initialize stat entry if missing
- if( !isset( $this->pStats[$name] ) ) {
- $this->pStats[$name] = ['cnt' => 0, 'time' => 0.0];
- }
-
- $this->pStats[$name]['cnt']++;
- $this->pStats[$name]['time'] += $elapsed;
- unset( $this->pTimers[$name] );
- }
-
- public function profileReport(): string {
- if (empty($this->pStats)) {
- return "<p>No profiling data collected.</p>";
- }
-
- // Sort by total time descending
- uasort($this->pStats, fn($a, $b) => $b['time'] <=> $a['time']);
-
- $html = '<table border="1" cellspacing="0" cellpadding="5" style="border-collapse: collapse; font-family: monospace; width: 100%;">';
- $html .= '<thead style="background: #eee;"><tr>';
- $html .= '<th style="text-align: left;">Method</th>';
- $html .= '<th style="text-align: right;">Calls</th>';
- $html .= '<th style="text-align: right;">Total (ms)</th>';
- $html .= '<th style="text-align: right;">Avg (ms)</th>';
- $html .= '</tr></thead><tbody>';
-
- foreach ($this->pStats as $name => $stat) {
- $totalMs = $stat['time'] * 1000;
- $avgMs = $stat['cnt'] > 0 ? $totalMs / $stat['cnt'] : 0;
-
- // Remove namespace/class prefix for cleaner display
- $cleanName = str_replace(__CLASS__ . '::', '', $name);
-
- $html .= '<tr>';
- $html .= '<td>' . htmlspecialchars($cleanName) . '</td>';
- $html .= '<td style="text-align: right;">' . $stat['cnt'] . '</td>';
- $html .= '<td style="text-align: right;">' . number_format($totalMs, 2) . '</td>';
- $html .= '<td style="text-align: right;">' . number_format($avgMs, 2) . '</td>';
- $html .= '</tr>';
- }
-
- $html .= '</tbody></table>';
-
- return $html;
- }
-
- // --- Core Methods (Instrumented) ---
-
- public function setRepository($repoPath) {
- $this->path = rtrim( $repoPath, '/' );
- $this->objPath = $this->path . '/objects';
- $this->packFiles = glob( "{$this->objPath}/pack/*.idx" ) ?: [];
- }
-
- public function getObjectSize( string $sha ): int {
- $this->enter( __METHOD__ );
- $info = $this->getPackOffset( $sha );
-
- if( $info['offset'] !== -1 ) {
- $res = $this->extractPackedSize( $info );
- $this->leave( __METHOD__ );
- return $res;
- }
-
- $prefix = substr( $sha, 0, 2 );
- $suffix = substr( $sha, 2 );
- $loosePath = "{$this->objPath}/{$prefix}/{$suffix}";
-
- $res = file_exists( $loosePath )
- ? $this->getLooseObjectSize( $loosePath )
- : 0;
-
- $this->leave( __METHOD__ );
- return $res;
- }
-
- private function getLooseObjectSize( string $path ): int {
- $this->enter( __METHOD__ );
- $size = 0;
- $fileHandle = @fopen( $path, 'rb' );
-
- if( $fileHandle ) {
- $data = $this->decompressHeader( $fileHandle );
- $header = explode( "\0", $data, 2 )[0];
- $parts = explode( ' ', $header );
- $size = isset( $parts[1] ) ? (int)$parts[1] : 0;
- fclose( $fileHandle );
- }
-
- $this->leave( __METHOD__ );
- return $size;
- }
-
- private function decompressHeader( $fileHandle ): string {
- $data = '';
- $inflateContext = inflate_init( ZLIB_ENCODING_DEFLATE );
-
- while( !feof( $fileHandle ) ) {
- $chunk = fread( $fileHandle, self::CHUNK_SIZE );
- $inflated = @inflate_add( $inflateContext, $chunk, ZLIB_NO_FLUSH );
-
- if( $inflated === false ) {
- break;
- }
-
- $data .= $inflated;
-
- if( strpos( $data, "\0" ) !== false ) {
- break;
- }
- }
-
- return $data;
- }
-
- private function getPackedObjectSize( string $sha ): int {
- $info = $this->getPackOffset( $sha );
-
- $size = ($info['offset'] !== -1)
- ? $this->extractPackedSize( $info )
- : 0;
-
- return $size;
- }
-
- private function extractPackedSize( array $info ): int {
- $this->enter( __METHOD__ );
- $targetSize = 0;
- $packPath = $info['file'];
-
- if( !isset( $this->fileHandles[$packPath] ) ) {
- $this->fileHandles[$packPath] = @fopen( $packPath, 'rb' );
- }
-
- $packFile = $this->fileHandles[$packPath];
-
- if( $packFile ) {
- fseek( $packFile, $info['offset'] );
- $buffer = fread( $packFile, 64 );
- $pos = 0;
-
- $byte = ord( $buffer[$pos++] );
- $type = ($byte >> 4) & 7;
- $targetSize = $byte & 15;
- $shift = 4;
-
- while( $byte & 128 ) {
- $byte = ord( $buffer[$pos++] );
- $targetSize |= (($byte & 127) << $shift);
- $shift += 7;
- }
-
- if( $type === 6 || $type === 7 ) {
- $targetSize = $this->readDeltaTargetSize( $packFile, $type, $buffer, $pos );
- }
- }
-
- $this->leave( __METHOD__ );
- return $targetSize;
- }
-
- private function readVarInt( $fileHandle ): array {
- $byte = ord( fread( $fileHandle, 1 ) );
- $value = $byte & 15;
- $shift = 4;
- $firstByte = $byte;
-
- while( $byte & 128 ) {
- $byte = ord( fread( $fileHandle, 1 ) );
- $value |= (($byte & 127) << $shift);
- $shift += 7;
- }
-
- return ['value' => $value, 'byte' => $firstByte];
- }
-
- private function readDeltaTargetSize( $fileHandle, int $type, string $buffer, int $pos ): int {
- $this->enter( __METHOD__ );
- if( $type === 6 ) {
- $byte = ord( $buffer[$pos++] );
- while( $byte & 128 ) {
- $byte = ord( $buffer[$pos++] );
- }
- } else {
- $pos += 20;
- }
-
- $inflateContext = inflate_init( ZLIB_ENCODING_DEFLATE );
- $headerData = '';
-
- if( $pos < strlen( $buffer ) ) {
- $chunk = substr( $buffer, $pos );
- $inflated = @inflate_add( $inflateContext, $chunk, ZLIB_NO_FLUSH );
- if( $inflated !== false ) {
- $headerData .= $inflated;
- }
- }
-
- while( !feof( $fileHandle ) && strlen( $headerData ) < 32 ) {
- if( inflate_get_status( $inflateContext ) === ZLIB_STREAM_END ) {
- break;
- }
-
- $inflated = @inflate_add(
- $inflateContext,
- fread( $fileHandle, 512 ),
- ZLIB_NO_FLUSH
- );
-
- if( $inflated !== false ) {
- $headerData .= $inflated;
- }
- }
-
- $result = 0;
- $position = 0;
-
- if( strlen( $headerData ) > 0 ) {
- $this->skipSize( $headerData, $position );
- $result = $this->readSize( $headerData, $position );
- }
-
- $this->leave( __METHOD__ );
- return $result;
- }
-
- public function getMainBranch(): array {
- $result = ['name' => '', 'hash' => ''];
- $branches = [];
- $this->eachBranch( function( $name, $sha ) use( &$branches ) {
- $branches[$name] = $sha;
- } );
-
- foreach( ['main', 'master', 'trunk', 'develop'] as $branch ) {
- if( isset( $branches[$branch] ) ) {
- $result = ['name' => $branch, 'hash' => $branches[$branch]];
- break;
- }
- }
-
- if( $result['name'] === '' ) {
- $firstKey = array_key_first( $branches );
-
- if( $firstKey !== null ) {
- $result = ['name' => $firstKey, 'hash' => $branches[$firstKey]];
- }
- }
-
- return $result;
- }
-
- 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 );
- $data = ($sha !== '') ? $this->read( $sha ) : '';
-
- if( preg_match( '/^tree ([0-9a-f]{40})$/m', $data, $matches ) ) {
- $data = $this->read( $matches[1] );
- }
-
- if( $this->isTreeData( $data ) ) {
- $this->processTree( $data, $callback );
- }
- }
-
- private function processTree( string $data, callable $callback ): void {
- $this->enter( __METHOD__ );
- $position = 0;
-
- while( $position < strlen( $data ) ) {
- $spacePos = strpos( $data, ' ', $position );
- $nullPos = strpos( $data, "\0", $spacePos );
-
- if( $spacePos === false || $nullPos === false ) {
- break;
- }
-
- $mode = substr( $data, $position, $spacePos - $position );
- $name = substr( $data, $spacePos + 1, $nullPos - $spacePos - 1 );
- $entrySha = bin2hex( substr( $data, $nullPos + 1, 20 ) );
-
- $isDir = ($mode === self::MODE_TREE || $mode === self::MODE_TREE_A);
-
- // Recursive call tracked
- $size = $isDir ? 0 : $this->getObjectSize( $entrySha );
-
- $callback( new File( $name, $entrySha, $mode, 0, $size ) );
- $position = $nullPos + 21;
- }
- $this->leave( __METHOD__ );
- }
-
- private function isTreeData( string $data ): bool {
- $result = false;
- $pattern = '/^(40000|100644|100755|120000|160000) /';
-
- if( strlen( $data ) >= 25 && preg_match( $pattern, $data ) ) {
- $nullPos = strpos( $data, "\0" );
- $result = ($nullPos !== false && ($nullPos + 21 <= strlen( $data )));
- }
-
- return $result;
- }
-
- public function history( string $ref, int $limit, callable $cb ): void {
- $currentSha = $this->resolve( $ref );
- $count = 0;
-
- while( $currentSha !== '' && $count < $limit ) {
- $data = $this->read( $currentSha );
-
- if( $data === '' ) {
- break;
- }
-
- $pos = strpos( $data, "\n\n" );
- $message = ($pos !== false) ? substr( $data, $pos + 2 ) : '';
- preg_match( '/^author (.*) <(.*)> (\d+)/m', $data, $m );
-
- $cb( (object)[
- 'sha' => $currentSha,
- 'message' => trim( $message ),
- 'author' => $m[1] ?? 'Unknown',
- 'email' => $m[2] ?? '',
- 'date' => (int)($m[3] ?? 0)
- ] );
-
- $currentSha = preg_match( '/^parent ([0-9a-f]{40})$/m', $data, $ms )
- ? $ms[1] : '';
- $count++;
- }
- }
-
- public function stream( string $sha, callable $callback ): void {
- $data = $this->read( $sha );
-
- if( $data !== '' ) {
- $callback( $data );
- }
- }
-
- public function resolve( string $input ): string {
- $this->enter( __METHOD__ );
- $result = '';
-
- if( preg_match( '/^[0-9a-f]{40}$/', $input ) ) {
- $result = $input;
- } elseif( $input === 'HEAD' &&
- file_exists( $headFile = "{$this->path}/HEAD" ) ) {
- $head = trim( file_get_contents( $headFile ) );
- $result = (strpos( $head, 'ref: ' ) === 0)
- ? $this->resolve( substr( $head, 5 ) ) : $head;
- } else {
- $result = $this->resolveRef( $input );
- }
-
- $this->leave( __METHOD__ );
- return $result;
- }
-
- private function resolveRef( string $input ): string {
- $found = '';
- $refPaths = [$input, "refs/heads/$input", "refs/tags/$input"];
-
- foreach( $refPaths as $path ) {
- if( file_exists( $filePath = "{$this->path}/$path" ) ) {
- $found = trim( file_get_contents( $filePath ) );
- break;
- }
- }
-
- if( $found === '' &&
- file_exists( $packed = "{$this->path}/packed-refs" ) ) {
- $found = $this->findInPackedRefs( $packed, $input );
- }
-
- return $found;
- }
-
- private function findInPackedRefs( string $path, string $input ): string {
- $result = '';
- $targets = [$input, "refs/heads/$input", "refs/tags/$input"];
-
- foreach( file( $path ) as $line ) {
- if( $line[0] === '#' || $line[0] === '^' ) {
- continue;
- }
-
- $parts = explode( ' ', trim( $line ) );
-
- if( count( $parts ) >= 2 && in_array( $parts[1], $targets ) ) {
- $result = $parts[0];
- break;
- }
- }
-
- return $result;
- }
-
- public function read( string $sha ): string {
- $this->enter( __METHOD__ );
- $result = '';
- $prefix = substr( $sha, 0, 2 );
- $suffix = substr( $sha, 2 );
- $loose = "{$this->objPath}/{$prefix}/{$suffix}";
-
- if( file_exists( $loose ) ) {
- $raw = file_get_contents( $loose );
- $inflated = $raw ? @gzuncompress( $raw ) : false;
- $result = $inflated ? explode( "\0", $inflated, 2 )[1] : '';
- } else {
- $result = $this->fromPack( $sha );
- }
-
- $this->leave( __METHOD__ );
- return $result;
- }
-
- private function fromPack( string $sha ): string {
- $info = $this->getPackOffset( $sha );
- $result = '';
-
- if( $info['offset'] !== -1 ) {
- $packPath = $info['file'];
-
- if( !isset( $this->fileHandles[$packPath] ) ) {
- $this->fileHandles[$packPath] = @fopen( $packPath, 'rb' );
- }
-
- $packFile = $this->fileHandles[$packPath];
-
- if( $packFile ) {
- $result = $this->readPackEntry( $packFile, $info['offset'] );
- }
- }
-
- return $result;
- }
-
- private function getPackOffset( string $sha ): array {
- $this->enter( __METHOD__ );
- $result = ['file' => '', 'offset' => -1];
-
- if( strlen( $sha ) === 40 && ctype_xdigit( $sha ) ) {
- $binSha = hex2bin( $sha );
-
- if( $this->lastPack ) {
- $offset = $this->findInPack( $this->lastPack, $binSha );
- if( $offset !== -1 ) {
- $this->leave( __METHOD__ );
- return [
- 'file' => str_replace( '.idx', '.pack', $this->lastPack ),
- 'offset' => $offset
- ];
- }
- }
-
- foreach( $this->packFiles as $idxFile ) {
- if( $idxFile === $this->lastPack ) {
- continue;
- }
-
- $offset = $this->findInPack( $idxFile, $binSha );
-
- if( $offset !== -1 ) {
- $this->lastPack = $idxFile;
- $result = [
- 'file' => str_replace( '.idx', '.pack', $idxFile ),
- 'offset' => $offset
- ];
- break;
- }
- }
- }
-
- $this->leave( __METHOD__ );
- return $result;
- }
-
- private function findInPack( string $idxFile, string $binSha ): int {
- $this->enter( __METHOD__ );
-
- if( !isset( $this->fileHandles[$idxFile] ) ) {
- $handle = @fopen( $idxFile, 'rb' );
- if( !$handle ) {
- $this->leave( __METHOD__ );
- return -1;
- }
-
- $this->fileHandles[$idxFile] = $handle;
- fseek( $handle, 0 );
-
- if( fread( $handle, 8 ) === "\377tOc\0\0\0\2" ) {
- $this->fanoutCache[$idxFile] = array_values( unpack( 'N*', fread( $handle, 1024 ) ) );
- } else {
- $this->fanoutCache[$idxFile] = null;
- }
- }
-
- $handle = $this->fileHandles[$idxFile];
- $fanout = $this->fanoutCache[$idxFile] ?? null;
-
- if( !$handle || !$fanout ) {
- $this->leave( __METHOD__ );
- return -1;
- }
-
- $firstByte = ord( $binSha[0] );
- $start = ($firstByte === 0) ? 0 : $fanout[$firstByte - 1];
- $end = $fanout[$firstByte];
-
- if( $end <= $start ) {
- $this->leave( __METHOD__ );
- return -1;
- }
-
- $cacheKey = "$idxFile:$firstByte";
-
- if( isset( $this->shaBucketCache[$cacheKey] ) ) {
- $shaBlock = $this->shaBucketCache[$cacheKey];
- } else {
- $count = $end - $start;
- fseek( $handle, 1032 + ($start * 20) );
- $shaBlock = fread( $handle, $count * 20 );
- $this->shaBucketCache[$cacheKey] = $shaBlock;
-
- $total = $fanout[255];
- $layer4Start = 1032 + ($total * 24);
-
- fseek( $handle, $layer4Start + ($start * 4) );
- $this->offsetBucketCache[$cacheKey] = fread( $handle, $count * 4 );
- }
-
- $count = strlen( $shaBlock ) / 20;
- $foundIdx = $this->searchShaBlock( $shaBlock, $count, $binSha );
-
- if( $foundIdx === -1 ) {
- $this->leave( __METHOD__ );
- return -1;
- }
-
- $offsetData = substr( $this->offsetBucketCache[$cacheKey], $foundIdx * 4, 4 );
- $offset = unpack( 'N', $offsetData )[1];
-
- if( $offset & 0x80000000 ) {
- $total = $fanout[255];
- $layer5Start = 1032 + ($total * 24) + ($total * 4);
- fseek( $handle, $layer5Start + (($offset & 0x7FFFFFFF) * 8) );
- $data64 = fread( $handle, 8 );
- $offset = $data64 ? unpack( 'J', $data64 )[1] : 0;
- }
-
- $this->leave( __METHOD__ );
- return (int)$offset;
- }
-
- private function searchShaBlock(
- string $shaBlock,
- int $count,
- string $binSha
- ): int {
- $low = 0;
- $high = $count - 1;
-
- while( $low <= $high ) {
- $mid = ($low + $high) >> 1;
- $currentSha = substr( $shaBlock, $mid * 20, 20 );
-
- if( $currentSha < $binSha ) {
- $low = $mid + 1;
- } elseif( $currentSha > $binSha ) {
- $high = $mid - 1;
- } else {
- return $mid;
- }
- }
-
- return -1;
- }
-
- private function readPackEntry( $fileHandle, int $offset ): string {
- $this->enter( __METHOD__ );
- fseek( $fileHandle, $offset );
- $header = $this->readVarInt( $fileHandle );
- $type = ($header['byte'] >> 4) & 7;
-
- if( $type === 6 ) {
- $res = $this->handleOfsDelta( $fileHandle, $offset );
- $this->leave( __METHOD__ );
- return $res;
- }
- if( $type === 7 ) {
- $res = $this->handleRefDelta( $fileHandle );
- $this->leave( __METHOD__ );
- return $res;
- }
-
- $inf = inflate_init( ZLIB_ENCODING_DEFLATE );
- $res = '';
-
- while( !feof( $fileHandle ) ) {
- $chunk = fread( $fileHandle, 8192 );
- $data = @inflate_add( $inf, $chunk );
-
- if( $data !== false ) $res .= $data;
- if( $data === false || ($inf && inflate_get_status( $inf ) === ZLIB_STREAM_END) ) break;
- }
-
- $this->leave( __METHOD__ );
- return $res;
- }
-
- private function deltaCopy(
- string $base, string $delta, int &$position, int $opcode
- ): string {
- $offset = 0;
- $length = 0;
-
- if( $opcode & 0x01 ) $offset |= ord( $delta[$position++] );
- if( $opcode & 0x02 ) $offset |= ord( $delta[$position++] ) << 8;
- if( $opcode & 0x04 ) $offset |= ord( $delta[$position++] ) << 16;
- if( $opcode & 0x08 ) $offset |= ord( $delta[$position++] ) << 24;
-
- if( $opcode & 0x10 ) $length |= ord( $delta[$position++] );
- if( $opcode & 0x20 ) $length |= ord( $delta[$position++] ) << 8;
- if( $opcode & 0x40 ) $length |= ord( $delta[$position++] ) << 16;
-
- if( $length === 0 ) $length = 0x10000;
-
- return substr( $base, $offset, $length );
- }
-
- private function handleOfsDelta( $fileHandle, int $offset ): string {
- $byte = ord( fread( $fileHandle, 1 ) );
- $negOffset = $byte & 127;
-
- while( $byte & 128 ) {
- $byte = ord( fread( $fileHandle, 1 ) );
- $negOffset = (($negOffset + 1) << 7) | ($byte & 127);
- }
-
- $currentPos = ftell( $fileHandle );
- $base = $this->readPackEntry( $fileHandle, $offset - $negOffset );
- fseek( $fileHandle, $currentPos );
-
- $delta = @gzuncompress( fread( $fileHandle, self::MAX_READ ) ) ?: '';
-
- return $this->applyDelta( $base, $delta );
- }
-
- private function handleRefDelta( $fileHandle ): string {
- $base = $this->read( bin2hex( fread( $fileHandle, 20 ) ) );
- $delta = @gzuncompress( fread( $fileHandle, self::MAX_READ ) ) ?: '';
-
- return $this->applyDelta( $base, $delta );
- }
-
- private function applyDelta( string $base, string $delta ): string {
- $this->enter( __METHOD__ );
- $out = '';
-
- if( $base !== '' && $delta !== '' ) {
- $position = 0;
- $this->skipSize( $delta, $position );
- $this->skipSize( $delta, $position );
-
- while( $position < strlen( $delta ) ) {
- $opcode = ord( $delta[$position++] );
-
- if( $opcode & 128 ) {
- $out .= $this->deltaCopy( $base, $delta, $position, $opcode );
- } else {
- $len = $opcode & 127;
- $out .= substr( $delta, $position, $len );
- $position += $len;
- }
- }
- }
-
- $this->leave( __METHOD__ );
- return $out;
- }
-
- private function skipSize( string $data, int &$position ): void {
- while( ord( $data[$position++] ) & 128 ) {
- }
- }
-
- private function readSize( string $data, int &$position ): int {
- $byte = ord( $data[$position++] );
- $value = $byte & 127;
- $shift = 7;
-
- while( $byte & 128 ) {
- $byte = ord( $data[$position++] );
- $value |= (($byte & 127) << $shift);
- $shift += 7;
- }
-
- return $value;
- }
-
- private function skipOffsetDelta( $fileHandle ): void {
- $byte = ord( fread( $fileHandle, 1 ) );
-
- while( $byte & 128 ) {
- $byte = ord( fread( $fileHandle, 1 ) );
- }
- }
-
- private function scanRefs( string $prefix, callable $callback ): void {
- $directory = "{$this->path}/$prefix";
-
- if( is_dir( $directory ) ) {
- foreach( array_diff( scandir( $directory ), ['.', '..'] ) as $fileName ) {
- $content = file_get_contents( "$directory/$fileName" );
- $callback( $fileName, trim( $content ) );
- }
- }
+require_once 'GitRefs.php';
+require_once 'GitPacks.php';
+
+class Git {
+ private const CHUNK_SIZE = 128;
+ private string $path;
+ private string $objPath;
+
+ private GitRefs $refs;
+ private GitPacks $packs;
+
+ public function __construct(string $repoPath) {
+ $this->path = rtrim($repoPath, '/');
+ $this->objPath = $this->path . '/objects';
+ $this->refs = new GitRefs($this->path);
+ $this->packs = new GitPacks($this->objPath);
+ }
+
+ public function resolve(string $ref): string {
+ return $this->refs->resolve($ref);
+ }
+
+ public function getMainBranch(): array {
+ return $this->refs->getMainBranch();
+ }
+
+ public function eachBranch(callable $cb): void {
+ $this->refs->scanRefs('refs/heads', $cb);
+ }
+
+ public function eachTag(callable $cb): void {
+ $this->refs->scanRefs('refs/tags', $cb);
+ }
+
+ public function getObjectSize(string $sha): int {
+ $size = $this->packs->getSize($sha);
+ if ($size !== null) return $size;
+
+ return $this->getLooseObjectSize($sha);
+ }
+
+ public function read(string $sha): string {
+ $loosePath = $this->getLoosePath($sha);
+
+ if (file_exists($loosePath)) {
+ $raw = file_get_contents($loosePath);
+ $inflated = $raw ? @gzuncompress($raw) : false;
+ return $inflated ? explode("\0", $inflated, 2)[1] : '';
+ }
+
+ return $this->packs->read($sha) ?? '';
+ }
+
+ public function stream(string $sha, callable $callback): void {
+ $data = $this->read($sha);
+ if ($data !== '') $callback($data);
+ }
+
+ public function history(string $ref, int $limit, callable $cb): void {
+ $curr = $this->resolve($ref);
+ $count = 0;
+
+ while ($curr !== '' && $count < $limit) {
+ $data = $this->read($curr);
+ if ($data === '') break;
+
+ $pos = strpos($data, "\n\n");
+ $msg = ($pos !== false) ? substr($data, $pos + 2) : '';
+ preg_match('/^author (.*) <(.*)> (\d+)/m', $data, $m);
+
+ $cb((object)[
+ 'sha' => $curr,
+ 'message' => trim($msg),
+ 'author' => $m[1] ?? 'Unknown',
+ 'email' => $m[2] ?? '',
+ 'date' => (int)($m[3] ?? 0)
+ ]);
+
+ $curr = preg_match('/^parent ([0-9a-f]{40})$/m', $data, $ms) ? $ms[1] : '';
+ $count++;
+ }
+ }
+
+ public function walk(string $refOrSha, callable $callback): void {
+ $sha = $this->resolve($refOrSha);
+ $data = ($sha !== '') ? $this->read($sha) : '';
+
+ if (preg_match('/^tree ([0-9a-f]{40})$/m', $data, $m)) {
+ $data = $this->read($m[1]);
+ }
+
+ if ($this->isTreeData($data)) {
+ $this->processTree($data, $callback);
+ }
+ }
+
+ private function processTree(string $data, callable $callback): void {
+ $pos = 0;
+ $len = strlen($data);
+
+ while ($pos < $len) {
+ $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);
+ $sha = bin2hex(substr($data, $null + 1, 20));
+
+ $isDir = ($mode === '40000' || $mode === '040000');
+ $size = $isDir ? 0 : $this->getObjectSize($sha);
+
+ $callback(new File($name, $sha, $mode, 0, $size));
+ $pos = $null + 21;
+ }
+ }
+
+ private function isTreeData(string $data): bool {
+ $pat = '/^(40000|100644|100755|120000|160000) /';
+ if (strlen($data) >= 25 && preg_match($pat, $data)) {
+ $null = strpos($data, "\0");
+ return ($null !== false && ($null + 21 <= strlen($data)));
+ }
+ return false;
+ }
+
+ private function getLoosePath(string $sha): string {
+ return "{$this->objPath}/" . substr($sha, 0, 2) . "/" . substr($sha, 2);
+ }
+
+ private function getLooseObjectSize(string $sha): int {
+ $path = $this->getLoosePath($sha);
+ if (!file_exists($path)) return 0;
+
+ $h = @fopen($path, 'rb');
+ if (!$h) return 0;
+
+ $data = '';
+ $inf = inflate_init(ZLIB_ENCODING_DEFLATE);
+
+ while (!feof($h)) {
+ $chunk = fread($h, self::CHUNK_SIZE);
+ $out = @inflate_add($inf, $chunk, ZLIB_NO_FLUSH);
+ if ($out === false) break;
+ $data .= $out;
+ if (strpos($data, "\0") !== false) break;
+ }
+ fclose($h);
+
+ $header = explode("\0", $data, 2)[0];
+ $parts = explode(' ', $header);
+ return isset($parts[1]) ? (int)$parts[1] : 0;
}
}
GitPacks.php
+<?php
+class GitPacks {
+ private const MAX_READ = 16777216;
+
+ private string $objPath;
+ private array $packFiles;
+ private ?string $lastPack = null;
+
+ // Caches and Resources
+ private array $fileHandles = [];
+ private array $fanoutCache = [];
+ private array $shaBucketCache = [];
+ private array $offsetBucketCache = [];
+
+ public function __construct(string $objPath) {
+ $this->objPath = $objPath;
+ $this->packFiles = glob("{$this->objPath}/pack/*.idx") ?: [];
+ }
+
+ public function __destruct() {
+ foreach ($this->fileHandles as $h) {
+ if (is_resource($h)) fclose($h);
+ }
+ }
+
+ public function read(string $sha): ?string {
+ $info = $this->findPackInfo($sha);
+ if ($info['offset'] === -1) return null;
+
+ $handle = $this->getHandle($info['file']);
+ return $handle ? $this->readPackEntry($handle, $info['offset']) : null;
+ }
+
+ public function getSize(string $sha): ?int {
+ $info = $this->findPackInfo($sha);
+ if ($info['offset'] === -1) return null;
+
+ return $this->extractPackedSize($info['file'], $info['offset']);
+ }
+
+ private function findPackInfo(string $sha): array {
+ if (!ctype_xdigit($sha) || strlen($sha) !== 40) return ['offset' => -1];
+ $binSha = hex2bin($sha);
+
+ // Try last used pack first
+ if ($this->lastPack) {
+ $offset = $this->findInIdx($this->lastPack, $binSha);
+ if ($offset !== -1) return $this->makeResult($this->lastPack, $offset);
+ }
+
+ foreach ($this->packFiles as $idx) {
+ if ($idx === $this->lastPack) continue;
+ $offset = $this->findInIdx($idx, $binSha);
+ if ($offset !== -1) {
+ $this->lastPack = $idx;
+ return $this->makeResult($idx, $offset);
+ }
+ }
+
+ return ['offset' => -1];
+ }
+
+ private function makeResult(string $idxPath, int $offset): array {
+ return [
+ 'file' => str_replace('.idx', '.pack', $idxPath),
+ 'offset' => $offset
+ ];
+ }
+
+ private function findInIdx(string $idxFile, string $binSha): int {
+ $h = $this->getHandle($idxFile);
+ if (!$h) return -1;
+
+ // Load Fanout
+ if (!isset($this->fanoutCache[$idxFile])) {
+ fseek($h, 0);
+ if (fread($h, 8) === "\377tOc\0\0\0\2") {
+ $this->fanoutCache[$idxFile] = array_values(unpack('N*', fread($h, 1024)));
+ } else {
+ return -1;
+ }
+ }
+ $fanout = $this->fanoutCache[$idxFile];
+
+ $first = ord($binSha[0]);
+ $start = ($first === 0) ? 0 : $fanout[$first - 1];
+ $end = $fanout[$first];
+ if ($end <= $start) return -1;
+
+ // Load SHA Cache
+ $key = "$idxFile:$first";
+ if (!isset($this->shaBucketCache[$key])) {
+ $count = $end - $start;
+ fseek($h, 1032 + ($start * 20));
+ $this->shaBucketCache[$key] = fread($h, $count * 20);
+
+ // Load Offsets (4-byte)
+ fseek($h, 1032 + ($fanout[255] * 24) + ($start * 4));
+ $this->offsetBucketCache[$key] = fread($h, $count * 4);
+ }
+
+ // Binary Search
+ $shaBlock = $this->shaBucketCache[$key];
+ $count = strlen($shaBlock) / 20;
+ $low = 0;
+ $high = $count - 1;
+ $foundIdx = -1;
+
+ while ($low <= $high) {
+ $mid = ($low + $high) >> 1;
+ $cmp = substr($shaBlock, $mid * 20, 20);
+ if ($cmp < $binSha) $low = $mid + 1;
+ elseif ($cmp > $binSha) $high = $mid - 1;
+ else { $foundIdx = $mid; break; }
+ }
+
+ if ($foundIdx === -1) return -1;
+
+ $offData = substr($this->offsetBucketCache[$key], $foundIdx * 4, 4);
+ $offset = unpack('N', $offData)[1];
+
+ // Handle 64-bit offsets
+ if ($offset & 0x80000000) {
+ $packTotal = $fanout[255];
+ $pos64 = 1032 + ($packTotal * 28) + (($offset & 0x7FFFFFFF) * 8);
+ fseek($h, $pos64);
+ $offset = unpack('J', fread($h, 8))[1];
+ }
+
+ return (int)$offset;
+ }
+
+ private function readPackEntry($h, int $offset): string {
+ fseek($h, $offset);
+ $header = $this->readVarInt($h);
+ $type = ($header['byte'] >> 4) & 7;
+
+ if ($type === 6) return $this->handleOfsDelta($h, $offset);
+ if ($type === 7) return $this->handleRefDelta($h);
+
+ $inf = inflate_init(ZLIB_ENCODING_DEFLATE);
+ $res = '';
+ while (!feof($h)) {
+ $chunk = fread($h, 8192);
+ $data = @inflate_add($inf, $chunk);
+ if ($data !== false) $res .= $data;
+ if ($data === false || inflate_get_status($inf) === ZLIB_STREAM_END) break;
+ }
+ return $res;
+ }
+
+ private function extractPackedSize(string $packPath, int $offset): int {
+ $h = $this->getHandle($packPath);
+ if (!$h) return 0;
+
+ fseek($h, $offset);
+ $header = $this->readVarInt($h);
+ $size = $header['value'];
+ $type = ($header['byte'] >> 4) & 7;
+
+ if ($type === 6 || $type === 7) {
+ return $this->readDeltaTargetSize($h, $type);
+ }
+ return $size;
+ }
+
+ // --- Delta & Helper Methods ---
+
+ private function handleOfsDelta($h, int $offset): string {
+ $byte = ord(fread($h, 1));
+ $neg = $byte & 127;
+ while ($byte & 128) {
+ $byte = ord(fread($h, 1));
+ $neg = (($neg + 1) << 7) | ($byte & 127);
+ }
+ $cur = ftell($h);
+ $base = $this->readPackEntry($h, $offset - $neg);
+ fseek($h, $cur);
+ $delta = @gzuncompress(fread($h, self::MAX_READ)) ?: '';
+ return $this->applyDelta($base, $delta);
+ }
+
+ private function handleRefDelta($h): string {
+ $baseSha = bin2hex(fread($h, 20));
+ // Note: Recursive call to read() for base object
+ // We need a way to callback to main Git class or resolve internally.
+ // For simplicity assuming self-contained or base is in same pack system:
+ $base = $this->read($baseSha) ?? '';
+ $delta = @gzuncompress(fread($h, self::MAX_READ)) ?: '';
+ return $this->applyDelta($base, $delta);
+ }
+
+ private function applyDelta(string $base, string $delta): string {
+ $pos = 0;
+ $this->skipSize($delta, $pos); // src size
+ $this->skipSize($delta, $pos); // dst size
+ $out = '';
+
+ while ($pos < strlen($delta)) {
+ $opcode = ord($delta[$pos++]);
+ if ($opcode & 128) {
+ $off = 0; $len = 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) $len |= ord($delta[$pos++]);
+ if ($opcode & 0x20) $len |= ord($delta[$pos++]) << 8;
+ if ($opcode & 0x40) $len |= ord($delta[$pos++]) << 16;
+ if ($len === 0) $len = 0x10000;
+ $out .= substr($base, $off, $len);
+ } else {
+ $len = $opcode & 127;
+ $out .= substr($delta, $pos, $len);
+ $pos += $len;
+ }
+ }
+ return $out;
+ }
+
+ private function readVarInt($h): array {
+ $byte = ord(fread($h, 1));
+ $val = $byte & 15;
+ $shift = 4;
+ $first = $byte;
+ while ($byte & 128) {
+ $byte = ord(fread($h, 1));
+ $val |= (($byte & 127) << $shift);
+ $shift += 7;
+ }
+ return ['value' => $val, 'byte' => $first];
+ }
+
+ private function readDeltaTargetSize($h, int $type): int {
+ if ($type === 6) { // Offset delta
+ $byte = ord(fread($h, 1));
+ while ($byte & 128) $byte = ord(fread($h, 1));
+ } else { // Ref delta
+ fseek($h, 20, SEEK_CUR);
+ }
+
+ // Decompress just enough to read target size
+ $inf = inflate_init(ZLIB_ENCODING_DEFLATE);
+ $hdr = '';
+ while (!feof($h) && strlen($hdr) < 32) {
+ $chunk = fread($h, 512);
+ $out = @inflate_add($inf, $chunk, ZLIB_NO_FLUSH);
+ if ($out !== false) $hdr .= $out;
+ if (inflate_get_status($inf) === ZLIB_STREAM_END) break;
+ }
+
+ $pos = 0;
+ if (strlen($hdr) > 0) {
+ $this->skipSize($hdr, $pos);
+ return $this->readSize($hdr, $pos);
+ }
+ return 0;
+ }
+
+ private function skipSize(string $d, int &$p): void {
+ while (ord($d[$p++]) & 128);
+ }
+
+ private function readSize(string $d, int &$p): int {
+ $byte = ord($d[$p++]);
+ $val = $byte & 127;
+ $shift = 7;
+ while ($byte & 128) {
+ $byte = ord($d[$p++]);
+ $val |= (($byte & 127) << $shift);
+ $shift += 7;
+ }
+ return $val;
+ }
+
+ private function getHandle(string $path) {
+ if (!isset($this->fileHandles[$path])) {
+ $this->fileHandles[$path] = @fopen($path, 'rb');
+ }
+ return $this->fileHandles[$path];
+ }
+}
+
GitRefs.php
+<?php
+class GitRefs {
+ private string $repoPath;
+
+ public function __construct(string $repoPath) {
+ $this->repoPath = $repoPath;
+ }
+
+ public function resolve(string $input): string {
+ if (preg_match('/^[0-9a-f]{40}$/', $input)) {
+ return $input;
+ }
+
+ if ($input === 'HEAD' && file_exists($headFile = "{$this->repoPath}/HEAD")) {
+ $head = trim(file_get_contents($headFile));
+ return (strpos($head, 'ref: ') === 0)
+ ? $this->resolve(substr($head, 5))
+ : $head;
+ }
+
+ return $this->resolveRef($input);
+ }
+
+ public function getMainBranch(): array {
+ $branches = [];
+ $this->scanRefs('refs/heads', function ($name, $sha) use (&$branches) {
+ $branches[$name] = $sha;
+ });
+
+ foreach (['main', 'master', 'trunk', 'develop'] as $try) {
+ if (isset($branches[$try])) {
+ return ['name' => $try, 'hash' => $branches[$try]];
+ }
+ }
+
+ $first = array_key_first($branches);
+ return $first ? ['name' => $first, 'hash' => $branches[$first]] : ['name' => '', 'hash' => ''];
+ }
+
+ public function scanRefs(string $prefix, callable $callback): void {
+ $dir = "{$this->repoPath}/$prefix";
+ if (is_dir($dir)) {
+ foreach (array_diff(scandir($dir), ['.', '..']) as $file) {
+ $callback($file, trim(file_get_contents("$dir/$file")));
+ }
+ }
+ }
+
+ private function resolveRef(string $input): string {
+ $paths = [$input, "refs/heads/$input", "refs/tags/$input"];
+
+ foreach ($paths as $ref) {
+ if (file_exists($path = "{$this->repoPath}/$ref")) {
+ return trim(file_get_contents($path));
+ }
+ }
+
+ return file_exists($packed = "{$this->repoPath}/packed-refs")
+ ? $this->findInPackedRefs($packed, $input)
+ : '';
+ }
+
+ private function findInPackedRefs(string $path, string $input): string {
+ $targets = [$input, "refs/heads/$input", "refs/tags/$input"];
+ foreach (file($path) as $line) {
+ if ($line[0] === '#' || $line[0] === '^') continue;
+ $parts = explode(' ', trim($line));
+ if (count($parts) >= 2 && in_array($parts[1], $targets)) {
+ return $parts[0];
+ }
+ }
+ return '';
+ }
+}
Delta509 lines added, 770 lines removed, 261-line decrease