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

    exit;
  }

  private function redirectBrowser(): void {
    $url = str_replace( '.git', '', $_SERVER['REQUEST_URI'] );

    header( "Location: $url" );
  }

  private function renderInfoRefs(): void {
    $service = $_GET['service'] ?? '';

    if( $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 {
    header( 'Content-Type: application/x-git-upload-pack-result' );
    header( 'Cache-Control: no-cache' );

    $input = file_get_contents( 'php://input' );
    $wants = [];
    $haves = [];
    $offset = 0;

    while( $offset < strlen( $input ) ) {
      $line = $this->readPacketLine( $input, $offset );

      if( $line === null || $line === 'done' ) {
        break;
      }

      if( $line === '' ) {
        continue;
      }

      $trim = trim( $line );

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

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

      $objects = $this->git->collectObjects( $wants, $haves );
      $pack    = $this->git->generatePackfile( $objects );

      $this->sendSidebandData( 1, $pack );
    }

    $this->packetFlush();
  }

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

    for( $offset = 0; $offset < $len; $offset += $chunkSize ) {
      $chunk = substr( $data, $offset, $chunkSize );

      $this->packetWrite( chr( $band ) . $chunk );
    }
  }

  private function readPacketLine( string $input, int &$offset ): ?string {
    $line = null;

    if( $offset + 4 <= strlen( $input ) ) {
      $lenHex = substr( $input, $offset, 4 );

      if( ctype_xdigit( $lenHex ) ) {
        $len = hexdec( $lenHex );
        $offset += 4;
        $valid = $len >= 4 && $offset + ( $len - 4 ) <= strlen( $input );

        $line = ($len === 0)
          ? ''
          : ($valid ? substr( $input, $offset, $len - 4 ) : null);

        $offset += ($len >= 4) ? ($len - 4) : 0;
      }
    }

    return $line;
  }

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