Dave Jarvis' Repositories

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

class Git {
  private const CHUNK_SIZE  = 128;
  private const MAX_READ    = 16777216;
  private const MODE_TREE   = '40000';
  private const MODE_TREE_A = '040000';

  private string $path;
  private string $objPath;

  public function __construct( string $repoPath ) {
    $this->path = rtrim( $repoPath, '/' );
    $this->objPath = $this->path . '/objects';
  }

  public function getObjectSize( string $sha ): int {
    $prefix = substr( $sha, 0, 2 );
    $suffix = substr( $sha, 2 );
    $loosePath = "{$this->objPath}/{$prefix}/{$suffix}";

    $size = file_exists( $loosePath )
      ? $this->getLooseObjectSize( $loosePath )
      : $this->getPackedObjectSize( $sha );

    return $size;
  }

  private function getLooseObjectSize( string $path ): int {
    $size = 0;
    $fileHandle = @fopen( $path, 'rb' );

    if( $fileHandle ) {
      $data = $this->decompressHeader( $fileHandle );
      $header = explode( "\0", $data, 2 )[0];
      $parts = explode( ' ', $header );
      $size = isset( $parts[1] ) ? (int)$parts[1] : 0;
      fclose( $fileHandle );
    }

    return $size;
  }

  private function decompressHeader( $fileHandle ): string {
    $data = '';
    $inflateContext = inflate_init( ZLIB_ENCODING_DEFLATE );

    while( !feof( $fileHandle ) ) {
      $chunk = fread( $fileHandle, self::CHUNK_SIZE );
      $inflated = @inflate_add( $inflateContext, $chunk, ZLIB_NO_FLUSH );

      if( $inflated === false ) {
        break;
      }

      $data .= $inflated;

      if( strpos( $data, "\0" ) !== false ) {
        break;
      }
    }

    return $data;
  }

  private function getPackedObjectSize( string $sha ): int {
    $info = $this->getPackOffset( $sha );

    $size = ($info['offset'] !== -1)
      ? $this->extractPackedSize( $info )
      : 0;

    return $size;
  }

  private function extractPackedSize( array $info ): int {
    $targetSize = 0;
    $packFile = @fopen( $info['file'], 'rb' );

    if( $packFile ) {
      fseek( $packFile, $info['offset'] );
      $header = $this->readVarInt( $packFile );
      $type = ($header['byte'] >> 4) & 7;

      $targetSize = ($type === 6 || $type === 7)
        ? $this->readDeltaTargetSize( $packFile, $type )
        : $header['value'];

      fclose( $packFile );
    }

    return $targetSize;
  }

  private function readVarInt( $fileHandle ): array {
    $byte = ord( fread( $fileHandle, 1 ) );
    $value = $byte & 15;
    $shift = 4;
    $firstByte = $byte;

    while( $byte & 128 ) {
      $byte = ord( fread( $fileHandle, 1 ) );
      $value |= (($byte & 127) << $shift);
      $shift += 7;
    }

    return ['value' => $value, 'byte' => $firstByte];
  }

  private function readDeltaTargetSize( $fileHandle, int $type ): int {
    $dummy = ($type === 6)
      ? $this->skipOffsetDelta( $fileHandle )
      : fread( $fileHandle, 20 );

    $inflateContext = inflate_init( ZLIB_ENCODING_DEFLATE );
    $headerData = '';

    while( !feof( $fileHandle ) && strlen( $headerData ) < 32 ) {
      $inflated = @inflate_add(
        $inflateContext,
        fread( $fileHandle, 512 ),
        ZLIB_NO_FLUSH
      );

      if( $inflated !== false ) {
        $headerData .= $inflated;
      }
    }

    $result = 0;
    $position = 0;

    if( strlen( $headerData ) > 0 ) {
      $this->skipSize( $headerData, $position );
      $result = $this->readSize( $headerData, $position );
    }

    return $result;
  }

  public function getMainBranch(): array {
    $result = ['name' => '', 'hash' => ''];
    $branches = [];
    $this->eachBranch( function( $name, $sha ) use( &$branches ) {
      $branches[$name] = $sha;
    } );

    foreach( ['main', 'master', 'trunk', 'develop'] as $branch ) {
      if( isset( $branches[$branch] ) ) {
        $result = ['name' => $branch, 'hash' => $branches[$branch]];
        break;
      }
    }

    if( $result['name'] === '' ) {
      $firstKey = array_key_first( $branches );

      if( $firstKey !== null ) {
        $result = ['name' => $firstKey, 'hash' => $branches[$firstKey]];
      }
    }

    return $result;
  }

