<?php require_once __DIR__ . '/BasePage.php'; require_once __DIR__ . '/../UrlBuilder.php'; require_once __DIR__ . '/../render/HtmlFileRenderer.php'; class FilePage extends BasePage { private const MAX_HIGHLIGHT_SIZE = 65536; private const MAX_DISPLAY_SIZE = 524288; private $currentRepo; private $git; private $hash; private $path; public function __construct( array $repositories, array $currentRepo, Git $git, string $hash = 'HEAD', string $path = '' ) { parent::__construct( $repositories ); $this->currentRepo = $currentRepo; $this->git = $git; $this->hash = $hash ?: 'HEAD'; $this->path = $path; $this->title = $currentRepo['name']; } public function render() { $this->renderLayout( function() { $main = $this->git->getMainBranch(); if( !$main ) { echo '<div class="empty-state"><h3>No branches</h3></div>'; } else { $target = $this->hash; $entries = []; $this->git->walk( $target, function( $file ) use ( &$entries ) { $entries[] = $file; }, $this->path ); if( !empty( $entries ) && !$this->isExactFileMatch( $entries ) ) { $this->renderTree( $main, $target, $entries ); } else { $this->renderBlob( $target ); } } }, $this->currentRepo ); } private function isExactFileMatch($entries) { return count( $entries ) === 1 && $entries[0]->isName( basename( $this->path ) ) && !$entries[0]->isDir; } private function renderTree( $main, $targetHash, $entries ) { $this->emitBreadcrumbs( $targetHash, 'Tree', $this->path ); echo '<h2>' . htmlspecialchars( $this->currentRepo['name'] ) . ' <span class="branch-badge">' . htmlspecialchars( $targetHash ) . '</span></h2>'; usort( $entries, function( $a, $b ) { return $a->compare( $b ); } ); echo '<table class="file-list-table">'; echo '<thead><tr><th></th><th>Name</th><th class="file-mode-cell">Mode</th><th class="file-size-cell">Size</th></tr></thead>'; echo '<tbody>'; $renderer = new HtmlFileRenderer( $this->currentRepo['safe_name'], $this->path, $targetHash ); foreach( $entries as $file ) { $file->renderListEntry( $renderer ); } echo '</tbody></table>'; } private function renderBlob( $targetHash ) { $filename = $this->path; $file = $this->git->readFile( $targetHash, $filename ); $size = $this->git->getObjectSize( $targetHash, $filename ); $renderer = new HtmlFileRenderer( $this->currentRepo['safe_name'], dirname($filename), $targetHash ); $this->emitBreadcrumbs( $targetHash, 'File', $filename ); if( $size === 0 && !$file ) { echo '<div class="empty-state">File not found.</div>'; return; } $rawUrl = (new UrlBuilder()) ->withRepo( $this->currentRepo['safe_name'] ) ->withAction( 'raw' ) ->withHash( $targetHash ) ->withName( $filename ) ->build(); if( !$file->renderMedia( $renderer, $rawUrl ) ) { if( $file->isText() ) { if( $size > self::MAX_DISPLAY_SIZE ) { ob_start(); $file->renderSize( $renderer ); $sizeStr = ob_get_clean(); $this->renderDownloadState( $targetHash, "File is too large to display ($sizeStr)." ); } else { $content = ''; $this->git->stream( $targetHash, function( $d ) use ( &$content ) { $content .= $d; }, $filename ); echo '<div class="blob-content"><pre class="blob-code">' . ($size > self::MAX_HIGHLIGHT_SIZE ? htmlspecialchars( $content ) : $file->highlight( $renderer, $content )) . '</pre></div>'; } } else { $this->renderDownloadState( $targetHash, "This is a binary file." ); } } } private function renderDownloadState( $hash, $reason ) { $url = (new UrlBuilder()) ->withRepo( $this->currentRepo['safe_name'] ) ->withAction( 'raw' ) ->withHash( $hash ) ->withName( $this->path ) ->build(); echo '<div class="empty-state download-state">'; echo '<p>' . htmlspecialchars( $reason ) . '</p>'; echo '<a href="' . $url . '" class="btn-download">Download</a>'; echo '</div>'; } private function emitBreadcrumbs( $hash, $type, $path ) { $trail = []; if( $path ) { $parts = explode( '/', trim( $path, '/' ) ); $acc = ''; foreach( $parts as $idx => $part ) { $acc .= ($idx === 0 ? '' : '/') . $part; if( $idx === count( $parts ) - 1 ) { $trail[] = htmlspecialchars( $part ); } else { $url = (new UrlBuilder()) ->withRepo( $this->currentRepo['safe_name'] ) ->withAction( 'tree' ) ->withHash( $hash ) ->withName( $acc ) ->build(); $trail[] = '<a href="' . $url . '">' . htmlspecialchars( $part ) . '</a>'; } } } elseif( $hash ) { $trail[] = $type . ' ' . substr( $hash, 0, 7 ); } $this->renderBreadcrumbs( $this->currentRepo, $trail ); } }