Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/treetrek.git
M git/CompressionStream.php
22
require_once __DIR__ . '/StreamReader.php';
33
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
}
1810
19
  public static function createExtractor(): self {
11
class ZlibExtractorStream implements CompressionStream {
12
  public function stream(
13
    StreamReader $stream,
14
    int $chunkSize = 8192
15
  ): Generator {
2016
    $context = \inflate_init( \ZLIB_ENCODING_DEFLATE );
17
    $done    = false;
2118
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 ) {
2424
        $before = \inflate_get_read_len( $context );
2525
        @\inflate_add( $context, $chunk );
2626
27
        return \substr(
27
        $data = \substr(
2828
          $chunk,
2929
          0,
3030
          \inflate_get_read_len( $context ) - $before
3131
        );
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 );
6632
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
        }
7136
72
        return $data === false ? '' : $data;
73
      },
74
      function(): bool {
75
        return false;
37
        $done = \inflate_get_status( $context ) === \ZLIB_STREAM_END;
7638
      }
77
    );
39
    }
7840
  }
41
}
7942
43
class ZlibInflaterStream implements CompressionStream {
8044
  public function stream(
8145
    StreamReader $stream,
8246
    int $chunkSize = 8192
8347
  ): Generator {
84
    $done = false;
48
    $context = \inflate_init( \ZLIB_ENCODING_DEFLATE );
49
    $done    = false;
8550
8651
    while( !$done && !$stream->eof() ) {
8752
      $chunk = $stream->read( $chunkSize );
8853
      $done  = $chunk === '';
8954
9055
      if( !$done ) {
91
        $data = $this->pump( $chunk );
56
        $data = @\inflate_add( $context, $chunk );
9257
93
        if( $data !== '' ) {
58
        if( $data !== false && $data !== '' ) {
9459
          yield $data;
9560
        }
9661
97
        $done = $this->finished();
62
        $done = \inflate_get_status( $context ) === \ZLIB_STREAM_END;
9863
      }
9964
    }
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)();
11265
  }
11366
}
M git/DeltaDecoder.php
1717
  ];
