Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/treetrek.git

Reformats code

AuthorDave Jarvis <email>
Date2026-02-14 16:31:44 GMT-0800
Commitd1bc5e98861ac43bb366343afc15f8c2070760c0
Parentfe43743
Config.php
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' );
}
}
File.php
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
RepositoryList.php
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;
- });
+ } );
}
}
Router.php
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;
}
Tag.php
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,
git/Git.php
$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 );
git/GitDiff.php
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
+ );
}
}
git/GitPacks.php
}
- 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 );
index.php
Config::init();
-$router = new Router(Config::getReposPath());
+$router = new Router( Config::getReposPath() );
$page = $router->route();
+
$page->render();
pages/BasePage.php
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>
pages/ClonePage.php
public function __construct( Git $git, string $subPath ) {
- $this->git = $git;
+ $this->git = $git;
$this->subPath = $subPath;
}
pages/CommitsPage.php
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) . ' &bull; ' . date('Y-m-d', $commit->date) . '</span>';
+ echo '<span class="meta">' . htmlspecialchars( $commit->author ) .
+ ' &bull; ' . 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>';
}
}
pages/DiffPage.php
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>';
}
}
pages/FilePage.php
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>';
}
}
pages/HomePage.php
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>';
}
}
pages/RawPage.php
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;
pages/TagsPage.php
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>';
}
}
render/FileRenderer.php
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];
}
}
render/TagRenderer.php
): 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;
}
Delta1578 lines added, 1297 lines removed, 281-line increase