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