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] );

      if( $reuse ) {
        $hdr  = $this->encodeEntryHeader(
          6, $entry['deltaSize']
        );
        $hdr .= $this->encodeOffsetDelta(
          $outPos - $written[$baseSha]
        );

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

        foreach(
          $this->packs->streamRawDelta(
            $sha
          ) as $chunk
        ) {
          \hash_update( $ctx, $chunk );
          $outPos += \strlen( $chunk );
          yield $chunk;
        }
      } else {
        $size = $this->getObjectSize( $sha );
        $hdr  = $this->encodeEntryHeader(
          $entry['logicalType'], $size
        );

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

        foreach(
          $this->streamCompressed(
            $sha
          ) 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 ) {
      if(
        $e['packType'] === 6
        && $e['baseOffset'] > 0
      ) {
        $e['baseSha']
          = $offToSha[$e['packFile']][$e['baseOffset']]
            ?? '';
      }
    }

    unset( $e );

    \uasort(
      $entries,
      function( array $a, array $b ): int {
        $cmp = $a['packFile'] <=> $b['packFile'];

        return $cmp !== 0
          ? $cmp
          : $a['offset'] <=> $b['offset'];
      }
    );

    return $entries;
  }

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

  private function streamCompressed(
    string $sha
  ): Generator {
    $yielded = false;

    foreach(
      $this->packs->streamRawCompressed(
        $sha
      ) as $chunk
    ) {
      $yielded = true;
      yield $chunk;
    }

    if( !$yielded ) {
      $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 {
    $any = false;

    foreach(
      $this->loose->streamChunks( $sha ) as $chunk
    ) {
      $any = true;
      yield $chunk;
    }

    if( !$any ) {
      foreach(
        $this->packs->streamGenerator(
          $sha
        ) as $chunk
      ) {
        $any = true;
        yield $chunk;
      }
    }

    if( !$any ) {
      $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;
    }

    $hdr .= \chr( $byte );

    return $hdr;
  }

  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;
  }
}