Dave Jarvis' Repositories

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

Copies raw compressed deltas instead of reconstructed ones

AuthorDave Jarvis <email>
Date2026-02-22 21:41:51 GMT-0800
Commitea52160c9e4f0491d430caf0c3f9d691db3be353
Parent1e414c6
git/Git.php
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 ) {
- $callback(
- $this->parseTagData( $name, $sha, $this->read( $sha ) )
- );
- }
- );
- }
-
- public function walk(
- string $refOrSha,
- callable $callback,
- string $path = ''
- ): void {
- $sha = $this->resolve( $refOrSha );
- $treeSha = $sha !== '' ? $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 ) : [];
-
- return isset( $info['sha'] ) && !$info['isDir'] && $info['sha'] !== ''
- ? new File(
- \basename( $path ),
- $info['sha'],
- $info['mode'],
- 0,
- $this->getObjectSize( $info['sha'] ),
- $this->peek( $info['sha'] )
- )
- : new MissingFile();
- }
-
- 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;
- $done = false;
-
- while( !$done && $sha !== '' && $count < $limit ) {
- $data = $this->read( $sha );
-
- if( $data === '' ) {
- $done = true;
- } else {
- $id = $this->parseIdentity(
- $data, '/^author (.*) <(.*)> (\d+)/m'
- );
- $parentSha = $this->extractPattern(
- $data, '/^parent (.*)$/m', 1
- );
-
- $commit = new Commit(
- $sha,
- $this->extractMessage( $data ),
- $id['name'],
- $id['email'],
- $id['timestamp'],
- $parentSha
- );
-
- if( $callback( $commit ) === false ) {
- $done = true;
- } else {
- $sha = $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 ) {
- \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;
-
- foreach( $this->streamCompressed( $sha ) as $compressed ) {
- \hash_update( $ctx, $compressed );
- yield $compressed;
- }
- }
-
- yield \hash_final( $ctx, true );
- }
-
- private function streamCompressed( string $sha ): Generator {
- $yielded = false;
-
- foreach( $this->packs->streamRawCompressed( $sha ) as $chunk ) {
- $yielded = true;
- yield $chunk;
- }
-
- if( !$yielded ) {
- $deflate = \deflate_init( \ZLIB_ENCODING_DEFLATE );
-
- foreach( $this->slurpChunks( $sha ) as $raw ) {
- $compressed = \deflate_add( $deflate, $raw, \ZLIB_NO_FLUSH );
-
- if( $compressed !== '' ) {
- yield $compressed;
- }
- }
-
- $final = \deflate_add( $deflate, '', \ZLIB_FINISH );
-
- if( $final !== '' ) {
- yield $final;
- }
- }
- }
-
- private function slurpChunks( string $sha ): Generator {
- $path = $this->getLoosePath( $sha );
-
- if( \is_file( $path ) ) {
- foreach( $this->streamInflatedObjects( $path ) as $chunk ) {
- if( $chunk['body'] !== '' ) {
- yield $chunk['body'];
- }
- }
- } 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 streamInflatedObjects(
- string $path,
- int $bufSz = 16384
- ): Generator {
- $reader = new BufferedReader( $path );
- $infl = $reader->isOpen()
- ? \inflate_init( \ZLIB_ENCODING_DEFLATE )
- : false;
-
- if( $reader->isOpen() && $infl !== false ) {
- $found = false;
- $buffer = '';
-
- while( !$reader->eof() ) {
- $chunk = $reader->read( $bufSz );
- $inflated = \inflate_add( $infl, $chunk );
-
- if( $inflated === false ) {
- break;
- }
-
- if( !$found ) {
- $buffer .= $inflated;
- $eos = \strpos( $buffer, "\0" );
-
- if( $eos !== false ) {
- $found = true;
-
- yield [
- 'head' => \substr( $buffer, 0, $eos ),
- 'body' => \substr( $buffer, $eos + 1 )
- ];
- }
- } elseif( $inflated !== '' ) {
- yield [ 'head' => '', 'body' => $inflated ];
- }
- }
- }
- }
-
- 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 = $part !== '' && $sha !== ''
- ? $this->findTreeEntry( $sha, $part )
- : [ 'sha' => '', 'mode' => '' ];
-
- $sha = $entry['sha'];
- $mode = $entry['mode'];
- }
-
- return [
- 'sha' => $sha,
- 'mode' => $mode,
- 'isDir' => $mode === '40000' || $mode === '040000'
- ];
- }
-
- private function findTreeEntry( string $treeSha, string $name ): array {
- $entry = [ 'sha' => '', 'mode' => '' ];
-
- $this->parseTreeData(
- $this->read( $treeSha ),
- function( $n, $s, $m ) use ( $name, &$entry ) {
- if( $n === $name ) {
- $entry = [ 'sha' => $s, 'mode' => $m ];
-
- return false;
- }
- }
- );
-
- return $entry;
- }
-
- private function parseTagData(
- string $name,
- string $sha,
- string $data
- ): Tag {
- $isAnn = \strncmp( $data, 'object ', 7 ) === 0;
- $id = $this->parseIdentity(
- $data,
- $isAnn
- ? '/^tagger (.*) <(.*)> (\d+) [+\-]\d{4}$/m'
- : '/^author (.*) <(.*)> (\d+) [+\-]\d{4}$/m'
- );
-
- return new Tag(
- $name,
- $sha,
- $isAnn ? $this->extractPattern( $data, '/^object (.*)$/m', 1, $sha ) : $sha,
- $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 ) ) {
- foreach( $this->streamInflatedObjects( $path ) as $chunk ) {
- if( $chunk['body'] !== '' ) {
- $callback( $chunk['body'] );
- }
- }
- } elseif( !$this->packs->stream( $sha, $callback ) ) {
- $data = $this->packs->read( $sha );
-
- if( $data !== '' ) {
- $callback( $data );
- }
- }
- }
-
- private function peekLooseObject( string $sha, int $length ): string {
- $path = $this->getLoosePath( $sha );
- $buf = '';
-
- if( \is_file( $path ) ) {
- foreach( $this->streamInflatedObjects( $path, 8192 ) as $chunk ) {
- $buf .= $chunk['body'];
-
- if( \strlen( $buf ) >= $length ) {
- break;
- }
- }
- }
-
- return \substr( $buf, 0, $length );
- }
-
- private function walkTree( string $sha, callable $callback ): void {
- $data = $this->read( $sha );
- $tree = $data !== '' && \preg_match( '/^tree (.*)$/m', $data, $m )
- ? $this->read( $m[1] )
- : $data;
-
- if( $tree !== '' && $this->isTreeData( $tree ) ) {
- $this->parseTreeData(
- $tree,
- function( $n, $s, $m ) use ( $callback ) {
- $dir = $m === '40000' || $m === '040000';
- $isSub = $m === '160000';
-
- $callback( new File(
- $n,
- $s,
- $m,
- 0,
- $dir || $isSub ? 0 : $this->getObjectSize( $s ),
- $dir || $isSub ? '' : $this->peek( $s )
- ) );
- }
- );
- }
- }
-
- public function parseTreeData( string $data, callable $callback ): void {
- $pos = 0;
- $len = \strlen( $data );
-
- while( $pos < $len ) {
- $space = \strpos( $data, ' ', $pos );
- $eos = \strpos( $data, "\0", $space );
-
- if( $space === false || $eos === false || $eos + 21 > $len ) {
- break;
- }
-
- $mode = \substr( $data, $pos, $space - $pos );
- $name = \substr( $data, $space + 1, $eos - $space - 1 );
- $sha = \bin2hex( \substr( $data, $eos + 1, 20 ) );
-
- if( $callback( $name, $sha, $mode ) === false ) {
- break;
- }
-
- $pos = $eos + 21;
- }
- }
-
- private function isTreeData( string $data ): bool {
- $len = \strlen( $data );
- $match = $len >= 25 &&
- \preg_match( '/^(40000|100644|100755|120000|160000) /', $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 ) ) {
- foreach( $this->streamInflatedObjects( $path ) as $chunk ) {
- $parts = \explode( ' ', $chunk['head'] );
- $size = isset( $parts[1] ) ? (int)$parts[1] : 0;
- break;
- }
- }
-
- return $size;
- }
-
- public function collectObjects( array $wants, array $haves = [] ): array {
- $objs = $this->traverseObjects( $wants );
-
- if( !empty( $haves ) ) {
- foreach( $this->traverseObjects( $haves ) as $sha => $type ) {
- unset( $objs[$sha] );
- }
- }
-
- return $objs;
- }
-
- private function traverseObjects( array $roots ): array {
- $objs = [];
- $queue = [];
-
- foreach( $roots as $sha ) {
- $queue[] = [ 'sha' => $sha, 'type' => 0 ];
- }
-
- while( !empty( $queue ) ) {
- $item = \array_pop( $queue );
- $sha = $item['sha'];
- $type = $item['type'];
-
- if( !isset( $objs[$sha] ) ) {
- $data = $type !== 3 ? $this->read( $sha ) : '';
- $type = $type === 0 ? $this->getObjectType( $data ) : $type;
-
- $objs[$sha] = $type;
-
- if( $type === 1 ) {
- if( \preg_match( '/^tree ([0-9a-f]{40})/m', $data, $m ) ) {
- $queue[] = [ 'sha' => $m[1], 'type' => 2 ];
- }
-
- if( \preg_match_all( '/^parent ([0-9a-f]{40})/m', $data, $m ) ) {
- foreach( $m[1] as $parentSha ) {
- $queue[] = [ 'sha' => $parentSha, 'type' => 1 ];
- }
- }
- } elseif( $type === 2 ) {
- $pos = 0;
- $len = \strlen( $data );
-
- while( $pos < $len ) {
- $space = \strpos( $data, ' ', $pos );
- $eos = \strpos( $data, "\0", $space );
-
- if( $space === false || $eos === false ) {
- break;
- }
-
- $mode = \substr( $data, $pos, $space - $pos );
- $hash = \bin2hex( \substr( $data, $eos + 1, 20 ) );
-
- if( $mode !== '160000' ) {
- $queue[] = [
- 'sha' => $hash,
- 'type' => $mode === '40000' || $mode === '040000' ? 2 : 3
- ];
- }
-
- $pos = $eos + 21;
- }
- } elseif( $type === 4 ) {
- if( \preg_match( '/^object ([0-9a-f]{40})/m', $data, $m ) ) {
- $nextType = 1;
-
- if( \preg_match( '/^type (commit|tree|blob|tag)/m', $data, $t ) ) {
- $map = [
- 'commit' => 1,
- 'tree' => 2,
- 'blob' => 3,
- 'tag' => 4
- ];
- $nextType = $map[$t[1]] ?? 1;
- }
-
- $queue[] = [ 'sha' => $m[1], 'type' => $nextType ];
- }
- }
- }
- }
-
- return $objs;
- }
-
- private function getObjectType( string $data ): int {
- $result = 3;
-
- if( \strpos( $data, "tree " ) === 0 ) {
- $result = 1;
- } elseif( \strpos( $data, "object " ) === 0 ) {
- $result = 4;
- } elseif( $this->isTreeData( $data ) ) {
- $result = 2;
- }
-
- return $result;
- }
-}
-
-class MissingFile extends File {
- public function __construct() {
- parent::__construct( '', '', '0', 0, 0, '' );
+ $this->refs->scanRefs(
+ 'refs/heads', $callback
+ );
+ }
+
+ public function eachTag( callable $callback ): void {
+ $this->refs->scanRefs(
+ 'refs/tags',
+ function( $name, $sha ) use ( $callback ) {
+ $callback(
+ $this->parseTagData(
+ $name, $sha, $this->read( $sha )
+ )
+ );
+ }
+ );
+ }
+
+ public function walk(
+ string $refOrSha,
+ callable $callback,
+ string $path = ''
+ ): void {
+ $sha = $this->resolve( $refOrSha );
+ $treeSha = $sha !== ''
+ ? $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 )
+ : [];
+
+ return isset( $info['sha'] )
+ && !$info['isDir']
+ && $info['sha'] !== ''
+ ? new File(
+ \basename( $path ),
+ $info['sha'],
+ $info['mode'],
+ 0,
+ $this->getObjectSize( $info['sha'] ),
+ $this->peek( $info['sha'] )
+ )
+ : new MissingFile();
+ }
+
+ 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;
+ $done = false;
+
+ while( !$done && $sha !== '' && $count < $limit ) {
+ $data = $this->read( $sha );
+
+ if( $data === '' ) {
+ $done = true;
+ } else {
+ $id = $this->parseIdentity(
+ $data, '/^author (.*) <(.*)> (\d+)/m'
+ );
+ $parentSha = $this->extractPattern(
+ $data, '/^parent (.*)$/m', 1
+ );
+
+ $commit = new Commit(
+ $sha,
+ $this->extractMessage( $data ),
+ $id['name'],
+ $id['email'],
+ $id['timestamp'],
+ $parentSha
+ );
+
+ if( $callback( $commit ) === false ) {
+ $done = true;
+ } else {
+ $sha = $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
+ ) {
+ \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 {
+ $entries = $this->buildPackEntries( $objs );
+ $ctx = \hash_init( 'sha1' );
+ $head = "PACK"
+ . \pack( 'N', 2 )
+ . \pack( 'N', \count( $objs ) );
+
+ \hash_update( $ctx, $head );
+ yield $head;
+
+ $written = [];
+ $outPos = 12;
+
+ foreach( $entries as $sha => $entry ) {
+ $written[$sha] = $outPos;
+ $baseSha = $entry['baseSha'];
+
+ $reuse = $baseSha !== ''
+ && isset( $written[$baseSha] );
+
+ if( $reuse ) {
+ $hdr = $this->encodeEntryHeader(
+ 6, $entry['deltaSize']
+ );
+ $hdr .= $this->encodeOffsetDelta(
+ $outPos - $written[$baseSha]
+ );
+
+ \hash_update( $ctx, $hdr );
+ $outPos += \strlen( $hdr );
+ yield $hdr;
+
+ foreach(
+ $this->packs->streamRawDelta(
+ $sha
+ ) as $chunk
+ ) {
+ \hash_update( $ctx, $chunk );
+ $outPos += \strlen( $chunk );
+ yield $chunk;
+ }
+ } else {
+ $size = $this->getObjectSize( $sha );
+ $hdr = $this->encodeEntryHeader(
+ $entry['logicalType'], $size
+ );
+
+ \hash_update( $ctx, $hdr );
+ $outPos += \strlen( $hdr );
+ yield $hdr;
+
+ foreach(
+ $this->streamCompressed( $sha ) as $chunk
+ ) {
+ \hash_update( $ctx, $chunk );
+ $outPos += \strlen( $chunk );
+ yield $chunk;
+ }
+ }
+ }
+
+ yield \hash_final( $ctx, true );
+ }
+
+ private function buildPackEntries(
+ array $objs
+ ): array {
+ $entries = [];
+ $offToSha = [];
+
+ foreach( $objs as $sha => $logicalType ) {
+ $meta = $this->packs->getEntryMeta( $sha );
+
+ $entries[$sha] = [
+ 'logicalType' => $logicalType,
+ 'packType' => $meta['type'],
+ 'deltaSize' => $meta['size'],
+ 'packFile' => $meta['file'],
+ 'offset' => $meta['offset'],
+ 'baseOffset' => $meta['baseOffset'] ?? 0,
+ 'baseSha' => $meta['baseSha'] ?? '',
+ ];
+
+ if( $meta['file'] !== '' ) {
+ $offToSha[$meta['file']][$meta['offset']]
+ = $sha;
+ }
+ }
+
+ foreach( $entries as &$e ) {
+ if(
+ $e['packType'] === 6
+ && $e['baseOffset'] > 0
+ ) {
+ $e['baseSha']
+ = $offToSha[$e['packFile']][$e['baseOffset']]
+ ?? '';
+ }
+ }
+
+ unset( $e );
+
+ \uasort(
+ $entries,
+ function( array $a, array $b ): int {
+ $cmp = $a['packFile'] <=> $b['packFile'];
+
+ return $cmp !== 0
+ ? $cmp
+ : $a['offset'] <=> $b['offset'];
+ }
+ );
+
+ return $entries;
+ }
+
+ private function encodeEntryHeader(
+ int $type,
+ int $size
+ ): string {
+ $byte = $type << 4 | $size & 0x0f;
+ $sz = $size >> 4;
+ $hdr = '';
+
+ while( $sz > 0 ) {
+ $hdr .= \chr( $byte | 0x80 );
+ $byte = $sz & 0x7f;
+ $sz >>= 7;
+ }
+
+ $hdr .= \chr( $byte );
+
+ return $hdr;
+ }
+
+ private function encodeOffsetDelta(
+ int $offset
+ ): string {
+ $buf = \chr( $offset & 0x7F );
+ $n = $offset >> 7;
+
+ while( $n > 0 ) {
+ $n--;
+ $buf = \chr( 0x80 | ($n & 0x7F) ) . $buf;
+ $n >>= 7;
+ }
+
+ return $buf;
+ }
+
+ private function streamCompressed(
+ string $sha
+ ): Generator {
+ $yielded = false;
+
+ foreach(
+ $this->packs->streamRawCompressed(
+ $sha
+ ) as $chunk
+ ) {
+ $yielded = true;
+ yield $chunk;
+ }
+
+ if( !$yielded ) {
+ $deflate = \deflate_init(
+ \ZLIB_ENCODING_DEFLATE
+ );
+
+ foreach( $this->slurpChunks( $sha ) as $raw ) {
+ $compressed = \deflate_add(
+ $deflate, $raw, \ZLIB_NO_FLUSH
+ );
+
+ if( $compressed !== '' ) {
+ yield $compressed;
+ }
+ }
+
+ $final = \deflate_add(
+ $deflate, '', \ZLIB_FINISH
+ );
+
+ if( $final !== '' ) {
+ yield $final;
+ }
+ }
+ }
+
+ private function slurpChunks(
+ string $sha
+ ): Generator {
+ $path = $this->getLoosePath( $sha );
+
+ if( \is_file( $path ) ) {
+ foreach(
+ $this->streamInflatedObjects(
+ $path
+ ) as $chunk
+ ) {
+ if( $chunk['body'] !== '' ) {
+ yield $chunk['body'];
+ }
+ }
+ } 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 streamInflatedObjects(
+ string $path,
+ int $bufSz = 16384
+ ): Generator {
+ $reader = new BufferedReader( $path );
+ $infl = $reader->isOpen()
+ ? \inflate_init( \ZLIB_ENCODING_DEFLATE )
+ : false;
+
+ if( $reader->isOpen() && $infl !== false ) {
+ $found = false;
+ $buffer = '';
+
+ while( !$reader->eof() ) {
+ $chunk = $reader->read( $bufSz );
+ $inflated = \inflate_add( $infl, $chunk );
+
+ if( $inflated === false ) {
+ break;
+ }
+
+ if( !$found ) {
+ $buffer .= $inflated;
+ $eos = \strpos( $buffer, "\0" );
+
+ if( $eos !== false ) {
+ $found = true;
+
+ yield [
+ 'head' => \substr(
+ $buffer, 0, $eos
+ ),
+ 'body' => \substr(
+ $buffer, $eos + 1
+ )
+ ];
+ }
+ } elseif( $inflated !== '' ) {
+ yield [
+ 'head' => '',
+ 'body' => $inflated
+ ];
+ }
+ }
+ }
+ }
+
+ 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 = $part !== '' && $sha !== ''
+ ? $this->findTreeEntry( $sha, $part )
+ : [ 'sha' => '', 'mode' => '' ];
+
+ $sha = $entry['sha'];
+ $mode = $entry['mode'];
+ }
+
+ return [
+ 'sha' => $sha,
+ 'mode' => $mode,
+ 'isDir' => $mode === '40000'
+ || $mode === '040000'
+ ];
+ }
+
+ private function findTreeEntry(
+ string $treeSha,
+ string $name
+ ): array {
+ $entry = [ 'sha' => '', 'mode' => '' ];
+
+ $this->parseTreeData(
+ $this->read( $treeSha ),
+ function(
+ $n, $s, $m
+ ) use ( $name, &$entry ) {
+ if( $n === $name ) {
+ $entry = [
+ 'sha' => $s,
+ 'mode' => $m
+ ];
+
+ return false;
+ }
+ }
+ );
+
+ return $entry;
+ }
+
+ private function parseTagData(
+ string $name,
+ string $sha,
+ string $data
+ ): Tag {
+ $isAnn = \strncmp( $data, 'object ', 7 ) === 0;
+ $id = $this->parseIdentity(
+ $data,
+ $isAnn
+ ? '/^tagger (.*) <(.*)> (\d+) [+\-]\d{4}$/m'
+ : '/^author (.*) <(.*)> (\d+) [+\-]\d{4}$/m'
+ );
+
+ return new Tag(
+ $name,
+ $sha,
+ $isAnn
+ ? $this->extractPattern(
+ $data, '/^object (.*)$/m', 1, $sha
+ )
+ : $sha,
+ $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 ) ) {
+ foreach(
+ $this->streamInflatedObjects(
+ $path
+ ) as $chunk
+ ) {
+ if( $chunk['body'] !== '' ) {
+ $callback( $chunk['body'] );
+ }
+ }
+ } elseif(
+ !$this->packs->stream( $sha, $callback )
+ ) {
+ $data = $this->packs->read( $sha );
+
+ if( $data !== '' ) {
+ $callback( $data );
+ }
+ }
+ }
+
+ private function peekLooseObject(
+ string $sha,
+ int $length
+ ): string {
+ $path = $this->getLoosePath( $sha );
+ $buf = '';
+
+ if( \is_file( $path ) ) {
+ foreach(
+ $this->streamInflatedObjects(
+ $path, 8192
+ ) as $chunk
+ ) {
+ $buf .= $chunk['body'];
+
+ if( \strlen( $buf ) >= $length ) {
+ break;
+ }
+ }
+ }
+
+ return \substr( $buf, 0, $length );
+ }
+
+ private function walkTree(
+ string $sha,
+ callable $callback
+ ): void {
+ $data = $this->read( $sha );
+ $tree = $data !== ''
+ && \preg_match(
+ '/^tree (.*)$/m', $data, $m
+ )
+ ? $this->read( $m[1] )
+ : $data;
+
+ if( $tree !== '' && $this->isTreeData( $tree ) ) {
+ $this->parseTreeData(
+ $tree,
+ function(
+ $n, $s, $m
+ ) use ( $callback ) {
+ $dir = $m === '40000'
+ || $m === '040000';
+ $isSub = $m === '160000';
+
+ $callback( new File(
+ $n,
+ $s,
+ $m,
+ 0,
+ $dir || $isSub
+ ? 0
+ : $this->getObjectSize( $s ),
+ $dir || $isSub
+ ? ''
+ : $this->peek( $s )
+ ) );
+ }
+ );
+ }
+ }
+
+ public function parseTreeData(
+ string $data,
+ callable $callback
+ ): void {
+ $pos = 0;
+ $len = \strlen( $data );
+
+ while( $pos < $len ) {
+ $space = \strpos( $data, ' ', $pos );
+ $eos = \strpos( $data, "\0", $space );
+
+ if(
+ $space === false
+ || $eos === false
+ || $eos + 21 > $len
+ ) {
+ break;
+ }
+
+ $mode = \substr(
+ $data, $pos, $space - $pos
+ );
+ $name = \substr(
+ $data, $space + 1, $eos - $space - 1
+ );
+ $sha = \bin2hex(
+ \substr( $data, $eos + 1, 20 )
+ );
+
+ if(
+ $callback( $name, $sha, $mode ) === false
+ ) {
+ break;
+ }
+
+ $pos = $eos + 21;
+ }
+ }
+
+ private function isTreeData( string $data ): bool {
+ $len = \strlen( $data );
+ $match = $len >= 25
+ && \preg_match(
+ '/^(40000|100644|100755|120000|160000) /',
+ $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 ) ) {
+ foreach(
+ $this->streamInflatedObjects(
+ $path
+ ) as $chunk
+ ) {
+ $parts = \explode( ' ', $chunk['head'] );
+ $size = isset( $parts[1] )
+ ? (int)$parts[1]
+ : 0;
+ break;
+ }
+ }
+
+ return $size;
+ }
+
+ public function collectObjects(
+ array $wants,
+ array $haves = []
+ ): array {
+ $objs = $this->traverseObjects( $wants );
+
+ if( !empty( $haves ) ) {
+ foreach(
+ $this->traverseObjects(
+ $haves
+ ) as $sha => $type
+ ) {
+ unset( $objs[$sha] );
+ }
+ }
+
+ return $objs;
+ }
+
+ private function traverseObjects(
+ array $roots
+ ): array {
+ $objs = [];
+ $queue = [];
+
+ foreach( $roots as $sha ) {
+ $queue[] = [
+ 'sha' => $sha,
+ 'type' => 0
+ ];
+ }
+
+ while( !empty( $queue ) ) {
+ $item = \array_pop( $queue );
+ $sha = $item['sha'];
+ $type = $item['type'];
+
+ if( !isset( $objs[$sha] ) ) {
+ $data = $type !== 3
+ ? $this->read( $sha )
+ : '';
+ $type = $type === 0
+ ? $this->getObjectType( $data )
+ : $type;
+
+ $objs[$sha] = $type;
+
+ if( $type === 1 ) {
+ if(
+ \preg_match(
+ '/^tree ([0-9a-f]{40})/m',
+ $data,
+ $m
+ )
+ ) {
+ $queue[] = [
+ 'sha' => $m[1],
+ 'type' => 2
+ ];
+ }
+
+ if(
+ \preg_match_all(
+ '/^parent ([0-9a-f]{40})/m',
+ $data,
+ $m
+ )
+ ) {
+ foreach( $m[1] as $parentSha ) {
+ $queue[] = [
+ 'sha' => $parentSha,
+ 'type' => 1
+ ];
+ }
+ }
+ } elseif( $type === 2 ) {
+ $pos = 0;
+ $len = \strlen( $data );
+
+ while( $pos < $len ) {
+ $space = \strpos(
+ $data, ' ', $pos
+ );
+ $eos = \strpos(
+ $data, "\0", $space
+ );
+
+ if(
+ $space === false
+ || $eos === false
+ ) {
+ break;
+ }
+
+ $mode = \substr(
+ $data, $pos, $space - $pos
+ );
+ $hash = \bin2hex(
+ \substr( $data, $eos + 1, 20 )
+ );
+
+ if( $mode !== '160000' ) {
+ $queue[] = [
+ 'sha' => $hash,
+ 'type' => $mode === '40000'
+ || $mode === '040000'
+ ? 2
+ : 3
+ ];
+ }
+
+ $pos = $eos + 21;
+ }
+ } elseif( $type === 4 ) {
+ if(
+ \preg_match(
+ '/^object ([0-9a-f]{40})/m',
+ $data,
+ $m
+ )
+ ) {
+ $nextType = 1;
+
+ if(
+ \preg_match(
+ '/^type (commit|tree|blob|tag)/m',
+ $data,
+ $t
+ )
+ ) {
+ $map = [
+ 'commit' => 1,
+ 'tree' => 2,
+ 'blob' => 3,
+ 'tag' => 4
+ ];
+
+ $nextType = $map[$t[1]] ?? 1;
+ }
+
+ $queue[] = [
+ 'sha' => $m[1],
+ 'type' => $nextType
+ ];
+ }
+ }
+ }
+ }
+
+ return $objs;
+ }
+
+ private function getObjectType(
+ string $data
+ ): int {
+ $result = 3;
+
+ if( \strpos( $data, "tree " ) === 0 ) {
+ $result = 1;
+ } elseif(
+ \strpos( $data, "object " ) === 0
+ ) {
+ $result = 4;
+ } elseif( $this->isTreeData( $data ) ) {
+ $result = 2;
+ }
+
+ return $result;
+ }
+}
+
+class MissingFile extends File {
+ public function __construct() {
+ parent::__construct(
+ '', '', '0', 0, 0, ''
+ );
}
git/GitPacks.php
private PackLocator $locator;
private PackEntryReader $reader;
- private array $cacheLoc = [ 'sha' => '', 'file' => '', 'off' => 0 ];
+ private array $cacheLoc = [
+ 'sha' => '',
+ 'file' => '',
+ 'off' => 0
+ ];
public function __construct( string $objectsPath ) {
$this->manager = new PackStreamManager();
- $this->locator = new PackLocator( $this->manager, $objectsPath );
- $this->reader = new PackEntryReader( new DeltaDecoder() );
+ $this->locator = new PackLocator(
+ $this->manager, $objectsPath
+ );
+ $this->reader = new PackEntryReader(
+ new DeltaDecoder()
+ );
}
- private function locate( string $sha, callable $callback ): void {
+ private function locate(
+ string $sha,
+ callable $callback
+ ): void {
if( $this->cacheLoc['sha'] === $sha ) {
- $callback( $this->cacheLoc['file'], $this->cacheLoc['off'] );
+ $callback(
+ $this->cacheLoc['file'],
+ $this->cacheLoc['off']
+ );
} else {
$this->locator->locate(
$sha,
- function( string $packFile, int $offset ) use (
- $sha,
- $callback
- ): void {
+ function(
+ string $packFile,
+ int $offset
+ ) use ( $sha, $callback ): void {
$this->cacheLoc = [
'sha' => $sha,
}
- public function peek( string $sha, int $len = 12 ): string {
+ public function getEntryMeta( string $sha ): array {
+ $result = [
+ 'type' => 0,
+ 'size' => 0,
+ 'file' => '',
+ 'offset' => 0,
+ ];
+
+ $this->locate(
+ $sha,
+ function(
+ string $packFile,
+ int $offset
+ ) use ( &$result ): void {
+ $context = $this->createContext(
+ $packFile, $offset, 0
+ );
+ $meta = $this->reader->getEntryMeta(
+ $context
+ );
+
+ $result = $meta;
+ $result['file'] = $packFile;
+ $result['offset'] = $offset;
+ }
+ );
+
+ return $result;
+ }
+
+ public function peek(
+ string $sha,
+ int $len = 12
+ ): string {
$result = '';
$this->locate(
$sha,
- function( string $packFile, int $offset ) use ( &$result, $len ): void {
- $context = $this->createContext( $packFile, $offset, 0 );
+ function(
+ string $packFile,
+ int $offset
+ ) use ( &$result, $len ): void {
+ $context = $this->createContext(
+ $packFile, $offset, 0
+ );
$result = $this->reader->read(
$context,
$len,
- function( string $baseSha, int $cap ): string {
+ function(
+ string $baseSha,
+ int $cap
+ ): string {
return $this->peek( $baseSha, $cap );
}
$this->locate(
$sha,
- function( string $packFile, int $offset ) use ( &$result ): void {
- $context = $this->createContext( $packFile, $offset, 0 );
+ function(
+ string $packFile,
+ int $offset
+ ) use ( &$result ): void {
+ $context = $this->createContext(
+ $packFile, $offset, 0
+ );
$size = $this->reader->getSize( $context );
if( $size <= self::MAX_RAM ) {
$result = $this->reader->read(
$context,
0,
- function( string $baseSha, int $cap ): string {
+ function(
+ string $baseSha,
+ int $cap
+ ): string {
return $cap > 0
? $this->peek( $baseSha, $cap )
}
- public function stream( string $sha, callable $callback ): bool {
+ public function stream(
+ string $sha,
+ callable $callback
+ ): bool {
$result = false;
- foreach( $this->streamGenerator( $sha ) as $chunk ) {
+ foreach(
+ $this->streamGenerator( $sha ) as $chunk
+ ) {
$callback( $chunk );
$result = true;
}
return $result;
}
- public function streamGenerator( string $sha ): Generator {
+ public function streamGenerator(
+ string $sha
+ ): Generator {
yield from $this->streamShaGenerator( $sha, 0 );
}
- public function streamRawCompressed( string $sha ): Generator {
+ public function streamRawCompressed(
+ string $sha
+ ): Generator {
$found = false;
$file = '';
$off = 0;
$this->locate(
$sha,
- function( string $packFile, int $offset ) use (
- &$found,
- &$file,
- &$off
- ): void {
+ function(
+ string $packFile,
+ int $offset
+ ) use ( &$found, &$file, &$off ): void {
$found = true;
$file = $packFile;
$off = $offset;
}
);
if( $found ) {
- $context = $this->createContext( $file, $off, 0 );
+ $context = $this->createContext(
+ $file, $off, 0
+ );
- yield from $this->reader->streamRawCompressed( $context );
+ yield from $this->reader->streamRawCompressed(
+ $context
+ );
}
}
- private function streamShaGenerator( string $sha, int $depth ): Generator {
+ public function streamRawDelta(
+ string $sha
+ ): Generator {
$found = false;
$file = '';
$off = 0;
$this->locate(
$sha,
- function( string $packFile, int $offset ) use (
- &$found,
- &$file,
- &$off
- ): void {
+ function(
+ string $packFile,
+ int $offset
+ ) use ( &$found, &$file, &$off ): void {
$found = true;
$file = $packFile;
$off = $offset;
}
);
if( $found ) {
- $context = $this->createContext( $file, $off, $depth );
+ $context = $this->createContext(
+ $file, $off, 0
+ );
- yield from $this->reader->streamEntryGenerator( $context );
+ yield from $this->reader->streamRawDelta(
+ $context
+ );
+ }
+ }
+
+ private function streamShaGenerator(
+ string $sha,
+ int $depth
+ ): Generator {
+ $found = false;
+ $file = '';
+ $off = 0;
+
+ $this->locate(
+ $sha,
+ function(
+ string $packFile,
+ int $offset
+ ) use ( &$found, &$file, &$off ): void {
+ $found = true;
+ $file = $packFile;
+ $off = $offset;
+ }
+ );
+
+ if( $found ) {
+ $context = $this->createContext(
+ $file, $off, $depth
+ );
+
+ yield from $this->reader->streamEntryGenerator(
+ $context
+ );
}
}
public function getSize( string $sha ): int {
$result = 0;
$this->locate(
$sha,
- function( string $packFile, int $offset ) use ( &$result ): void {
- $context = $this->createContext( $packFile, $offset, 0 );
+ function(
+ string $packFile,
+ int $offset
+ ) use ( &$result ): void {
+ $context = $this->createContext(
+ $packFile, $offset, 0
+ );
$result = $this->reader->getSize( $context );
}
return $this->getSize( $baseSha );
},
- function( string $baseSha, int $baseDepth ): Generator {
- yield from $this->streamShaGenerator( $baseSha, $baseDepth );
+ function(
+ string $baseSha,
+ int $baseDepth
+ ): Generator {
+ yield from $this->streamShaGenerator(
+ $baseSha, $baseDepth
+ );
}
);
git/PackContext.php
}
- public function deriveOffsetContext( int $negativeOffset ): self {
+ public function deriveOffsetContext(
+ int $negativeOffset
+ ): self {
return new self(
$this->manager,
}
- public function computeInt( callable $callback, int $default ): int {
+ public function computeInt(
+ callable $callback,
+ int $default
+ ): int {
return $this->manager->computeInt(
$this->packFile,
- function( StreamReader $stream ) use ( $callback ): int {
+ function( StreamReader $stream ) use (
+ $callback
+ ): int {
return $callback( $stream, $this->offset );
},
$default
);
}
- public function computeIntDedicated( callable $callback, int $default ): int {
+ public function computeIntDedicated(
+ callable $callback,
+ int $default
+ ): int {
return $this->manager->computeIntDedicated(
$this->packFile,
- function( StreamReader $stream ) use ( $callback ): int {
+ function( StreamReader $stream ) use (
+ $callback
+ ): int {
return $callback( $stream, $this->offset );
},
return $this->manager->computeStringDedicated(
$this->packFile,
- function( StreamReader $stream ) use ( $callback ): string {
+ function( StreamReader $stream ) use (
+ $callback
+ ): string {
return $callback( $stream, $this->offset );
},
$default
);
}
- public function streamGenerator( callable $callback ): Generator {
+ public function computeArray(
+ callable $callback,
+ array $default
+ ): array {
+ return $this->manager->computeArray(
+ $this->packFile,
+ function( StreamReader $stream ) use (
+ $callback
+ ): array {
+ return $callback( $stream, $this->offset );
+ },
+ $default
+ );
+ }
+
+ public function streamGenerator(
+ callable $callback
+ ): Generator {
yield from $this->manager->streamGenerator(
$this->packFile,
- function( StreamReader $stream ) use ( $callback ): Generator {
- yield from $callback( $stream, $this->offset );
+ function( StreamReader $stream ) use (
+ $callback
+ ): Generator {
+ yield from $callback(
+ $stream, $this->offset
+ );
}
);
}
- public function streamGeneratorDedicated( callable $callback ): Generator {
+ public function streamGeneratorDedicated(
+ callable $callback
+ ): Generator {
yield from $this->manager->streamGeneratorDedicated(
$this->packFile,
- function( StreamReader $stream ) use ( $callback ): Generator {
- yield from $callback( $stream, $this->offset );
+ function( StreamReader $stream ) use (
+ $callback
+ ): Generator {
+ yield from $callback(
+ $stream, $this->offset
+ );
}
);
}
public function resolveBaseSize( string $sha ): int {
return ($this->sizeResolver)( $sha );
}
- public function resolveBaseStream( string $sha ): Generator {
- yield from ($this->streamResolver)( $sha, $this->depth + 1 );
+ public function resolveBaseStream(
+ string $sha
+ ): Generator {
+ yield from ($this->streamResolver)(
+ $sha, $this->depth + 1
+ );
}
git/PackEntryReader.php
}
- public function getSize( PackContext $context ): int {
- return $context->computeIntDedicated(
- function( StreamReader $stream, int $offset ): int {
- $hdr = $this->readEntryHeader( $stream, $offset );
-
- return $hdr['type'] === 6 || $hdr['type'] === 7
- ? $this->decoder->readDeltaTargetSize( $stream, $hdr['type'] )
- : $hdr['size'];
- },
- 0
- );
- }
-
- public function read(
- PackContext $context,
- int $cap,
- callable $readShaBaseFn
- ): string {
- return $context->computeStringDedicated(
- function( StreamReader $s, int $o ) use ( $cap, $readShaBaseFn ): string {
- return $this->readWithStream( $s, $o, $cap, $readShaBaseFn );
- },
- ''
- );
- }
-
- private function readWithStream(
- StreamReader $stream,
- int $offset,
- int $cap,
- callable $readShaBaseFn
- ): string {
- $result = '';
-
- if( isset( $this->cache[$offset] ) ) {
- $result = $cap > 0 && \strlen( $this->cache[$offset] ) > $cap
- ? \substr( $this->cache[$offset], 0, $cap )
- : $this->cache[$offset];
- } else {
- $hdr = $this->readEntryHeader( $stream, $offset );
- $type = $hdr['type'];
-
- if( $type === 6 ) {
- $neg = $this->readOffsetDelta( $stream );
- $cur = $stream->tell();
- $bData = $this->readWithStream(
- $stream,
- $offset - $neg,
- $cap,
- $readShaBaseFn
- );
-
- $stream->seek( $cur );
-
- $result = $this->decoder->apply(
- $bData,
- $this->inflate( $stream ),
- $cap
- );
- } elseif( $type === 7 ) {
- $sha = \bin2hex( $stream->read( 20 ) );
- $cur = $stream->tell();
- $bas = $readShaBaseFn( $sha, $cap );
-
- $stream->seek( $cur );
-
- $result = $this->decoder->apply(
- $bas,
- $this->inflate( $stream ),
- $cap
- );
- } else {
- $result = $this->inflate( $stream, $cap );
- }
-
- if( $cap === 0 ) {
- $this->cache[$offset] = $result;
-
- if( \count( $this->cache ) > self::MAX_CACHE ) {
- unset( $this->cache[\array_key_first( $this->cache )] );
- }
- }
- }
-
- return $result;
- }
-
- public function streamRawCompressed( PackContext $context ): Generator {
- yield from $context->streamGenerator(
- function( StreamReader $stream, int $offset ): Generator {
- $hdr = $this->readEntryHeader( $stream, $offset );
-
- yield from $hdr['type'] !== 6 && $hdr['type'] !== 7
- ? CompressionStream::createExtractor()->stream( $stream )
- : [];
- }
- );
- }
-
- public function streamEntryGenerator( PackContext $context ): Generator {
- yield from $context->streamGeneratorDedicated(
- function( StreamReader $stream, int $offset ) use (
- $context
- ): Generator {
- $hdr = $this->readEntryHeader( $stream, $offset );
-
- yield from $hdr['type'] === 6 || $hdr['type'] === 7
- ? $this->streamDeltaObjectGenerator(
- $stream,
- $context,
- $hdr['type'],
- $offset
- )
- : CompressionStream::createInflater()->stream( $stream );
- }
- );
- }
-
- private function readEntryHeader( StreamReader $stream, int $offset ): array {
- $stream->seek( $offset );
-
- $header = $this->readVarInt( $stream );
-
- return [
- 'type' => $header['byte'] >> 4 & 7,
- 'size' => $header['value']
- ];
- }
-
- private function streamDeltaObjectGenerator(
- StreamReader $stream,
- PackContext $context,
- int $type,
- int $offset
- ): Generator {
- $gen = $context->isWithinDepth( self::MAX_DEPTH )
- ? ( $type === 6
- ? $this->processOffsetDelta( $stream, $context, $offset )
- : $this->processRefDelta( $stream, $context )
- )
- : [];
-
- yield from $gen;
- }
-
- private function readSizeWithStream( StreamReader $stream, int $offset ): int {
- $result = 0;
-
- if( isset( $this->cache[$offset] ) ) {
- $result = \strlen( $this->cache[$offset] );
- } else {
- $cur = $stream->tell();
- $hdr = $this->readEntryHeader( $stream, $offset );
-
- $result = $hdr['type'] === 6 || $hdr['type'] === 7
- ? $this->decoder->readDeltaTargetSize( $stream, $hdr['type'] )
- : $hdr['size'];
-
- $stream->seek( $cur );
- }
-
- return $result;
- }
-
- private function processOffsetDelta(
- StreamReader $stream,
- PackContext $context,
- int $offset
- ): Generator {
- $neg = $this->readOffsetDelta( $stream );
- $cur = $stream->tell();
- $baseOff = $offset - $neg;
- $baseSrc = '';
-
- if( isset( $this->cache[$baseOff] ) ) {
- $baseSrc = $this->cache[$baseOff];
- } elseif(
- $this->readSizeWithStream( $stream, $baseOff ) <= self::MAX_BASE_RAM
- ) {
- $baseSrc = $this->readWithStream(
- $stream,
- $baseOff,
- 0,
- function( string $sha, int $cap ) use ( $context ): string {
- return $this->resolveBaseSha( $sha, $cap, $context );
- }
- );
- } else {
- $baseCtx = $context->deriveOffsetContext( $neg );
- [$b, $tmp] = $this->collectBase(
- $this->streamEntryGenerator( $baseCtx )
- );
- $baseSrc = $tmp instanceof BufferedReader ? $tmp : $b;
- }
-
- $stream->seek( $cur );
-
- yield from $this->decoder->applyStreamGenerator( $stream, $baseSrc );
- }
-
- private function processRefDelta(
- StreamReader $stream,
- PackContext $context
- ): Generator {
- $baseSha = \bin2hex( $stream->read( 20 ) );
- $cur = $stream->tell();
- $size = $context->resolveBaseSize( $baseSha );
- $baseSrc = '';
-
- if( $size <= self::MAX_BASE_RAM ) {
- $baseSrc = $this->resolveBaseSha( $baseSha, 0, $context );
- } else {
- [$b, $tmp] = $this->collectBase(
- $context->resolveBaseStream( $baseSha )
- );
- $baseSrc = $tmp instanceof BufferedReader ? $tmp : $b;
- }
-
- $stream->seek( $cur );
-
- yield from $this->decoder->applyStreamGenerator( $stream, $baseSrc );
- }
-
- private function collectBase( iterable $chunks ): array {
- $parts = [];
- $total = 0;
- $tmp = false;
-
- foreach( $chunks as $chunk ) {
- $total += \strlen( $chunk );
-
- if( $tmp instanceof BufferedReader ) {
- $tmp->write( $chunk );
- } elseif( $total > self::MAX_BASE_RAM ) {
- $tmp = new BufferedReader( 'php://temp/maxmemory:65536', 'w+b' );
-
- foreach( $parts as $part ) {
- $tmp->write( $part );
- }
-
- $tmp->write( $chunk );
- $parts = [];
- } else {
- $parts[] = $chunk;
- }
- }
-
- if( $tmp instanceof BufferedReader ) {
- $tmp->rewind();
- }
-
- return [ $tmp === false ? \implode( '', $parts ) : '', $tmp ];
- }
-
- private function resolveBaseSha(
- string $sha,
- int $cap,
- PackContext $context
- ): string {
- $chunks = [];
-
- foreach( $context->resolveBaseStream( $sha ) as $chunk ) {
- $chunks[] = $chunk;
- }
-
- $result = \implode( '', $chunks );
-
- return $cap > 0 && \strlen( $result ) > $cap
- ? \substr( $result, 0, $cap )
- : $result;
- }
-
- private function readVarInt( StreamReader $stream ): array {
- $data = $stream->read( 12 );
- $byte = isset( $data[0] ) ? \ord( $data[0] ) : 0;
- $val = $byte & 15;
- $shft = 4;
- $fst = $byte;
- $pos = 1;
-
- while( $byte & 128 ) {
- $byte = isset( $data[$pos] ) ? \ord( $data[$pos++] ) : 0;
- $val |= ( $byte & 127 ) << $shft;
- $shft += 7;
- }
-
- $rem = \strlen( $data ) - $pos;
-
- if( $rem > 0 ) {
- $stream->seek( -$rem, SEEK_CUR );
- }
-
- return [ 'value' => $val, 'byte' => $fst ];
- }
-
- private function readOffsetDelta( StreamReader $stream ): int {
- $data = $stream->read( 12 );
- $byte = isset( $data[0] ) ? \ord( $data[0] ) : 0;
- $result = $byte & 127;
- $pos = 1;
-
- while( $byte & 128 ) {
- $byte = isset( $data[$pos] ) ? \ord( $data[$pos++] ) : 0;
- $result = ( $result + 1 ) << 7 | $byte & 127;
- }
-
- $rem = \strlen( $data ) - $pos;
-
- if( $rem > 0 ) {
- $stream->seek( -$rem, SEEK_CUR );
- }
-
- return $result;
- }
-
- private function inflate( StreamReader $stream, int $cap = 0 ): string {
+ public function getEntryMeta(
+ PackContext $context
+ ): array {
+ return $context->computeArray(
+ function(
+ StreamReader $stream,
+ int $offset
+ ): array {
+ $hdr = $this->readEntryHeader(
+ $stream, $offset
+ );
+ $result = [
+ 'type' => $hdr['type'],
+ 'size' => $hdr['size'],
+ ];
+
+ if( $hdr['type'] === 6 ) {
+ $neg = $this->readOffsetDelta(
+ $stream
+ );
+ $result['baseOffset'] = $offset - $neg;
+ } elseif( $hdr['type'] === 7 ) {
+ $result['baseSha'] = \bin2hex(
+ $stream->read( 20 )
+ );
+ }
+
+ return $result;
+ },
+ [ 'type' => 0, 'size' => 0 ]
+ );
+ }
+
+ public function getSize( PackContext $context ): int {
+ return $context->computeIntDedicated(
+ function(
+ StreamReader $stream,
+ int $offset
+ ): int {
+ $hdr = $this->readEntryHeader(
+ $stream, $offset
+ );
+
+ return $hdr['type'] === 6 || $hdr['type'] === 7
+ ? $this->decoder->readDeltaTargetSize(
+ $stream, $hdr['type']
+ )
+ : $hdr['size'];
+ },
+ 0
+ );
+ }
+
+ public function read(
+ PackContext $context,
+ int $cap,
+ callable $readShaBaseFn
+ ): string {
+ return $context->computeStringDedicated(
+ function(
+ StreamReader $s,
+ int $o
+ ) use ( $cap, $readShaBaseFn ): string {
+ return $this->readWithStream(
+ $s, $o, $cap, $readShaBaseFn
+ );
+ },
+ ''
+ );
+ }
+
+ private function readWithStream(
+ StreamReader $stream,
+ int $offset,
+ int $cap,
+ callable $readShaBaseFn
+ ): string {
+ $result = '';
+
+ if( isset( $this->cache[$offset] ) ) {
+ $result = $cap > 0
+ && \strlen( $this->cache[$offset] ) > $cap
+ ? \substr( $this->cache[$offset], 0, $cap )
+ : $this->cache[$offset];
+ } else {
+ $hdr = $this->readEntryHeader(
+ $stream, $offset
+ );
+ $type = $hdr['type'];
+
+ if( $type === 6 ) {
+ $neg = $this->readOffsetDelta( $stream );
+ $cur = $stream->tell();
+ $bData = $this->readWithStream(
+ $stream,
+ $offset - $neg,
+ $cap,
+ $readShaBaseFn
+ );
+
+ $stream->seek( $cur );
+
+ $result = $this->decoder->apply(
+ $bData,
+ $this->inflate( $stream ),
+ $cap
+ );
+ } elseif( $type === 7 ) {
+ $sha = \bin2hex( $stream->read( 20 ) );
+ $cur = $stream->tell();
+ $bas = $readShaBaseFn( $sha, $cap );
+
+ $stream->seek( $cur );
+
+ $result = $this->decoder->apply(
+ $bas,
+ $this->inflate( $stream ),
+ $cap
+ );
+ } else {
+ $result = $this->inflate( $stream, $cap );
+ }
+
+ if( $cap === 0 ) {
+ $this->cache[$offset] = $result;
+
+ if( \count( $this->cache ) > self::MAX_CACHE ) {
+ unset(
+ $this->cache[
+ \array_key_first( $this->cache )
+ ]
+ );
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ public function streamRawCompressed(
+ PackContext $context
+ ): Generator {
+ yield from $context->streamGenerator(
+ function(
+ StreamReader $stream,
+ int $offset
+ ): Generator {
+ $hdr = $this->readEntryHeader(
+ $stream, $offset
+ );
+
+ yield from $hdr['type'] !== 6
+ && $hdr['type'] !== 7
+ ? CompressionStream::createExtractor()->stream(
+ $stream
+ )
+ : [];
+ }
+ );
+ }
+
+ public function streamRawDelta(
+ PackContext $context
+ ): Generator {
+ yield from $context->streamGenerator(
+ function(
+ StreamReader $stream,
+ int $offset
+ ): Generator {
+ $hdr = $this->readEntryHeader(
+ $stream, $offset
+ );
+
+ if( $hdr['type'] === 6 ) {
+ $this->readOffsetDelta( $stream );
+ } elseif( $hdr['type'] === 7 ) {
+ $stream->read( 20 );
+ }
+
+ yield from CompressionStream::createExtractor()
+ ->stream( $stream );
+ }
+ );
+ }
+
+ public function streamEntryGenerator(
+ PackContext $context
+ ): Generator {
+ yield from $context->streamGeneratorDedicated(
+ function(
+ StreamReader $stream,
+ int $offset
+ ) use ( $context ): Generator {
+ $hdr = $this->readEntryHeader(
+ $stream, $offset
+ );
+
+ yield from $hdr['type'] === 6
+ || $hdr['type'] === 7
+ ? $this->streamDeltaObjectGenerator(
+ $stream,
+ $context,
+ $hdr['type'],
+ $offset
+ )
+ : CompressionStream::createInflater()->stream(
+ $stream
+ );
+ }
+ );
+ }
+
+ private function readEntryHeader(
+ StreamReader $stream,
+ int $offset
+ ): array {
+ $stream->seek( $offset );
+
+ $header = $this->readVarInt( $stream );
+
+ return [
+ 'type' => $header['byte'] >> 4 & 7,
+ 'size' => $header['value']
+ ];
+ }
+
+ private function streamDeltaObjectGenerator(
+ StreamReader $stream,
+ PackContext $context,
+ int $type,
+ int $offset
+ ): Generator {
+ $gen = $context->isWithinDepth( self::MAX_DEPTH )
+ ? ( $type === 6
+ ? $this->processOffsetDelta(
+ $stream, $context, $offset
+ )
+ : $this->processRefDelta( $stream, $context )
+ )
+ : [];
+
+ yield from $gen;
+ }
+
+ private function readSizeWithStream(
+ StreamReader $stream,
+ int $offset
+ ): int {
+ $result = 0;
+
+ if( isset( $this->cache[$offset] ) ) {
+ $result = \strlen( $this->cache[$offset] );
+ } else {
+ $cur = $stream->tell();
+ $hdr = $this->readEntryHeader(
+ $stream, $offset
+ );
+
+ $result = $hdr['type'] === 6
+ || $hdr['type'] === 7
+ ? $this->decoder->readDeltaTargetSize(
+ $stream, $hdr['type']
+ )
+ : $hdr['size'];
+
+ $stream->seek( $cur );
+ }
+
+ return $result;
+ }
+
+ private function processOffsetDelta(
+ StreamReader $stream,
+ PackContext $context,
+ int $offset
+ ): Generator {
+ $neg = $this->readOffsetDelta( $stream );
+ $cur = $stream->tell();
+ $baseOff = $offset - $neg;
+ $baseSrc = '';
+
+ if( isset( $this->cache[$baseOff] ) ) {
+ $baseSrc = $this->cache[$baseOff];
+ } elseif(
+ $this->readSizeWithStream(
+ $stream, $baseOff
+ ) <= self::MAX_BASE_RAM
+ ) {
+ $baseSrc = $this->readWithStream(
+ $stream,
+ $baseOff,
+ 0,
+ function(
+ string $sha,
+ int $cap
+ ) use ( $context ): string {
+ return $this->resolveBaseSha(
+ $sha, $cap, $context
+ );
+ }
+ );
+ } else {
+ $baseCtx = $context->deriveOffsetContext(
+ $neg
+ );
+ [$b, $tmp] = $this->collectBase(
+ $this->streamEntryGenerator( $baseCtx )
+ );
+ $baseSrc = $tmp instanceof BufferedReader
+ ? $tmp
+ : $b;
+ }
+
+ $stream->seek( $cur );
+
+ yield from $this->decoder->applyStreamGenerator(
+ $stream, $baseSrc
+ );
+ }
+
+ private function processRefDelta(
+ StreamReader $stream,
+ PackContext $context
+ ): Generator {
+ $baseSha = \bin2hex( $stream->read( 20 ) );
+ $cur = $stream->tell();
+ $size = $context->resolveBaseSize( $baseSha );
+ $baseSrc = '';
+
+ if( $size <= self::MAX_BASE_RAM ) {
+ $baseSrc = $this->resolveBaseSha(
+ $baseSha, 0, $context
+ );
+ } else {
+ [$b, $tmp] = $this->collectBase(
+ $context->resolveBaseStream( $baseSha )
+ );
+ $baseSrc = $tmp instanceof BufferedReader
+ ? $tmp
+ : $b;
+ }
+
+ $stream->seek( $cur );
+
+ yield from $this->decoder->applyStreamGenerator(
+ $stream, $baseSrc
+ );
+ }
+
+ private function collectBase(
+ iterable $chunks
+ ): array {
+ $parts = [];
+ $total = 0;
+ $tmp = false;
+
+ foreach( $chunks as $chunk ) {
+ $total += \strlen( $chunk );
+
+ if( $tmp instanceof BufferedReader ) {
+ $tmp->write( $chunk );
+ } elseif( $total > self::MAX_BASE_RAM ) {
+ $tmp = new BufferedReader(
+ 'php://temp/maxmemory:65536', 'w+b'
+ );
+
+ foreach( $parts as $part ) {
+ $tmp->write( $part );
+ }
+
+ $tmp->write( $chunk );
+ $parts = [];
+ } else {
+ $parts[] = $chunk;
+ }
+ }
+
+ if( $tmp instanceof BufferedReader ) {
+ $tmp->rewind();
+ }
+
+ return [
+ $tmp === false ? \implode( '', $parts ) : '',
+ $tmp
+ ];
+ }
+
+ private function resolveBaseSha(
+ string $sha,
+ int $cap,
+ PackContext $context
+ ): string {
+ $chunks = [];
+
+ foreach(
+ $context->resolveBaseStream( $sha ) as $chunk
+ ) {
+ $chunks[] = $chunk;
+ }
+
+ $result = \implode( '', $chunks );
+
+ return $cap > 0 && \strlen( $result ) > $cap
+ ? \substr( $result, 0, $cap )
+ : $result;
+ }
+
+ private function readVarInt(
+ StreamReader $stream
+ ): array {
+ $data = $stream->read( 12 );
+ $byte = isset( $data[0] ) ? \ord( $data[0] ) : 0;
+ $val = $byte & 15;
+ $shft = 4;
+ $fst = $byte;
+ $pos = 1;
+
+ while( $byte & 128 ) {
+ $byte = isset( $data[$pos] )
+ ? \ord( $data[$pos++] )
+ : 0;
+ $val |= ( $byte & 127 ) << $shft;
+ $shft += 7;
+ }
+
+ $rem = \strlen( $data ) - $pos;
+
+ if( $rem > 0 ) {
+ $stream->seek( -$rem, SEEK_CUR );
+ }
+
+ return [ 'value' => $val, 'byte' => $fst ];
+ }
+
+ private function readOffsetDelta(
+ StreamReader $stream
+ ): int {
+ $data = $stream->read( 12 );
+ $byte = isset( $data[0] ) ? \ord( $data[0] ) : 0;
+ $result = $byte & 127;
+ $pos = 1;
+
+ while( $byte & 128 ) {
+ $byte = isset( $data[$pos] )
+ ? \ord( $data[$pos++] )
+ : 0;
+ $result = ( $result + 1 ) << 7 | $byte & 127;
+ }
+
+ $rem = \strlen( $data ) - $pos;
+
+ if( $rem > 0 ) {
+ $stream->seek( -$rem, SEEK_CUR );
+ }
+
+ return $result;
+ }
+
+ private function inflate(
+ StreamReader $stream,
+ int $cap = 0
+ ): string {
$inflater = CompressionStream::createInflater();
$chunks = [];
git/PackStreamManager.php
}
+ public function computeArray(
+ string $path,
+ callable $callback,
+ array $default
+ ): array {
+ $result = $default;
+ $reader = $this->acquire( $path );
+
+ if( $reader->isOpen() ) {
+ try {
+ $result = $callback( $reader );
+ } finally {
+ $this->release( $path, $reader );
+ }
+ }
+
+ return $result;
+ }
+
public function streamGenerator(
string $path,
}
- private function release( string $path, BufferedReader $reader ): void {
+ private function release(
+ string $path,
+ BufferedReader $reader
+ ): void {
if( !isset( $this->readers[$path] ) ) {
$this->readers[$path] = [];
Delta1713 lines added, 1019 lines removed, 694-line increase