Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/treetrek.git
<?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' )
    );
  }
}