<?php require_once __DIR__ . '/BasePage.php'; require_once __DIR__ . '/../model/UrlBuilder.php'; require_once __DIR__ . '/../model/Commit.php'; require_once __DIR__ . '/../render/HtmlCommitRenderer.php'; class CommitsPage extends BasePage { private const PER_PAGE = 100; private array $currentRepo; private Git $git; private string $hash; public function __construct( array $repositories, array $currentRepo, Git $git, string $hash ) { parent::__construct( $repositories, $currentRepo['name'] ); $this->currentRepo = $currentRepo; $this->git = $git; $this->hash = $hash; } public function render(): void { $this->renderLayout( function() { $main = $this->git->getMainBranch(); if( !$main ) { echo '<div class="empty-state"><h3>No branches</h3>' . '<p>Empty repository.</p></div>'; } else { $this->renderBreadcrumbs( $this->currentRepo, ['Commits'] ); echo '<h2>Commit History <span class="branch-badge">' . htmlspecialchars( $main['name'] ) . '</span></h2>'; echo '<div class="commit-list">'; $start = $this->hash !== '' ? $this->hash : $main['hash']; $commits = []; $this->git->history( $start, self::PER_PAGE, function( Commit $commit ) use( &$commits ) { $commits[] = $commit; } ); $nav = $this->buildPagination( $main['hash'], count( $commits ) ); $this->renderPagination( $nav ); $renderer = new HtmlCommitRenderer( $this->currentRepo['safe_name'] ); foreach( $commits as $commit ) { $commit->render( $renderer ); } echo '</div>'; $this->renderPagination( $nav ); } }, $this->currentRepo ); } private function renderPagination( array $nav ): void { $pages = $nav['pages']; $current = $nav['current']; $hasNext = $nav['hasNext']; $hasPrev = $current > 1; $total = count( $pages ); if( $hasPrev || $hasNext ) { echo '<div class="pagination">'; if( $hasPrev ) { echo '<a href="' . $this->pageUrl( $pages[0] ) . '" class="page-link page-nav" aria-label="first">' . $this->svgArrow( 'first' ) . '</a>'; echo '<a href="' . $this->pageUrl( $pages[$current - 2] ) . '" class="page-link page-nav" aria-label="back">' . $this->svgArrow( 'back' ) . '</a>'; } else { echo '<span class="page-link page-nav page-nav-hidden" ' . 'aria-hidden="true">' . $this->svgArrow( 'first' ) . '</span>'; echo '<span class="page-link page-nav page-nav-hidden" ' . 'aria-hidden="true">' . $this->svgArrow( 'back' ) . '</span>'; } $this->renderPageNumbers( $pages, $current ); if( $hasNext ) { echo '<a href="' . $this->pageUrl( $pages[$current] ) . '" class="page-link page-nav" aria-label="next">' . $this->svgArrow( 'next' ) . '</a>'; echo '<a href="' . $this->pageUrl( $pages[$total - 1] ) . '" class="page-link page-nav" aria-label="last">' . $this->svgArrow( 'last' ) . '</a>'; } else { echo '<span class="page-link page-nav page-nav-hidden" ' . 'aria-hidden="true">' . $this->svgArrow( 'next' ) . '</span>'; echo '<span class="page-link page-nav page-nav-hidden" ' . 'aria-hidden="true">' . $this->svgArrow( 'last' ) . '</span>'; } echo '</div>'; } } private function svgArrow( string $type ): string { $icons = [ 'back' => '<path d="M14 17 L9 12 L14 7" />', 'next' => '<path d="M10 17 L15 12 L10 7" />', 'first' => '<path d="M13 17 L6 12 L13 7 M19 17 L12 12 L19 7" />', 'last' => '<path d="M11 17 L18 12 L11 7 M5 17 L12 12 L5 7" />', ]; $inner = $icons[$type] ?? ''; return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" ' . 'fill="none" stroke="currentColor" stroke-width="2" ' . 'stroke-linecap="round" stroke-linejoin="round" ' . 'aria-label="' . $type . '" role="img">' . '<title>' . $type . '</title>' . $inner . '</svg>'; } private function renderPageNumbers( array $pages, int $current ): void { $total = count( $pages ); $start = max( 1, $current - 4 ); $end = min( $total, $start + 9 ); $actual = 1; if( $end === $total ) { $start = max( 1, $end - 9 ); } while( $actual <= $total ) { if( $actual >= $start && $actual <= $end ) { if( $actual === $current ) { echo '<span class="page-badge">' . $actual . '</span>'; } else { echo '<a href="' . $this->pageUrl( $pages[$actual - 1] ) . '" class="page-link">' . $actual . '</a>'; } } $actual++; } } private function buildPagination( string $mainHash, int $count ): array { $target = $this->hash !== '' ? $this->hash : $mainHash; $pageCommits = []; $commits = 0; $currentPage = 1; $found = false; $hitLimit = false; $this->git->history( $mainHash, PHP_INT_MAX, function( Commit $commit ) use( $target, &$pageCommits, &$commits, &$currentPage, &$found, &$hitLimit ) { $continue = true; if( $commits % self::PER_PAGE === 0 ) { $pageCommits[] = $commit; } if( $commit->isSha( $target ) ) { $currentPage = count( $pageCommits ); $found = true; } if( $found && count( $pageCommits ) > $currentPage + 10 ) { $hitLimit = true; $continue = false; } if( $continue ) { $commits++; } return $continue; } ); return [ 'pages' => $pageCommits, 'current' => $currentPage, 'hasAll' => !$hitLimit, 'hasNext' => $count === self::PER_PAGE && isset( $pageCommits[$currentPage] ) ]; } private function pageUrl( Commit $commit ): string { return $commit->toUrl( (new UrlBuilder()) ->withRepo( $this->currentRepo['safe_name'] ) ->withAction( 'commits' ) ); } }