  public function eachBranch( callable $callback ): void {
    $this->scanRefs( 'refs/heads', $callback );
  }

  public function eachTag( callable $callback ): void {
    $this->scanRefs( 'refs/tags', $callback );
  }

  public function walk( string $refOrSha, callable $callback ): void {
    $sha = $this->resolve( $refOrSha );
    $data = ($sha !== '') ? $this->read( $sha ) : '';

    if( preg_match( '/^tree ([0-9a-f]{40})$/m', $data, $matches ) ) {
      $data = $this->read( $matches[1] );
    }

    if( $this->isTreeData( $data ) ) {
      $this->processTree( $data, $callback );
    }
  }

  private function processTree( string $data, callable $callback ): void {
    $position = 0;

    while( $position < strlen( $data ) ) {
      $spacePos = strpos( $data, ' ', $position );
      $nullPos = strpos( $data, "\0", $spacePos );

      if( $spacePos === false || $nullPos === false ) {
        break;
      }

      $mode = substr( $data, $position, $spacePos - $position );
      $name = substr( $data, $spacePos + 1, $nullPos - $spacePos - 1 );
      $entrySha = bin2hex( substr( $data, $nullPos + 1, 20 ) );

      $isDir = ($mode === self::MODE_TREE || $mode === self::MODE_TREE_A);
      $size = $isDir ? 0 : $this->getObjectSize( $entrySha );

      $callback( new File( $name, $entrySha, $mode, 0, $size ) );
      $position = $nullPos + 21;
    }
  }

  private function isTreeData( string $data ): bool {
    $result = false;
    $pattern = '/^(40000|100644|100755|120000|160000) /';

    if( strlen( $data ) >= 25 && preg_match( $pattern, $data ) ) {
      $nullPos = strpos( $data, "\0" );
      $result = ($nullPos !== false && ($nullPos + 21 <= strlen( $data )));
    }

    return $result;
  }

  public function history( string $ref, int $limit, callable $cb ): void {
    $currentSha = $this->resolve( $ref );
    $count = 0;

    while( $currentSha !== '' && $count < $limit ) {
      $data = $this->read( $currentSha );

      if( $data === '' ) {
        break;
      }

      $pos = strpos( $data, "\n\n" );
      $message = ($pos !== false) ? substr( $data, $pos + 2 ) : '';
      preg_match( '/^author (.*) <(.*)> (\d+)/m', $data, $m );

      $cb( (object)[
        'sha'     => $currentSha,
        'message' => trim( $message ),
        'author'  => $m[1] ?? 'Unknown',
        'email'   => $m[2] ?? '',
        'date'    => (int)($m[3] ?? 0)
      ] );

      $currentSha = preg_match( '/^parent ([0-9a-f]{40})$/m', $data, $ms )
        ? $ms[1] : '';
      $count++;
    }
  }

