Dave Jarvis' Repositories

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

Refactors compression streams and model

AuthorDave Jarvis <email>
Date2026-02-22 22:33:59 GMT-0800
Commit0103b15edbf577eef210c21b9633ed5a1c0634f1
Parent46c3653
git/CompressionStream.php
require_once __DIR__ . '/StreamReader.php';
-class CompressionStream {
- private Closure $pumper;
- private Closure $finisher;
- private Closure $status;
-
- private function __construct(
- Closure $pumper,
- Closure $finisher,
- Closure $status
- ) {
- $this->pumper = $pumper;
- $this->finisher = $finisher;
- $this->status = $status;
- }
+interface CompressionStream {
+ public function stream(
+ StreamReader $stream,
+ int $chunkSize = 8192
+ ): Generator;
+}
- public static function createExtractor(): self {
+class ZlibExtractorStream implements CompressionStream {
+ public function stream(
+ StreamReader $stream,
+ int $chunkSize = 8192
+ ): Generator {
$context = \inflate_init( \ZLIB_ENCODING_DEFLATE );
+ $done = false;
- return new self(
- function( string $chunk ) use ( $context ): string {
+ while( !$done && !$stream->eof() ) {
+ $chunk = $stream->read( $chunkSize );
+ $done = $chunk === '';
+
+ if( !$done ) {
$before = \inflate_get_read_len( $context );
@\inflate_add( $context, $chunk );
- return \substr(
+ $data = \substr(
$chunk,
0,
\inflate_get_read_len( $context ) - $before
);
- },
- function(): string {
- return '';
- },
- function() use ( $context ): bool {
- return \inflate_get_status( $context ) === \ZLIB_STREAM_END;
- }
- );
- }
-
- public static function createInflater(): self {
- $context = \inflate_init( \ZLIB_ENCODING_DEFLATE );
-
- return new self(
- function( string $chunk ) use ( $context ): string {
- $data = @\inflate_add( $context, $chunk );
-
- return $data === false ? '' : $data;
- },
- function(): string {
- return '';
- },
- function() use ( $context ): bool {
- return \inflate_get_status( $context ) === \ZLIB_STREAM_END;
- }
- );
- }
-
- public static function createDeflater(): self {
- $context = \deflate_init( \ZLIB_ENCODING_DEFLATE );
-
- return new self(
- function( string $chunk ) use ( $context ): string {
- $data = \deflate_add( $context, $chunk, \ZLIB_NO_FLUSH );
- return $data === false ? '' : $data;
- },
- function() use ( $context ): string {
- $data = \deflate_add( $context, '', \ZLIB_FINISH );
+ if( $data !== '' ) {
+ yield $data;
+ }
- return $data === false ? '' : $data;
- },
- function(): bool {
- return false;
+ $done = \inflate_get_status( $context ) === \ZLIB_STREAM_END;
}
- );
+ }
}
+}
+class ZlibInflaterStream implements CompressionStream {
public function stream(
StreamReader $stream,
int $chunkSize = 8192
): Generator {
- $done = false;
+ $context = \inflate_init( \ZLIB_ENCODING_DEFLATE );
+ $done = false;
while( !$done && !$stream->eof() ) {
$chunk = $stream->read( $chunkSize );
$done = $chunk === '';
if( !$done ) {
- $data = $this->pump( $chunk );
+ $data = @\inflate_add( $context, $chunk );
- if( $data !== '' ) {
+ if( $data !== false && $data !== '' ) {
yield $data;
}
- $done = $this->finished();
+ $done = \inflate_get_status( $context ) === \ZLIB_STREAM_END;
}
}
- }
-
- public function pump( string $chunk ): string {
- return $chunk === '' ? '' : ($this->pumper)( $chunk );
- }
-
- public function finish(): string {
- return ($this->finisher)();
- }
-
- public function finished(): bool {
- return ($this->status)();
}
}
git/Git.php
}
- public function setRepository(
- string $repoPath
- ): void {
- $this->repoPath = \rtrim( $repoPath, '/' );
-
- $objPath = $this->repoPath . '/objects';
- $this->refs = new GitRefs( $this->repoPath );
- $this->packs = new GitPacks( $objPath );
- $this->loose = new LooseObjects( $objPath );
- $this->packWriter = new PackfileWriter(
- $this->packs, $this->loose
- );
- }
-
- 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 ) {
- $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;
-
- if( $path !== '' ) {
- $info = $this->resolvePath(
- $this->getTreeSha(
- $this->resolve( $sha )
- ),
- $path
- );
- $target = $info['sha'] ?? '';
- }
-
- return $target !== ''
- ? $this->packs->getSize( $target )
- ?: $this->loose->getSize( $target )
- : 0;
- }
-
- 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 {
- return $this->packs->getSize( $sha ) > 0
- ? $this->packs->peek( $sha, $length )
- : $this->loose->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 {
- yield from $this->packWriter->generate(
- $objs
- );
- }
-
- 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;
- }
-
- 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 slurp(
- string $sha,
- callable $callback
- ): void {
- if(
- !$this->loose->stream( $sha, $callback )
- && !$this->packs->stream(
- $sha, $callback
- )
- ) {
- $data = $this->packs->read( $sha );
-
- if( $data !== '' ) {
- $callback( $data );
- }
- }
- }
-
- 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 )
- ) );
- }
- );
- }
- }
-
- 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 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 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 ) {
- $this->parseTreeData(
- $data,
- function(
- $n, $s, $m
- ) use ( &$queue ) {
- if( $m !== '160000' ) {
- $queue[] = [
- 'sha' => $s,
- 'type' => $m === '40000'
- || $m === '040000'
- ? 2
- : 3
- ];
- }
- }
- );
- } 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, ''
- );
+ public function setRepository( string $repoPath ): void {
+ $this->repoPath = \rtrim( $repoPath, '/' );
+ $objPath = $this->repoPath . '/objects';
+ $this->refs = new GitRefs( $this->repoPath );
+ $this->packs = new GitPacks( $objPath );
+ $this->loose = new LooseObjects( $objPath );
+ $this->packWriter = new PackfileWriter(
+ $this->packs, $this->loose
+ );
+ }
+
+ 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 ) {
+ $callback(
+ new Tag( $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 {
+ return $path !== ''
+ ? ( $this->resolvePath(
+ $this->getTreeSha( $this->resolve( $sha ) ),
+ $path
+ )['sha'] ?? '' ) !== ''
+ ? $this->packs->getSize( $this->resolvePath( $this->getTreeSha( $this->resolve( $sha ) ), $path )['sha'] )
+ ?: $this->loose->getSize( $this->resolvePath( $this->getTreeSha( $this->resolve( $sha ) ), $path )['sha'] )
+ : 0
+ : ( $sha !== ''
+ ? $this->packs->getSize( $sha ) ?: $this->loose->getSize( $sha )
+ : 0 );
+ }
+
+ 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 {
+ return $this->packs->getSize( $sha ) > 0
+ ? $this->packs->peek( $sha, $length )
+ : $this->loose->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 {
+ $this->traverseHistory(
+ $this->resolve( $ref ),
+ $limit,
+ $callback,
+ 0
+ );
+ }
+
+ private function traverseHistory(
+ string $sha,
+ int $limit,
+ callable $callback,
+ int $count
+ ): void {
+ $data = $sha !== '' && $count < $limit
+ ? $this->read( $sha )
+ : '';
+
+ if( $data !== '' ) {
+ $commit = new Commit( $sha, $data );
+
+ if( $callback( $commit ) !== false ) {
+ $commit->provideParent(
+ function( $parent ) use ( $limit, $callback, $count ): void {
+ $this->traverseHistory(
+ $parent,
+ $limit,
+ $callback,
+ $count + 1
+ );
+ }
+ );
+ }
+ }
+ }
+
+ public function streamRaw( string $subPath ): bool {
+ return \strpos( $subPath, '..' ) === false
+ && \is_file( "{$this->repoPath}/$subPath" )
+ && \realpath( "{$this->repoPath}/$subPath" ) !== false
+ && \strpos(
+ \realpath( "{$this->repoPath}/$subPath" ),
+ \realpath( $this->repoPath )
+ ) === 0
+ ? $this->sendHeaders( "{$this->repoPath}/$subPath" )
+ : false;
+ }
+
+ private function sendHeaders( string $path ): bool {
+ \header( 'X-Accel-Redirect: ' . $path );
+ \header( 'Content-Type: application/octet-stream' );
+
+ return true;
+ }
+
+ 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 {
+ yield from $this->packWriter->generate( $objs );
+ }
+
+ 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;
+ }
+
+ 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;
+ }
+
+ if(
+ $callback(
+ \substr( $data, $space + 1, $eos - $space - 1 ),
+ \bin2hex( \substr( $data, $eos + 1, 20 ) ),
+ \substr( $data, $pos, $space - $pos )
+ ) === false
+ ) {
+ break;
+ }
+
+ $pos = $eos + 21;
+ }
+ }
+
+ private function slurp( string $sha, callable $callback ): void {
+ if(
+ !$this->loose->stream( $sha, $callback )
+ && !$this->packs->stream( $sha, $callback )
+ ) {
+ $data = $this->packs->read( $sha );
+
+ if( $data !== '' ) {
+ $callback( $data );
+ }
+ }
+ }
+
+ 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 )
+ ) );
+ }
+ );
+ }
+ }
+
+ private function isTreeData( string $data ): bool {
+ $len = \strlen( $data );
+ $match = $len >= 25
+ && \preg_match(
+ '/^(40000|100644|100755|120000|160000) /',
+ $data
+ );
+
+ return $match
+ && \strpos( $data, "\0" ) !== false
+ && \strpos( $data, "\0" ) + 21 <= $len;
+ }
+
+ private function getTreeSha( string $commitOrTreeSha ): string {
+ $data = $this->read( $commitOrTreeSha );
+
+ return \preg_match( '/^object ([0-9a-f]{40})/m', $data, $matches )
+ ? $this->getTreeSha( $matches[1] )
+ : ( \preg_match( '/^tree ([0-9a-f]{40})/m', $data, $matches )
+ ? $matches[1]
+ : $commitOrTreeSha );
+ }
+
+ 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 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 ) {
+ $this->parseTreeData(
+ $data,
+ function( $n, $s, $m ) use ( &$queue ) {
+ if( $m !== '160000' ) {
+ $queue[] = [
+ 'sha' => $s,
+ 'type' => $m === '40000' || $m === '040000' ? 2 : 3
+ ];
+ }
+ }
+ );
+ } elseif( $type === 4 ) {
+ if( \preg_match( '/^object ([0-9a-f]{40})/m', $data, $m ) ) {
+ $queue[] = [
+ 'sha' => $m[1],
+ 'type' => \preg_match( '/^type (commit|tree|blob|tag)/m', $data, $t )
+ ? ([ 'commit' => 1, 'tree' => 2, 'blob' => 3, 'tag' => 4 ][$t[1]] ?? 1)
+ : 1
+ ];
+ }
+ }
+ }
+ }
+
+ return $objs;
+ }
+
+ private function getObjectType( string $data ): int {
+ return \strpos( $data, "tree " ) === 0
+ ? 1
+ : (\strpos( $data, "object " ) === 0
+ ? 4
+ : ($this->isTreeData( $data )
+ ? 2
+ : 3));
+ }
+}
+
+class MissingFile extends File {
+ public function __construct() {
+ parent::__construct( '', '', '0', 0, 0, '' );
}
model/Commit.php
private string $parentSha;
- public function __construct(
- string $sha,
- string $message,
- string $author,
- string $email,
- int $date,
- string $parentSha
- ) {
- $this->sha = $sha;
- $this->message = $message;
- $this->author = $author;
- $this->email = $email;
- $this->date = $date;
- $this->parentSha = $parentSha;
+ public function __construct( string $sha, string $rawData ) {
+ $this->sha = $sha;
+
+ $this->author = \preg_match( '/^author (.*?) </m', $rawData, $m )
+ ? \trim( $m[1] )
+ : 'Unknown';
+
+ $this->email = \preg_match( '/^author .*? <(.*?)>/m', $rawData, $m )
+ ? \trim( $m[1] )
+ : '';
+
+ $this->date = \preg_match( '/^author .*? <.*?> (\d+)/m', $rawData, $m )
+ ? (int)$m[1]
+ : 0;
+
+ $this->parentSha = \preg_match( '/^parent (.*)$/m', $rawData, $m )
+ ? \trim( $m[1] )
+ : '';
+
+ $pos = \strpos( $rawData, "\n\n" );
+
+ $this->message = $pos !== false
+ ? \trim( \substr( $rawData, $pos + 2 ) )
+ : '';
+ }
+
+ public function provideParent( callable $callback ): void {
+ if( $this->parentSha !== '' ) {
+ $callback( $this->parentSha );
+ }
}
model/Tag.php
private string $sha;
private string $targetSha;
- private int $timestamp;
+ private int $timestamp;
private string $message;
private string $author;
public function __construct(
string $name,
string $sha,
- string $targetSha,
- int $timestamp,
- string $message,
- string $author
+ string $rawData
) {
- $this->name = $name;
- $this->sha = $sha;
- $this->targetSha = $targetSha;
- $this->timestamp = $timestamp;
- $this->message = $message;
- $this->author = $author;
+ $this->name = $name;
+ $this->sha = $sha;
+
+ $isAnn = \strncmp( $rawData, 'object ', 7 ) === 0;
+
+ $this->targetSha = $isAnn
+ ? (\preg_match( '/^object (.*)$/m', $rawData, $m )
+ ? \trim( $m[1] )
+ : $sha)
+ : $sha;
+
+ $pattern = $isAnn
+ ? '/^tagger (.*) <(.*)> (\d+) [+\-]\d{4}$/m'
+ : '/^author (.*) <(.*)> (\d+) [+\-]\d{4}$/m';
+
+ $this->author = \preg_match( $pattern, $rawData, $m )
+ ? \trim( $m[1] )
+ : 'Unknown';
+
+ $this->timestamp = \preg_match( $pattern, $rawData, $m )
+ ? (int)$m[3]
+ : 0;
+
+ $pos = \strpos( $rawData, "\n\n" );
+
+ $this->message = $pos !== false
+ ? \trim( \substr( $rawData, $pos + 2 ) )
+ : '';
}
public function compare( Tag $other ): int {
return $other->timestamp <=> $this->timestamp;
}
- public function render( TagRenderer $renderer, ?Tag $prevTag = null ): void {
+ public function render(
+ TagRenderer $renderer,
+ Tag $prevTag
+ ): void {
$renderer->renderTagItem(
$this->name,
$this->sha,
$this->targetSha,
- $prevTag ? $prevTag->targetSha : null,
+ $prevTag->targetSha,
$this->timestamp,
$this->message,
$this->author
);
+ }
+}
+
+class MissingTag extends Tag {
+ public function __construct() {
+ parent::__construct( '', '', '' );
}
}
render/TagRenderer.php
string $sha,
string $targetSha,
- ?string $prevTargetSha,
+ string $prevTargetSha,
int $timestamp,
string $message,
Delta529 lines added, 809 lines removed, 280-line decrease