1818
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 {
2024
    $pos = 0;
25
2126
    $this->readDeltaSize( $delta, $pos );
2227
    $this->readDeltaSize( $delta, $pos );
2328
2429
    $chunks = [];
25
    $len    = strlen( $delta );
30
    $len    = \strlen( $delta );
2631
    $outLen = 0;
27
28
    while( $pos < $len ) {
29
      if( $cap > 0 && $outLen >= $cap ) break;
3032
31
      $op = ord( $delta[$pos++] );
33
    while( $pos < $len && ( $cap === 0 || $outLen < $cap ) ) {
34
      $op = \ord( $delta[$pos++] );
3235
3336
      if( $op & 128 ) {
34
        $off = $ln = 0;
37
        $off = 0;
38
        $ln  = 0;
3539
36
        $this->parseCopyInstruction( $op, $delta, $pos, $off, $ln );
40
        $this->parseCopyInstruction(
41
          $op, $delta, $pos, $off, $ln
42
        );
3743
38
        $chunks[] = substr( $base, $off, $ln );
44
        $chunks[] = \substr( $base, $off, $ln );
3945
        $outLen  += $ln;
4046
      } else {
4147
        $ln       = $op & 127;
42
        $chunks[] = substr( $delta, $pos, $ln );
48
        $chunks[] = \substr( $delta, $pos, $ln );
4349
        $outLen  += $ln;
4450
        $pos     += $ln;
4551
      }
4652
    }
4753
48
    $result = implode( '', $chunks );
54
    $result = \implode( '', $chunks );
4955
50
    return $cap > 0 && strlen( $result ) > $cap
51
      ? substr( $result, 0, $cap )
56
    return $cap > 0 && \strlen( $result ) > $cap
57
      ? \substr( $result, 0, $cap )
5258
      : $result;
5359
  }
5460
5561
  public function applyStreamGenerator(
5662
    StreamReader $handle,
5763
    mixed $base
5864
  ): Generator {
59
    $stream      = CompressionStream::createInflater();
65
    $stream      = new ZlibInflaterStream();
6066
    $state       = 0;
6167
    $buffer      = '';
...
7783
7884
          while( $pos < $bufLen ) {
79
            if( !(ord( $buffer[$pos] ) & 128) ) {
85
            if( !(\ord( $buffer[$pos] ) & 128) ) {
8086
              $found = true;
8187
              $pos++;
...
9399
          }
94100
        } else {
95
          $op = ord( $buffer[$offset] );
101
          $op = \ord( $buffer[$offset] );
96102
97103
          if( $op & 128 ) {
98104
            $need = self::COPY_INSTRUCTION_SIZES[$op & 0x7F];
99105
100106
            if( $len < 1 + $need ) {
101107
              break;
102108
            }
103109
104
            $off = $ln = 0;
110
            $off = 0;
111
            $ln  = 0;
105112
            $ptr = $offset + 1;
106113
107
            $this->parseCopyInstruction( $op, $buffer, $ptr, $off, $ln );
114
            $this->parseCopyInstruction(
115
              $op, $buffer, $ptr, $off, $ln
116
            );
108117
109118
            if( $isStream ) {
110119
              $base->seek( $off );
111120
              $rem = $ln;
112121
113122
              while( $rem > 0 ) {
114
                $slc = $base->read( min( self::CHUNK_SIZE, $rem ) );
123
                $slc = $base->read(
124
                  \min( self::CHUNK_SIZE, $rem )
125
                );
115126
116127
                if( $slc === '' ) {
117128
                  $rem = 0;
118129
                } else {
119
                  $slcLen       = strlen( $slc );
130
                  $slcLen       = \strlen( $slc );
120131
                  $yieldBuffer .= $slc;
121132
                  $yieldBufLen += $slcLen;
...
151162
            }
152163
153
            $yieldBuffer .= substr( $buffer, $offset + 1, $ln );
164
            $yieldBuffer .= \substr( $buffer, $offset + 1, $ln );
154165
            $yieldBufLen += $ln;
155166
            $offset      += 1 + $ln;
...
176187
  }
177188
178
  public function readDeltaTargetSize( StreamReader $handle, int $type ): int {
189
  public function readDeltaTargetSize(
190
    StreamReader $handle,
191
    int $type
192
  ): int {
179193
    if( $type === 6 ) {
180
      $byte = ord( $handle->read( 1 ) );
194
      $byte = \ord( $handle->read( 1 ) );
181195
182196
      while( $byte & 128 ) {
183
        $byte = ord( $handle->read( 1 ) );
197
        $byte = \ord( $handle->read( 1 ) );
184198
      }
185199
    } else {
186200
      $handle->seek( 20, SEEK_CUR );
187201
    }
188202
189203
    $head = $this->readInflatedHead( $handle );
190
191
    if( strlen( $head ) === 0 ) return 0;
204
    $pos  = 0;
192205
193
    $pos = 0;
194
    $this->readDeltaSize( $head, $pos );
206
    if( \strlen( $head ) > 0 ) {
207
      $this->readDeltaSize( $head, $pos );
208
    }
195209
196
    return $this->readDeltaSize( $head, $pos );
210
    return \strlen( $head ) > 0
211
      ? $this->readDeltaSize( $head, $pos )
212
      : 0;
197213
  }
198214
199
  public function readDeltaBaseSize( StreamReader $handle ): int {
215
  public function readDeltaBaseSize(
216
    StreamReader $handle
217
  ): int {
200218
    $head = $this->readInflatedHead( $handle );
201
202
    if( strlen( $head ) === 0 ) return 0;
203
204
    $pos = 0;
219
    $pos  = 0;
205220
206
    return $this->readDeltaSize( $head, $pos );
221
    return \strlen( $head ) > 0
222
      ? $this->readDeltaSize( $head, $pos )
223
      : 0;
207224
  }
208225
209
  private function readInflatedHead( StreamReader $handle ): string {
210
    $stream = CompressionStream::createInflater();
226
  private function readInflatedHead(
227
    StreamReader $handle
228
  ): string {
229
    $stream = new ZlibInflaterStream();
211230
    $head   = '';
212231
    $try    = 0;
213232
214233
    foreach( $stream->stream( $handle, 512 ) as $out ) {
215234
      $head .= $out;
216235
      $try++;
217236
218
      if( strlen( $head ) >= 32 || $try >= 64 ) {
237
      if( \strlen( $head ) >= 32 || $try >= 64 ) {
219238
        break;
220239
      }
...
234253
    $len = 0;
235254
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;
240259
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;
244263
245264
    $len = $len === 0 ? 0x10000 : $len;
246265
  }
247266
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 );
250272
    $val   = 0;
251273
    $shift = 0;
252274
    $done  = false;
253275
254276
    while( !$done && $pos < $len ) {
255
      $byte   = ord( $data[$pos++] );
277
      $byte   = \ord( $data[$pos++] );
256278
      $val   |= ($byte & 0x7F) << $shift;
257279
      $done   = !($byte & 0x80);
M git/Git.php
2121
  }
2222
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, '' );
727450
  }
728451
A git/GitPackStream.php
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
}
189
M git/PackEntryReader.php
55
require_once __DIR__ . '/BufferedReader.php';
66
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();
484377
    $chunks   = [];
485378
    $len      = 0;
M model/Commit.php
1111
  private string $parentSha;
1212
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
    }
2743
  }
2844
M model/Tag.php
66
  private string $sha;
77
  private string $targetSha;
8
  private int $timestamp;
8
  private int    $timestamp;
99
  private string $message;
1010
  private string $author;
1111
1212
  public function __construct(
1313
    string $name,
1414
    string $sha,
15
    string $targetSha,
16
    int $timestamp,
17
    string $message,
18
    string $author
15
    string $rawData
1916
  ) {
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
      : '';
2645
  }
2746
2847
  public function compare( Tag $other ): int {
2948
    return $other->timestamp <=> $this->timestamp;
3049
  }
3150
32
  public function render( TagRenderer $renderer, ?Tag $prevTag = null ): void {
51
  public function render(
52
    TagRenderer $renderer,
53
    Tag $prevTag
54
  ): void {
3355
    $renderer->renderTagItem(
3456
      $this->name,
3557
      $this->sha,
3658
      $this->targetSha,
37
      $prevTag ? $prevTag->targetSha : null,
59
      $prevTag->targetSha,
3860
      $this->timestamp,
3961
      $this->message,
4062
      $this->author
4163
    );
64
  }
65
}
66
67
class MissingTag extends Tag {
68
  public function __construct() {
69
    parent::__construct( '', '', '' );
4270
  }
4371
}
M render/TagRenderer.php
55
    string $sha,
66
    string $targetSha,
7
    ?string $prevTargetSha,
7
    string $prevTargetSha,
88
    int $timestamp,
99
    string $message,