| | |
| | 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 ); |
| | - } ); |
| | - } |
| | - |
| | - 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'; |
| | - |
| | - list( $author, $timestamp ) = $this->extractAuthorAndTime( $data, $pattern ); |
| | - $message = $this->extractMessage( $data ); |
| | - |
| | - return new Tag( $name, $sha, $targetSha, $timestamp, $message, $author ); |
| | - } |
| | - |
| | - 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 extractAuthorAndTime( string $data, string $pattern ): array { |
| | - $matches = []; |
| | - $found = preg_match( $pattern, $data, $matches ); |
| | - $author = $found ? trim( $matches[1] ) : ''; |
| | - $timestamp = $found && isset( $matches[3] ) ? (int)$matches[3] : ( $found && isset( $matches[2] ) ? (int)$matches[2] : 0 ); |
| | - return [ $author, $timestamp ]; |
| | - } |
| | - |
| | - private function extractMessage( string $data ): string { |
| | - $pos = strpos( $data, "\n\n" ); |
| | - $message = ( $pos !== false ) ? trim( substr( $data, $pos + 2 ) ) : ''; |
| | - return $message; |
| | - } |
| | - |
| | - public function getObjectSize( string $sha ): int { |
| | - $size = $this->packs->getSize( $sha ); |
| | - $result = ( $size !== null ) ? $size : $this->getLooseObjectSize( $sha ); |
| | - return $result; |
| | - } |
| | - |
| | - public function peek( string $sha, int $length = 255 ): string { |
| | - $size = $this->packs->getSize( $sha ); |
| | - $result = ( $size === null ) |
| | - ? $this->peekLooseObject( $sha, $length ) |
| | - : ( $this->packs->peek( $sha, $length ) ?? '' ); |
| | - return $result; |
| | - } |
| | - |
| | - 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; |
| | - } |
| | - |
| | - public function readFile( string $hash, string $name ) { |
| | - return new File( |
| | - $name, |
| | - $hash, |
| | - '100644', |
| | - 0, |
| | - $this->getObjectSize( $hash ), |
| | - $this->peek( $hash ) |
| | - ); |
| | - } |
| | - |
| | - public function stream( string $sha, callable $callback ): void { |
| | - $this->slurp( $sha, $callback ); |
| | - } |
| | - |
| | - 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 slurpLooseObject( string $path, callable $callback ): void { |
| | - $this->withInflatedFile( $path, function( $fileHandle, $inflator ) use ( $callback ) { |
| | - $buffer = ''; |
| | - $headerFound = false; |
| | - |
| | - while( !feof( $fileHandle ) ) { |
| | - $chunk = fread( $fileHandle, 16384 ); |
| | - $inflatedChunk = inflate_add( $inflator, $chunk ); |
| | - |
| | - if( $inflatedChunk === false ) break; |
| | - |
| | - $headerFound = $this->processInflatedChunk( |
| | - $inflatedChunk, |
| | - $headerFound, |
| | - $buffer, |
| | - $callback |
| | - ); |
| | - } |
| | - } ); |
| | - } |
| | - |
| | - 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 processInflatedChunk( |
| | - string $chunk, |
| | - bool $headerFound, |
| | - string &$buffer, |
| | - callable $callback |
| | - ): bool { |
| | - if( !$headerFound ) { |
| | - $buffer .= $chunk; |
| | - $nullPos = strpos( $buffer, "\0" ); |
| | - |
| | - if( $nullPos !== false ) { |
| | - $body = substr( $buffer, $nullPos + 1 ); |
| | - |
| | - if( $body !== '' ) { |
| | - $callback( $body ); |
| | - } |
| | - |
| | - $buffer = ''; |
| | - return true; |
| | - } |
| | - } else { |
| | - $callback( $chunk ); |
| | - } |
| | - |
| | - return $headerFound; |
| | - } |
| | - |
| | - 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 ); |
| | - $buffer = ''; |
| | - |
| | - if( is_file( $path ) ) { |
| | - $buffer = $this->inflateLooseObjectPrefix( $path, $length ); |
| | - } |
| | - |
| | - return $buffer; |
| | - } |
| | - |
| | - private function inflateLooseObjectPrefix( string $path, int $length ): string { |
| | - $buffer = ''; |
| | - |
| | - $this->withInflatedFile( $path, function( $fileHandle, $inflator ) use ( $length, &$buffer ) { |
| | - $headerFound = false; |
| | - |
| | - while( !feof( $fileHandle ) && strlen( $buffer ) < $length ) { |
| | - $chunk = fread( $fileHandle, 128 ); |
| | - $inflated = inflate_add( $inflator, $chunk ); |
| | - |
| | - if( $inflated === false ) { |
| | - break; |
| | - } |
| | - |
| | - $headerFound = $this->appendPrefixChunk( $inflated, $headerFound, $buffer ); |
| | - } |
| | - |
| | - $buffer = substr( $buffer, 0, $length ); |
| | - } ); |
| | - |
| | - return $buffer; |
| | - } |
| | - |
| | - private function appendPrefixChunk( |
| | - string $chunk, |
| | - bool $headerFound, |
| | - string &$buffer |
| | - ): bool { |
| | - if( !$headerFound ) { |
| | - $nullPos = strpos( $chunk, "\0" ); |
| | - |
| | - if( $nullPos !== false ) { |
| | - $buffer .= substr( $chunk, $nullPos + 1 ); |
| | - return true; |
| | - } |
| | - } else { |
| | - $buffer .= $chunk; |
| | - } |
| | - |
| | - return $headerFound; |
| | - } |
| | - |
| | - 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 ); |
| | - $result = ( $data === '' ) ? null : $this->buildCommitObject( $sha, $data ); |
| | - return $result; |
| | - } |
| | - |
| | - private function buildCommitObject( string $sha, string $data ): object { |
| | - list( $author, $timestamp ) = $this->extractAuthorAndTime( $data, '/^author (.*) <(.*)> (\d+)/m' ); |
| | - $email = $this->extractPattern( $data, '/^author .* <(.*)> \d+/m', 1 ); |
| | - $message = $this->extractMessage( $data ); |
| | - $parentSha = $this->extractPattern( $data, '/^parent ([0-9a-f]{40})$/m', 1 ); |
| | - |
| | - return (object)[ |
| | - 'sha' => $sha, |
| | - 'message' => $message, |
| | - 'author' => $author ?: 'Unknown', |
| | - 'email' => $email, |
| | - 'date' => $timestamp, |
| | - 'parentSha' => $parentSha |
| | - ]; |
| | - } |
| | - |
| | - public function walk( string $refOrSha, callable $callback ): void { |
| | - $sha = $this->resolve( $refOrSha ); |
| | - |
| | - if( $sha !== '' ) { |
| | - $this->walkTree( $sha, $callback ); |
| | - } |
| | - } |
| | - |
| | - 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 ); |
| | - $result = $hasValidPositions ? $this->buildTreeEntryResult( $data, $position, $spacePos, $nullPos ) : null; |
| | - return $result; |
| | - } |
| | - |
| | - 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; |
| | - $result = $matchesPattern && $nullPos !== false && ( $nullPos + 21 <= strlen( $data ) ); |
| | - return $result; |
| | - } |
| | - |
| | - 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 ); |
| | - $size = is_file( $path ) ? $this->readLooseObjectHeader( $path ) : 0; |
| | - return $size; |
| | - } |
| | - |
| | - private function readLooseObjectHeader( string $path ): int { |
| | - $size = 0; |
| | - |
| | - $this->withInflatedFile( $path, function( $fileHandle, $inflator ) use ( &$size ) { |
| | - $data = ''; |
| | - |
| | - while( !feof( $fileHandle ) ) { |
| | - $chunk = fread( $fileHandle, self::CHUNK_SIZE ); |
| | - $output = inflate_add( $inflator, $chunk, ZLIB_NO_FLUSH ); |
| | - |
| | - if( $output === false ) { |
| | - break; |
| | - } |
| | - |
| | - $data .= $output; |
| | - |
| | - if( strpos( $data, "\0" ) !== false ) { |
| | - break; |
| | - } |
| | - } |
| | - |
| | - $size = $this->parseSizeFromHeader( $data ); |
| | - } ); |
| | - |
| | - return $size; |
| | - } |
| | - |
| | - private function parseSizeFromHeader( string $data ): int { |
| | - $header = explode( "\0", $data, 2 )[0]; |
| | - $parts = explode( ' ', $header ); |
| | - $size = isset( $parts[1] ) ? (int)$parts[1] : 0; |
| | - return $size; |
| | - } |
| | - |
| | - public function streamRaw( string $subPath ): bool { |
| | - $result = false; |
| | - |
| | - if( strpos( $subPath, '..' ) === false ) { |
| | - $result = $this->streamRawFile( $subPath ); |
| | - } |
| | - |
| | - return $result; |
| | - } |
| | - |
| | - private function streamRawFile( string $subPath ): bool { |
| | - $fullPath = "{$this->repoPath}/$subPath"; |
| | - $result = file_exists( $fullPath ) ? $this->streamIfPathValid( $fullPath ) : false; |
| | - return $result; |
| | - } |
| | - |
| | - private function streamIfPathValid( string $fullPath ): bool { |
| | - $realPath = realpath( $fullPath ); |
| | - $repoReal = realpath( $this->repoPath ); |
| | - $isValid = ( $realPath && strpos( $realPath, $repoReal ) === 0 ); |
| | - |
| | - if( $isValid ) { |
| | - readfile( $fullPath ); |
| | - } |
| | - |
| | - return $isValid; |
| | - } |
| | - |
| | - 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 ) { |
| | + $this->refs->scanRefs( 'refs/tags', function( $name, $sha ) use ( |
| | + $callback |
| | + ) { |
| | + $data = $this->read( $sha ); |
| | + $tag = $this->parseTagData( $name, $sha, $data ); |
| | + $callback( $tag ); |
| | + } ); |
| | + } |
| | + |
| | + 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'; |
| | + |
| | + list( $author, $timestamp ) = |
| | + $this->extractAuthorAndTime( $data, $pattern ); |
| | + $message = $this->extractMessage( $data ); |
| | + |
| | + return new Tag( $name, $sha, $targetSha, $timestamp, $message, $author ); |
| | + } |
| | + |
| | + 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 extractAuthorAndTime( string $data, string $pattern |
| | + ): array { |
| | + $matches = []; |
| | + $found = preg_match( $pattern, $data, $matches ); |
| | + $author = $found ? trim( $matches[1] ) : ''; |
| | + $timestamp = $found && isset( $matches[3] ) |
| | + ? (int)$matches[3] |
| | + : $found && isset( $matches[2] ? (int)$matches[2] : 0 ); |
| | + |
| | + return [ $author, $timestamp ]; |
| | + } |
| | + |
| | + private function extractMessage( string $data ): string { |
| | + $pos = strpos( $data, "\n\n" ); |
| | + |
| | + return $pos !== false ? trim( substr( $data, $pos + 2 ) ) : ''; |
| | + } |
| | + |
| | + public function getObjectSize( string $sha ): int { |
| | + $size = $this->packs->getSize( $sha ); |
| | + return $size !== null ? $size : $this->getLooseObjectSize( $sha ); |
| | + } |
| | + |
| | + 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; |
| | + } |
| | + |
| | + public function readFile( string $hash, string $name ) { |
| | + return new File( |
| | + $name, |
| | + $hash, |
| | + '100644', |
| | + 0, |
| | + $this->getObjectSize( $hash ), |
| | + $this->peek( $hash ) |
| | + ); |
| | + } |
| | + |
| | + public function stream( string $sha, callable $callback ): void { |
| | + $this->slurp( $sha, $callback ); |
| | + } |
| | + |
| | + 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 slurpLooseObject( string $path, callable $callback ): |
| | + void { |
| | + $this->withInflatedFile( $path, function( $fileHandle, $inflator ) use ( |
| | + $callback |
| | + ) { |
| | + $buffer = ''; |
| | + $headerFound = false; |
| | + |
| | + while( !feof( $fileHandle ) ) { |
| | + $chunk = fread( $fileHandle, 16384 ); |
| | + $inflatedChunk = inflate_add( $inflator, $chunk ); |
| | + |
| | + if( $inflatedChunk === false ) { |
| | + break; |
| | + } |
| | + |
| | + $headerFound = $this->processInflatedChunk( |
| | + $inflatedChunk, |
| | + $headerFound, |
| | + $buffer, |
| | + $callback |
| | + ); |
| | + } |
| | + } ); |
| | + } |
| | + |
| | + 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 processInflatedChunk( |
| | + string $chunk, |
| | + bool $headerFound, |
| | + string &$buffer, |
| | + callable $callback |
| | + ): bool { |
| | + if( !$headerFound ) { |
| | + $buffer .= $chunk; |
| | + $nullPos = strpos( $buffer, "\0" ); |
| | + |
| | + if( $nullPos !== false ) { |
| | + $body = substr( $buffer, $nullPos + 1 ); |
| | + |
| | + if( $body !== '' ) { |
| | + $callback( $body ); |
| | + } |
| | + |
| | + $buffer = ''; |
| | + return true; |
| | + } |
| | + } else { |
| | + $callback( $chunk ); |
| | + } |
| | + |
| | + return $headerFound; |
| | + } |
| | + |
| | + 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->withInflatedFile( $path, function( $fileHandle, $inflator ) use ( |
| | + $length, &$buffer |
| | + ) { |
| | + $headerFound = false; |
| | + |
| | + while( !feof( $fileHandle ) && strlen( $buffer ) < $length ) { |
| | + $chunk = fread( $fileHandle, 128 ); |
| | + $inflated = inflate_add( $inflator, $chunk ); |
| | + |
| | + if( $inflated === false ) { |
| | + break; |
| | + } |
| | + |
| | + $headerFound = $this->appendPrefixChunk( $inflated, $headerFound, |
| | + $buffer ); |
| | + } |
| | + |
| | + $buffer = substr( $buffer, 0, $length ); |
| | + } ); |
| | + |
| | + return $buffer; |
| | + } |
| | + |
| | + private function appendPrefixChunk( |
| | + string $chunk, |
| | + bool $headerFound, |
| | + string &$buffer |
| | + ): bool { |
| | + if( !$headerFound ) { |
| | + $nullPos = strpos( $chunk, "\0" ); |
| | + |
| | + if( $nullPos !== false ) { |
| | + $buffer .= substr( $chunk, $nullPos + 1 ); |
| | + return true; |
| | + } |
| | + } else { |
| | + $buffer .= $chunk; |
| | + } |
| | + |
| | + return $headerFound; |
| | + } |
| | + |
| | + 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 { |
| | + list( $author, $timestamp ) = $this->extractAuthorAndTime( |
| | + $data, '/^author (.*) <(.*) (\d+)/m' |
| | + ); |
| | + $email = $this->extractPattern( $data, '/^author .* <(.*)> \d+/m', 1 ); |
| | + $message = $this->extractMessage( $data ); |
| | + $parentSha = $this->extractPattern( $data, '/^parent ([0-9a-f]{40})$/m', |
| | + 1 ); |
| | + |
| | + return (object)[ |
| | + 'sha' => $sha, |
| | + 'message' => $message, |
| | + 'author' => $author ?: 'Unknown', |
| | + 'email' => $email, |
| | + 'date' => $timestamp, |
| | + 'parentSha' => $parentSha |
| | + ]; |
| | + } |
| | + |
| | + public function walk( string $refOrSha, callable $callback ): void { |
| | + $sha = $this->resolve( $refOrSha ); |
| | + |
| | + if( $sha !== '' ) { |
| | + $this->walkTree( $sha, $callback ); |
| | + } |
| | + } |
| | + |
| | + 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->withInflatedFile( $path, function( $fileHandle, $inflator ) use ( |
| | + &$size |
| | + ) { |
| | + $data = ''; |
| | + |
| | + while( !feof( $fileHandle ) ) { |
| | + $chunk = fread( $fileHandle, self::CHUNK_SIZE ); |
| | + $output = inflate_add( $inflator, $chunk, ZLIB_NO_FLUSH ); |
| | + |
| | + if( $output === false ) { |
| | + break; |
| | + } |
| | + |
| | + $data .= $output; |
| | + |
| | + if( strpos( $data, "\0" ) !== false ) { |
| | + break; |
| | + } |
| | + } |
| | + |
| | + $size = $this->parseSizeFromHeader( $data ); |
| | + } ); |
| | + |
| | + return $size; |
| | + } |
| | + |
| | + private function parseSizeFromHeader( string $data ): int { |
| | + $header = explode( "\0", $data, 2 )[0]; |
| | + $parts = explode( ' ', $header ); |
| | + |
| | + return isset( $parts[1] ) ? (int)$parts[1] : 0; |
| | + } |
| | + |
| | + 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 file_exists( $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 ); |
| | } ); |