Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/treetrek.git
<?php
require_once __DIR__ . '/GitPacks.php';
require_once __DIR__ . '/LooseObjects.php';

class PackfileWriter {
  private GitPacks     $packs;
  private LooseObjects $loose;

  public function __construct(
    GitPacks $packs,
    LooseObjects $loose
  ) {
    $this->packs = $packs;
    $this->loose = $loose;
  }

  public function generate( array $objs ): Generator {
    $entries = $this->buildEntries( $objs );
    $ctx     = \hash_init( 'sha1' );
    $head    = "PACK"
      . \pack( 'N', 2 )
      . \pack( 'N', \count( $objs ) );

    \hash_update( $ctx, $head );
    yield $head;

    $written = [];
    $outPos  = 12;

    foreach( $entries as $sha => $entry ) {
      $written[$sha] = $outPos;
      $baseSha       = $entry['baseSha'];
      $reuse         = $baseSha !== ''
        && isset( $written[$baseSha] );

      $hdr = $reuse
        ? $this->encodeEntryHeader( 6, $entry['deltaSize'] )
          . $this->encodeOffsetDelta( $outPos - $written[$baseSha] )
        : $this->encodeEntryHeader(
            $entry['logicalType'],
            $entry['size'] > 0
              ? $entry['size']
              : $this->getObjectSize( $sha )
          );

      \hash_update( $ctx, $hdr );
      $outPos += \strlen( $hdr );
      yield $hdr;

      $stream = $reuse
        ? $this->packs->streamRawDelta( $sha )
        : $this->streamCompressed( $sha );

      foreach( $stream as $chunk ) {
        \hash_update( $ctx, $chunk );
        $outPos += \strlen( $chunk );
        yield $chunk;
      }
    }

    yield \hash_final( $ctx, true );
  }

  private function buildEntries( array $objs ): array {
    $entries  = [];
    $offToSha = [];

    foreach( $objs as $sha => $logicalType ) {
      $meta = $this->packs->getEntryMeta( $sha );

      $entries[$sha] = [
        'logicalType' => $logicalType,
        'packType'    => $meta['type'],
        'deltaSize'   => $meta['size'],
        'packFile'    => $meta['file'],
        'offset'      => $meta['offset'],
        'baseOffset'  => $meta['baseOffset'] ?? 0,
        'baseSha'     => $meta['baseSha'] ?? '',
      ];

      if( $meta['file'] !== '' ) {
        $offToSha[$meta['file']][$meta['offset']] = $sha;
      }
    }

    foreach( $entries as &$e ) {
      $e['baseSha'] = $e['packType'] === 6 && $e['baseOffset'] > 0
        ? ( $offToSha[$e['packFile']][$e['baseOffset']] ?? '' )
        : $e['baseSha'];
    }

    unset( $e );

    $files   = [];
    $offsets = [];

    foreach( $entries as $e ) {
      $files[]   = $e['packFile'];
      $offsets[] = $e['offset'];
    }

    \array_multisort(
      $files, \SORT_ASC, \SORT_STRING,
      $offsets, \SORT_ASC, \SORT_NUMERIC,
      $entries
    );

    foreach( $entries as $sha => &$e ) {
      $e['size'] = $e['baseSha'] === ''
        ? $this->getObjectSize( $sha )
        : 0;
    }

    unset( $e );

    return $entries;
  }

  private function getObjectSize( string $sha ): int {
    return $this->packs->getSize( $sha )
      ?: $this->loose->getSize( $sha );
  }

  private function streamCompressed( string $sha ): Generator {
    $generator = $this->packs->streamRawCompressed( $sha );
    $generator->rewind();

    if( $generator->valid() ) {
      yield from $generator;
    } else {
      $deflate = \deflate_init( \ZLIB_ENCODING_DEFLATE );

      foreach( $this->getDecompressedChunks( $sha ) as $raw ) {
        $compressed = \deflate_add(
          $deflate, $raw, \ZLIB_NO_FLUSH
        );

        if( $compressed !== '' ) {
          yield $compressed;
        }
      }

      $final = \deflate_add( $deflate, '', \ZLIB_FINISH );

      if( $final !== '' ) {
        yield $final;
      }
    }
  }

  private function getDecompressedChunks( string $sha ): Generator {
    $looseGen = $this->loose->streamChunks( $sha );
    $looseGen->rewind();

    if( $looseGen->valid() ) {
      yield from $looseGen;
    } else {
      $packGen = $this->packs->streamGenerator( $sha );
      $packGen->rewind();

      if( $packGen->valid() ) {
        yield from $packGen;
      } else {
        $data = $this->packs->read( $sha );

        if( $data !== '' ) {
          yield $data;
        }
      }
    }
  }

  private function encodeEntryHeader( int $type, int $size ): string {
    $byte = $type << 4 | $size & 0x0f;
    $sz   = $size >> 4;
    $hdr  = '';

    while( $sz > 0 ) {
      $hdr  .= \chr( $byte | 0x80 );
      $byte  = $sz & 0x7f;
      $sz  >>= 7;
    }

    return $hdr . \chr( $byte );
  }

  private function encodeOffsetDelta( int $offset ): string {
    $buf = \chr( $offset & 0x7F );
    $n   = $offset >> 7;

    while( $n > 0 ) {
      $n--;
      $buf = \chr( 0x80 | $n & 0x7F ) . $buf;
      $n >>= 7;
    }

    return $buf;
  }
}