| 2 | 2 | class Config { |
| 3 | 3 | public const SITE_TITLE = "Dave Jarvis' Repositories"; |
| 4 | private const REPOS_SUBDIR = '/repos'; | |
| 4 | private const REPOS_DIR = '/repos'; | |
| 5 | private const ORDER_FILE = '/order.txt'; | |
| 5 | 6 | private const LOG_FILE = '/error.log'; |
| 6 | 7 | |
| ... | ||
| 14 | 15 | |
| 15 | 16 | public function createRouter() { |
| 16 | $path = $this->getHomeDirectory() . self::REPOS_SUBDIR; | |
| 17 | $repos = $this->getHomeDirectory() . self::REPOS_DIR; | |
| 17 | 18 | |
| 18 | return new Router( $path ); | |
| 19 | return new Router( $repos, $repos . self::ORDER_FILE ); | |
| 19 | 20 | } |
| 20 | 21 | |
| 24 | 24 | export OWNER=username |
| 25 | 25 | export WEBDIR=/var/www |
| 26 | export WEBOWNER=www-data | |
| 26 | 27 | ``` |
| 27 | 28 | 1. Download code: |
| 28 | 29 | ```bash |
| 29 | 30 | mkdir -p /var/www/${REPO} |
| 30 | cd /var/www/${REPO} | |
| 31 | cd ${WEBDIR}/${REPO} | |
| 31 | 32 | git clone https://repo.autonoma.ca/repo/treetrek |
| 32 | sudo usermod -aG ${OWNER} www-data | |
| 33 | sudo usermod -aG ${OWNER} ${WEBOWNER} | |
| 33 | 34 | ``` |
| 34 | 35 | 1. Edit `${WEBDIR}/${REPO}/Config.php`. |
| 35 | 36 | 1. Set `SITE_TITLE`. |
| 36 | 1. Set `REPOS_SUBDIR`. | |
| 37 | 1. Set `REPOS_DIR`. | |
| 37 | 38 | 1. Save the file. |
| 38 | 1. Edit `${WEBDIR}/${REPO}/order.txt` | |
| 39 | 1. Add repositories you want to come first, hide using a hyphen prefix. | |
| 39 | 1. Edit `<REPOS_DIR>/order.txt` | |
| 40 | 1. List repositories in the order they must appear. | |
| 41 | 1. Hide repositories using a hyphen prefix. | |
| 42 | 1. Save the file. | |
| 40 | 43 | 1. Edit nginx configuration file. |
| 41 | 44 | 1. Add routing and security rules: |
| ... | ||
| 72 | 75 | } |
| 73 | 76 | ``` |
| 77 | 1. Save the file. | |
| 74 | 78 | 1. Apply changes: |
| 75 | 79 | ```bash |
| 1 | # Git Repository Viewer | |
| 1 | # TreeTrek | |
| 2 | 2 | |
| 3 | TreeTrek is a free, read-only, open-source, dependency- and authentication-free | |
| 4 | PHP application for browsing raw Git repositories in either a self-hosted or | |
| 5 | shared host environment. It functions entirely through direct filesystem access | |
| 6 | without invoking system commands. | |
| 3 | TreeTrek is a free, open-source, responsive, optimized, authentication- and | |
| 4 | dependency-free‡ PHP web application for browsing raw Git repositories in | |
| 5 | either a self-hosted or shared host environment. It is a human-guided, | |
| 6 | AI-implemented, greenfield implementation having zero command execution | |
| 7 | functions. | |
| 7 | 8 | |
| 8 | ## Core Git Implementation | |
| 9 | ## Git implementation | |
| 9 | 10 | |
| 10 | 11 | The engine implements low-level Git protocols to navigate and extract data from |
| ... | ||
| 19 | 20 | read-only cloning via standard Git clients. |
| 20 | 21 | |
| 21 | ## Content and Visualization | |
| 22 | ## User interface | |
| 22 | 23 | |
| 23 | 24 | The user interface provides developer-focused tools for inspecting history |
| 24 | 25 | and repository content. |
| 25 | 26 | |
| 26 | 27 | * **LCS Diff Engine**: Includes a Longest Common Subsequence (LCS) algorithm |
| 27 | 28 | to display line-level changes. |
| 28 | 29 | * **Syntax Highlighting**: Uses a regex-based engine with rules for common |
| 29 | programming languages. | |
| 30 | programming languages and configuration file syntaxes. | |
| 30 | 31 | * **Multimedia Rendering**: Detects media types to natively render |
| 31 | 32 | images, video, and audio files in the browser. |
| 32 | 33 | * **Breadcrumb Navigation**: Features a dynamic path trail and repository |
| 33 | 34 | selector for seamless project switching. |
| 34 | 35 | * **Streamed Content**: Streams object data in chunks to handle large files |
| 35 | 36 | without exceeding memory limits. |
| 36 | 37 | |
| 37 | ## Curation and Architecture | |
| 38 | ## Design | |
| 38 | 39 | |
| 39 | Project organization and system behavior are managed through flat-file | |
| 40 | Project organization and system behaviour are managed through flat-file | |
| 40 | 41 | configurations and an object-oriented design. This structure separates |
| 41 | 42 | repository logic from the presentation layer. |
| 42 | 43 | |
| 43 | 44 | * **Repository Curation**: Uses an `order.txt` file to define the |
| 44 | 45 | display sequence and visibility of projects. |
| 45 | 46 | * **Project Exclusion**: Allows hiding specific repositories by |
| 46 | 47 | prefixing their names with a hyphen in the `order.txt` config. |
| 47 | 48 | * **Stateless Routing**: Maps clean URIs directly to specialized page |
| 48 | 49 | controllers for consistent navigation. |
| 50 | ||
| 51 | --- | |
| 52 | ||
| 53 | ‡Uses FontAwesome. | |
| 49 | 54 | |
| 50 | 55 | |
| 9 | 9 | } |
| 10 | 10 | |
| 11 | class ZlibExtractorStream implements CompressionStream { | |
| 11 | class ZlibDeflatorStream implements CompressionStream { | |
| 12 | 12 | public function stream( |
| 13 | 13 | StreamReader $stream, |
| 93 | 93 | |
| 94 | 94 | public function getObjectSize( string $sha, string $path = '' ): int { |
| 95 | return $path !== '' | |
| 96 | ? ( $this->resolvePath( | |
| 97 | $this->getTreeSha( $this->resolve( $sha ) ), | |
| 98 | $path | |
| 99 | )['sha'] ?? '' ) !== '' | |
| 100 | ? $this->packs->getSize( $this->resolvePath( $this->getTreeSha( $this->resolve( $sha ) ), $path )['sha'] ) | |
| 101 | ?: $this->loose->getSize( $this->resolvePath( $this->getTreeSha( $this->resolve( $sha ) ), $path )['sha'] ) | |
| 102 | : 0 | |
| 103 | : ( $sha !== '' | |
| 104 | ? $this->packs->getSize( $sha ) ?: $this->loose->getSize( $sha ) | |
| 105 | : 0 ); | |
| 95 | $target = $sha; | |
| 96 | ||
| 97 | if( $path !== '' ) { | |
| 98 | $target = $this->resolvePath( | |
| 99 | $this->getTreeSha( $this->resolve( $sha ) ), | |
| 100 | $path | |
| 101 | )['sha'] ?? ''; | |
| 102 | } | |
| 103 | ||
| 104 | return $target !== '' | |
| 105 | ? $this->packs->getSize( $target ) | |
| 106 | ?: $this->loose->getSize( $target ) | |
| 107 | : 0; | |
| 106 | 108 | } |
| 107 | 109 |
| 70 | 70 | int $offset |
| 71 | 71 | ) use ( &$result ): void { |
| 72 | $context = $this->createContext( | |
| 73 | $packFile, $offset, 0 | |
| 74 | ); | |
| 75 | $meta = $this->reader->getEntryMeta( | |
| 76 | $context | |
| 72 | $result = $this->reader->getEntryMeta( | |
| 73 | $this->createContext( $packFile, $offset, 0 ) | |
| 77 | 74 | ); |
| 78 | ||
| 79 | $result = $meta; | |
| 80 | 75 | $result['file'] = $packFile; |
| 81 | 76 | $result['offset'] = $offset; |
| 82 | 77 | } |
| 83 | 78 | ); |
| 84 | 79 | |
| 85 | 80 | return $result; |
| 86 | 81 | } |
| 87 | 82 | |
| 88 | public function peek( | |
| 89 | string $sha, | |
| 90 | int $len = 12 | |
| 91 | ): string { | |
| 83 | public function peek( string $sha, int $len = 12 ): string { | |
| 92 | 84 | $result = ''; |
| 93 | 85 | |
| 94 | 86 | $this->locate( |
| 95 | 87 | $sha, |
| 96 | 88 | function( |
| 97 | 89 | string $packFile, |
| 98 | 90 | int $offset |
| 99 | 91 | ) use ( &$result, $len ): void { |
| 100 | $context = $this->createContext( | |
| 101 | $packFile, $offset, 0 | |
| 102 | ); | |
| 103 | $result = $this->reader->read( | |
| 104 | $context, | |
| 92 | $result = $this->reader->read( | |
| 93 | $this->createContext( $packFile, $offset, 0 ), | |
| 105 | 94 | $len, |
| 106 | function( | |
| 107 | string $baseSha, | |
| 108 | int $cap | |
| 109 | ): string { | |
| 95 | function( string $baseSha, int $cap ): string { | |
| 110 | 96 | return $this->peek( $baseSha, $cap ); |
| 111 | 97 | } |
| ... | ||
| 131 | 117 | $size = $this->reader->getSize( $context ); |
| 132 | 118 | |
| 133 | if( $size <= self::MAX_RAM ) { | |
| 134 | $result = $this->reader->read( | |
| 135 | $context, | |
| 136 | 0, | |
| 137 | function( | |
| 138 | string $baseSha, | |
| 139 | int $cap | |
| 140 | ): string { | |
| 141 | return $cap > 0 | |
| 142 | ? $this->peek( $baseSha, $cap ) | |
| 143 | : $this->read( $baseSha ); | |
| 144 | } | |
| 145 | ); | |
| 146 | } | |
| 119 | $result = $size <= self::MAX_RAM | |
| 120 | ? $this->reader->read( | |
| 121 | $context, | |
| 122 | 0, | |
| 123 | function( string $baseSha, int $cap ): string { | |
| 124 | return $cap > 0 | |
| 125 | ? $this->peek( $baseSha, $cap ) | |
| 126 | : $this->read( $baseSha ); | |
| 127 | } | |
| 128 | ) | |
| 129 | : $result; | |
| 147 | 130 | } |
| 148 | 131 | ); |
| ... | ||
| 157 | 140 | $result = false; |
| 158 | 141 | |
| 159 | foreach( | |
| 160 | $this->streamGenerator( $sha ) as $chunk | |
| 161 | ) { | |
| 142 | foreach( $this->streamGenerator( $sha ) as $chunk ) { | |
| 162 | 143 | $callback( $chunk ); |
| 163 | 144 | |
| 164 | 145 | $result = true; |
| 165 | 146 | } |
| 166 | 147 | |
| 167 | 148 | return $result; |
| 168 | 149 | } |
| 169 | 150 | |
| 170 | public function streamGenerator( | |
| 171 | string $sha | |
| 172 | ): Generator { | |
| 151 | public function streamGenerator( string $sha ): Generator { | |
| 173 | 152 | yield from $this->streamShaGenerator( $sha, 0 ); |
| 174 | 153 | } |
| 175 | 154 | |
| 176 | public function streamRawCompressed( | |
| 177 | string $sha | |
| 178 | ): Generator { | |
| 155 | public function streamRawCompressed( string $sha ): Generator { | |
| 179 | 156 | $found = false; |
| 180 | 157 | $file = ''; |
| ... | ||
| 192 | 169 | } |
| 193 | 170 | ); |
| 194 | ||
| 195 | if( $found ) { | |
| 196 | $context = $this->createContext( | |
| 197 | $file, $off, 0 | |
| 198 | ); | |
| 199 | 171 | |
| 200 | yield from $this->reader->streamRawCompressed( | |
| 201 | $context | |
| 202 | ); | |
| 203 | } | |
| 172 | yield from $found | |
| 173 | ? $this->reader->streamRawCompressed( | |
| 174 | $this->createContext( $file, $off, 0 ) | |
| 175 | ) | |
| 176 | : []; | |
| 204 | 177 | } |
| 205 | 178 | |
| 206 | public function streamRawDelta( | |
| 207 | string $sha | |
| 208 | ): Generator { | |
| 179 | public function streamRawDelta( string $sha ): Generator { | |
| 209 | 180 | $found = false; |
| 210 | 181 | $file = ''; |
| ... | ||
| 222 | 193 | } |
| 223 | 194 | ); |
| 224 | ||
| 225 | if( $found ) { | |
| 226 | $context = $this->createContext( | |
| 227 | $file, $off, 0 | |
| 228 | ); | |
| 229 | 195 | |
| 230 | yield from $this->reader->streamRawDelta( | |
| 231 | $context | |
| 232 | ); | |
| 233 | } | |
| 196 | yield from $found | |
| 197 | ? $this->reader->streamRawDelta( | |
| 198 | $this->createContext( $file, $off, 0 ) | |
| 199 | ) | |
| 200 | : []; | |
| 234 | 201 | } |
| 235 | 202 | |
| ... | ||
| 253 | 220 | } |
| 254 | 221 | ); |
| 255 | ||
| 256 | if( $found ) { | |
| 257 | $context = $this->createContext( | |
| 258 | $file, $off, $depth | |
| 259 | ); | |
| 260 | 222 | |
| 261 | yield from $this->reader->streamEntryGenerator( | |
| 262 | $context | |
| 263 | ); | |
| 264 | } | |
| 223 | yield from $found | |
| 224 | ? $this->reader->streamEntryGenerator( | |
| 225 | $this->createContext( $file, $off, $depth ) | |
| 226 | ) | |
| 227 | : []; | |
| 265 | 228 | } |
| 266 | 229 | |
| ... | ||
| 274 | 237 | int $offset |
| 275 | 238 | ) use ( &$result ): void { |
| 276 | $context = $this->createContext( | |
| 277 | $packFile, $offset, 0 | |
| 239 | $result = $this->reader->getSize( | |
| 240 | $this->createContext( $packFile, $offset, 0 ) | |
| 278 | 241 | ); |
| 279 | $result = $this->reader->getSize( $context ); | |
| 280 | 242 | } |
| 281 | 243 | ); |
| 12 | 12 | private const MAX_CACHE = 1024; |
| 13 | 13 | |
| 14 | private DeltaDecoder $decoder; | |
| 15 | private array $cache; | |
| 16 | ||
| 17 | public function __construct( DeltaDecoder $decoder ) { | |
| 18 | $this->decoder = $decoder; | |
| 19 | $this->cache = []; | |
| 20 | } | |
| 21 | ||
| 22 | public function getEntryMeta( PackContext $context ): array { | |
| 23 | return $context->computeArray( | |
| 24 | function( StreamReader $stream, int $offset ): array { | |
| 25 | $packStream = new GitPackStream( $stream ); | |
| 26 | $packStream->seek( $offset ); | |
| 27 | $hdr = $packStream->readVarInt(); | |
| 28 | ||
| 29 | return [ | |
| 30 | 'type' => $hdr['type'], | |
| 31 | 'size' => $hdr['size'], | |
| 32 | 'baseOffset' => $hdr['type'] === 6 | |
| 33 | ? $offset - $packStream->readOffsetDelta() | |
| 34 | : 0, | |
| 35 | 'baseSha' => $hdr['type'] === 7 | |
| 36 | ? \bin2hex( $packStream->read( 20 ) ) | |
| 37 | : '' | |
| 38 | ]; | |
| 39 | }, | |
| 40 | [ 'type' => 0, 'size' => 0 ] | |
| 41 | ); | |
| 42 | } | |
| 43 | ||
| 44 | public function getSize( PackContext $context ): int { | |
| 45 | return $context->computeIntDedicated( | |
| 46 | function( StreamReader $stream, int $offset ): int { | |
| 47 | $packStream = new GitPackStream( $stream ); | |
| 48 | $packStream->seek( $offset ); | |
| 49 | $hdr = $packStream->readVarInt(); | |
| 50 | ||
| 51 | return $hdr['type'] === 6 || $hdr['type'] === 7 | |
| 52 | ? $this->decoder->readDeltaTargetSize( | |
| 53 | $stream, $hdr['type'] | |
| 54 | ) | |
| 55 | : $hdr['size']; | |
| 56 | }, | |
| 57 | 0 | |
| 58 | ); | |
| 59 | } | |
| 60 | ||
| 61 | public function read( | |
| 62 | PackContext $context, | |
| 63 | int $cap, | |
| 64 | callable $readShaBaseFn | |
| 65 | ): string { | |
| 66 | return $context->computeStringDedicated( | |
| 67 | function( | |
| 68 | StreamReader $s, | |
| 69 | int $o | |
| 70 | ) use ( $cap, $readShaBaseFn ): string { | |
| 71 | return $this->readWithStream( | |
| 72 | new GitPackStream( $s ), | |
| 73 | $o, | |
| 74 | $cap, | |
| 75 | $readShaBaseFn | |
| 76 | ); | |
| 77 | }, | |
| 78 | '' | |
| 79 | ); | |
| 80 | } | |
| 81 | ||
| 82 | private function readWithStream( | |
| 83 | GitPackStream $stream, | |
| 84 | int $offset, | |
| 85 | int $cap, | |
| 86 | callable $readShaBaseFn | |
| 87 | ): string { | |
| 88 | $stream->seek( $offset ); | |
| 89 | $hdr = $stream->readVarInt(); | |
| 90 | $type = $hdr['type']; | |
| 91 | ||
| 92 | $result = isset( $this->cache[$offset] ) | |
| 93 | ? ( $cap > 0 && \strlen( $this->cache[$offset] ) > $cap | |
| 94 | ? \substr( $this->cache[$offset], 0, $cap ) | |
| 95 | : $this->cache[$offset] ) | |
| 96 | : ( $type === 6 | |
| 97 | ? $this->readOffsetDeltaContent( | |
| 98 | $stream, $offset, $cap, $readShaBaseFn | |
| 99 | ) | |
| 100 | : ( $type === 7 | |
| 101 | ? $this->readRefDeltaContent( | |
| 102 | $stream, $cap, $readShaBaseFn | |
| 103 | ) | |
| 104 | : $this->inflate( $stream, $cap ) ) ); | |
| 105 | ||
| 106 | if( $cap === 0 && !isset( $this->cache[$offset] ) ) { | |
| 107 | $this->cache[$offset] = $result; | |
| 108 | ||
| 109 | if( \count( $this->cache ) > self::MAX_CACHE ) { | |
| 110 | unset( | |
| 111 | $this->cache[\array_key_first( $this->cache )] | |
| 112 | ); | |
| 113 | } | |
| 114 | } | |
| 115 | ||
| 116 | return $result; | |
| 117 | } | |
| 118 | ||
| 119 | private function readOffsetDeltaContent( | |
| 120 | GitPackStream $stream, | |
| 121 | int $offset, | |
| 122 | int $cap, | |
| 123 | callable $readShaBaseFn | |
| 124 | ): string { | |
| 125 | $neg = $stream->readOffsetDelta(); | |
| 126 | $cur = $stream->tell(); | |
| 127 | $bData = $this->readWithStream( | |
| 128 | $stream, | |
| 129 | $offset - $neg, | |
| 130 | $cap, | |
| 131 | $readShaBaseFn | |
| 132 | ); | |
| 133 | ||
| 134 | $stream->seek( $cur ); | |
| 135 | ||
| 136 | return $this->decoder->apply( | |
| 137 | $bData, | |
| 138 | $this->inflate( $stream ), | |
| 139 | $cap | |
| 140 | ); | |
| 141 | } | |
| 142 | ||
| 143 | private function readRefDeltaContent( | |
| 144 | GitPackStream $stream, | |
| 145 | int $cap, | |
| 146 | callable $readShaBaseFn | |
| 147 | ): string { | |
| 148 | $sha = \bin2hex( $stream->read( 20 ) ); | |
| 149 | $cur = $stream->tell(); | |
| 150 | $bas = $readShaBaseFn( $sha, $cap ); | |
| 151 | ||
| 152 | $stream->seek( $cur ); | |
| 153 | ||
| 154 | return $this->decoder->apply( | |
| 155 | $bas, | |
| 156 | $this->inflate( $stream ), | |
| 157 | $cap | |
| 158 | ); | |
| 159 | } | |
| 160 | ||
| 161 | public function streamRawCompressed( | |
| 162 | PackContext $context | |
| 163 | ): Generator { | |
| 164 | yield from $context->streamGenerator( | |
| 165 | function( StreamReader $stream, int $offset ): Generator { | |
| 166 | $packStream = new GitPackStream( $stream ); | |
| 167 | $packStream->seek( $offset ); | |
| 168 | $hdr = $packStream->readVarInt(); | |
| 169 | ||
| 170 | yield from $hdr['type'] !== 6 && $hdr['type'] !== 7 | |
| 171 | ? (new ZlibExtractorStream())->stream( $stream ) | |
| 172 | : []; | |
| 173 | } | |
| 174 | ); | |
| 175 | } | |
| 176 | ||
| 177 | public function streamRawDelta( PackContext $context ): Generator { | |
| 178 | yield from $context->streamGenerator( | |
| 179 | function( StreamReader $stream, int $offset ): Generator { | |
| 180 | $packStream = new GitPackStream( $stream ); | |
| 181 | $packStream->seek( $offset ); | |
| 182 | $hdr = $packStream->readVarInt(); | |
| 183 | ||
| 184 | if( $hdr['type'] === 6 ) { | |
| 185 | $packStream->readOffsetDelta(); | |
| 186 | } elseif( $hdr['type'] === 7 ) { | |
| 187 | $packStream->read( 20 ); | |
| 188 | } | |
| 189 | ||
| 190 | yield from (new ZlibExtractorStream())->stream( $stream ); | |
| 191 | } | |
| 192 | ); | |
| 193 | } | |
| 194 | ||
| 195 | public function streamEntryGenerator( | |
| 196 | PackContext $context | |
| 197 | ): Generator { | |
| 198 | yield from $context->streamGeneratorDedicated( | |
| 199 | function( | |
| 200 | StreamReader $stream, | |
| 201 | int $offset | |
| 202 | ) use ( $context ): Generator { | |
| 203 | $packStream = new GitPackStream( $stream ); | |
| 204 | $packStream->seek( $offset ); | |
| 205 | $hdr = $packStream->readVarInt(); | |
| 206 | ||
| 207 | yield from $hdr['type'] === 6 || $hdr['type'] === 7 | |
| 208 | ? $this->streamDeltaObjectGenerator( | |
| 209 | $packStream, | |
| 210 | $context, | |
| 211 | $hdr['type'], | |
| 212 | $offset | |
| 213 | ) | |
| 214 | : (new ZlibInflaterStream())->stream( $stream ); | |
| 215 | } | |
| 216 | ); | |
| 217 | } | |
| 218 | ||
| 219 | private function streamDeltaObjectGenerator( | |
| 220 | GitPackStream $stream, | |
| 221 | PackContext $context, | |
| 222 | int $type, | |
| 223 | int $offset | |
| 224 | ): Generator { | |
| 225 | yield from $context->isWithinDepth( self::MAX_DEPTH ) | |
| 226 | ? ( $type === 6 | |
| 227 | ? $this->processOffsetDelta( | |
| 228 | $stream, $context, $offset | |
| 229 | ) | |
| 230 | : $this->processRefDelta( $stream, $context ) ) | |
| 231 | : []; | |
| 232 | } | |
| 233 | ||
| 234 | private function readSizeWithStream( | |
| 235 | GitPackStream $stream, | |
| 236 | int $offset | |
| 237 | ): int { | |
| 238 | $cur = $stream->tell(); | |
| 239 | $stream->seek( $offset ); | |
| 240 | $hdr = $stream->readVarInt(); | |
| 241 | ||
| 242 | $result = isset( $this->cache[$offset] ) | |
| 243 | ? \strlen( $this->cache[$offset] ) | |
| 244 | : ( $hdr['type'] === 6 || $hdr['type'] === 7 | |
| 245 | ? $this->decoder->readDeltaTargetSize( | |
| 246 | $stream, $hdr['type'] | |
| 247 | ) | |
| 248 | : $hdr['size'] ); | |
| 249 | ||
| 250 | if( !isset( $this->cache[$offset] ) ) { | |
| 251 | $stream->seek( $cur ); | |
| 252 | } | |
| 253 | ||
| 254 | return $result; | |
| 255 | } | |
| 256 | ||
| 257 | private function processOffsetDelta( | |
| 258 | GitPackStream $stream, | |
| 259 | PackContext $context, | |
| 260 | int $offset | |
| 261 | ): Generator { | |
| 262 | $neg = $stream->readOffsetDelta(); | |
| 263 | $cur = $stream->tell(); | |
| 264 | $baseOff = $offset - $neg; | |
| 265 | ||
| 266 | $baseSrc = isset( $this->cache[$baseOff] ) | |
| 267 | ? $this->cache[$baseOff] | |
| 268 | : ( $this->readSizeWithStream( $stream, $baseOff ) | |
| 269 | <= self::MAX_BASE_RAM | |
| 270 | ? $this->readWithStream( | |
| 271 | $stream, | |
| 272 | $baseOff, | |
| 273 | 0, | |
| 274 | function( | |
| 275 | string $sha, | |
| 276 | int $cap | |
| 277 | ) use ( $context ): string { | |
| 278 | return $this->resolveBaseSha( | |
| 279 | $sha, $cap, $context | |
| 280 | ); | |
| 281 | } | |
| 282 | ) | |
| 283 | : $this->collectBase( | |
| 284 | $this->streamEntryGenerator( | |
| 285 | $context->deriveOffsetContext( $neg ) | |
| 286 | ) | |
| 287 | ) ); | |
| 288 | ||
| 289 | $stream->seek( $cur ); | |
| 290 | ||
| 291 | yield from $this->decoder->applyStreamGenerator( | |
| 292 | $stream, $baseSrc | |
| 293 | ); | |
| 294 | } | |
| 295 | ||
| 296 | private function processRefDelta( | |
| 297 | GitPackStream $stream, | |
| 298 | PackContext $context | |
| 299 | ): Generator { | |
| 300 | $baseSha = \bin2hex( $stream->read( 20 ) ); | |
| 301 | $cur = $stream->tell(); | |
| 302 | $size = $context->resolveBaseSize( $baseSha ); | |
| 303 | ||
| 304 | $baseSrc = $size <= self::MAX_BASE_RAM | |
| 305 | ? $this->resolveBaseSha( $baseSha, 0, $context ) | |
| 306 | : $this->collectBase( | |
| 307 | $context->resolveBaseStream( $baseSha ) | |
| 308 | ); | |
| 309 | ||
| 310 | $stream->seek( $cur ); | |
| 311 | ||
| 312 | yield from $this->decoder->applyStreamGenerator( | |
| 313 | $stream, $baseSrc | |
| 314 | ); | |
| 315 | } | |
| 316 | ||
| 317 | private function collectBase( | |
| 318 | iterable $chunks | |
| 319 | ): BufferedReader|string { | |
| 320 | $parts = []; | |
| 321 | $total = 0; | |
| 322 | $tmp = false; | |
| 323 | ||
| 324 | foreach( $chunks as $chunk ) { | |
| 325 | $total += \strlen( $chunk ); | |
| 326 | ||
| 327 | if( $tmp instanceof BufferedReader ) { | |
| 328 | $tmp->write( $chunk ); | |
| 329 | } elseif( $total > self::MAX_BASE_RAM ) { | |
| 330 | $tmp = new BufferedReader( | |
| 331 | 'php://temp/maxmemory:65536', 'w+b' | |
| 332 | ); | |
| 333 | ||
| 334 | foreach( $parts as $part ) { | |
| 335 | $tmp->write( $part ); | |
| 336 | } | |
| 337 | ||
| 338 | $tmp->write( $chunk ); | |
| 339 | $parts = []; | |
| 340 | } else { | |
| 341 | $parts[] = $chunk; | |
| 342 | } | |
| 343 | } | |
| 344 | ||
| 345 | if( $tmp instanceof BufferedReader ) { | |
| 346 | $tmp->rewind(); | |
| 347 | } | |
| 348 | ||
| 349 | return $tmp === false ? \implode( '', $parts ) : $tmp; | |
| 350 | } | |
| 351 | ||
| 352 | private function resolveBaseSha( | |
| 353 | string $sha, | |
| 354 | int $cap, | |
| 355 | PackContext $context | |
| 356 | ): string { | |
| 357 | $chunks = []; | |
| 358 | ||
| 359 | foreach( | |
| 360 | $context->resolveBaseStream( $sha ) as $chunk | |
| 361 | ) { | |
| 362 | $chunks[] = $chunk; | |
| 363 | } | |
| 364 | ||
| 365 | $result = \implode( '', $chunks ); | |
| 366 | ||
| 367 | return $cap > 0 && \strlen( $result ) > $cap | |
| 368 | ? \substr( $result, 0, $cap ) | |
| 369 | : $result; | |
| 370 | } | |
| 371 | ||
| 372 | private function inflate( | |
| 373 | StreamReader $stream, | |
| 374 | int $cap = 0 | |
| 375 | ): string { | |
| 376 | $inflater = new ZlibInflaterStream(); | |
| 377 | $chunks = []; | |
| 378 | $len = 0; | |
| 379 | ||
| 380 | foreach( $inflater->stream( $stream ) as $data ) { | |
| 14 | private DeltaDecoder $decoder; | |
| 15 | private ZlibDeflatorStream $deflator; | |
| 16 | private ZlibInflaterStream $inflater; | |
| 17 | private array $cache; | |
| 18 | private int $cacheSize; | |
| 19 | ||
| 20 | public function __construct( DeltaDecoder $decoder ) { | |
| 21 | $this->decoder = $decoder; | |
| 22 | $this->deflator = new ZlibDeflatorStream(); | |
| 23 | $this->inflater = new ZlibInflaterStream(); | |
| 24 | $this->cache = []; | |
| 25 | $this->cacheSize = 0; | |
| 26 | } | |
| 27 | ||
| 28 | public function getEntryMeta( PackContext $context ): array { | |
| 29 | return $context->computeArray( | |
| 30 | function( StreamReader $stream, int $offset ): array { | |
| 31 | $packStream = new GitPackStream( $stream ); | |
| 32 | $packStream->seek( $offset ); | |
| 33 | $hdr = $packStream->readVarInt(); | |
| 34 | ||
| 35 | return [ | |
| 36 | 'type' => $hdr['type'], | |
| 37 | 'size' => $hdr['size'], | |
| 38 | 'baseOffset' => $hdr['type'] === 6 | |
| 39 | ? $offset - $packStream->readOffsetDelta() | |
| 40 | : 0, | |
| 41 | 'baseSha' => $hdr['type'] === 7 | |
| 42 | ? \bin2hex( $packStream->read( 20 ) ) | |
| 43 | : '' | |
| 44 | ]; | |
| 45 | }, | |
| 46 | [ 'type' => 0, 'size' => 0 ] | |
| 47 | ); | |
| 48 | } | |
| 49 | ||
| 50 | public function getSize( PackContext $context ): int { | |
| 51 | return $context->computeIntDedicated( | |
| 52 | function( StreamReader $stream, int $offset ): int { | |
| 53 | $packStream = new GitPackStream( $stream ); | |
| 54 | $packStream->seek( $offset ); | |
| 55 | $hdr = $packStream->readVarInt(); | |
| 56 | ||
| 57 | return $hdr['type'] === 6 || $hdr['type'] === 7 | |
| 58 | ? $this->decoder->readDeltaTargetSize( | |
| 59 | $stream, $hdr['type'] | |
| 60 | ) | |
| 61 | : $hdr['size']; | |
| 62 | }, | |
| 63 | 0 | |
| 64 | ); | |
| 65 | } | |
| 66 | ||
| 67 | public function read( | |
| 68 | PackContext $context, | |
| 69 | int $cap, | |
| 70 | callable $readShaBaseFn | |
| 71 | ): string { | |
| 72 | return $context->computeStringDedicated( | |
| 73 | function( | |
| 74 | StreamReader $s, | |
| 75 | int $o | |
| 76 | ) use ( $cap, $readShaBaseFn ): string { | |
| 77 | return $this->readWithStream( | |
| 78 | new GitPackStream( $s ), $o, $cap, $readShaBaseFn | |
| 79 | ); | |
| 80 | }, | |
| 81 | '' | |
| 82 | ); | |
| 83 | } | |
| 84 | ||
| 85 | private function readWithStream( | |
| 86 | GitPackStream $stream, | |
| 87 | int $offset, | |
| 88 | int $cap, | |
| 89 | callable $readShaBaseFn | |
| 90 | ): string { | |
| 91 | $stream->seek( $offset ); | |
| 92 | $hdr = $stream->readVarInt(); | |
| 93 | $type = $hdr['type']; | |
| 94 | ||
| 95 | $result = isset( $this->cache[$offset] ) | |
| 96 | ? ( $cap > 0 && \strlen( $this->cache[$offset] ) > $cap | |
| 97 | ? \substr( $this->cache[$offset], 0, $cap ) | |
| 98 | : $this->cache[$offset] ) | |
| 99 | : ( $type === 6 | |
| 100 | ? $this->readOffsetDeltaContent( | |
| 101 | $stream, $offset, $cap, $readShaBaseFn | |
| 102 | ) | |
| 103 | : ( $type === 7 | |
| 104 | ? $this->readRefDeltaContent( | |
| 105 | $stream, $cap, $readShaBaseFn | |
| 106 | ) | |
| 107 | : $this->inflate( $stream, $cap ) ) ); | |
| 108 | ||
| 109 | if( $cap === 0 && !isset( $this->cache[$offset] ) ) { | |
| 110 | $this->cache[$offset] = $result; | |
| 111 | $this->cacheSize++; | |
| 112 | ||
| 113 | if( $this->cacheSize > self::MAX_CACHE ) { | |
| 114 | unset( $this->cache[\array_key_first( $this->cache )] ); | |
| 115 | $this->cacheSize--; | |
| 116 | } | |
| 117 | } | |
| 118 | ||
| 119 | return $result; | |
| 120 | } | |
| 121 | ||
| 122 | private function readOffsetDeltaContent( | |
| 123 | GitPackStream $stream, | |
| 124 | int $offset, | |
| 125 | int $cap, | |
| 126 | callable $readShaBaseFn | |
| 127 | ): string { | |
| 128 | $neg = $stream->readOffsetDelta(); | |
| 129 | $cur = $stream->tell(); | |
| 130 | $bData = $this->readWithStream( | |
| 131 | $stream, $offset - $neg, $cap, $readShaBaseFn | |
| 132 | ); | |
| 133 | ||
| 134 | $stream->seek( $cur ); | |
| 135 | ||
| 136 | return $this->decoder->apply( | |
| 137 | $bData, | |
| 138 | $this->inflate( $stream ), | |
| 139 | $cap | |
| 140 | ); | |
| 141 | } | |
| 142 | ||
| 143 | private function readRefDeltaContent( | |
| 144 | GitPackStream $stream, | |
| 145 | int $cap, | |
| 146 | callable $readShaBaseFn | |
| 147 | ): string { | |
| 148 | $sha = \bin2hex( $stream->read( 20 ) ); | |
| 149 | $cur = $stream->tell(); | |
| 150 | $bas = $readShaBaseFn( $sha, $cap ); | |
| 151 | ||
| 152 | $stream->seek( $cur ); | |
| 153 | ||
| 154 | return $this->decoder->apply( | |
| 155 | $bas, | |
| 156 | $this->inflate( $stream ), | |
| 157 | $cap | |
| 158 | ); | |
| 159 | } | |
| 160 | ||
| 161 | public function streamRawCompressed( | |
| 162 | PackContext $context | |
| 163 | ): Generator { | |
| 164 | yield from $context->streamGenerator( | |
| 165 | function( StreamReader $stream, int $offset ): Generator { | |
| 166 | $packStream = new GitPackStream( $stream ); | |
| 167 | $packStream->seek( $offset ); | |
| 168 | $hdr = $packStream->readVarInt(); | |
| 169 | ||
| 170 | yield from $hdr['type'] !== 6 && $hdr['type'] !== 7 | |
| 171 | ? $this->deflator->stream( $stream ) | |
| 172 | : []; | |
| 173 | } | |
| 174 | ); | |
| 175 | } | |
| 176 | ||
| 177 | public function streamRawDelta( PackContext $context ): Generator { | |
| 178 | yield from $context->streamGenerator( | |
| 179 | function( StreamReader $stream, int $offset ): Generator { | |
| 180 | $packStream = new GitPackStream( $stream ); | |
| 181 | $packStream->seek( $offset ); | |
| 182 | $hdr = $packStream->readVarInt(); | |
| 183 | ||
| 184 | if( $hdr['type'] === 6 ) { | |
| 185 | $packStream->readOffsetDelta(); | |
| 186 | } elseif( $hdr['type'] === 7 ) { | |
| 187 | $packStream->read( 20 ); | |
| 188 | } | |
| 189 | ||
| 190 | yield from $this->deflator->stream( $stream ); | |
| 191 | } | |
| 192 | ); | |
| 193 | } | |
| 194 | ||
| 195 | public function streamEntryGenerator( | |
| 196 | PackContext $context | |
| 197 | ): Generator { | |
| 198 | yield from $context->streamGeneratorDedicated( | |
| 199 | function( | |
| 200 | StreamReader $stream, | |
| 201 | int $offset | |
| 202 | ) use ( $context ): Generator { | |
| 203 | $packStream = new GitPackStream( $stream ); | |
| 204 | $packStream->seek( $offset ); | |
| 205 | $hdr = $packStream->readVarInt(); | |
| 206 | ||
| 207 | yield from $hdr['type'] === 6 || $hdr['type'] === 7 | |
| 208 | ? $this->streamDeltaObjectGenerator( | |
| 209 | $packStream, $context, $hdr['type'], $offset | |
| 210 | ) | |
| 211 | : $this->inflater->stream( $stream ); | |
| 212 | } | |
| 213 | ); | |
| 214 | } | |
| 215 | ||
| 216 | private function streamDeltaObjectGenerator( | |
| 217 | GitPackStream $stream, | |
| 218 | PackContext $context, | |
| 219 | int $type, | |
| 220 | int $offset | |
| 221 | ): Generator { | |
| 222 | yield from $context->isWithinDepth( self::MAX_DEPTH ) | |
| 223 | ? ( $type === 6 | |
| 224 | ? $this->processOffsetDelta( $stream, $context, $offset ) | |
| 225 | : $this->processRefDelta( $stream, $context ) ) | |
| 226 | : []; | |
| 227 | } | |
| 228 | ||
| 229 | private function readSizeWithStream( | |
| 230 | GitPackStream $stream, | |
| 231 | int $offset | |
| 232 | ): int { | |
| 233 | $cur = $stream->tell(); | |
| 234 | $stream->seek( $offset ); | |
| 235 | $hdr = $stream->readVarInt(); | |
| 236 | ||
| 237 | $result = isset( $this->cache[$offset] ) | |
| 238 | ? \strlen( $this->cache[$offset] ) | |
| 239 | : ( $hdr['type'] === 6 || $hdr['type'] === 7 | |
| 240 | ? $this->decoder->readDeltaTargetSize( | |
| 241 | $stream, $hdr['type'] | |
| 242 | ) | |
| 243 | : $hdr['size'] ); | |
| 244 | ||
| 245 | if( !isset( $this->cache[$offset] ) ) { | |
| 246 | $stream->seek( $cur ); | |
| 247 | } | |
| 248 | ||
| 249 | return $result; | |
| 250 | } | |
| 251 | ||
| 252 | private function processOffsetDelta( | |
| 253 | GitPackStream $stream, | |
| 254 | PackContext $context, | |
| 255 | int $offset | |
| 256 | ): Generator { | |
| 257 | $neg = $stream->readOffsetDelta(); | |
| 258 | $cur = $stream->tell(); | |
| 259 | $baseOff = $offset - $neg; | |
| 260 | ||
| 261 | $baseSrc = isset( $this->cache[$baseOff] ) | |
| 262 | ? $this->cache[$baseOff] | |
| 263 | : ( $this->readSizeWithStream( $stream, $baseOff ) | |
| 264 | <= self::MAX_BASE_RAM | |
| 265 | ? $this->readWithStream( | |
| 266 | $stream, | |
| 267 | $baseOff, | |
| 268 | 0, | |
| 269 | function( string $sha, int $cap ) use ( $context ): string { | |
| 270 | return $this->resolveBaseSha( $sha, $cap, $context ); | |
| 271 | } | |
| 272 | ) | |
| 273 | : $this->collectBase( | |
| 274 | $this->streamEntryGenerator( | |
| 275 | $context->deriveOffsetContext( $neg ) | |
| 276 | ) | |
| 277 | ) ); | |
| 278 | ||
| 279 | $stream->seek( $cur ); | |
| 280 | ||
| 281 | yield from $this->decoder->applyStreamGenerator( | |
| 282 | $stream, $baseSrc | |
| 283 | ); | |
| 284 | } | |
| 285 | ||
| 286 | private function processRefDelta( | |
| 287 | GitPackStream $stream, | |
| 288 | PackContext $context | |
| 289 | ): Generator { | |
| 290 | $baseSha = \bin2hex( $stream->read( 20 ) ); | |
| 291 | $cur = $stream->tell(); | |
| 292 | $size = $context->resolveBaseSize( $baseSha ); | |
| 293 | ||
| 294 | $baseSrc = $size <= self::MAX_BASE_RAM | |
| 295 | ? $this->resolveBaseSha( $baseSha, 0, $context ) | |
| 296 | : $this->collectBase( | |
| 297 | $context->resolveBaseStream( $baseSha ) | |
| 298 | ); | |
| 299 | ||
| 300 | $stream->seek( $cur ); | |
| 301 | ||
| 302 | yield from $this->decoder->applyStreamGenerator( | |
| 303 | $stream, $baseSrc | |
| 304 | ); | |
| 305 | } | |
| 306 | ||
| 307 | private function collectBase( | |
| 308 | iterable $chunks | |
| 309 | ): BufferedReader|string { | |
| 310 | $parts = []; | |
| 311 | $total = 0; | |
| 312 | $tmp = false; | |
| 313 | ||
| 314 | foreach( $chunks as $chunk ) { | |
| 315 | $total += \strlen( $chunk ); | |
| 316 | ||
| 317 | if( $tmp instanceof BufferedReader ) { | |
| 318 | $tmp->write( $chunk ); | |
| 319 | } elseif( $total > self::MAX_BASE_RAM ) { | |
| 320 | $tmp = new BufferedReader( | |
| 321 | 'php://temp/maxmemory:65536', 'w+b' | |
| 322 | ); | |
| 323 | ||
| 324 | foreach( $parts as $part ) { | |
| 325 | $tmp->write( $part ); | |
| 326 | } | |
| 327 | ||
| 328 | $tmp->write( $chunk ); | |
| 329 | $parts = []; | |
| 330 | } else { | |
| 331 | $parts[] = $chunk; | |
| 332 | } | |
| 333 | } | |
| 334 | ||
| 335 | if( $tmp instanceof BufferedReader ) { | |
| 336 | $tmp->rewind(); | |
| 337 | } | |
| 338 | ||
| 339 | return $tmp === false ? \implode( '', $parts ) : $tmp; | |
| 340 | } | |
| 341 | ||
| 342 | private function resolveBaseSha( | |
| 343 | string $sha, | |
| 344 | int $cap, | |
| 345 | PackContext $context | |
| 346 | ): string { | |
| 347 | $chunks = []; | |
| 348 | ||
| 349 | foreach( $context->resolveBaseStream( $sha ) as $chunk ) { | |
| 350 | $chunks[] = $chunk; | |
| 351 | } | |
| 352 | ||
| 353 | $result = \implode( '', $chunks ); | |
| 354 | ||
| 355 | return $cap > 0 && \strlen( $result ) > $cap | |
| 356 | ? \substr( $result, 0, $cap ) | |
| 357 | : $result; | |
| 358 | } | |
| 359 | ||
| 360 | private function inflate( | |
| 361 | StreamReader $stream, | |
| 362 | int $cap = 0 | |
| 363 | ): string { | |
| 364 | $chunks = []; | |
| 365 | $len = 0; | |
| 366 | ||
| 367 | foreach( $this->inflater->stream( $stream ) as $data ) { | |
| 381 | 368 | $chunks[] = $data; |
| 382 | 369 | $len += \strlen( $data ); |
| 31 | 31 | $written[$sha] = $outPos; |
| 32 | 32 | $baseSha = $entry['baseSha']; |
| 33 | ||
| 34 | $reuse = $baseSha !== '' | |
| 33 | $reuse = $baseSha !== '' | |
| 35 | 34 | && isset( $written[$baseSha] ); |
| 36 | ||
| 37 | if( $reuse ) { | |
| 38 | $hdr = $this->encodeEntryHeader( | |
| 39 | 6, $entry['deltaSize'] | |
| 40 | ); | |
| 41 | $hdr .= $this->encodeOffsetDelta( | |
| 42 | $outPos - $written[$baseSha] | |
| 43 | ); | |
| 44 | 35 | |
| 45 | \hash_update( $ctx, $hdr ); | |
| 46 | $outPos += \strlen( $hdr ); | |
| 47 | yield $hdr; | |
| 36 | $hdr = $reuse | |
| 37 | ? $this->encodeEntryHeader( 6, $entry['deltaSize'] ) | |
| 38 | . $this->encodeOffsetDelta( $outPos - $written[$baseSha] ) | |
| 39 | : $this->encodeEntryHeader( | |
| 40 | $entry['logicalType'], | |
| 41 | $entry['size'] > 0 | |
| 42 | ? $entry['size'] | |
| 43 | : $this->getObjectSize( $sha ) | |
| 44 | ); | |
| 48 | 45 | |
| 49 | foreach( | |
| 50 | $this->packs->streamRawDelta( | |
| 51 | $sha | |
| 52 | ) as $chunk | |
| 53 | ) { | |
| 54 | \hash_update( $ctx, $chunk ); | |
| 55 | $outPos += \strlen( $chunk ); | |
| 56 | yield $chunk; | |
| 57 | } | |
| 58 | } else { | |
| 59 | $size = $this->getObjectSize( $sha ); | |
| 60 | $hdr = $this->encodeEntryHeader( | |
| 61 | $entry['logicalType'], $size | |
| 62 | ); | |
| 46 | \hash_update( $ctx, $hdr ); | |
| 47 | $outPos += \strlen( $hdr ); | |
| 48 | yield $hdr; | |
| 63 | 49 | |
| 64 | \hash_update( $ctx, $hdr ); | |
| 65 | $outPos += \strlen( $hdr ); | |
| 66 | yield $hdr; | |
| 50 | $stream = $reuse | |
| 51 | ? $this->packs->streamRawDelta( $sha ) | |
| 52 | : $this->streamCompressed( $sha ); | |
| 67 | 53 | |
| 68 | foreach( | |
| 69 | $this->streamCompressed( | |
| 70 | $sha | |
| 71 | ) as $chunk | |
| 72 | ) { | |
| 73 | \hash_update( $ctx, $chunk ); | |
| 74 | $outPos += \strlen( $chunk ); | |
| 75 | yield $chunk; | |
| 76 | } | |
| 54 | foreach( $stream as $chunk ) { | |
| 55 | \hash_update( $ctx, $chunk ); | |
| 56 | $outPos += \strlen( $chunk ); | |
| 57 | yield $chunk; | |
| 77 | 58 | } |
| 78 | 59 | } |
| 79 | 60 | |
| 80 | 61 | yield \hash_final( $ctx, true ); |
| 81 | 62 | } |
| 82 | 63 | |
| 83 | private function buildEntries( | |
| 84 | array $objs | |
| 85 | ): array { | |
| 64 | private function buildEntries( array $objs ): array { | |
| 86 | 65 | $entries = []; |
| 87 | 66 | $offToSha = []; |
| ... | ||
| 101 | 80 | |
| 102 | 81 | if( $meta['file'] !== '' ) { |
| 103 | $offToSha[$meta['file']][$meta['offset']] | |
| 104 | = $sha; | |
| 82 | $offToSha[$meta['file']][$meta['offset']] = $sha; | |
| 105 | 83 | } |
| 106 | 84 | } |
| 107 | 85 | |
| 108 | 86 | foreach( $entries as &$e ) { |
| 109 | if( | |
| 110 | $e['packType'] === 6 | |
| 111 | && $e['baseOffset'] > 0 | |
| 112 | ) { | |
| 113 | $e['baseSha'] | |
| 114 | = $offToSha[$e['packFile']][$e['baseOffset']] | |
| 115 | ?? ''; | |
| 116 | } | |
| 87 | $e['baseSha'] = $e['packType'] === 6 && $e['baseOffset'] > 0 | |
| 88 | ? ( $offToSha[$e['packFile']][$e['baseOffset']] ?? '' ) | |
| 89 | : $e['baseSha']; | |
| 117 | 90 | } |
| 118 | 91 | |
| 119 | 92 | unset( $e ); |
| 120 | 93 | |
| 121 | \uasort( | |
| 122 | $entries, | |
| 123 | function( array $a, array $b ): int { | |
| 124 | $cmp = $a['packFile'] <=> $b['packFile']; | |
| 94 | $files = []; | |
| 95 | $offsets = []; | |
| 125 | 96 | |
| 126 | return $cmp !== 0 | |
| 127 | ? $cmp | |
| 128 | : $a['offset'] <=> $b['offset']; | |
| 129 | } | |
| 97 | foreach( $entries as $e ) { | |
| 98 | $files[] = $e['packFile']; | |
| 99 | $offsets[] = $e['offset']; | |
| 100 | } | |
| 101 | ||
| 102 | \array_multisort( | |
| 103 | $files, \SORT_ASC, \SORT_STRING, | |
| 104 | $offsets, \SORT_ASC, \SORT_NUMERIC, | |
| 105 | $entries | |
| 130 | 106 | ); |
| 107 | ||
| 108 | foreach( $entries as $sha => &$e ) { | |
| 109 | $e['size'] = $e['baseSha'] === '' | |
| 110 | ? $this->getObjectSize( $sha ) | |
| 111 | : 0; | |
| 112 | } | |
| 113 | ||
| 114 | unset( $e ); | |
| 131 | 115 | |
| 132 | 116 | return $entries; |
| 133 | 117 | } |
| 134 | 118 | |
| 135 | private function getObjectSize( | |
| 136 | string $sha | |
| 137 | ): int { | |
| 119 | private function getObjectSize( string $sha ): int { | |
| 138 | 120 | return $this->packs->getSize( $sha ) |
| 139 | 121 | ?: $this->loose->getSize( $sha ); |
| 140 | 122 | } |
| 141 | ||
| 142 | private function streamCompressed( | |
| 143 | string $sha | |
| 144 | ): Generator { | |
| 145 | $yielded = false; | |
| 146 | 123 | |
| 147 | foreach( | |
| 148 | $this->packs->streamRawCompressed( | |
| 149 | $sha | |
| 150 | ) as $chunk | |
| 151 | ) { | |
| 152 | $yielded = true; | |
| 153 | yield $chunk; | |
| 154 | } | |
| 124 | private function streamCompressed( string $sha ): Generator { | |
| 125 | $generator = $this->packs->streamRawCompressed( $sha ); | |
| 126 | $generator->rewind(); | |
| 155 | 127 | |
| 156 | if( !$yielded ) { | |
| 157 | $deflate = \deflate_init( | |
| 158 | \ZLIB_ENCODING_DEFLATE | |
| 159 | ); | |
| 128 | if( $generator->valid() ) { | |
| 129 | yield from $generator; | |
| 130 | } else { | |
| 131 | $deflate = \deflate_init( \ZLIB_ENCODING_DEFLATE ); | |
| 160 | 132 | |
| 161 | foreach( | |
| 162 | $this->getDecompressedChunks( | |
| 163 | $sha | |
| 164 | ) as $raw | |
| 165 | ) { | |
| 133 | foreach( $this->getDecompressedChunks( $sha ) as $raw ) { | |
| 166 | 134 | $compressed = \deflate_add( |
| 167 | 135 | $deflate, $raw, \ZLIB_NO_FLUSH |
| 168 | 136 | ); |
| 169 | 137 | |
| 170 | 138 | if( $compressed !== '' ) { |
| 171 | 139 | yield $compressed; |
| 172 | 140 | } |
| 173 | 141 | } |
| 174 | 142 | |
| 175 | $final = \deflate_add( | |
| 176 | $deflate, '', \ZLIB_FINISH | |
| 177 | ); | |
| 143 | $final = \deflate_add( $deflate, '', \ZLIB_FINISH ); | |
| 178 | 144 | |
| 179 | 145 | if( $final !== '' ) { |
| 180 | 146 | yield $final; |
| 181 | 147 | } |
| 182 | 148 | } |
| 183 | 149 | } |
| 184 | ||
| 185 | private function getDecompressedChunks( | |
| 186 | string $sha | |
| 187 | ): Generator { | |
| 188 | $any = false; | |
| 189 | 150 | |
| 190 | foreach( | |
| 191 | $this->loose->streamChunks( $sha ) as $chunk | |
| 192 | ) { | |
| 193 | $any = true; | |
| 194 | yield $chunk; | |
| 195 | } | |
| 151 | private function getDecompressedChunks( string $sha ): Generator { | |
| 152 | $looseGen = $this->loose->streamChunks( $sha ); | |
| 153 | $looseGen->rewind(); | |
| 196 | 154 | |
| 197 | if( !$any ) { | |
| 198 | foreach( | |
| 199 | $this->packs->streamGenerator( | |
| 200 | $sha | |
| 201 | ) as $chunk | |
| 202 | ) { | |
| 203 | $any = true; | |
| 204 | yield $chunk; | |
| 205 | } | |
| 206 | } | |
| 155 | if( $looseGen->valid() ) { | |
| 156 | yield from $looseGen; | |
| 157 | } else { | |
| 158 | $packGen = $this->packs->streamGenerator( $sha ); | |
| 159 | $packGen->rewind(); | |
| 207 | 160 | |
| 208 | if( !$any ) { | |
| 209 | $data = $this->packs->read( $sha ); | |
| 161 | if( $packGen->valid() ) { | |
| 162 | yield from $packGen; | |
| 163 | } else { | |
| 164 | $data = $this->packs->read( $sha ); | |
| 210 | 165 | |
| 211 | if( $data !== '' ) { | |
| 212 | yield $data; | |
| 166 | if( $data !== '' ) { | |
| 167 | yield $data; | |
| 168 | } | |
| 213 | 169 | } |
| 214 | 170 | } |
| 215 | 171 | } |
| 216 | 172 | |
| 217 | private function encodeEntryHeader( | |
| 218 | int $type, | |
| 219 | int $size | |
| 220 | ): string { | |
| 173 | private function encodeEntryHeader( int $type, int $size ): string { | |
| 221 | 174 | $byte = $type << 4 | $size & 0x0f; |
| 222 | 175 | $sz = $size >> 4; |
| 223 | 176 | $hdr = ''; |
| 224 | 177 | |
| 225 | 178 | while( $sz > 0 ) { |
| 226 | 179 | $hdr .= \chr( $byte | 0x80 ); |
| 227 | 180 | $byte = $sz & 0x7f; |
| 228 | 181 | $sz >>= 7; |
| 229 | 182 | } |
| 230 | ||
| 231 | $hdr .= \chr( $byte ); | |
| 232 | 183 | |
| 233 | return $hdr; | |
| 184 | return $hdr . \chr( $byte ); | |
| 234 | 185 | } |
| 235 | 186 | |
| 236 | private function encodeOffsetDelta( | |
| 237 | int $offset | |
| 238 | ): string { | |
| 187 | private function encodeOffsetDelta( int $offset ): string { | |
| 239 | 188 | $buf = \chr( $offset & 0x7F ); |
| 240 | 189 | $n = $offset >> 7; |
| 241 | 190 | |
| 242 | 191 | while( $n > 0 ) { |
| 243 | 192 | $n--; |
| 244 | $buf = \chr( 0x80 | ($n & 0x7F) ) . $buf; | |
| 193 | $buf = \chr( 0x80 | $n & 0x7F ) . $buf; | |
| 245 | 194 | $n >>= 7; |
| 246 | 195 | } |
| 1 | 1 | <?php |
| 2 | 2 | class RepositoryList { |
| 3 | private const ORDER_FILE = __DIR__ . '/../order.txt'; | |
| 4 | 3 | private const GIT_EXT = '.git'; |
| 5 | 4 | private const GLOB_PATTERN = '/*'; |
| ... | ||
| 15 | 14 | |
| 16 | 15 | private string $reposPath; |
| 16 | private string $orderFile; | |
| 17 | 17 | |
| 18 | public function __construct( string $path ) { | |
| 18 | public function __construct( string $path, string $orderFile ) { | |
| 19 | 19 | $this->reposPath = $path; |
| 20 | $this->orderFile = $orderFile; | |
| 20 | 21 | } |
| 21 | 22 | |
| ... | ||
| 82 | 83 | |
| 83 | 84 | private function sortRepositories( array $repos ): array { |
| 84 | $file = self::ORDER_FILE; | |
| 85 | $file = $this->orderFile; | |
| 85 | 86 | |
| 86 | 87 | if( file_exists( $file ) ) { |
| 39 | 39 | private string $baseHash = ''; |
| 40 | 40 | |
| 41 | public function __construct( string $reposPath ) { | |
| 41 | public function __construct( string $reposPath, string $orderFile ) { | |
| 42 | 42 | $this->git = new Git( $reposPath ); |
| 43 | $list = new RepositoryList( $reposPath ); | |
| 43 | $list = new RepositoryList( $reposPath, $orderFile ); | |
| 44 | 44 | |
| 45 | 45 | $list->eachRepository( function( $repo ) { |
| 51 | 51 | public function render( |
| 52 | 52 | TagRenderer $renderer, |
| 53 | Tag $prevTag | |
| 53 | Tag $prevTag | |
| 54 | 54 | ): void { |
| 55 | $renderer->renderTagItem( | |
| 55 | $renderer->render( | |
| 56 | 56 | $this->name, |
| 57 | 57 | $this->sha, |
| 18 | 18 | public function render(): void { |
| 19 | 19 | $this->renderLayout( function() { |
| 20 | echo '<h2>Repositories</h2>'; | |
| 21 | ||
| 22 | 20 | if( empty( $this->repositories ) ) { |
| 23 | 21 | echo '<div class="empty-state">No repositories found.</div>'; |
| 5 | 5 | |
| 6 | 6 | class TagsPage extends BasePage { |
| 7 | private $currentRepo; | |
| 8 | private $git; | |
| 7 | private array $currentRepo; | |
| 8 | private Git $git; | |
| 9 | 9 | |
| 10 | 10 | public function __construct( |
| 11 | 11 | array $repositories, |
| 12 | 12 | array $currentRepo, |
| 13 | Git $git | |
| 13 | Git $git | |
| 14 | 14 | ) { |
| 15 | 15 | parent::__construct( |
| ... | ||
| 22 | 22 | } |
| 23 | 23 | |
| 24 | public function render() { | |
| 24 | public function render(): void { | |
| 25 | 25 | $this->renderLayout( function() { |
| 26 | 26 | $this->renderBreadcrumbs( $this->currentRepo, ['Tags'] ); |
| 27 | 27 | echo '<h2>Tags</h2>'; |
| 28 | 28 | |
| 29 | 29 | $tags = []; |
| 30 | ||
| 30 | 31 | $this->git->eachTag( function( Tag $tag ) use ( &$tags ) { |
| 31 | 32 | $tags[] = $tag; |
| ... | ||
| 54 | 55 | |
| 55 | 56 | $count = count( $tags ); |
| 57 | ||
| 56 | 58 | for( $i = 0; $i < $count; $i++ ) { |
| 57 | $tag = $tags[$i]; | |
| 58 | $prevTag = $tags[$i + 1] ?? null; | |
| 59 | $tag->render( $renderer, $prevTag ); | |
| 59 | $tags[$i]->render( | |
| 60 | $renderer, | |
| 61 | isset( $tags[$i + 1] ) ? $tags[$i + 1] : new MissingTag() | |
| 62 | ); | |
| 60 | 63 | } |
| 61 | 64 | |
| 14 | 14 | $this->content = $content; |
| 15 | 15 | $this->language = $this->detectLanguage( $mediaType, $filename ); |
| 16 | $this->rules = LanguageDefinitions::get( $this->language ); | |
| 16 | $this->rules = \LanguageDefinitions::get( $this->language ); | |
| 17 | 17 | } |
| 18 | 18 | |
| 19 | 19 | public function render(): string { |
| 20 | $result = htmlspecialchars( $this->content ); | |
| 20 | $result = \htmlspecialchars( $this->content ); | |
| 21 | 21 | |
| 22 | 22 | if( !empty( $this->rules ) ) { |
| 23 | 23 | $patterns = []; |
| 24 | 24 | |
| 25 | 25 | foreach( $this->rules as $name => $pattern ) { |
| 26 | 26 | $delimiter = $pattern[0]; |
| 27 | $pos = strrpos( $pattern, $delimiter ) - 1; | |
| 28 | $inner = substr( $pattern, 1, $pos ); | |
| 29 | $inner = str_replace( '~', '\~', $inner ); | |
| 27 | $inner = \str_replace( | |
| 28 | '~', | |
| 29 | '\~', | |
| 30 | \substr( $pattern, 1, \strrpos( $pattern, $delimiter ) - 1 ) | |
| 31 | ); | |
| 30 | 32 | $patterns[] = "(?P<{$name}>{$inner})"; |
| 31 | 33 | } |
| 32 | 34 | |
| 33 | if( !in_array( $this->language, ['markdown', 'rmd'] ) ) { | |
| 35 | if( !\in_array( $this->language, ['markdown', 'rmd'] ) ) { | |
| 34 | 36 | $patterns[] = "(?P<punctuation>[\\{\\}\\(\\)\\[\\]\\;\\,\\:])"; |
| 35 | 37 | } |
| 36 | 38 | |
| 37 | 39 | $patterns[] = "(?P<any>[\s\S])"; |
| 38 | $imploded = implode( '|', $patterns ); | |
| 39 | $combined = '~' . $imploded . '~msu'; | |
| 40 | $combined = '~' . \implode( '|', $patterns ) . '~msu'; | |
| 40 | 41 | |
| 41 | $processed = preg_replace_callback( $combined, function( $matches ) { | |
| 42 | $output = htmlspecialchars( $matches[0] ); | |
| 42 | $processed = \preg_replace_callback( | |
| 43 | $combined, | |
| 44 | function( $matches ) { | |
| 45 | $output = \htmlspecialchars( $matches[0] ); | |
| 43 | 46 | |
| 44 | foreach( $matches as $key => $value ) { | |
| 45 | if( !is_numeric( $key ) && $value !== '' ) { | |
| 46 | if( $key === 'any' ) { | |
| 47 | $output = htmlspecialchars( $value ); | |
| 48 | } elseif( $key === 'string_interp' ) { | |
| 49 | $output = $this->renderInterpolatedString( $value ); | |
| 50 | } elseif( $key === 'math' ) { | |
| 51 | $output = $this->renderMath( $value ); | |
| 52 | } else { | |
| 53 | $output = $this->wrap( $value, 'hl-' . $key ); | |
| 54 | } | |
| 47 | foreach( $matches as $key => $value ) { | |
| 48 | if( !\is_numeric( $key ) && $value !== '' ) { | |
| 49 | $output = match( $key ) { | |
| 50 | 'any' => \htmlspecialchars( $value ), | |
| 51 | 'string_interp' => $this->renderInterpolatedString( $value ), | |
| 52 | 'math' => $this->renderMath( $value ), | |
| 53 | default => $this->wrap( $value, 'hl-' . $key ) | |
| 54 | }; | |
| 55 | 55 | |
| 56 | break; | |
| 56 | break; | |
| 57 | } | |
| 57 | 58 | } |
| 58 | } | |
| 59 | 59 | |
| 60 | return $output; | |
| 61 | }, $this->content ); | |
| 60 | return $output; | |
| 61 | }, | |
| 62 | $this->content | |
| 63 | ); | |
| 62 | 64 | |
| 63 | if( is_string( $processed ) ) { | |
| 64 | $result = $processed; | |
| 65 | } | |
| 65 | $result = \is_string( $processed ) ? $processed : $result; | |
| 66 | 66 | } |
| 67 | 67 | |
| 68 | 68 | return $result; |
| 69 | 69 | } |
| 70 | 70 | |
| 71 | 71 | private function renderInterpolatedString( string $content ): string { |
| 72 | 72 | $pattern = '/(\$\{[a-zA-Z0-9_]+\}|\$[a-zA-Z0-9_]+)/'; |
| 73 | 73 | |
| 74 | 74 | return $this->processSegments( $content, $pattern, function( $part ) { |
| 75 | if( !str_starts_with( $part, '$' ) || strlen( $part ) <= 1 ) { | |
| 76 | $out = $this->wrap( $part, 'hl-string' ); | |
| 77 | } else { | |
| 78 | $isComplex = str_starts_with( $part, '${' ) && | |
| 79 | str_ends_with( $part, '}' ); | |
| 75 | $out = $this->wrap( $part, 'hl-string' ); | |
| 80 | 76 | |
| 81 | $inner = $isComplex ? substr( $part, 2, -1 ) : substr( $part, 1 ); | |
| 77 | if( \str_starts_with( $part, '$' ) && \strlen( $part ) > 1 ) { | |
| 78 | $isComplex = \str_starts_with( $part, '${' ) && | |
| 79 | \str_ends_with( $part, '}' ); | |
| 80 | ||
| 81 | $inner = $isComplex | |
| 82 | ? \substr( $part, 2, -1 ) | |
| 83 | : \substr( $part, 1 ); | |
| 82 | 84 | $prefix = $isComplex ? '${' : '$'; |
| 83 | 85 | $suffix = $isComplex |
| ... | ||
| 98 | 100 | |
| 99 | 101 | return $this->processSegments( $content, $pattern, function( $part ) { |
| 100 | $output = $this->wrap( $part, 'hl-math' ); | |
| 101 | ||
| 102 | if( str_starts_with( $part, '`' ) && str_ends_with( $part, '`' ) ) { | |
| 103 | $output = $this->wrap( $part, 'hl-function' ); | |
| 104 | } | |
| 102 | $isFunc = \str_starts_with( $part, '`' ) && | |
| 103 | \str_ends_with( $part, '`' ); | |
| 105 | 104 | |
| 106 | return $output; | |
| 105 | return $this->wrap( $part, $isFunc ? 'hl-function' : 'hl-math' ); | |
| 107 | 106 | } ); |
| 108 | 107 | } |
| 109 | 108 | |
| 110 | 109 | private function processSegments( |
| 111 | 110 | string $content, |
| 112 | 111 | string $pattern, |
| 113 | 112 | callable $callback |
| 114 | 113 | ): string { |
| 115 | $parts = preg_split( $pattern, $content, -1, PREG_SPLIT_DELIM_CAPTURE ); | |
| 116 | 114 | $output = ''; |
| 115 | ||
| 116 | $parts = \preg_split( | |
| 117 | $pattern, | |
| 118 | $content, | |
| 119 | -1, | |
| 120 | PREG_SPLIT_DELIM_CAPTURE | |
| 121 | ); | |
| 117 | 122 | |
| 118 | 123 | foreach( $parts as $part ) { |
| ... | ||
| 130 | 135 | bool $escape = true |
| 131 | 136 | ): string { |
| 132 | $safeContent = $content; | |
| 133 | ||
| 134 | if( $escape ) { | |
| 135 | $safeContent = htmlspecialchars( $content ); | |
| 136 | } | |
| 137 | $safe = $escape ? \htmlspecialchars( $content ) : $content; | |
| 137 | 138 | |
| 138 | return '<span class="' . $className . '">' . $safeContent . '</span>'; | |
| 139 | return '<span class="' . $className . '">' . $safe . '</span>'; | |
| 139 | 140 | } |
| 140 | 141 | |
| 141 | 142 | private function detectLanguage( |
| 142 | 143 | string $mediaType, |
| 143 | 144 | string $filename |
| 144 | 145 | ): string { |
| 145 | $basename = basename( $filename ); | |
| 146 | $extension = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) ); | |
| 147 | $language = match( $basename ) { | |
| 148 | 'Containerfile', | |
| 149 | 'Dockerfile' => 'containerfile', | |
| 150 | 'Makefile' => 'makefile', | |
| 151 | 'Jenkinsfile' => 'groovy', | |
| 152 | default => '' | |
| 153 | }; | |
| 154 | ||
| 155 | if( $language === '' ) { | |
| 156 | $language = match( $extension ) { | |
| 157 | 'php', 'phtml', 'php8', 'php7' => 'php', | |
| 158 | 'c', 'h' => 'c', | |
| 159 | 'cpp', 'hpp', 'cc', 'cxx' => 'cpp', | |
| 160 | 'cs', 'csx' => 'csharp', | |
| 161 | 'java' => 'java', | |
| 162 | 'kt', 'kts' => 'kotlin', | |
| 163 | 'scala', 'sc' => 'scala', | |
| 164 | 'groovy', 'gvy' => 'groovy', | |
| 165 | 'js', 'jsx', 'mjs' => 'javascript', | |
| 166 | 'ts', 'tsx' => 'typescript', | |
| 167 | 'dart' => 'dart', | |
| 168 | 'swift' => 'swift', | |
| 169 | 'go' => 'go', | |
| 170 | 'rs' => 'rust', | |
| 171 | 'py', 'pyw' => 'python', | |
| 172 | 'rb', 'erb' => 'ruby', | |
| 173 | 'pl', 'pm', 't' => 'perl', | |
| 174 | 'lua' => 'lua', | |
| 175 | 'sh', 'bash', 'zsh' => 'bash', | |
| 176 | 'ps1', 'psm1', 'psd1' => 'powershell', | |
| 177 | 'bat', 'cmd' => 'batch', | |
| 178 | 'md', 'markdown' => 'markdown', | |
| 179 | 'rmd' => 'rmd', | |
| 180 | 'r' => 'r', | |
| 181 | 'xml', 'svg' => 'xml', | |
| 182 | 'xsl', 'xslt' => 'xslt', | |
| 183 | 'html', 'htm' => 'html', | |
| 184 | 'css' => 'css', | |
| 185 | 'json', 'lock' => 'json', | |
| 186 | 'sql' => 'sql', | |
| 187 | 'yaml', 'yml' => 'yaml', | |
| 188 | 'gradle' => 'gradle', | |
| 189 | 'tex', 'sty', 'cls', 'ltx' => 'tex', | |
| 190 | 'properties', 'prop' => 'properties', | |
| 191 | 'ini', 'cfg', 'conf' => 'ini', | |
| 192 | 'toml' => 'toml', | |
| 193 | 'mk', 'mak' => 'makefile', | |
| 194 | 'diff', 'patch' => 'diff', | |
| 195 | 'for', 'f', 'f90', 'f95' => 'fortran', | |
| 196 | default => '' | |
| 197 | }; | |
| 198 | } | |
| 146 | $basename = \basename( $filename ); | |
| 147 | $extension = \strtolower( | |
| 148 | \pathinfo( $filename, PATHINFO_EXTENSION ) | |
| 149 | ); | |
| 199 | 150 | |
| 200 | if( $language === '' ) { | |
| 201 | $language = match( $mediaType ) { | |
| 202 | 'text/x-php', 'application/x-php', | |
| 203 | 'application/x-httpd-php' => 'php', | |
| 204 | 'text/html' => 'html', | |
| 205 | 'text/css' => 'css', | |
| 206 | 'application/javascript', | |
| 207 | 'text/javascript', | |
| 208 | 'text/x-javascript' => 'javascript', | |
| 209 | 'application/json', 'text/json', | |
| 210 | 'application/x-json' => 'json', | |
| 211 | 'application/xml', 'text/xml', | |
| 212 | 'image/svg+xml' => 'xml', | |
| 213 | 'application/xslt+xml' => 'xslt', | |
| 214 | 'text/x-shellscript', | |
| 215 | 'application/x-sh' => 'bash', | |
| 216 | 'text/x-c', 'text/x-csrc' => 'c', | |
| 217 | 'text/x-c++src', 'text/x-c++', | |
| 218 | 'text/x-cpp' => 'cpp', | |
| 219 | 'text/x-csharp' => 'csharp', | |
| 220 | 'text/x-java', | |
| 221 | 'text/x-java-source', | |
| 222 | 'application/java-archive' => 'java', | |
| 223 | 'text/x-kotlin' => 'kotlin', | |
| 224 | 'text/x-scala' => 'scala', | |
| 225 | 'text/x-swift' => 'swift', | |
| 226 | 'text/x-python', | |
| 227 | 'application/x-python-code' => 'python', | |
| 228 | 'text/x-ruby', 'application/x-ruby' => 'ruby', | |
| 229 | 'text/x-perl', 'application/x-perl' => 'perl', | |
| 230 | 'text/x-go', 'text/go' => 'go', | |
| 231 | 'text/rust', 'text/x-rust' => 'rust', | |
| 232 | 'text/x-lua', 'text/lua' => 'lua', | |
| 233 | 'text/markdown', | |
| 234 | 'text/x-markdown' => 'markdown', | |
| 235 | 'text/x-r', 'text/x-r-source', | |
| 236 | 'application/R' => 'r', | |
| 237 | 'application/sql', 'text/sql', | |
| 238 | 'text/x-sql' => 'sql', | |
| 239 | 'text/yaml', 'text/x-yaml', | |
| 240 | 'application/yaml' => 'yaml', | |
| 241 | 'application/typescript', | |
| 242 | 'text/typescript' => 'typescript', | |
| 243 | 'text/x-gradle' => 'gradle', | |
| 244 | 'text/x-tex', 'application/x-tex' => 'tex', | |
| 245 | 'text/x-java-properties', | |
| 246 | 'text/properties' => 'properties', | |
| 247 | 'text/ini', 'application/x-ini' => 'ini', | |
| 248 | 'application/toml', 'text/toml' => 'toml', | |
| 249 | 'text/x-diff', 'text/x-patch' => 'diff', | |
| 250 | 'text/x-fortran' => 'fortran', | |
| 251 | default => 'text' | |
| 252 | }; | |
| 253 | } | |
| 151 | $key = match( true ) { | |
| 152 | \in_array( $basename, [ | |
| 153 | 'Containerfile', 'Dockerfile', 'Makefile', 'Jenkinsfile' | |
| 154 | ], true ) => $basename, | |
| 155 | $extension !== '' => $extension, | |
| 156 | default => $mediaType | |
| 157 | }; | |
| 254 | 158 | |
| 255 | return $language; | |
| 159 | return match( $key ) { | |
| 160 | 'Containerfile', 'Dockerfile' => 'containerfile', | |
| 161 | 'Makefile', 'mk', 'mak' => 'makefile', | |
| 162 | 'Jenkinsfile', 'groovy', | |
| 163 | 'gvy' => 'groovy', | |
| 164 | 'php', 'phtml', 'php8', | |
| 165 | 'php7', 'text/x-php', | |
| 166 | 'application/x-php', | |
| 167 | 'application/x-httpd-php' => 'php', | |
| 168 | 'c', 'h', 'text/x-c', | |
| 169 | 'text/x-csrc' => 'c', | |
| 170 | 'cpp', 'hpp', 'cc', 'cxx', | |
| 171 | 'text/x-c++src', | |
| 172 | 'text/x-c++', 'text/x-cpp' => 'cpp', | |
| 173 | 'cs', 'csx', 'text/x-csharp' => 'csharp', | |
| 174 | 'java', 'text/x-java', | |
| 175 | 'text/x-java-source' => 'java', | |
| 176 | 'kt', 'kts', 'text/x-kotlin' => 'kotlin', | |
| 177 | 'scala', 'sc', 'text/x-scala' => 'scala', | |
| 178 | 'js', 'jsx', 'mjs', | |
| 179 | 'application/javascript', | |
| 180 | 'text/javascript', | |
| 181 | 'text/x-javascript' => 'javascript', | |
| 182 | 'ts', 'tsx', | |
| 183 | 'application/typescript', | |
| 184 | 'text/typescript' => 'typescript', | |
| 185 | 'dart' => 'dart', | |
| 186 | 'swift', 'text/x-swift' => 'swift', | |
| 187 | 'go', 'text/x-go', 'text/go' => 'go', | |
| 188 | 'rs', 'text/rust', | |
| 189 | 'text/x-rust' => 'rust', | |
| 190 | 'py', 'pyw', 'text/x-python', | |
| 191 | 'application/x-python-code' => 'python', | |
| 192 | 'rb', 'erb', 'text/x-ruby', | |
| 193 | 'application/x-ruby' => 'ruby', | |
| 194 | 'pl', 'pm', 't', | |
| 195 | 'text/x-perl', | |
| 196 | 'application/x-perl' => 'perl', | |
| 197 | 'lua', 'text/x-lua', | |
| 198 | 'text/lua' => 'lua', | |
| 199 | 'sh', 'bash', 'zsh', | |
| 200 | 'text/x-shellscript', | |
| 201 | 'application/x-sh' => 'bash', | |
| 202 | 'ps1', 'psm1', 'psd1' => 'powershell', | |
| 203 | 'bat', 'cmd' => 'batch', | |
| 204 | 'md', 'markdown', | |
| 205 | 'text/markdown', | |
| 206 | 'text/x-markdown' => 'markdown', | |
| 207 | 'rmd' => 'rmd', | |
| 208 | 'r', 'text/x-r', | |
| 209 | 'text/x-r-source', | |
| 210 | 'application/R' => 'r', | |
| 211 | 'xml', 'svg', | |
| 212 | 'application/xml', | |
| 213 | 'text/xml', 'image/svg+xml' => 'xml', | |
| 214 | 'xsl', 'xslt', | |
| 215 | 'application/xslt+xml' => 'xslt', | |
| 216 | 'html', 'htm', 'text/html' => 'html', | |
| 217 | 'css', 'text/css' => 'css', | |
| 218 | 'json', 'lock', | |
| 219 | 'application/json', | |
| 220 | 'text/json', | |
| 221 | 'application/x-json' => 'json', | |
| 222 | 'sql', 'application/sql', | |
| 223 | 'text/sql', 'text/x-sql' => 'sql', | |
| 224 | 'yaml', 'yml', 'text/yaml', | |
| 225 | 'text/x-yaml', | |
| 226 | 'application/yaml' => 'yaml', | |
| 227 | 'gradle', 'text/x-gradle' => 'gradle', | |
| 228 | 'tex', 'sty', 'cls', 'ltx', | |
| 229 | 'text/x-tex', | |
| 230 | 'application/x-tex' => 'tex', | |
| 231 | 'properties', 'prop', | |
| 232 | 'text/x-java-properties', | |
| 233 | 'text/properties' => 'properties', | |
| 234 | 'ini', 'cfg', 'conf', | |
| 235 | 'text/ini', | |
| 236 | 'application/x-ini' => 'ini', | |
| 237 | 'toml', 'application/toml', | |
| 238 | 'text/toml' => 'toml', | |
| 239 | 'diff', 'patch', | |
| 240 | 'text/x-diff', 'text/x-patch' => 'diff', | |
| 241 | 'for', 'f', 'f90', 'f95', | |
| 242 | 'text/x-fortran' => 'fortran', | |
| 243 | default => 'text' | |
| 244 | }; | |
| 256 | 245 | } |
| 257 | 246 | } |
| 1 | 1 | <?php |
| 2 | 2 | require_once __DIR__ . '/CommitRenderer.php'; |
| 3 | require_once __DIR__ . '/Renderer.php'; | |
| 3 | 4 | require_once __DIR__ . '/../model/UrlBuilder.php'; |
| 4 | 5 | |
| 5 | class HtmlCommitRenderer implements CommitRenderer { | |
| 6 | class HtmlCommitRenderer extends Renderer implements CommitRenderer { | |
| 6 | 7 | private string $repoSafeName; |
| 7 | 8 | |
| ... | ||
| 30 | 31 | ' • ' . date( 'Y-m-d', $date ) . '</span>'; |
| 31 | 32 | echo '</div>'; |
| 32 | } | |
| 33 | ||
| 34 | public function renderTime( int $timestamp ): void { | |
| 35 | $tokens = [ | |
| 36 | 31536000 => 'year', 2592000 => 'month', 604800 => 'week', | |
| 37 | 86400 => 'day', 3600 => 'hour', 60 => 'minute', 1 => 'second' | |
| 38 | ]; | |
| 39 | $diff = $timestamp ? time() - $timestamp : null; | |
| 40 | $result = 'never'; | |
| 41 | ||
| 42 | if( $diff && $diff >= 5 ) { | |
| 43 | foreach( $tokens as $unit => $text ) { | |
| 44 | if( $diff < $unit ) continue; | |
| 45 | $num = floor( $diff / $unit ); | |
| 46 | $result = $num . ' ' . $text . ($num > 1 ? 's' : '') . ' ago'; | |
| 47 | break; | |
| 48 | } | |
| 49 | } elseif( $diff ) { | |
| 50 | $result = 'just now'; | |
| 51 | } | |
| 52 | ||
| 53 | echo $result; | |
| 54 | 33 | } |
| 55 | 34 | } |
| 2 | 2 | require_once __DIR__ . '/FileRenderer.php'; |
| 3 | 3 | require_once __DIR__ . '/Highlighter.php'; |
| 4 | require_once __DIR__ . '/Renderer.php'; | |
| 4 | 5 | require_once __DIR__ . '/../model/UrlBuilder.php'; |
| 5 | 6 | |
| 6 | class HtmlFileRenderer implements FileRenderer { | |
| 7 | class HtmlFileRenderer extends Renderer implements FileRenderer { | |
| 7 | 8 | private string $repoSafeName; |
| 8 | 9 | private string $currentPath; |
| 9 | 10 | private string $currentRef; |
| 10 | 11 | |
| 11 | public function __construct( string $repoSafeName, string $currentPath = '', string $currentRef = 'HEAD' ) { | |
| 12 | public function __construct( | |
| 13 | string $repoSafeName, | |
| 14 | string $currentPath = '', | |
| 15 | string $currentRef = 'HEAD' | |
| 16 | ) { | |
| 12 | 17 | $this->repoSafeName = $repoSafeName; |
| 13 | $this->currentPath = trim( $currentPath, '/' ); | |
| 14 | $this->currentRef = $currentRef; | |
| 18 | $this->currentPath = trim( $currentPath, '/' ); | |
| 19 | $this->currentRef = $currentRef; | |
| 15 | 20 | } |
| 16 | 21 | |
| 17 | 22 | public function renderListEntry( |
| 18 | 23 | string $name, |
| 19 | 24 | string $sha, |
| 20 | 25 | string $mode, |
| 21 | 26 | string $iconClass, |
| 22 | int $timestamp, | |
| 23 | int $size | |
| 27 | int $timestamp, | |
| 28 | int $size | |
| 24 | 29 | ): void { |
| 25 | $fullPath = ($this->currentPath === '' ? '' : $this->currentPath . '/') . $name; | |
| 30 | $fullPath = ($this->currentPath === '' ? '' : $this->currentPath . '/') . | |
| 31 | $name; | |
| 26 | 32 | |
| 27 | $isDir = ($mode === '40000' || $mode === '040000'); | |
| 33 | $isDir = $mode === '40000' || $mode === '040000'; | |
| 28 | 34 | $action = $isDir ? 'tree' : 'blob'; |
| 29 | 35 | |
| ... | ||
| 36 | 42 | |
| 37 | 43 | echo '<tr>'; |
| 38 | echo '<td class="file-icon-cell"><i class="fas ' . $iconClass . '"></i></td>'; | |
| 39 | echo '<td class="file-name-cell"><a href="' . $url . '">' . htmlspecialchars( $name ) . '</a></td>'; | |
| 44 | echo '<td class="file-icon-cell"><i class="fas ' . $iconClass . | |
| 45 | '"></i></td>'; | |
| 46 | echo '<td class="file-name-cell"><a href="' . $url . '">' . | |
| 47 | htmlspecialchars( $name ) . '</a></td>'; | |
| 40 | 48 | echo '<td class="file-mode-cell">' . $this->formatMode( $mode ) . '</td>'; |
| 41 | echo '<td class="file-size-cell">' . ($size > 0 ? $this->formatSize( $size ) : '') . '</td>'; | |
| 49 | echo '<td class="file-size-cell">' . | |
| 50 | ($size > 0 ? $this->formatSize( $size ) : '') . '</td>'; | |
| 42 | 51 | echo '</tr>'; |
| 43 | 52 | } |
| 44 | 53 | |
| 45 | public function renderMedia( File $file, string $url, string $mediaType ): bool { | |
| 54 | public function renderMedia( | |
| 55 | File $file, | |
| 56 | string $url, | |
| 57 | string $mediaType | |
| 58 | ): bool { | |
| 59 | $result = false; | |
| 60 | ||
| 46 | 61 | if( $file->isImage() ) { |
| 47 | echo '<div class="blob-content blob-content-image"><img src="' . $url . '"></div>'; | |
| 48 | return true; | |
| 62 | echo '<div class="blob-content blob-content-image">' . | |
| 63 | '<img src="' . $url . '"></div>'; | |
| 64 | $result = true; | |
| 49 | 65 | } elseif( $file->isVideo() ) { |
| 50 | echo '<div class="blob-content blob-content-video"><video controls><source src="' . $url . '" type="' . $mediaType . '"></video></div>'; | |
| 51 | return true; | |
| 66 | echo '<div class="blob-content blob-content-video">' . | |
| 67 | '<video controls><source src="' . $url . '" type="' . | |
| 68 | $mediaType . '"></video></div>'; | |
| 69 | $result = true; | |
| 52 | 70 | } elseif( $file->isAudio() ) { |
| 53 | echo '<div class="blob-content blob-content-audio"><audio controls><source src="' . $url . '" type="' . $mediaType . '"></audio></div>'; | |
| 54 | return true; | |
| 71 | echo '<div class="blob-content blob-content-audio">' . | |
| 72 | '<audio controls><source src="' . $url . '" type="' . | |
| 73 | $mediaType . '"></audio></div>'; | |
| 74 | $result = true; | |
| 55 | 75 | } |
| 56 | return false; | |
| 76 | ||
| 77 | return $result; | |
| 57 | 78 | } |
| 58 | 79 | |
| 59 | 80 | public function renderSize( int $bytes ): void { |
| 60 | 81 | echo $this->formatSize( $bytes ); |
| 61 | } | |
| 62 | ||
| 63 | public function highlight( string $filename, string $content, string $mediaType ): string { | |
| 64 | return (new Highlighter($filename, $content, $mediaType))->render(); | |
| 65 | 82 | } |
| 66 | ||
| 67 | public function renderTime( int $timestamp ): void { | |
| 68 | $tokens = [ | |
| 69 | 31536000 => 'year', 2592000 => 'month', 604800 => 'week', | |
| 70 | 86400 => 'day', 3600 => 'hour', 60 => 'minute', 1 => 'second' | |
| 71 | ]; | |
| 72 | $diff = $timestamp ? time() - $timestamp : null; | |
| 73 | $result = 'never'; | |
| 74 | ||
| 75 | if( $diff && $diff >= 5 ) { | |
| 76 | foreach( $tokens as $unit => $text ) { | |
| 77 | if( $diff < $unit ) continue; | |
| 78 | $num = floor( $diff / $unit ); | |
| 79 | $result = $num . ' ' . $text . ($num > 1 ? 's' : '') . ' ago'; | |
| 80 | break; | |
| 81 | } | |
| 82 | } elseif( $diff ) { | |
| 83 | $result = 'just now'; | |
| 84 | } | |
| 85 | 83 | |
| 86 | echo $result; | |
| 84 | public function highlight( | |
| 85 | string $filename, | |
| 86 | string $content, | |
| 87 | string $mediaType | |
| 88 | ): string { | |
| 89 | return (new Highlighter( $filename, $content, $mediaType ))->render(); | |
| 87 | 90 | } |
| 88 | 91 | |
| 89 | 92 | private function formatSize( int $bytes ): string { |
| 90 | $units = [ 'B', 'KB', 'MB', 'GB', 'TB' ]; | |
| 91 | $i = 0; | |
| 93 | $units = ['B', 'KB', 'MB', 'GB', 'TB']; | |
| 94 | $i = 0; | |
| 92 | 95 | |
| 93 | 96 | while( $bytes >= 1024 && $i < count( $units ) - 1 ) { |
| 94 | $bytes /= 1024; $i++; | |
| 97 | $bytes /= 1024; | |
| 98 | $i++; | |
| 95 | 99 | } |
| 96 | 100 | |
| 97 | 101 | return ($bytes === 0 ? 0 : round( $bytes )) . ' ' . $units[$i]; |
| 98 | 102 | } |
| 99 | 103 | |
| 100 | 104 | private function formatMode( string $mode ): string { |
| 101 | switch( $mode ) { | |
| 102 | case '100644': return 'w'; | |
| 103 | case '100755': return 'x'; | |
| 104 | case '040000': return 'd'; | |
| 105 | case '120000': return 'l'; | |
| 106 | case '160000': return 'm'; | |
| 107 | default: return '?'; | |
| 108 | } | |
| 105 | return match( $mode ) { | |
| 106 | '100644' => 'w', | |
| 107 | '100755' => 'x', | |
| 108 | '40000' => 'd', | |
| 109 | '040000' => 'd', | |
| 110 | '120000' => 'l', | |
| 111 | '160000' => 'm', | |
| 112 | default => '?' | |
| 113 | }; | |
| 109 | 114 | } |
| 110 | 115 | } |
| 1 | 1 | <?php |
| 2 | 2 | require_once __DIR__ . '/TagRenderer.php'; |
| 3 | require_once __DIR__ . '/Renderer.php'; | |
| 3 | 4 | require_once __DIR__ . '/../model/UrlBuilder.php'; |
| 4 | 5 | |
| 5 | class HtmlTagRenderer implements TagRenderer { | |
| 6 | class HtmlTagRenderer extends Renderer implements TagRenderer { | |
| 6 | 7 | private string $repoSafeName; |
| 7 | 8 | |
| 8 | 9 | public function __construct( string $repoSafeName ) { |
| 9 | 10 | $this->repoSafeName = $repoSafeName; |
| 10 | 11 | } |
| 11 | 12 | |
| 12 | public function renderTagItem( | |
| 13 | public function render( | |
| 13 | 14 | string $name, |
| 14 | 15 | string $sha, |
| 15 | 16 | string $targetSha, |
| 16 | ?string $prevTargetSha, | |
| 17 | int $timestamp, | |
| 17 | string $prevTargetSha, | |
| 18 | int $timestamp, | |
| 18 | 19 | string $message, |
| 19 | 20 | string $author |
| ... | ||
| 31 | 32 | ->build(); |
| 32 | 33 | |
| 33 | if( $prevTargetSha ) { | |
| 34 | $diffUrl = (new UrlBuilder()) | |
| 34 | $diffUrl = $prevTargetSha !== '' | |
| 35 | ? (new UrlBuilder()) | |
| 35 | 36 | ->withRepo( $this->repoSafeName ) |
| 36 | 37 | ->withAction( 'compare' ) |
| 37 | 38 | ->withHash( $targetSha ) |
| 38 | 39 | ->withName( $prevTargetSha ) |
| 39 | ->build(); | |
| 40 | } else { | |
| 41 | $diffUrl = $commitUrl; | |
| 42 | } | |
| 40 | ->build() | |
| 41 | : $commitUrl; | |
| 43 | 42 | |
| 44 | 43 | echo '<tr>'; |
| 45 | 44 | echo '<td class="tag-name">'; |
| 46 | 45 | echo '<a href="' . $filesUrl . '"><i class="fas fa-tag"></i> ' . |
| 47 | 46 | htmlspecialchars( $name ) . '</a>'; |
| 48 | 47 | echo '</td>'; |
| 49 | 48 | echo '<td class="tag-message">'; |
| 50 | 49 | |
| 51 | echo ($message !== '') ? htmlspecialchars( strtok( $message, "\n" ) ) : | |
| 52 | '<span style="color: #484f58; font-style: italic;">No description</span>'; | |
| 50 | echo $message !== '' | |
| 51 | ? htmlspecialchars( strtok( $message, "\n" ) ) | |
| 52 | : '<span style="color: #484f58; font-style: italic;">' . | |
| 53 | 'No description</span>'; | |
| 53 | 54 | |
| 54 | 55 | echo '</td>'; |
| 55 | echo '<td class="tag-author">' . htmlspecialchars( $author ) . '</td>'; | |
| 56 | echo '<td class="tag-author">' . | |
| 57 | htmlspecialchars( $author ) . '</td>'; | |
| 56 | 58 | echo '<td class="tag-time">'; |
| 59 | ||
| 57 | 60 | $this->renderTime( $timestamp ); |
| 61 | ||
| 58 | 62 | echo '</td>'; |
| 59 | 63 | echo '<td class="tag-hash">'; |
| 60 | 64 | echo '<a href="' . $diffUrl . '" class="commit-hash">' . |
| 61 | 65 | substr( $sha, 0, 7 ) . '</a>'; |
| 62 | 66 | echo '</td>'; |
| 63 | 67 | echo '</tr>'; |
| 64 | } | |
| 65 | ||
| 66 | public function renderTime( int $timestamp ): void { | |
| 67 | if( !$timestamp ) { | |
| 68 | echo 'never'; | |
| 69 | return; | |
| 70 | } | |
| 71 | ||
| 72 | $diff = time() - $timestamp; | |
| 73 | ||
| 74 | if( $diff < 5 ) { | |
| 75 | echo 'just now'; | |
| 76 | return; | |
| 77 | } | |
| 78 | ||
| 79 | $tokens = [ | |
| 80 | 31536000 => 'year', | |
| 81 | 2592000 => 'month', | |
| 82 | 604800 => 'week', | |
| 83 | 86400 => 'day', | |
| 84 | 3600 => 'hour', | |
| 85 | 60 => 'minute', | |
| 86 | 1 => 'second' | |
| 87 | ]; | |
| 88 | ||
| 89 | foreach( $tokens as $unit => $text ) { | |
| 90 | if( $diff < $unit ) { | |
| 91 | continue; | |
| 92 | } | |
| 93 | ||
| 94 | $num = floor( $diff / $unit ); | |
| 95 | ||
| 96 | echo $num . ' ' . $text . ($num > 1 ? 's' : '') . ' ago'; | |
| 97 | return; | |
| 98 | } | |
| 99 | 68 | } |
| 100 | 69 | } |
| 1 | <?php | |
| 2 | abstract class Renderer { | |
| 3 | public function renderTime( int $timestamp ): void { | |
| 4 | $result = 'never'; | |
| 5 | ||
| 6 | if( $timestamp !== 0 ) { | |
| 7 | $diff = time() - $timestamp; | |
| 8 | ||
| 9 | if( $diff < 5 ) { | |
| 10 | $result = 'just now'; | |
| 11 | } else { | |
| 12 | $tokens = [ | |
| 13 | 31536000 => 'year', | |
| 14 | 2592000 => 'month', | |
| 15 | 604800 => 'week', | |
| 16 | 86400 => 'day', | |
| 17 | 3600 => 'hour', | |
| 18 | 60 => 'minute', | |
| 19 | 1 => 'second' | |
| 20 | ]; | |
| 21 | ||
| 22 | foreach( $tokens as $unit => $text ) { | |
| 23 | if( $diff >= $unit ) { | |
| 24 | $num = floor( $diff / $unit ); | |
| 25 | $result = $num . ' ' . $text . ($num > 1 ? 's' : '') . ' ago'; | |
| 26 | ||
| 27 | break; | |
| 28 | } | |
| 29 | } | |
| 30 | } | |
| 31 | } | |
| 32 | ||
| 33 | echo $result; | |
| 34 | } | |
| 35 | } | |
| 1 | 36 |
| 1 | 1 | <?php |
| 2 | 2 | interface TagRenderer { |
| 3 | public function renderTagItem( | |
| 3 | public function render( | |
| 4 | 4 | string $name, |
| 5 | 5 | string $sha, |
| 6 | 6 | string $targetSha, |
| 7 | 7 | string $prevTargetSha, |
| 8 | int $timestamp, | |
| 8 | int $timestamp, | |
| 9 | 9 | string $message, |
| 10 | 10 | string $author |