| Author | Dave Jarvis <email> |
|---|---|
| Date | 2026-02-14 16:31:44 GMT-0800 |
| Commit | d1bc5e98861ac43bb366343afc15f8c2070760c0 |
| Parent | fe43743 |
| private static function getHomeDirectory() { | ||
| - if (!empty($_SERVER['HOME'])) { | ||
| + if( !empty( $_SERVER['HOME'] ) ) { | ||
| return $_SERVER['HOME']; | ||
| } | ||
| - if (!empty(getenv('HOME'))) { | ||
| - return getenv('HOME'); | ||
| + | ||
| + if( !empty( getenv( 'HOME' ) ) ) { | ||
| + return getenv( 'HOME' ); | ||
| } | ||
| - if (function_exists('posix_getpwuid') && function_exists('posix_getuid')) { | ||
| - $userInfo = posix_getpwuid(posix_getuid()); | ||
| - if (!empty($userInfo['dir'])) { | ||
| + | ||
| + if( function_exists( 'posix_getpwuid' ) && | ||
| + function_exists( 'posix_getuid' ) ) { | ||
| + $userInfo = posix_getpwuid( posix_getuid() ); | ||
| + | ||
| + if( !empty( $userInfo['dir'] ) ) { | ||
| return $userInfo['dir']; | ||
| } | ||
| } | ||
| + | ||
| return ''; | ||
| } | ||
| public static function getReposPath() { | ||
| return self::getHomeDirectory() . '/repos'; | ||
| } | ||
| public static function init() { | ||
| - ini_set('display_errors', 0); | ||
| - ini_set('log_errors', 1); | ||
| - ini_set('error_log', __DIR__ . '/error.log'); | ||
| + ini_set( 'display_errors', 0 ); | ||
| + ini_set( 'log_errors', 1 ); | ||
| + ini_set( 'error_log', __DIR__ . '/error.log' ); | ||
| } | ||
| } |
| string $contents = '' | ||
| ) { | ||
| - $this->name = $name; | ||
| - $this->sha = $sha; | ||
| - $this->mode = $mode; | ||
| - $this->timestamp = $timestamp; | ||
| - $this->size = $size; | ||
| - $this->isDir = $mode === '40000' || $mode === '040000'; | ||
| + $this->name = $name; | ||
| + $this->sha = $sha; | ||
| + $this->mode = $mode; | ||
| + $this->timestamp = $timestamp; | ||
| + $this->size = $size; | ||
| + $this->isDir = $mode === '40000' || $mode === '040000'; | ||
| $buffer = $this->isDir ? '' : $contents; | ||
| - $this->mediaType = $this->detectMediaType($buffer); | ||
| - $this->category = $this->detectCategory($name); | ||
| - $this->binary = $this->detectBinary(); | ||
| - $this->icon = $this->resolveIcon(); | ||
| + $this->mediaType = $this->detectMediaType( $buffer ); | ||
| + $this->category = $this->detectCategory( $name ); | ||
| + $this->binary = $this->detectBinary(); | ||
| + $this->icon = $this->resolveIcon(); | ||
| } | ||
| - public function compare(File $other): int { | ||
| + public function compare( File $other ): int { | ||
| return $this->isDir !== $other->isDir | ||
| ? ($this->isDir ? -1 : 1) | ||
| - : strcasecmp($this->name, $other->name); | ||
| + : strcasecmp( $this->name, $other->name ); | ||
| } | ||
| - public function render(FileRenderer $renderer): void { | ||
| + public function render( FileRenderer $renderer ): void { | ||
| $renderer->renderFile( | ||
| $this->name, | ||
| } | ||
| - public function renderSize(FileRenderer $renderer): void { | ||
| - $renderer->renderSize($this->size); | ||
| + public function renderSize( FileRenderer $renderer ): void { | ||
| + $renderer->renderSize( $this->size ); | ||
| } | ||
| - public function renderMedia(string $url): bool { | ||
| + public function renderMedia( string $url ): bool { | ||
| $rendered = false; | ||
| - if ($this->isImage()) { | ||
| - echo '<div class="blob-content blob-content-image"><img src="' . $url . '"></div>'; | ||
| + if( $this->isImage() ) { | ||
| + echo '<div class="blob-content blob-content-image">' . | ||
| + '<img src="' . $url . '"></div>'; | ||
| $rendered = true; | ||
| - } elseif ($this->isVideo()) { | ||
| - echo '<div class="blob-content blob-content-video"><video controls><source src="' . $url . '" type="' . $this->mediaType . '"></video></div>'; | ||
| + } elseif( $this->isVideo() ) { | ||
| + echo '<div class="blob-content blob-content-video">' . | ||
| + '<video controls><source src="' . $url . '" type="' . | ||
| + $this->mediaType . '"></video></div>'; | ||
| $rendered = true; | ||
| - } elseif ($this->isAudio()) { | ||
| - echo '<div class="blob-content blob-content-audio"><audio controls><source src="' . $url . '" type="' . $this->mediaType . '"></audio></div>'; | ||
| + } elseif( $this->isAudio() ) { | ||
| + echo '<div class="blob-content blob-content-audio">' . | ||
| + '<audio controls><source src="' . $url . '" type="' . | ||
| + $this->mediaType . '"></audio></div>'; | ||
| $rendered = true; | ||
| } | ||
| return $rendered; | ||
| } | ||
| public function emitRawHeaders(): void { | ||
| - header("Content-Type: " . $this->mediaType); | ||
| - header("Content-Length: " . $this->size); | ||
| - header("Content-Disposition: attachment; filename=\"" . addslashes(basename($this->name)) . "\""); | ||
| + header( "Content-Type: " . $this->mediaType ); | ||
| + header( "Content-Length: " . $this->size ); | ||
| + header( "Content-Disposition: attachment; filename=\"" . | ||
| + addslashes( basename( $this->name ) ) . "\"" ); | ||
| } | ||
| return $this->isDir | ||
| ? 'fa-folder' | ||
| - : (str_contains($this->mediaType, 'application/pdf') | ||
| + : (str_contains( $this->mediaType, 'application/pdf' ) | ||
| ? 'fa-file-pdf' | ||
| - : match ($this->category) { | ||
| + : match( $this->category ) { | ||
| self::CAT_ARCHIVE => 'fa-file-archive', | ||
| - self::CAT_IMAGE => 'fa-file-image', | ||
| - self::CAT_AUDIO => 'fa-file-audio', | ||
| - self::CAT_VIDEO => 'fa-file-video', | ||
| - self::CAT_TEXT => 'fa-file-code', | ||
| - default => 'fa-file', | ||
| + self::CAT_IMAGE => 'fa-file-image', | ||
| + self::CAT_AUDIO => 'fa-file-audio', | ||
| + self::CAT_VIDEO => 'fa-file-video', | ||
| + self::CAT_TEXT => 'fa-file-code', | ||
| + default => 'fa-file', | ||
| }); | ||
| } | ||
| - private function detectMediaType(string $buffer): string { | ||
| - $finfo = new finfo(FILEINFO_MIME_TYPE); | ||
| - $mediaType = $finfo->buffer($buffer); | ||
| + private function detectMediaType( string $buffer ): string { | ||
| + $finfo = new finfo( FILEINFO_MIME_TYPE ); | ||
| + $mediaType = $finfo->buffer( $buffer ); | ||
| + | ||
| return $mediaType ?: 'application/octet-stream'; | ||
| } | ||
| - private function detectCategory(string $filename = ''): string { | ||
| - $parts = explode('/', $this->mediaType); | ||
| + private function detectCategory( string $filename = '' ): string { | ||
| + $parts = explode( '/', $this->mediaType ); | ||
| - return match(true) { | ||
| + return match( true ) { | ||
| $parts[0] === 'image' => self::CAT_IMAGE, | ||
| $parts[0] === 'video' => self::CAT_VIDEO, | ||
| $parts[0] === 'audio' => self::CAT_AUDIO, | ||
| $parts[0] === 'text' => self::CAT_TEXT, | ||
| - $this->isArchiveFile($filename) => self::CAT_ARCHIVE, | ||
| - str_contains($this->mediaType, 'compressed') => self::CAT_ARCHIVE, | ||
| + $this->isArchiveFile( $filename ) => self::CAT_ARCHIVE, | ||
| + str_contains( $this->mediaType, 'compressed' ) => self::CAT_ARCHIVE, | ||
| default => self::CAT_BINARY, | ||
| }; | ||
| } | ||
| private function detectBinary(): bool { | ||
| - return !str_starts_with($this->mediaType, 'text/'); | ||
| + return !str_starts_with( $this->mediaType, 'text/' ); | ||
| } | ||
| - private function isArchiveFile(string $filename): bool { | ||
| + private function isArchiveFile( string $filename ): bool { | ||
| return in_array( | ||
| - strtolower(pathinfo($filename, PATHINFO_EXTENSION)), | ||
| + strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) ), | ||
| self::ARCHIVE_EXTENSIONS, | ||
| true | ||
| private $reposPath; | ||
| - public function __construct($path) { | ||
| + public function __construct( $path ) { | ||
| $this->reposPath = $path; | ||
| } | ||
| - public function eachRepository(callable $callback) { | ||
| + public function eachRepository( callable $callback ) { | ||
| $repos = []; | ||
| - $dirs = glob($this->reposPath . '/*', GLOB_ONLYDIR); | ||
| + $dirs = glob( $this->reposPath . '/*', GLOB_ONLYDIR ); | ||
| - if ($dirs === false) return; | ||
| + if( $dirs === false ) { | ||
| + return; | ||
| + } | ||
| - foreach ($dirs as $dir) { | ||
| - $basename = basename($dir); | ||
| - if ($basename[0] === '.') continue; | ||
| + foreach( $dirs as $dir ) { | ||
| + $basename = basename( $dir ); | ||
| + | ||
| + if( $basename[0] === '.' ) { | ||
| + continue; | ||
| + } | ||
| $repos[$basename] = [ | ||
| 'name' => $basename, | ||
| 'safe_name' => $basename, | ||
| 'path' => $dir | ||
| ]; | ||
| } | ||
| - $this->sortRepositories($repos); | ||
| + $this->sortRepositories( $repos ); | ||
| - foreach ($repos as $repo) { | ||
| - $callback($repo); | ||
| + foreach( $repos as $repo ) { | ||
| + $callback( $repo ); | ||
| } | ||
| } | ||
| - private function sortRepositories(array &$repos) { | ||
| + private function sortRepositories( array &$repos ) { | ||
| $orderFile = __DIR__ . '/order.txt'; | ||
| - if (!file_exists($orderFile)) { | ||
| - ksort($repos, SORT_NATURAL | SORT_FLAG_CASE); | ||
| + if( !file_exists( $orderFile ) ) { | ||
| + ksort( $repos, SORT_NATURAL | SORT_FLAG_CASE ); | ||
| return; | ||
| } | ||
| - $lines = file($orderFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); | ||
| + $lines = file( $orderFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES ); | ||
| $order = []; | ||
| $exclude = []; | ||
| - foreach ($lines as $line) { | ||
| - $line = trim($line); | ||
| - if ($line === '') continue; | ||
| + foreach( $lines as $line ) { | ||
| + $line = trim( $line ); | ||
| - if ($line[0] === '-') { | ||
| - $exclude[substr($line, 1)] = true; | ||
| + if( $line === '' ) { | ||
| + continue; | ||
| + } | ||
| + | ||
| + if( $line[0] === '-' ) { | ||
| + $exclude[substr( $line, 1 )] = true; | ||
| } else { | ||
| - $order[$line] = count($order); | ||
| + $order[$line] = count( $order ); | ||
| } | ||
| } | ||
| - foreach ($repos as $key => $repo) { | ||
| - if (isset($exclude[$repo['safe_name']])) { | ||
| - unset($repos[$key]); | ||
| + foreach( $repos as $key => $repo ) { | ||
| + if( isset( $exclude[$repo['safe_name']] ) ) { | ||
| + unset( $repos[$key] ); | ||
| } | ||
| } | ||
| - uasort($repos, function($a, $b) use ($order) { | ||
| + uasort( $repos, function( $a, $b ) use ( $order ) { | ||
| $nameA = $a['safe_name']; | ||
| $nameB = $b['safe_name']; | ||
| - | ||
| $posA = $order[$nameA] ?? PHP_INT_MAX; | ||
| $posB = $order[$nameB] ?? PHP_INT_MAX; | ||
| - if ($posA === $posB) { | ||
| - return strcasecmp($nameA, $nameB); | ||
| + if( $posA === $posB ) { | ||
| + return strcasecmp( $nameA, $nameB ); | ||
| } | ||
| return $posA <=> $posB; | ||
| - }); | ||
| + } ); | ||
| } | ||
| } |
| require_once __DIR__ . '/RepositoryList.php'; | ||
| require_once __DIR__ . '/git/Git.php'; | ||
| - | ||
| require_once __DIR__ . '/pages/CommitsPage.php'; | ||
| require_once __DIR__ . '/pages/DiffPage.php'; | ||
| public function __construct( string $reposPath ) { | ||
| $this->git = new Git( $reposPath ); | ||
| - | ||
| $list = new RepositoryList( $reposPath ); | ||
| $list->eachRepository( function( $repo ) { | ||
| $this->repos[] = $repo; | ||
| } ); | ||
| } | ||
| public function route(): Page { | ||
| $reqRepo = $_GET['repo'] ?? ''; | ||
| - $action = $_GET['action'] ?? 'file'; | ||
| - $hash = $this->sanitize( $_GET['hash'] ?? '' ); | ||
| + $action = $_GET['action'] ?? 'file'; | ||
| + $hash = $this->sanitize( $_GET['hash'] ?? '' ); | ||
| $subPath = ''; | ||
| - | ||
| $uri = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ); | ||
| $scriptName = $_SERVER['SCRIPT_NAME']; | ||
| - if ( strpos( $uri, $scriptName ) === 0 ) { | ||
| + if( strpos( $uri, $scriptName ) === 0 ) { | ||
| $uri = substr( $uri, strlen( $scriptName ) ); | ||
| } | ||
| if( preg_match( '#^/([^/]+)\.git(?:/(.*))?$#', $uri, $matches ) ) { | ||
| $reqRepo = urldecode( $matches[1] ); | ||
| $subPath = isset( $matches[2] ) ? ltrim( $matches[2], '/' ) : ''; | ||
| - $action = 'clone'; | ||
| + $action = 'clone'; | ||
| } | ||
| $currRepo = null; | ||
| - $decoded = urldecode( $reqRepo ); | ||
| + $decoded = urldecode( $reqRepo ); | ||
| foreach( $this->repos as $repo ) { | ||
| if( $repo['safe_name'] === $reqRepo || $repo['name'] === $decoded ) { | ||
| $currRepo = $repo; | ||
| + | ||
| break; | ||
| } | ||
| $prefix = $repo['safe_name'] . '/'; | ||
| if( strpos( $reqRepo, $prefix ) === 0 ) { | ||
| $currRepo = $repo; | ||
| - $subPath = substr( $reqRepo, strlen( $prefix ) ); | ||
| - $action = 'clone'; | ||
| + $subPath = substr( $reqRepo, strlen( $prefix ) ); | ||
| + $action = 'clone'; | ||
| + | ||
| break; | ||
| } | ||
| string $author | ||
| ) { | ||
| - $this->name = $name; | ||
| - $this->sha = $sha; | ||
| + $this->name = $name; | ||
| + $this->sha = $sha; | ||
| $this->targetSha = $targetSha; | ||
| $this->timestamp = $timestamp; | ||
| - $this->message = $message; | ||
| - $this->author = $author; | ||
| + $this->message = $message; | ||
| + $this->author = $author; | ||
| } | ||
| - public function compare(Tag $other): int { | ||
| + public function compare( Tag $other ): int { | ||
| return $other->timestamp <=> $this->timestamp; | ||
| } | ||
| - public function render(TagRenderer $renderer): void { | ||
| + public function render( TagRenderer $renderer ): void { | ||
| $renderer->renderTagItem( | ||
| $this->name, |
| $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'; | ||
| - | ||
| - $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 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 { | ||
| - $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 | ||
| - ]; | ||
| - } | ||
| - | ||
| - 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 ) ) { | ||
| + | ||
| + $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'; | ||
| + | ||
| + $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 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 { | ||
| + $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 | ||
| + ]; | ||
| + } | ||
| + | ||
| + 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 ); |
| private const MAX_DIFF_SIZE = 1048576; | ||
| - public function __construct(Git $git) { | ||
| + public function __construct( Git $git ) { | ||
| $this->git = $git; | ||
| } | ||
| - public function compare(string $commitHash) { | ||
| - $commitData = $this->git->read($commitHash); | ||
| - $parentHash = preg_match('/^parent ([0-9a-f]{40})/m', $commitData, $matches) ? $matches[1] : ''; | ||
| + public function compare( string $commitHash ) { | ||
| + $commitData = $this->git->read( $commitHash ); | ||
| - $newTree = $this->getTreeHash($commitHash); | ||
| - $oldTree = $parentHash ? $this->getTreeHash($parentHash) : null; | ||
| + $parentHash = preg_match( | ||
| + '/^parent ([0-9a-f]{40})/m', | ||
| + $commitData, | ||
| + $matches | ||
| + ) ? $matches[1] : ''; | ||
| - return $this->diffTrees($oldTree, $newTree); | ||
| + $newTree = $this->getTreeHash( $commitHash ); | ||
| + $oldTree = $parentHash ? $this->getTreeHash( $parentHash ) : null; | ||
| + | ||
| + return $this->diffTrees( $oldTree, $newTree ); | ||
| } | ||
| - private function getTreeHash($commitSha) { | ||
| - $data = $this->git->read($commitSha); | ||
| - return preg_match('/^tree ([0-9a-f]{40})/m', $data, $matches) ? $matches[1] : null; | ||
| + private function getTreeHash( $commitSha ) { | ||
| + $data = $this->git->read( $commitSha ); | ||
| + | ||
| + return preg_match( '/^tree ([0-9a-f]{40})/m', $data, $matches ) | ||
| + ? $matches[1] | ||
| + : null; | ||
| } | ||
| - private function diffTrees($oldTreeSha, $newTreeSha, $path = '') { | ||
| + private function diffTrees( $oldTreeSha, $newTreeSha, $path = '' ) { | ||
| $changes = []; | ||
| - if ($oldTreeSha !== $newTreeSha) { | ||
| - $oldEntries = $oldTreeSha ? $this->parseTree($oldTreeSha) : []; | ||
| - $newEntries = $newTreeSha ? $this->parseTree($newTreeSha) : []; | ||
| + if( $oldTreeSha !== $newTreeSha ) { | ||
| + $oldEntries = $oldTreeSha ? $this->parseTree( $oldTreeSha ) : []; | ||
| + $newEntries = $newTreeSha ? $this->parseTree( $newTreeSha ) : []; | ||
| - $allNames = array_unique(array_merge(array_keys($oldEntries), array_keys($newEntries))); | ||
| - sort($allNames); | ||
| + $allNames = array_unique( | ||
| + array_merge( array_keys( $oldEntries ), array_keys( $newEntries ) ) | ||
| + ); | ||
| - foreach ($allNames as $name) { | ||
| + sort( $allNames ); | ||
| + | ||
| + foreach( $allNames as $name ) { | ||
| $old = $oldEntries[$name] ?? null; | ||
| $new = $newEntries[$name] ?? null; | ||
| $currentPath = $path ? "$path/$name" : $name; | ||
| - if (!$old) { | ||
| + if( !$old ) { | ||
| $changes = $new['is_dir'] | ||
| - ? array_merge($changes, $this->diffTrees(null, $new['sha'], $currentPath)) | ||
| - : array_merge($changes, [$this->createChange('A', $currentPath, null, $new['sha'])]); | ||
| - } elseif (!$new) { | ||
| + ? array_merge( | ||
| + $changes, | ||
| + $this->diffTrees( null, $new['sha'], $currentPath ) | ||
| + ) | ||
| + : array_merge( | ||
| + $changes, | ||
| + [$this->createChange( 'A', $currentPath, null, $new['sha'] )] | ||
| + ); | ||
| + } elseif( !$new ) { | ||
| $changes = $old['is_dir'] | ||
| - ? array_merge($changes, $this->diffTrees($old['sha'], null, $currentPath)) | ||
| - : array_merge($changes, [$this->createChange('D', $currentPath, $old['sha'], null)]); | ||
| - } elseif ($old['sha'] !== $new['sha']) { | ||
| + ? array_merge( | ||
| + $changes, | ||
| + $this->diffTrees( $old['sha'], null, $currentPath ) | ||
| + ) | ||
| + : array_merge( | ||
| + $changes, | ||
| + [$this->createChange( 'D', $currentPath, $old['sha'], null )] | ||
| + ); | ||
| + } elseif( $old['sha'] !== $new['sha'] ) { | ||
| $changes = ($old['is_dir'] && $new['is_dir']) | ||
| - ? array_merge($changes, $this->diffTrees($old['sha'], $new['sha'], $currentPath)) | ||
| + ? array_merge( | ||
| + $changes, | ||
| + $this->diffTrees( $old['sha'], $new['sha'], $currentPath ) | ||
| + ) | ||
| : (($old['is_dir'] || $new['is_dir']) | ||
| ? $changes | ||
| - : array_merge($changes, [$this->createChange('M', $currentPath, $old['sha'], $new['sha'])])); | ||
| + : array_merge( | ||
| + $changes, | ||
| + [$this->createChange( | ||
| + 'M', | ||
| + $currentPath, | ||
| + $old['sha'], | ||
| + $new['sha'] | ||
| + )] | ||
| + )); | ||
| } | ||
| } | ||
| } | ||
| return $changes; | ||
| } | ||
| - private function parseTree($sha) { | ||
| - $data = $this->git->read($sha); | ||
| + private function parseTree( $sha ) { | ||
| + $data = $this->git->read( $sha ); | ||
| $entries = []; | ||
| - $len = strlen($data); | ||
| + $len = strlen( $data ); | ||
| $pos = 0; | ||
| - while ($pos < $len) { | ||
| - $space = strpos($data, ' ', $pos); | ||
| - $null = strpos($data, "\0", $space); | ||
| + while( $pos < $len ) { | ||
| + $space = strpos( $data, ' ', $pos ); | ||
| + $null = strpos( $data, "\0", $space ); | ||
| - if ($space === false || $null === false) break; | ||
| + if( $space === false || $null === false ) { | ||
| + break; | ||
| + } | ||
| - $mode = substr($data, $pos, $space - $pos); | ||
| - $name = substr($data, $space + 1, $null - $space - 1); | ||
| - $hash = bin2hex(substr($data, $null + 1, 20)); | ||
| + $mode = substr( $data, $pos, $space - $pos ); | ||
| + $name = substr( $data, $space + 1, $null - $space - 1 ); | ||
| + $hash = bin2hex( substr( $data, $null + 1, 20 ) ); | ||
| $entries[$name] = [ | ||
| 'mode' => $mode, | ||
| 'sha' => $hash, | ||
| 'is_dir' => $mode === '40000' || $mode === '040000' | ||
| ]; | ||
| $pos = $null + 21; | ||
| } | ||
| + | ||
| return $entries; | ||
| } | ||
| - private function createChange($type, $path, $oldSha, $newSha) { | ||
| - $oldSize = $oldSha ? $this->git->getObjectSize($oldSha) : 0; | ||
| - $newSize = $newSha ? $this->git->getObjectSize($newSha) : 0; | ||
| + private function createChange( $type, $path, $oldSha, $newSha ) { | ||
| + $oldSize = $oldSha ? $this->git->getObjectSize( $oldSha ) : 0; | ||
| + $newSize = $newSha ? $this->git->getObjectSize( $newSha ) : 0; | ||
| $result = []; | ||
| - if ($oldSize > self::MAX_DIFF_SIZE || $newSize > self::MAX_DIFF_SIZE) { | ||
| + if( $oldSize > self::MAX_DIFF_SIZE || $newSize > self::MAX_DIFF_SIZE ) { | ||
| $result = [ | ||
| 'type' => $type, | ||
| 'path' => $path, | ||
| 'is_binary' => true, | ||
| 'hunks' => [] | ||
| ]; | ||
| } else { | ||
| - $oldContent = $oldSha ? $this->git->read($oldSha) : ''; | ||
| - $newContent = $newSha ? $this->git->read($newSha) : ''; | ||
| + $oldContent = $oldSha ? $this->git->read( $oldSha ) : ''; | ||
| + $newContent = $newSha ? $this->git->read( $newSha ) : ''; | ||
| - $isBinary = ($newSha && (new VirtualDiffFile($path, $newContent))->isBinary()) || | ||
| - (!$newSha && $oldSha && (new VirtualDiffFile($path, $oldContent))->isBinary()); | ||
| + $isBinary = | ||
| + ($newSha && (new VirtualDiffFile( $path, $newContent ))->isBinary()) || | ||
| + (!$newSha && $oldSha && | ||
| + (new VirtualDiffFile( $path, $oldContent ))->isBinary()); | ||
| $result = [ | ||
| 'type' => $type, | ||
| 'path' => $path, | ||
| 'is_binary' => $isBinary, | ||
| - 'hunks' => $isBinary ? null : $this->calculateDiff($oldContent, $newContent) | ||
| + 'hunks' => $isBinary | ||
| + ? null | ||
| + : $this->calculateDiff( $oldContent, $newContent ) | ||
| ]; | ||
| } | ||
| return $result; | ||
| } | ||
| - private function calculateDiff($old, $new) { | ||
| - $old = str_replace("\r\n", "\n", $old); | ||
| - $new = str_replace("\r\n", "\n", $new); | ||
| + private function calculateDiff( $old, $new ) { | ||
| + $old = str_replace( "\r\n", "\n", $old ); | ||
| + $new = str_replace( "\r\n", "\n", $new ); | ||
| - $oldLines = explode("\n", $old); | ||
| - $newLines = explode("\n", $new); | ||
| + $oldLines = explode( "\n", $old ); | ||
| + $newLines = explode( "\n", $new ); | ||
| - $m = count($oldLines); | ||
| - $n = count($newLines); | ||
| + $m = count( $oldLines ); | ||
| + $n = count( $newLines ); | ||
| $start = 0; | ||
| - while ($start < $m && $start < $n && $oldLines[$start] === $newLines[$start]) { | ||
| + | ||
| + while( | ||
| + $start < $m && | ||
| + $start < $n && | ||
| + $oldLines[$start] === $newLines[$start] | ||
| + ) { | ||
| $start++; | ||
| } | ||
| $end = 0; | ||
| - while ($m - $end > $start && $n - $end > $start && $oldLines[$m - 1 - $end] === $newLines[$n - 1 - $end]) { | ||
| + | ||
| + while( | ||
| + $m - $end > $start && | ||
| + $n - $end > $start && | ||
| + $oldLines[$m - 1 - $end] === $newLines[$n - 1 - $end] | ||
| + ) { | ||
| $end++; | ||
| } | ||
| - $oldSlice = array_slice($oldLines, $start, $m - $start - $end); | ||
| - $newSlice = array_slice($newLines, $start, $n - $start - $end); | ||
| + $oldSlice = array_slice( $oldLines, $start, $m - $start - $end ); | ||
| + $newSlice = array_slice( $newLines, $start, $n - $start - $end ); | ||
| $result = null; | ||
| - if ((count($oldSlice) * count($newSlice)) > 500000) { | ||
| + if( (count( $oldSlice ) * count( $newSlice )) > 500000 ) { | ||
| $result = [['t' => 'gap']]; | ||
| } else { | ||
| - $ops = $this->computeLCS($oldSlice, $newSlice); | ||
| + $ops = $this->computeLCS( $oldSlice, $newSlice ); | ||
| $groupedOps = []; | ||
| $bufferDel = []; | ||
| $bufferAdd = []; | ||
| - foreach ($ops as $op) { | ||
| - if ($op['t'] === ' ') { | ||
| - foreach ($bufferDel as $o) $groupedOps[] = $o; | ||
| - foreach ($bufferAdd as $o) $groupedOps[] = $o; | ||
| + foreach( $ops as $op ) { | ||
| + if( $op['t'] === ' ' ) { | ||
| + foreach( $bufferDel as $o ) { $groupedOps[] = $o; } | ||
| + foreach( $bufferAdd as $o ) { $groupedOps[] = $o; } | ||
| + | ||
| $bufferDel = []; | ||
| $bufferAdd = []; | ||
| $groupedOps[] = $op; | ||
| - } elseif ($op['t'] === '-') { | ||
| + } elseif( $op['t'] === '-' ) { | ||
| $bufferDel[] = $op; | ||
| - } elseif ($op['t'] === '+') { | ||
| + } elseif( $op['t'] === '+' ) { | ||
| $bufferAdd[] = $op; | ||
| } | ||
| } | ||
| - foreach ($bufferDel as $o) $groupedOps[] = $o; | ||
| - foreach ($bufferAdd as $o) $groupedOps[] = $o; | ||
| - $ops = $groupedOps; | ||
| + | ||
| + foreach( $bufferDel as $o ) { $groupedOps[] = $o; } | ||
| + foreach( $bufferAdd as $o ) { $groupedOps[] = $o; } | ||
| + $ops = $groupedOps; | ||
| $stream = []; | ||
| - for ($i = 0; $i < $start; $i++) { | ||
| - $stream[] = ['t' => ' ', 'l' => $oldLines[$i], 'no' => $i + 1, 'nn' => $i + 1]; | ||
| + for( $i = 0; $i < $start; $i++ ) { | ||
| + $stream[] = [ | ||
| + 't' => ' ', | ||
| + 'l' => $oldLines[$i], | ||
| + 'no' => $i + 1, | ||
| + 'nn' => $i + 1 | ||
| + ]; | ||
| } | ||
| $currO = $start + 1; | ||
| $currN = $start + 1; | ||
| - foreach ($ops as $op) { | ||
| - if ($op['t'] === ' ') { | ||
| - $stream[] = ['t' => ' ', 'l' => $op['l'], 'no' => $currO++, 'nn' => $currN++]; | ||
| - } elseif ($op['t'] === '-') { | ||
| - $stream[] = ['t' => '-', 'l' => $op['l'], 'no' => $currO++, 'nn' => null]; | ||
| - } elseif ($op['t'] === '+') { | ||
| - $stream[] = ['t' => '+', 'l' => $op['l'], 'no' => null, 'nn' => $currN++]; | ||
| + foreach( $ops as $op ) { | ||
| + if( $op['t'] === ' ' ) { | ||
| + $stream[] = [ | ||
| + 't' => ' ', | ||
| + 'l' => $op['l'], | ||
| + 'no' => $currO++, | ||
| + 'nn' => $currN++ | ||
| + ]; | ||
| + } elseif( $op['t'] === '-' ) { | ||
| + $stream[] = [ | ||
| + 't' => '-', | ||
| + 'l' => $op['l'], | ||
| + 'no' => $currO++, | ||
| + 'nn' => null | ||
| + ]; | ||
| + } elseif( $op['t'] === '+' ) { | ||
| + $stream[] = [ | ||
| + 't' => '+', | ||
| + 'l' => $op['l'], | ||
| + 'no' => null, | ||
| + 'nn' => $currN++ | ||
| + ]; | ||
| } | ||
| } | ||
| - for ($i = $m - $end; $i < $m; $i++) { | ||
| - $stream[] = ['t' => ' ', 'l' => $oldLines[$i], 'no' => $currO++, 'nn' => $currN++]; | ||
| + for( $i = $m - $end; $i < $m; $i++ ) { | ||
| + $stream[] = [ | ||
| + 't' => ' ', | ||
| + 'l' => $oldLines[$i], | ||
| + 'no' => $currO++, | ||
| + 'nn' => $currN++ | ||
| + ]; | ||
| } | ||
| $finalLines = []; | ||
| $lastVisibleIndex = -1; | ||
| - $streamLen = count($stream); | ||
| + $streamLen = count( $stream ); | ||
| $contextLines = 3; | ||
| - for ($i = 0; $i < $streamLen; $i++) { | ||
| + for( $i = 0; $i < $streamLen; $i++ ) { | ||
| $show = false; | ||
| - if ($stream[$i]['t'] !== ' ') { | ||
| + if( $stream[$i]['t'] !== ' ' ) { | ||
| $show = true; | ||
| } else { | ||
| - for ($j = 1; $j <= $contextLines; $j++) { | ||
| - if (($i + $j) < $streamLen && $stream[$i + $j]['t'] !== ' ') { | ||
| + for( $j = 1; $j <= $contextLines; $j++ ) { | ||
| + if( ($i + $j) < $streamLen && $stream[$i + $j]['t'] !== ' ' ) { | ||
| $show = true; | ||
| break; | ||
| } | ||
| } | ||
| - if (!$show) { | ||
| - for ($j = 1; $j <= $contextLines; $j++) { | ||
| - if (($i - $j) >= 0 && $stream[$i - $j]['t'] !== ' ') { | ||
| + | ||
| + if( !$show ) { | ||
| + for( $j = 1; $j <= $contextLines; $j++ ) { | ||
| + if( ($i - $j) >= 0 && $stream[$i - $j]['t'] !== ' ' ) { | ||
| $show = true; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| - if ($show) { | ||
| - if ($lastVisibleIndex !== -1 && $i > $lastVisibleIndex + 1) { | ||
| + if( $show ) { | ||
| + if( $lastVisibleIndex !== -1 && $i > $lastVisibleIndex + 1 ) { | ||
| $finalLines[] = ['t' => 'gap']; | ||
| } | ||
| + | ||
| $finalLines[] = $stream[$i]; | ||
| $lastVisibleIndex = $i; | ||
| } | ||
| } | ||
| + | ||
| $result = $finalLines; | ||
| } | ||
| return $result; | ||
| } | ||
| - private function computeLCS($old, $new) { | ||
| - $m = count($old); | ||
| - $n = count($new); | ||
| - $c = array_fill(0, $m + 1, array_fill(0, $n + 1, 0)); | ||
| + private function computeLCS( $old, $new ) { | ||
| + $m = count( $old ); | ||
| + $n = count( $new ); | ||
| + $c = array_fill( 0, $m + 1, array_fill( 0, $n + 1, 0 ) ); | ||
| - for ($i = 1; $i <= $m; $i++) { | ||
| - for ($j = 1; $j <= $n; $j++) { | ||
| + for( $i = 1; $i <= $m; $i++ ) { | ||
| + for( $j = 1; $j <= $n; $j++ ) { | ||
| $c[$i][$j] = ($old[$i - 1] === $new[$j - 1]) | ||
| ? $c[$i - 1][$j - 1] + 1 | ||
| - : max($c[$i][$j - 1], $c[$i - 1][$j]); | ||
| + : max( $c[$i][$j - 1], $c[$i - 1][$j] ); | ||
| } | ||
| } | ||
| $diff = []; | ||
| $i = $m; | ||
| $j = $n; | ||
| - while ($i > 0 || $j > 0) { | ||
| - if ($i > 0 && $j > 0 && $old[$i - 1] === $new[$j - 1]) { | ||
| - array_unshift($diff, ['t' => ' ', 'l' => $old[$i - 1]]); | ||
| + while( $i > 0 || $j > 0 ) { | ||
| + if( $i > 0 && $j > 0 && $old[$i - 1] === $new[$j - 1] ) { | ||
| + array_unshift( $diff, ['t' => ' ', 'l' => $old[$i - 1]] ); | ||
| $i--; | ||
| $j--; | ||
| - } elseif ($j > 0 && ($i === 0 || $c[$i][$j - 1] >= $c[$i - 1][$j])) { | ||
| - array_unshift($diff, ['t' => '+', 'l' => $new[$j - 1]]); | ||
| + } elseif( $j > 0 && ($i === 0 || $c[$i][$j - 1] >= $c[$i - 1][$j]) ) { | ||
| + array_unshift( $diff, ['t' => '+', 'l' => $new[$j - 1]] ); | ||
| $j--; | ||
| - } elseif ($i > 0 && ($j === 0 || $c[$i][$j - 1] < $c[$i - 1][$j])) { | ||
| - array_unshift($diff, ['t' => '-', 'l' => $old[$i - 1]]); | ||
| + } elseif( $i > 0 && ($j === 0 || $c[$i][$j - 1] < $c[$i - 1][$j]) ) { | ||
| + array_unshift( $diff, ['t' => '-', 'l' => $old[$i - 1]] ); | ||
| $i--; | ||
| } | ||
| } | ||
| + | ||
| return $diff; | ||
| } | ||
| } | ||
| class VirtualDiffFile extends File { | ||
| - public function __construct(string $name, string $content) { | ||
| - parent::__construct($name, '', '100644', 0, strlen($content), $content); | ||
| + public function __construct( string $name, string $content ) { | ||
| + parent::__construct( | ||
| + $name, | ||
| + '', | ||
| + '100644', | ||
| + 0, | ||
| + strlen( $content ), | ||
| + $content | ||
| + ); | ||
| } | ||
| } |
| } | ||
| - return $this->streamPackEntry( $handle, $info['offset'], $size, $callback ); | ||
| - } | ||
| - | ||
| - public function getSize( string $sha ): ?int { | ||
| - $info = $this->findPackInfo( $sha ); | ||
| - | ||
| - if( $info['offset'] === -1 ) { | ||
| - return null; | ||
| - } | ||
| - | ||
| - return $this->extractPackedSize( $info['file'], $info['offset'] ); | ||
| - } | ||
| - | ||
| - private function findPackInfo( string $sha ): array { | ||
| - if( !ctype_xdigit( $sha ) || strlen( $sha ) !== 40 ) { | ||
| - return ['offset' => -1]; | ||
| - } | ||
| - | ||
| - $binarySha = hex2bin( $sha ); | ||
| - | ||
| - if( $this->lastPack ) { | ||
| - $offset = $this->findInIdx( $this->lastPack, $binarySha ); | ||
| - | ||
| - if( $offset !== -1 ) { | ||
| - return $this->makeResult( $this->lastPack, $offset ); | ||
| - } | ||
| - } | ||
| - | ||
| - foreach( $this->packFiles as $indexFile ) { | ||
| - if( $indexFile === $this->lastPack ) { | ||
| - continue; | ||
| - } | ||
| - | ||
| - $offset = $this->findInIdx( $indexFile, $binarySha ); | ||
| - | ||
| - if( $offset !== -1 ) { | ||
| - $this->lastPack = $indexFile; | ||
| - | ||
| - return $this->makeResult( $indexFile, $offset ); | ||
| - } | ||
| - } | ||
| - | ||
| - return ['offset' => -1]; | ||
| - } | ||
| - | ||
| - private function makeResult( string $indexPath, int $offset ): array { | ||
| - return [ | ||
| - 'file' => str_replace( '.idx', '.pack', $indexPath ), | ||
| - 'offset' => $offset | ||
| - ]; | ||
| - } | ||
| - | ||
| - private function findInIdx( string $indexFile, string $binarySha ): int { | ||
| - $fileHandle = $this->getHandle( $indexFile ); | ||
| - | ||
| - if( !$fileHandle ) { | ||
| - return -1; | ||
| - } | ||
| - | ||
| - if( !isset( $this->fanoutCache[$indexFile] ) ) { | ||
| - fseek( $fileHandle, 0 ); | ||
| - | ||
| - if( fread( $fileHandle, 8 ) === "\377tOc\0\0\0\2" ) { | ||
| - $this->fanoutCache[$indexFile] = array_values( | ||
| - unpack( 'N*', fread( $fileHandle, 1024 ) ) | ||
| - ); | ||
| - } else { | ||
| - return -1; | ||
| - } | ||
| - } | ||
| - | ||
| - $fanout = $this->fanoutCache[$indexFile]; | ||
| - | ||
| - $firstByte = ord( $binarySha[0] ); | ||
| - $start = $firstByte === 0 ? 0 : $fanout[$firstByte - 1]; | ||
| - $end = $fanout[$firstByte]; | ||
| - | ||
| - if( $end <= $start ) { | ||
| - return -1; | ||
| - } | ||
| - | ||
| - $cacheKey = "$indexFile:$firstByte"; | ||
| - | ||
| - if( !isset( $this->shaBucketCache[$cacheKey] ) ) { | ||
| - $count = $end - $start; | ||
| - fseek( $fileHandle, 1032 + ($start * 20) ); | ||
| - $this->shaBucketCache[$cacheKey] = fread( $fileHandle, $count * 20 ); | ||
| - | ||
| - fseek( | ||
| - $fileHandle, | ||
| - 1032 + ($fanout[255] * 24) + ($start * 4) | ||
| - ); | ||
| - $this->offsetBucketCache[$cacheKey] = fread( $fileHandle, $count * 4 ); | ||
| - } | ||
| - | ||
| - $shaBlock = $this->shaBucketCache[$cacheKey]; | ||
| - $count = strlen( $shaBlock ) / 20; | ||
| - $low = 0; | ||
| - $high = $count - 1; | ||
| - $foundIdx = -1; | ||
| - | ||
| - while( $low <= $high ) { | ||
| - $mid = ($low + $high) >> 1; | ||
| - $compare = substr( $shaBlock, $mid * 20, 20 ); | ||
| - | ||
| - if( $compare < $binarySha ) { | ||
| - $low = $mid + 1; | ||
| - } elseif( $compare > $binarySha ) { | ||
| - $high = $mid - 1; | ||
| - } else { | ||
| - $foundIdx = $mid; | ||
| - break; | ||
| - } | ||
| - } | ||
| - | ||
| - if( $foundIdx === -1 ) { | ||
| - return -1; | ||
| - } | ||
| - | ||
| - $offsetData = substr( | ||
| - $this->offsetBucketCache[$cacheKey], | ||
| - $foundIdx * 4, | ||
| - 4 | ||
| - ); | ||
| - $offset = unpack( 'N', $offsetData )[1]; | ||
| - | ||
| - if( $offset & 0x80000000 ) { | ||
| - $packTotal = $fanout[255]; | ||
| - $pos64 = 1032 + ($packTotal * 28) + | ||
| - (($offset & 0x7FFFFFFF) * 8); | ||
| - fseek( $fileHandle, $pos64 ); | ||
| - $offset = unpack( 'J', fread( $fileHandle, 8 ) )[1]; | ||
| - } | ||
| - | ||
| - return (int)$offset; | ||
| - } | ||
| - | ||
| - private function readPackEntry( $fileHandle, int $offset, int $expectedSize, int $cap = 0 ): string { | ||
| - fseek( $fileHandle, $offset ); | ||
| - | ||
| - $header = $this->readVarInt( $fileHandle ); | ||
| - $type = ($header['byte'] >> 4) & 7; | ||
| - | ||
| - if( $type === 6 ) { | ||
| - return $this->handleOfsDelta( $fileHandle, $offset, $expectedSize, $cap ); | ||
| - } | ||
| - | ||
| - if( $type === 7 ) { | ||
| - return $this->handleRefDelta( $fileHandle, $expectedSize, $cap ); | ||
| - } | ||
| - | ||
| - return $this->decompressToString( $fileHandle, $expectedSize, $cap ); | ||
| - } | ||
| - | ||
| - private function streamPackEntry( $fileHandle, int $offset, int $expectedSize, callable $callback ): bool { | ||
| - fseek( $fileHandle, $offset ); | ||
| - | ||
| - $header = $this->readVarInt( $fileHandle ); | ||
| - $type = ($header['byte'] >> 4) & 7; | ||
| - | ||
| - if( $type === 6 || $type === 7 ) { | ||
| - return $this->streamDeltaObject( $fileHandle, $offset, $type, $expectedSize, $callback ); | ||
| - } | ||
| - | ||
| - return $this->streamDecompression( $fileHandle, $callback ); | ||
| - } | ||
| - | ||
| - private function streamDeltaObject( $fileHandle, int $offset, int $type, int $expectedSize, callable $callback ): bool { | ||
| - fseek( $fileHandle, $offset ); | ||
| - $this->readVarInt( $fileHandle ); | ||
| - | ||
| - if( $type === 6 ) { | ||
| - $byte = ord( fread( $fileHandle, 1 ) ); | ||
| - $negative = $byte & 127; | ||
| - | ||
| - while( $byte & 128 ) { | ||
| - $byte = ord( fread( $fileHandle, 1 ) ); | ||
| - $negative = (($negative + 1) << 7) | ($byte & 127); | ||
| - } | ||
| - | ||
| - $deltaPos = ftell( $fileHandle ); | ||
| - $baseOffset = $offset - $negative; | ||
| - | ||
| - $base = ''; | ||
| - $this->streamPackEntry( $fileHandle, $baseOffset, 0, function( $chunk ) use ( &$base ) { | ||
| - $base .= $chunk; | ||
| - } ); | ||
| - | ||
| - fseek( $fileHandle, $deltaPos ); | ||
| - } else { | ||
| - $baseSha = bin2hex( fread( $fileHandle, 20 ) ); | ||
| - | ||
| - $base = ''; | ||
| - $streamed = $this->stream( $baseSha, function( $chunk ) use ( &$base ) { | ||
| - $base .= $chunk; | ||
| - } ); | ||
| - | ||
| - if( !$streamed ) { | ||
| - return false; | ||
| - } | ||
| - } | ||
| - | ||
| - $compressed = fread( $fileHandle, self::MAX_READ ); | ||
| - $delta = @gzuncompress( $compressed ) ?: ''; | ||
| - | ||
| - $result = $this->applyDelta( $base, $delta ); | ||
| - | ||
| - $chunkSize = 8192; | ||
| - $length = strlen( $result ); | ||
| - for( $i = 0; $i < $length; $i += $chunkSize ) { | ||
| - $callback( substr( $result, $i, $chunkSize ) ); | ||
| - } | ||
| - | ||
| - return true; | ||
| - } | ||
| - | ||
| - private function streamDecompression( $fileHandle, callable $callback ): bool { | ||
| - $inflator = inflate_init( ZLIB_ENCODING_DEFLATE ); | ||
| - if( $inflator === false ) { | ||
| - return false; | ||
| - } | ||
| - | ||
| - while( !feof( $fileHandle ) ) { | ||
| - $chunk = fread( $fileHandle, 8192 ); | ||
| - | ||
| - if( $chunk === false || $chunk === '' ) { | ||
| - break; | ||
| - } | ||
| - | ||
| - $data = @inflate_add( $inflator, $chunk ); | ||
| - | ||
| - if( $data !== false && $data !== '' ) { | ||
| - $callback( $data ); | ||
| - } | ||
| - | ||
| - if( $data === false || | ||
| - inflate_get_status( $inflator ) === ZLIB_STREAM_END ) { | ||
| - break; | ||
| - } | ||
| - } | ||
| - | ||
| - return true; | ||
| - } | ||
| - | ||
| - private function decompressToString( $fileHandle, int $maxSize, int $cap = 0 ): string { | ||
| - $inflator = inflate_init( ZLIB_ENCODING_DEFLATE ); | ||
| - if( $inflator === false ) { | ||
| - return ''; | ||
| - } | ||
| - | ||
| - $result = ''; | ||
| - | ||
| - while( !feof( $fileHandle ) ) { | ||
| - $chunk = fread( $fileHandle, 8192 ); | ||
| - | ||
| - if( $chunk === false || $chunk === '' ) { | ||
| - break; | ||
| - } | ||
| - | ||
| - $data = @inflate_add( $inflator, $chunk ); | ||
| - | ||
| - if( $data !== false ) { | ||
| - $result .= $data; | ||
| - } | ||
| - | ||
| - if( $cap > 0 && strlen( $result ) >= $cap ) { | ||
| - return substr( $result, 0, $cap ); | ||
| - } | ||
| - | ||
| - if( $data === false || | ||
| - inflate_get_status( $inflator ) === ZLIB_STREAM_END ) { | ||
| - break; | ||
| - } | ||
| - } | ||
| - | ||
| - return $result; | ||
| - } | ||
| - | ||
| - private function extractPackedSize( string $packPath, int $offset ): int { | ||
| - $fileHandle = $this->getHandle( $packPath ); | ||
| - | ||
| - if( !$fileHandle ) { | ||
| - return 0; | ||
| - } | ||
| - | ||
| - fseek( $fileHandle, $offset ); | ||
| - | ||
| - $header = $this->readVarInt( $fileHandle ); | ||
| - $size = $header['value']; | ||
| - $type = ($header['byte'] >> 4) & 7; | ||
| - | ||
| - if( $type === 6 || $type === 7 ) { | ||
| - return $this->readDeltaTargetSize( $fileHandle, $type ); | ||
| - } | ||
| - | ||
| - return $size; | ||
| - } | ||
| - | ||
| - private function handleOfsDelta( $fileHandle, int $offset, int $expectedSize, int $cap = 0 ): string { | ||
| - $byte = ord( fread( $fileHandle, 1 ) ); | ||
| - $negative = $byte & 127; | ||
| - | ||
| - while( $byte & 128 ) { | ||
| - $byte = ord( fread( $fileHandle, 1 ) ); | ||
| - $negative = (($negative + 1) << 7) | ($byte & 127); | ||
| - } | ||
| - | ||
| - $currentPos = ftell( $fileHandle ); | ||
| - $baseOffset = $offset - $negative; | ||
| - | ||
| - fseek( $fileHandle, $baseOffset ); | ||
| - $baseHeader = $this->readVarInt( $fileHandle ); | ||
| - $baseSize = $baseHeader['value']; | ||
| - | ||
| - fseek( $fileHandle, $baseOffset ); | ||
| - $base = $this->readPackEntry( $fileHandle, $baseOffset, $baseSize, $cap ); | ||
| - | ||
| - fseek( $fileHandle, $currentPos ); | ||
| - | ||
| - $remainingBytes = min( self::MAX_READ, max( $expectedSize * 2, 1048576 ) ); | ||
| - $compressed = fread( $fileHandle, $remainingBytes ); | ||
| - $delta = @gzuncompress( $compressed ) ?: ''; | ||
| - | ||
| - return $this->applyDelta( $base, $delta, $cap ); | ||
| - } | ||
| - | ||
| - private function handleRefDelta( $fileHandle, int $expectedSize, int $cap = 0 ): string { | ||
| - $baseSha = bin2hex( fread( $fileHandle, 20 ) ); | ||
| - | ||
| - if ( $cap > 0 ) { | ||
| - $base = $this->peek( $baseSha, $cap ) ?? ''; | ||
| - } else { | ||
| - $base = $this->read( $baseSha ) ?? ''; | ||
| - } | ||
| - | ||
| - $remainingBytes = min( self::MAX_READ, max( $expectedSize * 2, 1048576 ) ); | ||
| - $compressed = fread( $fileHandle, $remainingBytes ); | ||
| - $delta = @gzuncompress( $compressed ) ?: ''; | ||
| - | ||
| - return $this->applyDelta( $base, $delta, $cap ); | ||
| - } | ||
| - | ||
| - private function applyDelta( string $base, string $delta, int $cap = 0 ): string { | ||
| - $position = 0; | ||
| - $this->skipSize( $delta, $position ); | ||
| - $this->skipSize( $delta, $position ); | ||
| - | ||
| - $output = ''; | ||
| - $deltaLength = strlen( $delta ); | ||
| - | ||
| - while( $position < $deltaLength ) { | ||
| - if( $cap > 0 && strlen( $output ) >= $cap ) { | ||
| - break; | ||
| - } | ||
| - | ||
| - $opcode = ord( $delta[$position++] ); | ||
| - | ||
| - if( $opcode & 128 ) { | ||
| - $offset = 0; | ||
| - $length = 0; | ||
| - | ||
| - if( $opcode & 0x01 ) { | ||
| - $offset |= ord( $delta[$position++] ); | ||
| - } | ||
| - if( $opcode & 0x02 ) { | ||
| - $offset |= ord( $delta[$position++] ) << 8; | ||
| - } | ||
| - if( $opcode & 0x04 ) { | ||
| - $offset |= ord( $delta[$position++] ) << 16; | ||
| - } | ||
| - if( $opcode & 0x08 ) { | ||
| - $offset |= ord( $delta[$position++] ) << 24; | ||
| - } | ||
| - | ||
| - if( $opcode & 0x10 ) { | ||
| - $length |= ord( $delta[$position++] ); | ||
| - } | ||
| - if( $opcode & 0x20 ) { | ||
| - $length |= ord( $delta[$position++] ) << 8; | ||
| - } | ||
| - if( $opcode & 0x40 ) { | ||
| - $length |= ord( $delta[$position++] ) << 16; | ||
| - } | ||
| - | ||
| - if( $length === 0 ) { | ||
| - $length = 0x10000; | ||
| - } | ||
| - | ||
| - $output .= substr( $base, $offset, $length ); | ||
| - } else { | ||
| - $length = $opcode & 127; | ||
| - $output .= substr( $delta, $position, $length ); | ||
| - $position += $length; | ||
| - } | ||
| - } | ||
| - | ||
| - return $output; | ||
| - } | ||
| - | ||
| - private function readVarInt( $fileHandle ): array { | ||
| - $byte = ord( fread( $fileHandle, 1 ) ); | ||
| - $value = $byte & 15; | ||
| - $shift = 4; | ||
| - $first = $byte; | ||
| - | ||
| - while( $byte & 128 ) { | ||
| - $byte = ord( fread( $fileHandle, 1 ) ); | ||
| - $value |= (($byte & 127) << $shift); | ||
| - $shift += 7; | ||
| - } | ||
| - | ||
| - return ['value' => $value, 'byte' => $first]; | ||
| - } | ||
| - | ||
| - private function readDeltaTargetSize( $fileHandle, int $type ): int { | ||
| - if( $type === 6 ) { | ||
| - $byte = ord( fread( $fileHandle, 1 ) ); | ||
| - | ||
| - while( $byte & 128 ) { | ||
| - $byte = ord( fread( $fileHandle, 1 ) ); | ||
| - } | ||
| - } else { | ||
| - fseek( $fileHandle, 20, SEEK_CUR ); | ||
| - } | ||
| - | ||
| - $inflator = inflate_init( ZLIB_ENCODING_DEFLATE ); | ||
| - if( $inflator === false ) { | ||
| - return 0; | ||
| - } | ||
| - | ||
| - $header = ''; | ||
| - $attempts = 0; | ||
| - $maxAttempts = 64; | ||
| - | ||
| - while( !feof( $fileHandle ) && strlen( $header ) < 32 && $attempts < $maxAttempts ) { | ||
| + return $this->streamPackEntry( | ||
| + $handle, | ||
| + $info['offset'], | ||
| + $size, | ||
| + $callback | ||
| + ); | ||
| + } | ||
| + | ||
| + public function getSize( string $sha ): ?int { | ||
| + $info = $this->findPackInfo( $sha ); | ||
| + | ||
| + if( $info['offset'] === -1 ) { | ||
| + return null; | ||
| + } | ||
| + | ||
| + return $this->extractPackedSize( $info['file'], $info['offset'] ); | ||
| + } | ||
| + | ||
| + private function findPackInfo( string $sha ): array { | ||
| + if( !ctype_xdigit( $sha ) || strlen( $sha ) !== 40 ) { | ||
| + return ['offset' => -1]; | ||
| + } | ||
| + | ||
| + $binarySha = hex2bin( $sha ); | ||
| + | ||
| + if( $this->lastPack ) { | ||
| + $offset = $this->findInIdx( $this->lastPack, $binarySha ); | ||
| + | ||
| + if( $offset !== -1 ) { | ||
| + return $this->makeResult( $this->lastPack, $offset ); | ||
| + } | ||
| + } | ||
| + | ||
| + foreach( $this->packFiles as $indexFile ) { | ||
| + if( $indexFile === $this->lastPack ) { | ||
| + continue; | ||
| + } | ||
| + | ||
| + $offset = $this->findInIdx( $indexFile, $binarySha ); | ||
| + | ||
| + if( $offset !== -1 ) { | ||
| + $this->lastPack = $indexFile; | ||
| + | ||
| + return $this->makeResult( $indexFile, $offset ); | ||
| + } | ||
| + } | ||
| + | ||
| + return ['offset' => -1]; | ||
| + } | ||
| + | ||
| + private function makeResult( string $indexPath, int $offset ): array { | ||
| + return [ | ||
| + 'file' => str_replace( '.idx', '.pack', $indexPath ), | ||
| + 'offset' => $offset | ||
| + ]; | ||
| + } | ||
| + | ||
| + private function findInIdx( string $indexFile, string $binarySha ): int { | ||
| + $fileHandle = $this->getHandle( $indexFile ); | ||
| + | ||
| + if( !$fileHandle ) { | ||
| + return -1; | ||
| + } | ||
| + | ||
| + if( !isset( $this->fanoutCache[$indexFile] ) ) { | ||
| + fseek( $fileHandle, 0 ); | ||
| + | ||
| + if( fread( $fileHandle, 8 ) === "\377tOc\0\0\0\2" ) { | ||
| + $this->fanoutCache[$indexFile] = array_values( | ||
| + unpack( 'N*', fread( $fileHandle, 1024 ) ) | ||
| + ); | ||
| + } else { | ||
| + return -1; | ||
| + } | ||
| + } | ||
| + | ||
| + $fanout = $this->fanoutCache[$indexFile]; | ||
| + | ||
| + $firstByte = ord( $binarySha[0] ); | ||
| + $start = $firstByte === 0 ? 0 : $fanout[$firstByte - 1]; | ||
| + $end = $fanout[$firstByte]; | ||
| + | ||
| + if( $end <= $start ) { | ||
| + return -1; | ||
| + } | ||
| + | ||
| + $cacheKey = "$indexFile:$firstByte"; | ||
| + | ||
| + if( !isset( $this->shaBucketCache[$cacheKey] ) ) { | ||
| + $count = $end - $start; | ||
| + | ||
| + fseek( $fileHandle, 1032 + ($start * 20) ); | ||
| + | ||
| + $this->shaBucketCache[$cacheKey] = fread( $fileHandle, $count * 20 ); | ||
| + | ||
| + fseek( | ||
| + $fileHandle, | ||
| + 1032 + ($fanout[255] * 24) + ($start * 4) | ||
| + ); | ||
| + | ||
| + $this->offsetBucketCache[$cacheKey] = fread( $fileHandle, $count * 4 ); | ||
| + } | ||
| + | ||
| + $shaBlock = $this->shaBucketCache[$cacheKey]; | ||
| + $count = strlen( $shaBlock ) / 20; | ||
| + $low = 0; | ||
| + $high = $count - 1; | ||
| + $foundIdx = -1; | ||
| + | ||
| + while( $low <= $high ) { | ||
| + $mid = ($low + $high) >> 1; | ||
| + $compare = substr( $shaBlock, $mid * 20, 20 ); | ||
| + | ||
| + if( $compare < $binarySha ) { | ||
| + $low = $mid + 1; | ||
| + } elseif( $compare > $binarySha ) { | ||
| + $high = $mid - 1; | ||
| + } else { | ||
| + $foundIdx = $mid; | ||
| + break; | ||
| + } | ||
| + } | ||
| + | ||
| + if( $foundIdx === -1 ) { | ||
| + return -1; | ||
| + } | ||
| + | ||
| + $offsetData = substr( | ||
| + $this->offsetBucketCache[$cacheKey], | ||
| + $foundIdx * 4, | ||
| + 4 | ||
| + ); | ||
| + | ||
| + $offset = unpack( 'N', $offsetData )[1]; | ||
| + | ||
| + if( $offset & 0x80000000 ) { | ||
| + $packTotal = $fanout[255]; | ||
| + $pos64 = 1032 + ($packTotal * 28) + | ||
| + (($offset & 0x7FFFFFFF) * 8); | ||
| + | ||
| + fseek( $fileHandle, $pos64 ); | ||
| + | ||
| + $offset = unpack( 'J', fread( $fileHandle, 8 ) )[1]; | ||
| + } | ||
| + | ||
| + return (int)$offset; | ||
| + } | ||
| + | ||
| + private function readPackEntry( | ||
| + $fileHandle, | ||
| + int $offset, | ||
| + int $expectedSize, | ||
| + int $cap = 0 | ||
| + ): string { | ||
| + fseek( $fileHandle, $offset ); | ||
| + | ||
| + $header = $this->readVarInt( $fileHandle ); | ||
| + $type = ($header['byte'] >> 4) & 7; | ||
| + | ||
| + if( $type === 6 ) { | ||
| + return $this->handleOfsDelta( | ||
| + $fileHandle, | ||
| + $offset, | ||
| + $expectedSize, | ||
| + $cap | ||
| + ); | ||
| + } | ||
| + | ||
| + if( $type === 7 ) { | ||
| + return $this->handleRefDelta( $fileHandle, $expectedSize, $cap ); | ||
| + } | ||
| + | ||
| + return $this->decompressToString( $fileHandle, $expectedSize, $cap ); | ||
| + } | ||
| + | ||
| + private function streamPackEntry( | ||
| + $fileHandle, | ||
| + int $offset, | ||
| + int $expectedSize, | ||
| + callable $callback | ||
| + ): bool { | ||
| + fseek( $fileHandle, $offset ); | ||
| + | ||
| + $header = $this->readVarInt( $fileHandle ); | ||
| + $type = ($header['byte'] >> 4) & 7; | ||
| + | ||
| + if( $type === 6 || $type === 7 ) { | ||
| + return $this->streamDeltaObject( | ||
| + $fileHandle, | ||
| + $offset, | ||
| + $type, | ||
| + $expectedSize, | ||
| + $callback | ||
| + ); | ||
| + } | ||
| + | ||
| + return $this->streamDecompression( $fileHandle, $callback ); | ||
| + } | ||
| + | ||
| + private function streamDeltaObject( | ||
| + $fileHandle, | ||
| + int $offset, | ||
| + int $type, | ||
| + int $expectedSize, | ||
| + callable $callback | ||
| + ): bool { | ||
| + fseek( $fileHandle, $offset ); | ||
| + $this->readVarInt( $fileHandle ); | ||
| + | ||
| + if( $type === 6 ) { | ||
| + $byte = ord( fread( $fileHandle, 1 ) ); | ||
| + $negative = $byte & 127; | ||
| + | ||
| + while( $byte & 128 ) { | ||
| + $byte = ord( fread( $fileHandle, 1 ) ); | ||
| + $negative = (($negative + 1) << 7) | ($byte & 127); | ||
| + } | ||
| + | ||
| + $deltaPos = ftell( $fileHandle ); | ||
| + $baseOffset = $offset - $negative; | ||
| + | ||
| + $base = ''; | ||
| + | ||
| + $this->streamPackEntry( | ||
| + $fileHandle, | ||
| + $baseOffset, | ||
| + 0, | ||
| + function( $chunk ) use ( &$base ) { $base .= $chunk; } | ||
| + ); | ||
| + | ||
| + fseek( $fileHandle, $deltaPos ); | ||
| + } else { | ||
| + $baseSha = bin2hex( fread( $fileHandle, 20 ) ); | ||
| + | ||
| + $base = ''; | ||
| + $streamed = $this->stream( | ||
| + $baseSha, | ||
| + function( $chunk ) use ( &$base ) { $base .= $chunk; } | ||
| + ); | ||
| + | ||
| + if( !$streamed ) { | ||
| + return false; | ||
| + } | ||
| + } | ||
| + | ||
| + $compressed = fread( $fileHandle, self::MAX_READ ); | ||
| + $delta = @gzuncompress( $compressed ) ?: ''; | ||
| + | ||
| + $result = $this->applyDelta( $base, $delta ); | ||
| + | ||
| + $chunkSize = 8192; | ||
| + $length = strlen( $result ); | ||
| + | ||
| + for( $i = 0; $i < $length; $i += $chunkSize ) { | ||
| + $callback( substr( $result, $i, $chunkSize ) ); | ||
| + } | ||
| + | ||
| + return true; | ||
| + } | ||
| + | ||
| + private function streamDecompression( $fileHandle, callable $callback ): bool { | ||
| + $inflator = inflate_init( ZLIB_ENCODING_DEFLATE ); | ||
| + | ||
| + if( $inflator === false ) { | ||
| + return false; | ||
| + } | ||
| + | ||
| + while( !feof( $fileHandle ) ) { | ||
| + $chunk = fread( $fileHandle, 8192 ); | ||
| + | ||
| + if( $chunk === false || $chunk === '' ) { | ||
| + break; | ||
| + } | ||
| + | ||
| + $data = @inflate_add( $inflator, $chunk ); | ||
| + | ||
| + if( $data !== false && $data !== '' ) { | ||
| + $callback( $data ); | ||
| + } | ||
| + | ||
| + if( | ||
| + $data === false || | ||
| + inflate_get_status( $inflator ) === ZLIB_STREAM_END | ||
| + ) { | ||
| + break; | ||
| + } | ||
| + } | ||
| + | ||
| + return true; | ||
| + } | ||
| + | ||
| + private function decompressToString( | ||
| + $fileHandle, | ||
| + int $maxSize, | ||
| + int $cap = 0 | ||
| + ): string { | ||
| + $inflator = inflate_init( ZLIB_ENCODING_DEFLATE ); | ||
| + | ||
| + if( $inflator === false ) { | ||
| + return ''; | ||
| + } | ||
| + | ||
| + $result = ''; | ||
| + | ||
| + while( !feof( $fileHandle ) ) { | ||
| + $chunk = fread( $fileHandle, 8192 ); | ||
| + | ||
| + if( $chunk === false || $chunk === '' ) { | ||
| + break; | ||
| + } | ||
| + | ||
| + $data = @inflate_add( $inflator, $chunk ); | ||
| + | ||
| + if( $data !== false ) { | ||
| + $result .= $data; | ||
| + } | ||
| + | ||
| + if( $cap > 0 && strlen( $result ) >= $cap ) { | ||
| + return substr( $result, 0, $cap ); | ||
| + } | ||
| + | ||
| + if( | ||
| + $data === false || | ||
| + inflate_get_status( $inflator ) === ZLIB_STREAM_END | ||
| + ) { | ||
| + break; | ||
| + } | ||
| + } | ||
| + | ||
| + return $result; | ||
| + } | ||
| + | ||
| + private function extractPackedSize( string $packPath, int $offset ): int { | ||
| + $fileHandle = $this->getHandle( $packPath ); | ||
| + | ||
| + if( !$fileHandle ) { | ||
| + return 0; | ||
| + } | ||
| + | ||
| + fseek( $fileHandle, $offset ); | ||
| + | ||
| + $header = $this->readVarInt( $fileHandle ); | ||
| + $size = $header['value']; | ||
| + $type = ($header['byte'] >> 4) & 7; | ||
| + | ||
| + if( $type === 6 || $type === 7 ) { | ||
| + return $this->readDeltaTargetSize( $fileHandle, $type ); | ||
| + } | ||
| + | ||
| + return $size; | ||
| + } | ||
| + | ||
| + private function handleOfsDelta( | ||
| + $fileHandle, | ||
| + int $offset, | ||
| + int $expectedSize, | ||
| + int $cap = 0 | ||
| + ): string { | ||
| + $byte = ord( fread( $fileHandle, 1 ) ); | ||
| + $negative = $byte & 127; | ||
| + | ||
| + while( $byte & 128 ) { | ||
| + $byte = ord( fread( $fileHandle, 1 ) ); | ||
| + $negative = (($negative + 1) << 7) | ($byte & 127); | ||
| + } | ||
| + | ||
| + $currentPos = ftell( $fileHandle ); | ||
| + $baseOffset = $offset - $negative; | ||
| + | ||
| + fseek( $fileHandle, $baseOffset ); | ||
| + | ||
| + $baseHeader = $this->readVarInt( $fileHandle ); | ||
| + $baseSize = $baseHeader['value']; | ||
| + | ||
| + fseek( $fileHandle, $baseOffset ); | ||
| + | ||
| + $base = $this->readPackEntry( $fileHandle, $baseOffset, $baseSize, $cap ); | ||
| + | ||
| + fseek( $fileHandle, $currentPos ); | ||
| + | ||
| + $remainingBytes = min( self::MAX_READ, max( $expectedSize * 2, 1048576 ) ); | ||
| + $compressed = fread( $fileHandle, $remainingBytes ); | ||
| + $delta = @gzuncompress( $compressed ) ?: ''; | ||
| + | ||
| + return $this->applyDelta( $base, $delta, $cap ); | ||
| + } | ||
| + | ||
| + private function handleRefDelta( | ||
| + $fileHandle, | ||
| + int $expectedSize, | ||
| + int $cap = 0 | ||
| + ): string { | ||
| + $baseSha = bin2hex( fread( $fileHandle, 20 ) ); | ||
| + | ||
| + if( $cap > 0 ) { | ||
| + $base = $this->peek( $baseSha, $cap ) ?? ''; | ||
| + } else { | ||
| + $base = $this->read( $baseSha ) ?? ''; | ||
| + } | ||
| + | ||
| + $remainingBytes = min( self::MAX_READ, max( $expectedSize * 2, 1048576 ) ); | ||
| + $compressed = fread( $fileHandle, $remainingBytes ); | ||
| + $delta = @gzuncompress( $compressed ) ?: ''; | ||
| + | ||
| + return $this->applyDelta( $base, $delta, $cap ); | ||
| + } | ||
| + | ||
| + private function applyDelta( string $base, string $delta, int $cap = 0 ): string { | ||
| + $position = 0; | ||
| + | ||
| + $this->skipSize( $delta, $position ); | ||
| + $this->skipSize( $delta, $position ); | ||
| + | ||
| + $output = ''; | ||
| + $deltaLength = strlen( $delta ); | ||
| + | ||
| + while( $position < $deltaLength ) { | ||
| + if( $cap > 0 && strlen( $output ) >= $cap ) { | ||
| + break; | ||
| + } | ||
| + | ||
| + $opcode = ord( $delta[$position++] ); | ||
| + | ||
| + if( $opcode & 128 ) { | ||
| + $offset = 0; | ||
| + $length = 0; | ||
| + | ||
| + if( $opcode & 0x01 ) { $offset |= ord( $delta[$position++] ); } | ||
| + if( $opcode & 0x02 ) { $offset |= ord( $delta[$position++] ) << 8; } | ||
| + if( $opcode & 0x04 ) { $offset |= ord( $delta[$position++] ) << 16; } | ||
| + if( $opcode & 0x08 ) { $offset |= ord( $delta[$position++] ) << 24; } | ||
| + | ||
| + if( $opcode & 0x10 ) { $length |= ord( $delta[$position++] ); } | ||
| + if( $opcode & 0x20 ) { $length |= ord( $delta[$position++] ) << 8; } | ||
| + if( $opcode & 0x40 ) { $length |= ord( $delta[$position++] ) << 16; } | ||
| + | ||
| + if( $length === 0 ) { $length = 0x10000; } | ||
| + | ||
| + $output .= substr( $base, $offset, $length ); | ||
| + } else { | ||
| + $length = $opcode & 127; | ||
| + $output .= substr( $delta, $position, $length ); | ||
| + $position += $length; | ||
| + } | ||
| + } | ||
| + | ||
| + return $output; | ||
| + } | ||
| + | ||
| + private function readVarInt( $fileHandle ): array { | ||
| + $byte = ord( fread( $fileHandle, 1 ) ); | ||
| + $value = $byte & 15; | ||
| + $shift = 4; | ||
| + $first = $byte; | ||
| + | ||
| + while( $byte & 128 ) { | ||
| + $byte = ord( fread( $fileHandle, 1 ) ); | ||
| + $value |= (($byte & 127) << $shift); | ||
| + $shift += 7; | ||
| + } | ||
| + | ||
| + return ['value' => $value, 'byte' => $first]; | ||
| + } | ||
| + | ||
| + private function readDeltaTargetSize( $fileHandle, int $type ): int { | ||
| + if( $type === 6 ) { | ||
| + $byte = ord( fread( $fileHandle, 1 ) ); | ||
| + | ||
| + while( $byte & 128 ) { | ||
| + $byte = ord( fread( $fileHandle, 1 ) ); | ||
| + } | ||
| + } else { | ||
| + fseek( $fileHandle, 20, SEEK_CUR ); | ||
| + } | ||
| + | ||
| + $inflator = inflate_init( ZLIB_ENCODING_DEFLATE ); | ||
| + | ||
| + if( $inflator === false ) { | ||
| + return 0; | ||
| + } | ||
| + | ||
| + $header = ''; | ||
| + $attempts = 0; | ||
| + $maxAttempts = 64; | ||
| + | ||
| + while( | ||
| + !feof( $fileHandle ) && | ||
| + strlen( $header ) < 32 && | ||
| + $attempts < $maxAttempts | ||
| + ) { | ||
| $chunk = fread( $fileHandle, 512 ); | ||
| Config::init(); | ||
| -$router = new Router(Config::getReposPath()); | ||
| +$router = new Router( Config::getReposPath() ); | ||
| $page = $router->route(); | ||
| + | ||
| $page->render(); | ||
| protected $title; | ||
| - public function __construct(array $repositories) { | ||
| + public function __construct( array $repositories ) { | ||
| $this->repositories = $repositories; | ||
| } | ||
| - protected function renderLayout($contentCallback, $currentRepo = null) { | ||
| + protected function renderLayout( $contentCallback, $currentRepo = null ) { | ||
| ?> | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| - <title><?php echo Config::SITE_TITLE . ($this->title ? ' - ' . htmlspecialchars($this->title) : ''); ?></title> | ||
| - <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css"> | ||
| + <title><?php | ||
| + echo Config::SITE_TITLE . | ||
| + ($this->title ? ' - ' . htmlspecialchars( $this->title ) : ''); | ||
| + ?></title> | ||
| + <link rel="stylesheet" | ||
| + href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css"> | ||
| <link rel="stylesheet" href="repo.css"> | ||
| </head> | ||
| <body> | ||
| <div class="container"> | ||
| <header> | ||
| <h1><?php echo Config::SITE_TITLE; ?></h1> | ||
| <nav class="nav"> | ||
| <a href="?">Home</a> | ||
| - <?php if ($currentRepo): | ||
| - $safeName = urlencode($currentRepo['safe_name']); ?> | ||
| + <?php if( $currentRepo ): | ||
| + $safeName = urlencode( $currentRepo['safe_name'] ); ?> | ||
| <a href="?repo=<?php echo $safeName; ?>">Files</a> | ||
| <a href="?action=commits&repo=<?php echo $safeName; ?>">Commits</a> | ||
| <a href="?action=refs&repo=<?php echo $safeName; ?>">Branches</a> | ||
| <a href="?action=tags&repo=<?php echo $safeName; ?>">Tags</a> | ||
| <?php endif; ?> | ||
| - <?php if ($currentRepo): ?> | ||
| + <?php if( $currentRepo ): ?> | ||
| <div class="repo-selector"> | ||
| <label>Repository:</label> | ||
| <select onchange="window.location.href='?repo=' + encodeURIComponent(this.value)"> | ||
| - <?php foreach ($this->repositories as $r): ?> | ||
| - <option value="<?php echo htmlspecialchars($r['safe_name']); ?>" | ||
| + <?php foreach( $this->repositories as $r ): ?> | ||
| + <option value="<?php echo htmlspecialchars( $r['safe_name'] ); ?>" | ||
| <?php echo $r['safe_name'] === $currentRepo['safe_name'] ? 'selected' : ''; ?>> | ||
| - <?php echo htmlspecialchars($r['name']); ?> | ||
| + <?php echo htmlspecialchars( $r['name'] ); ?> | ||
| </option> | ||
| <?php endforeach; ?> | ||
| </select> | ||
| </div> | ||
| <?php endif; ?> | ||
| </nav> | ||
| - <?php if ($currentRepo): ?> | ||
| + <?php if( $currentRepo ): ?> | ||
| <div class="repo-info-banner"> | ||
| - <span class="current-repo">Current: <strong><?php echo htmlspecialchars($currentRepo['name']); ?></strong></span> | ||
| + <span class="current-repo"> | ||
| + Current: <strong><?php | ||
| + echo htmlspecialchars( $currentRepo['name'] ); | ||
| + ?></strong> | ||
| + </span> | ||
| </div> | ||
| <?php endif; ?> | ||
| </header> | ||
| - <?php call_user_func($contentCallback); ?> | ||
| + <?php call_user_func( $contentCallback ); ?> | ||
| </div> |
| public function __construct( Git $git, string $subPath ) { | ||
| - $this->git = $git; | ||
| + $this->git = $git; | ||
| $this->subPath = $subPath; | ||
| } |
| private $hash; | ||
| - public function __construct(array $repositories, array $currentRepo, Git $git, string $hash) { | ||
| - parent::__construct($repositories); | ||
| + public function __construct( | ||
| + array $repositories, | ||
| + array $currentRepo, | ||
| + Git $git, | ||
| + string $hash | ||
| + ) { | ||
| + parent::__construct( $repositories ); | ||
| $this->currentRepo = $currentRepo; | ||
| $this->git = $git; | ||
| $this->hash = $hash; | ||
| $this->title = $currentRepo['name']; | ||
| } | ||
| public function render() { | ||
| - $this->renderLayout(function() { | ||
| - // Use local private $git | ||
| + $this->renderLayout( function() { | ||
| $main = $this->git->getMainBranch(); | ||
| - if (!$main) { | ||
| - echo '<div class="empty-state"><h3>No branches</h3><p>Empty repository.</p></div>'; | ||
| + if( !$main ) { | ||
| + echo '<div class="empty-state"><h3>No branches</h3>' . | ||
| + '<p>Empty repository.</p></div>'; | ||
| return; | ||
| } | ||
| $this->renderBreadcrumbs(); | ||
| - echo '<h2>Commit History <span class="branch-badge">' . htmlspecialchars($main['name']) . '</span></h2>'; | ||
| + | ||
| + echo '<h2>Commit History <span class="branch-badge">' . | ||
| + htmlspecialchars( $main['name'] ) . '</span></h2>'; | ||
| echo '<div class="commit-list">'; | ||
| $start = $this->hash ?: $main['hash']; | ||
| - $repoParam = '&repo=' . urlencode($this->currentRepo['safe_name']); | ||
| + $repoParam = '&repo=' . urlencode( $this->currentRepo['safe_name'] ); | ||
| - $this->git->history($start, 100, function($commit) use ($repoParam) { | ||
| - $msg = htmlspecialchars(explode("\n", $commit->message)[0]); | ||
| + $this->git->history( $start, 100, function( $commit ) use ( $repoParam ) { | ||
| + $msg = htmlspecialchars( explode( "\n", $commit->message )[0] ); | ||
| + | ||
| echo '<div class="commit-row">'; | ||
| - echo '<a href="?action=commit&hash=' . $commit->sha . $repoParam . '" class="sha">' . substr($commit->sha, 0, 7) . '</a>'; | ||
| + echo '<a href="?action=commit&hash=' . $commit->sha . $repoParam . | ||
| + '" class="sha">' . substr( $commit->sha, 0, 7 ) . '</a>'; | ||
| echo '<span class="message">' . $msg . '</span>'; | ||
| - echo '<span class="meta">' . htmlspecialchars($commit->author) . ' • ' . date('Y-m-d', $commit->date) . '</span>'; | ||
| + echo '<span class="meta">' . htmlspecialchars( $commit->author ) . | ||
| + ' • ' . date( 'Y-m-d', $commit->date ) . '</span>'; | ||
| echo '</div>'; | ||
| - }); | ||
| + } ); | ||
| + | ||
| echo '</div>'; | ||
| - }, $this->currentRepo); | ||
| + }, $this->currentRepo ); | ||
| } | ||
| private function renderBreadcrumbs() { | ||
| $repoUrl = '?repo=' . urlencode( $this->currentRepo['safe_name'] ); | ||
| - | ||
| $crumbs = [ | ||
| '<a href="?">Repositories</a>', | ||
| - '<a href="' . $repoUrl . '">' . htmlspecialchars($this->currentRepo['name']) . '</a>', | ||
| + '<a href="' . $repoUrl . '">' . | ||
| + htmlspecialchars( $this->currentRepo['name'] ) . '</a>', | ||
| 'Commits' | ||
| ]; | ||
| - echo '<div class="breadcrumb">' . implode(' / ', $crumbs) . '</div>'; | ||
| + echo '<div class="breadcrumb">' . implode( ' / ', $crumbs ) . '</div>'; | ||
| } | ||
| } |
| private $hash; | ||
| - public function __construct(array $repositories, array $currentRepo, Git $git, string $hash) { | ||
| - parent::__construct($repositories); | ||
| + public function __construct( | ||
| + array $repositories, | ||
| + array $currentRepo, | ||
| + Git $git, | ||
| + string $hash | ||
| + ) { | ||
| + parent::__construct( $repositories ); | ||
| $this->currentRepo = $currentRepo; | ||
| $this->git = $git; | ||
| $this->hash = $hash; | ||
| - $this->title = substr($hash, 0, 7); | ||
| + $this->title = substr( $hash, 0, 7 ); | ||
| } | ||
| public function render() { | ||
| - $this->renderLayout(function() { | ||
| - $commitData = $this->git->read($this->hash); | ||
| - $diffEngine = new GitDiff($this->git); | ||
| - | ||
| - $lines = explode("\n", $commitData); | ||
| + $this->renderLayout( function() { | ||
| + $commitData = $this->git->read( $this->hash ); | ||
| + $diffEngine = new GitDiff( $this->git ); | ||
| + $lines = explode( "\n", $commitData ); | ||
| $msg = ''; | ||
| $isMsg = false; | ||
| $headers = []; | ||
| - foreach ($lines as $line) { | ||
| - if ($line === '') { $isMsg = true; continue; } | ||
| - if ($isMsg) { $msg .= $line . "\n"; } | ||
| - else { | ||
| - if (preg_match('/^(\w+) (.*)$/', $line, $m)) $headers[$m[1]] = $m[2]; | ||
| + | ||
| + foreach( $lines as $line ) { | ||
| + if( $line === '' ) { | ||
| + $isMsg = true; | ||
| + continue; | ||
| + } | ||
| + | ||
| + if( $isMsg ) { | ||
| + $msg .= $line . "\n"; | ||
| + } else { | ||
| + if( preg_match( '/^(\w+) (.*)$/', $line, $m ) ) { | ||
| + $headers[$m[1]] = $m[2]; | ||
| + } | ||
| } | ||
| } | ||
| - $changes = $diffEngine->compare($this->hash); | ||
| + $changes = $diffEngine->compare( $this->hash ); | ||
| $this->renderBreadcrumbs(); | ||
| - // Fix 1: Redact email address | ||
| $author = $headers['author'] ?? 'Unknown'; | ||
| - $author = preg_replace('/<[^>]+>/', '<email>', $author); | ||
| + $author = preg_replace( '/<[^>]+>/', '<email>', $author ); | ||
| echo '<div class="commit-details">'; | ||
| echo '<div class="commit-header">'; | ||
| - echo '<h1 class="commit-title">' . htmlspecialchars(trim($msg)) . '</h1>'; | ||
| + echo '<h1 class="commit-title">' . htmlspecialchars( trim( $msg ) ) . '</h1>'; | ||
| echo '<div class="commit-info">'; | ||
| - echo '<div class="commit-info-row"><span class="commit-info-label">Author</span><span class="commit-author">' . htmlspecialchars($author) . '</span></div>'; | ||
| - echo '<div class="commit-info-row"><span class="commit-info-label">Commit</span><span class="commit-info-value">' . $this->hash . '</span></div>'; | ||
| + echo '<div class="commit-info-row"><span class="commit-info-label">Author</span>' . | ||
| + '<span class="commit-author">' . htmlspecialchars( $author ) . '</span></div>'; | ||
| + echo '<div class="commit-info-row"><span class="commit-info-label">Commit</span>' . | ||
| + '<span class="commit-info-value">' . $this->hash . '</span></div>'; | ||
| - if (isset($headers['parent'])) { | ||
| - // Fix 2: Use '&' instead of '?' because parameters (action & hash) already exist | ||
| - $repoUrl = '&repo=' . urlencode($this->currentRepo['safe_name']); | ||
| - echo '<div class="commit-info-row"><span class="commit-info-label">Parent</span><span class="commit-info-value">'; | ||
| - echo '<a href="?action=commit&hash=' . $headers['parent'] . $repoUrl . '" class="parent-link">' . substr($headers['parent'], 0, 7) . '</a>'; | ||
| - echo '</span></div>'; | ||
| + if( isset( $headers['parent'] ) ) { | ||
| + $repoUrl = '&repo=' . urlencode( $this->currentRepo['safe_name'] ); | ||
| + | ||
| + echo '<div class="commit-info-row"><span class="commit-info-label">Parent</span>' . | ||
| + '<span class="commit-info-value">'; | ||
| + echo '<a href="?action=commit&hash=' . $headers['parent'] . $repoUrl . | ||
| + '" class="parent-link">' . substr( $headers['parent'], 0, 7 ) . '</a>'; | ||
| + echo '</span></div>'; | ||
| } | ||
| + | ||
| echo '</div></div></div>'; | ||
| echo '<div class="diff-container">'; | ||
| - foreach ($changes as $change) { | ||
| - $this->renderFileDiff($change); | ||
| + | ||
| + foreach( $changes as $change ) { | ||
| + $this->renderFileDiff( $change ); | ||
| } | ||
| - if (empty($changes)) { | ||
| - echo '<div class="empty-state"><p>No changes detected.</p></div>'; | ||
| + | ||
| + if( empty( $changes ) ) { | ||
| + echo '<div class="empty-state"><p>No changes detected.</p></div>'; | ||
| } | ||
| - echo '</div>'; | ||
| - }, $this->currentRepo); | ||
| + echo '</div>'; | ||
| + }, $this->currentRepo ); | ||
| } | ||
| - private function renderFileDiff($change) { | ||
| + private function renderFileDiff( $change ) { | ||
| $statusIcon = 'fa-file'; | ||
| $statusClass = ''; | ||
| - if ($change['type'] === 'A') { $statusIcon = 'fa-plus-circle'; $statusClass = 'status-add'; } | ||
| - if ($change['type'] === 'D') { $statusIcon = 'fa-minus-circle'; $statusClass = 'status-del'; } | ||
| - if ($change['type'] === 'M') { $statusIcon = 'fa-pencil-alt'; $statusClass = 'status-mod'; } | ||
| + if( $change['type'] === 'A' ) { | ||
| + $statusIcon = 'fa-plus-circle'; | ||
| + $statusClass = 'status-add'; | ||
| + } | ||
| + | ||
| + if( $change['type'] === 'D' ) { | ||
| + $statusIcon = 'fa-minus-circle'; | ||
| + $statusClass = 'status-del'; | ||
| + } | ||
| + | ||
| + if( $change['type'] === 'M' ) { | ||
| + $statusIcon = 'fa-pencil-alt'; | ||
| + $statusClass = 'status-mod'; | ||
| + } | ||
| echo '<div class="diff-file">'; | ||
| echo '<div class="diff-header">'; | ||
| - echo '<span class="diff-status ' . $statusClass . '"><i class="fa ' . $statusIcon . '"></i></span>'; | ||
| - echo '<span class="diff-path">' . htmlspecialchars($change['path']) . '</span>'; | ||
| + echo '<span class="diff-status ' . $statusClass . '">' . | ||
| + '<i class="fa ' . $statusIcon . '"></i></span>'; | ||
| + echo '<span class="diff-path">' . htmlspecialchars( $change['path'] ) . '</span>'; | ||
| echo '</div>'; | ||
| - if ($change['is_binary']) { | ||
| - echo '<div class="diff-binary">Binary files differ</div>'; | ||
| + if( $change['is_binary'] ) { | ||
| + echo '<div class="diff-binary">Binary files differ</div>'; | ||
| } else { | ||
| echo '<div class="diff-content">'; | ||
| echo '<table><tbody>'; | ||
| - foreach ($change['hunks'] as $line) { | ||
| - if (isset($line['t']) && $line['t'] === 'gap') { | ||
| - echo '<tr class="diff-gap"><td colspan="3">...</td></tr>'; | ||
| - continue; | ||
| - } | ||
| + foreach( $change['hunks'] as $line ) { | ||
| + if( isset( $line['t'] ) && $line['t'] === 'gap' ) { | ||
| + echo '<tr class="diff-gap"><td colspan="3">...</td></tr>'; | ||
| + continue; | ||
| + } | ||
| - $class = 'diff-ctx'; | ||
| - $char = ' '; | ||
| - if ($line['t'] === '+') { $class = 'diff-add'; $char = '+'; } | ||
| - if ($line['t'] === '-') { $class = 'diff-del'; $char = '-'; } | ||
| + $class = 'diff-ctx'; | ||
| + $char = ' '; | ||
| - echo '<tr class="' . $class . '">'; | ||
| - echo '<td class="diff-num" data-num="' . $line['no'] . '"></td>'; | ||
| - echo '<td class="diff-num" data-num="' . $line['nn'] . '"></td>'; | ||
| - echo '<td class="diff-code"><span class="diff-marker">' . $char . '</span>' . htmlspecialchars($line['l']) . '</td>'; | ||
| - echo '</tr>'; | ||
| + if( $line['t'] === '+' ) { | ||
| + $class = 'diff-add'; | ||
| + $char = '+'; | ||
| + } | ||
| + | ||
| + if( $line['t'] === '-' ) { | ||
| + $class = 'diff-del'; | ||
| + $char = '-'; | ||
| + } | ||
| + | ||
| + echo '<tr class="' . $class . '">'; | ||
| + echo '<td class="diff-num" data-num="' . $line['no'] . '"></td>'; | ||
| + echo '<td class="diff-num" data-num="' . $line['nn'] . '"></td>'; | ||
| + echo '<td class="diff-code"><span class="diff-marker">' . $char . '</span>' . | ||
| + htmlspecialchars( $line['l'] ) . '</td>'; | ||
| + echo '</tr>'; | ||
| } | ||
| echo '</tbody></table>'; | ||
| echo '</div>'; | ||
| } | ||
| + | ||
| echo '</div>'; | ||
| } | ||
| private function renderBreadcrumbs() { | ||
| - $safeName = urlencode($this->currentRepo['safe_name']); | ||
| - | ||
| + $safeName = urlencode( $this->currentRepo['safe_name'] ); | ||
| $crumbs = [ | ||
| '<a href="?">Repositories</a>', | ||
| - '<a href="?repo=' . $safeName . '">' . htmlspecialchars($this->currentRepo['name']) . '</a>', | ||
| - // Fix 3: Use '&' separator for the repo parameter | ||
| + '<a href="?repo=' . $safeName . '">' . | ||
| + htmlspecialchars( $this->currentRepo['name'] ) . '</a>', | ||
| '<a href="?action=commits&repo=' . $safeName . '">Commits</a>', | ||
| - substr($this->hash, 0, 7) | ||
| + substr( $this->hash, 0, 7 ) | ||
| ]; | ||
| - echo '<div class="breadcrumb">' . implode(' / ', $crumbs) . '</div>'; | ||
| + | ||
| + echo '<div class="breadcrumb">' . implode( ' / ', $crumbs ) . '</div>'; | ||
| } | ||
| } |
| private $hash; | ||
| - public function __construct(array $repositories, array $currentRepo, Git $git, string $hash = '') { | ||
| - parent::__construct($repositories); | ||
| - | ||
| + public function __construct( | ||
| + array $repositories, | ||
| + array $currentRepo, | ||
| + Git $git, | ||
| + string $hash = '' | ||
| + ) { | ||
| + parent::__construct( $repositories ); | ||
| $this->currentRepo = $currentRepo; | ||
| - $this->git = $git; | ||
| - $this->hash = $hash; | ||
| - $this->title = $currentRepo['name']; | ||
| + $this->git = $git; | ||
| + $this->hash = $hash; | ||
| + $this->title = $currentRepo['name']; | ||
| } | ||
| public function render() { | ||
| - $this->renderLayout(function() { | ||
| + $this->renderLayout( function() { | ||
| $main = $this->git->getMainBranch(); | ||
| - if (!$main) { | ||
| + if( !$main ) { | ||
| echo '<div class="empty-state"><h3>No branches</h3></div>'; | ||
| } else { | ||
| - $target = $this->hash ?: $main['hash']; | ||
| + $target = $this->hash ?: $main['hash']; | ||
| $entries = []; | ||
| - $this->git->walk($target, function($file) use (&$entries) { | ||
| + $this->git->walk( $target, function( $file ) use ( &$entries ) { | ||
| $entries[] = $file; | ||
| - }); | ||
| + } ); | ||
| - if (!empty($entries)) { | ||
| - $this->renderTree($main, $target, $entries); | ||
| + if( !empty( $entries ) ) { | ||
| + $this->renderTree( $main, $target, $entries ); | ||
| } else { | ||
| - $this->renderBlob($target); | ||
| + $this->renderBlob( $target ); | ||
| } | ||
| } | ||
| - }, $this->currentRepo); | ||
| + }, $this->currentRepo ); | ||
| } | ||
| - private function renderTree($main, $targetHash, $entries) { | ||
| + private function renderTree( $main, $targetHash, $entries ) { | ||
| $path = $_GET['name'] ?? ''; | ||
| - $this->renderBreadcrumbs($targetHash, 'Tree'); | ||
| + $this->renderBreadcrumbs( $targetHash, 'Tree' ); | ||
| - echo '<h2>' . htmlspecialchars($this->currentRepo['name']) . | ||
| + echo '<h2>' . htmlspecialchars( $this->currentRepo['name'] ) . | ||
| ' <span class="branch-badge">' . | ||
| - htmlspecialchars($main['name']) . '</span></h2>'; | ||
| + htmlspecialchars( $main['name'] ) . '</span></h2>'; | ||
| - usort($entries, function($a, $b) { | ||
| - return $a->compare($b); | ||
| - }); | ||
| + usort( $entries, function( $a, $b ) { | ||
| + return $a->compare( $b ); | ||
| + } ); | ||
| echo '<div class="file-list">'; | ||
| - $renderer = new HtmlFileRenderer($this->currentRepo['safe_name'], $path); | ||
| + $renderer = new HtmlFileRenderer( $this->currentRepo['safe_name'], $path ); | ||
| - foreach ($entries as $file) { | ||
| - $file->render($renderer); | ||
| + foreach( $entries as $file ) { | ||
| + $file->render( $renderer ); | ||
| } | ||
| echo '</div>'; | ||
| } | ||
| - | ||
| - private function renderBlob($targetHash) { | ||
| - $repoParam = '&repo=' . urlencode($this->currentRepo['safe_name']); | ||
| - $filename = $_GET['name'] ?? ''; | ||
| - $file = $this->git->readFile($targetHash, $filename); | ||
| - $size = $this->git->getObjectSize($targetHash); | ||
| - $renderer = new HtmlFileRenderer($this->currentRepo['safe_name']); | ||
| + private function renderBlob( $targetHash ) { | ||
| + $repoParam = '&repo=' . urlencode( $this->currentRepo['safe_name'] ); | ||
| + $filename = $_GET['name'] ?? ''; | ||
| + $file = $this->git->readFile( $targetHash, $filename ); | ||
| + $size = $this->git->getObjectSize( $targetHash ); | ||
| + $renderer = new HtmlFileRenderer( $this->currentRepo['safe_name'] ); | ||
| - $this->renderBreadcrumbs($targetHash, 'File'); | ||
| + $this->renderBreadcrumbs( $targetHash, 'File' ); | ||
| - if ($size === 0) { | ||
| - $this->renderDownloadState($targetHash, "This file is empty."); | ||
| + if( $size === 0 ) { | ||
| + $this->renderDownloadState( $targetHash, "This file is empty." ); | ||
| } else { | ||
| - $rawUrl = '?action=raw&hash=' . $targetHash . $repoParam . '&name=' . urlencode($filename); | ||
| + $rawUrl = '?action=raw&hash=' . $targetHash . $repoParam . | ||
| + '&name=' . urlencode( $filename ); | ||
| - if (!$file->renderMedia($rawUrl)) { | ||
| - if ($file->isText()) { | ||
| - if ($size > 524288) { | ||
| + if( !$file->renderMedia( $rawUrl ) ) { | ||
| + if( $file->isText() ) { | ||
| + if( $size > 524288 ) { | ||
| ob_start(); | ||
| - $file->renderSize($renderer); | ||
| + $file->renderSize( $renderer ); | ||
| $sizeStr = ob_get_clean(); | ||
| - $this->renderDownloadState($targetHash, "File is too large to display ($sizeStr)."); | ||
| + | ||
| + $this->renderDownloadState( | ||
| + $targetHash, | ||
| + "File is too large to display ($sizeStr)." | ||
| + ); | ||
| } else { | ||
| $content = ''; | ||
| - $this->git->stream($targetHash, function($d) use (&$content) { $content .= $d; }); | ||
| - echo '<div class="blob-content"><pre class="blob-code">' . htmlspecialchars($content) . '</pre></div>'; | ||
| + | ||
| + $this->git->stream( $targetHash, function( $d ) use ( &$content ) { | ||
| + $content .= $d; | ||
| + } ); | ||
| + | ||
| + echo '<div class="blob-content"><pre class="blob-code">' . | ||
| + htmlspecialchars( $content ) . '</pre></div>'; | ||
| } | ||
| } else { | ||
| - $this->renderDownloadState($targetHash, "This is a binary file."); | ||
| + $this->renderDownloadState( $targetHash, "This is a binary file." ); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| - private function renderDownloadState($hash, $reason) { | ||
| + private function renderDownloadState( $hash, $reason ) { | ||
| $filename = $_GET['name'] ?? ''; | ||
| - $url = '?action=raw&hash=' . $hash . '&repo=' . urlencode($this->currentRepo['safe_name']) . '&name=' . urlencode($filename); | ||
| + $url = '?action=raw&hash=' . $hash . '&repo=' . | ||
| + urlencode( $this->currentRepo['safe_name'] ) . | ||
| + '&name=' . urlencode( $filename ); | ||
| echo '<div class="empty-state download-state">'; | ||
| - echo '<p>' . htmlspecialchars($reason) . '</p>'; | ||
| - echo '<a href="' . $url . '" class="btn-download">Download Raw File</a>'; | ||
| + echo '<p>' . htmlspecialchars( $reason ) . '</p>'; | ||
| + echo '<a href="' . $url . '" class="btn-download">Download Raw File</a>'; | ||
| echo '</div>'; | ||
| } | ||
| - | ||
| - private function renderBreadcrumbs($hash, $type) { | ||
| - $repoUrl = '?repo=' . urlencode($this->currentRepo['safe_name']); | ||
| - $path = $_GET['name'] ?? ''; | ||
| + private function renderBreadcrumbs( $hash, $type ) { | ||
| + $repoUrl = '?repo=' . urlencode( $this->currentRepo['safe_name'] ); | ||
| + $path = $_GET['name'] ?? ''; | ||
| $crumbs = [ | ||
| '<a href="?">Repositories</a>', | ||
| - '<a href="' . $repoUrl . '">' . htmlspecialchars($this->currentRepo['name']) . '</a>' | ||
| + '<a href="' . $repoUrl . '">' . | ||
| + htmlspecialchars( $this->currentRepo['name'] ) . '</a>' | ||
| ]; | ||
| - if ($path) { | ||
| - $parts = explode('/', trim($path, '/')); | ||
| - $acc = ''; | ||
| - foreach ($parts as $idx => $part) { | ||
| + if( $path ) { | ||
| + $parts = explode( '/', trim( $path, '/' ) ); | ||
| + $acc = ''; | ||
| + | ||
| + foreach( $parts as $idx => $part ) { | ||
| $acc .= ($idx === 0 ? '' : '/') . $part; | ||
| - if ($idx === count($parts) - 1) { | ||
| - $crumbs[] = htmlspecialchars($part); | ||
| + | ||
| + if( $idx === count( $parts ) - 1 ) { | ||
| + $crumbs[] = htmlspecialchars( $part ); | ||
| } else { | ||
| - $crumbs[] = '<a href="' . $repoUrl . '&name=' . urlencode($acc) . '">' . | ||
| - htmlspecialchars($part) . '</a>'; | ||
| + $crumbs[] = '<a href="' . $repoUrl . '&name=' . urlencode( $acc ) . '">' . | ||
| + htmlspecialchars( $part ) . '</a>'; | ||
| } | ||
| } | ||
| - } elseif ($this->hash) { | ||
| - $crumbs[] = $type . ' ' . substr($hash, 0, 7); | ||
| + } elseif( $this->hash ) { | ||
| + $crumbs[] = $type . ' ' . substr( $hash, 0, 7 ); | ||
| } | ||
| - echo '<div class="breadcrumb">' . implode(' / ', $crumbs) . '</div>'; | ||
| + echo '<div class="breadcrumb">' . implode( ' / ', $crumbs ) . '</div>'; | ||
| } | ||
| } |
| private $git; | ||
| - public function __construct(array $repositories, Git $git) { | ||
| - parent::__construct($repositories); | ||
| + public function __construct( array $repositories, Git $git ) { | ||
| + parent::__construct( $repositories ); | ||
| $this->git = $git; | ||
| } | ||
| public function render() { | ||
| - $this->renderLayout(function() { | ||
| + $this->renderLayout( function() { | ||
| echo '<h2>Repositories</h2>'; | ||
| - if (empty($this->repositories)) { | ||
| + | ||
| + if( empty( $this->repositories ) ) { | ||
| echo '<div class="empty-state">No repositories found.</div>'; | ||
| return; | ||
| } | ||
| + | ||
| echo '<div class="repo-grid">'; | ||
| - foreach ($this->repositories as $repo) { | ||
| - $this->renderRepoCard($repo); | ||
| + | ||
| + foreach( $this->repositories as $repo ) { | ||
| + $this->renderRepoCard( $repo ); | ||
| } | ||
| + | ||
| echo '</div>'; | ||
| - }); | ||
| + } ); | ||
| } | ||
| - private function renderRepoCard($repo) { | ||
| - $this->git->setRepository($repo['path']); | ||
| + private function renderRepoCard( $repo ) { | ||
| + $this->git->setRepository( $repo['path'] ); | ||
| $main = $this->git->getMainBranch(); | ||
| + $stats = [ | ||
| + 'branches' => 0, | ||
| + 'tags' => 0 | ||
| + ]; | ||
| - $stats = ['branches' => 0, 'tags' => 0]; | ||
| - $this->git->eachBranch(function() use (&$stats) { $stats['branches']++; }); | ||
| - $this->git->eachTag(function() use (&$stats) { $stats['tags']++; }); | ||
| + $this->git->eachBranch( function() use ( &$stats ) { | ||
| + $stats['branches']++; | ||
| + } ); | ||
| - echo '<a href="?repo=' . urlencode($repo['safe_name']) . '" class="repo-card">'; | ||
| - echo '<h3>' . htmlspecialchars($repo['name']) . '</h3>'; | ||
| + $this->git->eachTag( function() use ( &$stats ) { | ||
| + $stats['tags']++; | ||
| + } ); | ||
| + echo '<a href="?repo=' . urlencode( $repo['safe_name'] ) . '" class="repo-card">'; | ||
| + echo '<h3>' . htmlspecialchars( $repo['name'] ) . '</h3>'; | ||
| echo '<p class="repo-meta">'; | ||
| $branchLabel = $stats['branches'] === 1 ? 'branch' : 'branches'; | ||
| $tagLabel = $stats['tags'] === 1 ? 'tag' : 'tags'; | ||
| - echo $stats['branches'] . ' ' . $branchLabel . ', ' . $stats['tags'] . ' ' . $tagLabel; | ||
| + echo $stats['branches'] . ' ' . $branchLabel . ', ' . | ||
| + $stats['tags'] . ' ' . $tagLabel; | ||
| - if ($main) { | ||
| + if( $main ) { | ||
| echo ', '; | ||
| - $this->git->history('HEAD', 1, function($c) use ($repo) { | ||
| - $renderer = new HtmlFileRenderer($repo['safe_name']); | ||
| - $renderer->renderTime($c->date); | ||
| - }); | ||
| + | ||
| + $this->git->history( 'HEAD', 1, function( $c ) use ( $repo ) { | ||
| + $renderer = new HtmlFileRenderer( $repo['safe_name'] ); | ||
| + $renderer->renderTime( $c->date ); | ||
| + } ); | ||
| } | ||
| + | ||
| echo '</p>'; | ||
| $descPath = $repo['path'] . '/description'; | ||
| - if (file_exists($descPath)) { | ||
| - $description = trim(file_get_contents($descPath)); | ||
| - if ($description !== '') { | ||
| - echo '<p style="margin-top: 1.5em;">' . htmlspecialchars($description) . '</p>'; | ||
| + | ||
| + if( file_exists( $descPath ) ) { | ||
| + $description = trim( file_get_contents( $descPath ) ); | ||
| + | ||
| + if( $description !== '' ) { | ||
| + echo '<p style="margin-top: 1.5em;">' . | ||
| + htmlspecialchars( $description ) . '</p>'; | ||
| } | ||
| } |
| private $hash; | ||
| - public function __construct($git, $hash) { | ||
| + public function __construct( $git, $hash ) { | ||
| $this->git = $git; | ||
| $this->hash = $hash; | ||
| } | ||
| public function render() { | ||
| - $filename = basename($_GET['name'] ?? '') ?: 'file'; | ||
| - $file = $this->git->readFile($this->hash, $filename); | ||
| + $filename = basename( $_GET['name'] ?? '' ) ?: 'file'; | ||
| + $file = $this->git->readFile( $this->hash, $filename ); | ||
| - while (ob_get_level()) { | ||
| + while( ob_get_level() ) { | ||
| ob_end_clean(); | ||
| } | ||
| $file->emitRawHeaders(); | ||
| - | ||
| - $this->git->stream($this->hash, function($d) { | ||
| + $this->git->stream( $this->hash, function( $d ) { | ||
| echo $d; | ||
| - }); | ||
| + } ); | ||
| exit; |
| private $git; | ||
| - public function __construct(array $repositories, array $currentRepo, Git $git) { | ||
| - parent::__construct($repositories); | ||
| + public function __construct( | ||
| + array $repositories, | ||
| + array $currentRepo, | ||
| + Git $git | ||
| + ) { | ||
| + parent::__construct( $repositories ); | ||
| $this->currentRepo = $currentRepo; | ||
| $this->git = $git; | ||
| $this->title = $currentRepo['name'] . ' - Tags'; | ||
| } | ||
| public function render() { | ||
| - $this->renderLayout(function() { | ||
| + $this->renderLayout( function() { | ||
| $this->renderBreadcrumbs(); | ||
| $tags = []; | ||
| - $this->git->eachTag(function(Tag $tag) use (&$tags) { | ||
| + | ||
| + $this->git->eachTag( function( Tag $tag ) use ( &$tags ) { | ||
| $tags[] = $tag; | ||
| - }); | ||
| + } ); | ||
| - usort($tags, function(Tag $a, Tag $b) { | ||
| - return $a->compare($b); | ||
| - }); | ||
| + usort( $tags, function( Tag $a, Tag $b ) { | ||
| + return $a->compare( $b ); | ||
| + } ); | ||
| - $renderer = new HtmlTagRenderer($this->currentRepo['safe_name']); | ||
| + $renderer = new HtmlTagRenderer( $this->currentRepo['safe_name'] ); | ||
| - if (empty($tags)) { | ||
| - echo '<tr><td colspan="5"><div class="empty-state"><p>No tags found.</p></div></td></tr>'; | ||
| + if( empty( $tags ) ) { | ||
| + echo '<tr><td colspan="5"><div class="empty-state">' . | ||
| + '<p>No tags found.</p></div></td></tr>'; | ||
| } else { | ||
| - foreach ($tags as $tag) { | ||
| - $tag->render($renderer); | ||
| + foreach( $tags as $tag ) { | ||
| + $tag->render( $renderer ); | ||
| } | ||
| } | ||
| echo '</tbody>'; | ||
| echo '</table>'; | ||
| - }, $this->currentRepo); | ||
| + }, $this->currentRepo ); | ||
| } | ||
| private function renderBreadcrumbs() { | ||
| - $repoUrl = '?repo=' . urlencode($this->currentRepo['safe_name']); | ||
| - | ||
| + $repoUrl = '?repo=' . urlencode( $this->currentRepo['safe_name'] ); | ||
| $crumbs = [ | ||
| '<a href="?">Repositories</a>', | ||
| - '<a href="' . $repoUrl . '">' . htmlspecialchars($this->currentRepo['name']) . '</a>', | ||
| + '<a href="' . $repoUrl . '">' . | ||
| + htmlspecialchars( $this->currentRepo['name'] ) . '</a>', | ||
| 'Tags' | ||
| ]; | ||
| - echo '<div class="breadcrumb">' . implode(' / ', $crumbs) . '</div>'; | ||
| + echo '<div class="breadcrumb">' . implode( ' / ', $crumbs ) . '</div>'; | ||
| } | ||
| } | ||
| public function __construct( string $repoSafeName, string $currentPath = '' ) { | ||
| $this->repoSafeName = $repoSafeName; | ||
| - $this->currentPath = trim( $currentPath, '/' ); | ||
| + $this->currentPath = trim( $currentPath, '/' ); | ||
| } | ||
| int $size = 0 | ||
| ): void { | ||
| - $fullPath = ($this->currentPath===''?'':$this->currentPath.'/') . $name; | ||
| - $url = '?repo=' . urlencode( $this->repoSafeName ) . '&hash=' . $sha . '&name=' . urlencode( $fullPath ); | ||
| + $fullPath = ($this->currentPath === '' ? '' : $this->currentPath . '/') . | ||
| + $name; | ||
| + | ||
| + $url = '?repo=' . urlencode( $this->repoSafeName ) . '&hash=' . $sha . | ||
| + '&name=' . urlencode( $fullPath ); | ||
| echo '<a href="' . $url . '" class="file-item">'; | ||
| - echo '<span class="file-mode">' . $mode . '</span>'; | ||
| - echo '<span class="file-name">'; | ||
| - echo '<i class="fas ' . $iconClass . ' file-icon-container"></i>'; | ||
| - echo htmlspecialchars( $name ); | ||
| - echo '</span>'; | ||
| + echo '<span class="file-mode">' . $mode . '</span>'; | ||
| + echo '<span class="file-name">'; | ||
| + echo '<i class="fas ' . $iconClass . ' file-icon-container"></i>'; | ||
| + echo htmlspecialchars( $name ); | ||
| + echo '</span>'; | ||
| if( $size > 0 ) { | ||
| - echo '<span class="file-size">' . $this->formatSize($size) . '</span>'; | ||
| + echo '<span class="file-size">' . $this->formatSize( $size ) . '</span>'; | ||
| } | ||
| $tokens = [ | ||
| 31536000 => 'year', | ||
| - 2592000 => 'month', | ||
| - 604800 => 'week', | ||
| - 86400 => 'day', | ||
| - 3600 => 'hour', | ||
| - 60 => 'minute', | ||
| - 1 => 'second' | ||
| + 2592000 => 'month', | ||
| + 604800 => 'week', | ||
| + 86400 => 'day', | ||
| + 3600 => 'hour', | ||
| + 60 => 'minute', | ||
| + 1 => 'second' | ||
| ]; | ||
| public function renderSize( int $bytes ): void { | ||
| - echo $this->formatSize($bytes); | ||
| + echo $this->formatSize( $bytes ); | ||
| } | ||
| - private function formatSize(int $bytes): string { | ||
| - $units = ['B', 'KB', 'MB', 'GB', 'TB']; | ||
| + private function formatSize( int $bytes ): string { | ||
| + $units = [ 'B', 'KB', 'MB', 'GB', 'TB' ]; | ||
| $i = 0; | ||
| - while ($bytes >= 1024 && $i < count($units) - 1) { | ||
| + while( $bytes >= 1024 && $i < count( $units ) - 1 ) { | ||
| $bytes /= 1024; | ||
| $i++; | ||
| } | ||
| - return ($bytes === 0 ? 0 : round($bytes)) . ' ' . $units[$i]; | ||
| + return ($bytes === 0 ? 0 : round( $bytes )) . ' ' . $units[$i]; | ||
| } | ||
| } | ||
| ): void; | ||
| - public function renderTime(int $timestamp): void; | ||
| + public function renderTime( int $timestamp ): void; | ||
| } | ||
| class HtmlTagRenderer implements TagRenderer { | ||
| private string $repoSafeName; | ||
| - public function __construct(string $repoSafeName) { | ||
| + public function __construct( string $repoSafeName ) { | ||
| $this->repoSafeName = $repoSafeName; | ||
| } | ||
| string $author | ||
| ): void { | ||
| - $repoParam = '&repo=' . urlencode($this->repoSafeName); | ||
| - $filesUrl = '?hash=' . $targetSha . $repoParam; | ||
| + $repoParam = '&repo=' . urlencode( $this->repoSafeName ); | ||
| + $filesUrl = '?hash=' . $targetSha . $repoParam; | ||
| $commitUrl = '?action=commit&hash=' . $targetSha . $repoParam; | ||
| echo '<tr>'; | ||
| - | ||
| echo '<td class="tag-name">'; | ||
| - echo '<a href="' . $filesUrl . '"><i class="fas fa-tag"></i> ' . htmlspecialchars($name) . '</a>'; | ||
| + echo '<a href="' . $filesUrl . '"><i class="fas fa-tag"></i> ' . | ||
| + htmlspecialchars( $name ) . '</a>'; | ||
| echo '</td>'; | ||
| - | ||
| echo '<td class="tag-message">'; | ||
| - echo ($message !== '') ? htmlspecialchars(strtok($message, "\n")) : '<span style="color: #484f58; font-style: italic;">No description</span>'; | ||
| - echo '</td>'; | ||
| - echo '<td class="tag-author">' . htmlspecialchars($author) . '</td>'; | ||
| + echo ($message !== '') ? htmlspecialchars( strtok( $message, "\n" ) ) : | ||
| + '<span style="color: #484f58; font-style: italic;">No description</span>'; | ||
| + echo '</td>'; | ||
| + echo '<td class="tag-author">' . htmlspecialchars( $author ) . '</td>'; | ||
| echo '<td class="tag-time">'; | ||
| - $this->renderTime($timestamp); | ||
| + $this->renderTime( $timestamp ); | ||
| echo '</td>'; | ||
| - | ||
| echo '<td class="tag-hash">'; | ||
| - echo '<a href="' . $commitUrl . '" class="commit-hash">' . substr($sha, 0, 7) . '</a>'; | ||
| + echo '<a href="' . $commitUrl . '" class="commit-hash">' . | ||
| + substr( $sha, 0, 7 ) . '</a>'; | ||
| echo '</td>'; | ||
| - | ||
| echo '</tr>'; | ||
| } | ||
| - public function renderTime(int $timestamp): void { | ||
| - if (!$timestamp) { echo 'never'; return; } | ||
| + public function renderTime( int $timestamp ): void { | ||
| + if( !$timestamp ) { | ||
| + echo 'never'; | ||
| + return; | ||
| + } | ||
| + | ||
| $diff = time() - $timestamp; | ||
| - if ($diff < 5) { echo 'just now'; return; } | ||
| + | ||
| + if( $diff < 5 ) { | ||
| + echo 'just now'; | ||
| + return; | ||
| + } | ||
| $tokens = [ | ||
| 31536000 => 'year', | ||
| - 2592000 => 'month', | ||
| - 604800 => 'week', | ||
| - 86400 => 'day', | ||
| - 3600 => 'hour', | ||
| - 60 => 'minute', | ||
| - 1 => 'second' | ||
| + 2592000 => 'month', | ||
| + 604800 => 'week', | ||
| + 86400 => 'day', | ||
| + 3600 => 'hour', | ||
| + 60 => 'minute', | ||
| + 1 => 'second' | ||
| ]; | ||
| - foreach ($tokens as $unit => $text) { | ||
| - if ($diff < $unit) continue; | ||
| - $num = floor($diff / $unit); | ||
| - echo $num . ' ' . $text . (($num > 1) ? 's' : '') . ' ago'; | ||
| + foreach( $tokens as $unit => $text ) { | ||
| + if( $diff < $unit ) { | ||
| + continue; | ||
| + } | ||
| + | ||
| + $num = floor( $diff / $unit ); | ||
| + | ||
| + echo $num . ' ' . $text . ($num > 1 ? 's' : '') . ' ago'; | ||
| return; | ||
| } | ||
| Delta | 1578 lines added, 1297 lines removed, 281-line increase |
|---|