| 2 | 2 | require_once __DIR__ . '/StreamReader.php'; |
| 3 | 3 | |
| 4 | class CompressionStream { | |
| 5 | private Closure $pumper; | |
| 6 | private Closure $finisher; | |
| 7 | private Closure $status; | |
| 8 | ||
| 9 | private function __construct( | |
| 10 | Closure $pumper, | |
| 11 | Closure $finisher, | |
| 12 | Closure $status | |
| 13 | ) { | |
| 14 | $this->pumper = $pumper; | |
| 15 | $this->finisher = $finisher; | |
| 16 | $this->status = $status; | |
| 17 | } | |
| 4 | interface CompressionStream { | |
| 5 | public function stream( | |
| 6 | StreamReader $stream, | |
| 7 | int $chunkSize = 8192 | |
| 8 | ): Generator; | |
| 9 | } | |
| 18 | 10 | |
| 19 | public static function createExtractor(): self { | |
| 11 | class ZlibExtractorStream implements CompressionStream { | |
| 12 | public function stream( | |
| 13 | StreamReader $stream, | |
| 14 | int $chunkSize = 8192 | |
| 15 | ): Generator { | |
| 20 | 16 | $context = \inflate_init( \ZLIB_ENCODING_DEFLATE ); |
| 17 | $done = false; | |
| 21 | 18 | |
| 22 | return new self( | |
| 23 | function( string $chunk ) use ( $context ): string { | |
| 19 | while( !$done && !$stream->eof() ) { | |
| 20 | $chunk = $stream->read( $chunkSize ); | |
| 21 | $done = $chunk === ''; | |
| 22 | ||
| 23 | if( !$done ) { | |
| 24 | 24 | $before = \inflate_get_read_len( $context ); |
| 25 | 25 | @\inflate_add( $context, $chunk ); |
| 26 | 26 | |
| 27 | return \substr( | |
| 27 | $data = \substr( | |
| 28 | 28 | $chunk, |
| 29 | 29 | 0, |
| 30 | 30 | \inflate_get_read_len( $context ) - $before |
| 31 | 31 | ); |
| 32 | }, | |
| 33 | function(): string { | |
| 34 | return ''; | |
| 35 | }, | |
| 36 | function() use ( $context ): bool { | |
| 37 | return \inflate_get_status( $context ) === \ZLIB_STREAM_END; | |
| 38 | } | |
| 39 | ); | |
| 40 | } | |
| 41 | ||
| 42 | public static function createInflater(): self { | |
| 43 | $context = \inflate_init( \ZLIB_ENCODING_DEFLATE ); | |
| 44 | ||
| 45 | return new self( | |
| 46 | function( string $chunk ) use ( $context ): string { | |
| 47 | $data = @\inflate_add( $context, $chunk ); | |
| 48 | ||
| 49 | return $data === false ? '' : $data; | |
| 50 | }, | |
| 51 | function(): string { | |
| 52 | return ''; | |
| 53 | }, | |
| 54 | function() use ( $context ): bool { | |
| 55 | return \inflate_get_status( $context ) === \ZLIB_STREAM_END; | |
| 56 | } | |
| 57 | ); | |
| 58 | } | |
| 59 | ||
| 60 | public static function createDeflater(): self { | |
| 61 | $context = \deflate_init( \ZLIB_ENCODING_DEFLATE ); | |
| 62 | ||
| 63 | return new self( | |
| 64 | function( string $chunk ) use ( $context ): string { | |
| 65 | $data = \deflate_add( $context, $chunk, \ZLIB_NO_FLUSH ); | |
| 66 | 32 | |
| 67 | return $data === false ? '' : $data; | |
| 68 | }, | |
| 69 | function() use ( $context ): string { | |
| 70 | $data = \deflate_add( $context, '', \ZLIB_FINISH ); | |
| 33 | if( $data !== '' ) { | |
| 34 | yield $data; | |
| 35 | } | |
| 71 | 36 | |
| 72 | return $data === false ? '' : $data; | |
| 73 | }, | |
| 74 | function(): bool { | |
| 75 | return false; | |
| 37 | $done = \inflate_get_status( $context ) === \ZLIB_STREAM_END; | |
| 76 | 38 | } |
| 77 | ); | |
| 39 | } | |
| 78 | 40 | } |
| 41 | } | |
| 79 | 42 | |
| 43 | class ZlibInflaterStream implements CompressionStream { | |
| 80 | 44 | public function stream( |
| 81 | 45 | StreamReader $stream, |
| 82 | 46 | int $chunkSize = 8192 |
| 83 | 47 | ): Generator { |
| 84 | $done = false; | |
| 48 | $context = \inflate_init( \ZLIB_ENCODING_DEFLATE ); | |
| 49 | $done = false; | |
| 85 | 50 | |
| 86 | 51 | while( !$done && !$stream->eof() ) { |
| 87 | 52 | $chunk = $stream->read( $chunkSize ); |
| 88 | 53 | $done = $chunk === ''; |
| 89 | 54 | |
| 90 | 55 | if( !$done ) { |
| 91 | $data = $this->pump( $chunk ); | |
| 56 | $data = @\inflate_add( $context, $chunk ); | |
| 92 | 57 | |
| 93 | if( $data !== '' ) { | |
| 58 | if( $data !== false && $data !== '' ) { | |
| 94 | 59 | yield $data; |
| 95 | 60 | } |
| 96 | 61 | |
| 97 | $done = $this->finished(); | |
| 62 | $done = \inflate_get_status( $context ) === \ZLIB_STREAM_END; | |
| 98 | 63 | } |
| 99 | 64 | } |
| 100 | } | |
| 101 | ||
| 102 | public function pump( string $chunk ): string { | |
| 103 | return $chunk === '' ? '' : ($this->pumper)( $chunk ); | |
| 104 | } | |
| 105 | ||
| 106 | public function finish(): string { | |
| 107 | return ($this->finisher)(); | |
| 108 | } | |
| 109 | ||
| 110 | public function finished(): bool { | |
| 111 | return ($this->status)(); | |
| 112 | 65 | } |
| 113 | 66 | } |
| 17 | 17 | ]; |
| 18 | 18 | |
| 19 | public function apply( string $base, string $delta, int $cap ): string { | |
| 19 | public function apply( | |
| 20 | string $base, | |
| 21 | string $delta, | |
| 22 | int $cap | |
| 23 | ): string { | |
| 20 | 24 | $pos = 0; |
| 25 | ||
| 21 | 26 | $this->readDeltaSize( $delta, $pos ); |
| 22 | 27 | $this->readDeltaSize( $delta, $pos ); |
| 23 | 28 | |
| 24 | 29 | $chunks = []; |
| 25 | $len = strlen( $delta ); | |
| 30 | $len = \strlen( $delta ); | |
| 26 | 31 | $outLen = 0; |
| 27 | ||
| 28 | while( $pos < $len ) { | |
| 29 | if( $cap > 0 && $outLen >= $cap ) break; | |
| 30 | 32 | |
| 31 | $op = ord( $delta[$pos++] ); | |
| 33 | while( $pos < $len && ( $cap === 0 || $outLen < $cap ) ) { | |
| 34 | $op = \ord( $delta[$pos++] ); | |
| 32 | 35 | |
| 33 | 36 | if( $op & 128 ) { |
| 34 | $off = $ln = 0; | |
| 37 | $off = 0; | |
| 38 | $ln = 0; | |
| 35 | 39 | |
| 36 | $this->parseCopyInstruction( $op, $delta, $pos, $off, $ln ); | |
| 40 | $this->parseCopyInstruction( | |
| 41 | $op, $delta, $pos, $off, $ln | |
| 42 | ); | |
| 37 | 43 | |
| 38 | $chunks[] = substr( $base, $off, $ln ); | |
| 44 | $chunks[] = \substr( $base, $off, $ln ); | |
| 39 | 45 | $outLen += $ln; |
| 40 | 46 | } else { |
| 41 | 47 | $ln = $op & 127; |
| 42 | $chunks[] = substr( $delta, $pos, $ln ); | |
| 48 | $chunks[] = \substr( $delta, $pos, $ln ); | |
| 43 | 49 | $outLen += $ln; |
| 44 | 50 | $pos += $ln; |
| 45 | 51 | } |
| 46 | 52 | } |
| 47 | 53 | |
| 48 | $result = implode( '', $chunks ); | |
| 54 | $result = \implode( '', $chunks ); | |
| 49 | 55 | |
| 50 | return $cap > 0 && strlen( $result ) > $cap | |
| 51 | ? substr( $result, 0, $cap ) | |
| 56 | return $cap > 0 && \strlen( $result ) > $cap | |
| 57 | ? \substr( $result, 0, $cap ) | |
| 52 | 58 | : $result; |
| 53 | 59 | } |
| 54 | 60 | |
| 55 | 61 | public function applyStreamGenerator( |
| 56 | 62 | StreamReader $handle, |
| 57 | 63 | mixed $base |
| 58 | 64 | ): Generator { |
| 59 | $stream = CompressionStream::createInflater(); | |
| 65 | $stream = new ZlibInflaterStream(); | |
| 60 | 66 | $state = 0; |
| 61 | 67 | $buffer = ''; |
| ... | ||
| 77 | 83 | |
| 78 | 84 | while( $pos < $bufLen ) { |
| 79 | if( !(ord( $buffer[$pos] ) & 128) ) { | |
| 85 | if( !(\ord( $buffer[$pos] ) & 128) ) { | |
| 80 | 86 | $found = true; |
| 81 | 87 | $pos++; |
| ... | ||
| 93 | 99 | } |
| 94 | 100 | } else { |
| 95 | $op = ord( $buffer[$offset] ); | |
| 101 | $op = \ord( $buffer[$offset] ); | |
| 96 | 102 | |
| 97 | 103 | if( $op & 128 ) { |
| 98 | 104 | $need = self::COPY_INSTRUCTION_SIZES[$op & 0x7F]; |
| 99 | 105 | |
| 100 | 106 | if( $len < 1 + $need ) { |
| 101 | 107 | break; |
| 102 | 108 | } |
| 103 | 109 | |
| 104 | $off = $ln = 0; | |
| 110 | $off = 0; | |
| 111 | $ln = 0; | |
| 105 | 112 | $ptr = $offset + 1; |
| 106 | 113 | |
| 107 | $this->parseCopyInstruction( $op, $buffer, $ptr, $off, $ln ); | |
| 114 | $this->parseCopyInstruction( | |
| 115 | $op, $buffer, $ptr, $off, $ln | |
| 116 | ); | |
| 108 | 117 | |
| 109 | 118 | if( $isStream ) { |
| 110 | 119 | $base->seek( $off ); |
| 111 | 120 | $rem = $ln; |
| 112 | 121 | |
| 113 | 122 | while( $rem > 0 ) { |
| 114 | $slc = $base->read( min( self::CHUNK_SIZE, $rem ) ); | |
| 123 | $slc = $base->read( | |
| 124 | \min( self::CHUNK_SIZE, $rem ) | |
| 125 | ); | |
| 115 | 126 | |
| 116 | 127 | if( $slc === '' ) { |
| 117 | 128 | $rem = 0; |
| 118 | 129 | } else { |
| 119 | $slcLen = strlen( $slc ); | |
| 130 | $slcLen = \strlen( $slc ); | |
| 120 | 131 | $yieldBuffer .= $slc; |
| 121 | 132 | $yieldBufLen += $slcLen; |
| ... | ||
| 151 | 162 | } |
| 152 | 163 | |
| 153 | $yieldBuffer .= substr( $buffer, $offset + 1, $ln ); | |
| 164 | $yieldBuffer .= \substr( $buffer, $offset + 1, $ln ); | |
| 154 | 165 | $yieldBufLen += $ln; |
| 155 | 166 | $offset += 1 + $ln; |
| ... | ||
| 176 | 187 | } |
| 177 | 188 | |
| 178 | public function readDeltaTargetSize( StreamReader $handle, int $type ): int { | |
| 189 | public function readDeltaTargetSize( | |
| 190 | StreamReader $handle, | |
| 191 | int $type | |
| 192 | ): int { | |
| 179 | 193 | if( $type === 6 ) { |
| 180 | $byte = ord( $handle->read( 1 ) ); | |
| 194 | $byte = \ord( $handle->read( 1 ) ); | |
| 181 | 195 | |
| 182 | 196 | while( $byte & 128 ) { |
| 183 | $byte = ord( $handle->read( 1 ) ); | |
| 197 | $byte = \ord( $handle->read( 1 ) ); | |
| 184 | 198 | } |
| 185 | 199 | } else { |
| 186 | 200 | $handle->seek( 20, SEEK_CUR ); |
| 187 | 201 | } |
| 188 | 202 | |
| 189 | 203 | $head = $this->readInflatedHead( $handle ); |
| 190 | ||
| 191 | if( strlen( $head ) === 0 ) return 0; | |
| 204 | $pos = 0; | |
| 192 | 205 | |
| 193 | $pos = 0; | |
| 194 | $this->readDeltaSize( $head, $pos ); | |
| 206 | if( \strlen( $head ) > 0 ) { | |
| 207 | $this->readDeltaSize( $head, $pos ); | |
| 208 | } | |
| 195 | 209 | |
| 196 | return $this->readDeltaSize( $head, $pos ); | |
| 210 | return \strlen( $head ) > 0 | |
| 211 | ? $this->readDeltaSize( $head, $pos ) | |
| 212 | : 0; | |
| 197 | 213 | } |
| 198 | 214 | |
| 199 | public function readDeltaBaseSize( StreamReader $handle ): int { | |
| 215 | public function readDeltaBaseSize( | |
| 216 | StreamReader $handle | |
| 217 | ): int { | |
| 200 | 218 | $head = $this->readInflatedHead( $handle ); |
| 201 | ||
| 202 | if( strlen( $head ) === 0 ) return 0; | |
| 203 | ||
| 204 | $pos = 0; | |
| 219 | $pos = 0; | |
| 205 | 220 | |
| 206 | return $this->readDeltaSize( $head, $pos ); | |
| 221 | return \strlen( $head ) > 0 | |
| 222 | ? $this->readDeltaSize( $head, $pos ) | |
| 223 | : 0; | |
| 207 | 224 | } |
| 208 | 225 | |
| 209 | private function readInflatedHead( StreamReader $handle ): string { | |
| 210 | $stream = CompressionStream::createInflater(); | |
| 226 | private function readInflatedHead( | |
| 227 | StreamReader $handle | |
| 228 | ): string { | |
| 229 | $stream = new ZlibInflaterStream(); | |
| 211 | 230 | $head = ''; |
| 212 | 231 | $try = 0; |
| 213 | 232 | |
| 214 | 233 | foreach( $stream->stream( $handle, 512 ) as $out ) { |
| 215 | 234 | $head .= $out; |
| 216 | 235 | $try++; |
| 217 | 236 | |
| 218 | if( strlen( $head ) >= 32 || $try >= 64 ) { | |
| 237 | if( \strlen( $head ) >= 32 || $try >= 64 ) { | |
| 219 | 238 | break; |
| 220 | 239 | } |
| ... | ||
| 234 | 253 | $len = 0; |
| 235 | 254 | |
| 236 | ($op & 0x01) ? $off |= ord( $data[$pos++] ) : null; | |
| 237 | ($op & 0x02) ? $off |= ord( $data[$pos++] ) << 8 : null; | |
| 238 | ($op & 0x04) ? $off |= ord( $data[$pos++] ) << 16 : null; | |
| 239 | ($op & 0x08) ? $off |= ord( $data[$pos++] ) << 24 : null; | |
| 255 | $off |= ($op & 0x01) ? \ord( $data[$pos++] ) : 0; | |
| 256 | $off |= ($op & 0x02) ? \ord( $data[$pos++] ) << 8 : 0; | |
| 257 | $off |= ($op & 0x04) ? \ord( $data[$pos++] ) << 16 : 0; | |
| 258 | $off |= ($op & 0x08) ? \ord( $data[$pos++] ) << 24 : 0; | |
| 240 | 259 | |
| 241 | ($op & 0x10) ? $len |= ord( $data[$pos++] ) : null; | |
| 242 | ($op & 0x20) ? $len |= ord( $data[$pos++] ) << 8 : null; | |
| 243 | ($op & 0x40) ? $len |= ord( $data[$pos++] ) << 16 : null; | |
| 260 | $len |= ($op & 0x10) ? \ord( $data[$pos++] ) : 0; | |
| 261 | $len |= ($op & 0x20) ? \ord( $data[$pos++] ) << 8 : 0; | |
| 262 | $len |= ($op & 0x40) ? \ord( $data[$pos++] ) << 16 : 0; | |
| 244 | 263 | |
| 245 | 264 | $len = $len === 0 ? 0x10000 : $len; |
| 246 | 265 | } |
| 247 | 266 | |
| 248 | private function readDeltaSize( string $data, int &$pos ): int { | |
| 249 | $len = strlen( $data ); | |
| 267 | private function readDeltaSize( | |
| 268 | string $data, | |
| 269 | int &$pos | |
| 270 | ): int { | |
| 271 | $len = \strlen( $data ); | |
| 250 | 272 | $val = 0; |
| 251 | 273 | $shift = 0; |
| 252 | 274 | $done = false; |
| 253 | 275 | |
| 254 | 276 | while( !$done && $pos < $len ) { |
| 255 | $byte = ord( $data[$pos++] ); | |
| 277 | $byte = \ord( $data[$pos++] ); | |
| 256 | 278 | $val |= ($byte & 0x7F) << $shift; |
| 257 | 279 | $done = !($byte & 0x80); |
| 21 | 21 | } |
| 22 | 22 | |
| 23 | public function setRepository( | |
| 24 | string $repoPath | |
| 25 | ): void { | |
| 26 | $this->repoPath = \rtrim( $repoPath, '/' ); | |
| 27 | ||
| 28 | $objPath = $this->repoPath . '/objects'; | |
| 29 | $this->refs = new GitRefs( $this->repoPath ); | |
| 30 | $this->packs = new GitPacks( $objPath ); | |
| 31 | $this->loose = new LooseObjects( $objPath ); | |
| 32 | $this->packWriter = new PackfileWriter( | |
| 33 | $this->packs, $this->loose | |
| 34 | ); | |
| 35 | } | |
| 36 | ||
| 37 | public function resolve( | |
| 38 | string $reference | |
| 39 | ): string { | |
| 40 | return $this->refs->resolve( $reference ); | |
| 41 | } | |
| 42 | ||
| 43 | public function getMainBranch(): array { | |
| 44 | return $this->refs->getMainBranch(); | |
| 45 | } | |
| 46 | ||
| 47 | public function eachBranch( | |
| 48 | callable $callback | |
| 49 | ): void { | |
| 50 | $this->refs->scanRefs( | |
| 51 | 'refs/heads', $callback | |
| 52 | ); | |
| 53 | } | |
| 54 | ||
| 55 | public function eachTag( | |
| 56 | callable $callback | |
| 57 | ): void { | |
| 58 | $this->refs->scanRefs( | |
| 59 | 'refs/tags', | |
| 60 | function( $name, $sha ) use ( $callback ) { | |
| 61 | $callback( | |
| 62 | $this->parseTagData( | |
| 63 | $name, $sha, $this->read( $sha ) | |
| 64 | ) | |
| 65 | ); | |
| 66 | } | |
| 67 | ); | |
| 68 | } | |
| 69 | ||
| 70 | public function walk( | |
| 71 | string $refOrSha, | |
| 72 | callable $callback, | |
| 73 | string $path = '' | |
| 74 | ): void { | |
| 75 | $sha = $this->resolve( $refOrSha ); | |
| 76 | $treeSha = $sha !== '' | |
| 77 | ? $this->getTreeSha( $sha ) | |
| 78 | : ''; | |
| 79 | ||
| 80 | if( $path !== '' && $treeSha !== '' ) { | |
| 81 | $info = $this->resolvePath( | |
| 82 | $treeSha, $path | |
| 83 | ); | |
| 84 | $treeSha = $info['isDir'] ? $info['sha'] : ''; | |
| 85 | } | |
| 86 | ||
| 87 | if( $treeSha !== '' ) { | |
| 88 | $this->walkTree( $treeSha, $callback ); | |
| 89 | } | |
| 90 | } | |
| 91 | ||
| 92 | public function readFile( | |
| 93 | string $ref, | |
| 94 | string $path | |
| 95 | ): File { | |
| 96 | $sha = $this->resolve( $ref ); | |
| 97 | $tree = $sha !== '' | |
| 98 | ? $this->getTreeSha( $sha ) | |
| 99 | : ''; | |
| 100 | $info = $tree !== '' | |
| 101 | ? $this->resolvePath( $tree, $path ) | |
| 102 | : []; | |
| 103 | ||
| 104 | return isset( $info['sha'] ) | |
| 105 | && !$info['isDir'] | |
| 106 | && $info['sha'] !== '' | |
| 107 | ? new File( | |
| 108 | \basename( $path ), | |
| 109 | $info['sha'], | |
| 110 | $info['mode'], | |
| 111 | 0, | |
| 112 | $this->getObjectSize( $info['sha'] ), | |
| 113 | $this->peek( $info['sha'] ) | |
| 114 | ) | |
| 115 | : new MissingFile(); | |
| 116 | } | |
| 117 | ||
| 118 | public function getObjectSize( | |
| 119 | string $sha, | |
| 120 | string $path = '' | |
| 121 | ): int { | |
| 122 | $target = $sha; | |
| 123 | ||
| 124 | if( $path !== '' ) { | |
| 125 | $info = $this->resolvePath( | |
| 126 | $this->getTreeSha( | |
| 127 | $this->resolve( $sha ) | |
| 128 | ), | |
| 129 | $path | |
| 130 | ); | |
| 131 | $target = $info['sha'] ?? ''; | |
| 132 | } | |
| 133 | ||
| 134 | return $target !== '' | |
| 135 | ? $this->packs->getSize( $target ) | |
| 136 | ?: $this->loose->getSize( $target ) | |
| 137 | : 0; | |
| 138 | } | |
| 139 | ||
| 140 | public function stream( | |
| 141 | string $sha, | |
| 142 | callable $callback, | |
| 143 | string $path = '' | |
| 144 | ): void { | |
| 145 | $target = $sha; | |
| 146 | ||
| 147 | if( $path !== '' ) { | |
| 148 | $info = $this->resolvePath( | |
| 149 | $this->getTreeSha( | |
| 150 | $this->resolve( $sha ) | |
| 151 | ), | |
| 152 | $path | |
| 153 | ); | |
| 154 | $target = isset( $info['isDir'] ) | |
| 155 | && !$info['isDir'] | |
| 156 | ? $info['sha'] | |
| 157 | : ''; | |
| 158 | } | |
| 159 | ||
| 160 | if( $target !== '' ) { | |
| 161 | $this->slurp( $target, $callback ); | |
| 162 | } | |
| 163 | } | |
| 164 | ||
| 165 | public function peek( | |
| 166 | string $sha, | |
| 167 | int $length = 255 | |
| 168 | ): string { | |
| 169 | return $this->packs->getSize( $sha ) > 0 | |
| 170 | ? $this->packs->peek( $sha, $length ) | |
| 171 | : $this->loose->peek( $sha, $length ); | |
| 172 | } | |
| 173 | ||
| 174 | public function read( string $sha ): string { | |
| 175 | $size = $this->getObjectSize( $sha ); | |
| 176 | $content = ''; | |
| 177 | ||
| 178 | if( $size > 0 && $size <= self::MAX_READ ) { | |
| 179 | $this->slurp( | |
| 180 | $sha, | |
| 181 | function( $chunk ) use ( &$content ) { | |
| 182 | $content .= $chunk; | |
| 183 | } | |
| 184 | ); | |
| 185 | } | |
| 186 | ||
| 187 | return $content; | |
| 188 | } | |
| 189 | ||
| 190 | public function history( | |
| 191 | string $ref, | |
| 192 | int $limit, | |
| 193 | callable $callback | |
| 194 | ): void { | |
| 195 | $sha = $this->resolve( $ref ); | |
| 196 | $count = 0; | |
| 197 | $done = false; | |
| 198 | ||
| 199 | while( | |
| 200 | !$done && $sha !== '' && $count < $limit | |
| 201 | ) { | |
| 202 | $data = $this->read( $sha ); | |
| 203 | ||
| 204 | if( $data === '' ) { | |
| 205 | $done = true; | |
| 206 | } else { | |
| 207 | $id = $this->parseIdentity( | |
| 208 | $data, '/^author (.*) <(.*)> (\d+)/m' | |
| 209 | ); | |
| 210 | $parentSha = $this->extractPattern( | |
| 211 | $data, '/^parent (.*)$/m', 1 | |
| 212 | ); | |
| 213 | ||
| 214 | $commit = new Commit( | |
| 215 | $sha, | |
| 216 | $this->extractMessage( $data ), | |
| 217 | $id['name'], | |
| 218 | $id['email'], | |
| 219 | $id['timestamp'], | |
| 220 | $parentSha | |
| 221 | ); | |
| 222 | ||
| 223 | if( $callback( $commit ) === false ) { | |
| 224 | $done = true; | |
| 225 | } else { | |
| 226 | $sha = $parentSha; | |
| 227 | $count++; | |
| 228 | } | |
| 229 | } | |
| 230 | } | |
| 231 | } | |
| 232 | ||
| 233 | public function streamRaw( | |
| 234 | string $subPath | |
| 235 | ): bool { | |
| 236 | $result = false; | |
| 237 | ||
| 238 | if( \strpos( $subPath, '..' ) === false ) { | |
| 239 | $path = "{$this->repoPath}/$subPath"; | |
| 240 | ||
| 241 | if( \is_file( $path ) ) { | |
| 242 | $real = \realpath( $path ); | |
| 243 | $repo = \realpath( $this->repoPath ); | |
| 244 | ||
| 245 | if( | |
| 246 | $real !== false | |
| 247 | && \strpos( $real, $repo ) === 0 | |
| 248 | ) { | |
| 249 | \header( | |
| 250 | 'X-Accel-Redirect: ' . $path | |
| 251 | ); | |
| 252 | \header( | |
| 253 | 'Content-Type: application/octet-stream' | |
| 254 | ); | |
| 255 | $result = true; | |
| 256 | } | |
| 257 | } | |
| 258 | } | |
| 259 | ||
| 260 | return $result; | |
| 261 | } | |
| 262 | ||
| 263 | public function eachRef( | |
| 264 | callable $callback | |
| 265 | ): void { | |
| 266 | $head = $this->resolve( 'HEAD' ); | |
| 267 | ||
| 268 | if( $head !== '' ) { | |
| 269 | $callback( 'HEAD', $head ); | |
| 270 | } | |
| 271 | ||
| 272 | $this->refs->scanRefs( | |
| 273 | 'refs/heads', | |
| 274 | function( $n, $s ) use ( $callback ) { | |
| 275 | $callback( "refs/heads/$n", $s ); | |
| 276 | } | |
| 277 | ); | |
| 278 | ||
| 279 | $this->refs->scanRefs( | |
| 280 | 'refs/tags', | |
| 281 | function( $n, $s ) use ( $callback ) { | |
| 282 | $callback( "refs/tags/$n", $s ); | |
| 283 | } | |
| 284 | ); | |
| 285 | } | |
| 286 | ||
| 287 | public function generatePackfile( | |
| 288 | array $objs | |
| 289 | ): Generator { | |
| 290 | yield from $this->packWriter->generate( | |
| 291 | $objs | |
| 292 | ); | |
| 293 | } | |
| 294 | ||
| 295 | public function collectObjects( | |
| 296 | array $wants, | |
| 297 | array $haves = [] | |
| 298 | ): array { | |
| 299 | $objs = $this->traverseObjects( $wants ); | |
| 300 | ||
| 301 | if( !empty( $haves ) ) { | |
| 302 | foreach( | |
| 303 | $this->traverseObjects( | |
| 304 | $haves | |
| 305 | ) as $sha => $type | |
| 306 | ) { | |
| 307 | unset( $objs[$sha] ); | |
| 308 | } | |
| 309 | } | |
| 310 | ||
| 311 | return $objs; | |
| 312 | } | |
| 313 | ||
| 314 | public function parseTreeData( | |
| 315 | string $data, | |
| 316 | callable $callback | |
| 317 | ): void { | |
| 318 | $pos = 0; | |
| 319 | $len = \strlen( $data ); | |
| 320 | ||
| 321 | while( $pos < $len ) { | |
| 322 | $space = \strpos( $data, ' ', $pos ); | |
| 323 | $eos = \strpos( $data, "\0", $space ); | |
| 324 | ||
| 325 | if( | |
| 326 | $space === false | |
| 327 | || $eos === false | |
| 328 | || $eos + 21 > $len | |
| 329 | ) { | |
| 330 | break; | |
| 331 | } | |
| 332 | ||
| 333 | $mode = \substr( | |
| 334 | $data, $pos, $space - $pos | |
| 335 | ); | |
| 336 | $name = \substr( | |
| 337 | $data, $space + 1, $eos - $space - 1 | |
| 338 | ); | |
| 339 | $sha = \bin2hex( | |
| 340 | \substr( $data, $eos + 1, 20 ) | |
| 341 | ); | |
| 342 | ||
| 343 | if( | |
| 344 | $callback( $name, $sha, $mode ) === false | |
| 345 | ) { | |
| 346 | break; | |
| 347 | } | |
| 348 | ||
| 349 | $pos = $eos + 21; | |
| 350 | } | |
| 351 | } | |
| 352 | ||
| 353 | private function slurp( | |
| 354 | string $sha, | |
| 355 | callable $callback | |
| 356 | ): void { | |
| 357 | if( | |
| 358 | !$this->loose->stream( $sha, $callback ) | |
| 359 | && !$this->packs->stream( | |
| 360 | $sha, $callback | |
| 361 | ) | |
| 362 | ) { | |
| 363 | $data = $this->packs->read( $sha ); | |
| 364 | ||
| 365 | if( $data !== '' ) { | |
| 366 | $callback( $data ); | |
| 367 | } | |
| 368 | } | |
| 369 | } | |
| 370 | ||
| 371 | private function walkTree( | |
| 372 | string $sha, | |
| 373 | callable $callback | |
| 374 | ): void { | |
| 375 | $data = $this->read( $sha ); | |
| 376 | $tree = $data !== '' | |
| 377 | && \preg_match( | |
| 378 | '/^tree (.*)$/m', $data, $m | |
| 379 | ) | |
| 380 | ? $this->read( $m[1] ) | |
| 381 | : $data; | |
| 382 | ||
| 383 | if( | |
| 384 | $tree !== '' | |
| 385 | && $this->isTreeData( $tree ) | |
| 386 | ) { | |
| 387 | $this->parseTreeData( | |
| 388 | $tree, | |
| 389 | function( | |
| 390 | $n, $s, $m | |
| 391 | ) use ( $callback ) { | |
| 392 | $dir = $m === '40000' | |
| 393 | || $m === '040000'; | |
| 394 | $isSub = $m === '160000'; | |
| 395 | ||
| 396 | $callback( new File( | |
| 397 | $n, | |
| 398 | $s, | |
| 399 | $m, | |
| 400 | 0, | |
| 401 | $dir || $isSub | |
| 402 | ? 0 | |
| 403 | : $this->getObjectSize( $s ), | |
| 404 | $dir || $isSub | |
| 405 | ? '' | |
| 406 | : $this->peek( $s ) | |
| 407 | ) ); | |
| 408 | } | |
| 409 | ); | |
| 410 | } | |
| 411 | } | |
| 412 | ||
| 413 | private function isTreeData( | |
| 414 | string $data | |
| 415 | ): bool { | |
| 416 | $len = \strlen( $data ); | |
| 417 | $match = $len >= 25 | |
| 418 | && \preg_match( | |
| 419 | '/^(40000|100644|100755|120000|160000) /', | |
| 420 | $data | |
| 421 | ); | |
| 422 | $eos = $match | |
| 423 | ? \strpos( $data, "\0" ) | |
| 424 | : false; | |
| 425 | ||
| 426 | return $match | |
| 427 | && $eos !== false | |
| 428 | && $eos + 21 <= $len; | |
| 429 | } | |
| 430 | ||
| 431 | private function getTreeSha( | |
| 432 | string $commitOrTreeSha | |
| 433 | ): string { | |
| 434 | $data = $this->read( $commitOrTreeSha ); | |
| 435 | $sha = $commitOrTreeSha; | |
| 436 | ||
| 437 | if( | |
| 438 | \preg_match( | |
| 439 | '/^object ([0-9a-f]{40})/m', | |
| 440 | $data, | |
| 441 | $matches | |
| 442 | ) | |
| 443 | ) { | |
| 444 | $sha = $this->getTreeSha( $matches[1] ); | |
| 445 | } | |
| 446 | ||
| 447 | if( | |
| 448 | $sha === $commitOrTreeSha | |
| 449 | && \preg_match( | |
| 450 | '/^tree ([0-9a-f]{40})/m', | |
| 451 | $data, | |
| 452 | $matches | |
| 453 | ) | |
| 454 | ) { | |
| 455 | $sha = $matches[1]; | |
| 456 | } | |
| 457 | ||
| 458 | return $sha; | |
| 459 | } | |
| 460 | ||
| 461 | private function resolvePath( | |
| 462 | string $treeSha, | |
| 463 | string $path | |
| 464 | ): array { | |
| 465 | $parts = \explode( | |
| 466 | '/', \trim( $path, '/' ) | |
| 467 | ); | |
| 468 | ||
| 469 | $sha = $treeSha; | |
| 470 | $mode = '40000'; | |
| 471 | ||
| 472 | foreach( $parts as $part ) { | |
| 473 | $entry = $part !== '' && $sha !== '' | |
| 474 | ? $this->findTreeEntry( $sha, $part ) | |
| 475 | : [ 'sha' => '', 'mode' => '' ]; | |
| 476 | ||
| 477 | $sha = $entry['sha']; | |
| 478 | $mode = $entry['mode']; | |
| 479 | } | |
| 480 | ||
| 481 | return [ | |
| 482 | 'sha' => $sha, | |
| 483 | 'mode' => $mode, | |
| 484 | 'isDir' => $mode === '40000' | |
| 485 | || $mode === '040000' | |
| 486 | ]; | |
| 487 | } | |
| 488 | ||
| 489 | private function findTreeEntry( | |
| 490 | string $treeSha, | |
| 491 | string $name | |
| 492 | ): array { | |
| 493 | $entry = [ 'sha' => '', 'mode' => '' ]; | |
| 494 | ||
| 495 | $this->parseTreeData( | |
| 496 | $this->read( $treeSha ), | |
| 497 | function( | |
| 498 | $n, $s, $m | |
| 499 | ) use ( $name, &$entry ) { | |
| 500 | if( $n === $name ) { | |
| 501 | $entry = [ | |
| 502 | 'sha' => $s, | |
| 503 | 'mode' => $m | |
| 504 | ]; | |
| 505 | ||
| 506 | return false; | |
| 507 | } | |
| 508 | } | |
| 509 | ); | |
| 510 | ||
| 511 | return $entry; | |
| 512 | } | |
| 513 | ||
| 514 | private function parseTagData( | |
| 515 | string $name, | |
| 516 | string $sha, | |
| 517 | string $data | |
| 518 | ): Tag { | |
| 519 | $isAnn = \strncmp( | |
| 520 | $data, 'object ', 7 | |
| 521 | ) === 0; | |
| 522 | ||
| 523 | $id = $this->parseIdentity( | |
| 524 | $data, | |
| 525 | $isAnn | |
| 526 | ? '/^tagger (.*) <(.*)> (\d+) [+\-]\d{4}$/m' | |
| 527 | : '/^author (.*) <(.*)> (\d+) [+\-]\d{4}$/m' | |
| 528 | ); | |
| 529 | ||
| 530 | return new Tag( | |
| 531 | $name, | |
| 532 | $sha, | |
| 533 | $isAnn | |
| 534 | ? $this->extractPattern( | |
| 535 | $data, | |
| 536 | '/^object (.*)$/m', | |
| 537 | 1, | |
| 538 | $sha | |
| 539 | ) | |
| 540 | : $sha, | |
| 541 | $id['timestamp'], | |
| 542 | $this->extractMessage( $data ), | |
| 543 | $id['name'] | |
| 544 | ); | |
| 545 | } | |
| 546 | ||
| 547 | private function extractPattern( | |
| 548 | string $data, | |
| 549 | string $pattern, | |
| 550 | int $group, | |
| 551 | string $default = '' | |
| 552 | ): string { | |
| 553 | return \preg_match( | |
| 554 | $pattern, $data, $matches | |
| 555 | ) | |
| 556 | ? $matches[$group] | |
| 557 | : $default; | |
| 558 | } | |
| 559 | ||
| 560 | private function parseIdentity( | |
| 561 | string $data, | |
| 562 | string $pattern | |
| 563 | ): array { | |
| 564 | $found = \preg_match( | |
| 565 | $pattern, $data, $matches | |
| 566 | ); | |
| 567 | ||
| 568 | return [ | |
| 569 | 'name' => $found | |
| 570 | ? \trim( $matches[1] ) | |
| 571 | : 'Unknown', | |
| 572 | 'email' => $found | |
| 573 | ? $matches[2] | |
| 574 | : '', | |
| 575 | 'timestamp' => $found | |
| 576 | ? (int)$matches[3] | |
| 577 | : 0 | |
| 578 | ]; | |
| 579 | } | |
| 580 | ||
| 581 | private function extractMessage( | |
| 582 | string $data | |
| 583 | ): string { | |
| 584 | $pos = \strpos( $data, "\n\n" ); | |
| 585 | ||
| 586 | return $pos !== false | |
| 587 | ? \trim( \substr( $data, $pos + 2 ) ) | |
| 588 | : ''; | |
| 589 | } | |
| 590 | ||
| 591 | private function traverseObjects( | |
| 592 | array $roots | |
| 593 | ): array { | |
| 594 | $objs = []; | |
| 595 | $queue = []; | |
| 596 | ||
| 597 | foreach( $roots as $sha ) { | |
| 598 | $queue[] = [ | |
| 599 | 'sha' => $sha, | |
| 600 | 'type' => 0 | |
| 601 | ]; | |
| 602 | } | |
| 603 | ||
| 604 | while( !empty( $queue ) ) { | |
| 605 | $item = \array_pop( $queue ); | |
| 606 | $sha = $item['sha']; | |
| 607 | $type = $item['type']; | |
| 608 | ||
| 609 | if( !isset( $objs[$sha] ) ) { | |
| 610 | $data = $type !== 3 | |
| 611 | ? $this->read( $sha ) | |
| 612 | : ''; | |
| 613 | $type = $type === 0 | |
| 614 | ? $this->getObjectType( $data ) | |
| 615 | : $type; | |
| 616 | ||
| 617 | $objs[$sha] = $type; | |
| 618 | ||
| 619 | if( $type === 1 ) { | |
| 620 | if( | |
| 621 | \preg_match( | |
| 622 | '/^tree ([0-9a-f]{40})/m', | |
| 623 | $data, | |
| 624 | $m | |
| 625 | ) | |
| 626 | ) { | |
| 627 | $queue[] = [ | |
| 628 | 'sha' => $m[1], | |
| 629 | 'type' => 2 | |
| 630 | ]; | |
| 631 | } | |
| 632 | ||
| 633 | if( | |
| 634 | \preg_match_all( | |
| 635 | '/^parent ([0-9a-f]{40})/m', | |
| 636 | $data, | |
| 637 | $m | |
| 638 | ) | |
| 639 | ) { | |
| 640 | foreach( $m[1] as $parentSha ) { | |
| 641 | $queue[] = [ | |
| 642 | 'sha' => $parentSha, | |
| 643 | 'type' => 1 | |
| 644 | ]; | |
| 645 | } | |
| 646 | } | |
| 647 | } elseif( $type === 2 ) { | |
| 648 | $this->parseTreeData( | |
| 649 | $data, | |
| 650 | function( | |
| 651 | $n, $s, $m | |
| 652 | ) use ( &$queue ) { | |
| 653 | if( $m !== '160000' ) { | |
| 654 | $queue[] = [ | |
| 655 | 'sha' => $s, | |
| 656 | 'type' => $m === '40000' | |
| 657 | || $m === '040000' | |
| 658 | ? 2 | |
| 659 | : 3 | |
| 660 | ]; | |
| 661 | } | |
| 662 | } | |
| 663 | ); | |
| 664 | } elseif( $type === 4 ) { | |
| 665 | if( | |
| 666 | \preg_match( | |
| 667 | '/^object ([0-9a-f]{40})/m', | |
| 668 | $data, | |
| 669 | $m | |
| 670 | ) | |
| 671 | ) { | |
| 672 | $nextType = 1; | |
| 673 | ||
| 674 | if( | |
| 675 | \preg_match( | |
| 676 | '/^type (commit|tree|blob|tag)/m', | |
| 677 | $data, | |
| 678 | $t | |
| 679 | ) | |
| 680 | ) { | |
| 681 | $map = [ | |
| 682 | 'commit' => 1, | |
| 683 | 'tree' => 2, | |
| 684 | 'blob' => 3, | |
| 685 | 'tag' => 4 | |
| 686 | ]; | |
| 687 | ||
| 688 | $nextType = $map[$t[1]] ?? 1; | |
| 689 | } | |
| 690 | ||
| 691 | $queue[] = [ | |
| 692 | 'sha' => $m[1], | |
| 693 | 'type' => $nextType | |
| 694 | ]; | |
| 695 | } | |
| 696 | } | |
| 697 | } | |
| 698 | } | |
| 699 | ||
| 700 | return $objs; | |
| 701 | } | |
| 702 | ||
| 703 | private function getObjectType( | |
| 704 | string $data | |
| 705 | ): int { | |
| 706 | $result = 3; | |
| 707 | ||
| 708 | if( \strpos( $data, "tree " ) === 0 ) { | |
| 709 | $result = 1; | |
| 710 | } elseif( | |
| 711 | \strpos( $data, "object " ) === 0 | |
| 712 | ) { | |
| 713 | $result = 4; | |
| 714 | } elseif( $this->isTreeData( $data ) ) { | |
| 715 | $result = 2; | |
| 716 | } | |
| 717 | ||
| 718 | return $result; | |
| 719 | } | |
| 720 | } | |
| 721 | ||
| 722 | class MissingFile extends File { | |
| 723 | public function __construct() { | |
| 724 | parent::__construct( | |
| 725 | '', '', '0', 0, 0, '' | |
| 726 | ); | |
| 23 | public function setRepository( string $repoPath ): void { | |
| 24 | $this->repoPath = \rtrim( $repoPath, '/' ); | |
| 25 | $objPath = $this->repoPath . '/objects'; | |
| 26 | $this->refs = new GitRefs( $this->repoPath ); | |
| 27 | $this->packs = new GitPacks( $objPath ); | |
| 28 | $this->loose = new LooseObjects( $objPath ); | |
| 29 | $this->packWriter = new PackfileWriter( | |
| 30 | $this->packs, $this->loose | |
| 31 | ); | |
| 32 | } | |
| 33 | ||
| 34 | public function resolve( string $reference ): string { | |
| 35 | return $this->refs->resolve( $reference ); | |
| 36 | } | |
| 37 | ||
| 38 | public function getMainBranch(): array { | |
| 39 | return $this->refs->getMainBranch(); | |
| 40 | } | |
| 41 | ||
| 42 | public function eachBranch( callable $callback ): void { | |
| 43 | $this->refs->scanRefs( 'refs/heads', $callback ); | |
| 44 | } | |
| 45 | ||
| 46 | public function eachTag( callable $callback ): void { | |
| 47 | $this->refs->scanRefs( | |
| 48 | 'refs/tags', | |
| 49 | function( $name, $sha ) use ( $callback ) { | |
| 50 | $callback( | |
| 51 | new Tag( $name, $sha, $this->read( $sha ) ) | |
| 52 | ); | |
| 53 | } | |
| 54 | ); | |
| 55 | } | |
| 56 | ||
| 57 | public function walk( | |
| 58 | string $refOrSha, | |
| 59 | callable $callback, | |
| 60 | string $path = '' | |
| 61 | ): void { | |
| 62 | $sha = $this->resolve( $refOrSha ); | |
| 63 | $treeSha = $sha !== '' ? $this->getTreeSha( $sha ) : ''; | |
| 64 | ||
| 65 | if( $path !== '' && $treeSha !== '' ) { | |
| 66 | $info = $this->resolvePath( $treeSha, $path ); | |
| 67 | $treeSha = $info['isDir'] ? $info['sha'] : ''; | |
| 68 | } | |
| 69 | ||
| 70 | if( $treeSha !== '' ) { | |
| 71 | $this->walkTree( $treeSha, $callback ); | |
| 72 | } | |
| 73 | } | |
| 74 | ||
| 75 | public function readFile( string $ref, string $path ): File { | |
| 76 | $sha = $this->resolve( $ref ); | |
| 77 | $tree = $sha !== '' ? $this->getTreeSha( $sha ) : ''; | |
| 78 | $info = $tree !== '' ? $this->resolvePath( $tree, $path ) : []; | |
| 79 | ||
| 80 | return isset( $info['sha'] ) | |
| 81 | && !$info['isDir'] | |
| 82 | && $info['sha'] !== '' | |
| 83 | ? new File( | |
| 84 | \basename( $path ), | |
| 85 | $info['sha'], | |
| 86 | $info['mode'], | |
| 87 | 0, | |
| 88 | $this->getObjectSize( $info['sha'] ), | |
| 89 | $this->peek( $info['sha'] ) | |
| 90 | ) | |
| 91 | : new MissingFile(); | |
| 92 | } | |
| 93 | ||
| 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 ); | |
| 106 | } | |
| 107 | ||
| 108 | public function stream( | |
| 109 | string $sha, | |
| 110 | callable $callback, | |
| 111 | string $path = '' | |
| 112 | ): void { | |
| 113 | $target = $sha; | |
| 114 | ||
| 115 | if( $path !== '' ) { | |
| 116 | $info = $this->resolvePath( | |
| 117 | $this->getTreeSha( $this->resolve( $sha ) ), | |
| 118 | $path | |
| 119 | ); | |
| 120 | $target = isset( $info['isDir'] ) && !$info['isDir'] | |
| 121 | ? $info['sha'] | |
| 122 | : ''; | |
| 123 | } | |
| 124 | ||
| 125 | if( $target !== '' ) { | |
| 126 | $this->slurp( $target, $callback ); | |
| 127 | } | |
| 128 | } | |
| 129 | ||
| 130 | public function peek( string $sha, int $length = 255 ): string { | |
| 131 | return $this->packs->getSize( $sha ) > 0 | |
| 132 | ? $this->packs->peek( $sha, $length ) | |
| 133 | : $this->loose->peek( $sha, $length ); | |
| 134 | } | |
| 135 | ||
| 136 | public function read( string $sha ): string { | |
| 137 | $size = $this->getObjectSize( $sha ); | |
| 138 | $content = ''; | |
| 139 | ||
| 140 | if( $size > 0 && $size <= self::MAX_READ ) { | |
| 141 | $this->slurp( | |
| 142 | $sha, | |
| 143 | function( $chunk ) use ( &$content ) { | |
| 144 | $content .= $chunk; | |
| 145 | } | |
| 146 | ); | |
| 147 | } | |
| 148 | ||
| 149 | return $content; | |
| 150 | } | |
| 151 | ||
| 152 | public function history( | |
| 153 | string $ref, | |
| 154 | int $limit, | |
| 155 | callable $callback | |
| 156 | ): void { | |
| 157 | $this->traverseHistory( | |
| 158 | $this->resolve( $ref ), | |
| 159 | $limit, | |
| 160 | $callback, | |
| 161 | 0 | |
| 162 | ); | |
| 163 | } | |
| 164 | ||
| 165 | private function traverseHistory( | |
| 166 | string $sha, | |
| 167 | int $limit, | |
| 168 | callable $callback, | |
| 169 | int $count | |
| 170 | ): void { | |
| 171 | $data = $sha !== '' && $count < $limit | |
| 172 | ? $this->read( $sha ) | |
| 173 | : ''; | |
| 174 | ||
| 175 | if( $data !== '' ) { | |
| 176 | $commit = new Commit( $sha, $data ); | |
| 177 | ||
| 178 | if( $callback( $commit ) !== false ) { | |
| 179 | $commit->provideParent( | |
| 180 | function( $parent ) use ( $limit, $callback, $count ): void { | |
| 181 | $this->traverseHistory( | |
| 182 | $parent, | |
| 183 | $limit, | |
| 184 | $callback, | |
| 185 | $count + 1 | |
| 186 | ); | |
| 187 | } | |
| 188 | ); | |
| 189 | } | |
| 190 | } | |
| 191 | } | |
| 192 | ||
| 193 | public function streamRaw( string $subPath ): bool { | |
| 194 | return \strpos( $subPath, '..' ) === false | |
| 195 | && \is_file( "{$this->repoPath}/$subPath" ) | |
| 196 | && \realpath( "{$this->repoPath}/$subPath" ) !== false | |
| 197 | && \strpos( | |
| 198 | \realpath( "{$this->repoPath}/$subPath" ), | |
| 199 | \realpath( $this->repoPath ) | |
| 200 | ) === 0 | |
| 201 | ? $this->sendHeaders( "{$this->repoPath}/$subPath" ) | |
| 202 | : false; | |
| 203 | } | |
| 204 | ||
| 205 | private function sendHeaders( string $path ): bool { | |
| 206 | \header( 'X-Accel-Redirect: ' . $path ); | |
| 207 | \header( 'Content-Type: application/octet-stream' ); | |
| 208 | ||
| 209 | return true; | |
| 210 | } | |
| 211 | ||
| 212 | public function eachRef( callable $callback ): void { | |
| 213 | $head = $this->resolve( 'HEAD' ); | |
| 214 | ||
| 215 | if( $head !== '' ) { | |
| 216 | $callback( 'HEAD', $head ); | |
| 217 | } | |
| 218 | ||
| 219 | $this->refs->scanRefs( | |
| 220 | 'refs/heads', | |
| 221 | function( $n, $s ) use ( $callback ) { | |
| 222 | $callback( "refs/heads/$n", $s ); | |
| 223 | } | |
| 224 | ); | |
| 225 | ||
| 226 | $this->refs->scanRefs( | |
| 227 | 'refs/tags', | |
| 228 | function( $n, $s ) use ( $callback ) { | |
| 229 | $callback( "refs/tags/$n", $s ); | |
| 230 | } | |
| 231 | ); | |
| 232 | } | |
| 233 | ||
| 234 | public function generatePackfile( array $objs ): Generator { | |
| 235 | yield from $this->packWriter->generate( $objs ); | |
| 236 | } | |
| 237 | ||
| 238 | public function collectObjects( | |
| 239 | array $wants, | |
| 240 | array $haves = [] | |
| 241 | ): array { | |
| 242 | $objs = $this->traverseObjects( $wants ); | |
| 243 | ||
| 244 | if( !empty( $haves ) ) { | |
| 245 | foreach( $this->traverseObjects( $haves ) as $sha => $type ) { | |
| 246 | unset( $objs[$sha] ); | |
| 247 | } | |
| 248 | } | |
| 249 | ||
| 250 | return $objs; | |
| 251 | } | |
| 252 | ||
| 253 | public function parseTreeData( string $data, callable $callback ): void { | |
| 254 | $pos = 0; | |
| 255 | $len = \strlen( $data ); | |
| 256 | ||
| 257 | while( $pos < $len ) { | |
| 258 | $space = \strpos( $data, ' ', $pos ); | |
| 259 | $eos = \strpos( $data, "\0", $space ); | |
| 260 | ||
| 261 | if( $space === false || $eos === false || $eos + 21 > $len ) { | |
| 262 | break; | |
| 263 | } | |
| 264 | ||
| 265 | if( | |
| 266 | $callback( | |
| 267 | \substr( $data, $space + 1, $eos - $space - 1 ), | |
| 268 | \bin2hex( \substr( $data, $eos + 1, 20 ) ), | |
| 269 | \substr( $data, $pos, $space - $pos ) | |
| 270 | ) === false | |
| 271 | ) { | |
| 272 | break; | |
| 273 | } | |
| 274 | ||
| 275 | $pos = $eos + 21; | |
| 276 | } | |
| 277 | } | |
| 278 | ||
| 279 | private function slurp( string $sha, callable $callback ): void { | |
| 280 | if( | |
| 281 | !$this->loose->stream( $sha, $callback ) | |
| 282 | && !$this->packs->stream( $sha, $callback ) | |
| 283 | ) { | |
| 284 | $data = $this->packs->read( $sha ); | |
| 285 | ||
| 286 | if( $data !== '' ) { | |
| 287 | $callback( $data ); | |
| 288 | } | |
| 289 | } | |
| 290 | } | |
| 291 | ||
| 292 | private function walkTree( string $sha, callable $callback ): void { | |
| 293 | $data = $this->read( $sha ); | |
| 294 | $tree = $data !== '' && \preg_match( '/^tree (.*)$/m', $data, $m ) | |
| 295 | ? $this->read( $m[1] ) | |
| 296 | : $data; | |
| 297 | ||
| 298 | if( $tree !== '' && $this->isTreeData( $tree ) ) { | |
| 299 | $this->parseTreeData( | |
| 300 | $tree, | |
| 301 | function( $n, $s, $m ) use ( $callback ) { | |
| 302 | $dir = $m === '40000' || $m === '040000'; | |
| 303 | $isSub = $m === '160000'; | |
| 304 | ||
| 305 | $callback( new File( | |
| 306 | $n, | |
| 307 | $s, | |
| 308 | $m, | |
| 309 | 0, | |
| 310 | $dir || $isSub ? 0 : $this->getObjectSize( $s ), | |
| 311 | $dir || $isSub ? '' : $this->peek( $s ) | |
| 312 | ) ); | |
| 313 | } | |
| 314 | ); | |
| 315 | } | |
| 316 | } | |
| 317 | ||
| 318 | private function isTreeData( string $data ): bool { | |
| 319 | $len = \strlen( $data ); | |
| 320 | $match = $len >= 25 | |
| 321 | && \preg_match( | |
| 322 | '/^(40000|100644|100755|120000|160000) /', | |
| 323 | $data | |
| 324 | ); | |
| 325 | ||
| 326 | return $match | |
| 327 | && \strpos( $data, "\0" ) !== false | |
| 328 | && \strpos( $data, "\0" ) + 21 <= $len; | |
| 329 | } | |
| 330 | ||
| 331 | private function getTreeSha( string $commitOrTreeSha ): string { | |
| 332 | $data = $this->read( $commitOrTreeSha ); | |
| 333 | ||
| 334 | return \preg_match( '/^object ([0-9a-f]{40})/m', $data, $matches ) | |
| 335 | ? $this->getTreeSha( $matches[1] ) | |
| 336 | : ( \preg_match( '/^tree ([0-9a-f]{40})/m', $data, $matches ) | |
| 337 | ? $matches[1] | |
| 338 | : $commitOrTreeSha ); | |
| 339 | } | |
| 340 | ||
| 341 | private function resolvePath( string $treeSha, string $path ): array { | |
| 342 | $parts = \explode( '/', \trim( $path, '/' ) ); | |
| 343 | $sha = $treeSha; | |
| 344 | $mode = '40000'; | |
| 345 | ||
| 346 | foreach( $parts as $part ) { | |
| 347 | $entry = $part !== '' && $sha !== '' | |
| 348 | ? $this->findTreeEntry( $sha, $part ) | |
| 349 | : [ 'sha' => '', 'mode' => '' ]; | |
| 350 | ||
| 351 | $sha = $entry['sha']; | |
| 352 | $mode = $entry['mode']; | |
| 353 | } | |
| 354 | ||
| 355 | return [ | |
| 356 | 'sha' => $sha, | |
| 357 | 'mode' => $mode, | |
| 358 | 'isDir' => $mode === '40000' || $mode === '040000' | |
| 359 | ]; | |
| 360 | } | |
| 361 | ||
| 362 | private function findTreeEntry( string $treeSha, string $name ): array { | |
| 363 | $entry = [ 'sha' => '', 'mode' => '' ]; | |
| 364 | ||
| 365 | $this->parseTreeData( | |
| 366 | $this->read( $treeSha ), | |
| 367 | function( $n, $s, $m ) use ( $name, &$entry ) { | |
| 368 | if( $n === $name ) { | |
| 369 | $entry = [ 'sha' => $s, 'mode' => $m ]; | |
| 370 | ||
| 371 | return false; | |
| 372 | } | |
| 373 | } | |
| 374 | ); | |
| 375 | ||
| 376 | return $entry; | |
| 377 | } | |
| 378 | ||
| 379 | private function traverseObjects( array $roots ): array { | |
| 380 | $objs = []; | |
| 381 | $queue = []; | |
| 382 | ||
| 383 | foreach( $roots as $sha ) { | |
| 384 | $queue[] = [ 'sha' => $sha, 'type' => 0 ]; | |
| 385 | } | |
| 386 | ||
| 387 | while( !empty( $queue ) ) { | |
| 388 | $item = \array_pop( $queue ); | |
| 389 | $sha = $item['sha']; | |
| 390 | $type = $item['type']; | |
| 391 | ||
| 392 | if( !isset( $objs[$sha] ) ) { | |
| 393 | $data = $type !== 3 ? $this->read( $sha ) : ''; | |
| 394 | $type = $type === 0 ? $this->getObjectType( $data ) : $type; | |
| 395 | ||
| 396 | $objs[$sha] = $type; | |
| 397 | ||
| 398 | if( $type === 1 ) { | |
| 399 | if( \preg_match( '/^tree ([0-9a-f]{40})/m', $data, $m ) ) { | |
| 400 | $queue[] = [ 'sha' => $m[1], 'type' => 2 ]; | |
| 401 | } | |
| 402 | ||
| 403 | if( \preg_match_all( '/^parent ([0-9a-f]{40})/m', $data, $m ) ) { | |
| 404 | foreach( $m[1] as $parentSha ) { | |
| 405 | $queue[] = [ 'sha' => $parentSha, 'type' => 1 ]; | |
| 406 | } | |
| 407 | } | |
| 408 | } elseif( $type === 2 ) { | |
| 409 | $this->parseTreeData( | |
| 410 | $data, | |
| 411 | function( $n, $s, $m ) use ( &$queue ) { | |
| 412 | if( $m !== '160000' ) { | |
| 413 | $queue[] = [ | |
| 414 | 'sha' => $s, | |
| 415 | 'type' => $m === '40000' || $m === '040000' ? 2 : 3 | |
| 416 | ]; | |
| 417 | } | |
| 418 | } | |
| 419 | ); | |
| 420 | } elseif( $type === 4 ) { | |
| 421 | if( \preg_match( '/^object ([0-9a-f]{40})/m', $data, $m ) ) { | |
| 422 | $queue[] = [ | |
| 423 | 'sha' => $m[1], | |
| 424 | 'type' => \preg_match( '/^type (commit|tree|blob|tag)/m', $data, $t ) | |
| 425 | ? ([ 'commit' => 1, 'tree' => 2, 'blob' => 3, 'tag' => 4 ][$t[1]] ?? 1) | |
| 426 | : 1 | |
| 427 | ]; | |
| 428 | } | |
| 429 | } | |
| 430 | } | |
| 431 | } | |
| 432 | ||
| 433 | return $objs; | |
| 434 | } | |
| 435 | ||
| 436 | private function getObjectType( string $data ): int { | |
| 437 | return \strpos( $data, "tree " ) === 0 | |
| 438 | ? 1 | |
| 439 | : (\strpos( $data, "object " ) === 0 | |
| 440 | ? 4 | |
| 441 | : ($this->isTreeData( $data ) | |
| 442 | ? 2 | |
| 443 | : 3)); | |
| 444 | } | |
| 445 | } | |
| 446 | ||
| 447 | class MissingFile extends File { | |
| 448 | public function __construct() { | |
| 449 | parent::__construct( '', '', '0', 0, 0, '' ); | |
| 727 | 450 | } |
| 728 | 451 |
| 1 | <?php | |
| 2 | require_once __DIR__ . '/StreamReader.php'; | |
| 3 | ||
| 4 | class GitPackStream implements StreamReader { | |
| 5 | private StreamReader $stream; | |
| 6 | ||
| 7 | public function __construct( StreamReader $stream ) { | |
| 8 | $this->stream = $stream; | |
| 9 | } | |
| 10 | ||
| 11 | public function isOpen(): bool { | |
| 12 | return $this->stream->isOpen(); | |
| 13 | } | |
| 14 | ||
| 15 | public function read( int $length ): string { | |
| 16 | return $this->stream->read( $length ); | |
| 17 | } | |
| 18 | ||
| 19 | public function write( string $data ): bool { | |
| 20 | return $this->stream->write( $data ); | |
| 21 | } | |
| 22 | ||
| 23 | public function seek( int $offset, int $whence = SEEK_SET ): bool { | |
| 24 | return $this->stream->seek( $offset, $whence ); | |
| 25 | } | |
| 26 | ||
| 27 | public function tell(): int { | |
| 28 | return $this->stream->tell(); | |
| 29 | } | |
| 30 | ||
| 31 | public function eof(): bool { | |
| 32 | return $this->stream->eof(); | |
| 33 | } | |
| 34 | ||
| 35 | public function rewind(): void { | |
| 36 | $this->stream->rewind(); | |
| 37 | } | |
| 38 | ||
| 39 | public function readVarInt(): array { | |
| 40 | $data = $this->stream->read( 12 ); | |
| 41 | $byte = isset( $data[0] ) ? \ord( $data[0] ) : 0; | |
| 42 | $val = $byte & 15; | |
| 43 | $shft = 4; | |
| 44 | $fst = $byte; | |
| 45 | $pos = 1; | |
| 46 | ||
| 47 | while( $byte & 128 ) { | |
| 48 | $byte = isset( $data[$pos] ) | |
| 49 | ? \ord( $data[$pos++] ) | |
| 50 | : 0; | |
| 51 | $val |= ($byte & 127) << $shft; | |
| 52 | $shft += 7; | |
| 53 | } | |
| 54 | ||
| 55 | $rem = \strlen( $data ) - $pos; | |
| 56 | ||
| 57 | if( $rem > 0 ) { | |
| 58 | $this->stream->seek( -$rem, SEEK_CUR ); | |
| 59 | } | |
| 60 | ||
| 61 | return [ | |
| 62 | 'type' => $fst >> 4 & 7, | |
| 63 | 'size' => $val | |
| 64 | ]; | |
| 65 | } | |
| 66 | ||
| 67 | public function readOffsetDelta(): int { | |
| 68 | $data = $this->stream->read( 12 ); | |
| 69 | $byte = isset( $data[0] ) ? \ord( $data[0] ) : 0; | |
| 70 | $result = $byte & 127; | |
| 71 | $pos = 1; | |
| 72 | ||
| 73 | while( $byte & 128 ) { | |
| 74 | $byte = isset( $data[$pos] ) | |
| 75 | ? \ord( $data[$pos++] ) | |
| 76 | : 0; | |
| 77 | $result = ($result + 1) << 7 | $byte & 127; | |
| 78 | } | |
| 79 | ||
| 80 | $rem = \strlen( $data ) - $pos; | |
| 81 | ||
| 82 | if( $rem > 0 ) { | |
| 83 | $this->stream->seek( -$rem, SEEK_CUR ); | |
| 84 | } | |
| 85 | ||
| 86 | return $result; | |
| 87 | } | |
| 88 | } | |
| 1 | 89 |
| 5 | 5 | require_once __DIR__ . '/BufferedReader.php'; |
| 6 | 6 | require_once __DIR__ . '/PackContext.php'; |
| 7 | ||
| 8 | class PackEntryReader { | |
| 9 | private const MAX_DEPTH = 200; | |
| 10 | private const MAX_BASE_RAM = 8388608; | |
| 11 | private const MAX_CACHE = 1024; | |
| 12 | ||
| 13 | private DeltaDecoder $decoder; | |
| 14 | private array $cache; | |
| 15 | ||
| 16 | public function __construct( DeltaDecoder $decoder ) { | |
| 17 | $this->decoder = $decoder; | |
| 18 | $this->cache = []; | |
| 19 | } | |
| 20 | ||
| 21 | public function getEntryMeta( | |
| 22 | PackContext $context | |
| 23 | ): array { | |
| 24 | return $context->computeArray( | |
| 25 | function( | |
| 26 | StreamReader $stream, | |
| 27 | int $offset | |
| 28 | ): array { | |
| 29 | $hdr = $this->readEntryHeader( | |
| 30 | $stream, $offset | |
| 31 | ); | |
| 32 | $result = [ | |
| 33 | 'type' => $hdr['type'], | |
| 34 | 'size' => $hdr['size'], | |
| 35 | ]; | |
| 36 | ||
| 37 | if( $hdr['type'] === 6 ) { | |
| 38 | $neg = $this->readOffsetDelta( | |
| 39 | $stream | |
| 40 | ); | |
| 41 | $result['baseOffset'] = $offset - $neg; | |
| 42 | } elseif( $hdr['type'] === 7 ) { | |
| 43 | $result['baseSha'] = \bin2hex( | |
| 44 | $stream->read( 20 ) | |
| 45 | ); | |
| 46 | } | |
| 47 | ||
| 48 | return $result; | |
| 49 | }, | |
| 50 | [ 'type' => 0, 'size' => 0 ] | |
| 51 | ); | |
| 52 | } | |
| 53 | ||
| 54 | public function getSize( PackContext $context ): int { | |
| 55 | return $context->computeIntDedicated( | |
| 56 | function( | |
| 57 | StreamReader $stream, | |
| 58 | int $offset | |
| 59 | ): int { | |
| 60 | $hdr = $this->readEntryHeader( | |
| 61 | $stream, $offset | |
| 62 | ); | |
| 63 | ||
| 64 | return $hdr['type'] === 6 || $hdr['type'] === 7 | |
| 65 | ? $this->decoder->readDeltaTargetSize( | |
| 66 | $stream, $hdr['type'] | |
| 67 | ) | |
| 68 | : $hdr['size']; | |
| 69 | }, | |
| 70 | 0 | |
| 71 | ); | |
| 72 | } | |
| 73 | ||
| 74 | public function read( | |
| 75 | PackContext $context, | |
| 76 | int $cap, | |
| 77 | callable $readShaBaseFn | |
| 78 | ): string { | |
| 79 | return $context->computeStringDedicated( | |
| 80 | function( | |
| 81 | StreamReader $s, | |
| 82 | int $o | |
| 83 | ) use ( $cap, $readShaBaseFn ): string { | |
| 84 | return $this->readWithStream( | |
| 85 | $s, $o, $cap, $readShaBaseFn | |
| 86 | ); | |
| 87 | }, | |
| 88 | '' | |
| 89 | ); | |
| 90 | } | |
| 91 | ||
| 92 | private function readWithStream( | |
| 93 | StreamReader $stream, | |
| 94 | int $offset, | |
| 95 | int $cap, | |
| 96 | callable $readShaBaseFn | |
| 97 | ): string { | |
| 98 | $result = ''; | |
| 99 | ||
| 100 | if( isset( $this->cache[$offset] ) ) { | |
| 101 | $result = $cap > 0 | |
| 102 | && \strlen( $this->cache[$offset] ) > $cap | |
| 103 | ? \substr( $this->cache[$offset], 0, $cap ) | |
| 104 | : $this->cache[$offset]; | |
| 105 | } else { | |
| 106 | $hdr = $this->readEntryHeader( | |
| 107 | $stream, $offset | |
| 108 | ); | |
| 109 | $type = $hdr['type']; | |
| 110 | ||
| 111 | if( $type === 6 ) { | |
| 112 | $neg = $this->readOffsetDelta( $stream ); | |
| 113 | $cur = $stream->tell(); | |
| 114 | $bData = $this->readWithStream( | |
| 115 | $stream, | |
| 116 | $offset - $neg, | |
| 117 | $cap, | |
| 118 | $readShaBaseFn | |
| 119 | ); | |
| 120 | ||
| 121 | $stream->seek( $cur ); | |
| 122 | ||
| 123 | $result = $this->decoder->apply( | |
| 124 | $bData, | |
| 125 | $this->inflate( $stream ), | |
| 126 | $cap | |
| 127 | ); | |
| 128 | } elseif( $type === 7 ) { | |
| 129 | $sha = \bin2hex( $stream->read( 20 ) ); | |
| 130 | $cur = $stream->tell(); | |
| 131 | $bas = $readShaBaseFn( $sha, $cap ); | |
| 132 | ||
| 133 | $stream->seek( $cur ); | |
| 134 | ||
| 135 | $result = $this->decoder->apply( | |
| 136 | $bas, | |
| 137 | $this->inflate( $stream ), | |
| 138 | $cap | |
| 139 | ); | |
| 140 | } else { | |
| 141 | $result = $this->inflate( $stream, $cap ); | |
| 142 | } | |
| 143 | ||
| 144 | if( $cap === 0 ) { | |
| 145 | $this->cache[$offset] = $result; | |
| 146 | ||
| 147 | if( \count( $this->cache ) > self::MAX_CACHE ) { | |
| 148 | unset( | |
| 149 | $this->cache[ | |
| 150 | \array_key_first( $this->cache ) | |
| 151 | ] | |
| 152 | ); | |
| 153 | } | |
| 154 | } | |
| 155 | } | |
| 156 | ||
| 157 | return $result; | |
| 158 | } | |
| 159 | ||
| 160 | public function streamRawCompressed( | |
| 161 | PackContext $context | |
| 162 | ): Generator { | |
| 163 | yield from $context->streamGenerator( | |
| 164 | function( | |
| 165 | StreamReader $stream, | |
| 166 | int $offset | |
| 167 | ): Generator { | |
| 168 | $hdr = $this->readEntryHeader( | |
| 169 | $stream, $offset | |
| 170 | ); | |
| 171 | ||
| 172 | yield from $hdr['type'] !== 6 | |
| 173 | && $hdr['type'] !== 7 | |
| 174 | ? CompressionStream::createExtractor()->stream( | |
| 175 | $stream | |
| 176 | ) | |
| 177 | : []; | |
| 178 | } | |
| 179 | ); | |
| 180 | } | |
| 181 | ||
| 182 | public function streamRawDelta( | |
| 183 | PackContext $context | |
| 184 | ): Generator { | |
| 185 | yield from $context->streamGenerator( | |
| 186 | function( | |
| 187 | StreamReader $stream, | |
| 188 | int $offset | |
| 189 | ): Generator { | |
| 190 | $hdr = $this->readEntryHeader( | |
| 191 | $stream, $offset | |
| 192 | ); | |
| 193 | ||
| 194 | if( $hdr['type'] === 6 ) { | |
| 195 | $this->readOffsetDelta( $stream ); | |
| 196 | } elseif( $hdr['type'] === 7 ) { | |
| 197 | $stream->read( 20 ); | |
| 198 | } | |
| 199 | ||
| 200 | yield from CompressionStream::createExtractor() | |
| 201 | ->stream( $stream ); | |
| 202 | } | |
| 203 | ); | |
| 204 | } | |
| 205 | ||
| 206 | public function streamEntryGenerator( | |
| 207 | PackContext $context | |
| 208 | ): Generator { | |
| 209 | yield from $context->streamGeneratorDedicated( | |
| 210 | function( | |
| 211 | StreamReader $stream, | |
| 212 | int $offset | |
| 213 | ) use ( $context ): Generator { | |
| 214 | $hdr = $this->readEntryHeader( | |
| 215 | $stream, $offset | |
| 216 | ); | |
| 217 | ||
| 218 | yield from $hdr['type'] === 6 | |
| 219 | || $hdr['type'] === 7 | |
| 220 | ? $this->streamDeltaObjectGenerator( | |
| 221 | $stream, | |
| 222 | $context, | |
| 223 | $hdr['type'], | |
| 224 | $offset | |
| 225 | ) | |
| 226 | : CompressionStream::createInflater()->stream( | |
| 227 | $stream | |
| 228 | ); | |
| 229 | } | |
| 230 | ); | |
| 231 | } | |
| 232 | ||
| 233 | private function readEntryHeader( | |
| 234 | StreamReader $stream, | |
| 235 | int $offset | |
| 236 | ): array { | |
| 237 | $stream->seek( $offset ); | |
| 238 | ||
| 239 | $header = $this->readVarInt( $stream ); | |
| 240 | ||
| 241 | return [ | |
| 242 | 'type' => $header['byte'] >> 4 & 7, | |
| 243 | 'size' => $header['value'] | |
| 244 | ]; | |
| 245 | } | |
| 246 | ||
| 247 | private function streamDeltaObjectGenerator( | |
| 248 | StreamReader $stream, | |
| 249 | PackContext $context, | |
| 250 | int $type, | |
| 251 | int $offset | |
| 252 | ): Generator { | |
| 253 | $gen = $context->isWithinDepth( self::MAX_DEPTH ) | |
| 254 | ? ( $type === 6 | |
| 255 | ? $this->processOffsetDelta( | |
| 256 | $stream, $context, $offset | |
| 257 | ) | |
| 258 | : $this->processRefDelta( $stream, $context ) | |
| 259 | ) | |
| 260 | : []; | |
| 261 | ||
| 262 | yield from $gen; | |
| 263 | } | |
| 264 | ||
| 265 | private function readSizeWithStream( | |
| 266 | StreamReader $stream, | |
| 267 | int $offset | |
| 268 | ): int { | |
| 269 | $result = 0; | |
| 270 | ||
| 271 | if( isset( $this->cache[$offset] ) ) { | |
| 272 | $result = \strlen( $this->cache[$offset] ); | |
| 273 | } else { | |
| 274 | $cur = $stream->tell(); | |
| 275 | $hdr = $this->readEntryHeader( | |
| 276 | $stream, $offset | |
| 277 | ); | |
| 278 | ||
| 279 | $result = $hdr['type'] === 6 | |
| 280 | || $hdr['type'] === 7 | |
| 281 | ? $this->decoder->readDeltaTargetSize( | |
| 282 | $stream, $hdr['type'] | |
| 283 | ) | |
| 284 | : $hdr['size']; | |
| 285 | ||
| 286 | $stream->seek( $cur ); | |
| 287 | } | |
| 288 | ||
| 289 | return $result; | |
| 290 | } | |
| 291 | ||
| 292 | private function processOffsetDelta( | |
| 293 | StreamReader $stream, | |
| 294 | PackContext $context, | |
| 295 | int $offset | |
| 296 | ): Generator { | |
| 297 | $neg = $this->readOffsetDelta( $stream ); | |
| 298 | $cur = $stream->tell(); | |
| 299 | $baseOff = $offset - $neg; | |
| 300 | $baseSrc = ''; | |
| 301 | ||
| 302 | if( isset( $this->cache[$baseOff] ) ) { | |
| 303 | $baseSrc = $this->cache[$baseOff]; | |
| 304 | } elseif( | |
| 305 | $this->readSizeWithStream( | |
| 306 | $stream, $baseOff | |
| 307 | ) <= self::MAX_BASE_RAM | |
| 308 | ) { | |
| 309 | $baseSrc = $this->readWithStream( | |
| 310 | $stream, | |
| 311 | $baseOff, | |
| 312 | 0, | |
| 313 | function( | |
| 314 | string $sha, | |
| 315 | int $cap | |
| 316 | ) use ( $context ): string { | |
| 317 | return $this->resolveBaseSha( | |
| 318 | $sha, $cap, $context | |
| 319 | ); | |
| 320 | } | |
| 321 | ); | |
| 322 | } else { | |
| 323 | $baseCtx = $context->deriveOffsetContext( | |
| 324 | $neg | |
| 325 | ); | |
| 326 | [$b, $tmp] = $this->collectBase( | |
| 327 | $this->streamEntryGenerator( $baseCtx ) | |
| 328 | ); | |
| 329 | $baseSrc = $tmp instanceof BufferedReader | |
| 330 | ? $tmp | |
| 331 | : $b; | |
| 332 | } | |
| 333 | ||
| 334 | $stream->seek( $cur ); | |
| 335 | ||
| 336 | yield from $this->decoder->applyStreamGenerator( | |
| 337 | $stream, $baseSrc | |
| 338 | ); | |
| 339 | } | |
| 340 | ||
| 341 | private function processRefDelta( | |
| 342 | StreamReader $stream, | |
| 343 | PackContext $context | |
| 344 | ): Generator { | |
| 345 | $baseSha = \bin2hex( $stream->read( 20 ) ); | |
| 346 | $cur = $stream->tell(); | |
| 347 | $size = $context->resolveBaseSize( $baseSha ); | |
| 348 | $baseSrc = ''; | |
| 349 | ||
| 350 | if( $size <= self::MAX_BASE_RAM ) { | |
| 351 | $baseSrc = $this->resolveBaseSha( | |
| 352 | $baseSha, 0, $context | |
| 353 | ); | |
| 354 | } else { | |
| 355 | [$b, $tmp] = $this->collectBase( | |
| 356 | $context->resolveBaseStream( $baseSha ) | |
| 357 | ); | |
| 358 | $baseSrc = $tmp instanceof BufferedReader | |
| 359 | ? $tmp | |
| 360 | : $b; | |
| 361 | } | |
| 362 | ||
| 363 | $stream->seek( $cur ); | |
| 364 | ||
| 365 | yield from $this->decoder->applyStreamGenerator( | |
| 366 | $stream, $baseSrc | |
| 367 | ); | |
| 368 | } | |
| 369 | ||
| 370 | private function collectBase( | |
| 371 | iterable $chunks | |
| 372 | ): array { | |
| 373 | $parts = []; | |
| 374 | $total = 0; | |
| 375 | $tmp = false; | |
| 376 | ||
| 377 | foreach( $chunks as $chunk ) { | |
| 378 | $total += \strlen( $chunk ); | |
| 379 | ||
| 380 | if( $tmp instanceof BufferedReader ) { | |
| 381 | $tmp->write( $chunk ); | |
| 382 | } elseif( $total > self::MAX_BASE_RAM ) { | |
| 383 | $tmp = new BufferedReader( | |
| 384 | 'php://temp/maxmemory:65536', 'w+b' | |
| 385 | ); | |
| 386 | ||
| 387 | foreach( $parts as $part ) { | |
| 388 | $tmp->write( $part ); | |
| 389 | } | |
| 390 | ||
| 391 | $tmp->write( $chunk ); | |
| 392 | $parts = []; | |
| 393 | } else { | |
| 394 | $parts[] = $chunk; | |
| 395 | } | |
| 396 | } | |
| 397 | ||
| 398 | if( $tmp instanceof BufferedReader ) { | |
| 399 | $tmp->rewind(); | |
| 400 | } | |
| 401 | ||
| 402 | return [ | |
| 403 | $tmp === false ? \implode( '', $parts ) : '', | |
| 404 | $tmp | |
| 405 | ]; | |
| 406 | } | |
| 407 | ||
| 408 | private function resolveBaseSha( | |
| 409 | string $sha, | |
| 410 | int $cap, | |
| 411 | PackContext $context | |
| 412 | ): string { | |
| 413 | $chunks = []; | |
| 414 | ||
| 415 | foreach( | |
| 416 | $context->resolveBaseStream( $sha ) as $chunk | |
| 417 | ) { | |
| 418 | $chunks[] = $chunk; | |
| 419 | } | |
| 420 | ||
| 421 | $result = \implode( '', $chunks ); | |
| 422 | ||
| 423 | return $cap > 0 && \strlen( $result ) > $cap | |
| 424 | ? \substr( $result, 0, $cap ) | |
| 425 | : $result; | |
| 426 | } | |
| 427 | ||
| 428 | private function readVarInt( | |
| 429 | StreamReader $stream | |
| 430 | ): array { | |
| 431 | $data = $stream->read( 12 ); | |
| 432 | $byte = isset( $data[0] ) ? \ord( $data[0] ) : 0; | |
| 433 | $val = $byte & 15; | |
| 434 | $shft = 4; | |
| 435 | $fst = $byte; | |
| 436 | $pos = 1; | |
| 437 | ||
| 438 | while( $byte & 128 ) { | |
| 439 | $byte = isset( $data[$pos] ) | |
| 440 | ? \ord( $data[$pos++] ) | |
| 441 | : 0; | |
| 442 | $val |= ( $byte & 127 ) << $shft; | |
| 443 | $shft += 7; | |
| 444 | } | |
| 445 | ||
| 446 | $rem = \strlen( $data ) - $pos; | |
| 447 | ||
| 448 | if( $rem > 0 ) { | |
| 449 | $stream->seek( -$rem, SEEK_CUR ); | |
| 450 | } | |
| 451 | ||
| 452 | return [ 'value' => $val, 'byte' => $fst ]; | |
| 453 | } | |
| 454 | ||
| 455 | private function readOffsetDelta( | |
| 456 | StreamReader $stream | |
| 457 | ): int { | |
| 458 | $data = $stream->read( 12 ); | |
| 459 | $byte = isset( $data[0] ) ? \ord( $data[0] ) : 0; | |
| 460 | $result = $byte & 127; | |
| 461 | $pos = 1; | |
| 462 | ||
| 463 | while( $byte & 128 ) { | |
| 464 | $byte = isset( $data[$pos] ) | |
| 465 | ? \ord( $data[$pos++] ) | |
| 466 | : 0; | |
| 467 | $result = ( $result + 1 ) << 7 | $byte & 127; | |
| 468 | } | |
| 469 | ||
| 470 | $rem = \strlen( $data ) - $pos; | |
| 471 | ||
| 472 | if( $rem > 0 ) { | |
| 473 | $stream->seek( -$rem, SEEK_CUR ); | |
| 474 | } | |
| 475 | ||
| 476 | return $result; | |
| 477 | } | |
| 478 | ||
| 479 | private function inflate( | |
| 480 | StreamReader $stream, | |
| 481 | int $cap = 0 | |
| 482 | ): string { | |
| 483 | $inflater = CompressionStream::createInflater(); | |
| 7 | require_once __DIR__ . '/GitPackStream.php'; | |
| 8 | ||
| 9 | class PackEntryReader { | |
| 10 | private const MAX_DEPTH = 200; | |
| 11 | private const MAX_BASE_RAM = 8388608; | |
| 12 | private const MAX_CACHE = 1024; | |
| 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(); | |
| 484 | 377 | $chunks = []; |
| 485 | 378 | $len = 0; |
| 11 | 11 | private string $parentSha; |
| 12 | 12 | |
| 13 | public function __construct( | |
| 14 | string $sha, | |
| 15 | string $message, | |
| 16 | string $author, | |
| 17 | string $email, | |
| 18 | int $date, | |
| 19 | string $parentSha | |
| 20 | ) { | |
| 21 | $this->sha = $sha; | |
| 22 | $this->message = $message; | |
| 23 | $this->author = $author; | |
| 24 | $this->email = $email; | |
| 25 | $this->date = $date; | |
| 26 | $this->parentSha = $parentSha; | |
| 13 | public function __construct( string $sha, string $rawData ) { | |
| 14 | $this->sha = $sha; | |
| 15 | ||
| 16 | $this->author = \preg_match( '/^author (.*?) </m', $rawData, $m ) | |
| 17 | ? \trim( $m[1] ) | |
| 18 | : 'Unknown'; | |
| 19 | ||
| 20 | $this->email = \preg_match( '/^author .*? <(.*?)>/m', $rawData, $m ) | |
| 21 | ? \trim( $m[1] ) | |
| 22 | : ''; | |
| 23 | ||
| 24 | $this->date = \preg_match( '/^author .*? <.*?> (\d+)/m', $rawData, $m ) | |
| 25 | ? (int)$m[1] | |
| 26 | : 0; | |
| 27 | ||
| 28 | $this->parentSha = \preg_match( '/^parent (.*)$/m', $rawData, $m ) | |
| 29 | ? \trim( $m[1] ) | |
| 30 | : ''; | |
| 31 | ||
| 32 | $pos = \strpos( $rawData, "\n\n" ); | |
| 33 | ||
| 34 | $this->message = $pos !== false | |
| 35 | ? \trim( \substr( $rawData, $pos + 2 ) ) | |
| 36 | : ''; | |
| 37 | } | |
| 38 | ||
| 39 | public function provideParent( callable $callback ): void { | |
| 40 | if( $this->parentSha !== '' ) { | |
| 41 | $callback( $this->parentSha ); | |
| 42 | } | |
| 27 | 43 | } |
| 28 | 44 |
| 6 | 6 | private string $sha; |
| 7 | 7 | private string $targetSha; |
| 8 | private int $timestamp; | |
| 8 | private int $timestamp; | |
| 9 | 9 | private string $message; |
| 10 | 10 | private string $author; |
| 11 | 11 | |
| 12 | 12 | public function __construct( |
| 13 | 13 | string $name, |
| 14 | 14 | string $sha, |
| 15 | string $targetSha, | |
| 16 | int $timestamp, | |
| 17 | string $message, | |
| 18 | string $author | |
| 15 | string $rawData | |
| 19 | 16 | ) { |
| 20 | $this->name = $name; | |
| 21 | $this->sha = $sha; | |
| 22 | $this->targetSha = $targetSha; | |
| 23 | $this->timestamp = $timestamp; | |
| 24 | $this->message = $message; | |
| 25 | $this->author = $author; | |
| 17 | $this->name = $name; | |
| 18 | $this->sha = $sha; | |
| 19 | ||
| 20 | $isAnn = \strncmp( $rawData, 'object ', 7 ) === 0; | |
| 21 | ||
| 22 | $this->targetSha = $isAnn | |
| 23 | ? (\preg_match( '/^object (.*)$/m', $rawData, $m ) | |
| 24 | ? \trim( $m[1] ) | |
| 25 | : $sha) | |
| 26 | : $sha; | |
| 27 | ||
| 28 | $pattern = $isAnn | |
| 29 | ? '/^tagger (.*) <(.*)> (\d+) [+\-]\d{4}$/m' | |
| 30 | : '/^author (.*) <(.*)> (\d+) [+\-]\d{4}$/m'; | |
| 31 | ||
| 32 | $this->author = \preg_match( $pattern, $rawData, $m ) | |
| 33 | ? \trim( $m[1] ) | |
| 34 | : 'Unknown'; | |
| 35 | ||
| 36 | $this->timestamp = \preg_match( $pattern, $rawData, $m ) | |
| 37 | ? (int)$m[3] | |
| 38 | : 0; | |
| 39 | ||
| 40 | $pos = \strpos( $rawData, "\n\n" ); | |
| 41 | ||
| 42 | $this->message = $pos !== false | |
| 43 | ? \trim( \substr( $rawData, $pos + 2 ) ) | |
| 44 | : ''; | |
| 26 | 45 | } |
| 27 | 46 | |
| 28 | 47 | public function compare( Tag $other ): int { |
| 29 | 48 | return $other->timestamp <=> $this->timestamp; |
| 30 | 49 | } |
| 31 | 50 | |
| 32 | public function render( TagRenderer $renderer, ?Tag $prevTag = null ): void { | |
| 51 | public function render( | |
| 52 | TagRenderer $renderer, | |
| 53 | Tag $prevTag | |
| 54 | ): void { | |
| 33 | 55 | $renderer->renderTagItem( |
| 34 | 56 | $this->name, |
| 35 | 57 | $this->sha, |
| 36 | 58 | $this->targetSha, |
| 37 | $prevTag ? $prevTag->targetSha : null, | |
| 59 | $prevTag->targetSha, | |
| 38 | 60 | $this->timestamp, |
| 39 | 61 | $this->message, |
| 40 | 62 | $this->author |
| 41 | 63 | ); |
| 64 | } | |
| 65 | } | |
| 66 | ||
| 67 | class MissingTag extends Tag { | |
| 68 | public function __construct() { | |
| 69 | parent::__construct( '', '', '' ); | |
| 42 | 70 | } |
| 43 | 71 | } |
| 5 | 5 | string $sha, |
| 6 | 6 | string $targetSha, |
| 7 | ?string $prevTargetSha, | |
| 7 | string $prevTargetSha, | |
| 8 | 8 | int $timestamp, |
| 9 | 9 | string $message, |