Dave Jarvis' Repositories

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

Refactors code to eliminate duplication

Author Dave Jarvis <email>
Date 2026-02-20 17:00:48 GMT-0800
Commit d9fb2528783253665cb0661dca4de03190ff608d
Parent 2bee52a
git/BufferedFileReader.php
+<?php
+class BufferedFileReader {
+ private mixed $handle;
+ private bool $temporary;
+
+ private function __construct( mixed $handle, bool $temporary ) {
+ $this->handle = $handle;
+ $this->temporary = $temporary;
+ }
+
+ public static function open( string $path ): self {
+ return new self( fopen( $path, 'rb' ), false );
+ }
+
+ public static function createTemp(): self {
+ return new self( tmpfile(), true );
+ }
+
+ public function __destruct() {
+ if( $this->isOpen() ) {
+ fclose( $this->handle );
+ }
+ }
+
+ public function isOpen(): bool {
+ return is_resource( $this->handle );
+ }
+
+ public function read( int $length ): string {
+ return $this->isOpen() && !feof( $this->handle )
+ ? (string)fread( $this->handle, $length )
+ : '';
+ }
+
+ public function write( string $data ): bool {
+ return $this->temporary &&
+ $this->isOpen() &&
+ fwrite( $this->handle, $data ) !== false;
+ }
+
+ public function seek( int $offset, int $whence = SEEK_SET ): bool {
+ return $this->isOpen() &&
+ fseek( $this->handle, $offset, $whence ) === 0;
+ }
+
+ public function tell(): int {
+ return $this->isOpen()
+ ? (int)ftell( $this->handle )
+ : 0;
+ }
+
+ public function eof(): bool {
+ return $this->isOpen() ? feof( $this->handle ) : true;
+ }
+
+ public function rewind(): void {
+ if( $this->isOpen() ) {
+ rewind( $this->handle );
+ }
+ }
+}
git/Git.php
require_once __DIR__ . '/GitRefs.php';
require_once __DIR__ . '/GitPacks.php';
-
-class Git {
- private const MAX_READ = 1048576;
-
- private string $repoPath;
- private string $objPath;
- private GitRefs $refs;
- private GitPacks $packs;
-
- public function __construct( string $repoPath ) {
- $this->setRepository( $repoPath );
- }
-
- public function setRepository( string $repoPath ): void {
- $this->repoPath = rtrim( $repoPath, '/' );
- $this->objPath = $this->repoPath . '/objects';
- $this->refs = new GitRefs( $this->repoPath );
- $this->packs = new GitPacks( $this->objPath );
- }
-
- public function resolve( string $reference ): string {
- return $this->refs->resolve( $reference );
- }
-
- public function getMainBranch(): array {
- return $this->refs->getMainBranch();
- }
-
- public function eachBranch( callable $callback ): void {
- $this->refs->scanRefs( 'refs/heads', $callback );
- }
-
- public function eachTag( callable $callback ): void {
- $this->refs->scanRefs( 'refs/tags', function( $name, $sha ) use (
- $callback
- ) {
- $data = $this->read( $sha );
- $tag = $this->parseTagData( $name, $sha, $data );
-
- $callback( $tag );
- } );
- }
-
- public function walk(
- string $refOrSha,
- callable $callback,
- string $path = ''
- ): void {
- $sha = $this->resolve( $refOrSha );
- $treeSha = '';
-
- if( $sha !== '' ) {
- $treeSha = $this->getTreeSha( $sha );
- }
-
- if( $path !== '' && $treeSha !== '' ) {
- $info = $this->resolvePath( $treeSha, $path );
- $treeSha = $info['isDir'] ? $info['sha'] : '';
- }
-
- if( $treeSha !== '' ) {
- $this->walkTree( $treeSha, $callback );
- }
- }
-
- public function readFile( string $ref, string $path ): File {
- $sha = $this->resolve( $ref );
- $tree = $sha !== '' ? $this->getTreeSha( $sha ) : '';
- $info = $tree !== '' ? $this->resolvePath( $tree, $path ) : [];
- $file = new MissingFile();
-
- if( isset( $info['sha'] ) && !$info['isDir'] && $info['sha'] !== '' ) {
- $file = new File(
- basename( $path ),
- $info['sha'],
- $info['mode'],
- 0,
- $this->getObjectSize( $info['sha'] ),
- $this->peek( $info['sha'] )
- );
- }
-
- return $file;
- }
-
- public function getObjectSize( string $sha, string $path = '' ): int {
- $target = $sha;
- $result = 0;
-
- if( $path !== '' ) {
- $info = $this->resolvePath(
- $this->getTreeSha( $this->resolve( $sha ) ),
- $path
- );
- $target = $info['sha'] ?? '';
- }
-
- if( $target !== '' ) {
- $result = $this->packs->getSize( $target );
-
- if( $result === 0 ) {
- $result = $this->getLooseObjectSize( $target );
- }
- }
-
- return $result;
- }
-
- public function stream(
- string $sha,
- callable $callback,
- string $path = ''
- ): void {
- $target = $sha;
-
- if( $path !== '' ) {
- $info = $this->resolvePath(
- $this->getTreeSha( $this->resolve( $sha ) ),
- $path
- );
- $target = isset( $info['isDir'] ) && !$info['isDir']
- ? $info['sha']
- : '';
- }
-
- if( $target !== '' ) {
- $this->slurp( $target, $callback );
- }
- }
-
- public function peek( string $sha, int $length = 255 ): string {
- $size = $this->packs->getSize( $sha );
-
- return $size === 0
- ? $this->peekLooseObject( $sha, $length )
- : $this->packs->peek( $sha, $length );
- }
-
- public function read( string $sha ): string {
- $size = $this->getObjectSize( $sha );
- $content = '';
-
- if( $size > 0 && $size <= self::MAX_READ ) {
- $this->slurp( $sha, function( $chunk ) use ( &$content ) {
- $content .= $chunk;
- } );
- }
-
- return $content;
- }
-
- public function history(
- string $ref,
- int $limit,
- callable $callback
- ): void {
- $sha = $this->resolve( $ref );
- $count = 0;
-
- while( $sha !== '' && $count < $limit ) {
- $commit = $this->parseCommit( $sha );
-
- if( $commit->sha === '' ) {
- $sha = '';
- }
-
- if( $sha !== '' ) {
- $callback( $commit );
- $sha = $commit->parentSha;
- $count++;
- }
- }
- }
-
- public function streamRaw( string $subPath ): bool {
- $result = false;
-
- if( strpos( $subPath, '..' ) === false ) {
- $path = "{$this->repoPath}/$subPath";
-
- if( is_file( $path ) ) {
- $real = realpath( $path );
- $repo = realpath( $this->repoPath );
-
- if( $real && strpos( $real, $repo ) === 0 ) {
- $result = $this->streamFileContent( $path );
- }
- }
- }
-
- return $result;
- }
-
- private function streamFileContent( string $path ): bool {
- $result = false;
-
- if( $path !== '' ) {
- header( 'X-Accel-Redirect: ' . $path );
- header( 'Content-Type: application/octet-stream' );
-
- $result = true;
- }
-
- return $result;
- }
-
- public function eachRef( callable $callback ): void {
- $head = $this->resolve( 'HEAD' );
-
- if( $head !== '' ) {
- $callback( 'HEAD', $head );
- }
-
- $this->refs->scanRefs( 'refs/heads', function( $n, $s ) use ( $callback ) {
- $callback( "refs/heads/$n", $s );
- } );
-
- $this->refs->scanRefs( 'refs/tags', function( $n, $s ) use ( $callback ) {
- $callback( "refs/tags/$n", $s );
- } );
- }
-
- public function generatePackfile( array $objs ): Generator {
- $ctx = hash_init( 'sha1' );
- $head = "PACK" . pack( 'N', 2 ) . pack( 'N', count( $objs ) );
-
- hash_update( $ctx, $head );
- yield $head;
-
- foreach( $objs as $sha => $type ) {
- $size = $this->getObjectSize( $sha );
- $byte = $type << 4 | $size & 0x0f;
- $sz = $size >> 4;
- $hdr = '';
-
- while( $sz > 0 ) {
- $hdr .= chr( $byte | 0x80 );
- $byte = $sz & 0x7f;
- $sz >>= 7;
- }
-
- $hdr .= chr( $byte );
- hash_update( $ctx, $hdr );
- yield $hdr;
-
- $deflate = deflate_init( ZLIB_ENCODING_DEFLATE );
-
- foreach( $this->slurpChunks( $sha ) as $raw ) {
- $compressed = deflate_add( $deflate, $raw, ZLIB_NO_FLUSH );
-
- if( $compressed !== '' ) {
- hash_update( $ctx, $compressed );
- yield $compressed;
- }
- }
-
- $final = deflate_add( $deflate, '', ZLIB_FINISH );
-
- if( $final !== '' ) {
- hash_update( $ctx, $final );
- yield $final;
- }
- }
-
- yield hash_final( $ctx, true );
- }
-
- private function slurpChunks( string $sha ): Generator {
- $path = $this->getLoosePath( $sha );
-
- if( is_file( $path ) ) {
- yield from $this->looseObjectChunks( $path );
- } else {
- $any = false;
-
- foreach( $this->packs->streamGenerator( $sha ) as $chunk ) {
- $any = true;
- yield $chunk;
- }
-
- if( !$any ) {
- $data = $this->packs->read( $sha );
-
- if( $data !== '' ) {
- yield $data;
- }
- }
- }
- }
-
- private function looseObjectChunks( string $path ): Generator {
- $handle = fopen( $path, 'rb' );
- $infl = $handle ? inflate_init( ZLIB_ENCODING_DEFLATE ) : null;
-
- if( !$handle || !$infl ) {
- return;
- }
-
- $found = false;
- $buffer = '';
-
- while( !feof( $handle ) ) {
- $chunk = fread( $handle, 16384 );
- $inflated = inflate_add( $infl, $chunk );
-
- if( $inflated === false ) {
- break;
- }
-
- if( !$found ) {
- $buffer .= $inflated;
- $eos = strpos( $buffer, "\0" );
-
- if( $eos !== false ) {
- $found = true;
- $body = substr( $buffer, $eos + 1 );
-
- if( $body !== '' ) {
- yield $body;
- }
-
- $buffer = '';
- }
- } elseif( $inflated !== '' ) {
- yield $inflated;
- }
- }
-
- fclose( $handle );
- }
-
- private function streamCompressedObject( string $sha, $ctx ): Generator {
- $stream = CompressionStream::createDeflater();
- $buffer = '';
-
- $this->slurp( $sha, function( $chunk ) use (
- $stream,
- $ctx,
- &$buffer
- ) {
- $compressed = $stream->pump( $chunk );
-
- if( $compressed !== '' ) {
- hash_update( $ctx, $compressed );
- $buffer .= $compressed;
- }
- } );
-
- $final = $stream->finish();
-
- if( $final !== '' ) {
- hash_update( $ctx, $final );
- $buffer .= $final;
- }
-
- $pos = 0;
- $len = strlen( $buffer );
-
- while( $pos < $len ) {
- $chunk = substr( $buffer, $pos, 32768 );
-
- yield $chunk;
- $pos += 32768;
- }
- }
-
- private function getTreeSha( string $commitOrTreeSha ): string {
- $data = $this->read( $commitOrTreeSha );
- $sha = $commitOrTreeSha;
-
- if( preg_match( '/^object ([0-9a-f]{40})/m', $data, $matches ) ) {
- $sha = $this->getTreeSha( $matches[1] );
- }
-
- if( $sha === $commitOrTreeSha &&
- preg_match( '/^tree ([0-9a-f]{40})/m', $data, $matches ) ) {
- $sha = $matches[1];
- }
-
- return $sha;
- }
-
- private function resolvePath( string $treeSha, string $path ): array {
- $parts = explode( '/', trim( $path, '/' ) );
- $sha = $treeSha;
- $mode = '40000';
-
- foreach( $parts as $part ) {
- $entry = [ 'sha' => '', 'mode' => '' ];
-
- if( $part !== '' && $sha !== '' ) {
- $entry = $this->findTreeEntry( $sha, $part );
- }
-
- $sha = $entry['sha'];
- $mode = $entry['mode'];
- }
-
- return [
- 'sha' => $sha,
- 'mode' => $mode,
- 'isDir' => $mode === '40000' || $mode === '040000'
- ];
- }
-
- private function findTreeEntry( string $treeSha, string $name ): array {
- $data = $this->read( $treeSha );
- $pos = 0;
- $len = strlen( $data );
- $entry = [ 'sha' => '', 'mode' => '' ];
-
- while( $pos < $len ) {
- $space = strpos( $data, ' ', $pos );
- $eos = strpos( $data, "\0", $space );
-
- if( $space === false || $eos === false ) {
- break;
- }
-
- if( substr( $data, $space + 1, $eos - $space - 1 ) === $name ) {
- $entry = [
- 'sha' => bin2hex( substr( $data, $eos + 1, 20 ) ),
- 'mode' => substr( $data, $pos, $space - $pos )
- ];
- break;
- }
-
- $pos = $eos + 21;
- }
-
- return $entry;
- }
-
- private function parseTagData(
- string $name,
- string $sha,
- string $data
- ): Tag {
- $isAnn = strncmp( $data, 'object ', 7 ) === 0;
- $pattern = $isAnn
- ? '/^tagger (.*) <(.*)> (\d+) [+\-]\d{4}$/m'
- : '/^author (.*) <(.*)> (\d+) [+\-]\d{4}$/m';
- $id = $this->parseIdentity( $data, $pattern );
- $target = $isAnn
- ? $this->extractPattern( $data, '/^object (.*)$/m', 1, $sha )
- : $sha;
-
- return new Tag(
- $name,
- $sha,
- $target,
- $id['timestamp'],
- $this->extractMessage( $data ),
- $id['name']
- );
- }
-
- private function extractPattern(
- string $data,
- string $pattern,
- int $group,
- string $default = ''
- ): string {
- return preg_match( $pattern, $data, $matches )
- ? $matches[$group]
- : $default;
- }
-
- private function parseIdentity( string $data, string $pattern ): array {
- $found = preg_match( $pattern, $data, $matches );
-
- return [
- 'name' => $found ? trim( $matches[1] ) : 'Unknown',
- 'email' => $found ? $matches[2] : '',
- 'timestamp' => $found ? (int)$matches[3] : 0
- ];
- }
-
- private function extractMessage( string $data ): string {
- $pos = strpos( $data, "\n\n" );
-
- return $pos !== false ? trim( substr( $data, $pos + 2 ) ) : '';
- }
-
- private function slurp( string $sha, callable $callback ): void {
- $path = $this->getLoosePath( $sha );
-
- if( is_file( $path ) ) {
- $this->slurpLooseObject( $path, $callback );
- } else {
- $this->slurpPackedObject( $sha, $callback );
- }
- }
-
- private function slurpLooseObject( string $path, callable $callback ): void {
- $this->iterateInflated(
- $path,
- function( $chunk ) use ( $callback ) {
- if( $chunk !== '' ) {
- $callback( $chunk );
- }
- return true;
- }
- );
- }
-
- private function slurpPackedObject( string $sha, callable $callback ): void {
- $streamed = $this->packs->stream( $sha, $callback );
-
- if( !$streamed ) {
- $data = $this->packs->read( $sha );
-
- if( $data !== '' ) {
- $callback( $data );
- }
- }
- }
-
- private function iterateInflated(
- string $path,
- callable $processor
- ): void {
- $handle = fopen( $path, 'rb' );
- $infl = $handle ? inflate_init( ZLIB_ENCODING_DEFLATE ) : null;
- $found = false;
- $buffer = '';
-
- if( $handle && $infl ) {
- while( !feof( $handle ) ) {
- $chunk = fread( $handle, 16384 );
- $inflated = inflate_add( $infl, $chunk );
-
- if( $inflated === false ) {
- break;
- }
-
- if( !$found ) {
- $buffer .= $inflated;
- $eos = strpos( $buffer, "\0" );
-
- if( $eos !== false ) {
- $found = true;
- $body = substr( $buffer, $eos + 1 );
- $head = substr( $buffer, 0, $eos );
-
- if( $processor( $body, $head ) === false ) {
- break;
- }
- }
- } elseif( $processor( $inflated, null ) === false ) {
- break;
- }
- }
-
- fclose( $handle );
- }
- }
-
- private function peekLooseObject( string $sha, int $length ): string {
- $path = $this->getLoosePath( $sha );
- $buf = '';
-
- if( is_file( $path ) ) {
- $this->iterateInflated(
- $path,
- function( $chunk ) use ( $length, &$buf ) {
- $buf .= $chunk;
- return strlen( $buf ) < $length;
- }
- );
- }
-
- return substr( $buf, 0, $length );
- }
-
- private function parseCommit( string $sha ): object {
- $data = $this->read( $sha );
- $result = (object)[ 'sha' => '' ];
-
- if( $data !== '' ) {
- $id = $this->parseIdentity(
- $data,
- '/^author (.*) <(.*)> (\d+)/m'
- );
-
- $result = (object)[
- 'sha' => $sha,
- 'message' => $this->extractMessage( $data ),
- 'author' => $id['name'],
- 'email' => $id['email'],
- 'date' => $id['timestamp'],
- 'parentSha' => $this->extractPattern( $data, '/^parent (.*)$/m', 1 )
- ];
- }
-
- return $result;
- }
-
- private function walkTree( string $sha, callable $callback ): void {
- $data = $this->read( $sha );
- $tree = $data;
-
- if( $data !== '' && preg_match( '/^tree (.*)$/m', $data, $m ) ) {
- $tree = $this->read( $m[1] );
- }
-
- if( $tree !== '' && $this->isTreeData( $tree ) ) {
- $this->processTree( $tree, $callback );
- }
- }
-
- private function processTree( string $data, callable $callback ): void {
- $pos = 0;
- $len = strlen( $data );
-
- while( $pos < $len ) {
- $space = strpos( $data, ' ', $pos );
- $eos = strpos( $data, "\0", $space );
- $entry = null;
-
- if( $space !== false && $eos !== false && $eos + 21 <= $len ) {
- $mode = substr( $data, $pos, $space - $pos );
- $sha = bin2hex( substr( $data, $eos + 1, 20 ) );
- $dir = $mode === '40000' || $mode === '040000';
- $isSub = $mode === '160000';
-
- $entry = [
- 'file' => new File(
- substr( $data, $space + 1, $eos - $space - 1 ),
- $sha,
- $mode,
- 0,
- $dir || $isSub ? 0 : $this->getObjectSize( $sha ),
- $dir || $isSub ? '' : $this->peek( $sha )
- ),
- 'nextPosition' => $eos + 21
- ];
- }
-
- if( $entry === null ) {
- break;
- }
-
- $callback( $entry['file'] );
- $pos = $entry['nextPosition'];
- }
- }
-
- private function isTreeData( string $data ): bool {
- $len = strlen( $data );
- $patt = '/^(40000|100644|100755|120000|160000) /';
- $match = $len >= 25 && preg_match( $patt, $data );
- $eos = $match ? strpos( $data, "\0" ) : false;
-
- return $match && $eos !== false && $eos + 21 <= $len;
- }
-
- 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 );
- $size = 0;
-
- if( is_file( $path ) ) {
- $this->iterateInflated(
- $path,
- function( $c, $head ) use ( &$size ) {
- if( $head !== null ) {
- $parts = explode( ' ', $head );
- $size = isset( $parts[1] ) ? (int)$parts[1] : 0;
- }
+require_once __DIR__ . '/BufferedFileReader.php';
+
+class Git {
+ private const MAX_READ = 1048576;
+
+ private string $repoPath;
+ private string $objPath;
+ private GitRefs $refs;
+ private GitPacks $packs;
+
+ public function __construct( string $repoPath ) {
+ $this->setRepository( $repoPath );
+ }
+
+ public function setRepository( string $repoPath ): void {
+ $this->repoPath = rtrim( $repoPath, '/' );
+ $this->objPath = $this->repoPath . '/objects';
+ $this->refs = new GitRefs( $this->repoPath );
+ $this->packs = new GitPacks( $this->objPath );
+ }
+
+ public function resolve( string $reference ): string {
+ return $this->refs->resolve( $reference );
+ }
+
+ public function getMainBranch(): array {
+ return $this->refs->getMainBranch();
+ }
+
+ public function eachBranch( callable $callback ): void {
+ $this->refs->scanRefs( 'refs/heads', $callback );
+ }
+
+ public function eachTag( callable $callback ): void {
+ $this->refs->scanRefs( 'refs/tags', function( $name, $sha ) use (
+ $callback
+ ) {
+ $data = $this->read( $sha );
+ $tag = $this->parseTagData( $name, $sha, $data );
+
+ $callback( $tag );
+ } );
+ }
+
+ public function walk(
+ string $refOrSha,
+ callable $callback,
+ string $path = ''
+ ): void {
+ $sha = $this->resolve( $refOrSha );
+ $treeSha = '';
+
+ if( $sha !== '' ) {
+ $treeSha = $this->getTreeSha( $sha );
+ }
+
+ if( $path !== '' && $treeSha !== '' ) {
+ $info = $this->resolvePath( $treeSha, $path );
+ $treeSha = $info['isDir'] ? $info['sha'] : '';
+ }
+
+ if( $treeSha !== '' ) {
+ $this->walkTree( $treeSha, $callback );
+ }
+ }
+
+ public function readFile( string $ref, string $path ): File {
+ $sha = $this->resolve( $ref );
+ $tree = $sha !== '' ? $this->getTreeSha( $sha ) : '';
+ $info = $tree !== '' ? $this->resolvePath( $tree, $path ) : [];
+ $file = new MissingFile();
+
+ if( isset( $info['sha'] ) && !$info['isDir'] && $info['sha'] !== '' ) {
+ $file = new File(
+ basename( $path ),
+ $info['sha'],
+ $info['mode'],
+ 0,
+ $this->getObjectSize( $info['sha'] ),
+ $this->peek( $info['sha'] )
+ );
+ }
+
+ return $file;
+ }
+
+ public function getObjectSize( string $sha, string $path = '' ): int {
+ $target = $sha;
+ $result = 0;
+
+ if( $path !== '' ) {
+ $info = $this->resolvePath(
+ $this->getTreeSha( $this->resolve( $sha ) ),
+ $path
+ );
+ $target = $info['sha'] ?? '';
+ }
+
+ if( $target !== '' ) {
+ $result = $this->packs->getSize( $target );
+
+ if( $result === 0 ) {
+ $result = $this->getLooseObjectSize( $target );
+ }
+ }
+
+ return $result;
+ }
+
+ public function stream(
+ string $sha,
+ callable $callback,
+ string $path = ''
+ ): void {
+ $target = $sha;
+
+ if( $path !== '' ) {
+ $info = $this->resolvePath(
+ $this->getTreeSha( $this->resolve( $sha ) ),
+ $path
+ );
+ $target = isset( $info['isDir'] ) && !$info['isDir']
+ ? $info['sha']
+ : '';
+ }
+
+ if( $target !== '' ) {
+ $this->slurp( $target, $callback );
+ }
+ }
+
+ public function peek( string $sha, int $length = 255 ): string {
+ $size = $this->packs->getSize( $sha );
+
+ return $size === 0
+ ? $this->peekLooseObject( $sha, $length )
+ : $this->packs->peek( $sha, $length );
+ }
+
+ public function read( string $sha ): string {
+ $size = $this->getObjectSize( $sha );
+ $content = '';
+
+ if( $size > 0 && $size <= self::MAX_READ ) {
+ $this->slurp( $sha, function( $chunk ) use ( &$content ) {
+ $content .= $chunk;
+ } );
+ }
+
+ return $content;
+ }
+
+ public function history(
+ string $ref,
+ int $limit,
+ callable $callback
+ ): void {
+ $sha = $this->resolve( $ref );
+ $count = 0;
+
+ while( $sha !== '' && $count < $limit ) {
+ $commit = $this->parseCommit( $sha );
+
+ if( $commit->sha === '' ) {
+ $sha = '';
+ }
+
+ if( $sha !== '' ) {
+ $callback( $commit );
+ $sha = $commit->parentSha;
+ $count++;
+ }
+ }
+ }
+
+ public function streamRaw( string $subPath ): bool {
+ $result = false;
+
+ if( strpos( $subPath, '..' ) === false ) {
+ $path = "{$this->repoPath}/$subPath";
+
+ if( is_file( $path ) ) {
+ $real = realpath( $path );
+ $repo = realpath( $this->repoPath );
+
+ if( $real !== false && strpos( $real, $repo ) === 0 ) {
+ $result = $this->streamFileContent( $path );
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ private function streamFileContent( string $path ): bool {
+ $result = false;
+
+ if( $path !== '' ) {
+ header( 'X-Accel-Redirect: ' . $path );
+ header( 'Content-Type: application/octet-stream' );
+
+ $result = true;
+ }
+
+ return $result;
+ }
+
+ public function eachRef( callable $callback ): void {
+ $head = $this->resolve( 'HEAD' );
+
+ if( $head !== '' ) {
+ $callback( 'HEAD', $head );
+ }
+
+ $this->refs->scanRefs( 'refs/heads', function( $n, $s ) use ( $callback ) {
+ $callback( "refs/heads/$n", $s );
+ } );
+
+ $this->refs->scanRefs( 'refs/tags', function( $n, $s ) use ( $callback ) {
+ $callback( "refs/tags/$n", $s );
+ } );
+ }
+
+ public function generatePackfile( array $objs ): Generator {
+ $ctx = hash_init( 'sha1' );
+ $head = "PACK" . pack( 'N', 2 ) . pack( 'N', count( $objs ) );
+
+ hash_update( $ctx, $head );
+ yield $head;
+
+ foreach( $objs as $sha => $type ) {
+ $size = $this->getObjectSize( $sha );
+ $byte = $type << 4 | $size & 0x0f;
+ $sz = $size >> 4;
+ $hdr = '';
+
+ while( $sz > 0 ) {
+ $hdr .= chr( $byte | 0x80 );
+ $byte = $sz & 0x7f;
+ $sz >>= 7;
+ }
+
+ $hdr .= chr( $byte );
+ hash_update( $ctx, $hdr );
+ yield $hdr;
+
+ $deflate = deflate_init( ZLIB_ENCODING_DEFLATE );
+
+ foreach( $this->slurpChunks( $sha ) as $raw ) {
+ $compressed = deflate_add( $deflate, $raw, ZLIB_NO_FLUSH );
+
+ if( $compressed !== '' ) {
+ hash_update( $ctx, $compressed );
+ yield $compressed;
+ }
+ }
+
+ $final = deflate_add( $deflate, '', ZLIB_FINISH );
+
+ if( $final !== '' ) {
+ hash_update( $ctx, $final );
+ yield $final;
+ }
+ }
+
+ yield hash_final( $ctx, true );
+ }
+
+ private function slurpChunks( string $sha ): Generator {
+ $path = $this->getLoosePath( $sha );
+
+ if( is_file( $path ) ) {
+ yield from $this->looseObjectChunks( $path );
+ } else {
+ $any = false;
+
+ foreach( $this->packs->streamGenerator( $sha ) as $chunk ) {
+ $any = true;
+ yield $chunk;
+ }
+
+ if( !$any ) {
+ $data = $this->packs->read( $sha );
+
+ if( $data !== '' ) {
+ yield $data;
+ }
+ }
+ }
+ }
+
+ private function looseObjectChunks( string $path ): Generator {
+ $reader = BufferedFileReader::open( $path );
+ $infl = $reader->isOpen()
+ ? inflate_init( ZLIB_ENCODING_DEFLATE )
+ : false;
+
+ if( $reader->isOpen() && $infl !== false ) {
+ $found = false;
+ $buffer = '';
+
+ while( !$reader->eof() ) {
+ $chunk = $reader->read( 16384 );
+ $inflated = inflate_add( $infl, $chunk );
+
+ if( $inflated === false ) {
+ break;
+ }
+
+ if( !$found ) {
+ $buffer .= $inflated;
+ $eos = strpos( $buffer, "\0" );
+
+ if( $eos !== false ) {
+ $found = true;
+ $body = substr( $buffer, $eos + 1 );
+
+ if( $body !== '' ) {
+ yield $body;
+ }
+
+ $buffer = '';
+ }
+ } elseif( $inflated !== '' ) {
+ yield $inflated;
+ }
+ }
+ }
+ }
+
+ private function streamCompressedObject( string $sha, $ctx ): Generator {
+ $stream = CompressionStream::createDeflater();
+ $buffer = '';
+
+ $this->slurp( $sha, function( $chunk ) use (
+ $stream,
+ $ctx,
+ &$buffer
+ ) {
+ $compressed = $stream->pump( $chunk );
+
+ if( $compressed !== '' ) {
+ hash_update( $ctx, $compressed );
+ $buffer .= $compressed;
+ }
+ } );
+
+ $final = $stream->finish();
+
+ if( $final !== '' ) {
+ hash_update( $ctx, $final );
+ $buffer .= $final;
+ }
+
+ $pos = 0;
+ $len = strlen( $buffer );
+
+ while( $pos < $len ) {
+ $chunk = substr( $buffer, $pos, 32768 );
+
+ yield $chunk;
+ $pos += 32768;
+ }
+ }
+
+ private function getTreeSha( string $commitOrTreeSha ): string {
+ $data = $this->read( $commitOrTreeSha );
+ $sha = $commitOrTreeSha;
+
+ if( preg_match( '/^object ([0-9a-f]{40})/m', $data, $matches ) ) {
+ $sha = $this->getTreeSha( $matches[1] );
+ }
+
+ if( $sha === $commitOrTreeSha &&
+ preg_match( '/^tree ([0-9a-f]{40})/m', $data, $matches ) ) {
+ $sha = $matches[1];
+ }
+
+ return $sha;
+ }
+
+ private function resolvePath( string $treeSha, string $path ): array {
+ $parts = explode( '/', trim( $path, '/' ) );
+ $sha = $treeSha;
+ $mode = '40000';
+
+ foreach( $parts as $part ) {
+ $entry = [ 'sha' => '', 'mode' => '' ];
+
+ if( $part !== '' && $sha !== '' ) {
+ $entry = $this->findTreeEntry( $sha, $part );
+ }
+
+ $sha = $entry['sha'];
+ $mode = $entry['mode'];
+ }
+
+ return [
+ 'sha' => $sha,
+ 'mode' => $mode,
+ 'isDir' => $mode === '40000' || $mode === '040000'
+ ];
+ }
+
+ private function findTreeEntry( string $treeSha, string $name ): array {
+ $data = $this->read( $treeSha );
+ $pos = 0;
+ $len = strlen( $data );
+ $entry = [ 'sha' => '', 'mode' => '' ];
+
+ while( $pos < $len ) {
+ $space = strpos( $data, ' ', $pos );
+ $eos = strpos( $data, "\0", $space );
+
+ if( $space === false || $eos === false ) {
+ break;
+ }
+
+ if( substr( $data, $space + 1, $eos - $space - 1 ) === $name ) {
+ $entry = [
+ 'sha' => bin2hex( substr( $data, $eos + 1, 20 ) ),
+ 'mode' => substr( $data, $pos, $space - $pos )
+ ];
+ break;
+ }
+
+ $pos = $eos + 21;
+ }
+
+ return $entry;
+ }
+
+ private function parseTagData(
+ string $name,
+ string $sha,
+ string $data
+ ): Tag {
+ $isAnn = strncmp( $data, 'object ', 7 ) === 0;
+ $pattern = $isAnn
+ ? '/^tagger (.*) <(.*)> (\d+) [+\-]\d{4}$/m'
+ : '/^author (.*) <(.*)> (\d+) [+\-]\d{4}$/m';
+ $id = $this->parseIdentity( $data, $pattern );
+ $target = $isAnn
+ ? $this->extractPattern( $data, '/^object (.*)$/m', 1, $sha )
+ : $sha;
+
+ return new Tag(
+ $name,
+ $sha,
+ $target,
+ $id['timestamp'],
+ $this->extractMessage( $data ),
+ $id['name']
+ );
+ }
+
+ private function extractPattern(
+ string $data,
+ string $pattern,
+ int $group,
+ string $default = ''
+ ): string {
+ return preg_match( $pattern, $data, $matches )
+ ? $matches[$group]
+ : $default;
+ }
+
+ private function parseIdentity( string $data, string $pattern ): array {
+ $found = preg_match( $pattern, $data, $matches );
+
+ return [
+ 'name' => $found ? trim( $matches[1] ) : 'Unknown',
+ 'email' => $found ? $matches[2] : '',
+ 'timestamp' => $found ? (int)$matches[3] : 0
+ ];
+ }
+
+ private function extractMessage( string $data ): string {
+ $pos = strpos( $data, "\n\n" );
+
+ return $pos !== false ? trim( substr( $data, $pos + 2 ) ) : '';
+ }
+
+ private function slurp( string $sha, callable $callback ): void {
+ $path = $this->getLoosePath( $sha );
+
+ if( is_file( $path ) ) {
+ $this->slurpLooseObject( $path, $callback );
+ } else {
+ $this->slurpPackedObject( $sha, $callback );
+ }
+ }
+
+ private function slurpLooseObject( string $path, callable $callback ): void {
+ $this->iterateInflated(
+ $path,
+ function( $chunk ) use ( $callback ) {
+ if( $chunk !== '' ) {
+ $callback( $chunk );
+ }
+
+ return true;
+ }
+ );
+ }
+
+ private function slurpPackedObject( string $sha, callable $callback ): void {
+ $streamed = $this->packs->stream( $sha, $callback );
+
+ if( !$streamed ) {
+ $data = $this->packs->read( $sha );
+
+ if( $data !== '' ) {
+ $callback( $data );
+ }
+ }
+ }
+
+ private function iterateInflated(
+ string $path,
+ callable $processor
+ ): void {
+ $reader = BufferedFileReader::open( $path );
+ $infl = $reader->isOpen()
+ ? inflate_init( ZLIB_ENCODING_DEFLATE )
+ : false;
+ $found = false;
+ $buffer = '';
+
+ if( $reader->isOpen() && $infl !== false ) {
+ while( !$reader->eof() ) {
+ $chunk = $reader->read( 16384 );
+ $inflated = inflate_add( $infl, $chunk );
+
+ if( $inflated === false ) {
+ break;
+ }
+
+ if( !$found ) {
+ $buffer .= $inflated;
+ $eos = strpos( $buffer, "\0" );
+
+ if( $eos !== false ) {
+ $found = true;
+ $body = substr( $buffer, $eos + 1 );
+ $head = substr( $buffer, 0, $eos );
+
+ if( $processor( $body, $head ) === false ) {
+ break;
+ }
+ }
+ } elseif( $processor( $inflated, '' ) === false ) {
+ break;
+ }
+ }
+ }
+ }
+
+ private function peekLooseObject( string $sha, int $length ): string {
+ $path = $this->getLoosePath( $sha );
+ $buf = '';
+
+ if( is_file( $path ) ) {
+ $this->iterateInflated(
+ $path,
+ function( $chunk ) use ( $length, &$buf ) {
+ $buf .= $chunk;
+
+ return strlen( $buf ) < $length;
+ }
+ );
+ }
+
+ return substr( $buf, 0, $length );
+ }
+
+ private function parseCommit( string $sha ): object {
+ $data = $this->read( $sha );
+ $result = (object)[ 'sha' => '' ];
+
+ if( $data !== '' ) {
+ $id = $this->parseIdentity(
+ $data,
+ '/^author (.*) <(.*)> (\d+)/m'
+ );
+
+ $result = (object)[
+ 'sha' => $sha,
+ 'message' => $this->extractMessage( $data ),
+ 'author' => $id['name'],
+ 'email' => $id['email'],
+ 'date' => $id['timestamp'],
+ 'parentSha' => $this->extractPattern( $data, '/^parent (.*)$/m', 1 )
+ ];
+ }
+
+ return $result;
+ }
+
+ private function walkTree( string $sha, callable $callback ): void {
+ $data = $this->read( $sha );
+ $tree = $data;
+
+ if( $data !== '' && preg_match( '/^tree (.*)$/m', $data, $m ) ) {
+ $tree = $this->read( $m[1] );
+ }
+
+ if( $tree !== '' && $this->isTreeData( $tree ) ) {
+ $this->processTree( $tree, $callback );
+ }
+ }
+
+ private function processTree( string $data, callable $callback ): void {
+ $pos = 0;
+ $len = strlen( $data );
+
+ while( $pos < $len ) {
+ $space = strpos( $data, ' ', $pos );
+ $eos = strpos( $data, "\0", $space );
+ $entry = null;
+
+ if( $space !== false && $eos !== false && $eos + 21 <= $len ) {
+ $mode = substr( $data, $pos, $space - $pos );
+ $sha = bin2hex( substr( $data, $eos + 1, 20 ) );
+ $dir = $mode === '40000' || $mode === '040000';
+ $isSub = $mode === '160000';
+
+ $entry = [
+ 'file' => new File(
+ substr( $data, $space + 1, $eos - $space - 1 ),
+ $sha,
+ $mode,
+ 0,
+ $dir || $isSub ? 0 : $this->getObjectSize( $sha ),
+ $dir || $isSub ? '' : $this->peek( $sha )
+ ),
+ 'nextPosition' => $eos + 21
+ ];
+ }
+
+ if( $entry === null ) {
+ break;
+ }
+
+ $callback( $entry['file'] );
+ $pos = $entry['nextPosition'];
+ }
+ }
+
+ private function isTreeData( string $data ): bool {
+ $len = strlen( $data );
+ $patt = '/^(40000|100644|100755|120000|160000) /';
+ $match = $len >= 25 && preg_match( $patt, $data );
+ $eos = $match ? strpos( $data, "\0" ) : false;
+
+ return $match && $eos !== false && $eos + 21 <= $len;
+ }
+
+ 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 );
+ $size = 0;
+
+ if( is_file( $path ) ) {
+ $this->iterateInflated(
+ $path,
+ function( $c, $head ) use ( &$size ) {
+ if( $head !== '' ) {
+ $parts = explode( ' ', $head );
+ $size = isset( $parts[1] ) ? (int)$parts[1] : 0;
+ }
+
return false;
}
git/GitDiff.php
}
}
+
$buffer = [];
}
git/GitPacks.php
public function stream( string $sha, callable $callback ): bool {
- return $this->streamInternal( $sha, $callback, 0 );
- }
-
- public function streamGenerator( string $sha ): Generator {
- $info = $this->findPackInfo( $sha );
-
- if( $info['offset'] !== 0 ) {
- $handle = $this->getHandle( $info['file'] );
-
- if( $handle ) {
- yield from $this->streamPackEntryGenerator(
- $handle,
- $info['offset'],
- 0
- );
- }
- }
- }
-
- private function streamInternal(
- string $sha,
- callable $callback,
- int $depth
- ): bool {
- $info = $this->findPackInfo( $sha );
- $result = false;
-
- if( $info['offset'] !== 0 ) {
- $size = $this->extractPackedSize( $info['file'], $info['offset'] );
- $handle = $this->getHandle( $info['file'] );
-
- if( $handle ) {
- $result = $this->streamPackEntry(
- $handle,
- $info['offset'],
- $size,
- $callback,
- $depth
- );
- }
- }
-
- return $result;
- }
-
- public function getSize( string $sha ): int {
- $info = $this->findPackInfo( $sha );
- $result = 0;
-
- if( $info['offset'] !== 0 ) {
- $result = $this->extractPackedSize( $info['file'], $info['offset'] );
- }
-
- return $result;
- }
-
- private function findPackInfo( string $sha ): array {
- $result = [ 'offset' => 0, 'file' => '' ];
-
- if( strlen( $sha ) === 40 && ctype_xdigit( $sha ) ) {
- $binarySha = hex2bin( $sha );
-
- if( $this->lastPack !== '' ) {
- $offset = $this->findInIdx( $this->lastPack, $binarySha );
-
- if( $offset !== 0 ) {
- $result = [
- 'file' => str_replace( '.idx', '.pack', $this->lastPack ),
- 'offset' => $offset
- ];
- }
- }
-
- if( $result['offset'] === 0 ) {
- $count = count( $this->packFiles );
- $idx = 0;
- $found = false;
-
- while( !$found && $idx < $count ) {
- $indexFile = $this->packFiles[$idx];
-
- if( $indexFile !== $this->lastPack ) {
- $offset = $this->findInIdx( $indexFile, $binarySha );
-
- if( $offset !== 0 ) {
- $this->lastPack = $indexFile;
- $result = [
- 'file' => str_replace( '.idx', '.pack', $indexFile ),
- 'offset' => $offset
- ];
- $found = true;
- }
- }
-
- $idx++;
- }
- }
- }
-
- return $result;
- }
-
- private function findInIdx( string $indexFile, string $binarySha ): int {
- $handle = $this->getHandle( $indexFile );
- $result = 0;
-
- if( $handle ) {
- if( !isset( $this->fanoutCache[$indexFile] ) ) {
- fseek( $handle, 0 );
- $head = fread( $handle, 8 );
-
- if( $head === "\377tOc\0\0\0\2" ) {
- $this->fanoutCache[$indexFile] = array_values(
- unpack( 'N*', fread( $handle, 1024 ) )
- );
- }
- }
-
- if( isset( $this->fanoutCache[$indexFile] ) ) {
- $fanout = $this->fanoutCache[$indexFile];
- $byte = ord( $binarySha[0] );
- $start = $byte === 0 ? 0 : $fanout[$byte - 1];
- $end = $fanout[$byte];
-
- if( $end > $start ) {
- $result = $this->binarySearchIdx(
- $indexFile,
- $handle,
- $start,
- $end,
- $binarySha,
- $fanout[255]
- );
- }
- }
- }
-
- return $result;
- }
-
- private function binarySearchIdx(
- string $indexFile,
- $handle,
- int $start,
- int $end,
- string $binarySha,
- int $total
- ): int {
- $key = "$indexFile:$start";
- $count = $end - $start;
- $result = 0;
-
- if( !isset( $this->shaBucketCache[$key] ) ) {
- fseek( $handle, 1032 + ($start * 20) );
- $this->shaBucketCache[$key] = fread( $handle, $count * 20 );
-
- fseek( $handle, 1032 + ($total * 24) + ($start * 4) );
- $this->offsetBucketCache[$key] = fread( $handle, $count * 4 );
- }
-
- $shaBlock = $this->shaBucketCache[$key];
- $low = 0;
- $high = $count - 1;
- $found = -1;
-
- while( $found === -1 && $low <= $high ) {
- $mid = ($low + $high) >> 1;
- $cmp = substr( $shaBlock, $mid * 20, 20 );
-
- if( $cmp < $binarySha ) {
- $low = $mid + 1;
- } elseif( $cmp > $binarySha ) {
- $high = $mid - 1;
- } else {
- $found = $mid;
- }
- }
-
- if( $found !== -1 ) {
- $packed = substr( $this->offsetBucketCache[$key], $found * 4, 4 );
- $offset = unpack( 'N', $packed )[1];
-
- if( $offset & 0x80000000 ) {
- $pos64 = 1032 + ($total * 28) + (($offset & 0x7FFFFFFF) * 8);
-
- fseek( $handle, $pos64 );
- $offset = unpack( 'J', fread( $handle, 8 ) )[1];
- }
-
- $result = (int)$offset;
- }
-
- return $result;
- }
-
- private function readPackEntry(
- $handle,
- int $offset,
- int $size,
- int $cap = 0
- ): string {
- fseek( $handle, $offset );
- $header = $this->readVarInt( $handle );
- $type = ($header['byte'] >> 4) & 7;
- $result = '';
-
- if( $type === 6 ) {
- $result = $this->handleOfsDelta( $handle, $offset, $size, $cap );
- } elseif( $type === 7 ) {
- $result = $this->handleRefDelta( $handle, $size, $cap );
- } else {
- $result = $this->decompressToString( $handle, $cap );
- }
-
- return $result;
- }
-
- private function streamPackEntry(
- $handle,
- int $offset,
- int $size,
- callable $callback,
- int $depth = 0
- ): bool {
- fseek( $handle, $offset );
- $header = $this->readVarInt( $handle );
- $type = ($header['byte'] >> 4) & 7;
- $result = false;
-
- if( $type === 6 || $type === 7 ) {
- $result = $this->streamDeltaObject(
- $handle,
- $offset,
- $type,
- $callback,
- $depth
- );
- } else {
- $result = $this->streamDecompression( $handle, $callback );
- }
-
- return $result;
- }
-
- private function streamDeltaObject(
- $handle,
- int $offset,
- int $type,
- callable $callback,
- int $depth = 0
- ): bool {
- $result = false;
-
- if( $depth < self::MAX_DEPTH ) {
- fseek( $handle, $offset );
- $this->readVarInt( $handle );
-
- if( $type === 6 ) {
- $neg = $this->readOffsetDelta( $handle );
- $deltaPos = ftell( $handle );
- $base = '';
- $baseSize = $this->extractPackedSize( $handle, $offset - $neg );
-
- if( $baseSize <= self::MAX_BASE_RAM ) {
- $this->streamPackEntry(
- $handle,
- $offset - $neg,
- 0,
- function( $c ) use ( &$base ) {
- $base .= $c;
- },
- $depth + 1
- );
-
- fseek( $handle, $deltaPos );
- $result = $this->applyDeltaStream( $handle, $base, $callback );
- } else {
- error_log(
- "[GitPacks] ofs base too large for RAM path: $baseSize"
- );
- }
- } else {
- $baseSha = bin2hex( fread( $handle, 20 ) );
- $baseSize = $this->getSize( $baseSha );
-
- if( $baseSize <= self::MAX_BASE_RAM ) {
- $base = '';
-
- if( $this->streamInternal(
- $baseSha,
- function( $c ) use ( &$base ) {
- $base .= $c;
- },
- $depth + 1
- ) ) {
- $result = $this->applyDeltaStream( $handle, $base, $callback );
- }
- } else {
- error_log(
- "[GitPacks] ref base too large: $baseSize (sha=$baseSha)"
- );
- }
- }
- } else {
- error_log( "[GitPacks] delta depth limit exceeded at offset $offset" );
- }
-
- return $result;
- }
-
- private function applyDeltaStream(
- $handle,
- string $base,
- callable $callback
- ): bool {
- $stream = CompressionStream::createInflater();
- $state = 0;
- $buffer = '';
- $done = false;
-
- while( !$done && !feof( $handle ) ) {
- $chunk = fread( $handle, 8192 );
- $done = $chunk === false || $chunk === '';
-
- if( !$done ) {
- $data = $stream->pump( $chunk );
-
- if( $data !== '' ) {
- $buffer .= $data;
- $doneBuffer = false;
-
- while( !$doneBuffer ) {
- $len = strlen( $buffer );
-
- if( $len === 0 ) {
- $doneBuffer = true;
- }
-
- if( !$doneBuffer ) {
- if( $state < 2 ) {
- $pos = 0;
-
- while( $pos < $len && (ord( $buffer[$pos] ) & 128) ) {
- $pos++;
- }
-
- if( $pos === $len && (ord( $buffer[$pos - 1] ) & 128) ) {
- $doneBuffer = true;
- }
-
- if( !$doneBuffer ) {
- $buffer = substr( $buffer, $pos + 1 );
- $state++;
- }
- } else {
- $op = ord( $buffer[0] );
-
- if( $op & 128 ) {
- $need = $this->getCopyInstructionSize( $op );
-
- if( $len < 1 + $need ) {
- $doneBuffer = true;
- }
-
- if( !$doneBuffer ) {
- $info = $this->parseCopyInstruction( $op, $buffer, 1 );
-
- $callback( substr( $base, $info['off'], $info['len'] ) );
- $buffer = substr( $buffer, 1 + $need );
- }
- } else {
- $ln = $op & 127;
-
- if( $len < 1 + $ln ) {
- $doneBuffer = true;
- }
-
- if( !$doneBuffer ) {
- $callback( substr( $buffer, 1, $ln ) );
- $buffer = substr( $buffer, 1 + $ln );
- }
- }
- }
- }
- }
- }
-
- $done = $stream->finished();
- }
- }
-
- return true;
- }
-
- private function streamPackEntryGenerator(
- $handle,
- int $offset,
- int $depth
- ): Generator {
- fseek( $handle, $offset );
- $header = $this->readVarInt( $handle );
- $type = ($header['byte'] >> 4) & 7;
-
- if( $type === 6 || $type === 7 ) {
- yield from $this->streamDeltaObjectGenerator(
- $handle,
- $offset,
- $type,
- $depth
- );
- } else {
- yield from $this->streamDecompressionGenerator( $handle );
- }
- }
-
- private function resolveBaseToTempFile(
- $packHandle,
- int $baseOffset,
- int $depth
- ) {
- $tmpHandle = tmpfile();
-
- if( $tmpHandle ) {
- foreach( $this->streamPackEntryGenerator(
- $packHandle,
- $baseOffset,
- $depth + 1
- ) as $chunk ) {
- fwrite( $tmpHandle, $chunk );
- }
-
- rewind( $tmpHandle );
- } else {
- error_log(
- "[GitPacks] tmpfile failed for ofs-delta base at $baseOffset"
- );
- }
-
- return $tmpHandle;
- }
-
- private function streamDeltaObjectGenerator(
- $handle,
- int $offset,
- int $type,
- int $depth
- ): Generator {
- if( $depth < self::MAX_DEPTH ) {
- fseek( $handle, $offset );
- $this->readVarInt( $handle );
-
- if( $type === 6 ) {
- $neg = $this->readOffsetDelta( $handle );
- $deltaPos = ftell( $handle );
- $baseSize = $this->extractPackedSize( $handle, $offset - $neg );
-
- if( $baseSize > self::MAX_BASE_RAM ) {
- $tmpHandle = $this->resolveBaseToTempFile(
- $handle,
- $offset - $neg,
- $depth
- );
-
- if( $tmpHandle !== null ) {
- fseek( $handle, $deltaPos );
- yield from $this->applyDeltaStreamFromFileGenerator(
- $handle,
- $tmpHandle
- );
-
- fclose( $tmpHandle );
- }
- } else {
- $base = '';
-
- $this->streamPackEntry(
- $handle,
- $offset - $neg,
- 0,
- function( $c ) use ( &$base ) {
- $base .= $c;
- },
- $depth + 1
- );
-
- fseek( $handle, $deltaPos );
- yield from $this->applyDeltaStreamGenerator( $handle, $base );
- }
- } else {
- $baseSha = bin2hex( fread( $handle, 20 ) );
- $baseSize = $this->getSize( $baseSha );
-
- if( $baseSize > self::MAX_BASE_RAM ) {
- $tmpHandle = tmpfile();
-
- if( $tmpHandle ) {
- $written = $this->streamInternal(
- $baseSha,
- function( $c ) use ( $tmpHandle ) {
- fwrite( $tmpHandle, $c );
- },
- $depth + 1
- );
-
- if( $written ) {
- rewind( $tmpHandle );
- yield from $this->applyDeltaStreamFromFileGenerator(
- $handle,
- $tmpHandle
- );
- }
-
- fclose( $tmpHandle );
- } else {
- error_log(
- "[GitPacks] tmpfile() failed for ref-delta (sha=$baseSha)"
- );
- }
- } else {
- $base = '';
-
- if( $this->streamInternal(
- $baseSha,
- function( $c ) use ( &$base ) {
- $base .= $c;
- },
- $depth + 1
- ) ) {
- yield from $this->applyDeltaStreamGenerator( $handle, $base );
- }
- }
- }
- } else {
- error_log( "[GitPacks] delta depth limit exceeded at offset $offset" );
- }
- }
-
- private function applyDeltaStreamGenerator(
- $handle,
- string $base
- ): Generator {
- $stream = CompressionStream::createInflater();
- $state = 0;
- $buffer = '';
- $done = false;
-
- while( !$done && !feof( $handle ) ) {
- $chunk = fread( $handle, 8192 );
- $done = $chunk === false || $chunk === '';
-
- if( !$done ) {
- $data = $stream->pump( $chunk );
-
- if( $data !== '' ) {
- $buffer .= $data;
- $doneBuffer = false;
-
- while( !$doneBuffer ) {
- $len = strlen( $buffer );
-
- if( $len === 0 ) {
- $doneBuffer = true;
- }
-
- if( !$doneBuffer ) {
- if( $state < 2 ) {
- $pos = 0;
-
- while( $pos < $len && (ord( $buffer[$pos] ) & 128) ) {
- $pos++;
- }
-
- if( $pos === $len && (ord( $buffer[$pos - 1] ) & 128) ) {
- $doneBuffer = true;
- }
-
- if( !$doneBuffer ) {
- $buffer = substr( $buffer, $pos + 1 );
- $state++;
- }
- } else {
- $op = ord( $buffer[0] );
-
- if( $op & 128 ) {
- $need = $this->getCopyInstructionSize( $op );
-
- if( $len < 1 + $need ) {
- $doneBuffer = true;
- }
-
- if( !$doneBuffer ) {
- $info = $this->parseCopyInstruction( $op, $buffer, 1 );
-
- yield substr( $base, $info['off'], $info['len'] );
- $buffer = substr( $buffer, 1 + $need );
- }
- } else {
- $ln = $op & 127;
-
- if( $len < 1 + $ln ) {
- $doneBuffer = true;
- }
-
- if( !$doneBuffer ) {
- yield substr( $buffer, 1, $ln );
- $buffer = substr( $buffer, 1 + $ln );
- }
- }
- }
- }
- }
- }
-
- $done = $stream->finished();
- }
- }
- }
-
- private function applyDeltaStreamFromFileGenerator(
- $deltaHandle,
- $baseHandle
- ): Generator {
- $stream = CompressionStream::createInflater();
- $state = 0;
- $buffer = '';
- $done = false;
-
- while( !$done && !feof( $deltaHandle ) ) {
- $chunk = fread( $deltaHandle, 8192 );
- $done = $chunk === false || $chunk === '';
-
- if( !$done ) {
- $data = $stream->pump( $chunk );
-
- if( $data !== '' ) {
- $buffer .= $data;
- $doneBuffer = false;
-
- while( !$doneBuffer ) {
- $len = strlen( $buffer );
-
- if( $len === 0 ) {
- $doneBuffer = true;
- }
-
- if( !$doneBuffer ) {
- if( $state < 2 ) {
- $pos = 0;
-
- while( $pos < $len && (ord( $buffer[$pos] ) & 128) ) {
- $pos++;
- }
-
- if( $pos === $len && (ord( $buffer[$pos - 1] ) & 128) ) {
- $doneBuffer = true;
- }
-
- if( !$doneBuffer ) {
- $buffer = substr( $buffer, $pos + 1 );
- $state++;
- }
- } else {
- $op = ord( $buffer[0] );
-
- if( $op & 128 ) {
- $need = $this->getCopyInstructionSize( $op );
-
- if( $len < 1 + $need ) {
- $doneBuffer = true;
- }
-
- if( !$doneBuffer ) {
- $info = $this->parseCopyInstruction( $op, $buffer, 1 );
-
- fseek( $baseHandle, $info['off'] );
- $remaining = $info['len'];
-
- while( $remaining > 0 ) {
- $slice = fread( $baseHandle, min( 65536, $remaining ) );
-
- if( $slice === false || $slice === '' ) {
- $remaining = 0;
- } else {
- yield $slice;
- $remaining -= strlen( $slice );
- }
- }
-
- $buffer = substr( $buffer, 1 + $need );
- }
- } else {
- $ln = $op & 127;
-
- if( $len < 1 + $ln ) {
- $doneBuffer = true;
- }
-
- if( !$doneBuffer ) {
- yield substr( $buffer, 1, $ln );
- $buffer = substr( $buffer, 1 + $ln );
- }
- }
- }
- }
- }
- }
-
- $done = $stream->finished();
- }
- }
- }
-
- private function streamDecompressionGenerator( $handle ): Generator {
- $stream = CompressionStream::createInflater();
- $done = false;
-
- while( !$done && !feof( $handle ) ) {
- $chunk = fread( $handle, 8192 );
- $done = $chunk === false || $chunk === '';
-
- if( !$done ) {
- $data = $stream->pump( $chunk );
-
- if( $data !== '' ) {
- yield $data;
- }
-
- $done = $stream->finished();
- }
- }
- }
-
- private function streamDecompression( $handle, callable $callback ): bool {
- $stream = CompressionStream::createInflater();
- $done = false;
-
- while( !$done && !feof( $handle ) ) {
- $chunk = fread( $handle, 8192 );
- $done = $chunk === false || $chunk === '';
-
- if( !$done ) {
- $data = $stream->pump( $chunk );
-
- if( $data !== '' ) {
- $callback( $data );
- }
-
- $done = $stream->finished();
- }
- }
-
- return true;
- }
-
- private function decompressToString(
- $handle,
- int $cap = 0
- ): string {
- $stream = CompressionStream::createInflater();
- $res = '';
- $done = false;
-
- while( !$done && !feof( $handle ) ) {
- $chunk = fread( $handle, 8192 );
- $done = $chunk === false || $chunk === '';
-
- if( !$done ) {
- $data = $stream->pump( $chunk );
-
- if( $data !== '' ) {
- $res .= $data;
- }
-
- if( $cap > 0 && strlen( $res ) >= $cap ) {
- $res = substr( $res, 0, $cap );
- $done = true;
- }
-
- if( !$done ) {
- $done = $stream->finished();
- }
- }
- }
-
- return $res;
- }
-
- private function extractPackedSize( $packPathOrHandle, int $offset ): int {
- $handle = is_resource( $packPathOrHandle )
- ? $packPathOrHandle
- : $this->getHandle( $packPathOrHandle );
- $size = 0;
-
- if( $handle ) {
- fseek( $handle, $offset );
- $header = $this->readVarInt( $handle );
- $size = $header['value'];
- $type = ($header['byte'] >> 4) & 7;
-
- if( $type === 6 || $type === 7 ) {
- $size = $this->readDeltaTargetSize( $handle, $type );
- }
- }
-
- return $size;
- }
-
- private function handleOfsDelta(
- $handle,
- int $offset,
- int $size,
- int $cap
- ): string {
- $neg = $this->readOffsetDelta( $handle );
- $cur = ftell( $handle );
- $base = $offset - $neg;
-
- fseek( $handle, $base );
- $bHead = $this->readVarInt( $handle );
-
- fseek( $handle, $base );
- $bData = $this->readPackEntry( $handle, $base, $bHead['value'], $cap );
-
- fseek( $handle, $cur );
- $rem = min( self::MAX_READ, max( $size * 2, 1048576 ) );
- $comp = fread( $handle, $rem );
- $delta = @gzuncompress( $comp ) ?: '';
-
- return $this->applyDelta( $bData, $delta, $cap );
- }
-
- private function handleRefDelta( $handle, int $size, int $cap ): string {
- $sha = bin2hex( fread( $handle, 20 ) );
- $bas = $cap > 0 ? $this->peek( $sha, $cap ) : $this->read( $sha );
- $rem = min( self::MAX_READ, max( $size * 2, 1048576 ) );
- $cmp = fread( $handle, $rem );
- $del = @gzuncompress( $cmp ) ?: '';
-
- return $this->applyDelta( $bas, $del, $cap );
- }
-
- private function applyDelta( string $base, string $delta, int $cap ): string {
- $pos = 0;
- $res = $this->readDeltaSize( $delta, $pos );
- $pos += $res['used'];
- $res = $this->readDeltaSize( $delta, $pos );
- $pos += $res['used'];
-
- $out = '';
- $len = strlen( $delta );
- $done = false;
-
- while( !$done && $pos < $len ) {
- if( $cap > 0 && strlen( $out ) >= $cap ) {
- $done = true;
- }
-
- if( !$done ) {
- $op = ord( $delta[$pos++] );
-
- if( $op & 128 ) {
- $info = $this->parseCopyInstruction( $op, $delta, $pos );
- $out .= substr( $base, $info['off'], $info['len'] );
- $pos += $info['used'];
- } else {
- $ln = $op & 127;
- $out .= substr( $delta, $pos, $ln );
- $pos += $ln;
- }
- }
- }
-
- return $out;
- }
-
- private function parseCopyInstruction(
- int $op,
- string $data,
- int $pos
- ): array {
- $off = 0;
- $len = 0;
- $ptr = $pos;
-
- if( $op & 0x01 ) {
- $off |= ord( $data[$ptr++] );
- }
-
- if( $op & 0x02 ) {
- $off |= ord( $data[$ptr++] ) << 8;
- }
-
- if( $op & 0x04 ) {
- $off |= ord( $data[$ptr++] ) << 16;
- }
-
- if( $op & 0x08 ) {
- $off |= ord( $data[$ptr++] ) << 24;
- }
-
- if( $op & 0x10 ) {
- $len |= ord( $data[$ptr++] );
- }
-
- if( $op & 0x20 ) {
- $len |= ord( $data[$ptr++] ) << 8;
- }
-
- if( $op & 0x40 ) {
- $len |= ord( $data[$ptr++] ) << 16;
- }
-
- return [
- 'off' => $off,
- 'len' => $len === 0 ? 0x10000 : $len,
- 'used' => $ptr - $pos
- ];
- }
-
- private function getCopyInstructionSize( int $op ): int {
- $c = $op & 0x7F;
- $c = $c - (( $c >> 1 ) & 0x55);
- $c = (( $c >> 2 ) & 0x33) + ( $c & 0x33 );
- $c = (( $c >> 4 ) + $c) & 0x0F;
+ $result = false;
+
+ foreach( $this->streamGenerator( $sha ) as $chunk ) {
+ $callback( $chunk );
+ $result = true;
+ }
+
+ return $result;
+ }
+
+ public function streamGenerator( string $sha ): Generator {
+ yield from $this->streamShaGenerator( $sha, 0 );
+ }
+
+ private function streamShaGenerator( string $sha, int $depth ): Generator {
+ $info = $this->findPackInfo( $sha );
+
+ if( $info['offset'] !== 0 ) {
+ $handle = $this->getHandle( $info['file'] );
+
+ if( $handle ) {
+ yield from $this->streamPackEntryGenerator(
+ $handle,
+ $info['offset'],
+ $depth
+ );
+ }
+ }
+ }
+
+ public function getSize( string $sha ): int {
+ $info = $this->findPackInfo( $sha );
+ $result = 0;
+
+ if( $info['offset'] !== 0 ) {
+ $result = $this->extractPackedSize( $info['file'], $info['offset'] );
+ }
+
+ return $result;
+ }
+
+ private function findPackInfo( string $sha ): array {
+ $result = [ 'offset' => 0, 'file' => '' ];
+
+ if( strlen( $sha ) === 40 && ctype_xdigit( $sha ) ) {
+ $binarySha = hex2bin( $sha );
+
+ if( $this->lastPack !== '' ) {
+ $offset = $this->findInIdx( $this->lastPack, $binarySha );
+
+ if( $offset !== 0 ) {
+ $result = [
+ 'file' => str_replace( '.idx', '.pack', $this->lastPack ),
+ 'offset' => $offset
+ ];
+ }
+ }
+
+ if( $result['offset'] === 0 ) {
+ $count = count( $this->packFiles );
+ $idx = 0;
+ $found = false;
+
+ while( !$found && $idx < $count ) {
+ $indexFile = $this->packFiles[$idx];
+
+ if( $indexFile !== $this->lastPack ) {
+ $offset = $this->findInIdx( $indexFile, $binarySha );
+
+ if( $offset !== 0 ) {
+ $this->lastPack = $indexFile;
+ $result = [
+ 'file' => str_replace( '.idx', '.pack', $indexFile ),
+ 'offset' => $offset
+ ];
+ $found = true;
+ }
+ }
+
+ $idx++;
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ private function findInIdx( string $indexFile, string $binarySha ): int {
+ $handle = $this->getHandle( $indexFile );
+ $result = 0;
+
+ if( $handle ) {
+ if( !isset( $this->fanoutCache[$indexFile] ) ) {
+ fseek( $handle, 0 );
+ $head = fread( $handle, 8 );
+
+ if( $head === "\377tOc\0\0\0\2" ) {
+ $this->fanoutCache[$indexFile] = array_values(
+ unpack( 'N*', fread( $handle, 1024 ) )
+ );
+ }
+ }
+
+ if( isset( $this->fanoutCache[$indexFile] ) ) {
+ $fanout = $this->fanoutCache[$indexFile];
+ $byte = ord( $binarySha[0] );
+ $start = $byte === 0 ? 0 : $fanout[$byte - 1];
+ $end = $fanout[$byte];
+
+ if( $end > $start ) {
+ $result = $this->binarySearchIdx(
+ $indexFile,
+ $handle,
+ $start,
+ $end,
+ $binarySha,
+ $fanout[255]
+ );
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ private function binarySearchIdx(
+ string $indexFile,
+ $handle,
+ int $start,
+ int $end,
+ string $binarySha,
+ int $total
+ ): int {
+ $key = "$indexFile:$start";
+ $count = $end - $start;
+ $result = 0;
+
+ if( !isset( $this->shaBucketCache[$key] ) ) {
+ fseek( $handle, 1032 + ($start * 20) );
+ $this->shaBucketCache[$key] = fread( $handle, $count * 20 );
+
+ fseek( $handle, 1032 + ($total * 24) + ($start * 4) );
+ $this->offsetBucketCache[$key] = fread( $handle, $count * 4 );
+ }
+
+ $shaBlock = $this->shaBucketCache[$key];
+ $low = 0;
+ $high = $count - 1;
+ $found = -1;
+
+ while( $found === -1 && $low <= $high ) {
+ $mid = ($low + $high) >> 1;
+ $cmp = substr( $shaBlock, $mid * 20, 20 );
+
+ if( $cmp < $binarySha ) {
+ $low = $mid + 1;
+ } elseif( $cmp > $binarySha ) {
+ $high = $mid - 1;
+ } else {
+ $found = $mid;
+ }
+ }
+
+ if( $found !== -1 ) {
+ $packed = substr( $this->offsetBucketCache[$key], $found * 4, 4 );
+ $offset = unpack( 'N', $packed )[1];
+
+ if( $offset & 0x80000000 ) {
+ $pos64 = 1032 + ($total * 28) + (($offset & 0x7FFFFFFF) * 8);
+
+ fseek( $handle, $pos64 );
+ $offset = unpack( 'J', fread( $handle, 8 ) )[1];
+ }
+
+ $result = (int)$offset;
+ }
+
+ return $result;
+ }
+
+ private function readPackEntry(
+ $handle,
+ int $offset,
+ int $size,
+ int $cap = 0
+ ): string {
+ fseek( $handle, $offset );
+ $header = $this->readVarInt( $handle );
+ $type = ($header['byte'] >> 4) & 7;
+ $result = '';
+
+ if( $type === 6 ) {
+ $result = $this->handleOfsDelta( $handle, $offset, $size, $cap );
+ } elseif( $type === 7 ) {
+ $result = $this->handleRefDelta( $handle, $size, $cap );
+ } else {
+ $result = $this->decompressToString( $handle, $cap );
+ }
+
+ return $result;
+ }
+
+ private function streamPackEntryGenerator(
+ $handle,
+ int $offset,
+ int $depth
+ ): Generator {
+ fseek( $handle, $offset );
+ $header = $this->readVarInt( $handle );
+ $type = ($header['byte'] >> 4) & 7;
+
+ if( $type === 6 || $type === 7 ) {
+ yield from $this->streamDeltaObjectGenerator(
+ $handle,
+ $offset,
+ $type,
+ $depth
+ );
+ } else {
+ yield from $this->streamDecompressionGenerator( $handle );
+ }
+ }
+
+ private function resolveBaseToTempFile(
+ $packHandle,
+ int $baseOffset,
+ int $depth
+ ) {
+ $tmpHandle = tmpfile();
+
+ if( $tmpHandle !== false ) {
+ foreach( $this->streamPackEntryGenerator(
+ $packHandle,
+ $baseOffset,
+ $depth + 1
+ ) as $chunk ) {
+ fwrite( $tmpHandle, $chunk );
+ }
+
+ rewind( $tmpHandle );
+ } else {
+ error_log(
+ "[GitPacks] tmpfile failed for ofs-delta base at $baseOffset"
+ );
+ }
+
+ return $tmpHandle;
+ }
+
+ private function streamDeltaObjectGenerator(
+ $handle,
+ int $offset,
+ int $type,
+ int $depth
+ ): Generator {
+ if( $depth < self::MAX_DEPTH ) {
+ fseek( $handle, $offset );
+ $this->readVarInt( $handle );
+
+ if( $type === 6 ) {
+ $neg = $this->readOffsetDelta( $handle );
+ $deltaPos = ftell( $handle );
+ $baseSize = $this->extractPackedSize( $handle, $offset - $neg );
+
+ if( $baseSize > self::MAX_BASE_RAM ) {
+ $tmpHandle = $this->resolveBaseToTempFile(
+ $handle,
+ $offset - $neg,
+ $depth
+ );
+
+ if( $tmpHandle !== false ) {
+ fseek( $handle, $deltaPos );
+ yield from $this->applyDeltaStreamGenerator(
+ $handle,
+ $tmpHandle
+ );
+
+ fclose( $tmpHandle );
+ }
+ } else {
+ $base = '';
+
+ foreach( $this->streamPackEntryGenerator(
+ $handle,
+ $offset - $neg,
+ $depth + 1
+ ) as $chunk ) {
+ $base .= $chunk;
+ }
+
+ fseek( $handle, $deltaPos );
+ yield from $this->applyDeltaStreamGenerator( $handle, $base );
+ }
+ } else {
+ $baseSha = bin2hex( fread( $handle, 20 ) );
+ $baseSize = $this->getSize( $baseSha );
+
+ if( $baseSize > self::MAX_BASE_RAM ) {
+ $tmpHandle = tmpfile();
+
+ if( $tmpHandle !== false ) {
+ $written = false;
+
+ foreach( $this->streamShaGenerator(
+ $baseSha,
+ $depth + 1
+ ) as $chunk ) {
+ fwrite( $tmpHandle, $chunk );
+ $written = true;
+ }
+
+ if( $written ) {
+ rewind( $tmpHandle );
+ yield from $this->applyDeltaStreamGenerator(
+ $handle,
+ $tmpHandle
+ );
+ }
+
+ fclose( $tmpHandle );
+ } else {
+ error_log(
+ "[GitPacks] tmpfile() failed for ref-delta (sha=$baseSha)"
+ );
+ }
+ } else {
+ $base = '';
+ $written = false;
+
+ foreach( $this->streamShaGenerator(
+ $baseSha,
+ $depth + 1
+ ) as $chunk ) {
+ $base .= $chunk;
+ $written = true;
+ }
+
+ if( $written ) {
+ yield from $this->applyDeltaStreamGenerator( $handle, $base );
+ }
+ }
+ }
+ } else {
+ error_log( "[GitPacks] delta depth limit exceeded at offset $offset" );
+ }
+ }
+
+ private function applyDeltaStreamGenerator(
+ $handle,
+ $base
+ ): Generator {
+ $stream = CompressionStream::createInflater();
+ $state = 0;
+ $buffer = '';
+ $done = false;
+ $isFile = is_resource( $base );
+
+ while( !$done && !feof( $handle ) ) {
+ $chunk = fread( $handle, 8192 );
+ $done = $chunk === false || $chunk === '';
+
+ if( !$done ) {
+ $data = $stream->pump( $chunk );
+
+ if( $data !== '' ) {
+ $buffer .= $data;
+ $doneBuffer = false;
+
+ while( !$doneBuffer ) {
+ $len = strlen( $buffer );
+
+ if( $len === 0 ) {
+ $doneBuffer = true;
+ }
+
+ if( !$doneBuffer ) {
+ if( $state < 2 ) {
+ $pos = 0;
+
+ while( $pos < $len && (ord( $buffer[$pos] ) & 128) ) {
+ $pos++;
+ }
+
+ if( $pos === $len && (ord( $buffer[$pos - 1] ) & 128) ) {
+ $doneBuffer = true;
+ }
+
+ if( !$doneBuffer ) {
+ $buffer = substr( $buffer, $pos + 1 );
+ $state++;
+ }
+ } else {
+ $op = ord( $buffer[0] );
+
+ if( $op & 128 ) {
+ $need = $this->getCopyInstructionSize( $op );
+
+ if( $len < 1 + $need ) {
+ $doneBuffer = true;
+ }
+
+ if( !$doneBuffer ) {
+ $info = $this->parseCopyInstruction( $op, $buffer, 1 );
+
+ if( $isFile ) {
+ fseek( $base, $info['off'] );
+ $rem = $info['len'];
+
+ while( $rem > 0 ) {
+ $slc = fread( $base, min( 65536, $rem ) );
+
+ if( $slc === false || $slc === '' ) {
+ $rem = 0;
+ } else {
+ yield $slc;
+ $rem -= strlen( $slc );
+ }
+ }
+ } else {
+ yield substr( $base, $info['off'], $info['len'] );
+ }
+
+ $buffer = substr( $buffer, 1 + $need );
+ }
+ } else {
+ $ln = $op & 127;
+
+ if( $len < 1 + $ln ) {
+ $doneBuffer = true;
+ }
+
+ if( !$doneBuffer ) {
+ yield substr( $buffer, 1, $ln );
+ $buffer = substr( $buffer, 1 + $ln );
+ }
+ }
+ }
+ }
+ }
+ }
+
+ $done = $stream->finished();
+ }
+ }
+ }
+
+ private function streamDecompressionGenerator( $handle ): Generator {
+ $stream = CompressionStream::createInflater();
+ $done = false;
+
+ while( !$done && !feof( $handle ) ) {
+ $chunk = fread( $handle, 8192 );
+ $done = $chunk === false || $chunk === '';
+
+ if( !$done ) {
+ $data = $stream->pump( $chunk );
+
+ if( $data !== '' ) {
+ yield $data;
+ }
+
+ $done = $stream->finished();
+ }
+ }
+ }
+
+ private function decompressToString(
+ $handle,
+ int $cap = 0
+ ): string {
+ $stream = CompressionStream::createInflater();
+ $res = '';
+ $done = false;
+
+ while( !$done && !feof( $handle ) ) {
+ $chunk = fread( $handle, 8192 );
+ $done = $chunk === false || $chunk === '';
+
+ if( !$done ) {
+ $data = $stream->pump( $chunk );
+
+ if( $data !== '' ) {
+ $res .= $data;
+ }
+
+ if( $cap > 0 && strlen( $res ) >= $cap ) {
+ $res = substr( $res, 0, $cap );
+ $done = true;
+ }
+
+ if( !$done ) {
+ $done = $stream->finished();
+ }
+ }
+ }
+
+ return $res;
+ }
+
+ private function extractPackedSize( $packPathOrHandle, int $offset ): int {
+ $handle = is_resource( $packPathOrHandle )
+ ? $packPathOrHandle
+ : $this->getHandle( $packPathOrHandle );
+ $size = 0;
+
+ if( $handle ) {
+ fseek( $handle, $offset );
+ $header = $this->readVarInt( $handle );
+ $size = $header['value'];
+ $type = ($header['byte'] >> 4) & 7;
+
+ if( $type === 6 || $type === 7 ) {
+ $size = $this->readDeltaTargetSize( $handle, $type );
+ }
+ }
+
+ return $size;
+ }
+
+ private function handleOfsDelta(
+ $handle,
+ int $offset,
+ int $size,
+ int $cap
+ ): string {
+ $neg = $this->readOffsetDelta( $handle );
+ $cur = ftell( $handle );
+ $base = $offset - $neg;
+
+ fseek( $handle, $base );
+ $bHead = $this->readVarInt( $handle );
+
+ fseek( $handle, $base );
+ $bData = $this->readPackEntry( $handle, $base, $bHead['value'], $cap );
+
+ fseek( $handle, $cur );
+ $rem = min( self::MAX_READ, max( $size * 2, 1048576 ) );
+ $comp = fread( $handle, $rem );
+ $delta = @gzuncompress( $comp ) ?: '';
+
+ return $this->applyDelta( $bData, $delta, $cap );
+ }
+
+ private function handleRefDelta( $handle, int $size, int $cap ): string {
+ $sha = bin2hex( fread( $handle, 20 ) );
+ $bas = $cap > 0 ? $this->peek( $sha, $cap ) : $this->read( $sha );
+ $rem = min( self::MAX_READ, max( $size * 2, 1048576 ) );
+ $cmp = fread( $handle, $rem );
+ $del = @gzuncompress( $cmp ) ?: '';
+
+ return $this->applyDelta( $bas, $del, $cap );
+ }
+
+ private function applyDelta( string $base, string $delta, int $cap ): string {
+ $pos = 0;
+ $res = $this->readDeltaSize( $delta, $pos );
+ $pos += $res['used'];
+ $res = $this->readDeltaSize( $delta, $pos );
+ $pos += $res['used'];
+
+ $out = '';
+ $len = strlen( $delta );
+ $done = false;
+
+ while( !$done && $pos < $len ) {
+ if( $cap > 0 && strlen( $out ) >= $cap ) {
+ $done = true;
+ }
+
+ if( !$done ) {
+ $op = ord( $delta[$pos++] );
+
+ if( $op & 128 ) {
+ $info = $this->parseCopyInstruction( $op, $delta, $pos );
+ $out .= substr( $base, $info['off'], $info['len'] );
+ $pos += $info['used'];
+ } else {
+ $ln = $op & 127;
+ $out .= substr( $delta, $pos, $ln );
+ $pos += $ln;
+ }
+ }
+ }
+
+ return $out;
+ }
+
+ private function parseCopyInstruction(
+ int $op,
+ string $data,
+ int $pos
+ ): array {
+ $off = 0;
+ $len = 0;
+ $ptr = $pos;
+
+ if( $op & 0x01 ) {
+ $off |= ord( $data[$ptr++] );
+ }
+
+ if( $op & 0x02 ) {
+ $off |= ord( $data[$ptr++] ) << 8;
+ }
+
+ if( $op & 0x04 ) {
+ $off |= ord( $data[$ptr++] ) << 16;
+ }
+
+ if( $op & 0x08 ) {
+ $off |= ord( $data[$ptr++] ) << 24;
+ }
+
+ if( $op & 0x10 ) {
+ $len |= ord( $data[$ptr++] );
+ }
+
+ if( $op & 0x20 ) {
+ $len |= ord( $data[$ptr++] ) << 8;
+ }
+
+ if( $op & 0x40 ) {
+ $len |= ord( $data[$ptr++] ) << 16;
+ }
+
+ return [
+ 'off' => $off,
+ 'len' => $len === 0 ? 0x10000 : $len,
+ 'used' => $ptr - $pos
+ ];
+ }
+
+ private function getCopyInstructionSize( int $op ): int {
+ $c = $op & 0x7F;
+ $c = $c - (($c >> 1) & 0x55);
+ $c = (($c >> 2) & 0x33) + ($c & 0x33);
+ $c = (($c >> 4) + $c) & 0x0F;
return $c;
git/GitRefs.php
<?php
+require_once __DIR__ . '/BufferedFileReader.php';
+
class GitRefs {
private string $repoPath;
$headFile = "{$this->repoPath}/HEAD";
- if( $input === 'HEAD' && file_exists( $headFile ) ) {
- $head = trim( file_get_contents( $headFile ) );
+ if( $input === 'HEAD' && is_file( $headFile ) ) {
+ $size = filesize( $headFile );
+ $head = '';
+
+ if( $size > 0 ) {
+ $reader = BufferedFileReader::open( $headFile );
+ $head = trim( $reader->read( $size ) );
+ }
+
$result = strpos( $head, 'ref: ' ) === 0
? $this->resolve( substr( $head, 5 ) )
$this->traverseDirectory( $path, $callback, $name );
} elseif( is_file( $path ) ) {
- $sha = trim( file_get_contents( $path ) );
+ $size = filesize( $path );
- if( preg_match( '/^[0-9a-f]{40}$/', $sha ) ) {
- $callback( $name, $sha );
+ if( $size > 0 ) {
+ $reader = BufferedFileReader::open( $path );
+ $sha = trim( $reader->read( $size ) );
+
+ if( preg_match( '/^[0-9a-f]{40}$/', $sha ) ) {
+ $callback( $name, $sha );
+ }
}
}
$path = "{$this->repoPath}/$ref";
- if( file_exists( $path ) ) {
- $result = trim( file_get_contents( $path ) );
+ if( is_file( $path ) ) {
+ $size = filesize( $path );
+
+ if( $size > 0 ) {
+ $reader = BufferedFileReader::open( $path );
+ $result = trim( $reader->read( $size ) );
+ }
+
break;
}
}
if( $result === '' ) {
$packedPath = "{$this->repoPath}/packed-refs";
- if( file_exists( $packedPath ) ) {
+ if( is_file( $packedPath ) ) {
$result = $this->findInPackedRefs( $packedPath, $input );
}
private function findInPackedRefs( string $path, string $input ): string {
$targets = [$input, "refs/heads/$input", "refs/tags/$input"];
- $lines = file( $path );
+ $size = filesize( $path );
+ $lines = [];
$result = '';
+
+ if( $size > 0 ) {
+ $reader = BufferedFileReader::open( $path );
+ $lines = explode( "\n", $reader->read( $size ) );
+ }
foreach( $lines as $line ) {
- if( $line[0] !== '#' && $line[0] !== '^' ) {
+ if( $line !== '' && $line[0] !== '#' && $line[0] !== '^' ) {
$parts = explode( ' ', trim( $line ) );
Delta 1411 lines added, 1608 lines removed, 197-line decrease