  public function stream( string $sha, callable $callback ): void {
    $data = $this->read( $sha );

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

  public function resolve( string $input ): string {
    $result = '';

    if( preg_match( '/^[0-9a-f]{40}$/', $input ) ) {
      $result = $input;
    } elseif( $input === 'HEAD' &&
              file_exists( $headFile = "{$this->path}/HEAD" ) ) {
      $head = trim( file_get_contents( $headFile ) );
      $result = (strpos( $head, 'ref: ' ) === 0)
        ? $this->resolve( substr( $head, 5 ) ) : $head;
    } else {
      $result = $this->resolveRef( $input );
    }

    return $result;
  }

  private function resolveRef( string $input ): string {
    $found = '';
    $refPaths = [$input, "refs/heads/$input", "refs/tags/$input"];

    foreach( $refPaths as $path ) {
      if( file_exists( $filePath = "{$this->path}/$path" ) ) {
        $found = trim( file_get_contents( $filePath ) );
        break;
      }
    }

    if( $found === '' &&
        file_exists( $packed = "{$this->path}/packed-refs" ) ) {
      $found = $this->findInPackedRefs( $packed, $input );
    }

    return $found;
  }

  private function findInPackedRefs( string $path, string $input ): string {
    $result = '';
    $targets = [$input, "refs/heads/$input", "refs/tags/$input"];

    foreach( file( $path ) as $line ) {
      if( $line[0] === '#' || $line[0] === '^' ) {
        continue;
      }

      $parts = explode( ' ', trim( $line ) );

      if( count( $parts ) >= 2 && in_array( $parts[1], $targets ) ) {
        $result = $parts[0];
        break;
      }
    }

    return $result;
  }

  public function read( string $sha ): string {
    $result = '';
    $prefix = substr( $sha, 0, 2 );
    $suffix = substr( $sha, 2 );
    $loose = "{$this->objPath}/{$prefix}/{$suffix}";

    if( file_exists( $loose ) ) {
      $raw = file_get_contents( $loose );
      $inflated = $raw ? @gzuncompress( $raw ) : false;
      $result = $inflated ? explode( "\0", $inflated, 2 )[1] : '';
    } else {
      $result = $this->fromPack( $sha );
    }

    return $result;
  }

  private function fromPack( string $sha ): string {
    $info = $this->getPackOffset( $sha );
    $result = '';

    if( $info['offset'] !== -1 ) {
      $packFile = @fopen( $info['file'], 'rb' );

      if( $packFile ) {
        $result = $this->readPackEntry( $packFile, $info['offset'] );
        fclose( $packFile );
      }
    }

    return $result;
  }

  private function getPackOffset( string $sha ): array {
    $result = ['file' => '', 'offset' => -1];

    if( strlen( $sha ) === 40 && ctype_xdigit( $sha ) ) {
      $binSha = hex2bin( $sha );
      $packs = glob( "{$this->objPath}/pack/*.idx" );

      foreach( (array)$packs as $idxFile ) {
        $offset = $this->findInPack( $idxFile, $binSha );

        if( $offset !== -1 ) {
          $result = [
            'file'   => str_replace( '.idx', '.pack', $idxFile ),
            'offset' => $offset
          ];
          break;
        }
      }
    }

    return $result;
  }

  private function findInPack( string $idxFile, string $binSha ): int {
    $offset = -1;
    $fileHandle = @fopen( $idxFile, 'rb' );

    if( $fileHandle ) {
      $range = $this->getFanoutRange( $fileHandle, ord( $binSha[0] ) );

      if( $range['end'] > $range['start'] ) {
        $total = $this->getTotalObjects( $fileHandle );
        $foundIdx = $this->searchShaTable(
          $fileHandle,
          $range['start'],
          $range['end'],
          $binSha
        );

        if( $foundIdx !== -1 ) {
          $offset = $this->getOffsetFromTable(
            $fileHandle,
            $foundIdx,
            $total
          );
        }
      }

      fclose( $fileHandle );
    }

    return $offset;
  }

  private function getFanoutRange( $fileHandle, int $firstByte ): array {
    $range = ['start' => 0, 'end' => 0];
    fseek( $fileHandle, 0 );

    if( fread( $fileHandle, 8 ) === "\377tOc\0\0\0\2" ) {
      fseek( $fileHandle, 8 + ($firstByte * 4) );
      $range['end'] = unpack( 'N', fread( $fileHandle, 4 ) )[1];

      if( $firstByte > 0 ) {
        fseek( $fileHandle, 8 + (($firstByte - 1) * 4) );
        $range['start'] = unpack( 'N', fread( $fileHandle, 4 ) )[1];
      }
    }

    return $range;
  }

  private function getTotalObjects( $fileHandle ): int {
    fseek( $fileHandle, 1028 );
    $data = fread( $fileHandle, 4 );

    return $data ? unpack( 'N', $data )[1] : 0;
  }

  private function searchShaTable(
    $fileHandle,
    int $start,
    int $end,
    string $binSha
  ): int {
    $result = -1;
    fseek( $fileHandle, 1032 + ($start * 20) );

    for( $i = $start; $i < $end; $i++ ) {
      if( fread( $fileHandle, 20 ) === $binSha ) {
        $result = $i;
        break;
      }
    }

    return $result;
  }

  private function getOffsetFromTable( $fileHandle, int $idx, int $total ): int {
    $pos = 1032 + ($total * 20) + ($total * 4) + ($idx * 4);
    fseek( $fileHandle, $pos );
    $data = fread( $fileHandle, 4 );
    $offset = $data ? unpack( 'N', $data )[1] : 0;

    if( $offset & 0x80000000 ) {
      $base = 1032 + ($total * 24) + ($total * 4);
      fseek( $fileHandle, $base + (($offset & 0x7FFFFFFF) * 8) );
      $data64 = fread( $fileHandle, 8 );
      $offset = $data64 ? unpack( 'J', $data64 )[1] : 0;
    }

    return (int)$offset;
  }

  private function readPackEntry( $fileHandle, int $offset ): string {
    fseek( $fileHandle, $offset );
    $header = $this->readVarInt( $fileHandle );
    $type = ($header['byte'] >> 4) & 7;

    if( $type === 6 ) return $this->handleOfsDelta( $fileHandle, $offset );
    if( $type === 7 ) return $this->handleRefDelta( $fileHandle );

    $inf = inflate_init( ZLIB_ENCODING_DEFLATE );
    $res = '';

    while( !feof( $fileHandle ) ) {
        $chunk = fread( $fileHandle, 8192 );
        $data = @inflate_add( $inf, $chunk );

        if( $data !== false ) $res .= $data;
        if( $data === false || ($inf && inflate_get_status( $inf ) === ZLIB_STREAM_END) ) break;
    }

    return $res;
  }

  private function deltaCopy(
    string $base, string $delta, int &$position, int $opcode
  ): string {
    $offset = 0;
    $length = 0;

    if( $opcode & 0x01 ) $offset |= ord( $delta[$position++] );
    if( $opcode & 0x02 ) $offset |= ord( $delta[$position++] ) << 8;
    if( $opcode & 0x04 ) $offset |= ord( $delta[$position++] ) << 16;
    if( $opcode & 0x08 ) $offset |= ord( $delta[$position++] ) << 24;

    if( $opcode & 0x10 ) $length |= ord( $delta[$position++] );
    if( $opcode & 0x20 ) $length |= ord( $delta[$position++] ) << 8;
    if( $opcode & 0x40 ) $length |= ord( $delta[$position++] ) << 16;

    if( $length === 0 ) $length = 0x10000;

    return substr( $base, $offset, $length );
  }

  private function handleOfsDelta( $fileHandle, int $offset ): string {
    $byte = ord( fread( $fileHandle, 1 ) );
    $negOffset = $byte & 127;

    while( $byte & 128 ) {
      $byte = ord( fread( $fileHandle, 1 ) );
      $negOffset = (($negOffset + 1) << 7) | ($byte & 127);
    }

    // Capture the current position (start of delta data)
    $currentPos = ftell( $fileHandle );

    // Recursive call moves the file pointer
    $base = $this->readPackEntry( $fileHandle, $offset - $negOffset );

    // Restore the position to read the delta data
    fseek( $fileHandle, $currentPos );

    $delta = @gzuncompress( fread( $fileHandle, self::MAX_READ ) ) ?: '';

    return $this->applyDelta( $base, $delta );
  }

  private function handleRefDelta( $fileHandle ): string {
    $base = $this->read( bin2hex( fread( $fileHandle, 20 ) ) );
    $delta = @gzuncompress( fread( $fileHandle, self::MAX_READ ) ) ?: '';

    return $this->applyDelta( $base, $delta );
  }

  private function applyDelta( string $base, string $delta ): string {
    $out = '';

    if( $base !== '' && $delta !== '' ) {
      $position = 0;
      $this->skipSize( $delta, $position );
      $this->skipSize( $delta, $position );

      while( $position < strlen( $delta ) ) {
        $opcode = ord( $delta[$position++] );

        if( $opcode & 128 ) {
          $out .= $this->deltaCopy( $base, $delta, $position, $opcode );
        } else {
          $len = $opcode & 127;
          $out .= substr( $delta, $position, $len );
          $position += $len;
        }
      }
    }

    return $out;
  }

  private function skipSize( string $data, int &$position ): void {
    while( ord( $data[$position++] ) & 128 ) {
      // Intentionally empty
    }
  }

  private function readSize( string $data, int &$position ): int {
    $byte = ord( $data[$position++] );
    $value = $byte & 127;
    $shift = 7;

    while( $byte & 128 ) {
      $byte = ord( $data[$position++] );
      $value |= (($byte & 127) << $shift);
      $shift += 7;
    }

    return $value;
  }

  private function skipOffsetDelta( $fileHandle ): void {
    $byte = ord( fread( $fileHandle, 1 ) );

    while( $byte & 128 ) {
      $byte = ord( fread( $fileHandle, 1 ) );
    }
  }

  private function scanRefs( string $prefix, callable $callback ): void {
    $directory = "{$this->path}/$prefix";

    if( is_dir( $directory ) ) {
      foreach( array_diff( scandir( $directory ), ['.', '..'] ) as $fileName ) {
        $content = file_get_contents( "$directory/$fileName" );
        $callback( $fileName, trim( $content ) );
      }
    }
  }
}