Dave Jarvis' Repositories

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

Reformats code, fixes null file

Author Dave Jarvis <email>
Date 2026-02-16 12:19:54 GMT-0800
Commit d1eedc6fca8f557347b9cf12fdf14d5ef9299971
Parent 22aaf07
git/Git.php
$this->objectsPath = $this->repoPath . '/objects';
- $this->refs = new GitRefs( $this->repoPath );
- $this->packs = new GitPacks( $this->objectsPath );
- }
-
- 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 );
-
- if( $sha === '' ) return;
-
- $treeSha = $this->getTreeSha( $sha );
-
- if( $path !== '' ) {
- $info = $this->resolvePath( $treeSha, $path );
-
- if( !$info || !$info['isDir'] ) {
- return;
- }
-
- $treeSha = $info['sha'];
- }
-
- if( $treeSha ) {
- $this->walkTree( $treeSha, $callback );
- }
- }
-
- public function readFile( string $ref, string $path ): ?File {
- $sha = $this->resolve( $ref );
- if( $sha === '' ) return null;
-
- $treeSha = $this->getTreeSha( $sha );
- $info = $this->resolvePath( $treeSha, $path );
-
- if( !$info ) {
- return null;
- }
-
- $size = $this->getObjectSize( $info['sha'] );
- $content = $info['isDir'] ? '' : $this->peek( $info['sha'] );
-
- return new File(
- basename( $path ),
- $info['sha'],
- $info['mode'],
- 0,
- $size,
- $content
- );
- }
-
- public function getObjectSize( string $sha, string $path = '' ): int {
- if( $path !== '' ) {
- $rootSha = $this->resolve( $sha );
- $treeSha = $this->getTreeSha( $rootSha );
- $info = $this->resolvePath( $treeSha, $path );
- $sha = $info ? $info['sha'] : '';
- }
-
- if( $sha === '' ) return 0;
-
- $size = $this->packs->getSize( $sha );
- return $size !== null ? $size : $this->getLooseObjectSize( $sha );
- }
-
- public function stream( string $sha, callable $callback, string $path = '' ): void {
- if( $path !== '' ) {
- $rootSha = $this->resolve( $sha );
- $treeSha = $this->getTreeSha( $rootSha );
- $info = $this->resolvePath( $treeSha, $path );
- $sha = $info ? $info['sha'] : '';
- }
-
- if( $sha !== '' ) {
- $this->slurp( $sha, $callback );
- }
- }
-
- private function getTreeSha( string $commitOrTreeSha ): string {
- $data = $this->read( $commitOrTreeSha );
-
- if( preg_match( '/^object ([0-9a-f]{40})/m', $data, $matches ) ) {
- return $this->getTreeSha( $matches[1] );
- }
-
- if( preg_match( '/^tree ([0-9a-f]{40})/m', $data, $matches ) ) {
- return $matches[1];
- }
-
- return $commitOrTreeSha;
- }
-
- private function resolvePath( string $treeSha, string $path ): ?array {
- $parts = explode( '/', trim( $path, '/' ) );
- $currentSha = $treeSha;
- $currentMode = '40000';
-
- foreach( $parts as $part ) {
- if( $part === '' ) continue;
-
- $entry = $this->findTreeEntry( $currentSha, $part );
-
- if( !$entry ) {
- return null;
- }
-
- $currentSha = $entry['sha'];
- $currentMode = $entry['mode'];
- }
-
- $isDir = $currentMode === '40000' || $currentMode === '040000';
-
- return [
- 'sha' => $currentSha,
- 'mode' => $currentMode,
- 'isDir' => $isDir
- ];
- }
-
- private function findTreeEntry( string $treeSha, string $name ): ?array {
- $data = $this->read( $treeSha );
- $position = 0;
- $length = strlen( $data );
-
- while( $position < $length ) {
- $spacePos = strpos( $data, ' ', $position );
- $nullPos = strpos( $data, "\0", $spacePos );
-
- if( $spacePos === false || $nullPos === false ) break;
-
- $entryName = substr( $data, $spacePos + 1, $nullPos - $spacePos - 1 );
-
- if( $entryName === $name ) {
- $mode = substr( $data, $position, $spacePos - $position );
- $sha = bin2hex( substr( $data, $nullPos + 1, 20 ) );
- return ['sha' => $sha, 'mode' => $mode];
- }
-
- $position = $nullPos + 21;
- }
-
- return null;
- }
-
- private function parseTagData(
- string $name,
- string $sha,
- string $data
- ): Tag {
- $isAnnotated = strncmp( $data, 'object ', 7 ) === 0;
-
- $targetSha = $isAnnotated
- ? $this->extractPattern(
- $data,
- '/^object ([0-9a-f]{40})$/m',
- 1,
- $sha
- )
- : $sha;
-
- $pattern = $isAnnotated
- ? '/^tagger (.*) <(.*)> (\d+) [+\-]\d{4}$/m'
- : '/^author (.*) <(.*)> (\d+) [+\-]\d{4}$/m';
-
- $identity = $this->parseIdentity( $data, $pattern );
- $message = $this->extractMessage( $data );
-
- return new Tag(
- $name,
- $sha,
- $targetSha,
- $identity['timestamp'],
- $message,
- $identity['name']
- );
- }
-
- private function extractPattern(
- string $data,
- string $pattern,
- int $group,
- string $default = ''
- ): string {
- $matches = [];
-
- $result = preg_match( $pattern, $data, $matches )
- ? $matches[$group]
- : $default;
-
- return $result;
- }
-
- private function parseIdentity( string $data, string $pattern ): array {
- $matches = [];
- $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 ) ) : '';
- }
-
- public function peek( string $sha, int $length = 255 ): string {
- $size = $this->packs->getSize( $sha );
-
- return $size === null
- ? $this->peekLooseObject( $sha, $length )
- : $this->packs->peek( $sha, $length ) ?? '';
- }
-
- public function read( string $sha ): string {
- $size = $this->getObjectSize( $sha );
-
- if( $size > self::MAX_READ_SIZE ) {
- return '';
- }
-
- $content = '';
-
- $this->slurp( $sha, function( $chunk ) use ( &$content ) {
- $content .= $chunk;
- } );
-
- return $content;
- }
-
- private function slurp( string $sha, callable $callback ): void {
- $loosePath = $this->getLoosePath( $sha );
-
- if( is_file( $loosePath ) ) {
- $this->slurpLooseObject( $loosePath, $callback );
- } else {
- $this->slurpPackedObject( $sha, $callback );
- }
- }
-
- private function iterateInflated( string $path, callable $processor ): void {
- $this->withInflatedFile(
- $path,
- function( $fileHandle, $inflator ) use ( $processor ) {
- $headerFound = false;
- $buffer = '';
-
- while( !feof( $fileHandle ) ) {
- $chunk = fread( $fileHandle, 16384 );
- $inflated = inflate_add( $inflator, $chunk );
-
- if( $inflated === false ) {
- break;
- }
-
- if( !$headerFound ) {
- $buffer .= $inflated;
- $nullPos = strpos( $buffer, "\0" );
-
- if( $nullPos !== false ) {
- $headerFound = true;
- $header = substr( $buffer, 0, $nullPos );
- $body = substr( $buffer, $nullPos + 1 );
-
- if( $processor( $body, $header ) === false ) {
- return;
- }
- }
- } else {
- if( $processor( $inflated, null ) === false ) {
- return;
- }
- }
- }
- }
- );
- }
-
- private function slurpLooseObject(
- string $path,
- callable $callback
- ): void {
- $this->iterateInflated(
- $path,
- function( $chunk ) use ( $callback ) {
- if( $chunk !== '' ) {
- $callback( $chunk );
- }
-
- return true;
- }
- );
- }
-
- private function withInflatedFile( string $path, callable $callback ): void {
- $fileHandle = fopen( $path, 'rb' );
- $inflator = $fileHandle ? inflate_init( ZLIB_ENCODING_DEFLATE ) : null;
-
- if( $fileHandle && $inflator ) {
- $callback( $fileHandle, $inflator );
- fclose( $fileHandle );
- }
- }
-
- private function slurpPackedObject(
- string $sha,
- callable $callback
- ): void {
- $streamed = $this->packs->stream( $sha, $callback );
-
- if( !$streamed ) {
- $data = $this->packs->read( $sha );
-
- if( $data !== null && $data !== '' ) {
- $callback( $data );
- }
- }
- }
-
- private function peekLooseObject( string $sha, int $length ): string {
- $path = $this->getLoosePath( $sha );
-
- return is_file( $path )
- ? $this->inflateLooseObjectPrefix( $path, $length )
- : '';
- }
-
- private function inflateLooseObjectPrefix(
- string $path,
- int $length
- ): string {
- $buffer = '';
-
- $this->iterateInflated(
- $path,
- function( $chunk ) use ( $length, &$buffer ) {
- $buffer .= $chunk;
- return strlen( $buffer ) < $length;
- }
- );
-
- return substr( $buffer, 0, $length );
- }
-
- public function history( string $ref, int $limit, callable $callback ): void {
- $currentSha = $this->resolve( $ref );
- $count = 0;
-
- while( $currentSha !== '' && $count < $limit ) {
- $commit = $this->parseCommit( $currentSha );
-
- if( $commit === null ) {
- break;
- }
-
- $callback( $commit );
- $currentSha = $commit->parentSha;
- $count++;
- }
- }
-
- private function parseCommit( string $sha ): ?object {
- $data = $this->read( $sha );
-
- return $data === '' ? null : $this->buildCommitObject( $sha, $data );
- }
-
- private function buildCommitObject( string $sha, string $data ): object {
- $identity = $this->parseIdentity( $data, '/^author (.*) <(.*)> (\d+)/m' );
- $message = $this->extractMessage( $data );
- $parentSha = $this->extractPattern(
- $data,
- '/^parent ([0-9a-f]{40})$/m',
- 1
- );
-
- return (object)[
- 'sha' => $sha,
- 'message' => $message,
- 'author' => $identity['name'],
- 'email' => $identity['email'],
- 'date' => $identity['timestamp'],
- 'parentSha' => $parentSha
- ];
- }
-
- private function walkTree( string $sha, callable $callback ): void {
- $data = $this->read( $sha );
- $treeData = $data !== '' && preg_match(
- '/^tree ([0-9a-f]{40})$/m',
- $data,
- $matches
- ) ? $this->read( $matches[1] ) : $data;
-
- if( $treeData !== '' && $this->isTreeData( $treeData ) ) {
- $this->processTree( $treeData, $callback );
- }
- }
-
- private function processTree( string $data, callable $callback ): void {
- $position = 0;
- $length = strlen( $data );
-
- while( $position < $length ) {
- $result = $this->parseTreeEntry( $data, $position, $length );
-
- if( $result === null ) {
- break;
- }
-
- $callback( $result['file'] );
- $position = $result['nextPosition'];
- }
- }
-
- private function parseTreeEntry(
- string $data,
- int $position,
- int $length
- ): ?array {
- $spacePos = strpos( $data, ' ', $position );
- $nullPos = strpos( $data, "\0", $spacePos );
-
- $hasValidPositions =
- $spacePos !== false &&
- $nullPos !== false &&
- $nullPos + 21 <= $length;
-
- return $hasValidPositions
- ? $this->buildTreeEntryResult( $data, $position, $spacePos, $nullPos )
- : null;
- }
-
- private function buildTreeEntryResult(
- string $data,
- int $position,
- int $spacePos,
- int $nullPos
- ): array {
- $mode = substr( $data, $position, $spacePos - $position );
- $name = substr( $data, $spacePos + 1, $nullPos - $spacePos - 1 );
- $sha = bin2hex( substr( $data, $nullPos + 1, 20 ) );
-
- $isDirectory = $mode === '40000' || $mode === '040000';
- $size = $isDirectory ? 0 : $this->getObjectSize( $sha );
- $contents = $isDirectory ? '' : $this->peek( $sha );
-
- $file = new File( $name, $sha, $mode, 0, $size, $contents );
-
- return [
- 'file' => $file,
- 'nextPosition' => $nullPos + 21
- ];
- }
-
- private function isTreeData( string $data ): bool {
- $pattern = '/^(40000|100644|100755|120000|160000) /';
- $minLength = strlen( $data ) >= 25;
- $matchesPattern = $minLength && preg_match( $pattern, $data );
- $nullPos = $matchesPattern ? strpos( $data, "\0" ) : false;
-
- return $matchesPattern &&
- $nullPos !== false &&
- $nullPos + 21 <= strlen( $data );
- }
-
- private function getLoosePath( string $sha ): string {
- return "{$this->objectsPath}/" .
- substr( $sha, 0, 2 ) . "/" .
- substr( $sha, 2 );
- }
-
- private function getLooseObjectSize( string $sha ): int {
- $path = $this->getLoosePath( $sha );
-
- return is_file( $path ) ? $this->readLooseObjectHeader( $path ) : 0;
- }
-
- private function readLooseObjectHeader( string $path ): int {
- $size = 0;
-
- $this->iterateInflated(
- $path,
- function( $chunk, $header ) use ( &$size ) {
- if( $header !== null ) {
- $parts = explode( ' ', $header );
- $size = isset( $parts[1] ) ? (int)$parts[1] : 0;
- }
- return false;
- }
- );
-
- return $size;
- }
-
- public function streamRaw( string $subPath ): bool {
- return strpos( $subPath, '..' ) === false
- ? $this->streamRawFile( $subPath )
- : false;
- }
-
- private function streamRawFile( string $subPath ): bool {
- $fullPath = "{$this->repoPath}/$subPath";
-
- return is_file( $fullPath )
- ? $this->streamIfPathValid( $fullPath )
- : false;
- }
-
- private function streamIfPathValid( string $fullPath ): bool {
- $realPath = realpath( $fullPath );
- $repoReal = realpath( $this->repoPath );
- $isValid = $realPath && strpos( $realPath, $repoReal ) === 0;
-
- return $isValid ? readfile( $fullPath ) !== false : false;
- }
-
- public function eachRef( callable $callback ): void {
- $head = $this->resolve( 'HEAD' );
-
- if( $head !== '' ) {
- $callback( 'HEAD', $head );
- }
-
- $this->refs->scanRefs(
- 'refs/heads',
- function( $name, $sha ) use ( $callback ) {
- $callback( "refs/heads/$name", $sha );
- }
- );
-
- $this->refs->scanRefs(
- 'refs/tags',
- function( $name, $sha ) use ( $callback ) {
- $callback( "refs/tags/$name", $sha );
- }
- );
- }
-
- public function collectObjects( array $wants, array $haves = [] ): array {
- $objects = [];
- $visited = [];
-
- foreach( $wants as $sha ) {
- $this->collectObjectsRecursive( $sha, $objects, $visited );
- }
-
- foreach( $haves as $sha ) {
- if( isset( $objects[$sha] ) ) {
- unset( $objects[$sha] );
- }
- }
-
- return $objects;
- }
-
- private function collectObjectsRecursive(
- string $sha,
- array &$objects,
- array &$visited
- ): void {
- if( isset( $visited[$sha] ) ) return;
- $visited[$sha] = true;
-
- $data = $this->read( $sha );
- $type = $this->getObjectType( $data );
- $objects[$sha] = ['type' => $type, 'size' => strlen( $data )];
-
- if( $type === 1 ) { // Commit
- if( preg_match( '/^tree ([0-9a-f]{40})/m', $data, $matches ) ) {
- $this->collectObjectsRecursive( $matches[1], $objects, $visited );
- }
- if( preg_match( '/^parent ([0-9a-f]{40})/m', $data, $matches ) ) {
- $this->collectObjectsRecursive( $matches[1], $objects, $visited );
- }
- } elseif( $type === 2 ) { // Tree
- $position = 0;
- $length = strlen( $data );
- while( $position < $length ) {
- $spacePos = strpos( $data, ' ', $position );
- $nullPos = strpos( $data, "\0", $spacePos );
- if( $spacePos === false || $nullPos === false ) break;
-
- $entrySha = bin2hex( substr( $data, $nullPos + 1, 20 ) );
- $this->collectObjectsRecursive( $entrySha, $objects, $visited );
- $position = $nullPos + 21;
- }
- } elseif( $type === 4 ) { // Tag
- if( preg_match( '/^object ([0-9a-f]{40})/m', $data, $matches ) ) {
- $this->collectObjectsRecursive( $matches[1], $objects, $visited );
- }
- }
- }
-
- private function getObjectType( string $data ): int {
- if( strpos( $data, "tree " ) === 0 ) {
- return 1;
- }
-
- if( $this->isTreeData( $data ) ) {
- return 2;
- }
-
- if( strpos( $data, "object " ) === 0 ) {
- return 4;
- }
-
- return 3;
- }
-
- public function generatePackfile( array $objects ): string {
- if( empty( $objects ) ) {
- $pack = "PACK" . pack( 'N', 2 ) . pack( 'N', 0 );
- return $pack . hash( 'sha1', $pack, true );
- }
-
- $packObjects = '';
-
- foreach( $objects as $sha => $info ) {
- $content = $this->read( $sha );
- $type = $info['type'];
- $size = strlen( $content );
- $byte = ($type << 4) | ($size & 0x0f);
- $size >>= 4;
-
- while( $size > 0 ) {
- $packObjects .= chr( $byte | 0x80 );
- $byte = $size & 0x7f;
- $size >>= 7;
- }
-
- $packObjects .= chr( $byte );
- $packObjects .= gzcompress( $content );
- }
-
- $objectCount = count( $objects );
- $header = "PACK" . pack( 'N', 2 ) . pack( 'N', $objectCount );
- $packData = $header . $packObjects;
-
- $checksum = hash( 'sha1', $packData, true );
- return $packData . $checksum;
+ $this->refs = new GitRefs( $this->repoPath );
+ $this->packs = new GitPacks( $this->objectsPath );
+ }
+
+ 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;
+
+ if( $path !== '' ) {
+ $info = $this->resolvePath(
+ $this->getTreeSha( $this->resolve( $sha ) ),
+ $path
+ );
+ $target = $info['sha'] ?? '';
+ }
+
+ return $target !== ''
+ ? $this->packs->getSize( $target ) ?? $this->getLooseObjectSize( $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 );
+ }
+ }
+
+ 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 {
+ $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 ) ) : '';
+ }
+
+ public function peek( string $sha, int $length = 255 ): string {
+ $size = $this->packs->getSize( $sha );
+
+ return $size === null
+ ? $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_SIZE ) {
+ $this->slurp( $sha, function( $chunk ) use ( &$content ) {
+ $content .= $chunk;
+ } );
+ }
+
+ return $content;
+ }
+
+ private function slurp( string $sha, callable $callback ): void {
+ $path = $this->getLoosePath( $sha );
+
+ if( is_file($path) ) {
+ $this->slurpLooseObject( $path, $callback );
+ }
+
+ if( !is_file($path) ) {
+ $this->slurpPackedObject( $sha, $callback );
+ }
+ }
+
+ private function iterateInflated( string $path, callable $processor ): void {
+ $this->withInflatedFile(
+ $path,
+ function( $handle, $inflator ) use ( $processor ) {
+ $found = false;
+ $buffer = '';
+
+ while( !feof($handle) ) {
+ $inflated = inflate_add( $inflator, fread( $handle, 16384 ) );
+
+ 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;
+ }
+ }
+ }
+
+ if( $found ) {
+ if( $processor( $inflated, null ) === false ) {
+ break;
+ }
+ }
+ }
+ }
+ );
+ }
+
+ private function slurpLooseObject(
+ string $path,
+ callable $callback
+ ): void {
+ $this->iterateInflated(
+ $path,
+ function( $chunk ) use ( $callback ) {
+ if( $chunk !== '' ) {
+ $callback( $chunk );
+ }
+
+ return true;
+ }
+ );
+ }
+
+ private function withInflatedFile( string $path, callable $callback ): void {
+ $handle = fopen( $path, 'rb' );
+ $infl = $handle ? inflate_init( ZLIB_ENCODING_DEFLATE ) : null;
+
+ if( $handle && $infl ) {
+ $callback( $handle, $infl );
+ fclose( $handle );
+ }
+ }
+
+ private function slurpPackedObject( string $sha, callable $callback ): void {
+ $streamed = $this->packs->stream( $sha, $callback );
+
+ if( !$streamed ) {
+ $data = $this->packs->read( $sha );
+
+ if( $data !== null && $data !== '' ) {
+ $callback( $data );
+ }
+ }
+ }
+
+ private function peekLooseObject( string $sha, int $length ): string {
+ $path = $this->getLoosePath( $sha );
+
+ return is_file($path)
+ ? $this->inflateLooseObjectPrefix( $path, $length )
+ : '';
+ }
+
+ private function inflateLooseObjectPrefix(
+ string $path,
+ int $length
+ ): string {
+ $buf = '';
+
+ $this->iterateInflated(
+ $path,
+ function( $chunk ) use ( $length, &$buf ) {
+ $buf .= $chunk;
+
+ return strlen($buf) < $length;
+ }
+ );
+
+ return substr( $buf, 0, $length );
+ }
+
+ 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++;
+ }
+ }
+ }
+
+ private function parseCommit( string $sha ): object {
+ $data = $this->read( $sha );
+
+ return $data !== ''
+ ? $this->buildCommitObject( $sha, $data )
+ : (object)[ 'sha' => '' ];
+ }
+
+ private function buildCommitObject( string $sha, string $data ): object {
+ $id = $this->parseIdentity( $data, '/^author (.*) <(.*)> (\d+)/m' );
+
+ return (object)[
+ 'sha' => $sha,
+ 'message' => $this->extractMessage( $data ),
+ 'author' => $id['name'],
+ 'email' => $id['email'],
+ 'date' => $id['timestamp'],
+ 'parentSha' => $this->extractPattern( $data, '/^parent (.*)$/m', 1 )
+ ];
+ }
+
+ 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->processTree( $tree, $callback );
+ }
+ }
+
+ private function processTree( string $data, callable $callback ): void {
+ $pos = 0;
+ $len = strlen( $data );
+
+ while( $pos < $len ) {
+ $entry = $this->parseTreeEntry( $data, $pos, $len );
+
+ if( $entry === null ) {
+ break;
+ }
+
+ $callback( $entry['file'] );
+ $pos = $entry['nextPosition'];
+ }
+ }
+
+ private function parseTreeEntry(
+ string $data,
+ int $pos,
+ int $len
+ ): ?array {
+ $space = strpos( $data, ' ', $pos );
+ $eos = strpos( $data, "\0", $space );
+
+ return $space !== false && $eos !== false && $eos + 21 <= $len
+ ? $this->buildTreeEntryResult( $data, $pos, $space, $eos )
+ : null;
+ }
+
+ private function buildTreeEntryResult(
+ string $data,
+ int $pos,
+ int $space,
+ int $eos
+ ): array {
+ $mode = substr( $data, $pos, $space - $pos );
+ $sha = bin2hex( substr( $data, $eos + 1, 20 ) );
+ $isD = $mode === '40000' || $mode === '040000';
+
+ return [
+ 'file' => new File(
+ substr( $data, $space + 1, $eos - $space - 1 ),
+ $sha,
+ $mode,
+ 0,
+ $isD ? 0 : $this->getObjectSize( $sha ),
+ $isD ? '' : $this->peek( $sha )
+ ),
+ 'nextPosition' => $eos + 21
+ ];
+ }
+
+ 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->objectsPath}/" .
+ substr( $sha, 0, 2 ) . "/" .
+ substr( $sha, 2 );
+ }
+
+ private function getLooseObjectSize( string $sha ): int {
+ $path = $this->getLoosePath( $sha );
+
+ return is_file($path) ? $this->readLooseObjectHeader($path) : 0;
+ }
+
+ private function readLooseObjectHeader( string $path ): int {
+ $size = 0;
+
+ $this->iterateInflated( $path, function( $chunk, $header ) use ( &$size ) {
+ if( $header !== null ) {
+ $parts = explode( ' ', $header );
+ $size = isset( $parts[1] ) ? (int)$parts[1] : 0;
+ }
+
+ return false;
+ } );
+
+ return $size;
+ }
+
+ public function streamRaw( string $subPath ): bool {
+ return strpos($subPath, '..') === false
+ ? $this->streamRawFile( $subPath )
+ : false;
+ }
+
+ private function streamRawFile( string $subPath ): bool {
+ $path = "{$this->repoPath}/$subPath";
+
+ return is_file($path) ? $this->streamIfPathValid($path) : false;
+ }
+
+ private function streamIfPathValid( string $fullPath ): bool {
+ $real = realpath( $fullPath );
+ $repo = realpath( $this->repoPath );
+ $isValid = $real && strpos($real, $repo) === 0;
+
+ return $isValid ? readfile($fullPath) !== false : false;
+ }
+
+ 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 collectObjects( array $wants, array $haves = [] ): array {
+ $objs = [];
+ $seen = [];
+
+ foreach( $wants as $sha ) {
+ $this->collectObjectsRecursive( $sha, $objs, $seen );
+ }
+
+ foreach( $haves as $sha ) {
+ unset($objs[$sha]);
+ }
+
+ return $objs;
+ }
+
+ private function collectObjectsRecursive(
+ string $sha,
+ array &$objs,
+ array &$seen
+ ): void {
+ if( !isset( $seen[$sha] ) ) {
+ $seen[$sha] = true;
+ $data = $this->read( $sha );
+ $type = $this->getObjectType( $data );
+ $objs[$sha] = [ 'type' => $type, 'size' => strlen($data) ];
+
+ if( $type === 1 ) {
+ $this->collectCommitLinks( $data, $objs, $seen );
+ }
+
+ if( $type === 2 ) {
+ $this->collectTreeLinks( $data, $objs, $seen );
+ }
+
+ if( $type === 4 && preg_match( '/^object (.*)$/m', $data, $m ) ) {
+ $this->collectObjectsRecursive( $m[1], $objs, $seen );
+ }
+ }
+ }
+
+ private function collectCommitLinks( $data, &$objs, &$seen ): void {
+ if( preg_match( '/^tree (.*)$/m', $data, $m ) ) {
+ $this->collectObjectsRecursive( $m[1], $objs, $seen );
+ }
+
+ if( preg_match( '/^parent (.*)$/m', $data, $m ) ) {
+ $this->collectObjectsRecursive( $m[1], $objs, $seen );
+ }
+ }
+
+ private function collectTreeLinks( $data, &$objs, &$seen ): void {
+ $pos = 0;
+ $len = strlen( $data );
+
+ while( $pos < $len ) {
+ $space = strpos( $data, ' ', $pos );
+ $eos = strpos( $data, "\0", $space );
+
+ if( $space === false || $eos === false ) {
+ break;
+ }
+
+ $sha = bin2hex( substr( $data, $eos + 1, 20 ) );
+ $this->collectObjectsRecursive( $sha, $objs, $seen );
+ $pos = $eos + 21;
+ }
+ }
+
+ private function getObjectType( string $data ): int {
+ $isTree = strpos($data, "tree ") === 0;
+ $isObj = strpos($data, "object ") === 0;
+
+ return $isTree
+ ? 1
+ : ( $this->isTreeData($data) ? 2 : ( $isObj ? 4 : 3 ) );
+ }
+
+ public function generatePackfile( array $objs ): string {
+ $pData = '';
+
+ if( empty($objs) ) {
+ $pData = "PACK" . pack( 'N', 2 ) . pack( 'N', 0 );
+ }
+
+ if( !empty($objs) ) {
+ $data = '';
+
+ foreach( $objs as $sha => $info ) {
+ $cont = $this->read( $sha );
+ $size = strlen($cont);
+ $byte = $info['type'] << 4 | $size & 0x0f;
+ $size >>= 4;
+
+ while( $size > 0 ) {
+ $data .= chr( $byte | 0x80 );
+ $byte = $size & 0x7f;
+ $size >>= 7;
+ }
+
+ $data .= chr( $byte ) . gzcompress( $cont );
+ }
+
+ $pData = "PACK" . pack( 'N', 2 ) . pack( 'N', count($objs) ) . $data;
+ }
+
+ return $pData . hash( 'sha1', $pData, true );
+ }
+}
+
+class MissingFile extends File {
+ public function __construct() {
+ parent::__construct( '', '', '0', 0, 0, '' );
+ }
+
+ public function emitRawHeaders(): void {
+ header( "HTTP/1.1 404 Not Found" );
+ exit;
}
}
pages/RawPage.php
public function __construct( $git, $hash ) {
- $this->git = $git;
+ $this->git = $git;
$this->hash = $hash;
}
public function render() {
- $filename = basename( $_GET['name'] ?? '' ) ?: 'file';
- $file = $this->git->readFile( $this->hash, $filename );
+ $name = $_GET['name'] ?? '';
+ $file = $this->git->readFile( $this->hash, $name );
while( ob_get_level() ) {
ob_end_clean();
}
$file->emitRawHeaders();
$this->git->stream( $this->hash, function( $d ) {
echo $d;
- } );
+ }, $name );
exit;
Delta 655 lines added, 670 lines removed, 15-line decrease