<?php require_once __DIR__ . '/BasePage.php'; require_once __DIR__ . '/../git/GitDiff.php'; require_once __DIR__ . '/../model/UrlBuilder.php'; class DiffPage extends BasePage { private $currentRepo; private $git; private $hash; public function __construct( array $repositories, array $currentRepo, Git $git, string $hash ) { parent::__construct( $repositories, substr( $hash, 0, 7 ) ); $this->currentRepo = $currentRepo; $this->git = $git; $this->hash = $hash; } public function render() { $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; } elseif( $isMsg ) { $msg .= $line . "\n"; } else { if( preg_match( '/^(\w+) (.*)$/', $line, $m ) ) { $headers[$m[1]] = $m[2]; } } } $changes = iterator_to_array( $diffEngine->compare( $this->hash ) ); $added = 0; $deleted = 0; foreach( $changes as $change ) { if( isset( $change['hunks'] ) ) { foreach( $change['hunks'] as $hunkLine ) { if( isset( $hunkLine['t'] ) ) { if( $hunkLine['t'] === '+' ) { $added++; } elseif( $hunkLine['t'] === '-' ) { $deleted++; } } } } } $commitsUrl = (new UrlBuilder()) ->withRepo( $this->currentRepo['safe_name'] ) ->withAction( 'commits' ) ->build(); $this->renderBreadcrumbs( $this->currentRepo, [ '<a href="' . $commitsUrl . '">Commits</a>', substr( $this->hash, 0, 7 ) ] ); $authorRaw = $headers['author'] ?? 'Unknown'; $authorName = preg_replace( '/<[^>]+>/', '<email>', $authorRaw ); $authorName = htmlspecialchars( $authorName ); $commitDate = ''; if( preg_match( '/^(.*?) <.*?> (\d+) ([-+]\d{4})$/', $authorRaw, $m ) ) { $authorName = htmlspecialchars( $m[1] ) . ' <email>'; $timestamp = (int)$m[2]; $offsetStr = $m[3]; $pattern = '/([-+])(\d{2})(\d{2})/'; $tzString = preg_replace( $pattern, '$1$2:$3', $offsetStr ); $dt = new DateTime( '@' . $timestamp ); $dt->setTimezone( new DateTimeZone( $tzString ) ); $commitDate = $dt->format( 'Y-m-d H:i:s \G\M\TO' ); } echo '<div class="commit-details">'; echo '<div class="commit-header">'; echo '<h1 class="commit-title">' . htmlspecialchars( trim( $msg ) ) . '</h1>'; echo '<table class="commit-info-table"><tbody>'; echo '<tr>' . '<th class="commit-info-label">Author</th>' . '<td class="commit-info-value">' . $authorName . '</td></tr>'; if( $commitDate !== '' ) { echo '<tr>' . '<th class="commit-info-label">Date</th>' . '<td class="commit-info-value">' . $commitDate . '</td></tr>'; } echo '<tr>' . '<th class="commit-info-label">Commit</th>' . '<td class="commit-info-value">' . $this->hash . '</td></tr>'; if( isset( $headers['parent'] ) ) { $url = (new UrlBuilder()) ->withRepo( $this->currentRepo['safe_name'] ) ->withAction( 'commit' ) ->withHash( $headers['parent'] ) ->build(); echo '<tr>' . '<th class="commit-info-label">Parent</th>' . '<td class="commit-info-value">'; echo '<a href="' . $url . '" class="parent-link">' . substr( $headers['parent'], 0, 7 ) . '</a>'; echo '</td></tr>'; } $diffNet = $added - $deleted; $pluralize = function( int $count ): string { $suffix = ''; if( $count !== 1 ) { $suffix = 's'; } return $count . ' line' . $suffix; }; $deltaMsg = $pluralize( $added ) . ' added, ' . $pluralize( $deleted ) . ' removed'; if( $diffNet !== 0 ) { $direction = $diffNet > 0 ? 'increase' : 'decrease'; $deltaMsg .= ', ' . abs( $diffNet ) . "-line $direction"; } echo '<tr>' . '<th class="commit-info-label">Delta</th>' . '<td class="commit-info-value">' . $deltaMsg . '</td></tr>'; echo '</tbody></table></div></div>'; echo '<div class="diff-container">'; foreach( $changes as $change ) { $this->renderFileDiff( $change ); } if( empty( $changes ) ) { echo '<div class="empty-state"><p>No changes detected.</p></div>'; } echo '</div>'; }, $this->currentRepo ); } private function renderFileDiff( array $change ) { $statusIcon = 'fa-file'; $statusClass = ''; if( $change['type'] === 'A' ) { $statusIcon = 'fa-plus-circle'; $statusClass = 'status-add'; } elseif( $change['type'] === 'D' ) { $statusIcon = 'fa-minus-circle'; $statusClass = 'status-del'; } elseif( $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 '</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">'; echo '<img src="/images/diff-gap.svg" class="diff-gap-icon" />'; echo '</td></tr>'; } else { $class = 'diff-ctx'; $char = ' '; if( $line['t'] === '+' ) { $class = 'diff-add'; $char = '+'; } elseif( $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>'; } }