Dave Jarvis' Repositories

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

class ClonePage implements Page {
  private $git;
  private $subPath;

  public function __construct( Git $git, string $subPath ) {
    $this->git     = $git;
    $this->subPath = $subPath;
  }

  public function render(): void {
    $path = $this->subPath;

    if( $path === '' ) {
      $this->redirectBrowser();
    } elseif( \str_ends_with( $path, 'info/refs' ) ) {
      $this->renderInfoRefs();
    } elseif( \str_ends_with( $path, 'git-upload-pack' ) ) {
      $this->handleUploadPack();
    } elseif( \str_ends_with( $path, 'git-receive-pack' ) ) {
      \http_response_code( 403 );
      echo "Read-only repository.";
    } elseif( $path === 'HEAD' ) {
      $this->serve( 'HEAD', 'text/plain' );
    } elseif( \strpos( $path, 'objects/' ) === 0 ) {
      $this->serve( $path, 'application/x-git-object' );
    } else {
      \http_response_code( 404 );
      echo "Not Found";
    }

    exit;
  }

  private function redirectBrowser(): void {
    \header(
      "Location: " . \str_replace( '.git', '', $_SERVER['REQUEST_URI'] )
    );
  }

  private function renderInfoRefs(): void {
    if( ( $_GET['service'] ?? '' ) === 'git-upload-pack' ) {
      \header( 'Content-Type: application/x-git-upload-pack-advertisement' );
      \header( 'Cache-Control: no-cache' );

      $this->packetWrite( "# service=git-upload-pack\n" );
      $this->packetFlush();

      $refs = [];

      $this->git->eachRef( function( $ref, $sha ) use ( &$refs ) {
        $refs[] = [ 'ref' => $ref, 'sha' => $sha ];
      } );

      $caps = "multi_ack_detailed thin-pack side-band side-band-64k " .
              "ofs-delta shallow no-progress include-tag";

      if( empty( $refs ) ) {
        $this->packetWrite(
          "0000000000000000000000000000000000000000 capabilities^{}\0" .
          $caps . "\n"
        );
      } else {
        $this->packetWrite(
          $refs[0]['sha'] . " " . $refs[0]['ref'] . "\0" . $caps . "\n"
        );

        for( $i = 1; $i < \count( $refs ); $i++ ) {
          $this->packetWrite(
            $refs[$i]['sha'] . " " . $refs[$i]['ref'] . "\n"
          );
        }
      }

      $this->packetFlush();
    } else {
      \header( 'Content-Type: text/plain' );

      if( !$this->git->streamRaw( 'info/refs' ) ) {
        $this->git->eachRef( function( $ref, $sha ) {
          echo "$sha\t$ref\n";
        } );
      }
    }
  }

  private function handleUploadPack(): void {
    \set_time_limit( 0 );
    \header( 'Content-Type: application/x-git-upload-pack-result' );
    \header( 'Cache-Control: no-cache' );

    $wants  = [];
    $haves  = [];
    $handle = \fopen( 'php://input', 'rb' );

    if( $handle ) {
      if( isset( $_SERVER['HTTP_CONTENT_ENCODING'] ) &&
          $_SERVER['HTTP_CONTENT_ENCODING'] === 'gzip' ) {
        \stream_filter_append(
          $handle,
          'zlib.inflate',
          \STREAM_FILTER_READ,
          [ 'window' => 31 ]
        );
      }

      while( !\feof( $handle ) ) {
        $lenHex = \fread( $handle, 4 );
        $len    = \strlen( $lenHex ) === 4 ? \hexdec( $lenHex ) : 0;

        if( $len === 0 ) {
          break;
        }

        if( $len > 4 ) {
          $trim = \trim( \fread( $handle, $len - 4 ) );

          if( \strpos( $trim, 'want ' ) === 0 ) {
            $wants[] = \explode( ' ', $trim )[1];
          } elseif( \strpos( $trim, 'have ' ) === 0 ) {
            $haves[] = \explode( ' ', $trim )[1];
          } elseif( $trim === 'done' ) {
            break;
          }
        }
      }

      \fclose( $handle );
    }

    if( !empty( $wants ) ) {
      $this->packetWrite( "NAK\n" );

      $objects = $this->git->collectObjects( $wants, $haves );
      $lastHb  = \time();

      foreach( $this->git->generatePackfile( $objects ) as $chunk ) {
        if( $chunk !== '' ) {
          $this->sendSidebandData( 1, $chunk );
        }

        $now = \time();

        if( $now - $lastHb >= 5 ) {
          $this->sendSidebandData( 2, "\r" );
          $lastHb = $now;
        }
      }
    }

    $this->packetFlush();
  }

  private function sendSidebandData( int $band, string $data ): void {
    $chunkSize = 65515;
    $len       = \strlen( $data );

    for( $offset = 0; $offset < $len; $offset += $chunkSize ) {
      $this->packetWrite(
        \chr( $band ) . \substr( $data, $offset, $chunkSize )
      );
    }
  }

  private function serve( string $path, string $contentType ): void {
    \header( 'Content-Type: ' . $contentType );

    if( !$this->git->streamRaw( $path ) ) {
      \http_response_code( 404 );
      echo "Missing: $path";
    }
  }

  private function packetWrite( string $data ): void {
    \printf( "%04x%s", \strlen( $data ) + 4, $data );
  }

  private function packetFlush(): void {
    echo "0000";
  }
}