| Author | Dave Jarvis <email> |
|---|---|
| Date | 2026-02-14 13:22:54 GMT-0800 |
| Commit | 92f7020a2ac64c6dc829baac336ee1012b252805 |
| Parent | 7a01dd7 |
| class File { | ||
| - private ?Git $git; | ||
| private string $name; | ||
| private string $sha; | ||
| private string $mode; | ||
| private int $timestamp; | ||
| private int $size; | ||
| private bool $isDir; | ||
| + private string $icon; | ||
| - public function __construct( ?Git $git, string $name, string $sha, string $mode, int $timestamp = 0, int $size = 0 ) { | ||
| - $this->git = $git; | ||
| - $this->name = $name; | ||
| - $this->sha = $sha; | ||
| - $this->mode = $mode; | ||
| - $this->timestamp = $timestamp; | ||
| - $this->size = $size; | ||
| - $this->isDir = ( $mode === '40000' || $mode === '040000' ); | ||
| - } | ||
| + private string $mediaType; | ||
| + private string $category; | ||
| + private bool $binary; | ||
| - public function compare( File $other ): int { | ||
| - if( $this->isDir !== $other->isDir ) { | ||
| - return $this->isDir ? -1 : 1; | ||
| - } | ||
| + public function __construct( | ||
| + string $name, | ||
| + string $sha, | ||
| + string $mode, | ||
| + int $timestamp, | ||
| + int $size, | ||
| + string $contents = '' | ||
| + ) { | ||
| + $this->name = $name; | ||
| + $this->sha = $sha; | ||
| + $this->mode = $mode; | ||
| + $this->timestamp = $timestamp; | ||
| + $this->size = $size; | ||
| + $this->isDir = $mode === '40000' || $mode === '040000'; | ||
| - return strcasecmp( $this->name, $other->name ); | ||
| + $buffer = $this->isDir ? '' : substr($contents, 0, 12); | ||
| + | ||
| + $this->mediaType = MediaTypeSniffer::isMediaType($buffer, $name); | ||
| + $this->category = MediaTypeSniffer::isCategory($buffer, $name); | ||
| + $this->binary = MediaTypeSniffer::isBinary($buffer, $name); | ||
| + $this->icon = $this->resolveIcon(); | ||
| } | ||
| - public function render( FileRenderer $renderer ): void { | ||
| - $renderer->renderFileItem( | ||
| + public function compare(File $other): int { | ||
| + return $this->isDir !== $other->isDir | ||
| + ? ($this->isDir ? -1 : 1) | ||
| + : strcasecmp($this->name, $other->name); | ||
| + } | ||
| + | ||
| + public function render(FileRenderer $renderer): void { | ||
| + $renderer->render( | ||
| $this->name, | ||
| $this->sha, | ||
| $this->mode, | ||
| - $this->getIconClass(), | ||
| + $this->icon, | ||
| $this->timestamp, | ||
| - $this->isDir ? '' : $this->getFormattedSize() | ||
| + $this->size | ||
| ); | ||
| } | ||
| - private function getIconClass(): string { | ||
| - if( $this->isDir ) return 'fa-folder'; | ||
| + public function renderMedia(string $url): bool { | ||
| + $rendered = false; | ||
| - return match ( true ) { | ||
| - $this->isType( 'application/pdf' ) => 'fa-file-pdf', | ||
| - $this->isCategory( MediaTypeSniffer::CAT_ARCHIVE ) => 'fa-file-archive', | ||
| - $this->isCategory( MediaTypeSniffer::CAT_IMAGE ) => 'fa-file-image', | ||
| - $this->isCategory( MediaTypeSniffer::CAT_AUDIO ) => 'fa-file-audio', | ||
| - $this->isCategory( MediaTypeSniffer::CAT_VIDEO ) => 'fa-file-video', | ||
| - $this->isCategory( MediaTypeSniffer::CAT_TEXT ) => 'fa-file-code', | ||
| - default => 'fa-file', | ||
| - }; | ||
| + 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>'; | ||
| + $rendered = true; | ||
| + } 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; | ||
| } | ||
| - private function getFormattedSize(): string { | ||
| - if( $this->size <= 0 ) return '0 B'; | ||
| - $units = ['B', 'KB', 'MB', 'GB']; | ||
| - $i = (int)floor( log( $this->size, 1024 ) ); | ||
| - return round( $this->size / pow( 1024, $i ), 1 ) . ' ' . $units[$i]; | ||
| + public function renderSize(FileRenderer $renderer): void { | ||
| + $renderer->renderSize($this->size); | ||
| } | ||
| - public function isType( string $type ): bool { | ||
| - return str_contains( MediaTypeSniffer::isMediaType( $this->getSniffBuffer(), $this->name ), $type ); | ||
| + public function emitRawHeaders(): void { | ||
| + header("Content-Type: " . $this->mediaType); | ||
| + header("Content-Length: " . $this->size); | ||
| + header("Content-Disposition: inline; filename=\"" . addslashes($this->name) . "\""); | ||
| } | ||
| - public function isCategory( string $category ): bool { | ||
| - return MediaTypeSniffer::isCategory( $this->getSniffBuffer(), $this->name ) === $category; | ||
| + public function isImage(): bool { | ||
| + return $this->category === MediaTypeSniffer::CAT_IMAGE; | ||
| } | ||
| - public function isBinary(): bool { | ||
| - return MediaTypeSniffer::isBinary( $this->getSniffBuffer(), $this->name ); | ||
| + public function isVideo(): bool { | ||
| + return $this->category === MediaTypeSniffer::CAT_VIDEO; | ||
| } | ||
| - private function getSniffBuffer(): string { | ||
| - if( $this->isDir ) return ''; | ||
| + public function isAudio(): bool { | ||
| + return $this->category === MediaTypeSniffer::CAT_AUDIO; | ||
| + } | ||
| - if( $this->git ) { | ||
| - return $this->git->peek( $this->sha ); | ||
| - } | ||
| + public function isText(): bool { | ||
| + return $this->category === MediaTypeSniffer::CAT_TEXT; | ||
| + } | ||
| - if( !file_exists( $this->name ) ) return ''; | ||
| - $handle = @fopen( $this->name, 'rb' ); | ||
| - if( !$handle ) return ''; | ||
| - $read = fread( $handle, 12 ); | ||
| - fclose( $handle ); | ||
| - return ( $read !== false ) ? $read : ''; | ||
| + public function isBinary(): bool { | ||
| + return $this->binary; | ||
| + } | ||
| + | ||
| + private function resolveIcon(): string { | ||
| + return $this->isDir | ||
| + ? 'fa-folder' | ||
| + : (str_contains($this->mediaType, 'application/pdf') | ||
| + ? 'fa-file-pdf' | ||
| + : match ($this->category) { | ||
| + MediaTypeSniffer::CAT_ARCHIVE => 'fa-file-archive', | ||
| + MediaTypeSniffer::CAT_IMAGE => 'fa-file-image', | ||
| + MediaTypeSniffer::CAT_AUDIO => 'fa-file-audio', | ||
| + MediaTypeSniffer::CAT_VIDEO => 'fa-file-video', | ||
| + MediaTypeSniffer::CAT_TEXT => 'fa-file-code', | ||
| + default => 'fa-file', | ||
| + }); | ||
| } | ||
| } |
| class GitDiff { | ||
| - private $git; | ||
| + private Git $git; | ||
| 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 = ''; | ||
| - if( preg_match( '/^parent ([0-9a-f]{40})/m', $commitData, $matches ) ) { | ||
| - $parentHash = $matches[1]; | ||
| - } | ||
| + public function compare(string $commitHash) { | ||
| + $commitData = $this->git->read($commitHash); | ||
| + $parentHash = preg_match('/^parent ([0-9a-f]{40})/m', $commitData, $matches) ? $matches[1] : ''; | ||
| - $newTree = $this->getTreeHash( $commitHash ); | ||
| - $oldTree = $parentHash ? $this->getTreeHash( $parentHash ) : null; | ||
| + $newTree = $this->getTreeHash($commitHash); | ||
| + $oldTree = $parentHash ? $this->getTreeHash($parentHash) : null; | ||
| - return $this->diffTrees( $oldTree, $newTree ); | ||
| + return $this->diffTrees($oldTree, $newTree); | ||
| } | ||
| - private function getTreeHash( $commitSha ) { | ||
| - $data = $this->git->read( $commitSha ); | ||
| - if( preg_match( '/^tree ([0-9a-f]{40})/m', $data, $matches ) ) { | ||
| - return $matches[1]; | ||
| - } | ||
| - return 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 ) return []; | ||
| - $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))); | ||
| + sort($allNames); | ||
| - foreach( $allNames as $name ) { | ||
| - $old = $oldEntries[$name] ?? null; | ||
| - $new = $newEntries[$name] ?? null; | ||
| - $currentPath = $path ? "$path/$name" : $name; | ||
| + foreach ($allNames as $name) { | ||
| + $old = $oldEntries[$name] ?? null; | ||
| + $new = $newEntries[$name] ?? null; | ||
| + $currentPath = $path ? "$path/$name" : $name; | ||
| - if( !$old ) { | ||
| - if( $new['is_dir'] ) { | ||
| - $changes = array_merge( $changes, $this->diffTrees( null, $new['sha'], $currentPath ) ); | ||
| - } else { | ||
| - $changes[] = $this->createChange( 'A', $currentPath, null, $new['sha'] ); | ||
| - } | ||
| - } elseif( !$new ) { | ||
| - if( $old['is_dir'] ) { | ||
| - $changes = array_merge( $changes, $this->diffTrees( $old['sha'], null, $currentPath ) ); | ||
| - } else { | ||
| - $changes[] = $this->createChange( 'D', $currentPath, $old['sha'], null ); | ||
| - } | ||
| - } elseif( $old['sha'] !== $new['sha'] ) { | ||
| - if( $old['is_dir'] && $new['is_dir'] ) { | ||
| - $changes = array_merge( $changes, $this->diffTrees( $old['sha'], $new['sha'], $currentPath ) ); | ||
| - } elseif( !$old['is_dir'] && !$new['is_dir'] ) { | ||
| - $changes[] = $this->createChange( 'M', $currentPath, $old['sha'], $new['sha'] ); | ||
| + 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) { | ||
| + $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']) { | ||
| + $changes = ($old['is_dir'] && $new['is_dir']) | ||
| + ? 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'])])); | ||
| } | ||
| } | ||
| } | ||
| 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' ) | ||
| + '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 ) { | ||
| - return [ | ||
| + if ($oldSize > self::MAX_DIFF_SIZE || $newSize > self::MAX_DIFF_SIZE) { | ||
| + $result = [ | ||
| 'type' => $type, | ||
| 'path' => $path, | ||
| 'is_binary' => true, | ||
| 'hunks' => [] | ||
| ]; | ||
| - } | ||
| - | ||
| - $oldContent = $oldSha ? $this->git->read( $oldSha ) : ''; | ||
| - $newContent = $newSha ? $this->git->read( $newSha ) : ''; | ||
| - | ||
| - $isBinary = false; | ||
| + } else { | ||
| + $oldContent = $oldSha ? $this->git->read($oldSha) : ''; | ||
| + $newContent = $newSha ? $this->git->read($newSha) : ''; | ||
| - if( $newSha ) { | ||
| - $f = new VirtualDiffFile( $path, $newContent ); | ||
| - if( $f->isBinary() ) $isBinary = true; | ||
| - } | ||
| - if( !$isBinary && $oldSha ) { | ||
| - $f = new VirtualDiffFile( $path, $oldContent ); | ||
| - if( $f->isBinary() ) $isBinary = true; | ||
| - } | ||
| + $isBinary = ($newSha && (new VirtualDiffFile($path, $newContent))->isBinary()) || | ||
| + (!$newSha && $oldSha && (new VirtualDiffFile($path, $oldContent))->isBinary()); | ||
| - $diff = null; | ||
| - if( !$isBinary ) { | ||
| - $diff = $this->calculateDiff( $oldContent, $newContent ); | ||
| + $result = [ | ||
| + 'type' => $type, | ||
| + 'path' => $path, | ||
| + 'is_binary' => $isBinary, | ||
| + 'hunks' => $isBinary ? null : $this->calculateDiff($oldContent, $newContent) | ||
| + ]; | ||
| } | ||
| - return [ | ||
| - 'type' => $type, | ||
| - 'path' => $path, | ||
| - 'is_binary' => $isBinary, | ||
| - 'hunks' => $diff | ||
| - ]; | ||
| + 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 ); | ||
| - $cntOld = count( $oldSlice ); | ||
| - $cntNew = count( $newSlice ); | ||
| + $oldSlice = array_slice($oldLines, $start, $m - $start - $end); | ||
| + $newSlice = array_slice($newLines, $start, $n - $start - $end); | ||
| - if( ( $cntOld * $cntNew ) > 500000 ) { | ||
| - return [['t' => 'gap']]; | ||
| - } | ||
| + $result = null; | ||
| - $ops = $this->computeLCS( $oldSlice, $newSlice ); | ||
| + if ((count($oldSlice) * count($newSlice)) > 500000) { | ||
| + $result = [['t' => 'gap']]; | ||
| + } else { | ||
| + $ops = $this->computeLCS($oldSlice, $newSlice); | ||
| - $groupedOps = []; | ||
| - $bufferDel = []; | ||
| - $bufferAdd = []; | ||
| + $groupedOps = []; | ||
| + $bufferDel = []; | ||
| + $bufferAdd = []; | ||
| - 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'] === '-' ) { | ||
| - $bufferDel[] = $op; | ||
| - } elseif( $op['t'] === '+' ) { | ||
| - $bufferAdd[] = $op; | ||
| + 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'] === '-') { | ||
| + $bufferDel[] = $op; | ||
| + } 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 = []; | ||
| + $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; | ||
| + $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 ); | ||
| - $contextLines = 3; | ||
| + $finalLines = []; | ||
| + $lastVisibleIndex = -1; | ||
| + $streamLen = count($stream); | ||
| + $contextLines = 3; | ||
| - for( $i = 0; $i < $streamLen; $i++ ) { | ||
| - $show = false; | ||
| + for ($i = 0; $i < $streamLen; $i++) { | ||
| + $show = false; | ||
| - if( $stream[$i]['t'] !== ' ' ) { | ||
| - $show = true; | ||
| - } else { | ||
| - 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 ($stream[$i]['t'] !== ' ') { | ||
| + $show = true; | ||
| + } else { | ||
| + 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'] !== ' ') { | ||
| + $show = true; | ||
| + break; | ||
| + } | ||
| } | ||
| } | ||
| } | ||
| - } | ||
| - if( $show ) { | ||
| - if( $lastVisibleIndex !== -1 && $i > $lastVisibleIndex + 1 ) { | ||
| - $finalLines[] = ['t' => 'gap']; | ||
| + if ($show) { | ||
| + if ($lastVisibleIndex !== -1 && $i > $lastVisibleIndex + 1) { | ||
| + $finalLines[] = ['t' => 'gap']; | ||
| + } | ||
| + $finalLines[] = $stream[$i]; | ||
| + $lastVisibleIndex = $i; | ||
| } | ||
| - $finalLines[] = $stream[$i]; | ||
| - $lastVisibleIndex = $i; | ||
| } | ||
| + $result = $finalLines; | ||
| } | ||
| - return $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++ ) { | ||
| - if( $old[$i - 1] === $new[$j - 1] ) { | ||
| - $c[$i][$j] = $c[$i - 1][$j - 1] + 1; | ||
| - } else { | ||
| - $c[$i][$j] = max( $c[$i][$j - 1], $c[$i - 1][$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]); | ||
| } | ||
| } | ||
| $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 { | ||
| - private $content; | ||
| - private $vName; | ||
| - | ||
| - public function __construct( $name, $content ) { | ||
| - parent::__construct( null, $name, '', '100644', 0, strlen( $content ) ); | ||
| - $this->vName = $name; | ||
| - $this->content = $content; | ||
| - } | ||
| - | ||
| - public function isBinary(): bool { | ||
| - $buffer = substr( $this->content, 0, 12 ); | ||
| - return MediaTypeSniffer::isBinary( $buffer, $this->vName ); | ||
| + public function __construct(string $name, string $content) { | ||
| + parent::__construct($name, '', '100644', 0, strlen($content), $content); | ||
| } | ||
| } |
| public function render() { | ||
| - $this->renderLayout( function() { | ||
| - // Use the injected private Git instance | ||
| + $this->renderLayout(function() { | ||
| $main = $this->git->getMainBranch(); | ||
| - if( !$main ) { | ||
| + if (!$main) { | ||
| echo '<div class="empty-state"><h3>No branches</h3></div>'; | ||
| - return; | ||
| - } | ||
| - | ||
| - $target = $this->hash ?: $main['hash']; | ||
| - $entries = []; | ||
| + } else { | ||
| + $target = $this->hash ?: $main['hash']; | ||
| + $entries = []; | ||
| - // Use the injected private Git instance | ||
| - $this->git->walk( $target, function( $file ) use ( &$entries ) { | ||
| - $entries[] = $file; | ||
| - } ); | ||
| + $this->git->walk($target, function($file) use (&$entries) { | ||
| + $entries[] = $file; | ||
| + }); | ||
| - if( !empty( $entries ) ) { | ||
| - $this->renderTree( $main, $target, $entries ); | ||
| - } else { | ||
| - $this->renderBlob( $target ); | ||
| + if (!empty($entries)) { | ||
| + $this->renderTree($main, $target, $entries); | ||
| + } else { | ||
| + $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'] ); | ||
| - | ||
| - // Use the injected private Git instance | ||
| - $size = $this->git->getObjectSize( $targetHash ); | ||
| - $buffer = ''; | ||
| - | ||
| - // Use the injected private Git instance | ||
| - $this->git->stream( $targetHash, function( $d ) use ( &$buffer ) { | ||
| - if( strlen( $buffer ) < 12 ) $buffer .= $d; | ||
| - } ); | ||
| + private function renderBlob($targetHash) { | ||
| + $repoParam = '&repo=' . urlencode($this->currentRepo['safe_name']); | ||
| $filename = $_GET['name'] ?? ''; | ||
| - $category = MediaTypeSniffer::isCategory( $buffer, $filename ); | ||
| - $mediaType = MediaTypeSniffer::isMediaType( $buffer, $filename ); | ||
| + $file = $this->git->readFile($targetHash, $filename); | ||
| + $size = $this->git->getObjectSize($targetHash); | ||
| - $this->renderBreadcrumbs( $targetHash, 'File' ); | ||
| + $renderer = new HtmlFileRenderer($this->currentRepo['safe_name']); | ||
| - if( $size === 0 ) { | ||
| - $this->renderDownloadState( $targetHash, "This file is empty." ); | ||
| - return; | ||
| - } | ||
| + $this->renderBreadcrumbs($targetHash, 'File'); | ||
| - $rawUrl = '?action=raw&hash=' . $targetHash . $repoParam . '&name=' . urlencode( $filename ); | ||
| + if ($size === 0) { | ||
| + $this->renderDownloadState($targetHash, "This file is empty."); | ||
| + } else { | ||
| + $rawUrl = '?action=raw&hash=' . $targetHash . $repoParam . '&name=' . urlencode($filename); | ||
| - if( $category === MediaTypeSniffer::CAT_IMAGE ) { | ||
| - echo '<div class="blob-content blob-content-image"><img src="' . $rawUrl . '"></div>'; | ||
| - } elseif( $category === MediaTypeSniffer::CAT_VIDEO ) { | ||
| - echo '<div class="blob-content blob-content-video"><video controls><source src="' . $rawUrl . '" type="' . $mediaType . '"></video></div>'; | ||
| - } elseif( $category === MediaTypeSniffer::CAT_AUDIO ) { | ||
| - echo '<div class="blob-content blob-content-audio"><audio controls><source src="' . $rawUrl . '" type="' . $mediaType . '"></audio></div>'; | ||
| - } elseif( $category === MediaTypeSniffer::CAT_TEXT ) { | ||
| - if( $size > 524288 ) { | ||
| - $this->renderDownloadState( $targetHash, "File is too large to display (" . $this->formatSize( $size ) . ")." ); | ||
| - } else { | ||
| - $content = ''; | ||
| - // Use the injected private Git instance | ||
| - $this->git->stream( $targetHash, function( $d ) use ( &$content ) { $content .= $d; } ); | ||
| - echo '<div class="blob-content"><pre class="blob-code">' . htmlspecialchars( $content ) . '</pre></div>'; | ||
| + if (!$file->renderMedia($rawUrl)) { | ||
| + if ($file->isText()) { | ||
| + if ($size > 524288) { | ||
| + ob_start(); | ||
| + $file->renderSize($renderer); | ||
| + $sizeStr = ob_get_clean(); | ||
| + $this->renderDownloadState($targetHash, "File is too large to display ($sizeStr)."); | ||
| + } else { | ||
| + $content = ''; | ||
| + $this->git->stream($targetHash, function($d) use (&$content) { $content .= $d; }); | ||
| + echo '<div class="blob-content"><pre class="blob-code">' . htmlspecialchars($content) . '</pre></div>'; | ||
| + } | ||
| + } else { | ||
| + $this->renderDownloadState($targetHash, "This is a binary file."); | ||
| + } | ||
| } | ||
| - } else { | ||
| - $this->renderDownloadState( $targetHash, "This is a binary file." ); | ||
| } | ||
| } | ||
| - private function renderDownloadState( $hash, $reason ) { | ||
| - $url = '?action=raw&hash=' . $hash . '&repo=' . urlencode( $this->currentRepo['safe_name'] ); | ||
| + private function renderDownloadState($hash, $reason) { | ||
| + $url = '?action=raw&hash=' . $hash . '&repo=' . urlencode($this->currentRepo['safe_name']); | ||
| echo '<div class="empty-state download-state">'; | ||
| - echo '<p>' . htmlspecialchars( $reason ) . '</p>'; | ||
| + echo '<p>' . htmlspecialchars($reason) . '</p>'; | ||
| echo '<a href="' . $url . '" class="btn-download">Download Raw File</a>'; | ||
| echo '</div>'; | ||
| - } | ||
| - | ||
| - private function formatSize( $size ) { | ||
| - if( $size <= 0 ) return '0 B'; | ||
| - | ||
| - $units = ['B', 'KB', 'MB', 'GB']; | ||
| - $i = (int)floor( log( $size, 1024 ) ); | ||
| - | ||
| - return round( $size / pow( 1024, $i ), 1 ) . ' ' . $units[$i]; | ||
| } | ||
| - private function renderBreadcrumbs( $hash, $type ) { | ||
| - $repoUrl = '?repo=' . urlencode( $this->currentRepo['safe_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, '/' ) ); | ||
| + if ($path) { | ||
| + $parts = explode('/', trim($path, '/')); | ||
| $acc = ''; | ||
| - foreach ( $parts as $idx => $part ) { | ||
| - $acc .= ( $idx === 0 ? '' : '/' ) . $part; | ||
| - | ||
| - // The last segment isn't a link | ||
| - if ( $idx === count( $parts ) - 1 ) { | ||
| - $crumbs[] = htmlspecialchars( $part ); | ||
| + foreach ($parts as $idx => $part) { | ||
| + $acc .= ($idx === 0 ? '' : '/') . $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>'; | ||
| } | ||
| } |
| private $hash; | ||
| - public function __construct( $git, $hash ) { | ||
| + public function __construct($git, $hash) { | ||
| $this->git = $git; | ||
| $this->hash = $hash; | ||
| } | ||
| public function render() { | ||
| $filename = $_GET['name'] ?? 'file'; | ||
| - $buffer = ''; | ||
| - | ||
| - $size = $this->git->getObjectSize( $this->hash ); | ||
| - | ||
| - $this->git->stream( $this->hash, function( $d ) use ( &$buffer ) { | ||
| - if( strlen( $buffer ) < 12 ) { | ||
| - $buffer .= $d; | ||
| - } | ||
| - } ); | ||
| - $mediaType = MediaTypeSniffer::isMediaType( $buffer, $filename ); | ||
| + $file = $this->git->readFile($this->hash, $filename); | ||
| - while( ob_get_level() ) { | ||
| + while (ob_get_level()) { | ||
| ob_end_clean(); | ||
| } | ||
| - header( "Content-Type: " . $mediaType ); | ||
| - header( "Content-Length: " . $size ); | ||
| - header( "Content-Disposition: inline; filename=\"" . addslashes( $filename ) . "\"" ); | ||
| + $file->emitRawHeaders(); | ||
| - $this->git->stream( $this->hash, function( $d ) { | ||
| + $this->git->stream($this->hash, function($d) { | ||
| echo $d; | ||
| - } ); | ||
| + }); | ||
| exit; |
| Delta | 314 lines added, 353 lines removed, 39-line decrease |
|---|